Files
ISA-Frontend/libs/checkout/data-access/README.md
Lorenz Hilpert 2b5da00249 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
2025-10-21 14:28:52 +02:00

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

  • 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:

  • webshopisOnline: true
  • guestisGuest: true
  • b2bisB2B: true
  • p4mUserhasCustomerCard: true
  • staffisStaff: 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 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:

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:

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:

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:

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:

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:

  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

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

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

  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.