Added "skip:ci" tag to multiple project configurations to prevent CI runs for certain libraries. This change affects the following libraries: crm-feature-customer-card-transactions, crm-feature-customer-loyalty-cards, oms-data-access, oms-feature-return-details, oms-feature-return-process, oms-feature-return-summary, remission-data-access, remission-feature-remission-list, remission-feature-remission-return-receipt-details, remission-feature-remission-return-receipt-list, remission-shared-remission-start-dialog, remission-shared-return-receipt-actions, shared-address, shared-delivery, ui-carousel, and ui-dialog. Also updated CI command in package.json to exclude tests with the "skip:ci" tag.
@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
- Quick Start
- Core Concepts
- API Reference
- Usage Examples
- Order Types
- Validation and Business Rules
- Error Handling
- Testing
- 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
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
async checkAvailability(): Promise<void> {
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
async checkSingleItem(): Promise<void> {
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
async checkWithCancellation(): Promise<void> {
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
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
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:
// 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 failsResponseArgsError- If API returns an errorError- If default branch/logistician not found (B2B only)
Example:
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<Availability | undefined>
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 failsResponseArgsError- If API returns an error
Example:
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.
// 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:
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:
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:
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:
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)
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<void> {
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)
async checkPickupAvailability(branchId: number): Promise<void> {
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)
import { AvailabilityType, calculateEstimatedDate } from '@isa/availability/data-access';
async checkDeliveryAvailability(): Promise<void> {
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)
async checkB2BDelivery(): Promise<void> {
// 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
import { isDownloadAvailable } from '@isa/availability/data-access';
async checkDownloadAvailability(): Promise<void> {
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
async checkSingleItemWithTimeout(): Promise<void> {
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
import { OrderType } from '@isa/checkout/data-access';
async checkMultipleOrderTypes(
orderType: OrderType,
items: Array<{ itemId: number; ean: string; quantity: number }>
): Promise<void> {
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)
{
orderType: 'Rücklage',
branchId?: number, // Optional branch ID
itemsIds: number[] // Array of item IDs only
}
Pickup, Delivery, DIG-Versand, B2B-Versand
{
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
{
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:
// 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:
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():
-
Supplier 16 with 0 stock = unavailable
if (availability.supplierId === 16 && availability.qty === 0) { return false; // Not available } -
Valid status codes for downloads
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:
-
Automatic default branch fetching
- No branchId parameter required
- Service automatically fetches default branch via
BranchService - Throws error if default branch has no ID
-
Logistician 2470 override
- Automatically fetches logistician '2470'
- Overrides all availability responses with this logisticianId
- Throws error if logistician 2470 not found
-
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:
// 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:
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:
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:
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:
// Logged automatically on error
{
orderType: 'Versand',
itemIds: [123, 456],
additional: { /* context-specific data */ }
}
Request Cancellation
Use AbortSignal to cancel in-flight requests:
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
# 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
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
DefaultBranchProviderinterface - Inject provider instead of concrete BranchService
- Implement at app level for domain independence
Impact: Improves domain boundaries and testability
Performance Considerations
- Parallel Requests - B2B-Versand fetches branch and logistician in parallel
- Early Validation - Zod validation fails fast before API calls
- Preferred Selection - Efficient filtering with Array.find()
- Request Cancellation - AbortSignal support prevents wasted bandwidth
Future Enhancements
Potential improvements identified:
- Caching Layer - Cache availability responses for short periods
- Batch Optimization - Optimize multiple availability checks
- Retry Logic - Automatic retry for transient failures
- Analytics Integration - Track availability check patterns
- 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 servicezod- Schema validationrxjs- Reactive programming
Path Alias
Import from: @isa/availability/data-access
License
Internal ISA Frontend library - not for external distribution.