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
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
- Quick Start
- Core Concepts
- API Reference
- Usage Examples
- Search Types
- Validation and Business Rules
- Error Handling
- Testing
- 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
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 forabortSignal?: 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 failsError- 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 parametersorderBy?: OrderBy[]- Sort optionsskip?: 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
Basic EAN Search
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}`);
}
}
Error-Resilient Search
async searchWithErrorHandling(searchTerm: string): Promise<void> {
const abortController = new AbortController();
try {
const result = await this.#catalogueSearch.searchByTerm(
{
searchTerm,
skip: 0,
take: 20
},
abortController.signal
);
if (result.error) {
console.error('Search returned error:', result.message);
return;
}
this.displayResults(result.result);
} catch (error) {
if (error instanceof ZodError) {
console.error('Invalid search parameters:', error.errors);
this.showValidationError(error);
} else {
console.error('Search failed:', error);
this.showGenericError();
}
}
}
Search Types
EAN Search
Searches by European Article Number (barcode).
Characteristics:
- No pagination (returns all matches)
- Returns empty array on error
- Can search multiple EANs simultaneously
- Fast and exact matching
Use Cases:
- Barcode scanning
- Direct product lookup
- Batch item retrieval
Term Search
Full-text search across catalog.
Characteristics:
- Pagination support (skip/take)
- Throws errors on API failure
- Requires AbortSignal
- Tracks search analytics (doNotTrack: true)
Use Cases:
- Product search interface
- Auto-complete suggestions
- Browse functionality
Loyalty Search
Specialized search for loyalty reward items.
Characteristics:
- Automatic loyalty filters applied
- Supports advanced filtering and sorting
- Custom filter merging
- Returns undefined on error
Automatic Filters:
format: '!eb;!dl'- Excludes ebooks and downloadspraemie: '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:
-
Status Code Validation
// Only these codes are valid for downloads [2, 32, 256, 1024, 2048, 4096].includes(availability.status) -
Supplier 16 Stock Rule
// Supplier 16 with 0 stock = unavailable if (availability.supplierId === 16 && availability.inStock === 0) { return false; } -
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 errorsearchByTerm: Throws errorsearchLoyaltyItems: 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
ShoppingCartItemto@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
- Batch EAN Search - Single API call for multiple EANs (efficient)
- Early Validation - Zod validation fails fast before API calls
- Pagination Support - Prevents loading excessive data
- Request Cancellation - AbortSignal prevents wasted bandwidth
- Error Resilience - Graceful degradation for non-critical failures
Future Enhancements
Potential improvements identified:
- Caching Layer - Cache search results for repeated queries
- Search Debouncing - Built-in debounce for term searches
- Retry Logic - Automatic retry for transient failures
- Analytics Integration - Track search patterns and performance
- Type Narrowing - Use discriminated unions for different search types
- 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 servicezod- Schema validationrxjs- Reactive programming
Development Dependencies
@angular/core/testing- Angular testing utilitiesjest- 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.