# @isa/catalogue/data-access

A comprehensive product catalogue search and availability service for Angular applications, providing catalog item search, loyalty program integration, and specialized availability validation for download and delivery order types.

## Overview

The Catalogue Data Access library provides a unified interface for searching and retrieving product catalog data, managing loyalty reward items, and validating product availability for digital downloads, DIG shipping, and B2B delivery. It integrates with the generated cat-search-api and availability-api clients to provide intelligent search routing, validation, and transformation of catalog and availability data.

## 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)
- [Availability Validation](#availability-validation)
- [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
- **Availability validation** - Download, DIG delivery, and B2B delivery validation
- **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 Services

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

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

### 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)
}
```

### 5. Validate Download Availability

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

  validations.forEach(validation => {
    if (!validation.isAvailable) {
      console.log(`Item ${validation.itemId} is not available for download`);
    } else {
      console.log(`Item ${validation.itemId} is available`);
      // validation.availabilityUpdate contains the updated availability data
    }
  });
}
```

## 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`);
  });
}
```

### AvailabilityService

Service for validating product availability for download and delivery order types.

**Note:** This service is focused on availability validation for the catalogue domain. For full availability checking across all order types, use `@isa/availability/data-access`.

#### `validateDownloadAvailabilities(items, abortSignal?): Promise<DownloadAvailabilityValidation[]>`

Validates download item availabilities and returns validation results.

**Parameters:**
- `items: ShoppingCartItem[]` - Shopping cart items to validate
- `abortSignal?: AbortSignal` - Optional abort signal for request cancellation

**Returns:** Promise resolving to array of validation results

**Business Rules:**
- Only processes items with `orderType === 'Download'`
- Skips items that already have `lastRequest` (already validated)
- Validates against download-specific status codes: 2, 32, 256, 1024, 2048, 4096
- Rejects supplier 16 with 0 stock

**Example:**
```typescript
const cartItems: ShoppingCartItem[] = [
  {
    id: 1,
    data: {
      features: { orderType: 'Download' },
      product: { ean: '123456', catalogProductNumber: 789 },
      availability: { price: 9.99 }
    }
  }
];

const validations = await service.validateDownloadAvailabilities(cartItems);

validations.forEach(validation => {
  if (validation.isAvailable) {
    console.log(`Item ${validation.itemId} available for download`);
    // Update cart with validation.availabilityUpdate
  } else {
    console.log(`Item ${validation.itemId} not available`);
    // Remove from cart or mark as unavailable
  }
});
```

**Validation Result Structure:**
```typescript
interface DownloadAvailabilityValidation {
  itemId: number;
  isAvailable: boolean;
  availabilityUpdate?: {
    availabilityType: number;
    ssc: number;
    sscText: string;
    supplier: { id: number };
    isPrebooked: boolean;
    estimatedShippingDate: string;
    price: number;
    lastRequest: string;               // ISO timestamp
  };
}
```

#### `getDigDeliveryAvailability(item, abortSignal?): Promise<any>`

Gets DIG delivery availability for a shopping cart item.

**Parameters:**
- `item: ShoppingCartItem` - Shopping cart item
- `abortSignal?: AbortSignal` - Optional abort signal for request cancellation

**Returns:** Promise resolving to availability data or null if not available

**Example:**
```typescript
const item: ShoppingCartItem = {
  id: 1,
  data: {
    product: { ean: '123456', catalogProductNumber: 789 },
    quantity: 2,
    availability: { price: 15.99 }
  }
};

const availability = await service.getDigDeliveryAvailability(item);

if (availability) {
  console.log(`Available: ${availability.sscText}`);
  console.log(`Delivery: ${availability.estimatedShippingDate}`);
  console.log(`Supplier: ${availability.supplier.id}`);
  console.log(`Logistician: ${availability.logistician.id}`);
}
```

**Response Structure:**
```typescript
{
  availabilityType: number;
  ssc: number;
  sscText: string;
  supplier: { id: number };
  isPrebooked: boolean;
  estimatedShippingDate: string;
  estimatedDelivery: string;
  price: number;
  logistician: { id: number };
  supplierProductNumber: string;
  supplierInfo: string;
  lastRequest: string;
  priceMaintained: boolean;
}
```

#### `getB2bDeliveryAvailability(item, defaultBranch, logistician, abortSignal?): Promise<any>`

Gets B2B delivery availability for a shopping cart item with branch and logistician context.

**Parameters:**
- `item: ShoppingCartItem` - Shopping cart item
- `defaultBranch: Branch` - Default branch for stock lookup
- `logistician: Logistician` - Logistician data (typically ID 2470)
- `abortSignal?: AbortSignal` - Optional abort signal for request cancellation

**Returns:** Promise resolving to availability data or null if not available

**Special Handling:**
- Uses store availability endpoint (not shipping)
- Calculates total stock across all availabilities
- Always uses provided logistician ID in response

**Example:**
```typescript
const item: ShoppingCartItem = {
  id: 1,
  data: {
    product: { ean: '123456', catalogProductNumber: 789 },
    quantity: 10,
    availability: { price: 99.99 }
  }
};

const branch = { id: 42, name: 'Main Branch' };
const logistician = { id: 2470, name: 'Standard Logistician' };

const availability = await service.getB2bDeliveryAvailability(
  item,
  branch,
  logistician
);

if (availability) {
  console.log(`In stock: ${availability.inStock} units`);
  console.log(`Order deadline: ${availability.orderDeadline}`);
  console.log(`Logistician: ${availability.logistician.id}`);
}
```

**Response Structure:**
```typescript
{
  orderDeadline: string;
  availabilityType: number;
  ssc: number;
  sscText: string;
  supplier: { id: number };
  isPrebooked: boolean;
  estimatedShippingDate: string;
  price: number;
  inStock: number;                      // Total across all availabilities
  supplierProductNumber: string;
  supplierInfo: string;
  lastRequest: string;
  priceMaintained: boolean;
  logistician: { id: number };
}
```

### 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.
