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
42 KiB
DomainCheckoutService.completeCheckout() - Complete Flow Documentation
Table of Contents
- Overview
- File Locations
- Function Signatures
- Dependencies
- Input Parameters
- Return Types
- Execution Flow
- Data Structures and Types
- Data Transformations
- State Management
- Error Handling
- Performance Considerations
- 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
Related State Management Files
- 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: trueflag - API Call:
StoreCheckoutShoppingCartService.StoreCheckoutShoppingCartGetShoppingCart() - State Update: Dispatches
setShoppingCartaction
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
setCheckoutaction
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
setCheckoutaction
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
setCheckoutaction
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
setCheckoutaction (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
lastRequesttimestamp - For each download item:
- Calls
DomainAvailabilityService.getDownloadAvailability() - Validates availability
- Updates item availability with timestamp if available
- Collects unavailable item IDs
- Calls
- Error Handling: Throws error if any items unavailable
- State Update: Dispatches
setOlaErroraction with error IDs - API Calls:
- Multiple
DomainAvailabilityService.getDownloadAvailability()calls - Multiple
StoreCheckoutShoppingCartService.StoreCheckoutShoppingCartUpdateShoppingCartItemAvailability()calls
- Multiple
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
- DIG-Versand: Calls
- API Calls:
- Multiple availability service calls
- Multiple
updateItemInShoppingCart()calls
- State Updates: Dispatches
setShoppingCartandaddShoppingCartItemAvailabilityToHistoryactions
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
setCheckoutaction
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()andbufferCount()
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
setOrdersaction 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:
- Update destinations for customer
- Refresh shopping cart
- Set special comment on items
- Refresh checkout
- Check download availabilities
- Update shipping availabilities
- Set buyer
- Set notification channels
- Set payer (conditional)
- Set payment type
- Update destination shipping addresses
- 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 orders128: Rechnung (Invoice) - for delivery/download orders
NotificationChannel
type NotificationChannel = 0 | 1 | 2 | 4 | 8 | 16;
Values:
0: None1: Email2: SMS4: Phone8: Push notification16: 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
EntityDTOContainerwrapper - Converts
catalogProductNumberfromstringtonumber - Flattens nested price structure:
item.data.availability.price.value.value→price
AvailabilityAdapter
Purpose: Converts catalogue-api availability to checkout-api format
class AvailabilityAdapter {
static toCheckoutFormat(
catalogueAvailability: CatalogueAvailabilityResponse,
originalPrice?: number
): AvailabilityDTO
}
Transformations:
- Converts
sscfromnumbertostring - Wraps supplier ID:
{ id: number }→{ id: number, data: { id: number } } - Creates nested price structure:
price: number→price: { value: { value: number } } - Critical: Preserves
originalPriceparameter 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
-
setShoppingCart- When: Shopping cart is created or updated
- Payload:
{ processId, shoppingCart }
-
setCheckout- When: Checkout entity is created or updated
- Payload:
{ processId, checkout }
-
addShoppingCartItemAvailabilityToHistory- When: Item availability is updated
- Payload:
{ processId, availability, shoppingCartItemId }
-
setOrders- When: Orders are created or conflict detected
- Payload:
{ orders }
-
setOlaError- When: Download availability validation fails
- Payload:
{ processId, olaErrorIds }
NgRx Selectors Used
selectShoppingCartByProcessId- Get shopping cart for processselectCheckoutByProcessId- Get checkout entity for processselectCustomerFeaturesByProcessId- Get customer featuresselectShippingAddressByProcessId- Get shipping addressselectPayerByProcessId- Get payer informationselectBuyerByProcessId- Get buyer informationselectSpecialComment- Get special commentselectNotificationChannels- Get notification preferencesselectCheckoutEntityByProcessId- Get complete checkout entityselectOrders- 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 tabselectedBranchId: Selected branch for checkout processrewardShoppingCartId: 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 subscribersshippingAddressDestination$- Shared for destination updatessetPaymentType$- 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:
- Validates shopping cart and availability data
- Updates customer information (buyer, payer, notifications)
- Configures payment type based on order types
- Manages destinations and shipping addresses
- Creates orders from the completed checkout
- 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