# @isa/availability/data-access A comprehensive product availability service for Angular applications supporting multiple order types and delivery methods across retail operations. ## Overview The Availability Data Access library provides a unified interface for checking product availability across six different order types: in-store pickup (Rücklage), customer pickup (Abholung), standard shipping (Versand), digital shipping (DIG-Versand), B2B shipping (B2B-Versand), and digital downloads (Download). It integrates with the generated availability API client and provides intelligent routing, validation, and transformation of availability data. ## Table of Contents - [Features](#features) - [Quick Start](#quick-start) - [Core Concepts](#core-concepts) - [API Reference](#api-reference) - [Usage Examples](#usage-examples) - [Order Types](#order-types) - [Validation and Business Rules](#validation-and-business-rules) - [Error Handling](#error-handling) - [Testing](#testing) - [Architecture Notes](#architecture-notes) ## Features - **Six order type support** - InStore, Pickup, Delivery, DIG-Versand, B2B-Versand, Download - **Intelligent routing** - Automatic endpoint selection based on order type - **Zod validation** - Runtime schema validation for all parameters - **Request cancellation** - AbortSignal support for all operations - **Batch and single-item APIs** - Flexible interfaces for different use cases - **Preferred availability selection** - Automatic selection of preferred suppliers - **Business rule enforcement** - Download validation, B2B logistician override - **Type-safe transformations** - Adapter pattern for API request/response mapping - **Comprehensive logging** - Integration with @isa/core/logging for debugging - **Stock integration** - Direct stock service integration for in-store availability ## Quick Start ### 1. Import and Inject ```typescript import { Component, inject } from '@angular/core'; import { AvailabilityService } from '@isa/availability/data-access'; @Component({ selector: 'app-product-detail', template: '...' }) export class ProductDetailComponent { #availabilityService = inject(AvailabilityService); } ``` ### 2. Check Availability for Multiple Items ```typescript async checkAvailability(): Promise { const availabilities = await this.#availabilityService.getAvailabilities({ orderType: 'Versand', items: [ { itemId: 123, ean: '1234567890123', quantity: 2 }, { itemId: 456, ean: '9876543210987', quantity: 1 } ] }); // Result: { '123': Availability, '456': Availability } const item123Availability = availabilities['123']; console.log(`Item 123 status: ${item123Availability.status}`); console.log(`Item 123 quantity: ${item123Availability.qty}`); } ``` ### 3. Check Availability for Single Item ```typescript async checkSingleItem(): Promise { const availability = await this.#availabilityService.getAvailability({ orderType: 'Versand', item: { itemId: 123, ean: '1234567890123', quantity: 1 } }); if (availability) { console.log(`Available: ${availability.qty} units`); console.log(`Price: ${availability.price?.value?.value}`); } else { console.log('Item not available'); } } ``` ### 4. Request Cancellation ```typescript async checkWithCancellation(): Promise { const abortController = new AbortController(); // Cancel after 5 seconds setTimeout(() => abortController.abort(), 5000); try { const availabilities = await this.#availabilityService.getAvailabilities( { orderType: 'Versand', items: [{ itemId: 123, ean: '1234567890123', quantity: 1 }] }, abortController.signal ); } catch (error) { console.log('Request cancelled or failed'); } } ``` ## Core Concepts ### Order Types The library supports six distinct order types, each with specific requirements and behavior: #### 1. InStore (Rücklage) - **Purpose**: Branch-based in-store availability for customer reservation - **Endpoint**: Stock service (not availability API) - **Required**: branchId, itemsIds array - **Special handling**: Uses RemissionStockService to fetch real-time stock quantities #### 2. Pickup (Abholung) - **Purpose**: Customer pickup at branch location - **Endpoint**: Store availability API - **Required**: branchId, items array with itemId, ean, quantity - **Special handling**: Uses store endpoint with branch context #### 3. Delivery (Versand) - **Purpose**: Standard shipping to customer address - **Endpoint**: Shipping availability API - **Required**: items array with itemId, ean, quantity - **Special handling**: Excludes supplier/logistician fields to prevent automatic orderType change #### 4. DIG-Versand - **Purpose**: Digital shipping for webshop customers - **Endpoint**: Shipping availability API - **Required**: items array with itemId, ean, quantity - **Special handling**: Standard transformation, includes supplier/logistician #### 5. B2B-Versand - **Purpose**: Business-to-business shipping with specific logistician - **Endpoint**: Store availability API - **Required**: items array with itemId, ean, quantity - **Special handling**: - Automatically fetches default branch (no branchId parameter needed) - Fetches logistician '2470' and overrides response logisticianId - Uses store endpoint (not shipping) #### 6. Download - **Purpose**: Digital product downloads - **Endpoint**: Shipping availability API - **Required**: items array with itemId, ean (no quantity) - **Special handling**: - Quantity forced to 1 - Validates download availability (supplier 16 with 0 stock = unavailable) - Validates status codes against whitelist ### Availability Response Structure ```typescript interface Availability { itemId: number; // Product item ID status: AvailabilityType; // Availability status code (see below) qty: number; // Available quantity ssc?: string; // Shipping service code sscText?: string; // Shipping service description supplierId?: number; // Supplier ID supplier?: string; // Supplier name logisticianId?: number; // Logistician ID logistician?: string; // Logistician name price?: Price; // Current price with VAT priceMaintained?: boolean; // Price maintenance flag at?: string; // Estimated delivery date (ISO format) altAt?: string; // Alternative delivery date requestStatusCode?: string; // Request status from API preferred?: number; // Preferred availability flag (1 = preferred) } ``` ### Availability Type Codes ```typescript const AvailabilityType = { NotSet: 0, // Not determined NotAvailable: 1, // Not available PrebookAtBuyer: 2, // Pre-order at buyer PrebookAtRetailer: 32, // Pre-order at retailer PrebookAtSupplier: 256, // Pre-order at supplier TemporaryNotAvailable: 512, // Temporarily unavailable Available: 1024, // Available for immediate delivery OnDemand: 2048, // Available on demand AtProductionDate: 4096, // Available at production date Discontinued: 8192, // Discontinued product EndOfLife: 16384, // End of life product }; ``` ### Validation with Zod All input parameters are validated using Zod schemas before processing: ```typescript // Example: Delivery availability params const params = { orderType: 'Versand', items: [ { itemId: '123', // Coerced to number ean: '1234567890123', quantity: '2', // Coerced to number price: { ... } } ] }; // Validation happens automatically const result = await service.getAvailabilities(params); // Throws ZodError if validation fails ``` ## API Reference ### AvailabilityService Main service for checking product availability across order types. #### `getAvailabilities(params, abortSignal?): Promise<{ [itemId: string]: Availability }>` Checks availability for multiple items based on order type. **Parameters:** - `params: GetAvailabilityInputParams` - Availability parameters (automatically validated) - `abortSignal?: AbortSignal` - Optional abort signal for request cancellation **Returns:** Promise resolving to dictionary mapping itemId to Availability **Throws:** - `ZodError` - If params validation fails - `ResponseArgsError` - If API returns an error - `Error` - If default branch/logistician not found (B2B only) **Example:** ```typescript const availabilities = await service.getAvailabilities({ orderType: 'Versand', items: [ { itemId: 123, ean: '1234567890', quantity: 2 }, { itemId: 456, ean: '0987654321', quantity: 1 } ] }); // Result: { '123': Availability, '456': Availability } ``` #### `getAvailability(params, abortSignal?): Promise` Checks availability for a single item. **Parameters:** - `params: GetSingleItemAvailabilityInputParams` - Single item parameters (automatically validated) - `abortSignal?: AbortSignal` - Optional abort signal for request cancellation **Returns:** Promise resolving to Availability, or undefined if not available **Throws:** - `ZodError` - If params validation fails - `ResponseArgsError` - If API returns an error **Example:** ```typescript const availability = await service.getAvailability({ orderType: 'Versand', item: { itemId: 123, ean: '1234567890', quantity: 1 } }); if (availability) { console.log(`Available: ${availability.qty} units`); } ``` ### AvailabilityFacade Pass-through facade for AvailabilityService. **Note**: This facade is currently under architectural review. It provides no additional value over direct service injection and may be removed in a future refactoring. Consider injecting `AvailabilityService` directly. ```typescript // Current pattern (via facade) #availabilityFacade = inject(AvailabilityFacade); // Recommended pattern (direct service) #availabilityService = inject(AvailabilityService); ``` ### Helper Functions #### `isDownloadAvailable(availability): boolean` Validates if a download item is available based on business rules. **Business Rules:** - Supplier ID 16 with 0 stock = unavailable - Must have valid availability type code (see VALID_DOWNLOAD_STATUS_CODES) **Parameters:** - `availability: Availability | null | undefined` - Availability to validate **Returns:** true if download is available, false otherwise **Example:** ```typescript import { isDownloadAvailable } from '@isa/availability/data-access'; if (isDownloadAvailable(availability)) { console.log('Download ready'); } ``` #### `selectPreferredAvailability(availabilities): Availability | undefined` Selects the preferred availability from a list (marked with `preferred === 1`). **Parameters:** - `availabilities: Availability[]` - List of availability options **Returns:** The preferred availability, or undefined if none found **Example:** ```typescript import { selectPreferredAvailability } from '@isa/availability/data-access'; const preferred = selectPreferredAvailability(apiResponse); ``` #### `calculateEstimatedDate(availability): string | undefined` Calculates the estimated shipping/delivery date based on API response. **Business Rule:** - If requestStatusCode === '32', use altAt (alternative date) - Otherwise, use at (standard date) **Parameters:** - `availability: Availability | null | undefined` - Availability data **Returns:** The estimated date string (ISO format), or undefined **Example:** ```typescript import { calculateEstimatedDate } from '@isa/availability/data-access'; const estimatedDate = calculateEstimatedDate(availability); console.log(`Delivery expected: ${estimatedDate}`); ``` #### `hasValidPrice(availability): boolean` Type guard to check if an availability has a valid price. **Parameters:** - `availability: Availability | null | undefined` - Availability to check **Returns:** true if availability has a price with a value > 0 **Example:** ```typescript import { hasValidPrice } from '@isa/availability/data-access'; if (hasValidPrice(availability)) { // TypeScript narrows type - price is guaranteed to exist console.log(`Price: ${availability.price.value.value}`); } ``` #### `isPriceMaintained(availability): boolean` Checks if an availability is price-maintained. **Parameters:** - `availability: Availability | null | undefined` - Availability to check **Returns:** true if price-maintained flag is set ## Usage Examples ### Checking In-Store Availability (Rücklage) ```typescript import { Component, inject } from '@angular/core'; import { AvailabilityService } from '@isa/availability/data-access'; @Component({ selector: 'app-in-store-check', template: '...' }) export class InStoreCheckComponent { #availabilityService = inject(AvailabilityService); async checkInStoreAvailability(branchId: number, itemIds: number[]): Promise { const availabilities = await this.#availabilityService.getAvailabilities({ orderType: 'Rücklage', branchId: branchId, itemsIds: itemIds }); for (const [itemId, availability] of Object.entries(availabilities)) { console.log(`Item ${itemId}: ${availability.qty} in stock`); } } } ``` ### Checking Pickup Availability (Abholung) ```typescript async checkPickupAvailability(branchId: number): Promise { const availabilities = await this.#availabilityService.getAvailabilities({ orderType: 'Abholung', branchId: branchId, items: [ { itemId: 123, ean: '1234567890', quantity: 2 }, { itemId: 456, ean: '0987654321', quantity: 1 } ] }); // Check if items are available for pickup for (const [itemId, availability] of Object.entries(availabilities)) { if (availability.status === AvailabilityType.Available) { console.log(`Item ${itemId} ready for pickup at branch ${branchId}`); } } } ``` ### Checking Standard Delivery (Versand) ```typescript import { AvailabilityType, calculateEstimatedDate } from '@isa/availability/data-access'; async checkDeliveryAvailability(): Promise { const availabilities = await this.#availabilityService.getAvailabilities({ orderType: 'Versand', items: [ { itemId: 123, ean: '1234567890', quantity: 1, price: { value: { value: 19.99, currency: 'EUR', currencySymbol: '€' }, vat: { value: 3.18, inPercent: 19, label: '19%', vatType: 1 } } } ] }); const item123 = availabilities['123']; if (item123) { const estimatedDate = calculateEstimatedDate(item123); console.log(`Available for delivery: ${item123.qty} units`); console.log(`Estimated delivery: ${estimatedDate}`); console.log(`Supplier: ${item123.supplier} (ID: ${item123.supplierId})`); } } ``` ### Checking B2B Delivery (B2B-Versand) ```typescript async checkB2BDelivery(): Promise { // No branchId required - automatically uses default branch // Logistician '2470' is automatically fetched and applied const availabilities = await this.#availabilityService.getAvailabilities({ orderType: 'B2B-Versand', items: [ { itemId: 123, ean: '1234567890', quantity: 10 } ] }); const item123 = availabilities['123']; if (item123) { console.log(`B2B availability: ${item123.qty} units`); console.log(`Logistician: ${item123.logisticianId} (overridden to 2470)`); } } ``` ### Checking Download Availability ```typescript import { isDownloadAvailable } from '@isa/availability/data-access'; async checkDownloadAvailability(): Promise { const availabilities = await this.#availabilityService.getAvailabilities({ orderType: 'Download', items: [ { itemId: 123, ean: '1234567890' } // No quantity needed ] }); const item123 = availabilities['123']; if (item123 && isDownloadAvailable(item123)) { console.log('Download ready for immediate delivery'); } else { console.log('Download not available'); } } ``` ### Single Item with AbortSignal ```typescript async checkSingleItemWithTimeout(): Promise { const abortController = new AbortController(); // Set 10 second timeout const timeoutId = setTimeout(() => { abortController.abort(); }, 10000); try { const availability = await this.#availabilityService.getAvailability( { orderType: 'Versand', item: { itemId: 123, ean: '1234567890', quantity: 1 } }, abortController.signal ); clearTimeout(timeoutId); if (availability) { console.log(`Item available: ${availability.qty} units`); } } catch (error) { clearTimeout(timeoutId); console.error('Request failed or timed out', error); } } ``` ### Handling Multiple Order Types ```typescript import { OrderType } from '@isa/checkout/data-access'; async checkMultipleOrderTypes( orderType: OrderType, items: Array<{ itemId: number; ean: string; quantity: number }> ): Promise { let params: GetAvailabilityInputParams; switch (orderType) { case 'Rücklage': params = { orderType: 'Rücklage', branchId: this.selectedBranchId, itemsIds: items.map(i => i.itemId) }; break; case 'Abholung': params = { orderType: 'Abholung', branchId: this.selectedBranchId, items: items }; break; case 'Versand': case 'DIG-Versand': params = { orderType: orderType, items: items }; break; case 'B2B-Versand': params = { orderType: 'B2B-Versand', items: items }; break; case 'Download': params = { orderType: 'Download', items: items.map(i => ({ itemId: i.itemId, ean: i.ean })) }; break; } const availabilities = await this.#availabilityService.getAvailabilities(params); console.log(`${orderType} availability:`, availabilities); } ``` ## Order Types ### Parameter Requirements by Order Type | Order Type | Required Parameters | Optional | Notes | |------------|-------------------|----------|-------| | **Rücklage** (InStore) | `orderType`, `itemsIds` | `branchId` | Uses stock service | | **Abholung** (Pickup) | `orderType`, `branchId`, `items` | - | Store endpoint | | **Versand** (Delivery) | `orderType`, `items` | - | Shipping endpoint, excludes supplier/logistician | | **DIG-Versand** | `orderType`, `items` | - | Shipping endpoint | | **B2B-Versand** | `orderType`, `items` | - | Fetches default branch + logistician 2470 | | **Download** | `orderType`, `items` (no quantity) | - | Quantity forced to 1, validation applied | ### Item Structure by Order Type #### InStore (Rücklage) ```typescript { orderType: 'Rücklage', branchId?: number, // Optional branch ID itemsIds: number[] // Array of item IDs only } ``` #### Pickup, Delivery, DIG-Versand, B2B-Versand ```typescript { orderType: 'Abholung' | 'Versand' | 'DIG-Versand' | 'B2B-Versand', branchId?: number, // Required only for Abholung items: Array<{ itemId: number, ean: string, quantity: number, price?: Price // Optional price information }> } ``` #### Download ```typescript { orderType: 'Download', items: Array<{ itemId: number, ean: string, price?: Price // Optional price information // No quantity field - always 1 }> } ``` ## Validation and Business Rules ### Zod Schema Validation All parameters are validated using Zod schemas before processing: **Type Coercion:** ```typescript // String to number coercion { itemId: '123' } → { itemId: 123 } { quantity: '2' } → { quantity: 2 } // Validation requirements itemId: z.coerce.number().int().positive() // Must be positive integer quantity: z.coerce.number().int().positive().default(1) // Positive with default ean: z.string() // Required string ``` **Minimum Array Lengths:** ```typescript items: z.array(ItemSchema).min(1) // At least 1 item required itemsIds: z.array(z.coerce.number()).min(1) // At least 1 ID required ``` ### Download Validation Rules Downloads have special validation requirements enforced by `isDownloadAvailable()`: 1. **Supplier 16 with 0 stock = unavailable** ```typescript if (availability.supplierId === 16 && availability.qty === 0) { return false; // Not available } ``` 2. **Valid status codes for downloads** ```typescript const VALID_CODES = [ AvailabilityType.PrebookAtBuyer, // 2 AvailabilityType.PrebookAtRetailer, // 32 AvailabilityType.PrebookAtSupplier, // 256 AvailabilityType.Available, // 1024 AvailabilityType.OnDemand, // 2048 AvailabilityType.AtProductionDate // 4096 ]; ``` ### B2B Special Handling B2B-Versand has unique requirements: 1. **Automatic default branch fetching** - No branchId parameter required - Service automatically fetches default branch via `BranchService` - Throws error if default branch has no ID 2. **Logistician 2470 override** - Automatically fetches logistician '2470' - Overrides all availability responses with this logisticianId - Throws error if logistician 2470 not found 3. **Store endpoint usage** - Uses store availability endpoint (not shipping) - Similar to Pickup but with automatic branch selection ### Preferred Availability Selection When multiple availability options exist for an item: ```typescript // API might return multiple availabilities per item // The service automatically selects the preferred one const preferred = availabilities.find(av => av.preferred === 1); ``` Only the preferred availability is included in the result dictionary. ## Error Handling ### Error Types #### ZodError Thrown when input parameters fail validation: ```typescript try { await service.getAvailabilities({ orderType: 'Versand', items: [] // Empty array - fails min(1) validation }); } catch (error) { if (error instanceof ZodError) { console.error('Validation error:', error.errors); // error.errors contains detailed validation failures } } ``` #### ResponseArgsError Thrown when the API returns an error: ```typescript import { ResponseArgsError } from '@isa/common/data-access'; try { await service.getAvailabilities(params); } catch (error) { if (error instanceof ResponseArgsError) { console.error('API error:', error.message); // Check error.message for details } } ``` #### Error (Generic) Thrown for business logic failures: ```typescript try { // B2B-Versand without default branch await service.getAvailabilities({ orderType: 'B2B-Versand', items: [{ itemId: 123, ean: '123', quantity: 1 }] }); } catch (error) { if (error.message === 'Default branch has no ID') { console.error('Branch configuration error'); } if (error.message === 'Logistician 2470 not found') { console.error('Logistician configuration error'); } } ``` ### Error Context Logging The service automatically logs errors with context: ```typescript // Logged automatically on error { orderType: 'Versand', itemIds: [123, 456], additional: { /* context-specific data */ } } ``` ### Request Cancellation Use AbortSignal to cancel in-flight requests: ```typescript const controller = new AbortController(); // Start request const promise = service.getAvailabilities(params, controller.signal); // Cancel if needed controller.abort(); try { await promise; } catch (error) { // Handle cancellation or other errors console.log('Request cancelled or failed'); } ``` ## Testing The library uses **Vitest** with **Angular Testing Utilities** for testing. ### Running Tests ```bash # Run tests for this library npx nx test availability-data-access --skip-nx-cache # Run tests with coverage npx nx test availability-data-access --code-coverage --skip-nx-cache # Run tests in watch mode npx nx test availability-data-access --watch ``` ### Test Structure The library includes comprehensive unit tests covering: - **Order type routing** - Validates correct endpoint selection for each order type - **Validation** - Tests Zod schema validation for all parameter types - **Business rules** - Tests download validation, B2B logistician override, etc. - **Error handling** - Tests API errors, validation failures, missing data - **Abort signal support** - Tests request cancellation - **Multiple items** - Tests batch processing - **Preferred selection** - Tests preferred availability selection logic ### Example Test ```typescript import { TestBed } from '@angular/core/testing'; import { describe, it, expect, beforeEach, vi } from 'vitest'; import { AvailabilityService } from './availability.service'; describe('AvailabilityService', () => { let service: AvailabilityService; beforeEach(() => { TestBed.configureTestingModule({ providers: [ AvailabilityService, // Mock providers... ] }); service = TestBed.inject(AvailabilityService); }); it('should fetch standard delivery availability', async () => { const result = await service.getAvailabilities({ orderType: 'Versand', items: [{ itemId: 123, ean: '1234567890', quantity: 3 }] }); expect(result).toHaveProperty('123'); expect(result['123'].itemId).toBe(123); }); }); ``` ## Architecture Notes ### Current Architecture The library follows a layered architecture: ``` Components/Features ↓ AvailabilityFacade (optional, pass-through) ↓ AvailabilityService (main business logic) ↓ ├─→ RemissionStockService (InStore) ├─→ AvailabilityRequestAdapter (request mapping) ├─→ Generated API Client (availability-api) └─→ Helper functions (transformers, validators) ``` ### Known Architectural Considerations #### 1. Facade Evaluation (Medium Priority) The `AvailabilityFacade` is currently under evaluation: **Current State:** - Pass-through wrapper with no added value - Just delegates to AvailabilityService - No orchestration logic **Recommendation:** - Consider removal if no orchestration is planned - Update components to inject AvailabilityService directly - Keep facade only if future orchestration is planned **Impact:** Low risk, reduces one layer of indirection #### 2. Order Type Handler Duplication (High Priority) The service contains 6 similar handler methods with significant code duplication: **Current State:** - ~180 lines of duplicated code - Bug fixes need to be applied to multiple methods **Proposed Refactoring:** - Template Method + Strategy pattern - Handler registry with common workflow - Post-processing hooks for special cases **Impact:** High value, reduces complexity significantly #### 3. Cross-Domain Dependency The library depends on `@isa/remission/data-access` for `BranchService`: **Current State:** - Direct dependency on remission domain - Availability domain cannot be used without remission domain **Proposed Solution:** - Create abstract `DefaultBranchProvider` interface - Inject provider instead of concrete BranchService - Implement at app level for domain independence **Impact:** Improves domain boundaries and testability ### Performance Considerations 1. **Parallel Requests** - B2B-Versand fetches branch and logistician in parallel 2. **Early Validation** - Zod validation fails fast before API calls 3. **Preferred Selection** - Efficient filtering with Array.find() 4. **Request Cancellation** - AbortSignal support prevents wasted bandwidth ### Future Enhancements Potential improvements identified: 1. **Caching Layer** - Cache availability responses for short periods 2. **Batch Optimization** - Optimize multiple availability checks 3. **Retry Logic** - Automatic retry for transient failures 4. **Analytics Integration** - Track availability check patterns 5. **Schema Simplification** - Reduce single-item schema duplication ## Dependencies ### Required Libraries - `@angular/core` - Angular framework - `@generated/swagger/availability-api` - Generated API client - `@isa/common/data-access` - Common data access utilities - `@isa/core/logging` - Logging service - `@isa/checkout/data-access` - Supplier and OrderType - `@isa/remission/data-access` - Stock and branch services - `@isa/oms/data-access` - Logistician service - `zod` - Schema validation - `rxjs` - Reactive programming ### Path Alias Import from: `@isa/availability/data-access` ## License Internal ISA Frontend library - not for external distribution.