mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
feat(checkout): add reward order confirmation feature with schema migrations
- 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
This commit is contained in:
@@ -1,7 +1,980 @@
|
||||
# availability-data-access
|
||||
# @isa/availability/data-access
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
A comprehensive product availability service for Angular applications supporting multiple order types and delivery methods across retail operations.
|
||||
|
||||
## Running unit tests
|
||||
## Overview
|
||||
|
||||
Run `nx test availability-data-access` to execute the unit tests.
|
||||
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<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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```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<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 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<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)
|
||||
|
||||
```typescript
|
||||
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)
|
||||
|
||||
```typescript
|
||||
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)
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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)
|
||||
```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.
|
||||
|
||||
@@ -32,57 +32,57 @@ import { PriceSchema } from '@isa/common/data-access';
|
||||
|
||||
// Base item schema - used for all availability checks
|
||||
const ItemSchema = z.object({
|
||||
itemId: z.coerce.number().int().positive(),
|
||||
ean: z.string(),
|
||||
price: PriceSchema.optional(),
|
||||
quantity: z.coerce.number().int().positive().default(1),
|
||||
itemId: z.coerce.number().int().positive().describe('Unique item identifier'),
|
||||
ean: z.string().describe('European Article Number barcode'),
|
||||
price: PriceSchema.describe('Item price information').optional(),
|
||||
quantity: z.coerce.number().int().positive().default(1).describe('Quantity of items to check availability for'),
|
||||
});
|
||||
|
||||
// Download items don't require quantity (always 1)
|
||||
const DownloadItemSchema = z.object({
|
||||
itemId: z.coerce.number().int().positive(),
|
||||
ean: z.string(),
|
||||
price: PriceSchema.optional(),
|
||||
itemId: z.coerce.number().int().positive().describe('Unique item identifier'),
|
||||
ean: z.string().describe('European Article Number barcode'),
|
||||
price: PriceSchema.describe('Item price information').optional(),
|
||||
});
|
||||
|
||||
const ItemsSchema = z.array(ItemSchema).min(1);
|
||||
const DownloadItemsSchema = z.array(DownloadItemSchema).min(1);
|
||||
const ItemsSchema = z.array(ItemSchema).min(1).describe('List of items to check availability for');
|
||||
const DownloadItemsSchema = z.array(DownloadItemSchema).min(1).describe('List of download items to check availability for');
|
||||
|
||||
// In-Store availability (Rücklage) - requires branch context
|
||||
export const GetInStoreAvailabilityParamsSchema = z.object({
|
||||
orderType: z.literal(OrderType.InStore),
|
||||
branchId: z.coerce.number().int().positive().optional(),
|
||||
itemsIds: z.array(z.coerce.number().int().positive()).min(1),
|
||||
orderType: z.literal(OrderType.InStore).describe('Order type specifying in-store availability check'),
|
||||
branchId: z.coerce.number().int().positive().describe('Branch identifier for in-store availability').optional(),
|
||||
itemsIds: z.array(z.coerce.number().int().positive()).min(1).describe('List of item identifiers to check in-store availability'),
|
||||
});
|
||||
|
||||
// Pickup availability (Abholung) - requires branch context
|
||||
export const GetPickupAvailabilityParamsSchema = z.object({
|
||||
orderType: z.literal(OrderType.Pickup),
|
||||
branchId: z.coerce.number().int().positive(),
|
||||
orderType: z.literal(OrderType.Pickup).describe('Order type specifying pickup availability check'),
|
||||
branchId: z.coerce.number().int().positive().describe('Branch identifier where items will be picked up'),
|
||||
items: ItemsSchema,
|
||||
});
|
||||
|
||||
// Standard delivery availability (Versand)
|
||||
export const GetDeliveryAvailabilityParamsSchema = z.object({
|
||||
orderType: z.literal(OrderType.Delivery),
|
||||
orderType: z.literal(OrderType.Delivery).describe('Order type specifying standard delivery availability check'),
|
||||
items: ItemsSchema,
|
||||
});
|
||||
|
||||
// DIG delivery availability (DIG-Versand) - for webshop customers
|
||||
export const GetDigDeliveryAvailabilityParamsSchema = z.object({
|
||||
orderType: z.literal(OrderType.DigitalShipping),
|
||||
orderType: z.literal(OrderType.DigitalShipping).describe('Order type specifying DIG delivery availability check for webshop customers'),
|
||||
items: ItemsSchema,
|
||||
});
|
||||
|
||||
// B2B delivery availability (B2B-Versand) - uses default branch
|
||||
export const GetB2bDeliveryAvailabilityParamsSchema = z.object({
|
||||
orderType: z.literal(OrderType.B2BShipping),
|
||||
orderType: z.literal(OrderType.B2BShipping).describe('Order type specifying B2B delivery availability check'),
|
||||
items: ItemsSchema,
|
||||
});
|
||||
|
||||
// Download availability - quantity always 1
|
||||
export const GetDownloadAvailabilityParamsSchema = z.object({
|
||||
orderType: z.literal(OrderType.Download),
|
||||
orderType: z.literal(OrderType.Download).describe('Order type specifying download availability check'),
|
||||
items: DownloadItemsSchema,
|
||||
});
|
||||
|
||||
@@ -125,34 +125,34 @@ export type GetDownloadAvailabilityParams = z.infer<
|
||||
|
||||
// Single-item schemas use the same structure but accept a single item instead of an array
|
||||
const SingleInStoreAvailabilityParamsSchema = z.object({
|
||||
orderType: z.literal(OrderType.InStore),
|
||||
branchId: z.coerce.number().int().positive(),
|
||||
itemId: z.number().int().positive(),
|
||||
orderType: z.literal(OrderType.InStore).describe('Order type specifying in-store availability check'),
|
||||
branchId: z.coerce.number().int().positive().describe('Branch identifier for in-store availability').optional(),
|
||||
itemId: z.number().int().positive().describe('Unique item identifier to check in-store availability'),
|
||||
});
|
||||
|
||||
const SinglePickupAvailabilityParamsSchema = z.object({
|
||||
orderType: z.literal(OrderType.Pickup),
|
||||
branchId: z.coerce.number().int().positive(),
|
||||
orderType: z.literal(OrderType.Pickup).describe('Order type specifying pickup availability check'),
|
||||
branchId: z.coerce.number().int().positive().describe('Branch identifier where item will be picked up'),
|
||||
item: ItemSchema,
|
||||
});
|
||||
|
||||
const SingleDeliveryAvailabilityParamsSchema = z.object({
|
||||
orderType: z.literal(OrderType.Delivery),
|
||||
orderType: z.literal(OrderType.Delivery).describe('Order type specifying standard delivery availability check'),
|
||||
item: ItemSchema,
|
||||
});
|
||||
|
||||
const SingleDigDeliveryAvailabilityParamsSchema = z.object({
|
||||
orderType: z.literal(OrderType.DigitalShipping),
|
||||
orderType: z.literal(OrderType.DigitalShipping).describe('Order type specifying DIG delivery availability check for webshop customers'),
|
||||
item: ItemSchema,
|
||||
});
|
||||
|
||||
const SingleB2bDeliveryAvailabilityParamsSchema = z.object({
|
||||
orderType: z.literal(OrderType.B2BShipping),
|
||||
orderType: z.literal(OrderType.B2BShipping).describe('Order type specifying B2B delivery availability check'),
|
||||
item: ItemSchema,
|
||||
});
|
||||
|
||||
const SingleDownloadAvailabilityParamsSchema = z.object({
|
||||
orderType: z.literal(OrderType.Download),
|
||||
orderType: z.literal(OrderType.Download).describe('Order type specifying download availability check'),
|
||||
item: DownloadItemSchema,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user