mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
- 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
792 lines
26 KiB
Markdown
792 lines
26 KiB
Markdown
# @isa/checkout/data-access
|
|
|
|
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.
|
|
|
|
## Overview
|
|
|
|
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.
|