- Add new reward-order-confirmation feature library with components and store - Implement checkout completion orchestrator service for order finalization - Migrate checkout/oms/crm models to Zod schemas for better type safety - Add order creation facade and display order schemas - Update shopping cart facade with order completion flow - Add comprehensive tests for shopping cart facade - Update routing to include order confirmation page
38 KiB
@isa/catalogue/data-access
A comprehensive product catalogue search and availability service for Angular applications, providing catalog item search, loyalty program integration, and specialized availability validation for download and delivery order types.
Overview
The Catalogue Data Access library provides a unified interface for searching and retrieving product catalog data, managing loyalty reward items, and validating product availability for digital downloads, DIG shipping, and B2B delivery. It integrates with the generated cat-search-api and availability-api clients to provide intelligent search routing, validation, and transformation of catalog and availability data.
Table of Contents
- Features
- Quick Start
- Core Concepts
- API Reference
- Usage Examples
- Search Types
- Availability Validation
- 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
- Availability validation - Download, DIG delivery, and B2B delivery validation
- Zod validation - Runtime schema validation for all parameters
- Request cancellation - AbortSignal support for all operations
- Pagination support - Skip/take parameters for search results
- Filtering and sorting - Advanced query token system with filters and orderBy
- Type-safe transformations - Extends generated DTOs with domain types
- Comprehensive logging - Integration with @isa/core/logging for debugging
- Error resilience - Graceful error handling with fallback responses
Quick Start
1. Import and Inject Services
import { Component, inject } from '@angular/core';
import { CatalougeSearchService, AvailabilityService } from '@isa/catalogue/data-access';
@Component({
selector: 'app-product-search',
template: '...'
})
export class ProductSearchComponent {
#catalogueSearch = inject(CatalougeSearchService);
#availabilityService = inject(AvailabilityService);
}
2. Search Products by EAN
async searchByEan(): Promise<void> {
const items = await this.#catalogueSearch.searchByEans([
'1234567890123',
'9876543210987'
]);
// Result: Item[] with product details and availability
items.forEach(item => {
console.log(`${item.product.name}: ${item.catalogAvailability.price.value.value}€`);
});
}
3. Search Products by Term
async searchProducts(): Promise<void> {
const abortController = new AbortController();
const result = await this.#catalogueSearch.searchByTerm(
{
searchTerm: 'laptop',
skip: 0,
take: 20
},
abortController.signal
);
console.log(`Found ${result.total} items`);
console.log(`Showing ${result.result.length} items`);
}
4. Search Loyalty Items
async searchLoyaltyItems(): Promise<void> {
const abortController = new AbortController();
const result = await this.#catalogueSearch.searchLoyaltyItems(
{
filter: { category: 'electronics' },
input: { search: 'tablet' },
skip: 0,
take: 25
},
abortController.signal
);
// Result includes only loyalty items (praemie = 1-)
// Automatically excludes ebooks and downloads (format: !eb;!dl)
}
5. Validate Download Availability
async validateDownloads(cartItems: ShoppingCartItem[]): Promise<void> {
const validations = await this.#availabilityService.validateDownloadAvailabilities(
cartItems
);
validations.forEach(validation => {
if (!validation.isAvailable) {
console.log(`Item ${validation.itemId} is not available for download`);
} else {
console.log(`Item ${validation.itemId} is available`);
// validation.availabilityUpdate contains the updated availability data
}
});
}
Core Concepts
Item Structure
The core Item interface extends the generated API DTO with domain-specific properties:
interface Item {
id: number; // Unique item identifier
product: Product; // Product details
catalogAvailability: Availability; // Catalog availability data
redemptionPoints?: number; // Loyalty points (for reward items)
// ... other fields from ItemDTO
}
Product Information
Product details include essential product metadata:
interface Product {
name: string; // Product name
contributors: string; // Author/artist/creator
catalogProductNumber: string; // Internal catalog number
ean: string; // European Article Number (barcode)
format: string; // Product format (e.g., 'CD', 'DVD', 'Book')
formatDetail: string; // Detailed format information
volume: string; // Size/volume description
manufacturer: string; // Manufacturer/publisher
// ... other fields from ProductDTO
}
Availability Information
Catalog availability includes pricing and stock data:
interface Availability {
price: Price; // Current price with VAT
// ... other fields from AvailabilityDTO
}
interface Price {
value: PriceValue; // Price amount and currency
// ... other fields from PriceDTO
}
interface PriceValue {
value: number; // Numeric price value
// ... other fields from PriceValueDTO
}
Query Settings
Loyalty query settings configure search UI:
type QuerySettings = UISettingsDTO; // Settings from API
// Contains:
// - filters: Available filter options
// - orderByOptions: Sort options
// - other UI configuration
Search Validation with Zod
All search parameters are validated using Zod schemas:
// Term search validation
const params = {
searchTerm: 'laptop',
skip: '0', // Coerced to number
take: '20' // Coerced to number
};
// Validation happens automatically
const result = await service.searchByTerm(params, abortSignal);
// Throws ZodError if validation fails
API Reference
CatalougeSearchService
Main service for searching catalog items and loyalty rewards.
searchByEans(ean, abortSignal?): Promise<Item[]>
Searches for items by their European Article Numbers (EANs/barcodes).
Parameters:
ean: string[]- Array of EAN codes to search 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`);
});
}
AvailabilityService
Service for validating product availability for download and delivery order types.
Note: This service is focused on availability validation for the catalogue domain. For full availability checking across all order types, use @isa/availability/data-access.
validateDownloadAvailabilities(items, abortSignal?): Promise<DownloadAvailabilityValidation[]>
Validates download item availabilities and returns validation results.
Parameters:
items: ShoppingCartItem[]- Shopping cart items to validateabortSignal?: AbortSignal- Optional abort signal for request cancellation
Returns: Promise resolving to array of validation results
Business Rules:
- Only processes items with
orderType === 'Download' - Skips items that already have
lastRequest(already validated) - Validates against download-specific status codes: 2, 32, 256, 1024, 2048, 4096
- Rejects supplier 16 with 0 stock
Example:
const cartItems: ShoppingCartItem[] = [
{
id: 1,
data: {
features: { orderType: 'Download' },
product: { ean: '123456', catalogProductNumber: 789 },
availability: { price: 9.99 }
}
}
];
const validations = await service.validateDownloadAvailabilities(cartItems);
validations.forEach(validation => {
if (validation.isAvailable) {
console.log(`Item ${validation.itemId} available for download`);
// Update cart with validation.availabilityUpdate
} else {
console.log(`Item ${validation.itemId} not available`);
// Remove from cart or mark as unavailable
}
});
Validation Result Structure:
interface DownloadAvailabilityValidation {
itemId: number;
isAvailable: boolean;
availabilityUpdate?: {
availabilityType: number;
ssc: number;
sscText: string;
supplier: { id: number };
isPrebooked: boolean;
estimatedShippingDate: string;
price: number;
lastRequest: string; // ISO timestamp
};
}
getDigDeliveryAvailability(item, abortSignal?): Promise<any>
Gets DIG delivery availability for a shopping cart item.
Parameters:
item: ShoppingCartItem- Shopping cart itemabortSignal?: AbortSignal- Optional abort signal for request cancellation
Returns: Promise resolving to availability data or null if not available
Example:
const item: ShoppingCartItem = {
id: 1,
data: {
product: { ean: '123456', catalogProductNumber: 789 },
quantity: 2,
availability: { price: 15.99 }
}
};
const availability = await service.getDigDeliveryAvailability(item);
if (availability) {
console.log(`Available: ${availability.sscText}`);
console.log(`Delivery: ${availability.estimatedShippingDate}`);
console.log(`Supplier: ${availability.supplier.id}`);
console.log(`Logistician: ${availability.logistician.id}`);
}
Response Structure:
{
availabilityType: number;
ssc: number;
sscText: string;
supplier: { id: number };
isPrebooked: boolean;
estimatedShippingDate: string;
estimatedDelivery: string;
price: number;
logistician: { id: number };
supplierProductNumber: string;
supplierInfo: string;
lastRequest: string;
priceMaintained: boolean;
}
getB2bDeliveryAvailability(item, defaultBranch, logistician, abortSignal?): Promise<any>
Gets B2B delivery availability for a shopping cart item with branch and logistician context.
Parameters:
item: ShoppingCartItem- Shopping cart itemdefaultBranch: Branch- Default branch for stock lookuplogistician: Logistician- Logistician data (typically ID 2470)abortSignal?: AbortSignal- Optional abort signal for request cancellation
Returns: Promise resolving to availability data or null if not available
Special Handling:
- Uses store availability endpoint (not shipping)
- Calculates total stock across all availabilities
- Always uses provided logistician ID in response
Example:
const item: ShoppingCartItem = {
id: 1,
data: {
product: { ean: '123456', catalogProductNumber: 789 },
quantity: 10,
availability: { price: 99.99 }
}
};
const branch = { id: 42, name: 'Main Branch' };
const logistician = { id: 2470, name: 'Standard Logistician' };
const availability = await service.getB2bDeliveryAvailability(
item,
branch,
logistician
);
if (availability) {
console.log(`In stock: ${availability.inStock} units`);
console.log(`Order deadline: ${availability.orderDeadline}`);
console.log(`Logistician: ${availability.logistician.id}`);
}
Response Structure:
{
orderDeadline: string;
availabilityType: number;
ssc: number;
sscText: string;
supplier: { id: number };
isPrebooked: boolean;
estimatedShippingDate: string;
price: number;
inStock: number; // Total across all availabilities
supplierProductNumber: string;
supplierInfo: string;
lastRequest: string;
priceMaintained: boolean;
logistician: { id: number };
}
Schema Types
SearchByTerm
interface SearchByTermInput {
searchTerm: string; // Min 1 character
skip?: number; // Default: 0
take?: number; // Default: 20, max: 100
}
QueryToken
interface QueryTokenInput {
filter?: Record<string, any>; // Filter criteria
input?: Record<string, string>; // Additional input
orderBy?: OrderBy[]; // Sort options
skip?: number; // Default: 0
take?: number; // Default: 25
}
interface OrderBy {
by: string; // Field name to sort by
label: string; // Display label
desc: boolean; // Descending sort
selected: boolean; // Currently selected
}
Usage Examples
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.