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