Files
ISA-Frontend/libs/catalogue/data-access/README.md
Lorenz Hilpert bfd151dd84 Merged PR 1989: fix(checkout): resolve currency constraint violations in price handling
fix(checkout): resolve currency constraint violations in price handling

- Add ensureCurrencyDefaults() helper to normalize price objects with EUR defaults
- Fix currency constraint violation in shopping cart item additions (bug #5405)
- Apply price normalization across availability, checkout, and shopping cart services
- Update 8 locations: availability.adapter, checkout.service, shopping-cart.service,
  get-availability-params.adapter, availability-transformers, reward quantity control
- Refactor OrderType to @isa/common/data-access for cross-domain reusability
- Remove duplicate availability service from catalogue library
- Enhance PriceValue and VatValue schemas with proper currency defaults
- Add availability-transformers.spec.ts test coverage
- Fix QuantityControl fallback from 0 to 1 to prevent invalid state warnings

Resolves issue where POST requests to /checkout/v6/store/shoppingcart/{id}/item
were sending price objects without required currency/currencySymbol fields,
causing 400 Bad Request with 'Currency: Constraint violation: NotNull' error.

Related work items: #5405
2025-10-28 10:34:39 +00:00

33 KiB

@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

  • 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

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

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

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

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.