Files
ISA-Frontend/checkout-completion-flow-documentation.md
Lorenz Hilpert 4fc5f16721 refactor(checkout): consolidate adapters and implement unified checkout completion flow
Refactor checkout data-access layer to use centralized adapter pattern for converting between CRM and Checkout domain models. Extract business logic into dedicated helper modules and implement complete order button component for reward shopping cart.

Changes:
- Add 8 new adapters (availability, branch, customer, logistician, payer, product-number, shipping-address, shopping-cart-item) with comprehensive unit tests
- Create 3 helper modules: checkout-analysis, checkout-business-logic, checkout-data for separation of concerns
- Implement complete-order-button component with Tailwind styling for reward shopping cart
- Extend checkout models with Buyer and Payer types, update OrderOptions interface
- Add CustomerType, BuyerType, PayerType enums to common and CRM data-access layers
- Refactor customer component address selection to use new CustomerAdapter and ShippingAddressAdapter
- Update CheckoutService with refactored logic using new adapters and helpers
- Update CrmTabMetadataService to use consistent payer/shipping address ID tracking
- Add comprehensive documentation for checkout completion flow and service architecture
2025-10-07 13:50:11 +02:00

42 KiB

DomainCheckoutService.completeCheckout() - Complete Flow Documentation

Table of Contents

  1. Overview
  2. File Locations
  3. Function Signatures
  4. Dependencies
  5. Input Parameters
  6. Return Types
  7. Execution Flow
  8. Data Structures and Types
  9. Data Transformations
  10. State Management
  11. Error Handling
  12. Performance Considerations
  13. Modern Implementation Improvements

Overview

The completeCheckout function orchestrates the entire checkout completion workflow for the ISA application. This is a complex, multi-step process that:

  • Validates shopping cart data
  • Updates customer information (buyer, payer, notification channels)
  • Manages payment details
  • Handles shipping addresses and destinations
  • Validates product availability
  • Creates orders from the completed checkout

Total Implementation: 268 lines in legacy version, 168 lines in modern version (37% reduction)

API Calls: 15+ sequential API calls with conditional execution

State Updates: 10+ NgRx store actions dispatched throughout the flow


File Locations

Primary Service File

Path: /apps/isa-app/src/domain/checkout/checkout.service.ts Lines: 980-1247 (268 lines) Class: DomainCheckoutService (Injectable service, lines 78-1636)

Modern Refactored Service

Path: /libs/checkout/data-access/src/lib/services/checkout.service.ts Lines: 97-264 (168 lines) Method: complete() - Modern implementation with better separation of concerns

  • Selectors: /apps/isa-app/src/domain/checkout/store/domain-checkout.selectors.ts
  • Actions: /apps/isa-app/src/domain/checkout/store/domain-checkout.actions.ts
  • State: /apps/isa-app/src/domain/checkout/store/domain-checkout.state.ts
  • Entity Definition: /apps/isa-app/src/domain/checkout/store/defs/checkout.entity.ts

Function Signatures

Legacy Implementation (DomainCheckoutService)

completeCheckout({
  processId,
}: {
  processId: number;
}): Observable<DisplayOrderDTO[]>

Location: Lines 980-1247

Parameters:

  • processId: The process/tab ID for the checkout session

Returns: Observable of DisplayOrderDTO[] (created orders)

Modern Implementation (CheckoutService)

async complete(
  params: CompleteCheckoutParams,
  abortSignal?: AbortSignal,
): Promise<Order[]>

Location: /libs/checkout/data-access/src/lib/services/checkout.service.ts, Lines 97-264

CompleteCheckoutParams Interface:

interface CompleteCheckoutParams {
  // Required fields
  shoppingCartId: number;                    // Shopping cart ID to process
  buyer: BuyerDTO;                           // Buyer information
  notificationChannels: NotificationChannel; // Communication channels (bitwise enum)
  customerFeatures: Record<string, string>;  // Customer type features

  // Optional fields (conditionally required based on order type)
  payer?: PayerDTO;                          // Required for B2B/delivery/download
  shippingAddress?: ShippingAddressDTO;      // Required for delivery orders
  specialComment?: string;                   // Optional comment for all items
}

Dependencies

Constructor-Injected Services (DomainCheckoutService)

constructor(
  private store: Store<any>,                              // NgRx store
  private _config: Config,                                // App configuration
  private applicationService: ApplicationService,         // Application services
  private storeCheckoutService: StoreCheckoutService,    // Checkout API
  private orderCheckoutService: OrderCheckoutService,    // Order API
  private availabilityService: DomainAvailabilityService, // Availability validation
  private _shoppingCartService: StoreCheckoutShoppingCartService,  // Cart API
  private _paymentService: StoreCheckoutPaymentService,   // Payment API
  private _buyerService: StoreCheckoutBuyerService,       // Buyer API
  private _payerService: StoreCheckoutPayerService,       // Payer API
  private _branchService: StoreCheckoutBranchService,     // Branch API
  private _kulturpassService: KulturPassService,          // KulturPass API
)

Field-Injected Services

#checkoutMetadataService = inject(CheckoutMetadataService); // Tab metadata management

Modern Service Dependencies (CheckoutService)

#storeCheckoutService = inject(StoreCheckoutService);
#shoppingCartService = inject(StoreCheckoutShoppingCartService);
#buyerService = inject(StoreCheckoutBuyerService);
#payerService = inject(StoreCheckoutPayerService);
#paymentService = inject(StoreCheckoutPaymentService);
#shoppingCartDataService = inject(ShoppingCartService);
#orderCreationService = inject(OrderCreationService);
#availabilityService = inject(AvailabilityService);
#branchService = inject(BranchService);

Input Parameters

Legacy Implementation

  • processId: number - The process/tab ID for the checkout session
  • Data retrieved from NgRx store based on processId

Modern Implementation

All data provided via CompleteCheckoutParams:

Parameter Type Required Conditional Requirement
shoppingCartId number Yes -
buyer BuyerDTO Yes -
notificationChannels NotificationChannel Yes -
customerFeatures Record<string, string> Yes -
payer PayerDTO ⚠️ Optional Required for B2B, delivery, or download orders
shippingAddress ShippingAddressDTO ⚠️ Optional Required for delivery orders
specialComment string ⚠️ Optional -
abortSignal AbortSignal ⚠️ Optional For request cancellation

Return Type

Legacy

Observable<DisplayOrderDTO[]>

Modern

Promise<Order[]>

Order/DisplayOrderDTO Structure:

  • Order ID
  • Order number
  • Order items with pricing
  • Customer information (buyer, payer)
  • Payment details
  • Shipping information
  • Delivery information
  • Branch information
  • Order status

Execution Flow

The checkout completion process consists of 14 sequential steps organized into 8 phases:

Phase 1: Data Preparation and Analysis (Lines 985-1062)

Step 1: Fetch Latest Shopping Cart (Lines 985-988)

const refreshShoppingCart$ = this.getShoppingCart({
  processId,
  latest: true,
}).pipe(first());
  • Purpose: Get current shopping cart state with latest: true flag
  • API Call: StoreCheckoutShoppingCartService.StoreCheckoutShoppingCartGetShoppingCart()
  • State Update: Dispatches setShoppingCart action

Step 2: Refresh Checkout Entity (Lines 989-992)

const refreshCheckout$ = this.getCheckout({
  processId,
  refresh: true,
}).pipe(first());
  • Purpose: Create or refresh the checkout entity
  • API Call: StoreCheckoutService.StoreCheckoutCreateOrRefreshCheckout()
  • State Update: Dispatches setCheckout action

Step 3: Analyze Order Options (Lines 994-1029)

const itemOrderOptions$ = this.getShoppingCart({ processId }).pipe(
  first(),
  map((shoppingCart) => {
    // Analyzes shopping cart items to determine order types
    return {
      hasTakeAway,    // Items with 'Rücklage' order type
      hasPickUp,      // Items with 'Abholung' order type
      hasDownload,    // Items with 'Download' order type
      hasDelivery,    // Items with 'Versand' order type
      hasDigDelivery, // Items with 'DIG-Versand' order type
      hasB2BDelivery, // Items with 'B2B-Versand' order type
    };
  }),
);
  • Purpose: Determines which order types are present in cart
  • No API Call: Pure transformation of shopping cart data
  • Logic: Examines item.data.features['orderType'] for each item

Step 4: Analyze Customer Types (Lines 1031-1042)

const customerTypes$ = this.getCustomerFeatures({ processId }).pipe(
  first(),
  map((features) => {
    return {
      isOnline: !!features?.webshop,
      isGuest: !!features?.guest,
      isB2B: !!features?.b2b,
      hasCustomerCard: !!features?.p4mUser,
      isStaff: !!features?.staff,
    };
  }),
);
  • Purpose: Extract customer type characteristics from features
  • Data Source: NgRx store selector selectCustomerFeaturesByProcessId

Phase 2: Item Preparation (Lines 1044-1052)

Step 5: Set Special Comment on All Items (Lines 1044-1052)

const setSpecialComment$ = this.getSpecialComment({ processId }).pipe(
  first(),
  mergeMap((specialComment) => {
    if (specialComment) {
      return this.setSpecialCommentOnItem({ processId, specialComment });
    }
    return of(specialComment);
  }),
);
  • Purpose: Apply special agent comment to all shopping cart items
  • API Call: StoreCheckoutShoppingCartService.StoreCheckoutShoppingCartUpdateShoppingCartItem() for each item
  • Execution: Parallel updates using concat(...items).pipe(bufferCount())

Phase 3: Destination and Shipping Setup (Lines 1054-1156)

Step 6: Get Shipping Destinations (Lines 1054-1062)

const shippingAddressDestination$ = this.getCheckout({ processId }).pipe(
  first(),
  map((checkout) =>
    checkout?.destinations?.filter(
      (dest) => dest.data.target === 2 || dest.data.target === 16,
    ),
  ),
);
  • Purpose: Filter destinations requiring shipping addresses (target 2 or 16)
  • No API Call: Pure transformation of checkout data

Step 7: Set Buyer (Lines 1064-1067)

const setBuyer$ = this.getBuyer({ processId }).pipe(
  first(),
  mergeMap((buyer) => this._setBuyer({ processId, buyer })),
);
  • Purpose: Set buyer information on checkout
  • API Call: StoreCheckoutBuyerService.StoreCheckoutBuyerSetBuyerPOST()
  • State Update: Dispatches setCheckout action

Step 8: Set Notification Channels (Lines 1069-1076)

const setNotificationChannels$ = this.getNotificationChannels({
  processId,
}).pipe(
  first(),
  mergeMap((notificationChannels) =>
    this._setNotificationChannels({ processId, notificationChannels }),
  ),
);
  • Purpose: Configure customer notification preferences
  • API Call: StoreCheckoutService.StoreCheckoutSetNotificationChannels()
  • State Update: Dispatches setCheckout action

Step 9: Conditionally Set Payer (Lines 1078-1095)

const setPayer$ = combineLatest([itemOrderOptions$, customerTypes$]).pipe(
  first(),
  mergeMap(([itemOrderOptions, customerTypes]) => {
    if (
      customerTypes.isB2B ||
      itemOrderOptions.hasB2BDelivery ||
      itemOrderOptions.hasDelivery ||
      itemOrderOptions.hasDigDelivery ||
      itemOrderOptions.hasDownload
    ) {
      return this.getPayer({ processId }).pipe(first());
    }
    return of(undefined);
  }),
  mergeMap((payer) =>
    payer ? this._setPayer({ processId, payer }) : of(undefined),
  ),
);
  • Purpose: Set payer only when required (B2B, delivery, or download orders)
  • API Call: StoreCheckoutPayerService.StoreCheckoutPayerSetPayerPOST() (conditional)
  • State Update: Dispatches setCheckout action (conditional)

Step 10: Update Destinations for Customer (Lines 1097-1111)

const updateDestination$ = itemOrderOptions$.pipe(
  withLatestFrom(this.getCustomerFeatures({ processId })),
  mergeMap(([{ hasDownload, hasDelivery, hasDigDelivery, hasB2BDelivery }, customerFeatures]) => {
    const needsUpdate = hasDownload || hasDelivery || hasDigDelivery || hasB2BDelivery;
    return needsUpdate
      ? this.setDestinationForCustomer({ processId, customerFeatures })
      : of(undefined);
  }),
);
  • Purpose: Set logistician on destinations based on customer features
  • API Call: StoreCheckoutShoppingCartService.StoreCheckoutShoppingCartSetLogisticianOnDestinationsByBuyer()
  • Conditional: Only for delivery/download order types

Phase 4: Availability Validation (Lines 1113-1115)

Step 11: Check Download Availabilities (Line 1113)

const checkAvailabilities$ = this.checkAvailabilities({ processId });

Implementation (Lines 638-692):

  • Filters download items without lastRequest timestamp
  • For each download item:
    • Calls DomainAvailabilityService.getDownloadAvailability()
    • Validates availability
    • Updates item availability with timestamp if available
    • Collects unavailable item IDs
  • Error Handling: Throws error if any items unavailable
  • State Update: Dispatches setOlaError action with error IDs
  • API Calls:
    • Multiple DomainAvailabilityService.getDownloadAvailability() calls
    • Multiple StoreCheckoutShoppingCartService.StoreCheckoutShoppingCartUpdateShoppingCartItemAvailability() calls

Step 12: Update Shipping Availabilities (Line 1115)

const updateAvailabilities$ = this.updateAvailabilities({ processId });

Implementation (Lines 694-778):

  • Filters items with 'DIG-Versand' or 'B2B-Versand' order types
  • For each shipping item:
    • DIG-Versand: Calls DomainAvailabilityService.getDigDeliveryAvailability()
    • B2B-Versand: Calls DomainAvailabilityService.getB2bDeliveryAvailability() (requires branch)
    • Price Preservation: Always uses the price from the cart (not the API response)
    • Updates shopping cart item with new availability
  • API Calls:
    • Multiple availability service calls
    • Multiple updateItemInShoppingCart() calls
  • State Updates: Dispatches setShoppingCart and addShoppingCartItemAvailabilityToHistory actions

Phase 5: Payment Configuration (Lines 1117-1128)

Step 13: Set Payment Type (Lines 1117-1128)

const setPaymentType$ = itemOrderOptions$.pipe(
  mergeMap(({ hasDownload, hasDelivery, hasDigDelivery, hasB2BDelivery }) => {
    const paymentType =
      hasDownload || hasDelivery || hasDigDelivery || hasB2BDelivery
        ? 128 /* Rechnung (Invoice) */
        : 4;   /* Bar (Cash) */
    return this.setPayment({ processId, paymentType });
  }),
);
  • Purpose: Set payment type based on order types
  • Logic: Invoice (128) for delivery/download, Cash (4) otherwise
  • API Call: StoreCheckoutPaymentService.StoreCheckoutPaymentSetPaymentType()
  • State Update: Dispatches setCheckout action

Phase 6: Shipping Address Update (Lines 1130-1156)

Step 14: Update Destination Shipping Addresses (Lines 1130-1156)

const setDestination$ = combineLatest([
  this.getCheckout({ processId }).pipe(first()),
  shippingAddressDestination$,
  this.getShippingAddress({ processId }).pipe(first()),
]).pipe(
  mergeMap(([checkout, destinations, shippingAddress]) => {
    // For each shipping destination, merge shipping address
    const obs: Observable<ResponseArgsOfDestinationDTO>[] = [];
    if (destinations?.length > 0) {
      destinations.forEach((destination) => {
        const updatedDestination: DestinationDTO = {
          ...destination.data,
          shippingAddress: { ...shippingAddress },
        };
        obs.push(
          this.storeCheckoutService.StoreCheckoutUpdateDestination({
            checkoutId: checkout.id,
            destinationId: destination.id,
            destinationDTO: updatedDestination,
          }),
        );
      });
      return concat(...obs).pipe(bufferCount(destinations?.length));
    }
    return of(destinations);
  }),
);
  • Purpose: Merge shipping address into destination objects
  • API Call: StoreCheckoutService.StoreCheckoutUpdateDestination() for each destination
  • Execution: Parallel updates using concat() and bufferCount()

Phase 7: Order Creation (Lines 1158-1186)

Step 15: Create Orders (Lines 1158-1186)

const completeOrder$ = this.getCheckout({ processId }).pipe(
  first(),
  mergeMap((checkout) =>
    this.orderCheckoutService
      .OrderCheckoutCreateOrderPOST({
        checkoutId: checkout.id,
      })
      .pipe(
        catchError((error) => {
          if (error instanceof HttpErrorResponse) {
            if (error.status === 409) {
              // Conflict: order already exists
              this.store.dispatch(
                DomainCheckoutActions.setOrders({
                  orders: error.error.result,
                }),
              );
            }
            return throwError(error);
          }
        }),
        map((response) => {
          this.store.dispatch(
            DomainCheckoutActions.setOrders({ orders: response.result }),
          );
          return response.result;
        }),
      ),
  ),
);
  • Purpose: Create orders from the completed checkout
  • API Call: OrderCheckoutService.OrderCheckoutCreateOrderPOST()
  • State Update: Dispatches setOrders action with created orders
  • Special Handling: HTTP 409 (Conflict) means orders already exist - stores existing orders

Phase 8: Sequential Execution (Lines 1188-1246)

Step 16: Execute All Steps in Order (Lines 1188-1246)

return of(undefined)
  .pipe(
    // Phase 1: Setup
    mergeMap((_) => updateDestination$),
    mergeMap((_) => refreshShoppingCart$),
    mergeMap((_) => setSpecialComment$),
    mergeMap((_) => refreshCheckout$),

    // Phase 2: Availability Validation
    mergeMap((_) => checkAvailabilities$),
    mergeMap((_) => updateAvailabilities$),
  )
  .pipe(
    // Phase 3: Customer and Payment Setup
    mergeMap((_) => setBuyer$),
    mergeMap((_) => setNotificationChannels$),
    mergeMap((_) => setPayer$),
    mergeMap((_) => setPaymentType$),

    // Phase 4: Final Setup and Order Creation
    mergeMap((_) => setDestination$),
    mergeMap((_) => completeOrder$),
  );

Execution Order:

  1. Update destinations for customer
  2. Refresh shopping cart
  3. Set special comment on items
  4. Refresh checkout
  5. Check download availabilities
  6. Update shipping availabilities
  7. Set buyer
  8. Set notification channels
  9. Set payer (conditional)
  10. Set payment type
  11. Update destination shipping addresses
  12. Create orders

RxJS Pattern: Sequential execution using chained mergeMap() operators with console logging at each step.


Data Structures and Types

Input Parameter Types

CompleteCheckoutParams

interface CompleteCheckoutParams {
  // Required fields
  shoppingCartId: number;                    // ID of shopping cart to process
  buyer: BuyerDTO;                           // Buyer information
  notificationChannels: NotificationChannel; // Customer communication channels (bitwise enum)
  customerFeatures: Record<string, string>;  // Customer feature flags

  // Optional fields (conditionally required)
  payer?: PayerDTO;                          // Required for B2B/delivery/download
  shippingAddress?: ShippingAddressDTO;      // Required for delivery orders
  specialComment?: string;                   // Optional comment for all items
}

Analysis Result Types

OrderOptionsAnalysis

interface OrderOptionsAnalysis {
  hasTakeAway: boolean;      // orderType === 'Rücklage'
  hasPickUp: boolean;        // orderType === 'Abholung'
  hasDownload: boolean;      // orderType === 'Download'
  hasDelivery: boolean;      // orderType === 'Versand'
  hasDigDelivery: boolean;   // orderType === 'DIG-Versand'
  hasB2BDelivery: boolean;   // orderType === 'B2B-Versand'
}

CustomerTypeAnalysis

interface CustomerTypeAnalysis {
  isOnline: boolean;         // customerFeatures.webshop
  isGuest: boolean;          // customerFeatures.guest
  isB2B: boolean;            // customerFeatures.b2b
  hasCustomerCard: boolean;  // customerFeatures.p4mUser
  isStaff: boolean;          // customerFeatures.staff
}

Core Swagger Models

BuyerDTO

interface BuyerDTO extends AddresseeWithReferenceDTO {
  buyerNumber?: string;
  buyerStatus?: BuyerStatus;      // 0|1|2|4|8|16 (bitwise enum)
  buyerType?: BuyerType;          // 0|1|2|4|8|16 (bitwise enum)
  dateOfBirth?: string;
  isTemporaryAccount?: boolean;
}

PayerDTO

interface PayerDTO extends AddresseeWithReferenceDTO {
  payerNumber?: string;
  payerStatus?: PayerStatus;      // 0|1|2|4|8|16 (bitwise enum)
  payerType?: PayerType;          // 0|4|8|16 (bitwise enum)
}

ShoppingCartItemDTO

interface ShoppingCartItemDTO extends EntityDTOBase {
  availability?: AvailabilityDTO;
  features?: Record<string, string>;     // Critical: contains 'orderType' key
  product?: ProductDTO;
  quantity?: number;
  specialComment?: string;
  unitPrice?: PriceDTO;
  total?: PriceDTO;
  destination?: EntityDTOContainerOfDestinationDTO;
  // ... many more fields
}

CheckoutDTO

interface CheckoutDTO extends EntityDTOBase {
  buyer?: BuyerDTO;
  payer?: PayerDTO;
  destinations?: Array<EntityDTOContainerOfDestinationDTO>;
  items?: Array<EntityDTOContainerOfCheckoutItemDTO>;
  payment?: PaymentDTO;
  notificationChannels?: NotificationChannel;
  // ... more fields
}

DestinationDTO

interface DestinationDTO extends EntityDTOBase {
  checkout?: EntityDTOContainerOfCheckoutDTO;
  logistician?: EntityDTOContainerOfLogisticianDTO;
  shippingAddress?: ShippingAddressDTO;
  target?: ShippingTarget;          // 0|1|2|4|8|16|32 (bitwise enum)
  targetBranch?: EntityDTOContainerOfBranchDTO;
}

ShippingTarget Values:

  • 1: Branch (pickup)
  • 2: Delivery (shipping)
  • 16: Unknown/other

Enums and Constants

PaymentType

type PaymentType = 0 | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128 | 256 | 512 | ...;

Key values:

  • 4: Bar (Cash) - for pickup/takeaway orders
  • 128: Rechnung (Invoice) - for delivery/download orders

NotificationChannel

type NotificationChannel = 0 | 1 | 2 | 4 | 8 | 16;

Values:

  • 0: None
  • 1: Email
  • 2: SMS
  • 4: Phone
  • 8: Push notification
  • 16: Other

Data Transformations

1. Customer Features → Customer Type Analysis

Location: Lines 1031-1042

Input:  { webshop: 'webshop', guest: 'guest', b2b: 'b2b', p4mUser: 'p4mUser', staff: 'staff' }
Output: { isOnline: true, isGuest: true, isB2B: true, hasCustomerCard: true, isStaff: true }

2. Shopping Cart Items → Order Options Analysis

Location: Lines 994-1029

Input:  ShoppingCartDTO with items containing features.orderType
Output: {
  hasTakeAway: boolean,
  hasPickUp: boolean,
  hasDownload: boolean,
  hasDelivery: boolean,
  hasDigDelivery: boolean,
  hasB2BDelivery: boolean
}

3. Shipping Address → Destination DTO

Location: Lines 1139-1142

Input:  ShippingAddressDTO
Output: DestinationDTO {
  ...destination.data,
  shippingAddress: { ...shippingAddress }
}

4. Order Options → Payment Type

Location: Lines 1117-1125

Input:  { hasDownload, hasDelivery, hasDigDelivery, hasB2BDelivery }
Logic:  hasDownload || hasDelivery || hasDigDelivery || hasB2BDelivery ? 128 : 4
Output: 128 (Invoice) or 4 (Cash)

Adapter Implementations

ShoppingCartItemAdapter

Purpose: Converts checkout-api format to catalogue-api format

class ShoppingCartItemAdapter {
  static toCatalogueFormat(item: EntityDTOContainerOfShoppingCartItemDTO): CatalogueShoppingCartItem
  static toCatalogueFormatBulk(items: EntityDTOContainerOfShoppingCartItemDTO[]): CatalogueShoppingCartItem[]
}

Transformations:

  • Unwraps EntityDTOContainer wrapper
  • Converts catalogProductNumber from string to number
  • Flattens nested price structure: item.data.availability.price.value.valueprice

AvailabilityAdapter

Purpose: Converts catalogue-api availability to checkout-api format

class AvailabilityAdapter {
  static toCheckoutFormat(
    catalogueAvailability: CatalogueAvailabilityResponse,
    originalPrice?: number
  ): AvailabilityDTO
}

Transformations:

  • Converts ssc from number to string
  • Wraps supplier ID: { id: number }{ id: number, data: { id: number } }
  • Creates nested price structure: price: numberprice: { value: { value: number } }
  • Critical: Preserves originalPrice parameter for reward items

BranchAdapter & LogisticianAdapter

Purpose: Converts inventory/OMS formats to catalogue format

class BranchAdapter {
  static toCatalogueFormat(branch: InventoryBranchDTO): CatalogueBranch
}

class LogisticianAdapter {
  static toCatalogueFormat(logistician: OmsLogisticianDTO): CatalogueLogistician
}

Transformations: Simple pass-through with validation

Helper Functions (CRM → Checkout)

Location: /libs/checkout/data-access/src/lib/helpers/customer-to-checkout.helpers.ts

buildBuyerFromCustomer(customer: CustomerDTO): BuyerDTO
buildPayerFromCrmPayer(crmPayer: CrmPayerDTO): PayerDTO
buildShippingAddressFromCrm(address: CrmShippingAddressDTO): CheckoutShippingAddressDTO
extractCustomerFeatures(customer: CustomerDTO): Record<string, string>

State Management

NgRx Store Actions Dispatched

  1. setShoppingCart

    • When: Shopping cart is created or updated
    • Payload: { processId, shoppingCart }
  2. setCheckout

    • When: Checkout entity is created or updated
    • Payload: { processId, checkout }
  3. addShoppingCartItemAvailabilityToHistory

    • When: Item availability is updated
    • Payload: { processId, availability, shoppingCartItemId }
  4. setOrders

    • When: Orders are created or conflict detected
    • Payload: { orders }
  5. setOlaError

    • When: Download availability validation fails
    • Payload: { processId, olaErrorIds }

NgRx Selectors Used

  1. selectShoppingCartByProcessId - Get shopping cart for process
  2. selectCheckoutByProcessId - Get checkout entity for process
  3. selectCustomerFeaturesByProcessId - Get customer features
  4. selectShippingAddressByProcessId - Get shipping address
  5. selectPayerByProcessId - Get payer information
  6. selectBuyerByProcessId - Get buyer information
  7. selectSpecialComment - Get special comment
  8. selectNotificationChannels - Get notification preferences
  9. selectCheckoutEntityByProcessId - Get complete checkout entity
  10. selectOrders - Get created orders

Session Storage (via CheckoutMetadataService)

Location: /libs/checkout/data-access/src/lib/services/checkout-metadata.service.ts

Stored in tab metadata:

  • shoppingCartId: Shopping cart ID associated with tab
  • selectedBranchId: Selected branch for checkout process
  • rewardShoppingCartId: Reward shopping cart ID

Storage Method: TabService.patchTabMetadata() with Zod validation

NgRx Signals Store (Modern)

RewardCatalogStore (libs/checkout/data-access/src/lib/store/reward-catalog.store.ts)

export const RewardCatalogStore = signalStore(
  { providedIn: 'root' },
  withStorage('reward-checkout-data-access.reward-catalog-store', SessionStorageProvider),
  withEntities<RewardCatalogEntity>(config),
  withComputed(...),
  withMethods(...),
  withHooks(...)
)

Features:

  • Entity management with tab-scoped entities
  • Session persistence
  • Computed signals for derived state
  • Automatic orphan cleanup via withHooks()

Error Handling

1. HTTP 409 Conflict (Lines 1166-1176)

catchError((error) => {
  if (error instanceof HttpErrorResponse) {
    if (error.status === 409) {
      this.store.dispatch(
        DomainCheckoutActions.setOrders({
          orders: error.error.result,
        }),
      );
    }
    return throwError(error);
  }
})
  • Scenario: Order already exists for this checkout
  • Handling: Stores existing orders in state, then re-throws error

2. Availability Validation Failures (Lines 654-691)

if (errorIds.length > 0) {
  throw throwError(new Error(`Artikel nicht verfügbar`));
} else {
  return of(undefined);
}
  • Scenario: Download items are unavailable
  • Handling: Stores error IDs in state, throws error with German message

3. Observable Error Propagation

All API calls return observables that can emit errors. Errors propagate through the chain of mergeMap() operators, halting execution at the first failure.

Modern Error Handling (CheckoutService)

Error Types:

type CheckoutCompletionErrorCode =
  | 'SHOPPING_CART_NOT_FOUND'
  | 'SHOPPING_CART_EMPTY'
  | 'CHECKOUT_NOT_FOUND'
  | 'INVALID_AVAILABILITY'
  | 'MISSING_REQUIRED_DATA'
  | 'CHECKOUT_CONFLICT'
  | 'ORDER_CREATION_FAILED'
  | 'DOWNLOAD_UNAVAILABLE'
  | 'SHIPPING_AVAILABILITY_FAILED';

Error Factory Methods:

CheckoutCompletionError.shoppingCartEmpty(shoppingCartId)
CheckoutCompletionError.missingRequiredData('payer', 'Required for B2B/delivery/download')
CheckoutCompletionError.downloadItemsUnavailable([123, 456])
CheckoutCompletionError.checkoutConflict(existingOrders)

Performance Considerations

1. Sequential Execution Pattern

The entire flow executes sequentially using chained mergeMap() operators. This ensures data consistency but may be slower than parallel execution.

2. Multiple API Calls

The function makes 15+ API calls in sequence:

  • 2 refresh operations (cart, checkout)
  • N item updates (special comment)
  • 6 customer/payment setup calls
  • M destination updates
  • 1 order creation call
  • Multiple availability validation calls

3. Observable Sharing

Uses shareReplay() strategically to avoid duplicate API calls:

  • itemOrderOptions$ - Shared between multiple subscribers
  • shippingAddressDestination$ - Shared for destination updates
  • setPaymentType$ - Shared result

4. First Value Extraction

Extensive use of first() ensures observables complete quickly and don't leak subscriptions.

5. Parallel Operations Where Possible

  • Item updates (special comments) execute in parallel using concat() + bufferCount()
  • Destination updates execute in parallel
  • Shipping availability updates execute in parallel

Modern Implementation Improvements

The refactored CheckoutService introduces several improvements:

1. Parameter-Based Design

async complete(params: CompleteCheckoutParams, abortSignal?: AbortSignal): Promise<Order[]>

All data passed as parameters instead of reading from store (stateless service).

2. Async/Await Pattern

Uses modern async/await instead of RxJS chains for better readability.

3. Zod Validation

const validated = CompleteCheckoutParamsSchema.parse(params);

Runtime validation of input parameters.

4. Structured Logging

this.#logger.info('Starting checkout completion');
this.#logger.debug('Fetching shopping cart');
this.#logger.error('Checkout completion failed', error);

5. Cancellation Support

if (abortSignal) {
  req$ = req$.pipe(takeUntilAborted(abortSignal));
}

All API calls support cancellation via AbortSignal.

6. Better Error Handling

Custom error classes with specific error codes for better error handling and debugging.

7. Adapter Pattern

ShoppingCartItemAdapter.toCatalogueFormat(item)
AvailabilityAdapter.toCheckoutFormat(availability)
BranchAdapter.toCatalogueFormat(branch)

Clear separation between API boundaries.

8. Helper Methods

private analyzeOrderOptions(items): OrderOptionsAnalysis
private analyzeCustomerTypes(features): CustomerTypeAnalysis
private shouldSetPayer(options, customer): boolean
private determinePaymentType(options): PaymentType

Business logic extracted into testable helper methods.


Complete Data Flow Diagram

┌─────────────────────────────────────────────────────────────────────────────┐
│ STEP 1: USER INPUT (CompleteOrderButtonComponent)                          │
└─────────────────────────────────────────────────────────────────────────────┘
                                    │
                                    │ User clicks "Complete Order"
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ STEP 2: DATA GATHERING FROM CRM                                            │
├─────────────────────────────────────────────────────────────────────────────┤
│ Source: SelectedCustomerResource (CRM API)                                 │
│                                                                             │
│ customer: CustomerDTO → Extract:                                           │
│   ├── buyer: BuyerDTO (via buildBuyerFromCustomer)                         │
│   ├── payer: PayerDTO (via buildPayerFromCrmPayer)                         │
│   ├── shippingAddress: ShippingAddressDTO (via buildShippingAddressFromCrm)│
│   ├── notificationChannels: NotificationChannel                            │
│   └── customerFeatures: Record<string, string> (via extractCustomerFeatures)│
└─────────────────────────────────────────────────────────────────────────────┘
                                    │
                                    │ Create CompleteCheckoutParams
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ STEP 3: PARAMETER VALIDATION (Zod Schema)                                  │
├─────────────────────────────────────────────────────────────────────────────┤
│ CompleteCheckoutParamsSchema.parse(params)                                 │
│ - Validates all required fields                                            │
│ - Ensures type safety                                                      │
│ - Validates enum values                                                    │
└─────────────────────────────────────────────────────────────────────────────┘
                                    │
                                    │ Pass to CheckoutService
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ STEP 4: CHECKOUT SERVICE ORCHESTRATION (14 Sequential Steps)               │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│ Phase 1: Data Preparation                                                  │
│   1. Fetch shopping cart (with latest flag)                                │
│   2. Analyze order types (from item.data.features['orderType'])            │
│   3. Analyze customer types (from customerFeatures)                        │
│   4. Create/refresh checkout entity                                        │
│                                                                             │
│ Phase 2: Item Processing                                                   │
│   5. Update destinations for customer (conditional)                        │
│   6. Set special comment on all items (conditional)                        │
│                                                                             │
│ Phase 3: Availability Validation                                           │
│   7. Validate download availabilities (parallel per item)                  │
│      ├── ShoppingCartItemAdapter.toCatalogueFormat()                       │
│      ├── AvailabilityService.validateDownloadAvailabilities()              │
│      └── AvailabilityAdapter.toCheckoutFormat()                            │
│   8. Update shipping availabilities (parallel per item)                    │
│      ├── BranchAdapter.toCatalogueFormat()                                 │
│      ├── LogisticianAdapter.toCatalogueFormat()                            │
│      ├── AvailabilityService.getShippingAvailability()                     │
│      └── AvailabilityAdapter.toCheckoutFormat(availability, originalPrice) │
│                                                                             │
│ Phase 4: Customer and Payment Setup                                        │
│   9. Set buyer on checkout                                                 │
│  10. Set notification channels                                             │
│  11. Set payer (conditional: B2B/delivery/download)                        │
│  12. Set payment type (Invoice: 128 or Cash: 4)                            │
│                                                                             │
│ Phase 5: Shipping and Order Creation                                       │
│  13. Update destination shipping addresses (conditional)                   │
│  14. Create orders from checkout                                           │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
                                    │
                                    │ Return Order[]
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ STEP 5: SUCCESS RESPONSE                                                   │
├─────────────────────────────────────────────────────────────────────────────┤
│ Component receives Order[]                                                 │
│ User sees success notification                                             │
│ Orders stored in OMS system                                                │
└─────────────────────────────────────────────────────────────────────────────┘

ERROR HANDLING PATH:
┌─────────────────────────────────────────────────────────────────────────────┐
│ HttpErrorResponse 409 → CheckoutCompletionError.checkoutConflict()         │
│ Empty cart → CheckoutCompletionError.shoppingCartEmpty()                   │
│ Missing data → CheckoutCompletionError.missingRequiredData()               │
│ Unavailable items → CheckoutCompletionError.downloadItemsUnavailable()     │
└─────────────────────────────────────────────────────────────────────────────┘

Summary

The completeCheckout function is a complex, multi-step orchestration that:

  1. Validates shopping cart and availability data
  2. Updates customer information (buyer, payer, notifications)
  3. Configures payment type based on order types
  4. Manages destinations and shipping addresses
  5. Creates orders from the completed checkout
  6. Handles conflicts when orders already exist

Key Characteristics:

  • 268 lines in legacy version, 168 lines in modern version (37% reduction)
  • 15+ API calls executed sequentially with conditional logic
  • 10+ state actions dispatched throughout the flow
  • Comprehensive error handling for business logic failures
  • Conditional execution based on customer type and order types
  • Price preservation for reward items during availability updates
  • Multiple adapter layers for cross-domain data transformation
  • Type-safe with Zod validation and TypeScript interfaces

The modern refactored version improves maintainability through:

  • Parameter-based design (stateless)
  • Async/await patterns
  • Structured logging
  • Better error handling
  • Clear separation of concerns with adapters and helpers