# @isa/catalogue/data-access A comprehensive product catalogue search service for Angular applications, providing catalog item search and loyalty program integration. ## Overview The Catalogue Data Access library provides a unified interface for searching and retrieving product catalog data and managing loyalty reward items. It integrates with the generated cat-search-api client to provide intelligent search routing, validation, and transformation of catalog data. **For availability checking:** Use `@isa/availability/data-access` which provides comprehensive availability validation across all order types (Download, DIG-Versand, B2B-Versand, etc.). ## Table of Contents - [Features](#features) - [Quick Start](#quick-start) - [Core Concepts](#core-concepts) - [API Reference](#api-reference) - [Usage Examples](#usage-examples) - [Search Types](#search-types) - [Validation and Business Rules](#validation-and-business-rules) - [Error Handling](#error-handling) - [Testing](#testing) - [Architecture Notes](#architecture-notes) ## Features - **Multi-type search support** - EAN, term-based, and loyalty item search - **Loyalty program integration** - Specialized filtering and settings for reward items - **Zod validation** - Runtime schema validation for all parameters - **Request cancellation** - AbortSignal support for all operations - **Pagination support** - Skip/take parameters for search results - **Filtering and sorting** - Advanced query token system with filters and orderBy - **Type-safe transformations** - Extends generated DTOs with domain types - **Comprehensive logging** - Integration with @isa/core/logging for debugging - **Error resilience** - Graceful error handling with fallback responses ## Quick Start ### 1. Import and Inject Service ```typescript import { Component, inject } from '@angular/core'; import { CatalougeSearchService } from '@isa/catalogue/data-access'; @Component({ selector: 'app-product-search', template: '...' }) export class ProductSearchComponent { #catalogueSearch = inject(CatalougeSearchService); } ``` **Note:** For availability checking, import `AvailabilityService` from `@isa/availability/data-access` instead. ### 2. Search Products by EAN ```typescript async searchByEan(): Promise { 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) } ``` ## 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`); }); } ``` ### 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.