# @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 { // 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 { // 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 { // 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[]; // Cart items with metadata createdAt?: string; // Creation timestamp updatedAt?: string; // Last update timestamp features?: Record; // 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; // 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; // 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` 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` 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` 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` 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` Completes checkout and creates orders. **Parameters:** - `params: CompleteOrderParams` - `shoppingCartId: number` - Shopping cart to process - `buyer: Buyer` - Buyer information - `notificationChannels?: NotificationChannel` - Communication channels - `customerFeatures: Record` - 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` 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[]` - 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` - 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` - 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: `

Checkout

@if (loading()) {

Processing checkout...

} @if (error()) {
{{ error() }}
}
` }) export class CheckoutFlowComponent { #shoppingCartFacade = inject(ShoppingCartFacade); #customerResource = inject(CustomerResource); loading = signal(false); error = signal(null); orders = signal([]); shoppingCartId = 123; async processCheckout(): Promise { 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.