Files
ISA-Frontend/libs/catalogue/data-access/README.md
Lorenz Hilpert 2b5da00249 feat(checkout): add reward order confirmation feature with schema migrations
- Add new reward-order-confirmation feature library with components and store
- Implement checkout completion orchestrator service for order finalization
- Migrate checkout/oms/crm models to Zod schemas for better type safety
- Add order creation facade and display order schemas
- Update shopping cart facade with order completion flow
- Add comprehensive tests for shopping cart facade
- Update routing to include order confirmation page
2025-10-21 14:28:52 +02:00

38 KiB

@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

  • 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

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

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

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

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

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:

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:

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:

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:

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:

// 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:

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:

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:

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:

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:

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:

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:

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:

{
  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:

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:

{
  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

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

QueryToken

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

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

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

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

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

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

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

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

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:

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:

// 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:

// 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:

// 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:

// SearchByTermSchema
skip: 0
take: 20

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

Loyalty Filter Rules

Loyalty searches apply automatic filters:

// 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

    // Only these codes are valid for downloads
    [2, 32, 256, 1024, 2048, 4096].includes(availability.status)
    
  2. Supplier 16 Stock Rule

    // Supplier 16 with 0 stock = unavailable
    if (availability.supplierId === 16 && availability.inStock === 0) {
      return false;
    }
    
  3. Last Request Check

    // Skip items already validated
    if (item.data?.availability?.lastRequest) {
      return;  // Skip validation
    }
    

Error Handling

Error Types

ZodError

Thrown when input parameters fail validation:

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:

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

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

fetchLoyaltyQuerySettings - Undefined Return

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

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:

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:

// 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

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

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)

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:

// 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

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

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

Error Handling Best Practices

// 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

// 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

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