mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
feat(checkout): add reward order confirmation feature with schema migrations
- Add new reward-order-confirmation feature library with components and store - Implement checkout completion orchestrator service for order finalization - Migrate checkout/oms/crm models to Zod schemas for better type safety - Add order creation facade and display order schemas - Update shopping cart facade with order completion flow - Add comprehensive tests for shopping cart facade - Update routing to include order confirmation page
This commit is contained in:
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -90,5 +90,8 @@
|
|||||||
"cursor-global": true,
|
"cursor-global": true,
|
||||||
"cursor-workspace": 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
|
- **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
|
### 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]`)
|
- **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
|
- **Standalone Components**: All new components must be standalone with explicit imports - no more NgModules
|
||||||
- **Testing Framework Selection**:
|
- **Testing Framework Selection**:
|
||||||
@@ -281,6 +283,96 @@ npx nx affected:test
|
|||||||
- **Existing libraries**: Continue with Jest + Spectator until migrated
|
- **Existing libraries**: Continue with Jest + Spectator until migrated
|
||||||
- **Path Aliases**: Always use `@isa/[domain]/[layer]/[feature]` - avoid relative imports across domain boundaries
|
- **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
|
### API Integration Workflow
|
||||||
- **Swagger Generation**: Run `npm run generate:swagger` to regenerate all 10 API clients when backend changes
|
- **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
|
- **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,
|
(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,
|
purchaseOption,
|
||||||
);
|
);
|
||||||
|
|
||||||
let promotion: Promotion | null = { value: item.promoPoints };
|
let promotion: Promotion | undefined = { value: item.promoPoints };
|
||||||
let loyalty: Loyalty | null = null;
|
let loyalty: Loyalty | undefined = undefined;
|
||||||
const redemptionPoints: number | null = item.redemptionPoints || null;
|
const redemptionPoints: number | null = item.redemptionPoints || null;
|
||||||
|
|
||||||
// "Lesepunkte einlösen" logic
|
// "Lesepunkte einlösen" logic
|
||||||
// If "Lesepunkte einlösen" is checked and item has redemption points, set price to 0 and remove promotion
|
// If "Lesepunkte einlösen" is checked and item has redemption points, set price to 0 and remove promotion
|
||||||
if (this.useRedemptionPoints) {
|
if (this.useRedemptionPoints) {
|
||||||
// If loyalty is set, we need to remove promotion
|
// If loyalty is set, we need to remove promotion
|
||||||
promotion = null;
|
promotion = undefined;
|
||||||
// Set loyalty points from item
|
// Set loyalty points from item
|
||||||
loyalty = { value: redemptionPoints };
|
loyalty = { value: redemptionPoints };
|
||||||
// Set price to 0
|
// Set price to 0
|
||||||
|
|||||||
@@ -165,7 +165,9 @@ export class DetailsMainViewBillingAddressesComponent
|
|||||||
}
|
}
|
||||||
} else if (this.showCustomerAddress) {
|
} else if (this.showCustomerAddress) {
|
||||||
this._host.setPayer(
|
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) {
|
} else if (this.showCustomerAddress) {
|
||||||
this._host.setShippingAddress(
|
this._host.setShippingAddress(
|
||||||
ShippingAddressAdapter.fromCustomer(customer as Customer),
|
ShippingAddressAdapter.fromCustomer(
|
||||||
|
customer as unknown as Customer,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -279,7 +279,7 @@ export class CustomerDetailsViewMainComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
get buyer() {
|
get buyer() {
|
||||||
return CustomerAdapter.toBuyer(this.customer as Customer);
|
return CustomerAdapter.toBuyer(this.customer as unknown as Customer);
|
||||||
}
|
}
|
||||||
|
|
||||||
get payer() {
|
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
|
// Base item schema - used for all availability checks
|
||||||
const ItemSchema = z.object({
|
const ItemSchema = z.object({
|
||||||
itemId: z.coerce.number().int().positive(),
|
itemId: z.coerce.number().int().positive().describe('Unique item identifier'),
|
||||||
ean: z.string(),
|
ean: z.string().describe('European Article Number barcode'),
|
||||||
price: PriceSchema.optional(),
|
price: PriceSchema.describe('Item price information').optional(),
|
||||||
quantity: z.coerce.number().int().positive().default(1),
|
quantity: z.coerce.number().int().positive().default(1).describe('Quantity of items to check availability for'),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Download items don't require quantity (always 1)
|
// Download items don't require quantity (always 1)
|
||||||
const DownloadItemSchema = z.object({
|
const DownloadItemSchema = z.object({
|
||||||
itemId: z.coerce.number().int().positive(),
|
itemId: z.coerce.number().int().positive().describe('Unique item identifier'),
|
||||||
ean: z.string(),
|
ean: z.string().describe('European Article Number barcode'),
|
||||||
price: PriceSchema.optional(),
|
price: PriceSchema.describe('Item price information').optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const ItemsSchema = z.array(ItemSchema).min(1);
|
const ItemsSchema = z.array(ItemSchema).min(1).describe('List of items to check availability for');
|
||||||
const DownloadItemsSchema = z.array(DownloadItemSchema).min(1);
|
const DownloadItemsSchema = z.array(DownloadItemSchema).min(1).describe('List of download items to check availability for');
|
||||||
|
|
||||||
// In-Store availability (Rücklage) - requires branch context
|
// In-Store availability (Rücklage) - requires branch context
|
||||||
export const GetInStoreAvailabilityParamsSchema = z.object({
|
export const GetInStoreAvailabilityParamsSchema = z.object({
|
||||||
orderType: z.literal(OrderType.InStore),
|
orderType: z.literal(OrderType.InStore).describe('Order type specifying in-store availability check'),
|
||||||
branchId: z.coerce.number().int().positive().optional(),
|
branchId: z.coerce.number().int().positive().describe('Branch identifier for in-store availability').optional(),
|
||||||
itemsIds: z.array(z.coerce.number().int().positive()).min(1),
|
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
|
// Pickup availability (Abholung) - requires branch context
|
||||||
export const GetPickupAvailabilityParamsSchema = z.object({
|
export const GetPickupAvailabilityParamsSchema = z.object({
|
||||||
orderType: z.literal(OrderType.Pickup),
|
orderType: z.literal(OrderType.Pickup).describe('Order type specifying pickup availability check'),
|
||||||
branchId: z.coerce.number().int().positive(),
|
branchId: z.coerce.number().int().positive().describe('Branch identifier where items will be picked up'),
|
||||||
items: ItemsSchema,
|
items: ItemsSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Standard delivery availability (Versand)
|
// Standard delivery availability (Versand)
|
||||||
export const GetDeliveryAvailabilityParamsSchema = z.object({
|
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,
|
items: ItemsSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
// DIG delivery availability (DIG-Versand) - for webshop customers
|
// DIG delivery availability (DIG-Versand) - for webshop customers
|
||||||
export const GetDigDeliveryAvailabilityParamsSchema = z.object({
|
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,
|
items: ItemsSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
// B2B delivery availability (B2B-Versand) - uses default branch
|
// B2B delivery availability (B2B-Versand) - uses default branch
|
||||||
export const GetB2bDeliveryAvailabilityParamsSchema = z.object({
|
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,
|
items: ItemsSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Download availability - quantity always 1
|
// Download availability - quantity always 1
|
||||||
export const GetDownloadAvailabilityParamsSchema = z.object({
|
export const GetDownloadAvailabilityParamsSchema = z.object({
|
||||||
orderType: z.literal(OrderType.Download),
|
orderType: z.literal(OrderType.Download).describe('Order type specifying download availability check'),
|
||||||
items: DownloadItemsSchema,
|
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
|
// Single-item schemas use the same structure but accept a single item instead of an array
|
||||||
const SingleInStoreAvailabilityParamsSchema = z.object({
|
const SingleInStoreAvailabilityParamsSchema = z.object({
|
||||||
orderType: z.literal(OrderType.InStore),
|
orderType: z.literal(OrderType.InStore).describe('Order type specifying in-store availability check'),
|
||||||
branchId: z.coerce.number().int().positive(),
|
branchId: z.coerce.number().int().positive().describe('Branch identifier for in-store availability').optional(),
|
||||||
itemId: z.number().int().positive(),
|
itemId: z.number().int().positive().describe('Unique item identifier to check in-store availability'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const SinglePickupAvailabilityParamsSchema = z.object({
|
const SinglePickupAvailabilityParamsSchema = z.object({
|
||||||
orderType: z.literal(OrderType.Pickup),
|
orderType: z.literal(OrderType.Pickup).describe('Order type specifying pickup availability check'),
|
||||||
branchId: z.coerce.number().int().positive(),
|
branchId: z.coerce.number().int().positive().describe('Branch identifier where item will be picked up'),
|
||||||
item: ItemSchema,
|
item: ItemSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
const SingleDeliveryAvailabilityParamsSchema = z.object({
|
const SingleDeliveryAvailabilityParamsSchema = z.object({
|
||||||
orderType: z.literal(OrderType.Delivery),
|
orderType: z.literal(OrderType.Delivery).describe('Order type specifying standard delivery availability check'),
|
||||||
item: ItemSchema,
|
item: ItemSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
const SingleDigDeliveryAvailabilityParamsSchema = z.object({
|
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,
|
item: ItemSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
const SingleB2bDeliveryAvailabilityParamsSchema = z.object({
|
const SingleB2bDeliveryAvailabilityParamsSchema = z.object({
|
||||||
orderType: z.literal(OrderType.B2BShipping),
|
orderType: z.literal(OrderType.B2BShipping).describe('Order type specifying B2B delivery availability check'),
|
||||||
item: ItemSchema,
|
item: ItemSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
const SingleDownloadAvailabilityParamsSchema = z.object({
|
const SingleDownloadAvailabilityParamsSchema = z.object({
|
||||||
orderType: z.literal(OrderType.Download),
|
orderType: z.literal(OrderType.Download).describe('Order type specifying download availability check'),
|
||||||
item: DownloadItemSchema,
|
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.
|
* Used for sorting search results.
|
||||||
*/
|
*/
|
||||||
export const OrderBySchema = z.object({
|
export const OrderBySchema = z.object({
|
||||||
by: z.string(), // Field name to sort by
|
by: z.string().describe('Field name to sort by'),
|
||||||
label: z.string(), // Display label for the sort option
|
label: z.string().describe('Display label for the sort option'),
|
||||||
desc: z.boolean(), // Whether sorting is descending
|
desc: z.boolean().describe('Whether sorting is descending'),
|
||||||
selected: z.boolean(), // Whether this sort option is currently selected
|
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.
|
* Used for search operations to ensure consistent query structure.
|
||||||
*/
|
*/
|
||||||
export const QueryTokenSchema = z.object({
|
export const QueryTokenSchema = z.object({
|
||||||
filter: z.record(z.any()).default({}), // Filter criteria as key-value pairs
|
filter: z.record(z.any()).describe('Filter criteria as key-value pairs').default({}),
|
||||||
input: z.record(z.string()).default({}).optional(),
|
input: z.record(z.string()).describe('Input parameters as key-value pairs').default({}).optional(),
|
||||||
orderBy: z.array(OrderBySchema).default([]).optional(), // Sorting parameters
|
orderBy: z.array(OrderBySchema).describe('Sorting parameters').default([]).optional(),
|
||||||
skip: z.number().default(0),
|
skip: z.number().describe('Number of items to skip for pagination').default(0),
|
||||||
take: z.number().default(25),
|
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 { PriceDTO, Price } from '@generated/swagger/checkout-api';
|
||||||
import { Availability as AvaAvailability } from '@isa/availability/data-access';
|
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)
|
* Availability data from catalogue-api (raw response)
|
||||||
@@ -55,6 +55,8 @@ export class AvailabilityAdapter {
|
|||||||
data: {
|
data: {
|
||||||
id: catalogueAvailability.supplier.id,
|
id: catalogueAvailability.supplier.id,
|
||||||
},
|
},
|
||||||
|
// Explicitly omit externalReference to avoid TypeScript errors
|
||||||
|
// (generated DTOs require externalStatus when externalReference is present)
|
||||||
},
|
},
|
||||||
isPrebooked: catalogueAvailability.isPrebooked,
|
isPrebooked: catalogueAvailability.isPrebooked,
|
||||||
estimatedShippingDate: catalogueAvailability.estimatedShippingDate,
|
estimatedShippingDate: catalogueAvailability.estimatedShippingDate,
|
||||||
@@ -78,6 +80,8 @@ export class AvailabilityAdapter {
|
|||||||
data: {
|
data: {
|
||||||
id: catalogueAvailability.logistician.id,
|
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: {
|
data: {
|
||||||
id: availability.logisticianId,
|
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: {
|
data: {
|
||||||
id: availability.supplierId,
|
id: availability.supplierId,
|
||||||
},
|
},
|
||||||
|
// Explicitly omit externalReference to avoid TypeScript errors
|
||||||
|
// (generated DTOs require externalStatus when externalReference is present)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Customer, AssignedPayer } from '@isa/crm/data-access';
|
import { Customer, AssignedPayer } from '@isa/crm/data-access';
|
||||||
import { Buyer, Payer } from '../models';
|
import { Buyer, Payer } from '../schemas';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adapter for converting CRM customer data to checkout-api format.
|
* Adapter for converting CRM customer data to checkout-api format.
|
||||||
@@ -42,7 +42,11 @@ export class CustomerAdapter {
|
|||||||
static toBuyer(customer: Customer): Buyer {
|
static toBuyer(customer: Customer): Buyer {
|
||||||
return {
|
return {
|
||||||
source: customer.id,
|
source: customer.id,
|
||||||
reference: { id: customer.id },
|
reference: {
|
||||||
|
id: customer.id,
|
||||||
|
// Explicitly omit externalReference to avoid TypeScript errors
|
||||||
|
// (generated DTOs require externalStatus when externalReference is present)
|
||||||
|
},
|
||||||
buyerType: customer.customerType,
|
buyerType: customer.customerType,
|
||||||
buyerNumber: customer.customerNumber,
|
buyerNumber: customer.customerNumber,
|
||||||
gender: customer.gender,
|
gender: customer.gender,
|
||||||
@@ -86,7 +90,11 @@ export class CustomerAdapter {
|
|||||||
*/
|
*/
|
||||||
static toPayerFromCustomer(customer: Customer): Payer {
|
static toPayerFromCustomer(customer: Customer): Payer {
|
||||||
return {
|
return {
|
||||||
reference: { id: customer.id },
|
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'],
|
payerType: customer.customerType as Payer['payerType'],
|
||||||
payerNumber: customer.customerNumber,
|
payerNumber: customer.customerNumber,
|
||||||
payerStatus: 0, // Default status: active
|
payerStatus: 0, // Default status: active
|
||||||
@@ -138,9 +146,7 @@ export class CustomerAdapter {
|
|||||||
* }
|
* }
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
static toPayerFromAssignedPayer(
|
static toPayerFromAssignedPayer(assignedPayer: AssignedPayer): Payer | null {
|
||||||
assignedPayer: AssignedPayer,
|
|
||||||
): Payer | null {
|
|
||||||
const payer = assignedPayer?.payer?.data;
|
const payer = assignedPayer?.payer?.data;
|
||||||
|
|
||||||
if (!payer) {
|
if (!payer) {
|
||||||
@@ -148,7 +154,11 @@ export class CustomerAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
reference: { id: payer.id },
|
reference: {
|
||||||
|
id: payer.id,
|
||||||
|
// Explicitly omit externalReference to avoid TypeScript errors
|
||||||
|
// (generated DTOs require externalStatus when externalReference is present)
|
||||||
|
},
|
||||||
payerType: payer.payerType,
|
payerType: payer.payerType,
|
||||||
payerNumber: payer.payerNumber,
|
payerNumber: payer.payerNumber,
|
||||||
payerStatus: payer.payerStatus,
|
payerStatus: payer.payerStatus,
|
||||||
@@ -198,4 +208,12 @@ export class CustomerAdapter {
|
|||||||
typeof assignedPayer.payer.id === 'number'
|
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,5 +1,5 @@
|
|||||||
import { Payer as CrmPayer } from '@isa/crm/data-access';
|
import { Payer as CrmPayer } from '@isa/crm/data-access';
|
||||||
import { Payer } from '../models';
|
import { Payer } from '../schemas';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adapter for converting CRM-api payer responses to checkout-api format.
|
* Adapter for converting CRM-api payer responses to checkout-api format.
|
||||||
@@ -66,8 +66,7 @@ export class PayerAdapter {
|
|||||||
const payer = value as CrmPayer;
|
const payer = value as CrmPayer;
|
||||||
return (
|
return (
|
||||||
typeof payer.id === 'number' &&
|
typeof payer.id === 'number' &&
|
||||||
(payer.payerNumber === undefined ||
|
(payer.payerNumber === undefined || typeof payer.payerNumber === 'string')
|
||||||
typeof payer.payerNumber === 'string')
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Customer } from '@isa/crm/data-access';
|
import { Customer } from '@isa/crm/data-access';
|
||||||
import { ShippingAddressDTO as CrmShippingAddressDTO } from '@generated/swagger/crm-api';
|
import { ShippingAddress as CrmShippingAddress } from '@isa/crm/data-access';
|
||||||
import { ShippingAddressDTO as CheckoutShippingAddressDTO } from '@generated/swagger/checkout-api';
|
import { ShippingAddress as CheckoutShippingAddress } from '@isa/checkout/data-access';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adapter for converting CRM shipping address data to checkout-api format.
|
* Adapter for converting CRM shipping address data to checkout-api format.
|
||||||
@@ -44,8 +44,8 @@ export class ShippingAddressAdapter {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
static fromCrmShippingAddress(
|
static fromCrmShippingAddress(
|
||||||
address: CrmShippingAddressDTO,
|
address: CrmShippingAddress,
|
||||||
): CheckoutShippingAddressDTO {
|
): CheckoutShippingAddress {
|
||||||
return {
|
return {
|
||||||
reference: { id: address.id },
|
reference: { id: address.id },
|
||||||
gender: address.gender,
|
gender: address.gender,
|
||||||
@@ -85,7 +85,7 @@ export class ShippingAddressAdapter {
|
|||||||
* await checkoutService.complete({ shippingAddress: checkoutAddress, ... });
|
* await checkoutService.complete({ shippingAddress: checkoutAddress, ... });
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
static fromCustomer(customer: Customer): CheckoutShippingAddressDTO {
|
static fromCustomer(customer: Customer): CheckoutShippingAddress {
|
||||||
return {
|
return {
|
||||||
reference: { id: customer.id },
|
reference: { id: customer.id },
|
||||||
gender: customer.gender,
|
gender: customer.gender,
|
||||||
@@ -110,10 +110,10 @@ export class ShippingAddressAdapter {
|
|||||||
*/
|
*/
|
||||||
static isValidCrmShippingAddress(
|
static isValidCrmShippingAddress(
|
||||||
value: unknown,
|
value: unknown,
|
||||||
): value is CrmShippingAddressDTO {
|
): value is CrmShippingAddress {
|
||||||
if (typeof value !== 'object' || value === null) return false;
|
if (typeof value !== 'object' || value === null) return false;
|
||||||
|
|
||||||
const address = value as CrmShippingAddressDTO;
|
const address = value as CrmShippingAddress;
|
||||||
return typeof address.id === 'number';
|
return typeof address.id === 'number';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 { inject, Injectable } from '@angular/core';
|
||||||
import { ShoppingCartService, CheckoutService } from '../services';
|
|
||||||
import {
|
import {
|
||||||
CompleteCheckoutParams,
|
ShoppingCartService,
|
||||||
|
CheckoutService,
|
||||||
|
CheckoutMetadataService,
|
||||||
|
} from '../services';
|
||||||
|
import {
|
||||||
|
CompleteOrderParams,
|
||||||
RemoveShoppingCartItemParams,
|
RemoveShoppingCartItemParams,
|
||||||
UpdateShoppingCartItemParams,
|
UpdateShoppingCartItemParams,
|
||||||
|
CompleteCrmOrderParamsSchema,
|
||||||
|
CompleteCrmOrderParams,
|
||||||
} from '../schemas';
|
} from '../schemas';
|
||||||
import { Order } from '../models';
|
import { Order } from '../models';
|
||||||
|
import {
|
||||||
|
CustomerAdapter,
|
||||||
|
ShippingAddressAdapter,
|
||||||
|
PayerAdapter,
|
||||||
|
} from '../adapters';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class ShoppingCartFacade {
|
export class ShoppingCartFacade {
|
||||||
@@ -31,10 +42,87 @@ export class ShoppingCartFacade {
|
|||||||
return this.#shoppingCartService.updateItem(params);
|
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,
|
abortSignal?: AbortSignal,
|
||||||
): Promise<Order[]> {
|
): Promise<number> {
|
||||||
return this.#checkoutService.complete(params, abortSignal);
|
// 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,9 +1,9 @@
|
|||||||
import { EntityDTOContainerOfShoppingCartItemDTO } from '@generated/swagger/checkout-api';
|
|
||||||
import {
|
import {
|
||||||
CustomerTypeAnalysis,
|
CustomerTypeAnalysis,
|
||||||
OrderOptionsAnalysis,
|
OrderOptionsAnalysis,
|
||||||
ShoppingCartItem,
|
ShoppingCartItem,
|
||||||
} from '../models';
|
} from '../models';
|
||||||
|
import { EntityContainer } from '@isa/common/data-access';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Analyzes shopping cart items to determine which order types are present.
|
* Analyzes shopping cart items to determine which order types are present.
|
||||||
@@ -34,7 +34,7 @@ import {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export function analyzeOrderOptions(
|
export function analyzeOrderOptions(
|
||||||
items: EntityDTOContainerOfShoppingCartItemDTO[],
|
items: EntityContainer<ShoppingCartItem>[],
|
||||||
): OrderOptionsAnalysis {
|
): OrderOptionsAnalysis {
|
||||||
return {
|
return {
|
||||||
hasTakeAway: items.some(
|
hasTakeAway: items.some(
|
||||||
|
|||||||
@@ -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 const BranchType = {
|
||||||
|
|
||||||
export type BranchTypeEnum = BranchType;
|
|
||||||
|
|
||||||
export const BranchTypeEnum = {
|
|
||||||
NotSet: 0,
|
NotSet: 0,
|
||||||
Store: 1,
|
Store: 1,
|
||||||
WebStore: 2,
|
WebStore: 2,
|
||||||
CallCenter: 4,
|
CallCenter: 4,
|
||||||
Headquarter: 8,
|
Headquarter: 8,
|
||||||
|
Warehouse: 16,
|
||||||
} as const;
|
} 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 './branch-type';
|
||||||
export * from './availability';
|
|
||||||
export * from './branch';
|
|
||||||
export * from './buyer';
|
|
||||||
export * from './campaign';
|
export * from './campaign';
|
||||||
export * from './checkout-item';
|
export * from './checkout-item';
|
||||||
export * from './checkout';
|
export * from './checkout';
|
||||||
export * from './customer-type-analysis';
|
export * from './customer-type-analysis';
|
||||||
export * from './destination';
|
|
||||||
export * from './gender';
|
export * from './gender';
|
||||||
export * from './loyalty';
|
export * from './loyalty';
|
||||||
export * from './ola-availability';
|
export * from './ola-availability';
|
||||||
export * from './order-options';
|
export * from './order-options';
|
||||||
export * from './order-type';
|
export * from './order-type';
|
||||||
export * from './order';
|
export * from './order';
|
||||||
export * from './payer';
|
|
||||||
export * from './price';
|
export * from './price';
|
||||||
export * from './product';
|
export * from './product';
|
||||||
export * from './promotion';
|
export * from './promotion';
|
||||||
|
export * from './reward-selection-item';
|
||||||
export * from './shipping-address';
|
export * from './shipping-address';
|
||||||
export * from './shipping-target';
|
export * from './shipping-target';
|
||||||
export * from './shopping-cart-item';
|
export * from './shopping-cart-item';
|
||||||
export * from './supplier';
|
|
||||||
export * from './shopping-cart';
|
export * from './shopping-cart';
|
||||||
export * from './update-shopping-cart-item';
|
export * from './update-shopping-cart-item';
|
||||||
export * from './vat-type';
|
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 { z } from 'zod';
|
||||||
import {
|
import { EntityContainerSchema } from '@isa/common/data-access';
|
||||||
AvailabilityDTOSchema,
|
import { AvailabilitySchema } from './availability.schema';
|
||||||
CampaignDTOSchema,
|
import { CampaignSchema } from './campaign.schema';
|
||||||
LoyaltyDTOSchema,
|
import { DestinationSchema } from './destination.schema';
|
||||||
ProductDTOSchema,
|
import { ItemTypeSchema } from './item-type.schema';
|
||||||
PromotionDTOSchema,
|
import { LoyaltySchema } from './loyalty.schema';
|
||||||
PriceSchema,
|
import { PriceFlatSchema } from './price-flat.schema';
|
||||||
EntityDTOContainerOfDestinationDTOSchema,
|
import { ProductSchema } from './product.schema';
|
||||||
ItemTypeSchema,
|
import { PromotionSchema } from './promotion.schema';
|
||||||
PriceValueSchema,
|
|
||||||
} from './base-schemas';
|
|
||||||
|
|
||||||
const AddToShoppingCartDefaultSchema = z.object({
|
// Base schema for all add to shopping cart items
|
||||||
availability: AvailabilityDTOSchema,
|
const AddToShoppingCartBaseSchema = z.object({
|
||||||
campaign: CampaignDTOSchema,
|
availability: AvailabilitySchema.describe('Availability'),
|
||||||
destination: EntityDTOContainerOfDestinationDTOSchema,
|
campaign: CampaignSchema.describe('Campaign information').optional(),
|
||||||
itemType: ItemTypeSchema,
|
destination: EntityContainerSchema(DestinationSchema).describe(
|
||||||
product: ProductDTOSchema,
|
'Destination information',
|
||||||
promotion: PromotionDTOSchema,
|
),
|
||||||
quantity: z.number().int().positive(),
|
itemType: ItemTypeSchema.describe('Item type').optional(),
|
||||||
retailPrice: PriceSchema,
|
loyalty: LoyaltySchema.describe('Loyalty information').optional(),
|
||||||
shopItemId: z.number().int().positive().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
|
// Apply business rules validation
|
||||||
// and availability must not contain a price
|
const AddToShoppingCartSchema = AddToShoppingCartBaseSchema.refine(
|
||||||
// and loyalty must be present
|
(data) => {
|
||||||
const AddToShoppingCartWithRedemptionPointsSchema =
|
// When loyalty points are used, promotion must not be present
|
||||||
AddToShoppingCartDefaultSchema.omit({
|
if (data.loyalty && data.promotion) {
|
||||||
availability: true,
|
return false;
|
||||||
promotion: true,
|
}
|
||||||
}).extend({
|
return true;
|
||||||
availability: AvailabilityDTOSchema.unwrap()
|
},
|
||||||
.omit({ price: true })
|
{
|
||||||
.extend({
|
message: 'Promotion is not allowed when using loyalty points',
|
||||||
price: PriceSchema.unwrap()
|
},
|
||||||
.omit({ value: true })
|
).refine(
|
||||||
.extend({
|
(data) => {
|
||||||
value: PriceValueSchema.omit({ value: true }).extend({ value: z.literal(0) }),
|
// When loyalty points are used, price value should be 0
|
||||||
}),
|
if (data.loyalty && data.availability?.price?.value?.value !== 0) {
|
||||||
}),
|
return false;
|
||||||
loyalty: LoyaltyDTOSchema,
|
}
|
||||||
});
|
return true;
|
||||||
|
},
|
||||||
const AddToShoppingCartSchema = z.union([
|
{
|
||||||
AddToShoppingCartDefaultSchema,
|
message: 'Price value must be 0 when using loyalty points for redemption',
|
||||||
AddToShoppingCartWithRedemptionPointsSchema,
|
},
|
||||||
]);
|
);
|
||||||
|
|
||||||
export const AddItemToShoppingCartParamsSchema = z.object({
|
export const AddItemToShoppingCartParamsSchema = z.object({
|
||||||
shoppingCartId: z.number().int().positive(),
|
shoppingCartId: z
|
||||||
items: z.array(AddToShoppingCartSchema).min(1),
|
.number()
|
||||||
|
.int()
|
||||||
|
.positive()
|
||||||
|
.describe('Shopping cart identifier'),
|
||||||
|
items: z.array(AddToShoppingCartSchema).min(1).describe('List of items'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type AddItemToShoppingCartParams = z.infer<
|
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 { 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
|
// OrderType is a union of specific string literals
|
||||||
export { PriceValueSchema } from '@isa/common/data-access';
|
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');
|
||||||
|
|
||||||
// ItemType from generated API - it's a numeric bitwise enum
|
export type OrderType = z.infer<typeof OrderTypeSchema>;
|
||||||
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,
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -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 { z } from 'zod';
|
||||||
import { OrderTypeSchema } from './base-schemas';
|
import { OrderTypeSchema } from './base-schemas';
|
||||||
|
|
||||||
const CanAddPriceSchema = z.object({
|
const CanAddPriceSchema = z.object({
|
||||||
value: z
|
value: z
|
||||||
|
|
||||||
.object({
|
.object({
|
||||||
value: z.number().optional(),
|
value: z.number().describe('Value').optional(),
|
||||||
currency: z.string().optional(),
|
currency: z.string().describe('Currency code').optional(),
|
||||||
currencySymbol: z.string().optional(),
|
currencySymbol: z.string().describe('Currency symbol').optional(),
|
||||||
})
|
})
|
||||||
|
.describe('Value')
|
||||||
.optional(),
|
.optional(),
|
||||||
vat: z
|
vat: z
|
||||||
|
|
||||||
.object({
|
.object({
|
||||||
inPercent: z.number().optional(),
|
inPercent: z.number().describe('In percent').optional(),
|
||||||
label: z.string().optional(),
|
label: z.string().describe('Label').optional(),
|
||||||
value: z.number().optional(),
|
value: z.number().describe('Value').optional(),
|
||||||
vatType: z.number().optional(),
|
vatType: z.number().describe('VAT type').optional(),
|
||||||
})
|
})
|
||||||
|
.describe('Value Added Tax')
|
||||||
.optional(),
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const CanAddOLAAvailabilitySchema = z.object({
|
const CanAddOLAAvailabilitySchema = z.object({
|
||||||
altAt: z.string().optional(),
|
altAt: z.string().describe('Alt at').optional(),
|
||||||
at: z.string().optional(),
|
at: z.string().describe('At').optional(),
|
||||||
ean: z.string().optional(),
|
ean: z.string().describe('European Article Number barcode').optional(),
|
||||||
format: z.string().optional(),
|
format: z.string().describe('Format').optional(),
|
||||||
isPrebooked: z.boolean().optional(),
|
isPrebooked: z.boolean().describe('Whether prebooked').optional(),
|
||||||
itemId: z.number().int().optional(),
|
itemId: z.number().int().describe('Unique item identifier').optional(),
|
||||||
logistician: z.string().optional(),
|
logistician: z.string().describe('Logistician information').optional(),
|
||||||
logisticianId: z.number().int().optional(),
|
logisticianId: z.number().int().describe('Logistician identifier').optional(),
|
||||||
preferred: z.number().int().optional(),
|
preferred: z.number().int().describe('Preferred').optional(),
|
||||||
price: CanAddPriceSchema.optional(),
|
price: CanAddPriceSchema.describe('Price information').optional(),
|
||||||
qty: z.number().int().optional(),
|
qty: z.number().int().describe('Qty').optional(),
|
||||||
shop: z.number().int().optional(),
|
shop: z.number().int().describe('Shop').optional(),
|
||||||
ssc: z.string().optional(),
|
ssc: z.string().describe('Ssc').optional(),
|
||||||
sscText: z.string().optional(),
|
sscText: z.string().describe('Ssc text').optional(),
|
||||||
status: z.number().int(),
|
status: z.number().int().describe('Current status'),
|
||||||
supplier: z.string().optional(),
|
supplier: z.string().describe('Supplier information').optional(),
|
||||||
supplierId: z.number().int().optional(),
|
supplierId: z.number().int().describe('Supplier identifier').optional(),
|
||||||
supplierProductNumber: z.string().optional(),
|
supplierProductNumber: z
|
||||||
|
.string()
|
||||||
|
.describe('SupplierProduct number')
|
||||||
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const CanAddItemPayloadSchema = z.object({
|
const CanAddItemPayloadSchema = z.object({
|
||||||
availabilities: z.array(CanAddOLAAvailabilitySchema),
|
availabilities: z
|
||||||
customerFeatures: z.record(z.string().optional()),
|
.array(CanAddOLAAvailabilitySchema)
|
||||||
orderType: OrderTypeSchema,
|
.describe('Availabilities'),
|
||||||
id: z.string(),
|
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({
|
export const CanAddItemsToShoppingCartParamsSchema = z.object({
|
||||||
shoppingCartId: z.number().int().positive(),
|
shoppingCartId: z
|
||||||
payload: z.array(CanAddItemPayloadSchema).min(1),
|
.number()
|
||||||
|
.int()
|
||||||
|
.positive()
|
||||||
|
.describe('Shopping cart identifier'),
|
||||||
|
payload: z.array(CanAddItemPayloadSchema).min(1).describe('Payload'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type CanAddItemsToShoppingCartParams = z.infer<
|
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 './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 './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 './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 './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';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const RemoveShoppingCartItemParamsSchema = z.object({
|
export const RemoveShoppingCartItemParamsSchema = z.object({
|
||||||
shoppingCartId: z.number().int().positive(),
|
shoppingCartId: z.number().int().positive().describe('Shopping cart identifier'),
|
||||||
shoppingCartItemId: z.number().int().positive(),
|
shoppingCartItemId: z.number().int().positive().describe('Shopping cart item identifier'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type RemoveShoppingCartItemParams = z.infer<
|
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';
|
import { AddresseeWithReferenceSchema } from '@isa/common/data-access';
|
||||||
|
|
||||||
export const ShippingAddressSchema = AddresseeWithReferenceSchema.extend({});
|
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 { z } from 'zod';
|
||||||
import {
|
|
||||||
AvailabilityDTOSchema,
|
|
||||||
CampaignDTOSchema,
|
|
||||||
LoyaltyDTOSchema,
|
|
||||||
PromotionDTOSchema,
|
|
||||||
PriceSchema,
|
|
||||||
EntityDTOContainerOfDestinationDTOSchema,
|
|
||||||
} from './base-schemas';
|
|
||||||
import { UpdateShoppingCartItem } from '../models';
|
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({
|
const UpdateShoppingCartItemParamsValueDefaultSchema = z.object({
|
||||||
availability: AvailabilityDTOSchema,
|
availability: AvailabilitySchema.describe(
|
||||||
buyerComment: z.string().optional(),
|
'Availability information',
|
||||||
campaign: CampaignDTOSchema,
|
).optional(),
|
||||||
destination: EntityDTOContainerOfDestinationDTOSchema,
|
buyerComment: z.string().describe('Buyer comment text').optional(),
|
||||||
loyalty: LoyaltyDTOSchema,
|
campaign: CampaignSchema.describe('Campaign information').optional(),
|
||||||
promotion: PromotionDTOSchema,
|
destination: EntityContainerSchema(DestinationSchema)
|
||||||
quantity: z.number().int().positive().optional(),
|
.optional()
|
||||||
retailPrice: PriceSchema,
|
.describe('Destination information'),
|
||||||
specialComment: z.string().optional(),
|
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
|
// When loyalty points are used the price value must be 0 and promotion is not allowed
|
||||||
@@ -29,30 +31,48 @@ const UpdateShoppingCartItemParamsValueWithRedemptionPointsSchema =
|
|||||||
availability: true,
|
availability: true,
|
||||||
promotion: true,
|
promotion: true,
|
||||||
}).extend({
|
}).extend({
|
||||||
availability: AvailabilityDTOSchema.unwrap()
|
availability: AvailabilitySchema.omit({ price: true })
|
||||||
.omit({ price: true })
|
|
||||||
.extend({
|
.extend({
|
||||||
price: PriceSchema.unwrap()
|
price: PriceSchema.omit({ value: true })
|
||||||
.omit({ value: true })
|
|
||||||
.extend({
|
.extend({
|
||||||
value: z.object({
|
value: z
|
||||||
value: z.literal(0),
|
.object({
|
||||||
currency: z.string(),
|
value: z
|
||||||
}),
|
.literal(0)
|
||||||
}),
|
.describe('Price value must be 0 for loyalty redemption'),
|
||||||
}),
|
currency: z.string().describe('Currency code'),
|
||||||
loyalty: LoyaltyDTOSchema,
|
})
|
||||||
|
.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([
|
const UpdateShoppingCartItemParamsValueSchema = z
|
||||||
UpdateShoppingCartItemParamsValueDefaultSchema,
|
.union([
|
||||||
UpdateShoppingCartItemParamsValueWithRedemptionPointsSchema,
|
UpdateShoppingCartItemParamsValueDefaultSchema,
|
||||||
]);
|
UpdateShoppingCartItemParamsValueWithRedemptionPointsSchema,
|
||||||
|
])
|
||||||
|
.describe('Update shopping cart item params value');
|
||||||
|
|
||||||
export const UpdateShoppingCartItemParamsSchema = z.object({
|
export const UpdateShoppingCartItemParamsSchema = z.object({
|
||||||
shoppingCartId: z.number().int().positive(),
|
shoppingCartId: z
|
||||||
shoppingCartItemId: z.number().int().positive(),
|
.number()
|
||||||
values: UpdateShoppingCartItemParamsValueSchema,
|
.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 = {
|
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 { logger } from '@isa/core/logging';
|
||||||
import { firstValueFrom } from 'rxjs';
|
import { firstValueFrom } from 'rxjs';
|
||||||
import { Cache, CacheTimeToLive, InFlight } from '@isa/common/decorators';
|
import { Cache, CacheTimeToLive, InFlight } from '@isa/common/decorators';
|
||||||
import { Branch } from '../models';
|
import { Branch } from '../schemas';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class BranchService {
|
export class BranchService {
|
||||||
|
|||||||
@@ -6,32 +6,38 @@ import {
|
|||||||
StoreCheckoutBuyerService,
|
StoreCheckoutBuyerService,
|
||||||
StoreCheckoutPayerService,
|
StoreCheckoutPayerService,
|
||||||
StoreCheckoutPaymentService,
|
StoreCheckoutPaymentService,
|
||||||
|
DestinationDTO,
|
||||||
BuyerDTO,
|
BuyerDTO,
|
||||||
PayerDTO,
|
PayerDTO,
|
||||||
ShippingAddressDTO,
|
AvailabilityDTO,
|
||||||
NotificationChannel,
|
|
||||||
PaymentType,
|
|
||||||
DestinationDTO,
|
|
||||||
EntityDTOContainerOfShoppingCartItemDTO,
|
|
||||||
} from '@generated/swagger/checkout-api';
|
} 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 { logger } from '@isa/core/logging';
|
||||||
import { firstValueFrom } from 'rxjs';
|
import { firstValueFrom } from 'rxjs';
|
||||||
import { ShoppingCartService } from './shopping-cart.service';
|
import { ShoppingCartService } from './shopping-cart.service';
|
||||||
import {
|
import {
|
||||||
CompleteCheckoutParams,
|
Buyer,
|
||||||
CompleteCheckoutParamsSchema,
|
CompleteOrderParams,
|
||||||
|
CompleteOrderParamsSchema,
|
||||||
|
NotificationChannel,
|
||||||
|
Payer,
|
||||||
|
PaymentType,
|
||||||
} from '../schemas';
|
} from '../schemas';
|
||||||
import {
|
import {
|
||||||
Order,
|
Order,
|
||||||
OrderOptionsAnalysis,
|
OrderOptionsAnalysis,
|
||||||
CustomerTypeAnalysis,
|
CustomerTypeAnalysis,
|
||||||
Checkout,
|
Checkout,
|
||||||
ShoppingCartItem,
|
|
||||||
ShippingAddress,
|
ShippingAddress,
|
||||||
|
ShoppingCartItem,
|
||||||
} from '../models';
|
} from '../models';
|
||||||
import { CheckoutCompletionError } from '../errors';
|
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 { AvailabilityService } from '@isa/catalogue/data-access';
|
||||||
import { BranchService } from '@isa/remission/data-access';
|
import { BranchService } from '@isa/remission/data-access';
|
||||||
import {
|
import {
|
||||||
@@ -47,12 +53,10 @@ import {
|
|||||||
shouldSetPayer,
|
shouldSetPayer,
|
||||||
needsDestinationUpdate,
|
needsDestinationUpdate,
|
||||||
determinePaymentType,
|
determinePaymentType,
|
||||||
PaymentTypes,
|
|
||||||
filterShippingItems,
|
filterShippingItems,
|
||||||
filterDeliveryDestinations,
|
filterDeliveryDestinations,
|
||||||
hasValidItemId,
|
hasValidItemId,
|
||||||
hasValidDestinationData,
|
hasValidDestinationData,
|
||||||
ShippingTargets,
|
|
||||||
} from '../helpers';
|
} from '../helpers';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -82,22 +86,26 @@ export class CheckoutService {
|
|||||||
|
|
||||||
// Domain data-access services
|
// Domain data-access services
|
||||||
#shoppingCartDataService = inject(ShoppingCartService);
|
#shoppingCartDataService = inject(ShoppingCartService);
|
||||||
#orderCreationService = inject(OrderCreationService);
|
#logisticianService = inject(LogisticianService);
|
||||||
#availabilityService = inject(AvailabilityService);
|
#availabilityService = inject(AvailabilityService);
|
||||||
#branchService = inject(BranchService);
|
#branchService = inject(BranchService);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Completes the checkout process, creating orders.
|
* Completes the checkout process.
|
||||||
*
|
*
|
||||||
* @param params - Complete checkout parameters with all required data
|
* @param params - Complete checkout parameters with all required data
|
||||||
* @param abortSignal - Optional cancellation signal
|
* @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 {CheckoutCompletionError} for business logic failures
|
||||||
* @throws {ResponseArgsError} for API 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
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* const orders = await checkoutService.complete({
|
* const checkoutId = await checkoutService.complete({
|
||||||
* checkoutId: 123,
|
* checkoutId: 123,
|
||||||
* shoppingCartId: 456,
|
* shoppingCartId: 456,
|
||||||
* buyer: buyerDTO,
|
* buyer: buyerDTO,
|
||||||
@@ -110,10 +118,10 @@ export class CheckoutService {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
async complete(
|
async complete(
|
||||||
params: CompleteCheckoutParams,
|
params: CompleteOrderParams,
|
||||||
abortSignal?: AbortSignal,
|
abortSignal?: AbortSignal,
|
||||||
): Promise<Order[]> {
|
): Promise<number> {
|
||||||
const validated = CompleteCheckoutParamsSchema.parse(params);
|
const validated = CompleteOrderParamsSchema.parse(params);
|
||||||
|
|
||||||
this.#logger.info('Starting checkout completion');
|
this.#logger.info('Starting checkout completion');
|
||||||
|
|
||||||
@@ -213,7 +221,7 @@ export class CheckoutService {
|
|||||||
this.#logger.debug('Setting notification channels');
|
this.#logger.debug('Setting notification channels');
|
||||||
await this.setNotificationChannelsOnCheckout(
|
await this.setNotificationChannelsOnCheckout(
|
||||||
checkoutId,
|
checkoutId,
|
||||||
validated.notificationChannels,
|
validated.notificationChannels ?? 0,
|
||||||
abortSignal,
|
abortSignal,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -246,14 +254,12 @@ export class CheckoutService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 14: Create order(s)
|
// Checkout completion is done - return checkoutId for order creation
|
||||||
this.#logger.debug('Creating orders');
|
this.#logger.info('Checkout completed successfully', () => ({
|
||||||
const orders =
|
checkoutId,
|
||||||
await this.#orderCreationService.createOrdersFromCheckout(checkoutId);
|
}));
|
||||||
|
|
||||||
this.#logger.info('Checkout completed successfully');
|
return checkoutId;
|
||||||
|
|
||||||
return orders;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Handle HTTP 409 conflict (order already exists)
|
// Handle HTTP 409 conflict (order already exists)
|
||||||
if (error instanceof HttpErrorResponse && error.status === 409) {
|
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.
|
* Analyzes shopping cart items to determine which order types are present.
|
||||||
*/
|
*/
|
||||||
private analyzeOrderOptions(
|
private analyzeOrderOptions(
|
||||||
items: EntityDTOContainerOfShoppingCartItemDTO[],
|
items: EntityContainer<ShoppingCartItem>[],
|
||||||
): OrderOptionsAnalysis {
|
): OrderOptionsAnalysis {
|
||||||
return analyzeOrderOptions(items);
|
return analyzeOrderOptions(items);
|
||||||
}
|
}
|
||||||
@@ -352,7 +358,7 @@ export class CheckoutService {
|
|||||||
*/
|
*/
|
||||||
private async setSpecialCommentOnItems(
|
private async setSpecialCommentOnItems(
|
||||||
shoppingCartId: number,
|
shoppingCartId: number,
|
||||||
items: EntityDTOContainerOfShoppingCartItemDTO[],
|
items: EntityContainer<ShoppingCartItem>[],
|
||||||
comment: string,
|
comment: string,
|
||||||
abortSignal?: AbortSignal,
|
abortSignal?: AbortSignal,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@@ -417,7 +423,7 @@ export class CheckoutService {
|
|||||||
*/
|
*/
|
||||||
private async validateDownloadAvailabilities(
|
private async validateDownloadAvailabilities(
|
||||||
shoppingCartId: number,
|
shoppingCartId: number,
|
||||||
items: EntityDTOContainerOfShoppingCartItemDTO[],
|
items: EntityContainer<ShoppingCartItem>[],
|
||||||
abortSignal?: AbortSignal,
|
abortSignal?: AbortSignal,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Convert checkout-api items to catalogue-api format using adapter
|
// Convert checkout-api items to catalogue-api format using adapter
|
||||||
@@ -478,7 +484,7 @@ export class CheckoutService {
|
|||||||
*/
|
*/
|
||||||
private async updateShippingAvailabilities(
|
private async updateShippingAvailabilities(
|
||||||
shoppingCartId: number,
|
shoppingCartId: number,
|
||||||
items: EntityDTOContainerOfShoppingCartItemDTO[],
|
items: EntityContainer<ShoppingCartItem>[],
|
||||||
abortSignal?: AbortSignal,
|
abortSignal?: AbortSignal,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Filter shipping items
|
// Filter shipping items
|
||||||
@@ -489,7 +495,7 @@ export class CheckoutService {
|
|||||||
// Get branch and logistician in parallel
|
// Get branch and logistician in parallel
|
||||||
const [inventoryBranch, omsLogistician] = await Promise.all([
|
const [inventoryBranch, omsLogistician] = await Promise.all([
|
||||||
this.#branchService.getDefaultBranch(abortSignal),
|
this.#branchService.getDefaultBranch(abortSignal),
|
||||||
this.#orderCreationService.getLogistician('2470', abortSignal),
|
this.#logisticianService.getLogistician2470(abortSignal),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Convert to catalogue format using adapters
|
// Convert to catalogue format using adapters
|
||||||
@@ -566,12 +572,12 @@ export class CheckoutService {
|
|||||||
*/
|
*/
|
||||||
private async setBuyerOnCheckout(
|
private async setBuyerOnCheckout(
|
||||||
checkoutId: number,
|
checkoutId: number,
|
||||||
buyer: BuyerDTO,
|
buyerDTO: Buyer,
|
||||||
abortSignal?: AbortSignal,
|
abortSignal?: AbortSignal,
|
||||||
): Promise<Checkout> {
|
): Promise<Checkout> {
|
||||||
let req$ = this.#buyerService.StoreCheckoutBuyerSetBuyerPOST({
|
let req$ = this.#buyerService.StoreCheckoutBuyerSetBuyerPOST({
|
||||||
checkoutId,
|
checkoutId,
|
||||||
buyerDTO: buyer,
|
buyerDTO,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (abortSignal) {
|
if (abortSignal) {
|
||||||
@@ -594,12 +600,12 @@ export class CheckoutService {
|
|||||||
*/
|
*/
|
||||||
private async setPayerOnCheckout(
|
private async setPayerOnCheckout(
|
||||||
checkoutId: number,
|
checkoutId: number,
|
||||||
payer: PayerDTO,
|
payer: Payer,
|
||||||
abortSignal?: AbortSignal,
|
abortSignal?: AbortSignal,
|
||||||
): Promise<Checkout> {
|
): Promise<Checkout> {
|
||||||
let req$ = this.#payerService.StoreCheckoutPayerSetPayerPOST({
|
let req$ = this.#payerService.StoreCheckoutPayerSetPayerPOST({
|
||||||
checkoutId,
|
checkoutId,
|
||||||
payerDTO: payer as PayerDTO,
|
payerDTO: payer,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (abortSignal) {
|
if (abortSignal) {
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ export class ShoppingCartService {
|
|||||||
async updateItem(
|
async updateItem(
|
||||||
params: UpdateShoppingCartItemParams,
|
params: UpdateShoppingCartItemParams,
|
||||||
): Promise<ShoppingCart> {
|
): Promise<ShoppingCart> {
|
||||||
|
console.log('UpdateShoppingCartItemParams', params);
|
||||||
const parsed = UpdateShoppingCartItemParamsSchema.parse(params);
|
const parsed = UpdateShoppingCartItemParamsSchema.parse(params);
|
||||||
|
|
||||||
const req$ =
|
const req$ =
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ResponseArgsError, takeUntilAborted } from '@isa/common/data-access';
|
|||||||
import { logger } from '@isa/core/logging';
|
import { logger } from '@isa/core/logging';
|
||||||
import { firstValueFrom } from 'rxjs';
|
import { firstValueFrom } from 'rxjs';
|
||||||
import { Cache, CacheTimeToLive, InFlight } from '@isa/common/decorators';
|
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.
|
* 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