# @isa/catalogue/data-access

A comprehensive product catalogue search service for Angular applications, providing catalog item search and loyalty program integration.

## Overview

The Catalogue Data Access library provides a unified interface for searching and retrieving product catalog data and managing loyalty reward items. It integrates with the generated cat-search-api client to provide intelligent search routing, validation, and transformation of catalog data.

**For availability checking:** Use `@isa/availability/data-access` which provides comprehensive availability validation across all order types (Download, DIG-Versand, B2B-Versand, etc.).

## Table of Contents

- [Features](#features)
- [Quick Start](#quick-start)
- [Core Concepts](#core-concepts)
- [API Reference](#api-reference)
- [Usage Examples](#usage-examples)
- [Search Types](#search-types)
- [Validation and Business Rules](#validation-and-business-rules)
- [Error Handling](#error-handling)
- [Testing](#testing)
- [Architecture Notes](#architecture-notes)

## Features

- **Multi-type search support** - EAN, term-based, and loyalty item search
- **Loyalty program integration** - Specialized filtering and settings for reward items
- **Zod validation** - Runtime schema validation for all parameters
- **Request cancellation** - AbortSignal support for all operations
- **Pagination support** - Skip/take parameters for search results
- **Filtering and sorting** - Advanced query token system with filters and orderBy
- **Type-safe transformations** - Extends generated DTOs with domain types
- **Comprehensive logging** - Integration with @isa/core/logging for debugging
- **Error resilience** - Graceful error handling with fallback responses

## Quick Start

### 1. Import and Inject Service

```typescript
import { Component, inject } from '@angular/core';
import { CatalougeSearchService } from '@isa/catalogue/data-access';

@Component({
  selector: 'app-product-search',
  template: '...'
})
export class ProductSearchComponent {
  #catalogueSearch = inject(CatalougeSearchService);
}
```

**Note:** For availability checking, import `AvailabilityService` from `@isa/availability/data-access` instead.

### 2. Search Products by EAN

```typescript
async searchByEan(): Promise<void> {
  const items = await this.#catalogueSearch.searchByEans([
    '1234567890123',
    '9876543210987'
  ]);

  // Result: Item[] with product details and availability
  items.forEach(item => {
    console.log(`${item.product.name}: ${item.catalogAvailability.price.value.value}€`);
  });
}
```

### 3. Search Products by Term

```typescript
async searchProducts(): Promise<void> {
  const abortController = new AbortController();

  const result = await this.#catalogueSearch.searchByTerm(
    {
      searchTerm: 'laptop',
      skip: 0,
      take: 20
    },
    abortController.signal
  );

  console.log(`Found ${result.total} items`);
  console.log(`Showing ${result.result.length} items`);
}
```

### 4. Search Loyalty Items

```typescript
async searchLoyaltyItems(): Promise<void> {
  const abortController = new AbortController();

  const result = await this.#catalogueSearch.searchLoyaltyItems(
    {
      filter: { category: 'electronics' },
      input: { search: 'tablet' },
      skip: 0,
      take: 25
    },
    abortController.signal
  );

  // Result includes only loyalty items (praemie = 1-)
  // Automatically excludes ebooks and downloads (format: !eb;!dl)
}
```


## Core Concepts

### Item Structure

The core `Item` interface extends the generated API DTO with domain-specific properties:

```typescript
interface Item {
  id: number;                           // Unique item identifier
  product: Product;                     // Product details
  catalogAvailability: Availability;    // Catalog availability data
  redemptionPoints?: number;            // Loyalty points (for reward items)
  // ... other fields from ItemDTO
}
```

### Product Information

Product details include essential product metadata:

```typescript
interface Product {
  name: string;                         // Product name
  contributors: string;                 // Author/artist/creator
  catalogProductNumber: string;         // Internal catalog number
  ean: string;                          // European Article Number (barcode)
  format: string;                       // Product format (e.g., 'CD', 'DVD', 'Book')
  formatDetail: string;                 // Detailed format information
  volume: string;                       // Size/volume description
  manufacturer: string;                 // Manufacturer/publisher
  // ... other fields from ProductDTO
}
```

### Availability Information

Catalog availability includes pricing and stock data:

```typescript
interface Availability {
  price: Price;                         // Current price with VAT
  // ... other fields from AvailabilityDTO
}

interface Price {
  value: PriceValue;                    // Price amount and currency
  // ... other fields from PriceDTO
}

interface PriceValue {
  value: number;                        // Numeric price value
  // ... other fields from PriceValueDTO
}
```

### Query Settings

Loyalty query settings configure search UI:

```typescript
type QuerySettings = UISettingsDTO;    // Settings from API

// Contains:
// - filters: Available filter options
// - orderByOptions: Sort options
// - other UI configuration
```

### Search Validation with Zod

All search parameters are validated using Zod schemas:

```typescript
// Term search validation
const params = {
  searchTerm: 'laptop',
  skip: '0',          // Coerced to number
  take: '20'          // Coerced to number
};

// Validation happens automatically
const result = await service.searchByTerm(params, abortSignal);
// Throws ZodError if validation fails
```

## API Reference

### CatalougeSearchService

Main service for searching catalog items and loyalty rewards.

#### `searchByEans(ean, abortSignal?): Promise<Item[]>`

Searches for items by their European Article Numbers (EANs/barcodes).

**Parameters:**
- `ean: string[]` - Array of EAN codes to search for
- `abortSignal?: AbortSignal` - Optional abort signal for request cancellation

**Returns:** Promise resolving to array of matching items

**Error Handling:** Returns empty array on error (does not throw)

**Example:**
```typescript
const items = await service.searchByEans([
  '1234567890123',
  '9876543210987'
]);

// Result: Item[] (empty if not found or error)
items.forEach(item => {
  console.log(`${item.product.name} - ${item.product.ean}`);
});
```

#### `searchByTerm(params, abortSignal): Promise<ListResponseArgs<Item>>`

Searches catalog items by search term with pagination support.

**Parameters:**
- `params: SearchByTermInput` - Search parameters (automatically validated)
  - `searchTerm: string` - Search query (min 1 character)
  - `skip?: number` - Number of items to skip (default: 0)
  - `take?: number` - Number of items to return (default: 20, max: 100)
- `abortSignal: AbortSignal` - Required abort signal for request cancellation

**Returns:** Promise resolving to paginated search results

**Throws:**
- `ZodError` - If params validation fails
- `Error` - If API returns an error

**Example:**
```typescript
const abortController = new AbortController();

const result = await service.searchByTerm(
  {
    searchTerm: 'mystery novel',
    skip: 0,
    take: 50
  },
  abortController.signal
);

console.log(`Found ${result.total} items`);
console.log(`Page: ${result.result.length} items`);
```

#### `fetchLoyaltyQuerySettings(): Promise<QuerySettings>`

Fetches UI settings for loyalty item search configuration.

**Returns:** Promise resolving to query settings (undefined on error)

**Error Handling:** Returns undefined on error (does not throw)

**Example:**
```typescript
const settings = await service.fetchLoyaltyQuerySettings();

if (settings) {
  console.log('Available filters:', settings.filters);
  console.log('Sort options:', settings.orderByOptions);
}
```

#### `searchLoyaltyItems(params, abortSignal): Promise<ListResponseArgs<Item>>`

Searches loyalty reward items with automatic loyalty filtering applied.

**Parameters:**
- `params: QueryTokenInput` - Query parameters (automatically validated)
  - `filter?: Record<string, any>` - Custom filters (default: {})
  - `input?: Record<string, string>` - Additional input parameters
  - `orderBy?: OrderBy[]` - Sort options
  - `skip?: number` - Pagination offset (default: 0)
  - `take?: number` - Page size (default: 25)
- `abortSignal: AbortSignal` - Required abort signal for request cancellation

**Returns:** Promise resolving to paginated loyalty items (undefined on error)

**Automatic Filters Applied:**
- `format: '!eb;!dl'` - Excludes ebooks and downloads (can be overridden)
- `praemie: '1-'` - Only loyalty reward items

**Throws:** Returns undefined on error (does not throw)

**Example:**
```typescript
const abortController = new AbortController();

const result = await service.searchLoyaltyItems(
  {
    filter: { category: 'electronics' },
    input: { brand: 'samsung' },
    orderBy: [{ by: 'price', label: 'Price', desc: false, selected: true }],
    skip: 0,
    take: 20
  },
  abortController.signal
);

if (result) {
  console.log(`Loyalty items: ${result.total}`);
  result.result.forEach(item => {
    console.log(`${item.product.name}: ${item.redemptionPoints} points`);
  });
}
```

### Schema Types

#### SearchByTerm

```typescript
interface SearchByTermInput {
  searchTerm: string;                   // Min 1 character
  skip?: number;                        // Default: 0
  take?: number;                        // Default: 20, max: 100
}
```

#### QueryToken

```typescript
interface QueryTokenInput {
  filter?: Record<string, any>;         // Filter criteria
  input?: Record<string, string>;       // Additional input
  orderBy?: OrderBy[];                  // Sort options
  skip?: number;                        // Default: 0
  take?: number;                        // Default: 25
}

interface OrderBy {
  by: string;                           // Field name to sort by
  label: string;                        // Display label
  desc: boolean;                        // Descending sort
  selected: boolean;                    // Currently selected
}
```

## Usage Examples

### Basic EAN Search

```typescript
import { Component, inject } from '@angular/core';
import { CatalougeSearchService } from '@isa/catalogue/data-access';

@Component({
  selector: 'app-barcode-scanner',
  template: '...'
})
export class BarcodeScannerComponent {
  #catalogueSearch = inject(CatalougeSearchService);

  async scanBarcode(ean: string): Promise<void> {
    const items = await this.#catalogueSearch.searchByEans([ean]);

    if (items.length === 0) {
      console.log('Item not found in catalog');
      return;
    }

    const item = items[0];
    console.log(`Found: ${item.product.name}`);
    console.log(`Price: ${item.catalogAvailability.price.value.value}€`);
    console.log(`Manufacturer: ${item.product.manufacturer}`);
  }
}
```

### Paginated Search with Cancellation

```typescript
async searchWithPagination(): Promise<void> {
  const abortController = new AbortController();

  // Cancel after 10 seconds
  setTimeout(() => abortController.abort(), 10000);

  try {
    const result = await this.#catalogueSearch.searchByTerm(
      {
        searchTerm: 'science fiction',
        skip: 20,    // Second page (20 items per page)
        take: 20
      },
      abortController.signal
    );

    console.log(`Total results: ${result.total}`);
    console.log(`Current page: ${result.result.length} items`);
    console.log(`Has more: ${result.total > 40}`);

    result.result.forEach((item, index) => {
      const position = 20 + index + 1;
      console.log(`${position}. ${item.product.name} by ${item.product.contributors}`);
    });
  } catch (error) {
    console.error('Search cancelled or failed', error);
  }
}
```

### Advanced Loyalty Search with Filtering

```typescript
async searchLoyaltyWithFilters(): Promise<void> {
  const abortController = new AbortController();

  // Fetch settings first to understand available filters
  const settings = await this.#catalogueSearch.fetchLoyaltyQuerySettings();

  if (!settings) {
    console.error('Failed to load loyalty settings');
    return;
  }

  // Search with custom filters
  const result = await this.#catalogueSearch.searchLoyaltyItems(
    {
      filter: {
        category: 'books',
        format: 'hardcover',           // Overrides default '!eb;!dl'
        priceRange: '10-50'
      },
      input: {
        author: 'tolkien',
        language: 'en'
      },
      orderBy: [
        { by: 'redemptionPoints', label: 'Points', desc: false, selected: true }
      ],
      skip: 0,
      take: 30
    },
    abortController.signal
  );

  if (!result) {
    console.error('Search failed');
    return;
  }

  console.log(`Found ${result.total} loyalty items`);

  result.result.forEach(item => {
    console.log(
      `${item.product.name}: ${item.redemptionPoints} points ` +
      `(€${item.catalogAvailability.price.value.value})`
    );
  });
}
```

### Download Availability Validation

```typescript
import { AvailabilityService, ShoppingCartItem } from '@isa/catalogue/data-access';

@Component({
  selector: 'app-download-cart',
  template: '...'
})
export class DownloadCartComponent {
  #availabilityService = inject(AvailabilityService);

  async validateCart(cartItems: ShoppingCartItem[]): Promise<void> {
    const validations = await this.#availabilityService.validateDownloadAvailabilities(
      cartItems
    );

    const unavailableItems: number[] = [];
    const updates = new Map<number, any>();

    validations.forEach(validation => {
      if (!validation.isAvailable) {
        unavailableItems.push(validation.itemId);
      } else if (validation.availabilityUpdate) {
        updates.set(validation.itemId, validation.availabilityUpdate);
      }
    });

    if (unavailableItems.length > 0) {
      console.log(`Removing ${unavailableItems.length} unavailable items`);
      // Remove unavailable items from cart
      this.removeItems(unavailableItems);
    }

    if (updates.size > 0) {
      console.log(`Updating availability for ${updates.size} items`);
      // Update cart items with fresh availability data
      this.updateAvailabilities(updates);
    }
  }

  private removeItems(itemIds: number[]): void {
    // Implementation to remove items
  }

  private updateAvailabilities(updates: Map<number, any>): void {
    // Implementation to update availability data
  }
}
```

### DIG and B2B Availability

```typescript
async getDeliveryAvailability(
  item: ShoppingCartItem,
  orderType: 'DIG-Versand' | 'B2B-Versand'
): Promise<void> {
  let availability: any;

  if (orderType === 'DIG-Versand') {
    availability = await this.#availabilityService.getDigDeliveryAvailability(item);
  } else {
    // B2B requires branch and logistician
    const branch = await this.getBranch();  // Get from branch service
    const logistician = await this.getLogistician(2470);  // Standard B2B logistician

    availability = await this.#availabilityService.getB2bDeliveryAvailability(
      item,
      branch,
      logistician
    );
  }

  if (!availability) {
    console.log('Item not available for delivery');
    return;
  }

  console.log(`Availability: ${availability.sscText}`);
  console.log(`Delivery date: ${availability.estimatedShippingDate}`);
  console.log(`Price: ${availability.price}`);

  if (orderType === 'B2B-Versand') {
    console.log(`In stock: ${availability.inStock}`);
    console.log(`Order deadline: ${availability.orderDeadline}`);
  }
}
```

### Error-Resilient Search

```typescript
async searchWithErrorHandling(searchTerm: string): Promise<void> {
  const abortController = new AbortController();

  try {
    const result = await this.#catalogueSearch.searchByTerm(
      {
        searchTerm,
        skip: 0,
        take: 20
      },
      abortController.signal
    );

    if (result.error) {
      console.error('Search returned error:', result.message);
      return;
    }

    this.displayResults(result.result);
  } catch (error) {
    if (error instanceof ZodError) {
      console.error('Invalid search parameters:', error.errors);
      this.showValidationError(error);
    } else {
      console.error('Search failed:', error);
      this.showGenericError();
    }
  }
}
```

## Search Types

### EAN Search

Searches by European Article Number (barcode).

**Characteristics:**
- No pagination (returns all matches)
- Returns empty array on error
- Can search multiple EANs simultaneously
- Fast and exact matching

**Use Cases:**
- Barcode scanning
- Direct product lookup
- Batch item retrieval

### Term Search

Full-text search across catalog.

**Characteristics:**
- Pagination support (skip/take)
- Throws errors on API failure
- Requires AbortSignal
- Tracks search analytics (doNotTrack: true)

**Use Cases:**
- Product search interface
- Auto-complete suggestions
- Browse functionality

### Loyalty Search

Specialized search for loyalty reward items.

**Characteristics:**
- Automatic loyalty filters applied
- Supports advanced filtering and sorting
- Custom filter merging
- Returns undefined on error

**Automatic Filters:**
- `format: '!eb;!dl'` - Excludes ebooks and downloads
- `praemie: '1-'` - Only loyalty items

**Use Cases:**
- Reward catalog browsing
- Points redemption interface
- Loyalty program administration

## Availability Validation

### Download Validation

Downloads have strict availability requirements:

**Valid Status Codes:**
```typescript
const VALID_DOWNLOAD_CODES = [
  2,      // PrebookAtBuyer
  32,     // PrebookAtRetailer
  256,    // PrebookAtSupplier
  1024,   // Available
  2048,   // OnDemand
  4096    // AtProductionDate
];
```

**Special Rules:**
- Supplier 16 with 0 stock = unavailable
- Must have one of the valid status codes
- Quantity always forced to 1
- Only validates items without `lastRequest`

### DIG Delivery

Standard digital shipping availability.

**Characteristics:**
- Uses shipping availability endpoint
- Returns preferred availability
- Includes supplier and logistician data
- Estimated delivery date calculation

**Date Selection:**
```typescript
// If requestStatusCode === '32', use altAt
// Otherwise use at
estimatedShippingDate: requestStatusCode === '32' ? altAt : at
```

### B2B Delivery

Business-to-business shipping with branch context.

**Characteristics:**
- Uses store availability endpoint
- Requires branch and logistician context
- Calculates total stock across all availabilities
- Includes order deadline

**Stock Calculation:**
```typescript
// Sum quantities from all availabilities
inStock = availabilities.reduce((sum, av) => sum + (av?.qty || 0), 0)
```

## Validation and Business Rules

### Zod Schema Validation

All parameters are validated using Zod schemas before processing:

**Type Coercion:**
```typescript
// Number coercion
{ skip: '10' }  →  { skip: 10 }
{ take: '20' }  →  { take: 20 }

// Validation constraints
searchTerm: z.string().min(1)                    // Required, non-empty
skip: z.number().int().min(0).default(0)         // Non-negative integer
take: z.number().int().min(1).max(100).default(20)  // Between 1 and 100
```

**Default Values:**
```typescript
// SearchByTermSchema
skip: 0
take: 20

// QueryTokenSchema
filter: {}
skip: 0
take: 25
orderBy: []
```

### Loyalty Filter Rules

Loyalty searches apply automatic filters:

```typescript
// Base loyalty filter
{
  format: '!eb;!dl',    // Exclude ebooks and downloads
  praemie: '1-'         // Only loyalty items
}

// Custom filters are merged (custom overrides base)
customFilter = { category: 'books', format: 'hardcover' }
→ finalFilter = { format: 'hardcover', praemie: '1-', category: 'books' }
```

### Download Availability Rules

Downloads are validated against specific business rules:

1. **Status Code Validation**
   ```typescript
   // Only these codes are valid for downloads
   [2, 32, 256, 1024, 2048, 4096].includes(availability.status)
   ```

2. **Supplier 16 Stock Rule**
   ```typescript
   // Supplier 16 with 0 stock = unavailable
   if (availability.supplierId === 16 && availability.inStock === 0) {
     return false;
   }
   ```

3. **Last Request Check**
   ```typescript
   // Skip items already validated
   if (item.data?.availability?.lastRequest) {
     return;  // Skip validation
   }
   ```

## Error Handling

### Error Types

#### ZodError

Thrown when input parameters fail validation:

```typescript
import { ZodError } from 'zod';

try {
  await service.searchByTerm(
    {
      searchTerm: '',      // Empty string fails min(1)
      skip: -1,            // Negative fails min(0)
      take: 150            // Exceeds max(100)
    },
    abortSignal
  );
} catch (error) {
  if (error instanceof ZodError) {
    console.error('Validation errors:', error.errors);
    // error.errors contains detailed validation failures
    error.errors.forEach(err => {
      console.log(`${err.path}: ${err.message}`);
    });
  }
}
```

#### Error (Generic)

Thrown by searchByTerm when API returns an error:

```typescript
try {
  const result = await service.searchByTerm(params, abortSignal);
} catch (error) {
  if (error instanceof Error) {
    console.error('Search failed:', error.message);
    // Display user-friendly error message
  }
}
```

### Error Recovery Patterns

#### searchByEans - Graceful Degradation

```typescript
const items = await service.searchByEans(['invalid-ean']);
// Returns: []
// Does not throw - safe to use without try/catch
```

#### fetchLoyaltyQuerySettings - Undefined Return

```typescript
const settings = await service.fetchLoyaltyQuerySettings();
// Returns: undefined on error
// Always check for undefined:
if (settings) {
  // Use settings
} else {
  // Use default UI configuration
}
```

#### searchLoyaltyItems - Undefined Return

```typescript
const result = await service.searchLoyaltyItems(params, abortSignal);
// Returns: undefined on error
// Always check:
if (result) {
  // Display results
} else {
  // Show error message
}
```

### Request Cancellation

All methods support AbortSignal for cancellation:

```typescript
const controller = new AbortController();

// Set timeout
const timeoutId = setTimeout(() => {
  controller.abort();
  console.log('Search cancelled due to timeout');
}, 5000);

try {
  const result = await service.searchByTerm(
    params,
    controller.signal
  );
  clearTimeout(timeoutId);
  // Process result
} catch (error) {
  clearTimeout(timeoutId);
  // Handle cancellation or other errors
}
```

### Logging Context

The service automatically logs errors with context:

```typescript
// Logged automatically for searchByEans
{
  service: 'CatalougeSearchService',
  method: 'searchByEans',
  eanCount: 3
}

// Logged for availability validation
{
  service: 'AvailabilityService',
  method: 'getDigDeliveryAvailability',
  itemId: 123
}
```

## Testing

The library uses **Jest** with **Angular Testing Utilities** for testing.

### Running Tests

```bash
# Run tests for this library
npx nx test catalogue-data-access --skip-nx-cache

# Run tests with coverage
npx nx test catalogue-data-access --code-coverage --skip-nx-cache

# Run tests in watch mode
npx nx test catalogue-data-access --watch
```

### Test Structure

The library includes comprehensive unit tests covering:

**CatalougeSearchService Tests:**
- **EAN search** - Single/multiple EANs, empty results, error handling
- **Term search** - Pagination, validation, abort signal, default values
- **Loyalty settings** - Success/error scenarios, HTTP errors
- **Loyalty search** - Filtering, sorting, filter merging, error handling

**AvailabilityService Tests:**
- **Download validation** - Status codes, supplier rules, abort signal
- **DIG delivery** - Success/error scenarios, preferred selection
- **B2B delivery** - Stock calculation, branch context, logistician override

### Example Test (Jest with Angular Testing Utilities)

```typescript
import { TestBed } from '@angular/core/testing';
import { CatalougeSearchService } from './catalouge-search.service';
import { SearchService } from '@generated/swagger/cat-search-api';
import { of } from 'rxjs';

describe('CatalougeSearchService', () => {
  let service: CatalougeSearchService;
  let searchServiceSpy: jest.Mocked<SearchService>;

  beforeEach(() => {
    const searchServiceMock = {
      SearchByEAN: jest.fn(),
      SearchSearch: jest.fn(),
      SearchLoyaltySettings: jest.fn(),
    };

    TestBed.configureTestingModule({
      providers: [
        CatalougeSearchService,
        { provide: SearchService, useValue: searchServiceMock },
      ],
    });

    service = TestBed.inject(CatalougeSearchService);
    searchServiceSpy = TestBed.inject(SearchService) as jest.Mocked<SearchService>;
  });

  it('should return items when search is successful', async () => {
    // Arrange
    const mockItems = [
      { id: 1, product: { name: 'Item 1' } },
      { id: 2, product: { name: 'Item 2' } }
    ];
    const mockResponse = { error: false, result: mockItems };
    searchServiceSpy.SearchByEAN.mockReturnValue(of(mockResponse));

    // Act
    const result = await service.searchByEans(['123', '456']);

    // Assert
    expect(result).toEqual(mockItems);
    expect(searchServiceSpy.SearchByEAN).toHaveBeenCalledWith(['123', '456']);
  });
});
```

### Example Test (Spectator - Legacy Pattern)

```typescript
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
import { AvailabilityService } from './availability.service';
import { AvailabilityService as GeneratedAvailabilityService } from '@generated/swagger/availability-api';
import { of } from 'rxjs';

describe('AvailabilityService', () => {
  let spectator: SpectatorService<AvailabilityService>;
  const createService = createServiceFactory({
    service: AvailabilityService,
    mocks: [GeneratedAvailabilityService],
  });

  beforeEach(() => {
    spectator = createService();
  });

  it('should validate download availability', async () => {
    // Arrange
    const items = [{
      id: 1,
      data: {
        features: { orderType: 'Download' },
        product: { ean: '123456' }
      }
    }];
    const mockResponse = {
      error: false,
      result: [{ preferred: 1, status: 2 }]  // Valid status
    };
    const availabilityService = spectator.inject(GeneratedAvailabilityService);
    availabilityService.AvailabilityShippingAvailability.mockReturnValue(of(mockResponse));

    // Act
    const result = await spectator.service.validateDownloadAvailabilities(items);

    // Assert
    expect(result).toHaveLength(1);
    expect(result[0].isAvailable).toBe(true);
  });
});
```

## Architecture Notes

### Current Architecture

The library follows a layered architecture:

```
Components/Features
       ↓
CatalougeSearchService (search operations)
       ↓
├─→ Generated SearchService (cat-search-api)
└─→ Schema validation (Zod)

Components/Features
       ↓
AvailabilityService (availability validation)
       ↓
├─→ Generated AvailabilityService (availability-api)
└─→ Download validation logic
```

### Domain Model

The library extends generated API DTOs with domain types:

```typescript
// Generated DTO (from cat-search-api)
interface ItemDTO {
  // Generated fields from API
}

// Domain Model (this library)
interface Item extends ItemDTO {
  id: number;
  product: Product;
  catalogAvailability: Availability;
  redemptionPoints?: number;
}
```

**Benefits:**
- Type safety across application layers
- Clear separation of API and domain concerns
- Flexibility to add domain-specific fields
- Easier to mock in tests

### Service Responsibilities

#### CatalougeSearchService

**Responsibilities:**
- Execute catalog search operations
- Validate input parameters with Zod
- Transform API responses to domain models
- Handle errors gracefully
- Log operations for debugging

**Not Responsible For:**
- State management (use NgRx or signals in features)
- UI presentation logic
- Business workflows (handled by facades/features)

#### AvailabilityService

**Responsibilities:**
- Validate download item availability
- Fetch DIG and B2B delivery availability
- Apply download-specific business rules
- Transform availability responses

**Not Responsible For:**
- Full availability checking (see @isa/availability/data-access)
- Order type routing
- Branch/logistician management

### Known Architectural Considerations

#### 1. Shared Availability Concerns

The `AvailabilityService` in this library duplicates some functionality from `@isa/availability/data-access`:

**Current State:**
- Both libraries have availability validation logic
- Download validation is specific to catalogue domain
- DIG/B2B methods are specialized for cart items

**Recommendation:**
- Consider consolidating availability logic in `@isa/availability/data-access`
- Keep catalogue-specific validation in this library
- Use composition to delegate to availability service

**Impact:** Would reduce code duplication and improve maintainability

#### 2. Error Handling Inconsistency

Different methods use different error handling strategies:

**Current State:**
- `searchByEans`: Returns empty array on error
- `searchByTerm`: Throws error
- `searchLoyaltyItems`: Returns undefined on error

**Recommendation:**
- Standardize error handling approach
- Consider using Result<T, E> pattern
- Document error behavior clearly in JSDoc

**Impact:** Would improve API consistency and developer experience

#### 3. Shopping Cart Item Interface

The `ShoppingCartItem` interface is defined in this library but represents shopping cart domain:

**Current State:**
- Interface duplicated across multiple libraries
- Couples catalogue to shopping cart structure

**Proposed Solution:**
- Move `ShoppingCartItem` to `@isa/checkout/data-access`
- Use minimal interface in catalogue library
- Depend on checkout types where needed

**Impact:** Improves domain boundaries and reduces duplication

### Performance Considerations

1. **Batch EAN Search** - Single API call for multiple EANs (efficient)
2. **Early Validation** - Zod validation fails fast before API calls
3. **Pagination Support** - Prevents loading excessive data
4. **Request Cancellation** - AbortSignal prevents wasted bandwidth
5. **Error Resilience** - Graceful degradation for non-critical failures

### Future Enhancements

Potential improvements identified:

1. **Caching Layer** - Cache search results for repeated queries
2. **Search Debouncing** - Built-in debounce for term searches
3. **Retry Logic** - Automatic retry for transient failures
4. **Analytics Integration** - Track search patterns and performance
5. **Type Narrowing** - Use discriminated unions for different search types
6. **Batch Availability** - Validate multiple items in single request

## Dependencies

### Required Libraries

- `@angular/core` - Angular framework
- `@generated/swagger/cat-search-api` - Generated catalogue search API client
- `@generated/swagger/availability-api` - Generated availability API client
- `@isa/common/data-access` - Common data access utilities, error handling
- `@isa/core/logging` - Logging service
- `zod` - Schema validation
- `rxjs` - Reactive programming

### Development Dependencies

- `@angular/core/testing` - Angular testing utilities
- `jest` - Test framework
- `@ngneat/spectator` - Testing utilities (legacy, being phased out)

### Path Alias

Import from: `@isa/catalogue/data-access`

## Best Practices

### When to Use This Library

**Use for:**
- Product catalog search functionality
- Loyalty reward item browsing
- Download availability validation
- DIG/B2B delivery availability checks

**Don't Use for:**
- Full availability checking across all order types (use `@isa/availability/data-access`)
- Shopping cart management (use `@isa/checkout/data-access`)
- Product details display (use feature libraries)

### Service Injection Pattern

```typescript
// Modern pattern with inject()
#catalogueSearch = inject(CatalougeSearchService);
#availabilityService = inject(AvailabilityService);

// Not: constructor injection (unless required for legacy compatibility)
```

### Error Handling Best Practices

```typescript
// Always handle searchByTerm errors (throws)
try {
  const result = await service.searchByTerm(params, signal);
  // Process result
} catch (error) {
  // Handle error
}

// Check for undefined with searchLoyaltyItems
const result = await service.searchLoyaltyItems(params, signal);
if (result) {
  // Process result
} else {
  // Handle error state
}

// searchByEans is safe without try/catch
const items = await service.searchByEans(['123']);
if (items.length === 0) {
  // Handle not found
}
```

### AbortSignal Usage

```typescript
// Create controller at component level
#abortController?: AbortController;

// Reset on new search
startSearch(): void {
  this.#abortController?.abort();
  this.#abortController = new AbortController();

  this.search(this.#abortController.signal);
}

// Clean up on destroy
ngOnDestroy(): void {
  this.#abortController?.abort();
}
```

### Validation Pattern

```typescript
// Let Zod handle validation - don't pre-validate
// Good:
const result = await service.searchByTerm(
  { searchTerm: userInput, skip: 0, take: 20 },
  signal
);

// Bad - unnecessary pre-validation:
if (userInput && userInput.length > 0) {
  const result = await service.searchByTerm(...);
}
```

## License

Internal ISA Frontend library - not for external distribution.
