- 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
26 KiB
@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
- Quick Start
- Core Concepts
- API Reference
- Usage Examples
- Order Types
- Checkout Flow
- Reward System
- Data Transformation
- Error Handling
- Testing
- 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
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
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
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
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
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:
/**
* 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:
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:
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: trueguest→isGuest: trueb2b→isB2B: truep4mUser→hasCustomerCard: truestaff→isStaff: true
Reward System Architecture
The reward catalog store manages reward items and selections with automatic tab isolation:
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:
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 fetchabortSignal?: AbortSignal- Optional cancellation signal
Returns: Promise resolving to the shopping cart, or undefined if not found
Throws:
ResponseArgsError- If API returns an error
Example:
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: RemoveShoppingCartItemParamsshoppingCartId: number- Shopping cart IDshoppingCartItemId: number- Item ID to remove
Returns: Promise resolving to updated shopping cart
Throws:
ZodError- If params validation failsResponseArgsError- If API returns an error
Example:
const updatedCart = await facade.removeItem({
shoppingCartId: 123,
shoppingCartItemId: 456
});
updateItem(params): Promise<ShoppingCart>
Updates a shopping cart item (quantity, special comment, etc.).
Parameters:
params: UpdateShoppingCartItemParamsshoppingCartId: number- Shopping cart IDshoppingCartItemId: number- Item ID to updatevalues: UpdateShoppingCartItemDTO- Fields to update
Returns: Promise resolving to updated shopping cart
Throws:
ZodError- If params validation failsResponseArgsError- If API returns an error
Example:
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: CompleteOrderParamsshoppingCartId: number- Shopping cart to processbuyer: Buyer- Buyer informationnotificationChannels?: NotificationChannel- Communication channelscustomerFeatures: Record<string, string>- Customer feature flagspayer?: 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 failuresResponseArgsError- For API failures
Example:
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: CompleteCrmOrderParamsshoppingCartId: number- Shopping cart to processcrmCustomer: Customer- Customer from CRM servicecrmPayer?: PayerDTO- Payer from CRM service (optional)crmShippingAddress?: ShippingAddressDTO- Shipping address from CRM (optional)notificationChannels?: NotificationChannel- Communication channelsspecialComment?: string- Special instructions
abortSignal?: AbortSignal- Optional cancellation signal
Returns: Promise resolving to array of created orders
Throws:
CheckoutCompletionError- For validation or business logic failuresResponseArgsError- For API failures
Transformation Steps:
- Validates input with Zod schema
- Converts
crmCustomertobuyerusingCustomerAdapter.toBuyer() - Converts
crmShippingAddressusingShippingAddressAdapter.fromCrmShippingAddress() - Converts
crmPayerusingPayerAdapter.toCheckoutFormat() - Extracts customer features using
CustomerAdapter.extractCustomerFeatures() - Delegates to
complete()with transformed data
Example:
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:
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:
- If all items have
loyalty.value > 0: FREE (2) - Else if has delivery/download orders: INVOICE (128)
- 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 buyertoPayerFromCustomer(customer): Payer- Converts customer to payer (self-paying)toPayerFromAssignedPayer(assignedPayer): Payer | null- Unwraps AssignedPayer containerextractCustomerFeatures(customer): Record<string, string>- Extracts feature flags
Key Differences:
- Buyer: Includes
sourcefield anddateOfBirth - Payer from Customer: No
source, nodateOfBirth, setspayerStatus: 0 - Payer from AssignedPayer: Includes
sourcefield, unwraps EntityContainer
Usage Examples
Complete Multi-Step Checkout Workflow
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:
- Regular Shopping Cart (
shoppingCartId) - Items purchased with money - 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
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
# 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
- Stateless Checkout Service - All data passed as parameters for testability
- Separation of Checkout and Order Creation - Clean domain boundaries
- Dual Shopping Cart for Rewards - Payment and pricing isolation
- Extensive Adapter Pattern - 8 adapters for cross-domain transformation
- 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 servicezod- Schema validationrxjs- Reactive programming
Path Alias
Import from: @isa/checkout/data-access
License
Internal ISA Frontend library - not for external distribution.