# @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 { 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 { 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 { 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 { 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` 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>` 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` 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>` Searches loyalty reward items with automatic loyalty filtering applied. **Parameters:** - `params: QueryTokenInput` - Query parameters (automatically validated) - `filter?: Record` - Custom filters (default: {}) - `input?: Record` - 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` 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` 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` 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; // Filter criteria input?: Record; // 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 { 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 { 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 { 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 { const validations = await this.#availabilityService.validateDownloadAvailabilities( cartItems ); const unavailableItems: number[] = []; const updates = new Map(); 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): void { // Implementation to update availability data } } ``` ### DIG and B2B Availability ```typescript async getDeliveryAvailability( item: ShoppingCartItem, orderType: 'DIG-Versand' | 'B2B-Versand' ): Promise { 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 { 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; 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; }); 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; 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 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.