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:
Lorenz Hilpert
2025-10-21 14:28:52 +02:00
parent b96d889da5
commit 2b5da00249
259 changed files with 61347 additions and 2652 deletions

View File

@@ -90,5 +90,8 @@
"cursor-global": true,
"cursor-workspace": true
},
"chat.mcp.access": "all"
"chat.mcp.access": "all",
"typescript.inlayHints.parameterTypes.enabled": true,
"typescript.inlayHints.variableTypes.enabled": true,
"editor.hover.delay": 100
}

View File

@@ -274,6 +274,8 @@ npx nx affected:test
- **E2E Testing Requirements**: Always include `data-what`, `data-which`, and dynamic `data-*` attributes in HTML templates - these are essential for automated testing by QA colleagues
### Library Development Patterns
- **Understanding Internal Libraries**: Before using any internal library from the `libs/` directory, always read its README.md file first to understand its purpose, API, and proper usage patterns
- **Library Documentation**: All libraries have comprehensive README.md documentation. To prevent context pollution, **always use a subagent** (preferably `docs-architect` or `general-purpose`) to retrieve specific information from library documentation rather than reading the entire file directly
- **New Library Creation**: Use Nx generators with domain-specific naming (`[domain]-[layer]-[feature]`)
- **Standalone Components**: All new components must be standalone with explicit imports - no more NgModules
- **Testing Framework Selection**:
@@ -281,6 +283,96 @@ npx nx affected:test
- **Existing libraries**: Continue with Jest + Spectator until migrated
- **Path Aliases**: Always use `@isa/[domain]/[layer]/[feature]` - avoid relative imports across domain boundaries
#### Library Reference Guide
All 62 libraries in the monorepo have comprehensive README.md documentation. Use subagents to retrieve specific information from these READMEs to avoid context pollution.
**Availability Domain (1 library)**
- `@isa/availability/data-access` - Product availability service supporting 6 order types (InStore, Pickup, Delivery, DIG-Versand, B2B-Versand, Download) with intelligent routing, Zod validation, and business rule enforcement
**Catalogue Domain (1 library)**
- `@isa/catalogue/data-access` - Product search and availability validation service with multi-type search (EAN, Term, Loyalty), download validation, and DIG/B2B delivery availability
**Checkout Domain (6 libraries)**
- `@isa/checkout/data-access` - Shopping cart management and checkout orchestration supporting 6 order types with reward system integration, payment type logic, and CRM data transformation
- `@isa/checkout/feature/reward-order-confirmation` - Order confirmation page for reward/premium orders with address display and item list
- `@isa/checkout/feature/reward-shopping-cart` - Complete reward shopping cart feature with checkout workflow, item management, and order completion orchestration
- `@isa/checkout/feature/reward-catalog` - Reward catalog browsing with customer bonus points, filtering, pagination, and item selection
- `@isa/checkout/shared/product-info` - Product information display components for checkout (redemption info, destination info, stock info)
- `@isa/checkout/shared/reward-selection-dialog` - Product selection dialog for adding items to reward shopping cart
**Common Libraries (3 libraries)**
- `@isa/common/data-access` - Foundational data access utilities including error hierarchy, custom RxJS operators (takeUntilAborted, takeUntilKeydown), batching infrastructure, and Zod integration
- `@isa/common/decorators` - TypeScript decorators for validation (ValidateParams), caching (Cache), debouncing (Debounce), and in-flight request management (InFlight, InFlightWithKey, InFlightWithCache)
- `@isa/common/print` - Platform-aware print service supporting both label and office printers with smart printer selection and reusable print dialog components
**Core Libraries (5 libraries)**
- `@isa/core/config` - Type-safe configuration management with runtime Zod validation, nested object access via dot notation, and environment-specific configuration
- `@isa/core/logging` - Centralized logging service with log levels, contextual information, and Angular integration
- `@isa/core/navigation` - Context preservation service for multi-step navigation flows with automatic tab-scoped storage and cleanup
- `@isa/core/storage` - User storage abstraction with SessionStorage/IndexedDB backends and automatic serialization
- `@isa/core/tabs` - Tab management system with NgRx Signals, persistent storage, intelligent history pruning, and Router integration
**CRM Domain (1 library)**
- `@isa/crm/data-access` - Customer relationship management data access with customer fetching, shipping address management, bonus cards, payer info, and tab-based state management
**Icons (1 library)**
- `@isa/icons` - Icon library with Angular icon components and SVG assets
**OMS Domain (9 libraries)**
- `@isa/oms/data-access` - Order Management System data access with return search, question-based workflows, state management (3 stores), and print integration
- `@isa/oms/feature/return-details` - Receipt details view with item display, customer history, quantity management, and return eligibility validation
- `@isa/oms/feature/return-process` - Dynamic question-based return process with 6 product categories, 5 question types, form validation, and state persistence
- `@isa/oms/feature/return-review` - Final review step for return process with task summary, receipt reprinting, and navigation protection
- `@isa/oms/feature/return-search` - Return search with filtering, pagination, infinite scroll, and automatic redirect to details when single result found
- `@isa/oms/feature/return-summary` - Pre-submission summary of return process with item review and final confirmation before order creation
- `@isa/oms/shared/product-info` - Product display component for OMS workflows with image, navigation, and format icons
- `@isa/oms/shared/task-list` - Task list component with dual appearance modes (main/review), NgRx integration, and tab-based isolation
- `@isa/oms/utils/translation` - Receipt type translation utility with 13 German translations and dependency injection support
**Remission Domain (8 libraries)**
- `@isa/remission/data-access` - Remission/returns management data access supporting Pflichtremission (mandatory) and Abteilungsremission (department overflow) with stock batching, state management, and supplier/branch services
- `@isa/remission/feature/remission-list` - Main remission list with dual types, filtering, search, and resource-based data fetching
- `@isa/remission/feature/remission-return-receipt-details` - Receipt details view with items, metadata, and action integration
- `@isa/remission/feature/remission-return-receipt-list` - List view for all return receipts with sorting, filtering, and parallel resource fetching
- `@isa/remission/shared/product` - Product display components for remission workflows (info, stock info, shelf meta)
- `@isa/remission/shared/remission-start-dialog` - Two-step dialog for receipt creation and package assignment
- `@isa/remission/shared/return-receipt-actions` - Action components for receipt deletion, continuation, and completion
- `@isa/remission/shared/search-item-to-remit-dialog` - Dialog for adding unlisted items to remission with search-to-remit flow
**Shared Component Libraries (7 libraries)**
- `@isa/shared/address` - Address display components (multi-line and inline) with country name resolution and German address special handling
- `@isa/shared/filter` - Advanced filtering system with filter groups, date ranges, search, and scanner integration
- `@isa/shared/product-format` - Product format display with icon and text components supporting 6 format codes (HC, PB, EB, AB, DIG, AUD)
- `@isa/shared/product-image` - Product image directive with CDN integration, configurable dimensions, and lazy loading
- `@isa/shared/product-router-link` - EAN-based product navigation directive with pluggable URL builder pattern
- `@isa/shared/quantity-control` - Accessible quantity selector with dropdown presets, manual input mode, and smart logic
- `@isa/shared/scanner` - Barcode scanner integration with camera and keyboard input support
**UI Component Libraries (17 libraries)**
- `@isa/ui/bullet-list` - Bullet list component with parent-child icon inheritance and signal-based reactivity
- `@isa/ui/buttons` - Five button components (Button, TextButton, IconButton, InfoButton, StatefulButton) with pending states, size/color variants, and ARIA support
- `@isa/ui/datepicker` - Range datepicker with validators, calendar views, and ControlValueAccessor integration
- `@isa/ui/dialog` - Dialog system with 5 preset types (message, confirmation, text-input, number-input, feedback) and injection-based API
- `@isa/ui/empty-state` - Empty state component with 4 appearance variants (general, no-results, no-data, error) and SVG icons
- `@isa/ui/expandable` - Expandable/collapsible container with smooth animations
- `@isa/ui/input-controls` - Form control components (checkbox, dropdown, text-field, chips, checklist, listbox, inline-input) with ControlValueAccessor integration
- `@isa/ui/item-rows` - Item display rows with data components and directive-based composition
- `@isa/ui/label` - Label component with Tag/Notice types and High/Medium/Low priority levels
- `@isa/ui/layout` - Breakpoint service for responsive design with 4 breakpoints (Tablet, Desktop, DesktopL, DesktopXL)
- `@isa/ui/menu` - CDK Menu wrapper components with keyboard navigation and ARIA compliance
- `@isa/ui/progress-bar` - Determinate and indeterminate progress indicators with computed width
- `@isa/ui/search-bar` - Search bar with clear button, Angular Forms integration, and focus management
- `@isa/ui/skeleton-loader` - Loading state component with structural directive and customizable dimensions
- `@isa/ui/toolbar` - Toolbar component with two size variants (small/medium) and content projection
- `@isa/ui/tooltip` - Tooltip directive with positioning and accessibility
- `@isa/ui/bullet-list` - Bullet list component with customizable icons and nested list support
**Utility Libraries (3 libraries)**
- `@isa/utils/ean-validation` - EAN barcode validation with Angular Forms validator, standalone validation function, and comprehensive GS1 prefix reference
- `@isa/utils/scroll-position` - Scroll position restoration service with tab-based storage
- `@isa/utils/z-safe-parse` - Safe Zod parsing utility with automatic fallback and console warnings
### API Integration Workflow
- **Swagger Generation**: Run `npm run generate:swagger` to regenerate all 10 API clients when backend changes
- **Data Services**: Wrap generated API clients in domain-specific data-access services with proper error handling and Zod validation

View File

@@ -208,6 +208,13 @@ const routes: Routes = [
(m) => m.routes,
),
},
{
path: 'order-confirmation',
loadChildren: () =>
import('@isa/checkout/feature/reward-order-confirmation').then(
(m) => m.routes,
),
},
],
},

View File

@@ -1024,15 +1024,15 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
purchaseOption,
);
let promotion: Promotion | null = { value: item.promoPoints };
let loyalty: Loyalty | null = null;
let promotion: Promotion | undefined = { value: item.promoPoints };
let loyalty: Loyalty | undefined = undefined;
const redemptionPoints: number | null = item.redemptionPoints || null;
// "Lesepunkte einlösen" logic
// If "Lesepunkte einlösen" is checked and item has redemption points, set price to 0 and remove promotion
if (this.useRedemptionPoints) {
// If loyalty is set, we need to remove promotion
promotion = null;
promotion = undefined;
// Set loyalty points from item
loyalty = { value: redemptionPoints };
// Set price to 0

View File

@@ -165,7 +165,9 @@ export class DetailsMainViewBillingAddressesComponent
}
} else if (this.showCustomerAddress) {
this._host.setPayer(
CustomerAdapter.toPayerFromCustomer(customer as Customer),
CustomerAdapter.toPayerFromCustomer(
customer as unknown as Customer,
),
);
}
});

View File

@@ -189,7 +189,9 @@ export class DetailsMainViewDeliveryAddressesComponent
);
} else if (this.showCustomerAddress) {
this._host.setShippingAddress(
ShippingAddressAdapter.fromCustomer(customer as Customer),
ShippingAddressAdapter.fromCustomer(
customer as unknown as Customer,
),
);
}
});

View File

@@ -279,7 +279,7 @@ export class CustomerDetailsViewMainComponent
}
get buyer() {
return CustomerAdapter.toBuyer(this.customer as Customer);
return CustomerAdapter.toBuyer(this.customer as unknown as Customer);
}
get payer() {

8399
graph.json Normal file
View File

File diff suppressed because it is too large Load Diff

View File

@@ -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.

View File

@@ -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,
});

View File

File diff suppressed because it is too large Load Diff

View File

@@ -5,10 +5,10 @@ import { z } from 'zod';
* Used for sorting search results.
*/
export const OrderBySchema = z.object({
by: z.string(), // Field name to sort by
label: z.string(), // Display label for the sort option
desc: z.boolean(), // Whether sorting is descending
selected: z.boolean(), // Whether this sort option is currently selected
by: z.string().describe('Field name to sort by'),
label: z.string().describe('Display label for the sort option'),
desc: z.boolean().describe('Whether sorting is descending'),
selected: z.boolean().describe('Whether this sort option is currently selected'),
});
/**
@@ -16,11 +16,11 @@ export const OrderBySchema = z.object({
* Used for search operations to ensure consistent query structure.
*/
export const QueryTokenSchema = z.object({
filter: z.record(z.any()).default({}), // Filter criteria as key-value pairs
input: z.record(z.string()).default({}).optional(),
orderBy: z.array(OrderBySchema).default([]).optional(), // Sorting parameters
skip: z.number().default(0),
take: z.number().default(25),
filter: z.record(z.any()).describe('Filter criteria as key-value pairs').default({}),
input: z.record(z.string()).describe('Input parameters as key-value pairs').default({}).optional(),
orderBy: z.array(OrderBySchema).describe('Sorting parameters').default([]).optional(),
skip: z.number().describe('Number of items to skip for pagination').default(0),
take: z.number().describe('Number of items to return per page').default(25),
});
/**

View File

@@ -1,7 +1,791 @@
# data-access
# @isa/checkout/data-access
This library was generated with [Nx](https://nx.dev).
A comprehensive checkout and shopping cart management library for Angular applications supporting multiple order types, reward redemption, and complex multi-step checkout workflows across retail and e-commerce operations.
## Running unit tests
## Overview
Run `nx test data-access` to execute the unit tests.
The Checkout Data Access library provides the complete infrastructure for managing shopping carts, reward catalogs, and checkout processes. It handles six distinct order types (in-store pickup, customer pickup, standard shipping, digital shipping, B2B shipping, and digital downloads), reward/loyalty redemption flows, customer/payer data transformation, and multi-phase checkout orchestration with automatic availability validation and destination management.
## 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)
- [Checkout Flow](#checkout-flow)
- [Reward System](#reward-system)
- [Data Transformation](#data-transformation)
- [Error Handling](#error-handling)
- [Testing](#testing)
- [Architecture Notes](#architecture-notes)
## Features
- **Shopping Cart Management** - Create, update, and manage shopping carts with full CRUD operations
- **Six Order Type Support** - InStore (Rücklage), Pickup (Abholung), Delivery (Versand), DIG-Versand, B2B-Versand, Download
- **Reward Catalog Store** - NgRx Signals store for managing reward items and selections with tab isolation
- **Complete Checkout Orchestration** - 13-step checkout workflow with automatic validation and transformation
- **CRM Data Integration** - Seamless conversion between CRM and checkout-api formats
- **Multi-Domain Adapters** - 8 specialized adapters for cross-domain data transformation
- **Zod Validation** - Runtime schema validation for 58+ schemas
- **Payment Type Determination** - Automatic payment type selection (FREE/CASH/INVOICE) based on order analysis
- **Availability Validation** - Integrated download validation and shipping availability updates
- **Destination Management** - Automatic shipping address updates and logistician assignment
- **Session Persistence** - Reward catalog state persists across browser sessions
- **Request Cancellation** - AbortSignal support for all async operations
- **Comprehensive Error Handling** - Typed CheckoutCompletionError with specific error codes
- **Tab Isolation** - Shopping carts and reward selections scoped per browser tab
- **Business Logic Helpers** - 15+ pure functions for order analysis and validation
## Quick Start
### 1. Shopping Cart Operations
```typescript
import { Component, inject } from '@angular/core';
import { ShoppingCartFacade } from '@isa/checkout/data-access';
@Component({
selector: 'app-checkout',
template: '...'
})
export class CheckoutComponent {
#shoppingCartFacade = inject(ShoppingCartFacade);
async createCart(): Promise<void> {
// Create new shopping cart
const cart = await this.#shoppingCartFacade.createShoppingCart();
console.log('Cart created:', cart.id);
// Get shopping cart
const existingCart = await this.#shoppingCartFacade.getShoppingCart(cart.id!);
console.log('Cart items:', existingCart?.items?.length);
}
}
```
### 2. Complete Checkout with CRM Data
```typescript
import { Component, inject } from '@angular/core';
import { ShoppingCartFacade } from '@isa/checkout/data-access';
import { CustomerResource } from '@isa/crm/data-access';
@Component({
selector: 'app-complete-checkout',
template: '...'
})
export class CompleteCheckoutComponent {
#shoppingCartFacade = inject(ShoppingCartFacade);
#customerResource = inject(CustomerResource);
async completeCheckout(shoppingCartId: number): Promise<void> {
// Fetch customer from CRM
const customer = await this.#customerResource.value();
if (!customer) {
throw new Error('Customer not found');
}
// Complete checkout with automatic CRM data transformation
const orders = await this.#shoppingCartFacade.completeWithCrmData({
shoppingCartId,
crmCustomer: customer,
crmShippingAddress: customer.shippingAddresses?.[0]?.data,
crmPayer: customer.payers?.[0]?.payer?.data,
notificationChannels: customer.notificationChannels ?? 1,
specialComment: 'Please handle with care'
});
console.log('Orders created:', orders.length);
orders.forEach(order => {
console.log(`Order ${order.orderNumber}: ${order.orderType}`);
});
}
}
```
### 3. Reward Catalog Management
```typescript
import { Component, inject } from '@angular/core';
import { RewardCatalogStore } from '@isa/checkout/data-access';
import { Item } from '@isa/catalogue/data-access';
@Component({
selector: 'app-reward-selection',
template: '...'
})
export class RewardSelectionComponent {
#rewardCatalogStore = inject(RewardCatalogStore);
// Access reactive signals
items = this.#rewardCatalogStore.items;
selectedItems = this.#rewardCatalogStore.selectedItems;
hits = this.#rewardCatalogStore.hits;
selectReward(itemId: number, item: Item): void {
this.#rewardCatalogStore.selectItem(itemId, item);
}
removeReward(itemId: number): void {
this.#rewardCatalogStore.removeItem(itemId);
}
clearAllSelections(): void {
this.#rewardCatalogStore.clearSelectedItems();
}
}
```
### 4. Adding Items to Shopping Cart
```typescript
import { ShoppingCartService } from '@isa/checkout/data-access';
@Component({
selector: 'app-add-to-cart',
template: '...'
})
export class AddToCartComponent {
#shoppingCartService = inject(ShoppingCartService);
async addItemsToCart(shoppingCartId: number): Promise<void> {
// Check if items can be added first
const canAddResults = await this.#shoppingCartService.canAddItems({
shoppingCartId,
payload: [
{ itemId: 123, quantity: 2 },
{ itemId: 456, quantity: 1 }
]
});
// Process results
canAddResults.forEach(result => {
if (result.canAdd) {
console.log(`Item ${result.itemId} can be added`);
} else {
console.log(`Item ${result.itemId} cannot be added: ${result.reason}`);
}
});
// Add items to cart
const updatedCart = await this.#shoppingCartService.addItem({
shoppingCartId,
items: [
{
destination: { target: 2 },
product: {
id: 123,
catalogProductNumber: 'PROD-123',
description: 'Sample Product'
},
availability: {
price: {
value: { value: 29.99, currency: 'EUR' },
vat: { value: 4.78, inPercent: 19 }
}
},
quantity: 2
}
]
});
console.log('Updated cart:', updatedCart.items?.length);
}
}
```
## Core Concepts
### Order Types
The library supports six distinct order types, each with specific characteristics and handling requirements:
#### 1. InStore (Rücklage)
- **Purpose**: In-store reservation for later pickup
- **Characteristics**: Branch-based, no shipping required
- **Payment**: Cash (4)
- **Features**: `{ orderType: 'Rücklage' }`
#### 2. Pickup (Abholung)
- **Purpose**: Customer pickup at branch location
- **Characteristics**: Branch-based, customer collects items
- **Payment**: Cash (4)
- **Features**: `{ orderType: 'Abholung' }`
#### 3. Delivery (Versand)
- **Purpose**: Standard shipping to customer address
- **Characteristics**: Requires shipping address, logistician assignment
- **Payment**: Invoice (128)
- **Features**: `{ orderType: 'Versand' }`
#### 4. Digital Shipping (DIG-Versand)
- **Purpose**: Digital delivery for webshop customers
- **Characteristics**: Requires special availability validation
- **Payment**: Invoice (128)
- **Features**: `{ orderType: 'DIG-Versand' }`
#### 5. B2B Shipping (B2B-Versand)
- **Purpose**: Business-to-business delivery
- **Characteristics**: Requires logistician 2470, default branch
- **Payment**: Invoice (128)
- **Features**: `{ orderType: 'B2B-Versand' }`
#### 6. Download
- **Purpose**: Digital product downloads
- **Characteristics**: Requires download availability validation
- **Payment**: Invoice (128) or Free (2) for loyalty
- **Features**: `{ orderType: 'Download' }`
### Shopping Cart State
```typescript
interface ShoppingCart {
id?: number; // Shopping cart ID
items?: EntityContainer<ShoppingCartItem>[]; // Cart items with metadata
createdAt?: string; // Creation timestamp
updatedAt?: string; // Last update timestamp
features?: Record<string, string>; // Custom features/flags
}
interface ShoppingCartItem {
id?: number; // Item ID in cart
destination?: Destination; // Shipping/pickup destination
product?: Product; // Product information
availability?: OlaAvailability; // Availability and pricing
quantity: number; // Item quantity
loyalty?: Loyalty; // Loyalty points/rewards
promotion?: Promotion; // Applied promotions
features?: Record<string, string>; // Item features (orderType, etc.)
specialComment?: string; // Item-specific instructions
}
```
### Checkout Flow Overview
The checkout completion process consists of 13 coordinated steps:
```
1. Fetch and validate shopping cart
2. Analyze order types (delivery, pickup, download, etc.)
3. Analyze customer type (B2B, online, guest, staff)
4. Determine if payer is required
5. Create or refresh checkout entity
6. Update destinations for customer (if needed)
7. Set special comments on items (if provided)
8. Validate download availabilities
9. Update shipping availabilities (DIG-Versand, B2B-Versand)
10. Set buyer on checkout
11. Set notification channels
12. Set payer (if required)
13. Set payment type (FREE/CASH/INVOICE)
14. Update destination shipping addresses (if delivery)
Result: checkoutId → Pass to OrderCreationFacade for order creation
```
### Payment Type Logic
Payment type is automatically determined based on order analysis:
```typescript
/**
* Payment type decision tree:
*
* IF all items have loyalty.value > 0:
* → FREE (2) - Loyalty redemption
*
* ELSE IF hasDelivery OR hasDownload OR hasDigDelivery OR hasB2BDelivery:
* → INVOICE (128) - Invoicing required
*
* ELSE:
* → CASH (4) - In-store payment
*/
const PaymentTypes = {
FREE: 2, // Loyalty/reward orders
CASH: 4, // In-store pickup/take away
INVOICE: 128 // Delivery and download orders
};
```
### Order Options Analysis
The library analyzes shopping cart items to determine checkout requirements:
```typescript
interface OrderOptionsAnalysis {
hasTakeAway: boolean; // Has Rücklage items
hasPickUp: boolean; // Has Abholung items
hasDownload: boolean; // Has Download items
hasDelivery: boolean; // Has Versand items
hasDigDelivery: boolean; // Has DIG-Versand items
hasB2BDelivery: boolean; // Has B2B-Versand items
items: ShoppingCartItem[]; // Unwrapped items for processing
}
```
**Business Rules:**
- **Payer Required**: B2B customers OR any delivery/download orders
- **Destination Update Required**: Any delivery or download orders
- **Payment Type**: Determined by order types and loyalty status
- **Shipping Address Required**: Any delivery orders (Versand, DIG-Versand, B2B-Versand)
### Customer Type Analysis
Customer features are analyzed to determine handling requirements:
```typescript
interface CustomerTypeAnalysis {
isOnline: boolean; // Webshop customer
isGuest: boolean; // Guest account
isB2B: boolean; // Business customer
hasCustomerCard: boolean; // Loyalty card holder (Pay4More)
isStaff: boolean; // Employee/staff member
}
```
**Feature Mapping:**
- `webshop``isOnline: true`
- `guest``isGuest: true`
- `b2b``isB2B: true`
- `p4mUser``hasCustomerCard: true`
- `staff``isStaff: true`
### Reward System Architecture
The reward catalog store manages reward items and selections with automatic tab isolation:
```typescript
interface RewardCatalogEntity {
tabId: number; // Browser tab ID (isolation)
items: Item[]; // Available reward items
hits: number; // Total search results
selectedItems: Record<number, Item>; // Selected items by ID
}
```
**Key Features:**
- **Tab Isolation**: Each browser tab has independent reward state
- **Session Persistence**: State survives browser refreshes
- **Orphan Cleanup**: Automatically removes state when tabs close
- **Reactive Signals**: Computed signals for active tab data
- **Dual Shopping Carts**: Separate carts for regular and reward items
## API Reference
### ShoppingCartFacade
Main facade for shopping cart and checkout operations.
#### `createShoppingCart(): Promise<ShoppingCart>`
Creates a new empty shopping cart.
**Returns:** Promise resolving to the created shopping cart
**Example:**
```typescript
const cart = await facade.createShoppingCart();
console.log('Cart ID:', cart.id);
```
#### `getShoppingCart(shoppingCartId, abortSignal?): Promise<ShoppingCart | undefined>`
Fetches an existing shopping cart by ID.
**Parameters:**
- `shoppingCartId: number` - ID of the shopping cart to fetch
- `abortSignal?: AbortSignal` - Optional cancellation signal
**Returns:** Promise resolving to the shopping cart, or undefined if not found
**Throws:**
- `ResponseArgsError` - If API returns an error
**Example:**
```typescript
const cart = await facade.getShoppingCart(123, abortController.signal);
if (cart) {
console.log('Items:', cart.items?.length);
}
```
#### `removeItem(params): Promise<ShoppingCart>`
Removes an item from the shopping cart (sets quantity to 0).
**Parameters:**
- `params: RemoveShoppingCartItemParams`
- `shoppingCartId: number` - Shopping cart ID
- `shoppingCartItemId: number` - Item ID to remove
**Returns:** Promise resolving to updated shopping cart
**Throws:**
- `ZodError` - If params validation fails
- `ResponseArgsError` - If API returns an error
**Example:**
```typescript
const updatedCart = await facade.removeItem({
shoppingCartId: 123,
shoppingCartItemId: 456
});
```
#### `updateItem(params): Promise<ShoppingCart>`
Updates a shopping cart item (quantity, special comment, etc.).
**Parameters:**
- `params: UpdateShoppingCartItemParams`
- `shoppingCartId: number` - Shopping cart ID
- `shoppingCartItemId: number` - Item ID to update
- `values: UpdateShoppingCartItemDTO` - Fields to update
**Returns:** Promise resolving to updated shopping cart
**Throws:**
- `ZodError` - If params validation fails
- `ResponseArgsError` - If API returns an error
**Example:**
```typescript
const updatedCart = await facade.updateItem({
shoppingCartId: 123,
shoppingCartItemId: 456,
values: { quantity: 5, specialComment: 'Gift wrap please' }
});
```
#### `complete(params, abortSignal?): Promise<Order[]>`
Completes checkout and creates orders.
**Parameters:**
- `params: CompleteOrderParams`
- `shoppingCartId: number` - Shopping cart to process
- `buyer: Buyer` - Buyer information
- `notificationChannels?: NotificationChannel` - Communication channels
- `customerFeatures: Record<string, string>` - Customer feature flags
- `payer?: Payer` - Payer information (required for B2B/delivery/download)
- `shippingAddress?: ShippingAddress` - Shipping address (required for delivery)
- `specialComment?: string` - Special instructions
- `abortSignal?: AbortSignal` - Optional cancellation signal
**Returns:** Promise resolving to array of created orders
**Throws:**
- `CheckoutCompletionError` - For validation or business logic failures
- `ResponseArgsError` - For API failures
**Example:**
```typescript
const orders = await facade.complete({
shoppingCartId: 123,
buyer: buyerDTO,
customerFeatures: { webshop: 'webshop', p4mUser: 'p4mUser' },
notificationChannels: 1,
payer: payerDTO,
shippingAddress: addressDTO
}, abortController.signal);
console.log(`Created ${orders.length} orders`);
```
#### `completeWithCrmData(params, abortSignal?): Promise<Order[]>`
Completes checkout with CRM data, automatically transforming customer/payer/address.
**Parameters:**
- `params: CompleteCrmOrderParams`
- `shoppingCartId: number` - Shopping cart to process
- `crmCustomer: Customer` - Customer from CRM service
- `crmPayer?: PayerDTO` - Payer from CRM service (optional)
- `crmShippingAddress?: ShippingAddressDTO` - Shipping address from CRM (optional)
- `notificationChannels?: NotificationChannel` - Communication channels
- `specialComment?: string` - Special instructions
- `abortSignal?: AbortSignal` - Optional cancellation signal
**Returns:** Promise resolving to array of created orders
**Throws:**
- `CheckoutCompletionError` - For validation or business logic failures
- `ResponseArgsError` - For API failures
**Transformation Steps:**
1. Validates input with Zod schema
2. Converts `crmCustomer` to `buyer` using `CustomerAdapter.toBuyer()`
3. Converts `crmShippingAddress` using `ShippingAddressAdapter.fromCrmShippingAddress()`
4. Converts `crmPayer` using `PayerAdapter.toCheckoutFormat()`
5. Extracts customer features using `CustomerAdapter.extractCustomerFeatures()`
6. Delegates to `complete()` with transformed data
**Example:**
```typescript
const customer = await customerResource.value();
const orders = await facade.completeWithCrmData({
shoppingCartId: 123,
crmCustomer: customer,
crmShippingAddress: customer.shippingAddresses[0].data,
crmPayer: customer.payers[0].payer.data,
notificationChannels: customer.notificationChannels ?? 1,
specialComment: 'Rush delivery'
});
```
### Helper Functions
#### Analysis Helpers
##### `analyzeOrderOptions(items): OrderOptionsAnalysis`
Analyzes shopping cart items to determine which order types are present.
**Parameters:**
- `items: EntityContainer<ShoppingCartItem>[]` - Cart items to analyze
**Returns:** Analysis result with boolean flags for each order type
**Example:**
```typescript
const analysis = analyzeOrderOptions(cart.items);
if (analysis.hasDelivery) {
console.log('Delivery items present - shipping address required');
}
```
##### `analyzeCustomerTypes(features): CustomerTypeAnalysis`
Analyzes customer features to determine customer type characteristics.
**Parameters:**
- `features: Record<string, string>` - Customer feature flags
**Returns:** Analysis result with boolean flags for customer types
##### `shouldSetPayer(options, customer): boolean`
Determines if payer should be set based on order and customer analysis.
**Business Rule:** Payer required when: `isB2B OR hasB2BDelivery OR hasDelivery OR hasDigDelivery OR hasDownload`
##### `needsDestinationUpdate(options): boolean`
Determines if destination update is needed.
**Business Rule:** Update needed when: `hasDownload OR hasDelivery OR hasDigDelivery OR hasB2BDelivery`
##### `determinePaymentType(options): PaymentType`
Determines payment type based on order analysis.
**Business Rules:**
1. If all items have `loyalty.value > 0`: **FREE (2)**
2. Else if has delivery/download orders: **INVOICE (128)**
3. Else: **CASH (4)**
### Adapters
The library provides 8 specialized adapters for cross-domain data transformation:
#### CustomerAdapter
Converts CRM customer data to checkout-api format.
**Static Methods:**
- `toBuyer(customer): Buyer` - Converts customer to buyer
- `toPayerFromCustomer(customer): Payer` - Converts customer to payer (self-paying)
- `toPayerFromAssignedPayer(assignedPayer): Payer | null` - Unwraps AssignedPayer container
- `extractCustomerFeatures(customer): Record<string, string>` - Extracts feature flags
**Key Differences:**
- **Buyer**: Includes `source` field and `dateOfBirth`
- **Payer from Customer**: No `source`, no `dateOfBirth`, sets `payerStatus: 0`
- **Payer from AssignedPayer**: Includes `source` field, unwraps EntityContainer
## Usage Examples
### Complete Multi-Step Checkout Workflow
```typescript
import { Component, inject, signal } from '@angular/core';
import { ShoppingCartFacade, CheckoutCompletionError } from '@isa/checkout/data-access';
import { CustomerResource } from '@isa/crm/data-access';
@Component({
selector: 'app-checkout-flow',
template: `
<div class="checkout">
<h2>Checkout</h2>
@if (loading()) { <p>Processing checkout...</p> }
@if (error()) { <div class="error">{{ error() }}</div> }
<button (click)="processCheckout()" [disabled]="loading()">
Complete Checkout
</button>
</div>
`
})
export class CheckoutFlowComponent {
#shoppingCartFacade = inject(ShoppingCartFacade);
#customerResource = inject(CustomerResource);
loading = signal(false);
error = signal<string | null>(null);
orders = signal<Order[]>([]);
shoppingCartId = 123;
async processCheckout(): Promise<void> {
this.loading.set(true);
this.error.set(null);
try {
const customer = await this.#customerResource.value();
if (!customer) throw new Error('Customer not found');
const createdOrders = await this.#shoppingCartFacade.completeWithCrmData({
shoppingCartId: this.shoppingCartId,
crmCustomer: customer,
crmShippingAddress: customer.shippingAddresses?.[0]?.data,
crmPayer: customer.payers?.[0]?.payer?.data,
notificationChannels: customer.notificationChannels ?? 1
});
this.orders.set(createdOrders);
} catch (err) {
if (err instanceof CheckoutCompletionError) {
this.error.set(`Checkout failed: ${err.message}`);
} else {
this.error.set('An unexpected error occurred');
}
} finally {
this.loading.set(false);
}
}
}
```
## Order Types
### Parameter Requirements by Order Type
| Order Type | Features Flag | Payment Type | Payer Required | Shipping Address | Special Handling |
|------------|--------------|--------------|----------------|------------------|------------------|
| **Rücklage** (InStore) | `orderType: 'Rücklage'` | CASH (4) | No | No | In-store reservation |
| **Abholung** (Pickup) | `orderType: 'Abholung'` | CASH (4) | No | No | Customer pickup at branch |
| **Versand** (Delivery) | `orderType: 'Versand'` | INVOICE (128) | Yes | Yes | Standard shipping |
| **DIG-Versand** | `orderType: 'DIG-Versand'` | INVOICE (128) | Yes | Yes | Digital delivery, availability validation |
| **B2B-Versand** | `orderType: 'B2B-Versand'` | INVOICE (128) | Yes | Yes | Logistician 2470, default branch |
| **Download** | `orderType: 'Download'` | INVOICE (128) or FREE (2) | Yes | No | Download validation |
## Checkout Flow
The complete checkout process consists of 13 coordinated steps that validate data, transform cross-domain entities, update availabilities, and prepare the checkout for order creation.
## Reward System
### Dual Shopping Cart Architecture
The reward system uses separate shopping carts:
1. **Regular Shopping Cart** (`shoppingCartId`) - Items purchased with money
2. **Reward Shopping Cart** (`rewardShoppingCartId`) - Items purchased with loyalty points (zero price, loyalty.value set)
## Data Transformation
### CRM to Checkout Transformation
The library provides automatic transformation from CRM domain to checkout-api domain through specialized adapters.
## Error Handling
### CheckoutCompletionError Types
```typescript
type CheckoutCompletionErrorCode =
| 'SHOPPING_CART_NOT_FOUND' // Cart with ID doesn't exist
| 'SHOPPING_CART_EMPTY' // Cart has no items
| 'CHECKOUT_NOT_FOUND' // Checkout entity not found
| 'INVALID_AVAILABILITY' // Availability validation failed
| 'MISSING_BUYER' // Buyer data not provided
| 'MISSING_REQUIRED_DATA' // Required field missing
| 'CHECKOUT_CONFLICT' // Order already exists (HTTP 409)
| 'ORDER_CREATION_FAILED' // Order creation failed
| 'DOWNLOAD_UNAVAILABLE' // Download items not available
| 'SHIPPING_AVAILABILITY_FAILED'; // Shipping availability update failed
```
## Testing
The library uses **Vitest** with **Angular Testing Utilities** for testing.
### Running Tests
```bash
# Run tests for this library
npx nx test checkout-data-access --skip-nx-cache
# Run tests with coverage
npx nx test checkout-data-access --code-coverage --skip-nx-cache
# Run tests in watch mode
npx nx test checkout-data-access --watch
```
## Architecture Notes
### Current Architecture
```
Components/Features
ShoppingCartFacade (orchestration)
├─→ ShoppingCartService (cart CRUD)
├─→ CheckoutService (checkout workflow)
├─→ OrderCreationFacade (oms-api)
└─→ Adapters (cross-domain transformation)
Stores (NgRx Signals)
RewardCatalogStore (reward state management)
```
### Key Design Decisions
1. **Stateless Checkout Service** - All data passed as parameters for testability
2. **Separation of Checkout and Order Creation** - Clean domain boundaries
3. **Dual Shopping Cart for Rewards** - Payment and pricing isolation
4. **Extensive Adapter Pattern** - 8 adapters for cross-domain transformation
5. **Tab-Isolated Reward Catalog** - NgRx Signals with automatic cleanup
## Dependencies
### Required Libraries
- `@angular/core` - Angular framework
- `@ngrx/signals` - NgRx Signals for state management
- `@generated/swagger/checkout-api` - Generated checkout API client
- `@isa/common/data-access` - Common utilities
- `@isa/core/logging` - Logging service
- `@isa/core/storage` - Session storage
- `@isa/core/tabs` - Tab service
- `@isa/catalogue/data-access` - Availability services
- `@isa/crm/data-access` - Customer types
- `@isa/oms/data-access` - Order creation
- `@isa/remission/data-access` - Branch service
- `zod` - Schema validation
- `rxjs` - Reactive programming
### Path Alias
Import from: `@isa/checkout/data-access`
## License
Internal ISA Frontend library - not for external distribution.

View File

@@ -1,6 +1,6 @@
import { PriceDTO, Price } from '@generated/swagger/checkout-api';
import { Availability as AvaAvailability } from '@isa/availability/data-access';
import { Availability, AvailabilityType } from '../models';
import { Availability, AvailabilityType } from '../schemas';
/**
* Availability data from catalogue-api (raw response)
@@ -55,6 +55,8 @@ export class AvailabilityAdapter {
data: {
id: catalogueAvailability.supplier.id,
},
// Explicitly omit externalReference to avoid TypeScript errors
// (generated DTOs require externalStatus when externalReference is present)
},
isPrebooked: catalogueAvailability.isPrebooked,
estimatedShippingDate: catalogueAvailability.estimatedShippingDate,
@@ -78,6 +80,8 @@ export class AvailabilityAdapter {
data: {
id: catalogueAvailability.logistician.id,
},
// Explicitly omit externalReference to avoid TypeScript errors
// (generated DTOs require externalStatus when externalReference is present)
};
}
@@ -132,6 +136,8 @@ export class AvailabilityAdapter {
data: {
id: availability.logisticianId,
},
// Explicitly omit externalReference to avoid TypeScript errors
// (generated DTOs require externalStatus when externalReference is present)
};
}
@@ -142,6 +148,8 @@ export class AvailabilityAdapter {
data: {
id: availability.supplierId,
},
// Explicitly omit externalReference to avoid TypeScript errors
// (generated DTOs require externalStatus when externalReference is present)
};
}

View File

@@ -1,201 +1,219 @@
import { Customer, AssignedPayer } from '@isa/crm/data-access';
import { Buyer, Payer } from '../models';
/**
* Adapter for converting CRM customer data to checkout-api format.
*
* Handles three distinct conversion scenarios:
* 1. **Customer to Buyer** (`toBuyer`): Converts customer for checkout as the buyer.
* 2. **Customer to Payer** (`toPayerFromCustomer`): Uses customer as payer (self-paying).
* 3. **AssignedPayer to Payer** (`toPayerFromAssignedPayer`): Unwraps separate payer entity.
*
* **Key Patterns:**
* - Buyer conversion includes `dateOfBirth` and `source` field
* - Payer from customer omits `source`, sets default `payerStatus: 0`
* - AssignedPayer unwrapping handles `EntityContainer` structure with null safety
*/
export class CustomerAdapter {
private static readonly ADAPTER_NAME = 'CustomerAdapter';
/**
* Converts Customer to checkout-api Buyer.
*
* @remarks
* Used when the customer is the buyer in a checkout flow. Maps all relevant
* customer information including personal details, address, and organization.
*
* The buyer includes a `source` field referencing the customer entity ID,
* and preserves the `dateOfBirth` field which is specific to buyers.
*
* Type mapping: `customerType` (CRM) → `buyerType` (Checkout)
*
* @param customer - Customer from CRM service
* @returns Buyer compatible with checkout-api
*
* @example
* ```typescript
* const customer = await crmService.getCustomer(123);
* const buyer = CustomerAdapter.toBuyer(customer);
* await checkoutService.complete({ buyer, ... });
* ```
*/
static toBuyer(customer: Customer): Buyer {
return {
source: customer.id,
reference: { id: customer.id },
buyerType: customer.customerType,
buyerNumber: customer.customerNumber,
gender: customer.gender,
title: customer.title,
firstName: customer.firstName,
lastName: customer.lastName,
dateOfBirth: customer.dateOfBirth,
communicationDetails: customer.communicationDetails
? { ...customer.communicationDetails }
: undefined,
organisation: customer.organisation
? { ...customer.organisation }
: undefined,
address: customer.address ? { ...customer.address } : undefined,
};
}
/**
* Converts Customer to checkout-api Payer.
*
* @remarks
* Used when the customer acts as their own payer (self-paying scenarios,
* B2B customers, staff, customer card holders without separate billing address).
*
* **Important differences from toBuyer:**
* - No `source` field (indicates derived data, not a separate entity)
* - No `dateOfBirth` field (not relevant for payer)
* - Sets `payerStatus: 0` (active status by default)
*
* Type mapping: `customerType` (CRM) → `payerType` (Checkout)
*
* @param customer - Customer from CRM service
* @returns Payer compatible with checkout-api
*
* @example
* ```typescript
* const customer = await crmService.getCustomer(123);
* const payer = CustomerAdapter.toPayerFromCustomer(customer);
* await checkoutService.complete({ payer, ... });
* ```
*/
static toPayerFromCustomer(customer: Customer): Payer {
return {
reference: { id: customer.id },
payerType: customer.customerType as Payer['payerType'],
payerNumber: customer.customerNumber,
payerStatus: 0, // Default status: active
gender: customer.gender,
title: customer.title,
firstName: customer.firstName,
lastName: customer.lastName,
communicationDetails: customer.communicationDetails
? { ...customer.communicationDetails }
: undefined,
organisation: customer.organisation
? { ...customer.organisation }
: undefined,
address: customer.address ? { ...customer.address } : undefined,
};
}
/**
* Converts AssignedPayer (EntityContainer wrapper) to checkout-api Payer.
*
* @remarks
* Used when customer has a separate billing address/payer entity assigned.
* Handles the CRM's `EntityContainer` structure which wraps payer data.
*
* **Container structure:**
* ```typescript
* assignedPayer: {
* payer: {
* id: number;
* data?: PayerDTO; // Actual payer data
* displayName?: string;
* enabled?: boolean;
* }
* }
* ```
*
* Returns `null` if the payer data is missing or the container is invalid.
* Includes `source` field as this references a persistent payer entity.
*
* @param assignedPayer - AssignedPayer container from CRM service
* @returns Payer compatible with checkout-api, or null if data is missing
*
* @example
* ```typescript
* const assignedPayer = customer.payers.find(p => p.payer.id === selectedPayerId);
* const payer = CustomerAdapter.toPayerFromAssignedPayer(assignedPayer);
* if (payer) {
* await checkoutService.complete({ payer, ... });
* }
* ```
*/
static toPayerFromAssignedPayer(
assignedPayer: AssignedPayer,
): Payer | null {
const payer = assignedPayer?.payer?.data;
if (!payer) {
return null;
}
return {
reference: { id: payer.id },
payerType: payer.payerType,
payerNumber: payer.payerNumber,
payerStatus: payer.payerStatus,
gender: payer.gender,
title: payer.title,
firstName: payer.firstName,
lastName: payer.lastName,
communicationDetails: payer.communicationDetails
? { ...payer.communicationDetails }
: undefined,
organisation: payer.organisation ? { ...payer.organisation } : undefined,
address: payer.address ? { ...payer.address } : undefined,
source: payer.id,
};
}
/**
* Type guard for Customer
*
* @param value - Value to check
* @returns true if value is a valid Customer with required fields
*/
static isValidCustomer(value: unknown): value is Customer {
if (typeof value !== 'object' || value === null) return false;
const customer = value as Customer;
return (
typeof customer.id === 'number' &&
(customer.customerNumber === undefined ||
typeof customer.customerNumber === 'string')
);
}
/**
* Type guard for AssignedPayer
*
* @param value - Value to check
* @returns true if value is a valid AssignedPayer with container structure
*/
static isValidAssignedPayer(value: unknown): value is AssignedPayer {
if (typeof value !== 'object' || value === null) return false;
const assignedPayer = value as AssignedPayer;
return (
typeof assignedPayer.payer === 'object' &&
assignedPayer.payer !== null &&
typeof assignedPayer.payer.id === 'number'
);
}
}
import { Customer, AssignedPayer } from '@isa/crm/data-access';
import { Buyer, Payer } from '../schemas';
/**
* Adapter for converting CRM customer data to checkout-api format.
*
* Handles three distinct conversion scenarios:
* 1. **Customer to Buyer** (`toBuyer`): Converts customer for checkout as the buyer.
* 2. **Customer to Payer** (`toPayerFromCustomer`): Uses customer as payer (self-paying).
* 3. **AssignedPayer to Payer** (`toPayerFromAssignedPayer`): Unwraps separate payer entity.
*
* **Key Patterns:**
* - Buyer conversion includes `dateOfBirth` and `source` field
* - Payer from customer omits `source`, sets default `payerStatus: 0`
* - AssignedPayer unwrapping handles `EntityContainer` structure with null safety
*/
export class CustomerAdapter {
private static readonly ADAPTER_NAME = 'CustomerAdapter';
/**
* Converts Customer to checkout-api Buyer.
*
* @remarks
* Used when the customer is the buyer in a checkout flow. Maps all relevant
* customer information including personal details, address, and organization.
*
* The buyer includes a `source` field referencing the customer entity ID,
* and preserves the `dateOfBirth` field which is specific to buyers.
*
* Type mapping: `customerType` (CRM) → `buyerType` (Checkout)
*
* @param customer - Customer from CRM service
* @returns Buyer compatible with checkout-api
*
* @example
* ```typescript
* const customer = await crmService.getCustomer(123);
* const buyer = CustomerAdapter.toBuyer(customer);
* await checkoutService.complete({ buyer, ... });
* ```
*/
static toBuyer(customer: Customer): Buyer {
return {
source: customer.id,
reference: {
id: customer.id,
// Explicitly omit externalReference to avoid TypeScript errors
// (generated DTOs require externalStatus when externalReference is present)
},
buyerType: customer.customerType,
buyerNumber: customer.customerNumber,
gender: customer.gender,
title: customer.title,
firstName: customer.firstName,
lastName: customer.lastName,
dateOfBirth: customer.dateOfBirth,
communicationDetails: customer.communicationDetails
? { ...customer.communicationDetails }
: undefined,
organisation: customer.organisation
? { ...customer.organisation }
: undefined,
address: customer.address ? { ...customer.address } : undefined,
};
}
/**
* Converts Customer to checkout-api Payer.
*
* @remarks
* Used when the customer acts as their own payer (self-paying scenarios,
* B2B customers, staff, customer card holders without separate billing address).
*
* **Important differences from toBuyer:**
* - No `source` field (indicates derived data, not a separate entity)
* - No `dateOfBirth` field (not relevant for payer)
* - Sets `payerStatus: 0` (active status by default)
*
* Type mapping: `customerType` (CRM) → `payerType` (Checkout)
*
* @param customer - Customer from CRM service
* @returns Payer compatible with checkout-api
*
* @example
* ```typescript
* const customer = await crmService.getCustomer(123);
* const payer = CustomerAdapter.toPayerFromCustomer(customer);
* await checkoutService.complete({ payer, ... });
* ```
*/
static toPayerFromCustomer(customer: Customer): Payer {
return {
reference: {
id: customer.id,
// Explicitly omit externalReference to avoid TypeScript errors
// (generated DTOs require externalStatus when externalReference is present)
},
payerType: customer.customerType as Payer['payerType'],
payerNumber: customer.customerNumber,
payerStatus: 0, // Default status: active
gender: customer.gender,
title: customer.title,
firstName: customer.firstName,
lastName: customer.lastName,
communicationDetails: customer.communicationDetails
? { ...customer.communicationDetails }
: undefined,
organisation: customer.organisation
? { ...customer.organisation }
: undefined,
address: customer.address ? { ...customer.address } : undefined,
};
}
/**
* Converts AssignedPayer (EntityContainer wrapper) to checkout-api Payer.
*
* @remarks
* Used when customer has a separate billing address/payer entity assigned.
* Handles the CRM's `EntityContainer` structure which wraps payer data.
*
* **Container structure:**
* ```typescript
* assignedPayer: {
* payer: {
* id: number;
* data?: PayerDTO; // Actual payer data
* displayName?: string;
* enabled?: boolean;
* }
* }
* ```
*
* Returns `null` if the payer data is missing or the container is invalid.
* Includes `source` field as this references a persistent payer entity.
*
* @param assignedPayer - AssignedPayer container from CRM service
* @returns Payer compatible with checkout-api, or null if data is missing
*
* @example
* ```typescript
* const assignedPayer = customer.payers.find(p => p.payer.id === selectedPayerId);
* const payer = CustomerAdapter.toPayerFromAssignedPayer(assignedPayer);
* if (payer) {
* await checkoutService.complete({ payer, ... });
* }
* ```
*/
static toPayerFromAssignedPayer(assignedPayer: AssignedPayer): Payer | null {
const payer = assignedPayer?.payer?.data;
if (!payer) {
return null;
}
return {
reference: {
id: payer.id,
// Explicitly omit externalReference to avoid TypeScript errors
// (generated DTOs require externalStatus when externalReference is present)
},
payerType: payer.payerType,
payerNumber: payer.payerNumber,
payerStatus: payer.payerStatus,
gender: payer.gender,
title: payer.title,
firstName: payer.firstName,
lastName: payer.lastName,
communicationDetails: payer.communicationDetails
? { ...payer.communicationDetails }
: undefined,
organisation: payer.organisation ? { ...payer.organisation } : undefined,
address: payer.address ? { ...payer.address } : undefined,
source: payer.id,
};
}
/**
* Type guard for Customer
*
* @param value - Value to check
* @returns true if value is a valid Customer with required fields
*/
static isValidCustomer(value: unknown): value is Customer {
if (typeof value !== 'object' || value === null) return false;
const customer = value as Customer;
return (
typeof customer.id === 'number' &&
(customer.customerNumber === undefined ||
typeof customer.customerNumber === 'string')
);
}
/**
* Type guard for AssignedPayer
*
* @param value - Value to check
* @returns true if value is a valid AssignedPayer with container structure
*/
static isValidAssignedPayer(value: unknown): value is AssignedPayer {
if (typeof value !== 'object' || value === null) return false;
const assignedPayer = value as AssignedPayer;
return (
typeof assignedPayer.payer === 'object' &&
assignedPayer.payer !== null &&
typeof assignedPayer.payer.id === 'number'
);
}
static extractCustomerFeatures(customer: Customer): Record<string, any> {
const features = customer.features || [];
return features.reduce((acc: Record<string, string>, feature: any) => {
acc[feature.key] = feature.key;
return acc;
}, {});
}
}

View File

@@ -1,73 +1,72 @@
import { Payer as CrmPayer } from '@isa/crm/data-access';
import { Payer } from '../models';
/**
* Adapter for converting CRM-api payer responses to checkout-api format.
*
* Handles:
* - Filtering CRM-specific fields (agentComment, deactivationComment, etc.)
* - Entity reference creation (reference.id, source)
* - Nested object copying (communicationDetails, organisation, address)
*/
export class PayerAdapter {
private static readonly ADAPTER_NAME = 'PayerAdapter';
/**
* Converts CRM-api payer to checkout-api Payer.
*
* @remarks
* Maps payer information from the CRM domain to the checkout domain's
* payer representation. CRM-specific fields like agentComment, deactivationComment,
* defaultPaymentPeriod, isGuestAccount, payerGroup, paymentTypes,
* standardInvoiceText, statusChangeComment, and statusComment are filtered out.
*
* Nested objects (communicationDetails, organisation, address) are shallow-copied
* to prevent unintended mutations.
*
* @param crmPayer - Raw payer from CRM service
* @returns Payer compatible with checkout-api
*
* @example
* ```typescript
* const checkoutPayer = PayerAdapter.toCheckoutFormat(crmPayer);
* await checkoutService.complete({ payer: checkoutPayer, ... });
* ```
*/
static toCheckoutFormat(crmPayer: CrmPayer): Payer {
return {
reference: { id: crmPayer.id },
source: crmPayer.id,
payerType: crmPayer.payerType,
payerNumber: crmPayer.payerNumber,
payerStatus: crmPayer.payerStatus,
gender: crmPayer.gender,
title: crmPayer.title,
firstName: crmPayer.firstName,
lastName: crmPayer.lastName,
communicationDetails: crmPayer.communicationDetails
? { ...crmPayer.communicationDetails }
: undefined,
organisation: crmPayer.organisation
? { ...crmPayer.organisation }
: undefined,
address: crmPayer.address ? { ...crmPayer.address } : undefined,
};
}
/**
* Type guard for CRM payer response
*
* @param value - Value to check
* @returns true if value is a valid CRM payer with required fields
*/
static isValidCrmPayer(value: unknown): value is CrmPayer {
if (typeof value !== 'object' || value === null) return false;
const payer = value as CrmPayer;
return (
typeof payer.id === 'number' &&
(payer.payerNumber === undefined ||
typeof payer.payerNumber === 'string')
);
}
}
import { Payer as CrmPayer } from '@isa/crm/data-access';
import { Payer } from '../schemas';
/**
* Adapter for converting CRM-api payer responses to checkout-api format.
*
* Handles:
* - Filtering CRM-specific fields (agentComment, deactivationComment, etc.)
* - Entity reference creation (reference.id, source)
* - Nested object copying (communicationDetails, organisation, address)
*/
export class PayerAdapter {
private static readonly ADAPTER_NAME = 'PayerAdapter';
/**
* Converts CRM-api payer to checkout-api Payer.
*
* @remarks
* Maps payer information from the CRM domain to the checkout domain's
* payer representation. CRM-specific fields like agentComment, deactivationComment,
* defaultPaymentPeriod, isGuestAccount, payerGroup, paymentTypes,
* standardInvoiceText, statusChangeComment, and statusComment are filtered out.
*
* Nested objects (communicationDetails, organisation, address) are shallow-copied
* to prevent unintended mutations.
*
* @param crmPayer - Raw payer from CRM service
* @returns Payer compatible with checkout-api
*
* @example
* ```typescript
* const checkoutPayer = PayerAdapter.toCheckoutFormat(crmPayer);
* await checkoutService.complete({ payer: checkoutPayer, ... });
* ```
*/
static toCheckoutFormat(crmPayer: CrmPayer): Payer {
return {
reference: { id: crmPayer.id },
source: crmPayer.id,
payerType: crmPayer.payerType,
payerNumber: crmPayer.payerNumber,
payerStatus: crmPayer.payerStatus,
gender: crmPayer.gender,
title: crmPayer.title,
firstName: crmPayer.firstName,
lastName: crmPayer.lastName,
communicationDetails: crmPayer.communicationDetails
? { ...crmPayer.communicationDetails }
: undefined,
organisation: crmPayer.organisation
? { ...crmPayer.organisation }
: undefined,
address: crmPayer.address ? { ...crmPayer.address } : undefined,
};
}
/**
* Type guard for CRM payer response
*
* @param value - Value to check
* @returns true if value is a valid CRM payer with required fields
*/
static isValidCrmPayer(value: unknown): value is CrmPayer {
if (typeof value !== 'object' || value === null) return false;
const payer = value as CrmPayer;
return (
typeof payer.id === 'number' &&
(payer.payerNumber === undefined || typeof payer.payerNumber === 'string')
);
}
}

View File

@@ -1,136 +1,136 @@
import { Customer } from '@isa/crm/data-access';
import { ShippingAddressDTO as CrmShippingAddressDTO } from '@generated/swagger/crm-api';
import { ShippingAddressDTO as CheckoutShippingAddressDTO } from '@generated/swagger/checkout-api';
/**
* Adapter for converting CRM shipping address data to checkout-api format.
*
* Handles two distinct conversion scenarios:
* 1. **Separate Shipping Address Entity** (`fromCrmShippingAddress`): Converts a dedicated
* shipping address from CRM with `source` field, indicating a persistent entity.
* 2. **Customer Primary Address** (`fromCustomer`): Derives shipping address from customer's
* primary address without `source` field, indicating transient/derived data.
*
* **Key Differences:**
* - CRM ShippingAddressDTO: Filters out CRM-specific fields (type, validated, agentComment, etc.)
* - Customer Address: Uses customer ID for reference, omits `source` field
*/
export class ShippingAddressAdapter {
private static readonly ADAPTER_NAME = 'ShippingAddressAdapter';
/**
* Converts CRM-api ShippingAddressDTO to checkout-api format.
*
* @remarks
* Used when customer has separate delivery addresses stored in `customer.shippingAddresses[]`.
* The resulting address includes a `source` field, indicating it references a persistent
* shipping address entity in the CRM system.
*
* Filters out CRM-specific fields:
* - `type` (ShippingAddressType)
* - `validated` (validation flag)
* - `validationResult` (validation status code)
* - `agentComment` (internal notes)
* - `isDefault` (default address flag)
*
* @param address - Raw shipping address from CRM service
* @returns ShippingAddressDTO compatible with checkout-api, includes `source` field
*
* @example
* ```typescript
* const crmAddress = customer.shippingAddresses[0].data;
* const checkoutAddress = ShippingAddressAdapter.fromCrmShippingAddress(crmAddress);
* await checkoutService.complete({ shippingAddress: checkoutAddress, ... });
* ```
*/
static fromCrmShippingAddress(
address: CrmShippingAddressDTO,
): CheckoutShippingAddressDTO {
return {
reference: { id: address.id },
gender: address.gender,
title: address.title,
firstName: address.firstName,
lastName: address.lastName,
communicationDetails: address.communicationDetails
? { ...address.communicationDetails }
: undefined,
organisation: address.organisation
? { ...address.organisation }
: undefined,
address: address.address ? { ...address.address } : undefined,
source: address.id,
};
}
/**
* Converts Customer to checkout-api ShippingAddressDTO.
*
* @remarks
* Used when customer's primary address is used for shipping (e.g., B2B customers, staff,
* customer card holders without separate delivery addresses).
*
* The resulting address **does not include** a `source` field, indicating this is derived
* from the customer entity and not a separate persistent shipping address entity.
*
* Uses customer's primary address data (`customer.address`) and personal information.
*
* @param customer - Customer from CRM service
* @returns ShippingAddressDTO compatible with checkout-api, omits `source` field
*
* @example
* ```typescript
* const customer = await crmService.getCustomer(123);
* const checkoutAddress = ShippingAddressAdapter.fromCustomer(customer);
* await checkoutService.complete({ shippingAddress: checkoutAddress, ... });
* ```
*/
static fromCustomer(customer: Customer): CheckoutShippingAddressDTO {
return {
reference: { id: customer.id },
gender: customer.gender,
title: customer.title,
firstName: customer.firstName,
lastName: customer.lastName,
communicationDetails: customer.communicationDetails
? { ...customer.communicationDetails }
: undefined,
organisation: customer.organisation
? { ...customer.organisation }
: undefined,
address: customer.address ? { ...customer.address } : undefined,
};
}
/**
* Type guard for CRM shipping address response
*
* @param value - Value to check
* @returns true if value is a valid CRM shipping address with required fields
*/
static isValidCrmShippingAddress(
value: unknown,
): value is CrmShippingAddressDTO {
if (typeof value !== 'object' || value === null) return false;
const address = value as CrmShippingAddressDTO;
return typeof address.id === 'number';
}
/**
* Type guard for Customer
*
* @param value - Value to check
* @returns true if value is a valid Customer with required fields
*/
static isValidCustomer(value: unknown): value is Customer {
if (typeof value !== 'object' || value === null) return false;
const customer = value as Customer;
return (
typeof customer.id === 'number' &&
(customer.customerNumber === undefined ||
typeof customer.customerNumber === 'string')
);
}
}
import { Customer } from '@isa/crm/data-access';
import { ShippingAddress as CrmShippingAddress } from '@isa/crm/data-access';
import { ShippingAddress as CheckoutShippingAddress } from '@isa/checkout/data-access';
/**
* Adapter for converting CRM shipping address data to checkout-api format.
*
* Handles two distinct conversion scenarios:
* 1. **Separate Shipping Address Entity** (`fromCrmShippingAddress`): Converts a dedicated
* shipping address from CRM with `source` field, indicating a persistent entity.
* 2. **Customer Primary Address** (`fromCustomer`): Derives shipping address from customer's
* primary address without `source` field, indicating transient/derived data.
*
* **Key Differences:**
* - CRM ShippingAddressDTO: Filters out CRM-specific fields (type, validated, agentComment, etc.)
* - Customer Address: Uses customer ID for reference, omits `source` field
*/
export class ShippingAddressAdapter {
private static readonly ADAPTER_NAME = 'ShippingAddressAdapter';
/**
* Converts CRM-api ShippingAddressDTO to checkout-api format.
*
* @remarks
* Used when customer has separate delivery addresses stored in `customer.shippingAddresses[]`.
* The resulting address includes a `source` field, indicating it references a persistent
* shipping address entity in the CRM system.
*
* Filters out CRM-specific fields:
* - `type` (ShippingAddressType)
* - `validated` (validation flag)
* - `validationResult` (validation status code)
* - `agentComment` (internal notes)
* - `isDefault` (default address flag)
*
* @param address - Raw shipping address from CRM service
* @returns ShippingAddressDTO compatible with checkout-api, includes `source` field
*
* @example
* ```typescript
* const crmAddress = customer.shippingAddresses[0].data;
* const checkoutAddress = ShippingAddressAdapter.fromCrmShippingAddress(crmAddress);
* await checkoutService.complete({ shippingAddress: checkoutAddress, ... });
* ```
*/
static fromCrmShippingAddress(
address: CrmShippingAddress,
): CheckoutShippingAddress {
return {
reference: { id: address.id },
gender: address.gender,
title: address.title,
firstName: address.firstName,
lastName: address.lastName,
communicationDetails: address.communicationDetails
? { ...address.communicationDetails }
: undefined,
organisation: address.organisation
? { ...address.organisation }
: undefined,
address: address.address ? { ...address.address } : undefined,
source: address.id,
};
}
/**
* Converts Customer to checkout-api ShippingAddressDTO.
*
* @remarks
* Used when customer's primary address is used for shipping (e.g., B2B customers, staff,
* customer card holders without separate delivery addresses).
*
* The resulting address **does not include** a `source` field, indicating this is derived
* from the customer entity and not a separate persistent shipping address entity.
*
* Uses customer's primary address data (`customer.address`) and personal information.
*
* @param customer - Customer from CRM service
* @returns ShippingAddressDTO compatible with checkout-api, omits `source` field
*
* @example
* ```typescript
* const customer = await crmService.getCustomer(123);
* const checkoutAddress = ShippingAddressAdapter.fromCustomer(customer);
* await checkoutService.complete({ shippingAddress: checkoutAddress, ... });
* ```
*/
static fromCustomer(customer: Customer): CheckoutShippingAddress {
return {
reference: { id: customer.id },
gender: customer.gender,
title: customer.title,
firstName: customer.firstName,
lastName: customer.lastName,
communicationDetails: customer.communicationDetails
? { ...customer.communicationDetails }
: undefined,
organisation: customer.organisation
? { ...customer.organisation }
: undefined,
address: customer.address ? { ...customer.address } : undefined,
};
}
/**
* Type guard for CRM shipping address response
*
* @param value - Value to check
* @returns true if value is a valid CRM shipping address with required fields
*/
static isValidCrmShippingAddress(
value: unknown,
): value is CrmShippingAddress {
if (typeof value !== 'object' || value === null) return false;
const address = value as CrmShippingAddress;
return typeof address.id === 'number';
}
/**
* Type guard for Customer
*
* @param value - Value to check
* @returns true if value is a valid Customer with required fields
*/
static isValidCustomer(value: unknown): value is Customer {
if (typeof value !== 'object' || value === null) return false;
const customer = value as Customer;
return (
typeof customer.id === 'number' &&
(customer.customerNumber === undefined ||
typeof customer.customerNumber === 'string')
);
}
}

View File

@@ -0,0 +1,650 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { TestBed } from '@angular/core/testing';
import { ZodError } from 'zod';
import { ShoppingCartFacade } from './shopping-cart.facade';
import { CheckoutService, ShoppingCartService } from '../services';
import { Customer, Payer as CrmPayer } from '@isa/crm/data-access';
import { ShippingAddressDTO as CrmShippingAddressDTO } from '@generated/swagger/crm-api';
import { Order } from '../models';
describe('ShoppingCartFacade', () => {
let facade: ShoppingCartFacade;
let mockCheckoutService: {
complete: ReturnType<typeof vi.fn>;
};
let mockShoppingCartService: {
createShoppingCart: ReturnType<typeof vi.fn>;
getShoppingCart: ReturnType<typeof vi.fn>;
removeItem: ReturnType<typeof vi.fn>;
updateItem: ReturnType<typeof vi.fn>;
};
beforeEach(() => {
mockCheckoutService = {
complete: vi.fn(),
};
mockShoppingCartService = {
createShoppingCart: vi.fn(),
getShoppingCart: vi.fn(),
removeItem: vi.fn(),
updateItem: vi.fn(),
};
TestBed.configureTestingModule({
providers: [
ShoppingCartFacade,
{ provide: CheckoutService, useValue: mockCheckoutService },
{ provide: ShoppingCartService, useValue: mockShoppingCartService },
],
});
facade = TestBed.inject(ShoppingCartFacade);
});
describe('completeWithCrmData', () => {
it('should transform CRM data and complete order successfully', async () => {
// Arrange
const mockOrders: Order[] = [
{ id: 1, orderNumber: 'ORDER-001' } as Order,
];
mockCheckoutService.complete.mockResolvedValue(mockOrders);
const customer: Customer = {
id: 123,
customerNumber: 'CUST-123',
customerType: 8, // B2C
firstName: 'John',
lastName: 'Doe',
notificationChannels: 1,
features: [
{ key: 'FEATURE1', value: 'value1' },
{ key: 'FEATURE2', value: 'value2' },
],
} as Customer;
// Act
const result = await facade.completeWithCrmData({
shoppingCartId: 456,
customer,
});
// Assert
expect(result).toEqual(mockOrders);
expect(mockCheckoutService.complete).toHaveBeenCalledOnce();
expect(mockCheckoutService.complete).toHaveBeenCalledWith(
expect.objectContaining({
shoppingCartId: 456,
buyer: expect.objectContaining({
reference: { id: 123 },
source: 123,
buyerType: 8,
buyerNumber: 'CUST-123',
firstName: 'John',
lastName: 'Doe',
}),
notificationChannels: 1,
customerFeatures: { FEATURE1: 'FEATURE1', FEATURE2: 'FEATURE2' },
}),
undefined,
);
});
it('should transform CRM shipping address when provided', async () => {
// Arrange
const mockOrders: Order[] = [
{ id: 1, orderNumber: 'ORDER-001' } as Order,
];
mockCheckoutService.complete.mockResolvedValue(mockOrders);
const customer: Customer = {
id: 123,
customerNumber: 'CUST-123',
customerType: 8,
firstName: 'John',
lastName: 'Doe',
notificationChannels: 1,
features: [],
} as Customer;
const crmShippingAddress: CrmShippingAddressDTO = {
id: 789,
firstName: 'Jane',
lastName: 'Doe',
address: {
street: 'Main St',
streetNumber: '123',
zipCode: '12345',
city: 'Berlin',
country: 'DE',
},
};
// Act
await facade.completeWithCrmData({
shoppingCartId: 456,
customer,
crmShippingAddress,
});
// Assert
expect(mockCheckoutService.complete).toHaveBeenCalledWith(
expect.objectContaining({
shippingAddress: expect.objectContaining({
reference: { id: 789 },
source: 789,
firstName: 'Jane',
lastName: 'Doe',
address: expect.objectContaining({
street: 'Main St',
streetNumber: '123',
zipCode: '12345',
city: 'Berlin',
country: 'DE',
}),
}),
}),
undefined,
);
});
it('should transform CRM payer when provided', async () => {
// Arrange
const mockOrders: Order[] = [
{ id: 1, orderNumber: 'ORDER-001' } as Order,
];
mockCheckoutService.complete.mockResolvedValue(mockOrders);
const customer: Customer = {
id: 123,
customerNumber: 'CUST-123',
customerType: 8,
firstName: 'John',
lastName: 'Doe',
notificationChannels: 1,
features: [],
} as Customer;
const crmPayer: CrmPayer = {
id: 999,
payerNumber: 'PAY-999',
payerType: 16, // B2B
payerStatus: 0,
firstName: 'Billing',
lastName: 'Company',
} as CrmPayer;
// Act
await facade.completeWithCrmData({
shoppingCartId: 456,
customer,
crmPayer,
});
// Assert
expect(mockCheckoutService.complete).toHaveBeenCalledWith(
expect.objectContaining({
payer: expect.objectContaining({
reference: { id: 999 },
source: 999,
payerType: 16,
payerNumber: 'PAY-999',
payerStatus: 0,
firstName: 'Billing',
lastName: 'Company',
}),
}),
undefined,
);
});
it('should use provided notificationChannels over customer default', async () => {
// Arrange
mockCheckoutService.complete.mockResolvedValue([]);
const customer: Customer = {
id: 123,
customerNumber: 'CUST-123',
customerType: 8,
firstName: 'John',
lastName: 'Doe',
notificationChannels: 1, // Customer default
features: [],
} as Customer;
// Act
await facade.completeWithCrmData({
shoppingCartId: 456,
customer,
notificationChannels: 4, // Override
});
// Assert
expect(mockCheckoutService.complete).toHaveBeenCalledWith(
expect.objectContaining({
notificationChannels: 4, // Should use override
}),
undefined,
);
});
it('should use customer notificationChannels when not provided', async () => {
// Arrange
mockCheckoutService.complete.mockResolvedValue([]);
const customer: Customer = {
id: 123,
customerNumber: 'CUST-123',
customerType: 8,
firstName: 'John',
lastName: 'Doe',
notificationChannels: 2, // Customer default
features: [],
} as Customer;
// Act
await facade.completeWithCrmData({
shoppingCartId: 456,
customer,
});
// Assert
expect(mockCheckoutService.complete).toHaveBeenCalledWith(
expect.objectContaining({
notificationChannels: 2, // Should use customer default
}),
undefined,
);
});
it('should default to 1 when customer notificationChannels is undefined', async () => {
// Arrange
mockCheckoutService.complete.mockResolvedValue([]);
const customer: Customer = {
id: 123,
customerNumber: 'CUST-123',
customerType: 8,
firstName: 'John',
lastName: 'Doe',
notificationChannels: undefined, // Not set
features: [],
} as Customer;
// Act
await facade.completeWithCrmData({
shoppingCartId: 456,
customer,
});
// Assert
expect(mockCheckoutService.complete).toHaveBeenCalledWith(
expect.objectContaining({
notificationChannels: 1, // Should default to 1
}),
undefined,
);
});
it('should handle missing optional shipping address', async () => {
// Arrange
mockCheckoutService.complete.mockResolvedValue([]);
const customer: Customer = {
id: 123,
customerNumber: 'CUST-123',
customerType: 8,
firstName: 'John',
lastName: 'Doe',
notificationChannels: 1,
features: [],
} as Customer;
// Act
await facade.completeWithCrmData({
shoppingCartId: 456,
customer,
crmShippingAddress: undefined,
});
// Assert
expect(mockCheckoutService.complete).toHaveBeenCalledWith(
expect.objectContaining({
shippingAddress: undefined,
}),
undefined,
);
});
it('should handle missing optional payer', async () => {
// Arrange
mockCheckoutService.complete.mockResolvedValue([]);
const customer: Customer = {
id: 123,
customerNumber: 'CUST-123',
customerType: 8,
firstName: 'John',
lastName: 'Doe',
notificationChannels: 1,
features: [],
} as Customer;
// Act
await facade.completeWithCrmData({
shoppingCartId: 456,
customer,
crmPayer: undefined,
});
// Assert
expect(mockCheckoutService.complete).toHaveBeenCalledWith(
expect.objectContaining({
payer: undefined,
}),
undefined,
);
});
it('should pass abort signal to underlying service', async () => {
// Arrange
mockCheckoutService.complete.mockResolvedValue([]);
const customer: Customer = {
id: 123,
customerNumber: 'CUST-123',
customerType: 8,
firstName: 'John',
lastName: 'Doe',
notificationChannels: 1,
features: [],
} as Customer;
const abortController = new AbortController();
// Act
await facade.completeWithCrmData(
{
shoppingCartId: 456,
customer,
},
abortController.signal,
);
// Assert
expect(mockCheckoutService.complete).toHaveBeenCalledWith(
expect.any(Object),
abortController.signal,
);
});
it('should extract customer features correctly', async () => {
// Arrange
mockCheckoutService.complete.mockResolvedValue([]);
const customer: Customer = {
id: 123,
customerNumber: 'CUST-123',
customerType: 8,
firstName: 'John',
lastName: 'Doe',
notificationChannels: 1,
features: [
{ key: 'FEATURE_A', value: 'some value' },
{ key: 'FEATURE_B', value: 'another value' },
{ key: 'FEATURE_C', value: 'third value' },
],
} as Customer;
// Act
await facade.completeWithCrmData({
shoppingCartId: 456,
customer,
});
// Assert
expect(mockCheckoutService.complete).toHaveBeenCalledWith(
expect.objectContaining({
customerFeatures: {
FEATURE_A: 'FEATURE_A',
FEATURE_B: 'FEATURE_B',
FEATURE_C: 'FEATURE_C',
},
}),
undefined,
);
});
it('should handle empty customer features', async () => {
// Arrange
mockCheckoutService.complete.mockResolvedValue([]);
const customer: Customer = {
id: 123,
customerNumber: 'CUST-123',
customerType: 8,
firstName: 'John',
lastName: 'Doe',
notificationChannels: 1,
features: [],
} as Customer;
// Act
await facade.completeWithCrmData({
shoppingCartId: 456,
customer,
});
// Assert
expect(mockCheckoutService.complete).toHaveBeenCalledWith(
expect.objectContaining({
customerFeatures: {},
}),
undefined,
);
});
it('should throw error for invalid shopping cart ID', async () => {
// Arrange
const customer: Customer = {
id: 123,
customerNumber: 'CUST-123',
customerType: 8,
firstName: 'John',
lastName: 'Doe',
notificationChannels: 1,
features: [],
} as Customer;
// Act & Assert
try {
await facade.completeWithCrmData({
shoppingCartId: -1, // Invalid ID
customer,
});
expect.fail('Should have thrown ZodError');
} catch (error) {
expect(error).toBeInstanceOf(ZodError);
}
});
it('should throw error for invalid customer data', async () => {
// Act & Assert
try {
await facade.completeWithCrmData({
shoppingCartId: 456,
customer: { invalid: 'data' } as any, // Invalid customer
});
expect.fail('Should have thrown ZodError');
} catch (error) {
expect(error).toBeInstanceOf(ZodError);
}
});
it('should propagate errors from checkout service', async () => {
// Arrange
const checkoutError = new Error('Checkout failed');
mockCheckoutService.complete.mockRejectedValue(checkoutError);
const customer: Customer = {
id: 123,
customerNumber: 'CUST-123',
customerType: 8,
firstName: 'John',
lastName: 'Doe',
notificationChannels: 1,
features: [],
} as Customer;
// Act & Assert
await expect(
facade.completeWithCrmData({
shoppingCartId: 456,
customer,
}),
).rejects.toThrow('Checkout failed');
});
it('should handle all parameters together', async () => {
// Arrange
const mockOrders: Order[] = [
{ id: 1, orderNumber: 'ORDER-001' } as Order,
{ id: 2, orderNumber: 'ORDER-002' } as Order,
];
mockCheckoutService.complete.mockResolvedValue(mockOrders);
const customer: Customer = {
id: 123,
customerNumber: 'CUST-123',
customerType: 16, // B2B
firstName: 'John',
lastName: 'Doe',
gender: 2,
title: 'Mr.',
dateOfBirth: '1980-01-15',
notificationChannels: 4,
features: [{ key: 'B2B_CUSTOMER', value: 'true' }],
communicationDetails: {
email: 'john.doe@example.com',
phone: '+49 123 456789',
},
organisation: {
name: 'ACME Corp',
},
address: {
street: 'Main St',
streetNumber: '42',
zipCode: '12345',
city: 'Berlin',
country: 'DE',
},
} as Customer;
const crmShippingAddress: CrmShippingAddressDTO = {
id: 789,
firstName: 'Jane',
lastName: 'Doe',
address: {
street: 'Delivery St',
streetNumber: '99',
zipCode: '54321',
city: 'Hamburg',
country: 'DE',
},
};
const crmPayer: CrmPayer = {
id: 999,
payerNumber: 'PAY-999',
payerType: 16,
payerStatus: 0,
firstName: 'Billing',
lastName: 'Department',
address: {
street: 'Billing St',
streetNumber: '1',
zipCode: '11111',
city: 'Munich',
country: 'DE',
},
} as CrmPayer;
const abortController = new AbortController();
// Act
const result = await facade.completeWithCrmData(
{
shoppingCartId: 456,
customer,
crmShippingAddress,
crmPayer,
notificationChannels: 8,
},
abortController.signal,
);
// Assert
expect(result).toEqual(mockOrders);
expect(mockCheckoutService.complete).toHaveBeenCalledWith(
expect.objectContaining({
shoppingCartId: 456,
buyer: expect.objectContaining({
reference: { id: 123 },
source: 123,
buyerType: 16,
buyerNumber: 'CUST-123',
firstName: 'John',
lastName: 'Doe',
}),
shippingAddress: expect.objectContaining({
reference: { id: 789 },
source: 789,
firstName: 'Jane',
lastName: 'Doe',
}),
payer: expect.objectContaining({
reference: { id: 999 },
source: 999,
payerType: 16,
payerNumber: 'PAY-999',
}),
notificationChannels: 8,
customerFeatures: { B2B_CUSTOMER: 'B2B_CUSTOMER' },
}),
abortController.signal,
);
});
});
describe('complete', () => {
it('should delegate to checkout service', async () => {
// Arrange
const mockOrders: Order[] = [
{ id: 1, orderNumber: 'ORDER-001' } as Order,
];
mockCheckoutService.complete.mockResolvedValue(mockOrders);
const params = {
shoppingCartId: 123,
buyer: { buyerType: 8 } as any,
notificationChannels: 1 as any,
customerFeatures: {},
};
// Act
const result = await facade.complete(params);
// Assert
expect(result).toEqual(mockOrders);
expect(mockCheckoutService.complete).toHaveBeenCalledWith(
params,
undefined,
);
});
});
});

View File

@@ -1,11 +1,22 @@
import { inject, Injectable } from '@angular/core';
import { ShoppingCartService, CheckoutService } from '../services';
import {
CompleteCheckoutParams,
ShoppingCartService,
CheckoutService,
CheckoutMetadataService,
} from '../services';
import {
CompleteOrderParams,
RemoveShoppingCartItemParams,
UpdateShoppingCartItemParams,
CompleteCrmOrderParamsSchema,
CompleteCrmOrderParams,
} from '../schemas';
import { Order } from '../models';
import {
CustomerAdapter,
ShippingAddressAdapter,
PayerAdapter,
} from '../adapters';
@Injectable({ providedIn: 'root' })
export class ShoppingCartFacade {
@@ -31,10 +42,87 @@ export class ShoppingCartFacade {
return this.#shoppingCartService.updateItem(params);
}
complete(
params: CompleteCheckoutParams,
/**
* Complete checkout and create orders.
*
* @param params - Complete checkout parameters
* @param abortSignal - Optional AbortSignal for cancellation
* @returns Promise<number> - Checkout Id
* @throws CheckoutCompletionError - If validation or order creation fails
*/
async complete(
params: CompleteOrderParams,
abortSignal?: AbortSignal,
): Promise<Order[]> {
return this.#checkoutService.complete(params, abortSignal);
): Promise<number> {
// Complete checkout preparation
return await this.#checkoutService.complete(params, abortSignal);
}
/**
* Complete order with CRM data.
*
* @remarks
* Accepts raw CRM data (customer, shipping address, payer) and transforms it
* to checkout format before completing the order. This method encapsulates
* the business logic of converting CRM entities to checkout entities.
*
* **Transformation Steps:**
* 1. Validates input parameters with Zod schema
* 2. Transforms customer to buyer using CustomerAdapter.toBuyer()
* 3. Transforms CRM shipping address (if provided) using ShippingAddressAdapter
* 4. Transforms CRM payer (if provided) using PayerAdapter
* 5. Extracts customer features using CustomerAdapter.extractCustomerFeatures()
* 6. Delegates to complete() with transformed data
*
* @param params - CRM order completion parameters
* @param abortSignal - Optional AbortSignal for cancellation
* @returns Promise<number> - Checkout Id
* @throws CheckoutCompletionError - If validation or order creation fails
*
* @example
* ```typescript
* const customer = await customerResource.value();
* const orders = await facade.completeWithCrmData({
* shoppingCartId: 123,
* customer,
* crmShippingAddress: customer.shippingAddresses[0].data,
* crmPayer: customer.payers[0].payer.data,
* notificationChannels: customer.notificationChannels ?? 1,
* });
* ```
*/
completeWithCrmData(
params: CompleteCrmOrderParams,
abortSignal?: AbortSignal,
): Promise<number> {
// Validate input parameters
const validatedParams = CompleteCrmOrderParamsSchema.parse(params);
const {
shoppingCartId,
crmCustomer,
crmShippingAddress,
crmPayer,
notificationChannels,
specialComment,
} = validatedParams;
// Build checkout parameters
const checkoutParams: CompleteOrderParams = {
shoppingCartId,
buyer: CustomerAdapter.toBuyer(crmCustomer),
shippingAddress: ShippingAddressAdapter.fromCrmShippingAddress(
crmShippingAddress as any,
),
customerFeatures: CustomerAdapter.extractCustomerFeatures(
crmCustomer as any,
),
payer: PayerAdapter.toCheckoutFormat(crmPayer as any),
notificationChannels,
specialComment,
};
// Delegate to existing complete method
return this.complete(checkoutParams, abortSignal);
}
}

View File

@@ -1,95 +1,95 @@
import { EntityDTOContainerOfShoppingCartItemDTO } from '@generated/swagger/checkout-api';
import {
CustomerTypeAnalysis,
OrderOptionsAnalysis,
ShoppingCartItem,
} from '../models';
/**
* Analyzes shopping cart items to determine which order types are present.
*
* @remarks
* Pure function that examines the orderType feature of each item to identify:
* - Take away orders (Rücklage)
* - Pick up orders (Abholung)
* - Download orders
* - Standard delivery (Versand)
* - Digital delivery (DIG-Versand)
* - B2B delivery (B2B-Versand)
*
* Used during checkout completion to determine payment requirements and
* destination update needs.
*
* @param items - Shopping cart items to analyze
* @returns Analysis result with boolean flags for each order type and unwrapped items
*
* @example
* ```typescript
* const items = [
* { data: { features: { orderType: 'Versand' } } },
* { data: { features: { orderType: 'Abholung' } } }
* ];
* const analysis = analyzeOrderOptions(items);
* // { hasDelivery: true, hasPickUp: true, hasTakeAway: false, ... }
* ```
*/
export function analyzeOrderOptions(
items: EntityDTOContainerOfShoppingCartItemDTO[],
): OrderOptionsAnalysis {
return {
hasTakeAway: items.some(
(item) => item.data?.features?.['orderType'] === 'Rücklage',
),
hasPickUp: items.some(
(item) => item.data?.features?.['orderType'] === 'Abholung',
),
hasDownload: items.some(
(item) => item.data?.features?.['orderType'] === 'Download',
),
hasDelivery: items.some(
(item) => item.data?.features?.['orderType'] === 'Versand',
),
hasDigDelivery: items.some(
(item) => item.data?.features?.['orderType'] === 'DIG-Versand',
),
hasB2BDelivery: items.some(
(item) => item.data?.features?.['orderType'] === 'B2B-Versand',
),
items: items.map((d) => d.data as ShoppingCartItem),
};
}
/**
* Analyzes customer features to determine customer type characteristics.
*
* @remarks
* Pure function that converts feature flags into typed boolean properties:
* - isOnline: webshop customer
* - isGuest: guest account
* - isB2B: business customer
* - hasCustomerCard: loyalty card holder (Pay4More)
* - isStaff: employee/staff member
*
* Used during checkout to determine payment requirements and payer necessity.
*
* @param features - Customer feature flags (e.g., { webshop: 'webshop', b2b: 'b2b' })
* @returns Analysis result with boolean flags for each customer type
*
* @example
* ```typescript
* const features = { webshop: 'webshop', p4mUser: 'p4mUser' };
* const analysis = analyzeCustomerTypes(features);
* // { isOnline: true, hasCustomerCard: true, isB2B: false, ... }
* ```
*/
export function analyzeCustomerTypes(
features: Record<string, string>,
): CustomerTypeAnalysis {
return {
isOnline: !!features?.['webshop'],
isGuest: !!features?.['guest'],
isB2B: !!features?.['b2b'],
hasCustomerCard: !!features?.['p4mUser'],
isStaff: !!features?.['staff'],
};
}
import {
CustomerTypeAnalysis,
OrderOptionsAnalysis,
ShoppingCartItem,
} from '../models';
import { EntityContainer } from '@isa/common/data-access';
/**
* Analyzes shopping cart items to determine which order types are present.
*
* @remarks
* Pure function that examines the orderType feature of each item to identify:
* - Take away orders (Rücklage)
* - Pick up orders (Abholung)
* - Download orders
* - Standard delivery (Versand)
* - Digital delivery (DIG-Versand)
* - B2B delivery (B2B-Versand)
*
* Used during checkout completion to determine payment requirements and
* destination update needs.
*
* @param items - Shopping cart items to analyze
* @returns Analysis result with boolean flags for each order type and unwrapped items
*
* @example
* ```typescript
* const items = [
* { data: { features: { orderType: 'Versand' } } },
* { data: { features: { orderType: 'Abholung' } } }
* ];
* const analysis = analyzeOrderOptions(items);
* // { hasDelivery: true, hasPickUp: true, hasTakeAway: false, ... }
* ```
*/
export function analyzeOrderOptions(
items: EntityContainer<ShoppingCartItem>[],
): OrderOptionsAnalysis {
return {
hasTakeAway: items.some(
(item) => item.data?.features?.['orderType'] === 'Rücklage',
),
hasPickUp: items.some(
(item) => item.data?.features?.['orderType'] === 'Abholung',
),
hasDownload: items.some(
(item) => item.data?.features?.['orderType'] === 'Download',
),
hasDelivery: items.some(
(item) => item.data?.features?.['orderType'] === 'Versand',
),
hasDigDelivery: items.some(
(item) => item.data?.features?.['orderType'] === 'DIG-Versand',
),
hasB2BDelivery: items.some(
(item) => item.data?.features?.['orderType'] === 'B2B-Versand',
),
items: items.map((d) => d.data as ShoppingCartItem),
};
}
/**
* Analyzes customer features to determine customer type characteristics.
*
* @remarks
* Pure function that converts feature flags into typed boolean properties:
* - isOnline: webshop customer
* - isGuest: guest account
* - isB2B: business customer
* - hasCustomerCard: loyalty card holder (Pay4More)
* - isStaff: employee/staff member
*
* Used during checkout to determine payment requirements and payer necessity.
*
* @param features - Customer feature flags (e.g., { webshop: 'webshop', b2b: 'b2b' })
* @returns Analysis result with boolean flags for each customer type
*
* @example
* ```typescript
* const features = { webshop: 'webshop', p4mUser: 'p4mUser' };
* const analysis = analyzeCustomerTypes(features);
* // { isOnline: true, hasCustomerCard: true, isB2B: false, ... }
* ```
*/
export function analyzeCustomerTypes(
features: Record<string, string>,
): CustomerTypeAnalysis {
return {
isOnline: !!features?.['webshop'],
isGuest: !!features?.['guest'],
isB2B: !!features?.['b2b'],
hasCustomerCard: !!features?.['p4mUser'],
isStaff: !!features?.['staff'],
};
}

View File

@@ -1,18 +0,0 @@
import { AvailabilityType as GeneratedAvailabilityType } from '@generated/swagger/checkout-api';
export type AvailabilityType = GeneratedAvailabilityType;
// Helper constants for easier usage
export const AvailabilityType = {
Unknown: 0,
InStock: 1,
OutOfStock: 2,
PreOrder: 32,
BackOrder: 256,
Discontinued: 512,
OnRequest: 1024,
SpecialOrder: 2048,
DigitalDelivery: 4096,
PartialStock: 8192,
ExpectedDelivery: 16384,
} as const;

View File

@@ -1,3 +0,0 @@
import { AvailabilityDTO } from '@generated/swagger/checkout-api';
export type Availability = AvailabilityDTO;

View File

@@ -1,11 +1,10 @@
import { BranchType } from '@generated/swagger/checkout-api';
export type BranchTypeEnum = BranchType;
export const BranchTypeEnum = {
NotSet: 0,
Store: 1,
WebStore: 2,
CallCenter: 4,
Headquarter: 8,
} as const;
export const BranchType = {
NotSet: 0,
Store: 1,
WebStore: 2,
CallCenter: 4,
Headquarter: 8,
Warehouse: 16,
} as const;
export type BranchType = (typeof BranchType)[keyof typeof BranchType];

View File

@@ -1,3 +0,0 @@
import { BranchDTO } from '@generated/swagger/checkout-api';
export type Branch = BranchDTO;

View File

@@ -1,6 +0,0 @@
import { BuyerDTO } from '@generated/swagger/checkout-api';
import { BuyerType } from '@isa/common/data-access';
export type Buyer = BuyerDTO & {
buyerType: BuyerType;
};

View File

@@ -1,28 +1,21 @@
export * from './availability-type';
export * from './availability';
export * from './branch';
export * from './buyer';
export * from './branch-type';
export * from './campaign';
export * from './checkout-item';
export * from './checkout';
export * from './customer-type-analysis';
export * from './destination';
export * from './gender';
export * from './loyalty';
export * from './ola-availability';
export * from './order-options';
export * from './order-type';
export * from './order';
export * from './payer';
export * from './price';
export * from './product';
export * from './promotion';
export * from './reward-selection-item';
export * from './shipping-address';
export * from './shipping-target';
export * from './shopping-cart-item';
export * from './supplier';
export * from './shopping-cart';
export * from './update-shopping-cart-item';
export * from './vat-type';
export * from './reward-selection-item';
export * from './branch-type';

View File

@@ -1,6 +0,0 @@
import { PayerDTO } from '@generated/swagger/checkout-api';
import { PayerType } from '@isa/common/data-access';
export type Payer = PayerDTO & {
payerType: PayerType;
};

View File

@@ -1,3 +0,0 @@
import { SupplierDTO } from '@generated/swagger/checkout-api';
export type Supplier = SupplierDTO;

View File

@@ -1,56 +1,67 @@
import { z } from 'zod';
import {
AvailabilityDTOSchema,
CampaignDTOSchema,
LoyaltyDTOSchema,
ProductDTOSchema,
PromotionDTOSchema,
PriceSchema,
EntityDTOContainerOfDestinationDTOSchema,
ItemTypeSchema,
PriceValueSchema,
} from './base-schemas';
import { EntityContainerSchema } from '@isa/common/data-access';
import { AvailabilitySchema } from './availability.schema';
import { CampaignSchema } from './campaign.schema';
import { DestinationSchema } from './destination.schema';
import { ItemTypeSchema } from './item-type.schema';
import { LoyaltySchema } from './loyalty.schema';
import { PriceFlatSchema } from './price-flat.schema';
import { ProductSchema } from './product.schema';
import { PromotionSchema } from './promotion.schema';
const AddToShoppingCartDefaultSchema = z.object({
availability: AvailabilityDTOSchema,
campaign: CampaignDTOSchema,
destination: EntityDTOContainerOfDestinationDTOSchema,
itemType: ItemTypeSchema,
product: ProductDTOSchema,
promotion: PromotionDTOSchema,
quantity: z.number().int().positive(),
retailPrice: PriceSchema,
shopItemId: z.number().int().positive().optional(),
// Base schema for all add to shopping cart items
const AddToShoppingCartBaseSchema = z.object({
availability: AvailabilitySchema.describe('Availability'),
campaign: CampaignSchema.describe('Campaign information').optional(),
destination: EntityContainerSchema(DestinationSchema).describe(
'Destination information',
),
itemType: ItemTypeSchema.describe('Item type').optional(),
loyalty: LoyaltySchema.describe('Loyalty information').optional(),
product: ProductSchema.describe('Product').optional(),
promotion: PromotionSchema.describe('Promotion information').optional(),
quantity: z.number().int().positive().describe('Quantity'),
retailPrice: PriceFlatSchema.describe('Retail price').optional(),
shopItemId: z
.number()
.int()
.positive()
.describe('ShopItem identifier')
.optional(),
});
// When loyalty points are used the price value must be 0 and promotion is not allowed
// and availability must not contain a price
// and loyalty must be present
const AddToShoppingCartWithRedemptionPointsSchema =
AddToShoppingCartDefaultSchema.omit({
availability: true,
promotion: true,
}).extend({
availability: AvailabilityDTOSchema.unwrap()
.omit({ price: true })
.extend({
price: PriceSchema.unwrap()
.omit({ value: true })
.extend({
value: PriceValueSchema.omit({ value: true }).extend({ value: z.literal(0) }),
}),
}),
loyalty: LoyaltyDTOSchema,
});
const AddToShoppingCartSchema = z.union([
AddToShoppingCartDefaultSchema,
AddToShoppingCartWithRedemptionPointsSchema,
]);
// Apply business rules validation
const AddToShoppingCartSchema = AddToShoppingCartBaseSchema.refine(
(data) => {
// When loyalty points are used, promotion must not be present
if (data.loyalty && data.promotion) {
return false;
}
return true;
},
{
message: 'Promotion is not allowed when using loyalty points',
},
).refine(
(data) => {
// When loyalty points are used, price value should be 0
if (data.loyalty && data.availability?.price?.value?.value !== 0) {
return false;
}
return true;
},
{
message: 'Price value must be 0 when using loyalty points for redemption',
},
);
export const AddItemToShoppingCartParamsSchema = z.object({
shoppingCartId: z.number().int().positive(),
items: z.array(AddToShoppingCartSchema).min(1),
shoppingCartId: z
.number()
.int()
.positive()
.describe('Shopping cart identifier'),
items: z.array(AddToShoppingCartSchema).min(1).describe('List of items'),
});
export type AddItemToShoppingCartParams = z.infer<

View File

@@ -0,0 +1,60 @@
import { z } from 'zod';
export const AllergeneType = {
NotSet: 0,
SulfurDioxide: 1,
EggWhite: 2,
EggYolk: 4,
Egg: 6,
Shellfish: 8,
Fish: 16,
Mollusc: 32,
Celery: 64,
Mustard: 128,
Sesame: 256,
MilkProtein: 512,
Lactose: 1024,
Milk: 1536,
Nuts: 2048,
Hazelnut: 6144,
Almond: 10240,
Walnut: 18432,
Cashew: 34816,
Pecan: 67584,
BrazilNut: 133120,
Pistachio: 264192,
Macadamia: 526336,
Gluten: 1048576,
Wheat: 3145728,
Rye: 5242880,
Oat: 9437184,
Barley: 17825792,
Spelt: 34603008,
Kamut: 68157440,
Emmer: 135266304,
Pulse: 268435456,
Peanuts: 805306368,
Soy: 1342177280,
Chickpea: 2415919104,
Lentil: 4563402752,
Peavine: 8858370048,
Lupin: 17448304640,
Pea: 34628173824,
Bean: 68987912192,
Cinnamon: 137438953472,
} as const;
const ALL_FLAGS = Object.values(AllergeneType).reduce<number>(
(a, b) => a | b,
0,
);
export const AllergeneTypeSchema = z
.nativeEnum(AllergeneType)
.refine(
(val) => (val & ~ALL_FLAGS) === 0,
() => ({ message: 'Invalid allergene type combination' }),
)
.describe('Allergen type classification for food items');
export type AllergeneType = z.infer<typeof AllergeneTypeSchema>;

View File

@@ -0,0 +1,19 @@
import { z } from 'zod';
export const AvailabilityType = {
NotSet: 0,
NotAvailable: 1,
PrebookAtBuyer: 2,
PrebookAtRetailer: 32,
PrebookAtSupplier: 256,
TemporarilyNotAvailable: 512,
Available: 1024,
OnDemand: 2048,
AtProductionDate: 4096,
Discontinued: 8192,
EndOfLife: 16384,
} as const;
export const AvailabilityTypeSchema = z.nativeEnum(AvailabilityType).describe('Availability type');
export type AvailabilityType = z.infer<typeof AvailabilityTypeSchema>;

View File

@@ -0,0 +1,35 @@
import {
EntityContainerSchema,
PriceSchema,
TouchBaseSchema,
} from '@isa/common/data-access';
import { z } from 'zod';
import { AvailabilityTypeSchema } from './availability-type.schema';
import { DateRangeSchema } from './data-range.schema';
import { LogisticianSchema } from './logistician.schema';
import { SupplierSchema } from './supplier.schema';
export const AvailabilitySchema = z
.object({
availabilityType: AvailabilityTypeSchema.describe('Availability type').optional(),
estimatedDelivery: DateRangeSchema.describe('Estimated delivery').optional(),
estimatedShippingDate: z.string().describe('EstimatedShipping date').optional(),
inStock: z.number().describe('Whether item is in stock').optional(),
isPrebooked: z.boolean().describe('Whether prebooked').optional(),
lastRequest: z.string().describe('Last request').optional(),
logistician: EntityContainerSchema(LogisticianSchema).describe('Logistician information').optional(),
price: PriceSchema.describe('Price information').optional(),
requestReference: z.string().describe('Request reference').optional(),
shopItem: EntityContainerSchema(z.any()).describe('Shop item information').optional(),
ssc: z.string().describe('Ssc').optional(),
sscText: z.string().describe('Ssc text').optional(),
supplier: EntityContainerSchema(SupplierSchema).describe('Supplier information').optional(),
supplierInfo: z.string().describe('Supplier info').optional(),
supplierProductNumber: z.string().describe('SupplierProduct number').optional(),
supplierSSC: z.string().describe('Supplier s s c').optional(),
supplierSSCText: z.string().describe('Supplier s s c text').optional(),
supplyChannel: z.string().describe('Supply channel').optional(),
})
.extend(TouchBaseSchema.shape);
export type Availability = z.infer<typeof AvailabilitySchema>;

View File

@@ -0,0 +1,22 @@
import { z } from 'zod';
export const Avoirdupois = {
NotSet: 0,
Nanogramm: 1,
Mikrogramm: 2,
Milligramm: 4,
Gramm: 8,
Kilogramm: 16,
MetrischeTonne: 32,
Zentner: 64,
DoppelZentner: 128,
MetrischesKarat: 256,
Grain: 512,
Dram: 1024,
Ounce: 2048,
Pound: 4096,
} as const;
export const AvoirdupoisSchema = z.nativeEnum(Avoirdupois).describe('Avoirdupois');
export type Avoirdupois = z.infer<typeof AvoirdupoisSchema>;

View File

@@ -1,338 +1,13 @@
import { z } from 'zod';
import { AvailabilityType, Gender, ShippingTarget } from '../models';
import { OrderType } from '../models';
import { BranchTypeEnum } from '../models';
import {
AddressSchema,
CommunicationDetailsSchema,
EntityContainerSchema,
OrganisationSchema,
PriceValueSchema,
VatTypeSchema,
VatValueSchema,
} from '@isa/common/data-access';
// Re-export PriceValueSchema for other checkout schemas
export { PriceValueSchema } from '@isa/common/data-access';
// ItemType from generated API - it's a numeric bitwise enum
export const ItemTypeSchema = z.number().optional();
// Enum schemas based on generated swagger types
export const AvailabilityTypeSchema = z.nativeEnum(AvailabilityType).optional();
export const ShippingTargetSchema = z.nativeEnum(ShippingTarget).optional();
export const GenderSchema = z.nativeEnum(Gender).optional();
export const OrderTypeSchema = z.nativeEnum(OrderType).optional();
// Base schemas for nested objects
export const DateRangeSchema = z
.object({
start: z.string().optional(),
stop: z.string().optional(),
})
.optional();
// export const OrganisationSchema = z
// .object({
// name: z.string().optional(),
// taxNumber: z.string().optional(),
// })
// .optional();
// DTO Schemas based on generated API types
export const TouchedBaseSchema = z.object({
id: z.number().optional(),
createdAt: z.string().optional(),
modifiedAt: z.string().optional(),
});
export const PriceDTOSchema = z
.object({
id: z.number().optional(),
createdAt: z.string().optional(),
modifiedAt: z.string().optional(),
value: PriceValueSchema.optional(),
vat: VatValueSchema.optional(),
})
.optional();
export const PriceSchema = z
.object({
currency: z.string().optional(),
currencySymbol: z.string().optional(),
validFrom: z.string().optional(),
value: z.number(),
vatInPercent: z.number().optional(),
vatType: VatTypeSchema.optional(),
vatValue: z.number().optional(),
})
.optional();
export const CampaignDTOSchema = z
.object({
id: z.number().optional(),
createdAt: z.string().optional(),
modifiedAt: z.string().optional(),
code: z.string().optional(),
label: z.string().optional(),
type: z.string().optional(),
value: z.number().optional(),
})
.optional();
export const PromotionDTOSchema = z
.object({
id: z.number().optional(),
createdAt: z.string().optional(),
modifiedAt: z.string().optional(),
code: z.string().optional(),
label: z.string().optional(),
type: z.string().optional(),
value: z.number().optional(),
})
.optional();
export const LoyaltyDTOSchema = z
.object({
id: z.number().optional(),
createdAt: z.string().optional(),
modifiedAt: z.string().optional(),
code: z.string().optional(),
label: z.string().optional(),
type: z.string().optional(),
value: z.number().optional(),
})
.optional();
export const ProductDTOSchema = z
.object({
id: z.number().optional(),
createdAt: z.string().optional(),
modifiedAt: z.string().optional(),
additionalName: z.string().optional(),
catalogProductNumber: z.string().optional(),
contributors: z.string().optional(),
ean: z.string().optional(),
edition: z.string().optional(),
format: z.string().optional(),
formatDetail: z.string().optional(),
locale: z.string().optional(),
manufacturer: z.string().optional(),
name: z.string().optional(),
productGroup: z.string().optional(),
productGroupDetails: z.string().optional(),
publicationDate: z.string().optional(),
serial: z.string().optional(),
supplierProductNumber: z.string().optional(),
volume: z.string().optional(),
})
.optional();
export const LogisticianDTOSchema = z
.object({
id: z.number().optional(),
createdAt: z.string().optional(),
modifiedAt: z.string().optional(),
gln: z.string().optional(),
logisticianNumber: z.string().optional(),
name: z.string().optional(),
})
.optional();
export const SupplierDTOSchema = z
.object({
id: z.number().optional(),
createdAt: z.string().optional(),
modifiedAt: z.string().optional(),
key: z.string().optional(),
name: z.string().optional(),
supplierNumber: z.string().optional(),
supplierType: z
.union([z.literal(0), z.literal(1), z.literal(2), z.literal(4)])
.optional(),
})
.optional();
export const AvailabilityDTOSchema = z
.object({
id: z.number().optional(),
createdAt: z.string().optional(),
modifiedAt: z.string().optional(),
availabilityType: AvailabilityTypeSchema,
estimatedDelivery: DateRangeSchema,
estimatedShippingDate: z.string().optional(),
inStock: z.number().optional(),
isPrebooked: z.boolean().optional(),
lastRequest: z.string().optional(),
logistician: EntityContainerSchema(LogisticianDTOSchema).optional(),
price: PriceDTOSchema,
requestReference: z.string().optional(),
ssc: z.string().optional(),
sscText: z.string().optional(),
supplier: EntityContainerSchema(SupplierDTOSchema).optional(),
supplierInfo: z.string().optional(),
supplierProductNumber: z.string().optional(),
supplierSSC: z.string().optional(),
supplierSSCText: z.string().optional(),
supplyChannel: z.string().optional(),
})
.optional();
// LabelDTO schema for the EntityContainerSchema
export const LabelDTOSchema = z
.object({
id: z.number().optional(),
createdAt: z.string().optional(),
modifiedAt: z.string().optional(),
label: z.string().optional(),
})
.optional();
export const BranchDTOSchema: z.ZodOptional<z.ZodObject<any>> = z
.object({
id: z.number().optional(),
createdAt: z.string().optional(),
modifiedAt: z.string().optional(),
address: AddressSchema.optional(),
branchNumber: z.string().optional(),
branchType: z.nativeEnum(BranchTypeEnum).optional(),
isDefault: z.string().optional(),
isOnline: z.boolean().optional(),
isOrderingEnabled: z.boolean().optional(),
isShippingEnabled: z.boolean().optional(),
key: z.string().optional(),
label: EntityContainerSchema(LabelDTOSchema),
name: z.string().optional(),
parent: EntityContainerSchema(
z.lazy((): z.ZodOptional<z.ZodObject<any>> => BranchDTOSchema),
).optional(),
shortName: z.string().optional(),
})
.optional();
export const BranchSchema = BranchDTOSchema;
export const DestinationDTOSchema = z
.object({
id: z.number().optional(),
createdAt: z.string().optional(),
modifiedAt: z.string().optional(),
address: AddressSchema.optional(),
communicationDetails: CommunicationDetailsSchema.optional(),
firstName: z.string().optional(),
gender: GenderSchema,
lastName: z.string().optional(),
locale: z.string().optional(),
organisation: OrganisationSchema.optional(),
title: z.string().optional(),
target: ShippingTargetSchema.optional(),
targetBranch: EntityContainerSchema(BranchSchema).optional(),
})
.refine(
(data) => {
// targetBranch is only optional if target is 2 (Delivery)
// For other targets (like Branch = 1), targetBranch should be required
if (data?.target !== undefined && data.target !== 2) {
return data?.targetBranch !== undefined;
}
return true;
},
{
message: 'targetBranch is required when target is not Delivery (2)',
path: ['targetBranch'],
},
)
.optional();
export const EntityDTOContainerOfDestinationDTOSchema = z
.object({
id: z.number().optional(),
data: DestinationDTOSchema,
})
.optional();
// NotificationChannel is a bitwise enum
export const NotificationChannelSchema = z.union([
z.literal(0),
z.literal(1),
z.literal(2),
z.literal(4),
z.literal(8),
z.literal(16),
]);
// EntityReferenceDTO schema
export const EntityReferenceDTOSchema = TouchedBaseSchema.extend({
pId: z.string().optional(),
reference: z
.object({
id: z.number().optional(),
pId: z.string().optional(),
})
.optional(),
source: z.number().optional(),
});
// AddresseeWithReferenceDTO schema
export const AddresseeWithReferenceDTOSchema = EntityReferenceDTOSchema.extend({
address: AddressSchema.optional(),
communicationDetails: CommunicationDetailsSchema.optional(),
firstName: z.string().optional(),
gender: GenderSchema,
lastName: z.string().optional(),
locale: z.string().optional(),
organisation: OrganisationSchema.optional(),
title: z.string().optional(),
});
// BuyerStatus and PayerStatus enum schemas (bitwise enums matching generated API)
export const BuyerStatusSchema = z.union([
z.literal(0),
z.literal(1),
z.literal(2),
z.literal(4),
z.literal(8),
z.literal(16),
]);
export const PayerStatusSchema = z.union([
z.literal(0),
z.literal(1),
z.literal(2),
z.literal(4),
z.literal(8),
z.literal(16),
]);
export const BuyerTypeSchema = z.union([
z.literal(0),
z.literal(1),
z.literal(2),
z.literal(4),
z.literal(8),
z.literal(16),
]);
export const PayerTypeSchema = z.union([
z.literal(0),
z.literal(4),
z.literal(8),
z.literal(16),
]);
// BuyerDTO schema
export const BuyerDTOSchema = AddresseeWithReferenceDTOSchema.extend({
buyerNumber: z.string().optional(),
buyerStatus: BuyerStatusSchema.optional(),
buyerType: BuyerTypeSchema,
dateOfBirth: z.string().optional(),
isTemporaryAccount: z.boolean().optional(),
});
// PayerDTO schema
export const PayerDTOSchema = AddresseeWithReferenceDTOSchema.extend({
payerNumber: z.string().optional(),
payerStatus: PayerStatusSchema.optional(),
payerType: PayerTypeSchema,
});
import { z } from 'zod';
// OrderType is a union of specific string literals
export const OrderTypeSchema = z.union([
z.literal('Rücklage'),
z.literal('Abholung'),
z.literal('Versand'),
z.literal('DIG-Versand'),
z.literal('B2B-Versand'),
z.literal('Download'),
]).describe('Order type');
export type OrderType = z.infer<typeof OrderTypeSchema>;

View File

@@ -0,0 +1,4 @@
import { z } from 'zod';
import { BranchType } from '../models';
export const BranchTypeSchema = z.nativeEnum(BranchType).describe('Branch type');

View File

@@ -0,0 +1,29 @@
import {
AddressSchema,
EntityContainerSchema,
LabelSchema,
} from '@isa/common/data-access';
import { z } from 'zod';
import { BranchType } from '../models';
export const BranchSchema = z.object({
id: z.number().describe('Unique identifier').optional(),
createdAt: z.string().describe('Creation timestamp').optional(),
modifiedAt: z.string().describe('Modified at').optional(),
address: AddressSchema.describe('Branch physical address').optional(),
branchNumber: z.string().describe('Branch number identifier').optional(),
branchType: z.nativeEnum(BranchType).describe('Branch type classification').optional(),
isDefault: z.string().describe('Whether this is the default branch').optional(),
isOnline: z.boolean().describe('Whether branch is online').optional(),
isOrderingEnabled: z.boolean().describe('Whether ordering is enabled for this branch').optional(),
isShippingEnabled: z.boolean().describe('Whether shipping is enabled for this branch').optional(),
key: z.string().describe('Unique branch key identifier').optional(),
label: EntityContainerSchema(LabelSchema).describe('Branch label information'),
name: z.string().describe('Branch name').optional(),
parent: EntityContainerSchema(
z.lazy((): z.ZodType<any> => BranchSchema),
).describe('Parent branch entity').optional(),
shortName: z.string().describe('Branch short name').optional(),
});
export type Branch = z.infer<typeof BranchSchema>;

View File

@@ -0,0 +1,14 @@
import { z } from 'zod';
export const BuyerStatus = {
NotSet: 0,
Blocked: 1,
Check: 2,
LowDegreeOfCreditworthiness: 4,
Dunning1: 8,
Dunning2: 16,
} as const;
export const BuyerStatusSchema = z.nativeEnum(BuyerStatus).describe('Buyer status');
export type BuyerStatus = z.infer<typeof BuyerStatusSchema>;

View File

@@ -0,0 +1,18 @@
import { z } from 'zod';
import {
AddresseeWithReferenceSchema,
BuyerTypeSchema,
} from '@isa/common/data-access';
import { BuyerStatusSchema } from './buyer-status.schema';
export const BuyerSchema = z
.object({
buyerNumber: z.string().describe('Unique buyer identifier number').optional(),
buyerStatus: BuyerStatusSchema.describe('Current status of the buyer account').optional(),
buyerType: BuyerTypeSchema.describe('Buyer type').optional(),
dateOfBirth: z.string().describe('Date of birth').optional(),
isTemporaryAccount: z.boolean().describe('Whether temporaryAccount').optional(),
})
.extend(AddresseeWithReferenceSchema.shape);
export type Buyer = z.infer<typeof BuyerSchema>;

View File

@@ -0,0 +1,11 @@
import { TouchBaseSchema } from '@isa/common/data-access';
import { z } from 'zod';
export const CampaignSchema = z
.object({
code: z.string().describe('Code value').optional(),
label: z.string().describe('Label').optional(),
type: z.string().describe('Type').optional(),
value: z.number().describe('Value').optional(),
})
.extend(TouchBaseSchema.shape);

View File

@@ -1,56 +1,70 @@
import { ItemPayload } from '@generated/swagger/checkout-api';
import { z } from 'zod';
import { OrderTypeSchema } from './base-schemas';
const CanAddPriceSchema = z.object({
value: z
.object({
value: z.number().optional(),
currency: z.string().optional(),
currencySymbol: z.string().optional(),
value: z.number().describe('Value').optional(),
currency: z.string().describe('Currency code').optional(),
currencySymbol: z.string().describe('Currency symbol').optional(),
})
.describe('Value')
.optional(),
vat: z
.object({
inPercent: z.number().optional(),
label: z.string().optional(),
value: z.number().optional(),
vatType: z.number().optional(),
inPercent: z.number().describe('In percent').optional(),
label: z.string().describe('Label').optional(),
value: z.number().describe('Value').optional(),
vatType: z.number().describe('VAT type').optional(),
})
.describe('Value Added Tax')
.optional(),
});
const CanAddOLAAvailabilitySchema = z.object({
altAt: z.string().optional(),
at: z.string().optional(),
ean: z.string().optional(),
format: z.string().optional(),
isPrebooked: z.boolean().optional(),
itemId: z.number().int().optional(),
logistician: z.string().optional(),
logisticianId: z.number().int().optional(),
preferred: z.number().int().optional(),
price: CanAddPriceSchema.optional(),
qty: z.number().int().optional(),
shop: z.number().int().optional(),
ssc: z.string().optional(),
sscText: z.string().optional(),
status: z.number().int(),
supplier: z.string().optional(),
supplierId: z.number().int().optional(),
supplierProductNumber: z.string().optional(),
altAt: z.string().describe('Alt at').optional(),
at: z.string().describe('At').optional(),
ean: z.string().describe('European Article Number barcode').optional(),
format: z.string().describe('Format').optional(),
isPrebooked: z.boolean().describe('Whether prebooked').optional(),
itemId: z.number().int().describe('Unique item identifier').optional(),
logistician: z.string().describe('Logistician information').optional(),
logisticianId: z.number().int().describe('Logistician identifier').optional(),
preferred: z.number().int().describe('Preferred').optional(),
price: CanAddPriceSchema.describe('Price information').optional(),
qty: z.number().int().describe('Qty').optional(),
shop: z.number().int().describe('Shop').optional(),
ssc: z.string().describe('Ssc').optional(),
sscText: z.string().describe('Ssc text').optional(),
status: z.number().int().describe('Current status'),
supplier: z.string().describe('Supplier information').optional(),
supplierId: z.number().int().describe('Supplier identifier').optional(),
supplierProductNumber: z
.string()
.describe('SupplierProduct number')
.optional(),
});
const CanAddItemPayloadSchema = z.object({
availabilities: z.array(CanAddOLAAvailabilitySchema),
customerFeatures: z.record(z.string().optional()),
orderType: OrderTypeSchema,
id: z.string(),
availabilities: z
.array(CanAddOLAAvailabilitySchema)
.describe('Availabilities'),
customerFeatures: z
.record(z.string().optional())
.describe('Customer features'),
orderType: OrderTypeSchema.describe('Order type'),
id: z.string().describe('Unique identifier'),
});
export const CanAddItemsToShoppingCartParamsSchema = z.object({
shoppingCartId: z.number().int().positive(),
payload: z.array(CanAddItemPayloadSchema).min(1),
shoppingCartId: z
.number()
.int()
.positive()
.describe('Shopping cart identifier'),
payload: z.array(CanAddItemPayloadSchema).min(1).describe('Payload'),
});
export type CanAddItemsToShoppingCartParams = z.infer<

View File

@@ -0,0 +1,41 @@
import { z } from 'zod';
import { EntitySchema, EntityContainerSchema } from '@isa/common/data-access';
export const CategorySchema: z.ZodType<{
changed?: string;
created?: string;
id?: number;
pId?: string;
status?: number;
uId?: string;
version?: number;
name?: string;
parent?: {
id?: number;
pId?: string;
uId?: string;
data?: any;
};
type?: string;
key?: string;
sort?: number;
start?: string;
stop?: string;
tenant?: {
id?: number;
pId?: string;
uId?: string;
data?: any;
};
}> = EntitySchema.extend({
name: z.string().describe('Name').optional(),
parent: z.lazy(() => EntityContainerSchema(CategorySchema)).describe('Parent').optional(),
type: z.string().describe('Type').optional(),
key: z.string().describe('Key').optional(),
sort: z.number().int().describe('Sort criteria').optional(),
start: z.string().describe('Start').optional(),
stop: z.string().describe('Stop').optional(),
tenant: EntityContainerSchema(z.any()).describe('Tenant identifier').optional(),
});
export type Category = z.infer<typeof CategorySchema>;

View File

@@ -0,0 +1,51 @@
import { z } from 'zod';
import { EntitySchema, EntityContainerSchema, AddressSchema } from '@isa/common/data-access';
export const CompanySchema: z.ZodType<{
changed?: string;
created?: string;
id?: number;
pId?: string;
status?: number;
uId?: string;
version?: number;
parent?: {
id?: number;
pId?: string;
uId?: string;
data?: any;
};
companyNumber?: string;
locale?: string;
name?: string;
nameSuffix?: string;
legalForm?: string;
department?: string;
costUnit?: string;
vatId?: string;
address?: {
street?: string;
streetNumber?: string;
postalCode?: string;
city?: string;
country?: string;
additionalInfo?: string;
};
gln?: string;
sector?: string;
}> = EntitySchema.extend({
parent: z.lazy(() => EntityContainerSchema(CompanySchema)).describe('Parent').optional(),
companyNumber: z.string().describe('Company number').optional(),
locale: z.string().describe('Locale').optional(),
name: z.string().max(64).describe('Name').optional(),
nameSuffix: z.string().max(64).describe('Name suffix').optional(),
legalForm: z.string().max(64).describe('Legal form').optional(),
department: z.string().max(64).describe('Department').optional(),
costUnit: z.string().max(64).describe('Cost unit').optional(),
vatId: z.string().max(16).describe('Vat identifier').optional(),
address: AddressSchema.describe('Address').optional(),
gln: z.string().max(64).describe('Gln').optional(),
sector: z.string().max(64).describe('Sector').optional(),
});
export type Company = z.infer<typeof CompanySchema>;

View File

@@ -1,64 +0,0 @@
import { z } from 'zod';
import {
BuyerDTOSchema,
PayerDTOSchema,
NotificationChannelSchema,
} from './base-schemas';
import { ShippingAddressSchema } from './shipping-address.schema';
/**
* Schema for checkout completion parameters.
*
* @remarks
* This schema validates all data required to complete a checkout without accessing state management.
* All customer and order information must be provided as parameters.
*/
export const CompleteCheckoutParamsSchema = z.object({
/**
* ID of the shopping cart to process
* The checkout will be created/refreshed from this shopping cart
*/
shoppingCartId: z.number().int().positive(),
/**
* Buyer information (required)
* Will be set on the checkout before order creation
*/
buyer: BuyerDTOSchema,
/**
* Notification channels for customer communications (required)
*/
notificationChannels: NotificationChannelSchema,
/**
* Customer features for business logic determination
* Used to analyze customer type and determine order handling
*/
customerFeatures: z.record(z.string(), z.string()),
/**
* Payer information (optional)
* Required only for B2B, delivery, or download orders
*/
payer: PayerDTOSchema.optional(),
/**
* Shipping address (optional)
* Required only for delivery orders
* Will be merged into destination objects
*/
shippingAddress: ShippingAddressSchema.optional(),
/**
* Special comment to apply to all shopping cart items (optional)
*/
specialComment: z.string().optional(),
});
/**
* TypeScript type inferred from the Zod schema
*/
export type CompleteCheckoutParams = z.infer<
typeof CompleteCheckoutParamsSchema
>;

View File

@@ -0,0 +1,97 @@
import { z } from 'zod';
import { NotificationChannelSchema } from '@isa/common/data-access';
import {
CustomerSchema,
PayerSchema as CrmPayerSchema,
ShippingAddressSchema as CrmShippingAddressSchema,
} from '@isa/crm/data-access';
import { BuyerSchema } from './buyer.schema';
import { PayerSchema } from './payer.schema';
import { ShippingAddressSchema } from './shipping-address.schema';
/**
* Schema for checkout completion parameters.
*
* @remarks
* This schema validates all data required to complete a checkout without accessing state management.
* All customer and order information must be provided as parameters.
*/
export const CompleteOrderParamsSchema = z.object({
/**
* ID of the shopping cart to process
* The checkout will be created/refreshed from this shopping cart
*/
shoppingCartId: z
.number()
.int()
.positive()
.describe('Shopping cart identifier'),
/**
* Buyer information (required)
* Will be set on the checkout before order creation
*/
buyer: BuyerSchema.describe('Buyer information'),
/**
* Notification channels for customer communications (required)
*/
notificationChannels: NotificationChannelSchema.describe(
'Notification channels',
).optional(),
/**
* Customer features for business logic determination
* Used to analyze customer type and determine order handling
*/
customerFeatures: z.record(
z.string().describe('Customer features'),
z.string(),
),
/**
* Payer information (optional)
* Required only for B2B, delivery, or download orders
*/
payer: PayerSchema.describe('Payer information').optional(),
/**
* Shipping address (optional)
* Required only for delivery orders
* Will be merged into destination objects
*/
shippingAddress: ShippingAddressSchema.describe(
'Shipping address information',
).optional(),
/**
* Special comment to apply to all shopping cart items (optional)
*/
specialComment: z.string().describe('Special comment').optional(),
});
/**
* TypeScript type inferred from the Zod schema
*/
export type CompleteOrderParams = z.infer<typeof CompleteOrderParamsSchema>;
export const CompleteCrmOrderParamsSchema = z.object({
shoppingCartId: z
.number()
.int()
.positive()
.describe('Shopping cart identifier'),
crmCustomer: CustomerSchema.describe('Crm customer'),
crmPayer: CrmPayerSchema.describe('Crm payer').optional(),
crmShippingAddress: CrmShippingAddressSchema.describe(
'Crm shipping address',
).optional(),
notificationChannels: NotificationChannelSchema.describe(
'Notification channels',
).optional(),
specialComment: z.string().describe('Special comment').optional(),
});
export type CompleteCrmOrderParams = z.infer<
typeof CompleteCrmOrderParamsSchema
>;

View File

@@ -0,0 +1,15 @@
import { z } from 'zod';
export const ComponentItemDisplayType = {
Visible: 0,
Hidden: 1,
Emphasize: 2,
} as const;
export const ComponentItemDisplayTypeSchema = z.nativeEnum(
ComponentItemDisplayType,
).describe('ComponentItemDisplay type');
export type ComponentItemDisplayType = z.infer<
typeof ComponentItemDisplayTypeSchema
>;

View File

@@ -0,0 +1,27 @@
import {
EntityContainerSchema,
QuantityUnitTypeSchema,
TouchBaseSchema,
} from '@isa/common/data-access';
import { z } from 'zod';
import { CategorySchema } from './category.schema';
import { ComponentItemDisplayTypeSchema } from './component-item-display-type.schema';
export const ComponentItemSchema = z
.object({
name: z.string().describe('Name').optional(),
description: z.string().describe('Description text').optional(),
item: EntityContainerSchema(z.lazy(() => z.any())).describe('Item').optional(), // ItemSchema would create circular dependency
category: EntityContainerSchema(CategorySchema).describe('Category information').optional(),
required: z.boolean().describe('Whether this is required').optional(),
quantityMin: z.number().describe('Quantity min').optional(),
quantityMax: z.number().describe('Quantity max').optional(),
quantityUnitType: QuantityUnitTypeSchema.describe('QuantityUnit type').optional(),
unit: z.string().describe('Unit').optional(),
displayType: ComponentItemDisplayTypeSchema.describe('Display type').optional(),
start: z.string().describe('Start').optional(),
stop: z.string().describe('Stop').optional(),
})
.extend(TouchBaseSchema.shape);
export type ComponentItem = z.infer<typeof ComponentItemSchema>;

View File

@@ -0,0 +1,21 @@
import {
EntitySchema,
QuantityUnitTypeSchema,
} from '@isa/common/data-access';
import { z } from 'zod';
import { ComponentItemSchema } from './component-item.schema';
import { SetTypeSchema } from './set-type.schema';
export const ComponentsSchema = z
.object({
items: z.array(ComponentItemSchema).describe('List of items').optional(),
type: SetTypeSchema.describe('Type').optional(),
overallQuantityMin: z.number().describe('Overall quantity min').optional(),
overallQuantityMax: z.number().describe('Overall quantity max').optional(),
quantityUnitType: QuantityUnitTypeSchema.describe('QuantityUnit type').optional(),
unit: z.string().describe('Unit').optional(),
referenceQuantity: z.number().describe('Reference quantity').optional(),
})
.extend(EntitySchema.shape);
export type Components = z.infer<typeof ComponentsSchema>;

View File

@@ -0,0 +1,9 @@
import { z } from 'zod';
import { TouchBaseSchema, EntityContainerSchema } from '@isa/common/data-access';
export const ContributorHelperSchema = TouchBaseSchema.extend({
type: z.string().describe('Type').optional(),
contributor: EntityContainerSchema(z.any()).describe('Contributor information').optional(),
});
export type ContributorHelper = z.infer<typeof ContributorHelperSchema>;

View File

@@ -0,0 +1,11 @@
import { TouchBaseSchema } from '@isa/common/data-access';
import { z } from 'zod';
export const DateRangeSchema = z
.object({
start: z.string().describe('Start').optional(),
stop: z.string().describe('Stop').optional(),
})
.extend(TouchBaseSchema.shape);
export type DateRange = z.infer<typeof DateRangeSchema>;

View File

@@ -0,0 +1,30 @@
import { z } from 'zod';
export const DeclarableFoodAdditives = {
NotSet: 0,
NatriumcyclymatUndSaccarinNatrium: 1,
Suessungsmitteln: 2,
Phenylaninquelle: 4,
MitEinerZuckerartUndSuessungsmitteln: 8,
Geschmacksverstaerker: 16,
Geschwefelt: 32,
Geschwaerzt: 64,
Gewachst: 128,
Phosphat: 256,
Konservierungsstoff: 512,
Farbstoff: 1024,
Antioxidationsmittel: 2048,
Koffeinhaltig: 4096,
Saeurungsmittel: 8192,
Rauch: 16384,
Lachsersatz: 32768,
Vorderschinken: 65536,
} as const;
export const DeclarableFoodAdditivesSchema = z.nativeEnum(
DeclarableFoodAdditives,
).describe('Declarable food additives');
export type DeclarableFoodAdditives = z.infer<
typeof DeclarableFoodAdditivesSchema
>;

View File

@@ -0,0 +1,34 @@
import {
AddressSchema,
CommunicationDetailsSchema,
EntityContainerSchema,
GenderSchema,
OrganisationSchema,
} from '@isa/common/data-access';
import { z } from 'zod';
import { ShippingTargetSchema } from './shipping-target.schema';
import { BranchSchema } from './branch.schema';
export const DestinationSchema = z.object({
id: z.number().describe('Unique identifier').optional(),
createdAt: z.string().describe('Creation timestamp').optional(),
modifiedAt: z.string().describe('Modified at').optional(),
address: AddressSchema.describe('Address').optional(),
communicationDetails: CommunicationDetailsSchema.describe(
'Communication details',
).optional(),
firstName: z.string().describe('First name').optional(),
gender: GenderSchema.describe('Gender').default(0),
lastName: z.string().describe('Last name').optional(),
locale: z.string().describe('Locale').optional(),
organisation: OrganisationSchema.describe(
'Organisation information',
).optional(),
title: z.string().describe('Title').optional(),
target: ShippingTargetSchema.describe('Target').optional(),
targetBranch: EntityContainerSchema(BranchSchema)
.describe('Target branch')
.optional(),
});
export type Destination = z.infer<typeof DestinationSchema>;

View File

@@ -0,0 +1,18 @@
import { z } from 'zod';
import { EntitySchema } from '@isa/common/data-access';
export const FileSchema = EntitySchema.extend({
name: z.string().describe('Name').optional(),
type: z.string().describe('Type').optional(),
path: z.string().describe('Path').optional(),
mime: z.string().describe('Mime').optional(),
hash: z.string().describe('Whether has h').optional(),
size: z.number().describe('Size').optional(),
subtitle: z.string().describe('Subtitle').optional(),
copyright: z.string().describe('Copyright').optional(),
locale: z.string().describe('Locale').optional(),
license: z.string().describe('License').optional(),
sort: z.number().int().describe('Sort criteria').optional(),
});
export type File = z.infer<typeof FileSchema>;

View File

@@ -0,0 +1,26 @@
import { z } from 'zod';
export const FoodLabel = {
NotSet: 0,
Organic: 1,
Vegetarian: 2,
Vegan: 4,
Halal: 8,
Kashrut: 16,
Pasteurised: 32,
ExtendedShelfLife: 64,
UltraHighTemperature: 128,
RawFood: 256,
NotSuitableForPregnantWomen: 512,
NotSuitableForChildren: 1024,
ContainsAlcolhol: 3584,
ContainsCaffein: 5632,
ContainsBeef: 8192,
ContainsPork: 16384,
ContainsFish: 32768,
ContainsRawMilk: 66048,
} as const;
export const FoodLabelSchema = z.nativeEnum(FoodLabel).describe('Food label');
export type FoodLabel = z.infer<typeof FoodLabelSchema>;

View File

@@ -0,0 +1,19 @@
import { z } from 'zod';
import { TouchBaseSchema } from '@isa/common/data-access';
import { FoodLabelSchema } from './food-label.schema';
import { AllergeneTypeSchema } from './allergene-type.schema';
import { DeclarableFoodAdditivesSchema } from './declarable-food-additives.schema';
import { NutritionFactsSchema } from './nutrition-facts.schema';
export const FoodSchema = TouchBaseSchema.extend({
alcohol: z.number().describe('Alcohol').optional(),
foodLabel: FoodLabelSchema.describe('Food label information').optional(),
allergenes: AllergeneTypeSchema.describe('Allergenes').optional(),
allergenesDescription: z.string().describe('Allergenes description').optional(),
mayContainTracesOf: AllergeneTypeSchema.describe('May contain traces of').optional(),
mayContainTracesOfDescription: z.string().describe('May contain traces of description').optional(),
declarableFoodAdditives: DeclarableFoodAdditivesSchema.describe('Declarable food additives').optional(),
nutritionFacts: NutritionFactsSchema.describe('Nutrition facts information').optional(),
});
export type Food = z.infer<typeof FoodSchema>;

View File

@@ -0,0 +1,9 @@
import { z } from 'zod';
export const ImageSchema = z.object({
alt: z.string().describe('Alt').optional(),
path: z.string().describe('Path').optional(),
subtitle: z.string().describe('Subtitle').optional(),
});
export type Image = z.infer<typeof ImageSchema>;

View File

@@ -1,6 +1,56 @@
export * from './add-item-to-shopping-cart-params.schema';
export * from './base-schemas';
export * from './allergene-type.schema';
export * from './availability-type.schema';
export * from './availability.schema';
export * from './avoirdupois.schema';
export * from './branch-type.schema';
export * from './branch.schema';
export * from './buyer-status.schema';
export * from './buyer.schema';
export * from './can-add-items-to-shopping-cart-params.schema';
export * from './complete-checkout-params.schema';
export * from './campaign.schema';
export * from './category.schema';
export * from './company.schema';
export * from './component-item-display-type.schema';
export * from './component-item.schema';
export * from './components.schema';
export * from './complete-order-params.schema';
export * from './contributor-helper.schema';
export * from './data-range.schema';
export * from './declarable-food-additives.schema';
export * from './destination.schema';
export * from './file.schema';
export * from './food-label.schema';
export * from './food.schema';
export * from './image.schema';
export * from './item-label.schema';
export * from './item-type.schema';
export * from './item.schema';
export * from './logistician.schema';
export * from './loyalty.schema';
export * from './notification-channel.schema';
export * from './nutrition-fact-type.schema';
export * from './nutrition-fact.schema';
export * from './nutrition-facts.schema';
export * from './payer-status.schema';
export * from './payer-type.schema';
export * from './payer.schema';
export * from './payment-type.schema';
export * from './price-flat.schema';
export * from './product.schema';
export * from './promotion.schema';
export * from './quantity-unit-type.schema';
export * from './remove-shopping-cart-item-params.schema';
export * from './rezeptmasz.schema';
export * from './set-type.schema';
export * from './shipping-address.schema';
export * from './shipping-target.schema';
export * from './shop-item.schema';
export * from './size.schema';
export * from './supplier-type.schema';
export * from './supplier.schema';
export * from './tenant.schema';
export * from './text.schema';
export * from './update-shopping-cart-item-params.schema';
export * from './url.schema';
export * from './weight.schema';

View File

@@ -0,0 +1,10 @@
import { z } from 'zod';
import { TouchBaseSchema } from '@isa/common/data-access';
export const ItemLabelSchema = TouchBaseSchema.extend({
labelType: z.string().describe('Label type').optional(),
cultureInfo: z.string().describe('Culture info').optional(),
value: z.string().describe('Value').optional(),
});
export type ItemLabel = z.infer<typeof ItemLabelSchema>;

View File

@@ -0,0 +1,27 @@
import { z } from 'zod';
export const ItemType = {
NotSet: 0,
SingleUnit: 1,
SalesUnit: 2,
Set: 4,
PackagingUnit: 8,
Configurable: 16,
Discount: 32,
Percentaged: 64,
FixedPrice: 128,
Postage: 256,
HandlingFee: 512,
Voucher: 1024,
ComponentList: 2048,
Download: 4096,
Streaming: 8192,
ItemPrice: 16384,
SingleUnitItemPrice: 16385,
ComponentPrice: 32768,
CustomPrice: 65536,
} as const;
export const ItemTypeSchema = z.nativeEnum(ItemType).describe('Item type');
export type ItemType = z.infer<typeof ItemTypeSchema>;

View File

@@ -0,0 +1,54 @@
import { EntityContainerSchema, EntitySchema } from '@isa/common/data-access';
import { z } from 'zod';
import { CategorySchema } from './category.schema';
import { CompanySchema } from './company.schema';
import { ComponentsSchema } from './components.schema';
import { ContributorHelperSchema } from './contributor-helper.schema';
import { FileSchema } from './file.schema';
import { FoodSchema } from './food.schema';
import { ItemLabelSchema } from './item-label.schema';
import { ItemTypeSchema } from './item-type.schema';
import { SizeSchema } from './size.schema';
import { TenantSchema } from './tenant.schema';
import { TextSchema } from './text.schema';
import { WeightSchema } from './weight.schema';
export const ItemSchema = z
.object({
itemNumber: z.string().describe('Unique item number identifier').optional(),
name: z.string().describe('Item name').optional(),
subtitle: z.string().describe('Item subtitle').optional(),
description: z.string().describe('Item description text').optional(),
ean: z.string().describe('European Article Number barcode').optional(),
secondaryEAN: z.string().describe('Secondary EAN barcode').optional(),
precedingItem: EntityContainerSchema(
z.lazy((): z.ZodType<any> => ItemSchema),
).describe('Reference to preceding item version').optional(),
precedingItemEAN: z.string().describe('EAN of preceding item version').optional(),
contributors: z.array(ContributorHelperSchema).describe('List of contributors (authors, editors, etc)').optional(),
manufacturer: EntityContainerSchema(CompanySchema).describe('Manufacturer company information').optional(),
publicationDate: z.string().describe('Publication date of the item').optional(),
categories: z.array(EntityContainerSchema(CategorySchema)).describe('List of item categories').optional(),
manufacturingCosts: z.number().describe('Manufacturing costs').optional(),
size: SizeSchema.describe('Physical size dimensions').optional(),
weight: WeightSchema.describe('Total weight').optional(),
netWeight: WeightSchema.describe('Net weight excluding packaging').optional(),
weightOfPackaging: WeightSchema.describe('Weight of packaging only').optional(),
itemType: ItemTypeSchema.describe('Type classification of the item').optional(),
edition: z.string().describe('Edition information').optional(),
serial: z.string().describe('Serial number').optional(),
format: z.string().describe('Format code (e.g., HC, PB, EB)').optional(),
formatDetail: z.string().describe('Detailed format description').optional(),
volume: z.string().describe('Volume information').optional(),
toxins: z.string().describe('Toxin warnings or information').optional(),
files: z.array(EntityContainerSchema(FileSchema)).describe('List of associated files').optional(),
texts: z.array(EntityContainerSchema(TextSchema)).describe('List of associated text content').optional(),
set: EntityContainerSchema(ComponentsSchema).describe('Set components if item is a set').optional(),
accessories: EntityContainerSchema(ComponentsSchema).describe('Accessory components').optional(),
labels: z.array(ItemLabelSchema).describe('List of item labels').optional(),
food: FoodSchema.describe('Food-specific information if applicable').optional(),
tenant: EntityContainerSchema(TenantSchema).describe('Tenant identifier').optional(),
})
.extend(EntitySchema.shape);
export type Item = z.infer<typeof ItemSchema>;

View File

@@ -0,0 +1,12 @@
import { z } from 'zod';
import { EntitySchema } from '@isa/common/data-access';
export const LogisticianSchema = z
.object({
gln: z.string().describe('Gln').optional(),
logisticianNumber: z.string().describe('Logistician number').optional(),
name: z.string().describe('Name').optional(),
})
.extend(EntitySchema.shape);
export type Logistician = z.infer<typeof LogisticianSchema>;

View File

@@ -0,0 +1,11 @@
import { TouchBaseSchema } from '@isa/common/data-access';
import { z } from 'zod';
export const LoyaltySchema = z
.object({
code: z.string().describe('Code value').optional(),
label: z.string().describe('Label').optional(),
type: z.string().describe('Type').optional(),
value: z.number().describe('Value').optional(),
})
.extend(TouchBaseSchema.shape);

View File

@@ -0,0 +1,11 @@
/**
* @deprecated This schema has been moved to @isa/common/data-access.
* Please update imports to use:
* import { NotificationChannelSchema, NotificationChannel } from '@isa/common/data-access';
*
* This re-export will be removed in a future version.
*/
export {
NotificationChannelSchema,
NotificationChannel,
} from '@isa/common/data-access';

View File

@@ -0,0 +1,69 @@
import { z } from 'zod';
export const NutritionFactType = {
NotSet: 0,
Energy: 1,
Protein: 2,
VegetableProtein: 6,
AnimalProtein: 10,
Fat: 16,
FatSaturated: 48,
FatMonoUnsaturated: 80,
FatPolyUnsaturated: 144,
TransFat: 272,
Cholesterol: 512,
Carbohydrate: 1024,
Sugar: 3072,
Fiber: 5120,
Starch: 9216,
Alcohol: 17408,
Sodium: 32768,
QuantityElements: 65536,
Calcium: 196608,
Chlorine: 327680,
Potassium: 589824,
Magnesium: 1114112,
Natrium: 2162688,
Phosphor: 4259840,
Sulfur: 8454144,
Hydrogencarbonate: 16842752,
Sulfate: 33619968,
Nitrate: 67174400,
Fluorid: 134283264,
Vitamins: 268435456,
VitaminA: 805306368,
VitaminB: 1342177280,
VitaminB1: 3489660928,
VitaminB2: 5637144576,
VitaminB3: 9932111872,
VitaminB5: 18522046464,
VitaminB6: 35701915648,
VitaminB7: 70061654016,
VitaminB9: 138781130752,
VitaminB12: 276220084224,
VitaminC: 550024249344,
VitaminD: 1099780063232,
VitaminE: 2199291691008,
VitaminK: 4398314946560,
TraceElements: 8796093022208,
Arsic: 26388279066624,
Boron: 43980465111040,
Chromium: 79164837199872,
Rubidium: 149533581377536,
Tin: 290271069732864,
Cobalt: 571746046443520,
Iron: 1134695999864832,
Fluorine: 2260595906707456,
Iodinde: 4512395720392704,
Copper: 9015995347763200,
Manganese: 18023194602504192,
Molybdenum: 36037593111986176,
Selenium: 72066390130950144,
Silicon: 144123984168878080,
Zinc: 288239172244733952,
Vanadium: 576469548396445696,
} as const;
export const NutritionFactTypeSchema = z.nativeEnum(NutritionFactType).describe('NutritionFact type');
export type NutritionFactType = z.infer<typeof NutritionFactTypeSchema>;

View File

@@ -0,0 +1,14 @@
import { z } from 'zod';
import { TouchBaseSchema } from '@isa/common/data-access';
import { NutritionFactTypeSchema } from './nutrition-fact-type.schema';
export const NutritionFactSchema = TouchBaseSchema.extend({
label: z.string().describe('Label').optional(),
nutritionFactType: NutritionFactTypeSchema.describe('Nutrition fact type').optional(),
rdaQuantity: z.number().describe('Rda quantity').optional(),
percentageOfRDA: z.number().describe('Percentage of r d a').optional(),
kiloJoule: z.number().describe('Kilo joule').optional(),
quantity: z.number().describe('Quantity').optional(),
});
export type NutritionFact = z.infer<typeof NutritionFactSchema>;

View File

@@ -0,0 +1,14 @@
import { z } from 'zod';
import { TouchBaseSchema } from '@isa/common/data-access';
import { RezeptmaszSchema } from './rezeptmasz.schema';
import { NutritionFactSchema } from './nutrition-fact.schema';
export const NutritionFactsSchema = TouchBaseSchema.extend({
referenceQuantity: z.number().describe('Reference quantity').optional(),
referenceQuantityUnitType: RezeptmaszSchema.describe('ReferenceQuantityUnit type').optional(),
kiloJoule: z.number().describe('Kilo joule').optional(),
kiloCalories: z.number().describe('Kilo calories').optional(),
items: z.array(NutritionFactSchema).describe('List of items').optional(),
});
export type NutritionFacts = z.infer<typeof NutritionFactsSchema>;

View File

@@ -0,0 +1,14 @@
import { z } from 'zod';
export const PayerStatus = {
NotSet: 0,
Blocked: 1,
Check: 2,
LowDegreeOfCreditworthiness: 4,
Dunning1: 8,
Dunning2: 16,
} as const;
export const PayerStatusSchema = z.nativeEnum(PayerStatus).describe('Payer status');
export type PayerStatus = z.infer<typeof PayerStatusSchema>;

View File

@@ -0,0 +1,12 @@
import { z } from 'zod';
export const PayerType = {
NotSet: 0,
Staff: 4,
B2C: 8,
B2B: 16,
} as const;
export const PayerTypeSchema = z.nativeEnum(PayerType).describe('Payer type');
export type PayerType = z.infer<typeof PayerTypeSchema>;

View File

@@ -0,0 +1,14 @@
import { AddresseeWithReferenceSchema } from '@isa/common/data-access';
import { z } from 'zod';
import { PayerStatusSchema } from './payer-status.schema';
import { PayerTypeSchema } from './payer-type.schema';
export const PayerSchema = z
.object({
payerNumber: z.string().describe('Unique payer account number').optional(),
payerType: PayerTypeSchema.describe('Payer type').optional(),
payerStatus: PayerStatusSchema.describe('Current status of the payer account').optional(),
})
.extend(AddresseeWithReferenceSchema.shape);
export type Payer = z.infer<typeof PayerSchema>;

View File

@@ -0,0 +1,8 @@
/**
* @deprecated This schema has been moved to @isa/common/data-access.
* Please update imports to use:
* import { PaymentTypeSchema, PaymentType } from '@isa/common/data-access';
*
* This re-export will be removed in a future version.
*/
export { PaymentTypeSchema, PaymentType } from '@isa/common/data-access';

View File

@@ -0,0 +1,16 @@
import { z } from 'zod';
import { VatTypeSchema } from '@isa/common/data-access';
// Schema for the flat Price type (not PriceDTO)
// This matches the Price interface from generated/swagger/checkout-api/src/models/price.ts
export const PriceFlatSchema = z.object({
currency: z.string().describe('Currency code').optional(),
currencySymbol: z.string().describe('Currency symbol').optional(),
validFrom: z.string().describe('Validity start date').optional(),
value: z.number().describe('Value'),
vatInPercent: z.number().describe('Vat in percent').optional(),
vatType: VatTypeSchema.describe('VAT type'),
vatValue: z.number().describe('VAT amount value').optional(),
});
export type PriceFlat = z.infer<typeof PriceFlatSchema>;

View File

@@ -0,0 +1,27 @@
import { TouchBaseSchema } from '@isa/common/data-access';
import { z } from 'zod';
import { SizeSchema } from './size.schema';
import { WeightSchema } from './weight.schema';
export const ProductSchema = z
.object({
additionalName: z.string().describe('Additional name').optional(),
catalogProductNumber: z.string().describe('CatalogProduct number').optional(),
contributors: z.string().describe('Contributors').optional(),
ean: z.string().describe('European Article Number barcode').optional(),
edition: z.string().describe('Edition').optional(),
format: z.string().describe('Format').optional(),
formatDetail: z.string().describe('Format detail').optional(),
locale: z.string().describe('Locale').optional(),
manufacturer: z.string().describe('Manufacturer information').optional(),
name: z.string().describe('Name').optional(),
productGroup: z.string().describe('Product group').optional(),
productGroupDetails: z.string().describe('Product group details').optional(),
publicationDate: z.string().describe('Publication date').optional(),
serial: z.string().describe('Serial').optional(),
size: SizeSchema.describe('Size').optional(),
supplierProductNumber: z.string().describe('SupplierProduct number').optional(),
volume: z.string().describe('Volume').optional(),
weight: WeightSchema.describe('Weight').optional(),
})
.extend(TouchBaseSchema.shape);

View File

@@ -0,0 +1,11 @@
import { TouchBaseSchema } from '@isa/common/data-access';
import { z } from 'zod';
export const PromotionSchema = z
.object({
code: z.string().describe('Code value').optional(),
label: z.string().describe('Label').optional(),
type: z.string().describe('Type').optional(),
value: z.number().describe('Value').optional(),
})
.extend(TouchBaseSchema.shape);

View File

@@ -0,0 +1,11 @@
/**
* @deprecated This schema has been moved to @isa/common/data-access.
* Please update imports to use:
* import { QuantityUnitTypeSchema, QuantityUnitType } from '@isa/common/data-access';
*
* This re-export will be removed in a future version.
*/
export {
QuantityUnitTypeSchema,
QuantityUnitType,
} from '@isa/common/data-access';

View File

@@ -1,8 +1,8 @@
import { z } from 'zod';
export const RemoveShoppingCartItemParamsSchema = z.object({
shoppingCartId: z.number().int().positive(),
shoppingCartItemId: z.number().int().positive(),
shoppingCartId: z.number().int().positive().describe('Shopping cart identifier'),
shoppingCartItemId: z.number().int().positive().describe('Shopping cart item identifier'),
});
export type RemoveShoppingCartItemParams = z.infer<

View File

@@ -0,0 +1,40 @@
import { z } from 'zod';
export const Rezeptmasz = {
NotSet: 0,
Mikrogram: 1,
Milligram: 2,
Gram: 3,
Dekagramm: 4,
Lot: 5,
Kilogram: 6,
Pfund: 7,
Grain: 8,
Ounce: 9,
Pound: 10,
Milliliter: 11,
Centiliter: 12,
FluidOunce: 13,
Deciliter: 14,
Liter: 15,
Mass: 16,
Pint: 17,
Quart: 18,
Gallon: 19,
Drop: 20,
Dash: 21,
Schuss: 22,
Pinch: 23,
Messerspitze: 24,
Saitspoon: 25,
Teaspoon: 26,
Tablespoon: 27,
Tasse: 28,
Cup: 29,
Bund: 30,
Slice: 31,
} as const;
export const RezeptmaszSchema = z.nativeEnum(Rezeptmasz).describe('Rezeptmasz');
export type Rezeptmasz = z.infer<typeof RezeptmaszSchema>;

View File

@@ -0,0 +1,11 @@
import { z } from 'zod';
export const SetType = {
NotSet: 0,
Fixed: 1,
Choice: 2,
} as const;
export const SetTypeSchema = z.nativeEnum(SetType).describe('Set type');
export type SetType = z.infer<typeof SetTypeSchema>;

View File

@@ -1,2 +1,3 @@
import { AddresseeWithReferenceSchema } from '@isa/common/data-access';
export const ShippingAddressSchema = AddresseeWithReferenceSchema.extend({});

View File

@@ -0,0 +1,4 @@
import { z } from 'zod';
import { ShippingTarget } from '../models';
export const ShippingTargetSchema = z.nativeEnum(ShippingTarget).optional().describe('Shipping target');

View File

@@ -0,0 +1,18 @@
import { z } from 'zod';
import { AvailabilitySchema } from './availability.schema';
import { UrlSchema } from './url.schema';
import { ImageSchema } from './image.schema';
import { EntityContainerSchema } from '@isa/common/data-access';
export const ShopItemSchema = z.object({
availability: AvailabilitySchema.describe('Availability').optional(),
deepUrl: UrlSchema.describe('Deep url').optional(),
description: z.string().describe('Description text').optional(),
ean: z.string().describe('European Article Number barcode').optional(),
firstDayOfSale: z.string().describe('First day of sale').optional(),
format: z.string().describe('Format').optional(),
image: ImageSchema.describe('Image').optional(),
item: EntityContainerSchema(z.any()).describe('Item').optional(),
});
export type ShopItem = z.infer<typeof ShopItemSchema>;

View File

@@ -0,0 +1,10 @@
import { z } from 'zod';
export const SizeSchema = z.object({
height: z.number().describe('Height'),
width: z.number().describe('Width'),
length: z.number().describe('Length'),
unit: z.string().describe('Unit').optional(),
});
export type Size = z.infer<typeof SizeSchema>;

View File

@@ -0,0 +1,12 @@
import { z } from 'zod';
export const SupplierType = {
NotSet: 0,
Publisher: 1,
Wholesaler: 2,
Distributor: 4,
} as const;
export const SupplierTypeSchema = z.nativeEnum(SupplierType).describe('Supplier type');
export type SupplierType = z.infer<typeof SupplierTypeSchema>;

View File

@@ -0,0 +1,14 @@
import { EntitySchema } from '@isa/common/data-access';
import { z } from 'zod';
import { SupplierTypeSchema } from './supplier-type.schema';
export const SupplierSchema = z
.object({
key: z.string().describe('Key').optional(),
name: z.string().describe('Name').optional(),
supplierNumber: z.string().describe('Supplier number').optional(),
supplierType: SupplierTypeSchema.describe('Supplier type').optional(),
})
.extend(EntitySchema.shape);
export type Supplier = z.infer<typeof SupplierSchema>;

View File

@@ -0,0 +1,9 @@
import { z } from 'zod';
import { EntitySchema } from '@isa/common/data-access';
export const TenantSchema = EntitySchema.extend({
name: z.string().describe('Name').optional(),
key: z.string().describe('Key').optional(),
});
export type Tenant = z.infer<typeof TenantSchema>;

View File

@@ -0,0 +1,17 @@
import { z } from 'zod';
import { EntitySchema, EntityContainerSchema } from '@isa/common/data-access';
export const TextSchema = EntitySchema.extend({
cultureInfo: z.string().describe('Culture info').optional(),
name: z.string().describe('Name').optional(),
subtitle: z.string().describe('Subtitle').optional(),
copyright: z.string().describe('Copyright').optional(),
type: z.string().describe('Type').optional(),
content: z.string().describe('Content').optional(),
encoding: z.string().describe('Encoding').optional(),
mime: z.string().describe('Mime').optional(),
hash: z.string().describe('Whether has h').optional(),
tenant: EntityContainerSchema(z.any()).describe('Tenant identifier').optional(),
});
export type Text = z.infer<typeof TextSchema>;

View File

@@ -1,24 +1,26 @@
import { z } from 'zod';
import {
AvailabilityDTOSchema,
CampaignDTOSchema,
LoyaltyDTOSchema,
PromotionDTOSchema,
PriceSchema,
EntityDTOContainerOfDestinationDTOSchema,
} from './base-schemas';
import { UpdateShoppingCartItem } from '../models';
import { EntityContainerSchema, PriceSchema } from '@isa/common/data-access';
import { AvailabilitySchema } from './availability.schema';
import { CampaignSchema } from './campaign.schema';
import { DestinationSchema } from './destination.schema';
import { LoyaltySchema } from './loyalty.schema';
import { PromotionSchema } from './promotion.schema';
const UpdateShoppingCartItemParamsValueDefaultSchema = z.object({
availability: AvailabilityDTOSchema,
buyerComment: z.string().optional(),
campaign: CampaignDTOSchema,
destination: EntityDTOContainerOfDestinationDTOSchema,
loyalty: LoyaltyDTOSchema,
promotion: PromotionDTOSchema,
quantity: z.number().int().positive().optional(),
retailPrice: PriceSchema,
specialComment: z.string().optional(),
availability: AvailabilitySchema.describe(
'Availability information',
).optional(),
buyerComment: z.string().describe('Buyer comment text').optional(),
campaign: CampaignSchema.describe('Campaign information').optional(),
destination: EntityContainerSchema(DestinationSchema)
.optional()
.describe('Destination information'),
loyalty: LoyaltySchema.describe('Loyalty information').optional(),
promotion: PromotionSchema.describe('Promotion information').optional(),
quantity: z.number().int().positive().describe('Item quantity').optional(),
retailPrice: PriceSchema.describe('Retail price').optional(),
specialComment: z.string().describe('Special comment text').optional(),
});
// When loyalty points are used the price value must be 0 and promotion is not allowed
@@ -29,30 +31,48 @@ const UpdateShoppingCartItemParamsValueWithRedemptionPointsSchema =
availability: true,
promotion: true,
}).extend({
availability: AvailabilityDTOSchema.unwrap()
.omit({ price: true })
availability: AvailabilitySchema.omit({ price: true })
.extend({
price: PriceSchema.unwrap()
.omit({ value: true })
price: PriceSchema.omit({ value: true })
.extend({
value: z.object({
value: z.literal(0),
currency: z.string(),
}),
}),
}),
loyalty: LoyaltyDTOSchema,
value: z
.object({
value: z
.literal(0)
.describe('Price value must be 0 for loyalty redemption'),
currency: z.string().describe('Currency code'),
})
.describe('Price value object'),
})
.describe('Price information for loyalty redemption'),
})
.describe('Availability information for loyalty redemption'),
loyalty: LoyaltySchema.describe(
'Loyalty information for points redemption',
),
});
const UpdateShoppingCartItemParamsValueSchema = z.union([
UpdateShoppingCartItemParamsValueDefaultSchema,
UpdateShoppingCartItemParamsValueWithRedemptionPointsSchema,
]);
const UpdateShoppingCartItemParamsValueSchema = z
.union([
UpdateShoppingCartItemParamsValueDefaultSchema,
UpdateShoppingCartItemParamsValueWithRedemptionPointsSchema,
])
.describe('Update shopping cart item params value');
export const UpdateShoppingCartItemParamsSchema = z.object({
shoppingCartId: z.number().int().positive(),
shoppingCartItemId: z.number().int().positive(),
values: UpdateShoppingCartItemParamsValueSchema,
shoppingCartId: z
.number()
.int()
.positive()
.describe('Shopping cart identifier'),
shoppingCartItemId: z
.number()
.int()
.positive()
.describe('Shopping cart item identifier'),
values: UpdateShoppingCartItemParamsValueSchema.describe(
'Updated values for the cart item',
),
});
export type UpdateShoppingCartItemParams = {

View File

@@ -0,0 +1,13 @@
import { TouchBaseSchema } from '@isa/common/data-access';
import { z } from 'zod';
export const UrlSchema = z
.object({
nofollow: z.boolean().describe('Nofollow').optional(),
target: z.string().describe('Target').optional(),
title: z.string().describe('Title').optional(),
url: z.string().describe('URL address').optional(),
})
.extend(TouchBaseSchema.shape);
export type Url = z.infer<typeof UrlSchema>;

View File

@@ -0,0 +1,9 @@
import { z } from 'zod';
import { AvoirdupoisSchema } from './avoirdupois.schema';
export const WeightSchema = z.object({
value: z.number().describe('Value'),
unit: AvoirdupoisSchema.describe('Unit'),
});
export type Weight = z.infer<typeof WeightSchema>;

View File

@@ -4,7 +4,7 @@ import { ResponseArgsError, takeUntilAborted } from '@isa/common/data-access';
import { logger } from '@isa/core/logging';
import { firstValueFrom } from 'rxjs';
import { Cache, CacheTimeToLive, InFlight } from '@isa/common/decorators';
import { Branch } from '../models';
import { Branch } from '../schemas';
@Injectable({ providedIn: 'root' })
export class BranchService {

View File

@@ -6,32 +6,38 @@ import {
StoreCheckoutBuyerService,
StoreCheckoutPayerService,
StoreCheckoutPaymentService,
DestinationDTO,
BuyerDTO,
PayerDTO,
ShippingAddressDTO,
NotificationChannel,
PaymentType,
DestinationDTO,
EntityDTOContainerOfShoppingCartItemDTO,
AvailabilityDTO,
} from '@generated/swagger/checkout-api';
import { ResponseArgsError, takeUntilAborted } from '@isa/common/data-access';
import {
EntityContainer,
ResponseArgsError,
takeUntilAborted,
} from '@isa/common/data-access';
import { logger } from '@isa/core/logging';
import { firstValueFrom } from 'rxjs';
import { ShoppingCartService } from './shopping-cart.service';
import {
CompleteCheckoutParams,
CompleteCheckoutParamsSchema,
Buyer,
CompleteOrderParams,
CompleteOrderParamsSchema,
NotificationChannel,
Payer,
PaymentType,
} from '../schemas';
import {
Order,
OrderOptionsAnalysis,
CustomerTypeAnalysis,
Checkout,
ShoppingCartItem,
ShippingAddress,
ShoppingCartItem,
} from '../models';
import { CheckoutCompletionError } from '../errors';
import { OrderCreationService } from '@isa/oms/data-access';
import { LogisticianService } from '@isa/oms/data-access';
import { AvailabilityService } from '@isa/catalogue/data-access';
import { BranchService } from '@isa/remission/data-access';
import {
@@ -47,12 +53,10 @@ import {
shouldSetPayer,
needsDestinationUpdate,
determinePaymentType,
PaymentTypes,
filterShippingItems,
filterDeliveryDestinations,
hasValidItemId,
hasValidDestinationData,
ShippingTargets,
} from '../helpers';
/**
@@ -82,22 +86,26 @@ export class CheckoutService {
// Domain data-access services
#shoppingCartDataService = inject(ShoppingCartService);
#orderCreationService = inject(OrderCreationService);
#logisticianService = inject(LogisticianService);
#availabilityService = inject(AvailabilityService);
#branchService = inject(BranchService);
/**
* Completes the checkout process, creating orders.
* Completes the checkout process.
*
* @param params - Complete checkout parameters with all required data
* @param abortSignal - Optional cancellation signal
* @returns Promise resolving to an array of created orders
* @returns Promise resolving to the completed checkout ID
* @throws {CheckoutCompletionError} for business logic failures
* @throws {ResponseArgsError} for API failures
*
* @remarks
* This method prepares the checkout but does NOT create orders.
* Order creation should be handled separately by calling order creation services.
*
* @example
* ```typescript
* const orders = await checkoutService.complete({
* const checkoutId = await checkoutService.complete({
* checkoutId: 123,
* shoppingCartId: 456,
* buyer: buyerDTO,
@@ -110,10 +118,10 @@ export class CheckoutService {
* ```
*/
async complete(
params: CompleteCheckoutParams,
params: CompleteOrderParams,
abortSignal?: AbortSignal,
): Promise<Order[]> {
const validated = CompleteCheckoutParamsSchema.parse(params);
): Promise<number> {
const validated = CompleteOrderParamsSchema.parse(params);
this.#logger.info('Starting checkout completion');
@@ -213,7 +221,7 @@ export class CheckoutService {
this.#logger.debug('Setting notification channels');
await this.setNotificationChannelsOnCheckout(
checkoutId,
validated.notificationChannels,
validated.notificationChannels ?? 0,
abortSignal,
);
@@ -246,14 +254,12 @@ export class CheckoutService {
);
}
// Step 14: Create order(s)
this.#logger.debug('Creating orders');
const orders =
await this.#orderCreationService.createOrdersFromCheckout(checkoutId);
// Checkout completion is done - return checkoutId for order creation
this.#logger.info('Checkout completed successfully', () => ({
checkoutId,
}));
this.#logger.info('Checkout completed successfully');
return orders;
return checkoutId;
} catch (error) {
// Handle HTTP 409 conflict (order already exists)
if (error instanceof HttpErrorResponse && error.status === 409) {
@@ -274,7 +280,7 @@ export class CheckoutService {
* Analyzes shopping cart items to determine which order types are present.
*/
private analyzeOrderOptions(
items: EntityDTOContainerOfShoppingCartItemDTO[],
items: EntityContainer<ShoppingCartItem>[],
): OrderOptionsAnalysis {
return analyzeOrderOptions(items);
}
@@ -352,7 +358,7 @@ export class CheckoutService {
*/
private async setSpecialCommentOnItems(
shoppingCartId: number,
items: EntityDTOContainerOfShoppingCartItemDTO[],
items: EntityContainer<ShoppingCartItem>[],
comment: string,
abortSignal?: AbortSignal,
): Promise<void> {
@@ -417,7 +423,7 @@ export class CheckoutService {
*/
private async validateDownloadAvailabilities(
shoppingCartId: number,
items: EntityDTOContainerOfShoppingCartItemDTO[],
items: EntityContainer<ShoppingCartItem>[],
abortSignal?: AbortSignal,
): Promise<void> {
// Convert checkout-api items to catalogue-api format using adapter
@@ -478,7 +484,7 @@ export class CheckoutService {
*/
private async updateShippingAvailabilities(
shoppingCartId: number,
items: EntityDTOContainerOfShoppingCartItemDTO[],
items: EntityContainer<ShoppingCartItem>[],
abortSignal?: AbortSignal,
): Promise<void> {
// Filter shipping items
@@ -489,7 +495,7 @@ export class CheckoutService {
// Get branch and logistician in parallel
const [inventoryBranch, omsLogistician] = await Promise.all([
this.#branchService.getDefaultBranch(abortSignal),
this.#orderCreationService.getLogistician('2470', abortSignal),
this.#logisticianService.getLogistician2470(abortSignal),
]);
// Convert to catalogue format using adapters
@@ -566,12 +572,12 @@ export class CheckoutService {
*/
private async setBuyerOnCheckout(
checkoutId: number,
buyer: BuyerDTO,
buyerDTO: Buyer,
abortSignal?: AbortSignal,
): Promise<Checkout> {
let req$ = this.#buyerService.StoreCheckoutBuyerSetBuyerPOST({
checkoutId,
buyerDTO: buyer,
buyerDTO,
});
if (abortSignal) {
@@ -594,12 +600,12 @@ export class CheckoutService {
*/
private async setPayerOnCheckout(
checkoutId: number,
payer: PayerDTO,
payer: Payer,
abortSignal?: AbortSignal,
): Promise<Checkout> {
let req$ = this.#payerService.StoreCheckoutPayerSetPayerPOST({
checkoutId,
payerDTO: payer as PayerDTO,
payerDTO: payer,
});
if (abortSignal) {

View File

@@ -119,6 +119,7 @@ export class ShoppingCartService {
async updateItem(
params: UpdateShoppingCartItemParams,
): Promise<ShoppingCart> {
console.log('UpdateShoppingCartItemParams', params);
const parsed = UpdateShoppingCartItemParamsSchema.parse(params);
const req$ =

View File

@@ -4,7 +4,7 @@ import { ResponseArgsError, takeUntilAborted } from '@isa/common/data-access';
import { logger } from '@isa/core/logging';
import { firstValueFrom } from 'rxjs';
import { Cache, CacheTimeToLive, InFlight } from '@isa/common/decorators';
import { Supplier } from '../models';
import { Supplier } from '../schemas';
/**
* Service for fetching supplier information from the checkout API.

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
# checkout-feature-reward-order-confirmation
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test checkout-feature-reward-order-confirmation` to execute the unit tests.

View File

@@ -0,0 +1,34 @@
const nx = require('@nx/eslint-plugin');
const baseConfig = require('../../../../eslint.config.js');
module.exports = [
...baseConfig,
...nx.configs['flat/angular'],
...nx.configs['flat/angular-template'],
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'checkout',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'checkout',
style: 'kebab-case',
},
],
},
},
{
files: ['**/*.html'],
// Override or add rules here
rules: {},
},
];

View File

@@ -0,0 +1,20 @@
{
"name": "checkout-feature-reward-order-confirmation",
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/checkout/feature/reward-order-confirmation/src",
"prefix": "checkout",
"projectType": "library",
"tags": [],
"targets": {
"test": {
"executor": "@nx/vite:test",
"outputs": ["{options.reportsDirectory}"],
"options": {
"reportsDirectory": "../../../../coverage/libs/checkout/feature/reward-order-confirmation"
}
},
"lint": {
"executor": "@nx/eslint:lint"
}
}
}

View File

@@ -0,0 +1 @@
export * from './lib/routes';

View File

@@ -0,0 +1 @@
<h1>Order Confirmation Addresses</h1>

View File

@@ -0,0 +1,10 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'checkout-order-confirmation-addresses',
templateUrl: './order-confirmation-addresses.component.html',
styleUrls: ['./order-confirmation-addresses.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [],
})
export class OrderConfirmationAddressesComponent {}

Some files were not shown because too many files have changed in this diff Show More