Files
ISA-Frontend/libs/oms/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

25 KiB

@isa/oms/data-access

A comprehensive Order Management System (OMS) data access library for Angular applications providing return processing, receipt management, order creation, and print capabilities.

Overview

The OMS Data Access library provides a complete suite of services, stores, and utilities for managing the entire return lifecycle in a retail environment. It integrates with the generated OMS API client and provides intelligent state management, question-based return workflows, validation, and receipt printing capabilities. The library follows domain-driven design principles with clear separation between business logic, state management, and API integration.

Table of Contents

Features

  • Return Search - Advanced receipt search with query builder and filter support
  • Return Process Management - Question-based return workflow with branching logic
  • Product Category System - Category-specific questions and validation rules
  • Receipt Management - Full CRUD operations for receipts and receipt items
  • Order Creation - Create orders from completed checkouts
  • Print Integration - Automated return receipt printing via label printers
  • NgRx Signals Stores - Reactive state management with persistence
  • Session & IndexedDB Storage - Automatic state persistence across browser sessions
  • Orphaned Entity Cleanup - Automatic cleanup when tabs are closed
  • Zod Schema Validation - Runtime validation for all data structures
  • AbortSignal Support - Request cancellation for all async operations
  • Comprehensive Logging - Integration with @isa/core/logging for debugging
  • Question Branching Logic - Dynamic question flow based on previous answers

Quick Start

1. Import and Inject Services

import { Component, inject } from '@angular/core';
import {
  ReturnSearchService,
  ReturnDetailsService,
  ReturnProcessService
} from '@isa/oms/data-access';

@Component({
  selector: 'app-return-management',
  template: '...'
})
export class ReturnManagementComponent {
  #returnSearchService = inject(ReturnSearchService);
  #returnDetailsService = inject(ReturnDetailsService);
  #returnProcessService = inject(ReturnProcessService);
}

2. Search for Receipts

async searchReceipts(): Promise<void> {
  const results = await firstValueFrom(
    this.#returnSearchService.search({
      input: { qs: '12345' },  // Receipt number or search term
      filter: { receipt_type: '1;128;1024' },
      take: 20,
      skip: 0
    })
  );

  console.log(`Found ${results.hits} receipts`);
  results.result.forEach(receipt => {
    console.log(`Receipt #${receipt.receiptNumber}: ${receipt.date}`);
  });
}

3. Fetch Receipt Details

async getReceiptDetails(receiptId: number): Promise<void> {
  const receipt = await this.#returnDetailsService.fetchReturnDetails({
    receiptId: receiptId
  });

  console.log(`Receipt: ${receipt.receiptNumber}`);
  console.log(`Buyer: ${receipt.buyer.firstName} ${receipt.buyer.lastName}`);
  console.log(`Items: ${receipt.items.length}`);
}

4. Process a Return

async processReturn(): Promise<void> {
  // Start a return process
  const returnProcess: ReturnProcess = {
    id: 1,
    processId: 100,
    receiptId: 12345,
    receiptItem: receiptItem,
    receiptDate: '2025-10-20',
    answers: {},
    productCategory: ProductCategory.ElektronischeGeraete,
    quantity: 1
  };

  // Get active questions
  const questions = this.#returnProcessService.activeReturnProcessQuestions(returnProcess);

  // Check eligibility
  const eligibility = this.#returnProcessService.eligibleForReturn(returnProcess);
  if (eligibility?.state === EligibleForReturnState.Eligible) {
    console.log('Item is eligible for return');
  }
}

Core Concepts

Return Process Lifecycle

The return process follows a structured workflow:

1. Search & Selection
   └─> Search for receipts using ReturnSearchService
   └─> Select receipt items to return

2. Return Process Initialization
   └─> Create ReturnProcess entities via ReturnProcessStore
   └─> Assign product categories to items

3. Question Workflow
   └─> Display category-specific questions
   └─> Collect answers with validation
   └─> Follow branching logic based on answers

4. Eligibility Determination
   └─> Validate all questions answered
   └─> Check category-specific business rules
   └─> Determine if item is eligible for return

5. Completion
   └─> Generate return receipts via ReceiptService
   └─> Print return receipts via PrintReceiptsService
   └─> Update process with return receipt

Product Categories

The library supports seven product categories, each with specific return rules:

enum ProductCategory {
  BookCalendar = 'book-calendar',           // Books and calendars
  TonDatentraeger = 'ton-datentraeger',     // Audio and data media
  SpielwarenPuzzle = 'spielwaren-puzzle',   // Toys and puzzles
  SonstigesNonbook = 'sonstiges-nonbook',   // Other non-book items
  ElektronischeGeraete = 'elektronische-geraete',  // Electronic devices
  Tolino = 'tolino',                        // Tolino e-readers
  Unknown = 'unknown'                       // Uncategorized
}

Each category has:

  • Specific questions - Defined in the CategoryQuestions registry
  • Branching logic - Questions can have nextQuestion properties
  • Validation rules - Category-specific eligibility checks
  • Business rules - Special handling (e.g., sealed packages, electronic devices)

Question System Architecture

Questions follow a tree-like structure with branching:

interface ReturnProcessQuestion {
  key: string;                          // Unique question identifier
  type: ReturnProcessQuestionType;      // Question type (Select, Checklist, Info, Group, Product)
  question: string;                     // Question text to display
  options?: Array<{                     // Available answer options
    value: string;
    label: string;
    nextQuestion?: ReturnProcessQuestion;  // Branching to next question
  }>;
  questions?: ReturnProcessQuestion[];  // Sub-questions for Group type
}

Question Types:

  • Select - Single choice from options
  • Checklist - Multiple choice with optional "other" text
  • Info - Informational display (auto-answered)
  • Group - Container for multiple related questions
  • Product - Product-specific question

State Management with NgRx Signals

The library provides three signal stores with different persistence strategies:

1. ReturnSearchStore (Session Storage)

export const ReturnSearchStore = signalStore(
  { providedIn: 'root' },
  withStorage('oms-data-access.return-search-store', SessionStorageProvider),
  withEntities<ReturnSearchEntity>(config),
  // ...
);
  • Purpose: Manage search results and pagination
  • Persistence: SessionStorage (cleared when tab closes)
  • Entity: ReturnSearchEntity with status, items, hits
  • Features: Paginated search, status tracking (Idle, Pending, Success, Error)

2. ReturnProcessStore (IndexedDB)

export const ReturnProcessStore = signalStore(
  { providedIn: 'root' },
  withStorage('return-process', IDBStorageProvider),
  withEntities<ReturnProcess>(),
  // ...
);
  • Purpose: Manage active return processes
  • Persistence: IndexedDB (survives tab close, persistent across sessions)
  • Entity: ReturnProcess with answers, product category, receipt info
  • Features: Answer management, process completion tracking

3. ReturnDetailsStore (No Persistence)

export const ReturnDetailsStore = signalStore(
  withState(initialState),
  withEntities(receiptConfig),
  // ...
);
  • Purpose: Manage receipt details and item selection
  • Persistence: None (in-memory only)
  • Features: Item selection, category/quantity management, canReturn checks

Validation with Zod

All input parameters and API payloads are validated using Zod schemas:

import { QueryTokenSchema, ReturnReceiptValuesSchema } from '@isa/oms/data-access';

// Automatic validation in services
const validatedToken = QueryTokenSchema.parse(queryToken);

// Safe parsing with error handling
const result = ReturnReceiptValuesSchema.safeParse(payload);
if (!result.success) {
  console.error('Validation failed:', result.error);
}

Key Schemas:

  • QueryTokenSchema - Search query validation
  • FetchReturnDetailsSchema - Receipt detail fetch params
  • ReturnReceiptValuesSchema - Return submission payload
  • ReturnProcessChecklistAnswerSchema - Checklist answer validation

API Reference

Services

ReturnSearchService

Service for searching receipts in the OMS system.

querySettings(): Observable<QuerySettingsDTO>

Fetches query settings for receipt search configuration.

Returns: Observable containing query settings (available filters, sort options)

Example:

this.#returnSearchService.querySettings().subscribe(settings => {
  console.log('Available filters:', settings.filters);
});
search(queryToken: QueryTokenInput): Observable<ListResponseArgs<ReceiptListItem>>

Executes a search for receipts based on query parameters.

Parameters:

  • queryToken: QueryTokenInput - Search parameters (validated with Zod)
    • input.qs - Search query string
    • filter - Filter criteria (e.g., receipt_type)
    • take - Number of results to return
    • skip - Number of results to skip (pagination)

Returns: Observable containing list of receipt items with metadata

Throws:

  • ReturnParseQueryTokenError - If query token validation fails
  • ReturnSearchSearchError - If search fails due to API error

Example:

this.#returnSearchService.search({
  input: { qs: 'smith@example.com' },
  filter: { receipt_type: '1;128;1024' },
  take: 20,
  skip: 0
}).subscribe({
  next: (results) => console.log(`Found ${results.hits} receipts`),
  error: (error) => console.error('Search failed:', error)
});

ReturnDetailsService

Service for managing receipt details and return eligibility.

fetchReturnDetails(params: FetchReturnDetails, abortSignal?: AbortSignal): Promise<Receipt>

Fetches detailed receipt information including items, buyer, and shipping.

Parameters:

  • params.receiptId: number - Receipt ID to fetch (validated with Zod)
  • abortSignal?: AbortSignal - Optional signal to cancel request

Returns: Promise resolving to complete Receipt with items

Throws: Error if receipt not found or API error

Example:

const receipt = await this.#returnDetailsService.fetchReturnDetails(
  { receiptId: 12345 },
  abortController.signal
);

console.log(`Receipt ${receipt.receiptNumber} has ${receipt.items.length} items`);
fetchReceiptsByEmail(params: { email: string }, abortSignal?: AbortSignal): Promise<ReceiptListItem[]>

Fetches all receipts associated with an email address.

Parameters:

  • params.email: string - Email address to search for
  • abortSignal?: AbortSignal - Optional signal to cancel request

Returns: Promise resolving to array of receipt list items

Example:

const receipts = await this.#returnDetailsService.fetchReceiptsByEmail({
  email: 'customer@example.com'
});

receipts.forEach(r => console.log(`Receipt: ${r.receiptNumber}`));
canReturn(params: { receiptItemId: number, quantity: number, category: ProductCategory }, abortSignal?: AbortSignal): Promise<CanReturn>

Checks if a specific receipt item can be returned.

Parameters:

  • params.receiptItemId: number - ID of receipt item to check
  • params.quantity: number - Quantity to return
  • params.category: ProductCategory - Product category
  • abortSignal?: AbortSignal - Optional signal to cancel request

Returns: Promise resolving to CanReturn result with eligibility info

Example:

const canReturn = await this.#returnDetailsService.canReturn({
  receiptItemId: 456,
  quantity: 1,
  category: ProductCategory.ElektronischeGeraete
});

if (canReturn.result) {
  console.log('Item can be returned');
} else {
  console.log('Return not allowed:', canReturn.message);
}
availableCategories(): KeyValue<ProductCategory, string>[]

Gets all available product categories (excludes Unknown).

Returns: Array of key-value pairs representing categories

Example:

const categories = this.#returnDetailsService.availableCategories();
categories.forEach(cat => {
  console.log(`${cat.key}: ${cat.value}`);
});

ReturnProcessService

Service for managing the return process workflow including questions and eligibility.

activeReturnProcessQuestions(process: ReturnProcess): ReturnProcessQuestion[] | undefined

Gets active questions based on answers provided so far.

Parameters:

  • process: ReturnProcess - The return process containing answers

Returns: Array of active questions, or undefined if no questions apply

Throws:

  • PropertyNullOrUndefinedError - If questions cannot be found
  • Error - If cyclic question dependencies detected

Example:

const questions = this.#returnProcessService.activeReturnProcessQuestions(process);

questions?.forEach(q => {
  console.log(`Question: ${q.question}`);
  if (q.type === ReturnProcessQuestionType.Select) {
    q.options?.forEach(opt => console.log(`  - ${opt.label}`));
  }
});
returnProcessQuestionsProgress(returnProcess: ReturnProcess): { answered: number; total: number } | undefined

Calculates question progress for the return process.

Parameters:

  • returnProcess: ReturnProcess - The return process to analyze

Returns: Object with answered and total question counts, or undefined

Example:

const progress = this.#returnProcessService.returnProcessQuestionsProgress(process);
if (progress) {
  console.log(`Progress: ${progress.answered}/${progress.total}`);
  const percentage = (progress.answered / progress.total) * 100;
  console.log(`${percentage}% complete`);
}
eligibleForReturn(returnProcess: ReturnProcess): EligibleForReturn | undefined

Determines if the product is eligible for return based on answers.

Parameters:

  • returnProcess: ReturnProcess - The return process with complete answers

Returns: Eligibility status, or undefined if questions incomplete

Example:

const eligibility = this.#returnProcessService.eligibleForReturn(process);

switch (eligibility?.state) {
  case EligibleForReturnState.Eligible:
    console.log('Item is eligible for return');
    break;
  case EligibleForReturnState.NotEligible:
    console.log('Item is not eligible:', eligibility.reason);
    break;
  case EligibleForReturnState.Unknown:
    console.log('Cannot determine eligibility');
    break;
}
getReturnInfo(process: ReturnProcess): ReturnInfo | undefined

Retrieves consolidated return information from answers.

Parameters:

  • process: ReturnProcess - The return process

Returns: Consolidated return information or undefined

Example:

const info = this.#returnProcessService.getReturnInfo(process);
if (info) {
  console.log('Return reason:', info.reason);
  console.log('Additional notes:', info.notes);
}
completeReturnProcess(processes: ReturnProcess[]): Promise<Receipt[]>

Completes the return process and generates return receipts.

Parameters:

  • processes: ReturnProcess[] - Array of return processes to complete

Returns: Promise resolving to generated return receipts

Throws:

  • PropertyNullOrUndefinedError - If processes is null/undefined
  • PropertyIsEmptyError - If processes array is empty
  • ReturnProcessIsNotCompleteError - If any process has unanswered questions

Example:

try {
  const receipts = await this.#returnProcessService.completeReturnProcess([
    process1,
    process2
  ]);

  console.log(`Generated ${receipts.length} return receipts`);
  receipts.forEach(r => console.log(`Return receipt: ${r.receiptNumber}`));
} catch (error) {
  if (error instanceof ReturnProcessIsNotCompleteError) {
    console.error('Please answer all questions before completing');
  }
}

OrderCreationService

Service for creating orders from checkout.

createOrdersFromCheckout(checkoutId: number): Promise<Order[]>

Creates orders from a completed checkout.

Parameters:

  • checkoutId: number - The checkout ID to create orders from

Returns: Promise resolving to array of created orders

Throws:

  • Error - If checkoutId is invalid
  • ResponseArgsError - If order creation fails

Example:

const orders = await this.#orderCreationService.createOrdersFromCheckout(12345);
orders.forEach(order => {
  console.log(`Order ${order.orderNumber} created`);
});
getLogistician(logisticianNumber = '2470', abortSignal?: AbortSignal): Promise<LogisticianDTO>

Retrieves logistician information.

Parameters:

  • logisticianNumber: string - Logistician number (defaults to '2470')
  • abortSignal?: AbortSignal - Optional signal to cancel request

Returns: Promise resolving to logistician data

Throws:

  • ResponseArgsError - If request fails
  • Error - If logistician not found

Example:

const logistician = await this.#orderCreationService.getLogistician('2470');
console.log(`Logistician: ${logistician.name}`);

PrintReceiptsService

Service for printing return receipts.

printReturnReceipts({ returnReceiptIds }: { returnReceiptIds: number[] }): Promise<void>

Prints return receipts to a label printer.

Parameters:

  • returnReceiptIds: number[] - Array of return receipt IDs to print

Returns: Promise that resolves when printing is complete

Throws: Error if no receipt IDs provided

Example:

await this.#printReceiptsService.printReturnReceipts({
  returnReceiptIds: [123, 124, 125]
});
console.log('Receipts sent to printer');

Facades

OrderCreationFacade

Simplified interface for order creation operations. Delegates to OrderCreationService.

Methods:

  • createOrdersFromCheckout(checkoutId: number): Promise<Order[]>
  • getLogistician(logisticianNumber = '2470', abortSignal?: AbortSignal): Promise<LogisticianDTO>

Note: This is a pass-through facade. Consider injecting OrderCreationService directly unless future orchestration logic is planned.


Stores

For detailed store usage and examples, see the Usage Examples section.

ReturnSearchStore

Signal store for managing receipt search state with session persistence.

Key Methods:

  • getEntity(processId) - Get search entity by process ID
  • search(params) - Execute search with rxMethod
  • removeAllEntitiesByProcessId(processId) - Clean up search results

ReturnProcessStore

Signal store for managing return process entities with IndexedDB persistence.

Key Methods:

  • startProcess(params) - Initialize return process
  • setAnswer(id, question, answer) - Set question answer
  • removeAnswer(id, question) - Remove question answer
  • finishProcess(returnReceipts) - Mark process complete
  • removeAllEntitiesByProcessId(...processIds) - Clean up processes

ReturnDetailsStore

Signal store for managing receipt details and item selection (in-memory).

Key Methods:

  • receiptResource(receiptId) - Load receipt
  • canReturnResource(receiptItem) - Check return eligibility
  • addSelectedItems(itemIds) - Add items to selection
  • setProductCategory(itemId, category) - Set item category
  • setQuantity(itemId, quantity) - Set return quantity

Helper Functions

Return Process Helpers

Key helper functions exported from @isa/oms/data-access:

  • canReturnReceiptItem(item) - Check if item is returnable
  • getReceiptItemQuantity(item) - Get item quantity
  • getReceiptItemReturnedQuantity(item) - Get returned quantity
  • getReceiptItemProductCategory(item) - Get item category
  • receiptItemHasCategory(item, category) - Check category match
  • activeReturnProcessQuestions(questions, answers) - Get active questions
  • allReturnProcessQuestionsAnswered(params) - Check completion
  • calculateLongestQuestionDepth(questions, answers) - Calculate question tree depth

RxJS Operators

takeUntilAborted(signal: AbortSignal)

RxJS operator that completes the source Observable when an AbortSignal is aborted.

Example:

import { takeUntilAborted } from '@isa/oms/data-access';

const abortController = new AbortController();

observable$.pipe(
  takeUntilAborted(abortController.signal)
).subscribe({
  next: (data) => console.log('Data:', data),
  error: (err) => console.error('Aborted or failed')
});

// Cancel request
abortController.abort();

Error Classes

  • OmsError - Base error class
  • ReturnSearchSearchError - Search failure
  • ReturnParseQueryTokenError - Query validation failure
  • CreateReturnProcessError - Process creation failure
  • ReturnProcessIsNotCompleteError - Incomplete process submission

For detailed error handling patterns, see the Error Handling section.


Usage Examples

Complete Return Workflow

See the comprehensive workflow example in the full documentation above for a step-by-step guide covering:

  1. Searching for receipts
  2. Loading receipt details
  3. Selecting items for return
  4. Starting the return process
  5. Answering questions
  6. Checking eligibility
  7. Completing the return

Search with Pagination

Example showing how to implement paginated receipt search with the ReturnSearchStore.

Dynamic Question Display

Example component demonstrating how to render questions dynamically based on type and handle answers.

Return Process Workflow

The return process follows this workflow:

Search Receipt → Load Details → Select Items → Start Process
    → Answer Questions → Check Eligibility → Complete Return → Print Receipt

Question Branching

Questions support branching logic through the nextQuestion property, allowing different question flows based on previous answers.

Product Categories and Questions

Supported Categories

  • BookCalendar - Books and calendars (2 questions)
  • TonDatentraeger - Audio/data media (3-5 questions)
  • SpielwarenPuzzle - Toys and puzzles (2 questions)
  • SonstigesNonbook - Other non-book items (2 questions)
  • ElektronischeGeraete - Electronic devices (4-8 questions, complex branching)
  • Tolino - E-readers (6-12 questions, most complex)
  • Unknown - Uncategorized (0 questions, manual processing)

Each category has specific questions and validation rules defined in the CategoryQuestions registry.

State Management

Persistence Strategies

  • ReturnSearchStore: SessionStorage (tab-scoped, cleared on close)
  • ReturnProcessStore: IndexedDB (persistent across sessions)
  • ReturnDetailsStore: In-memory (no persistence)

Orphaned Entity Cleanup

All stores automatically clean up entities when tabs are closed using Angular effects and the @isa/core/tabs service.

Error Handling

Best Practices

  1. Use Zod safeParse() for input validation
  2. Handle service errors with try/catch
  3. Use Observable error callbacks for streams
  4. Check store entity status for error states
  5. Integrate with @isa/core/logging for debugging

Common Error Types

  • Validation Errors: ZodError from schema validation
  • API Errors: ResponseArgsError from API calls
  • Business Logic Errors: Custom OMS error classes
  • Process Errors: ReturnProcessIsNotCompleteError for incomplete submissions

Testing

The library uses Jest with Spectator for testing.

Running Tests

# Run tests
npx nx test oms-data-access --skip-nx-cache

# Run with coverage
npx nx test oms-data-access --code-coverage --skip-nx-cache

# Run in watch mode
npx nx test oms-data-access --watch

Architecture Notes

Layer Architecture

Features → Facades → Services → Stores → Helpers → Generated APIs

Design Patterns

  • Service Layer Pattern - Business logic encapsulation
  • State Management Pattern - NgRx Signals with persistence
  • Question Registry Pattern - Category-specific question lookup
  • Helper Function Pattern - Pure transformation functions
  • Adapter Pattern - API DTO to domain model mapping

Dependencies

  • @angular/core - Angular framework
  • @ngrx/signals - State management
  • @generated/swagger/oms-api - OMS API client
  • @isa/core/logging - Logging infrastructure
  • @isa/core/tabs - Tab management
  • @isa/core/storage - Storage providers
  • zod - Schema validation
  • rxjs - Reactive programming

Path Alias

Import from: @isa/oms/data-access


License

Internal ISA Frontend library - not for external distribution.