Added "skip:ci" tag to multiple project configurations to prevent CI runs for certain libraries. This change affects the following libraries: crm-feature-customer-card-transactions, crm-feature-customer-loyalty-cards, oms-data-access, oms-feature-return-details, oms-feature-return-process, oms-feature-return-summary, remission-data-access, remission-feature-remission-list, remission-feature-remission-return-receipt-details, remission-feature-remission-return-receipt-list, remission-shared-remission-start-dialog, remission-shared-return-receipt-actions, shared-address, shared-delivery, ui-carousel, and ui-dialog. Also updated CI command in package.json to exclude tests with the "skip:ci" tag.
@isa/oms/feature/return-process
A comprehensive Angular feature library for managing product returns with dynamic question flows, validation, and backend integration. Part of the Order Management System (OMS) domain.
Overview
The Return Process feature library provides a complete workflow for processing product returns in a retail environment. It handles complex question flows based on product categories (books, electronics, Tolino devices, etc.), validates user input in real-time, checks return eligibility through both frontend and backend validation, and guides users through the entire return journey from initial selection to final confirmation.
The library integrates with the OMS data-access layer for state management and business logic, leverages NgRx Signals for reactive state updates, and provides a type-safe, schema-validated approach to collecting return information.
Table of Contents
- Features
- Quick Start
- Core Components
- Question System Architecture
- API Reference
- Usage Examples
- Routing and Navigation
- State Management
- Validation and Eligibility
- Testing
- Architecture Notes
- Dependencies
Features
- Dynamic Question Flows - Category-specific questions for books, electronics, Tolino devices, and more
- Five Question Types - Select (chips), Product (EAN search), Checklist (multi-select), Info (display), and Group (nested questions)
- Real-time Validation - Frontend and backend validation with immediate feedback
- Progress Tracking - Visual progress bar showing question completion status
- Eligibility Checking - Automatic determination of return eligibility based on answers
- Barcode Scanning - Integrated scanner support for product identification
- Type-safe State - Zod schema validation for all user inputs
- Automatic Navigation - Conditional routing based on process completion
- Session Persistence - Answers preserved across browser sessions via IndexedDB
- Responsive Design - Tailwind CSS with ISA design system integration
Quick Start
1. Add Routes to Your Application
import { Route } from '@angular/router';
export const routes: Route[] = [
{
path: 'return-process/:id',
loadChildren: () =>
import('@isa/oms/feature/return-process').then((m) => m.routes),
},
];
2. Initialize a Return Process
import { Component, inject } from '@angular/core';
import { ReturnProcessStore } from '@isa/oms/data-access';
import { Router } from '@angular/router';
@Component({
selector: 'app-receipt-selection',
template: '...'
})
export class ReceiptSelectionComponent {
#returnProcessStore = inject(ReturnProcessStore);
#router = inject(Router);
async startReturn(receipt: Receipt, items: ReceiptItem[]): Promise<void> {
const processId = Date.now(); // Unique process ID
// Initialize the return process
this.#returnProcessStore.startProcess({
processId,
returns: [
{
receipt,
items: items.map(item => ({
receiptItem: item,
quantity: 1,
category: 'Buch/Kalender' // Product category
}))
}
]
});
// Navigate to return process
await this.#router.navigate(['/return-process', processId]);
}
}
3. The Library Handles the Rest
Once initialized, the library automatically:
- Displays appropriate questions based on product category
- Validates answers using Zod schemas
- Checks eligibility with backend API
- Shows progress and conditional navigation
- Persists state across sessions
Core Components
ReturnProcessComponent
Main orchestrator component that manages the overall return process workflow.
Selector: oms-feature-return-process
Key Responsibilities:
- Displays all return process items for a given process ID
- Manages navigation (back button, continue to summary)
- Aggregates eligibility across all items
- Validates completion before allowing navigation
Inputs: None (uses route parameter via injectTabId())
Outputs: None (navigation-based)
Template Features:
- Back navigation button
- Header with instructions
- List of return process items
- Conditional "Continue" button (only shows when all items are eligible)
Example Usage:
// Automatically rendered via routing
// Route: /return-process/:id
ReturnProcessItemComponent
Displays a single return process item with product info, questions, and eligibility status.
Selector: oms-feature-return-process-item
Inputs:
returnProcessId: number(required) - The unique identifier for the return process
Key Features:
- Product information display
- Question renderer integration
- Progress bar visualization
- Eligibility status indicators (checkmark/error with message)
- Loading spinner during backend validation
Eligibility States:
- Eligible - Green checkmark with "Artikel bereit für Rückgabe"
- Not Eligible - Red X with specific error message
- Unknown - Loading spinner during validation
Example:
<oms-feature-return-process-item
[returnProcessId]="123">
</oms-feature-return-process-item>
ReturnProcessQuestionsComponent
Container component that fetches and passes questions to the renderer.
Selector: oms-feature-return-process-questions
Inputs:
returnProcessId: number(required) - The return process identifier
Key Responsibilities:
- Retrieves active questions from ReturnProcessService
- Filters questions based on product category
- Passes questions to renderer for display
Example:
<oms-feature-return-process-questions
[returnProcessId]="processId">
</oms-feature-return-process-questions>
ReturnProcessQuestionsRendererComponent
Recursive renderer that displays questions dynamically based on their type.
Selector: oms-feature-return-process-questions-renderer
Inputs:
returnProcessId: number(required) - The return process identifierquestions: ReturnProcessQuestion[](required) - Array of questions to render
Supported Question Types:
- Select - Single-choice chip selection
- Product - EAN input with scanner integration
- Checklist - Multi-select with optional text field
- Info - Display-only informational content
- Group - Nested questions (recursive rendering)
Example:
<oms-feature-return-process-questions-renderer
[returnProcessId]="123"
[questions]="activeQuestions">
</oms-feature-return-process-questions-renderer>
Question Type Components
ReturnProcessSelectQuestionComponent
Single-choice question using chip UI components.
Selector: oms-feature-return-process-select-question
Inputs:
returnProcessId: number(required)question: ReturnProcessSelectQuestion(required)
Features:
- Reactive form control with validation
- Automatic answer persistence to store
- Answer removal when invalid
Example:
<oms-feature-return-process-select-question
[returnProcessId]="123"
[question]="selectQuestion">
</oms-feature-return-process-select-question>
ReturnProcessProductQuestionComponent
Product lookup question with EAN input and barcode scanning.
Selector: oms-feature-return-process-product-question
Inputs:
returnProcessId: number(required)question: ReturnProcessProductQuestion(required)
Features:
- EAN validation with custom validator
- Barcode scanner integration
- Product search via CatalogueSearchService
- Product preview with image and details
- Error states (not found, invalid EAN)
- Auto-focus on input field
States:
- Empty - Input field with "Check" button
- Fetching - Loading spinner on button
- Found - Product display with clear button
- Not Found - Error message "Kein Artikel gefunden"
- Invalid - Error message "Die eingegebene EAN ist ungültig"
Example:
<oms-feature-return-process-product-question
[returnProcessId]="123"
[question]="productQuestion">
</oms-feature-return-process-product-question>
ReturnProcessChecklistQuestionComponent
Multi-select checklist with optional "other" text field.
Selector: oms-feature-return-process-checklist-question
Inputs:
returnProcessId: number(required)question: ReturnProcessChecklistQuestion(required)
Features:
- Multiple option selection via checkboxes
- Optional textarea for "other" input
- Linked signals for reactive updates
- Zod schema validation
- Deep equality checking with lodash
Answer Structure:
{
options?: string[], // Selected checkbox values
other?: string // Optional text input
}
Example:
<oms-feature-return-process-checklist-question
[returnProcessId]="123"
[question]="checklistQuestion">
</oms-feature-return-process-checklist-question>
ReturnProcessInfoQuestionComponent
Display-only informational question (no user input).
Selector: oms-feature-return-process-info-question
Inputs:
question: ReturnProcessInfoQuestion(required)
Features:
- Displays description and content list
- No answer collection
- Always considered "answered"
Example:
<oms-feature-return-process-info-question
[question]="infoQuestion">
</oms-feature-return-process-info-question>
Question System Architecture
Product Categories
The question system supports six product categories, each with tailored question flows:
const ProductCategory = {
Unknown: 'unknown',
BookCalendar: 'Buch/Kalender',
TonDatentraeger: 'Ton-/Datenträger',
SpielwarenPuzzle: 'Spielwaren/Puzzle',
SonstigesNonbook: 'Sonstiges und Non-Book',
ElektronischeGeraete: 'Andere Elektronische Geräte',
Tolino: 'Tolino',
};
Question Types
Five distinct question types handle different input scenarios:
const ReturnProcessQuestionType = {
Select: 'select', // Single choice (chips)
Product: 'product', // EAN lookup
Checklist: 'checklist', // Multi-select
Info: 'info', // Display only
Group: 'group', // Nested questions
};
Question Structure
Select Question Example
{
key: 'item-condition',
description: 'Wie ist der Artikelzustand?',
type: 'select',
options: [
{
label: 'Neuwertig/Originalverpackt',
value: 'Originalverpackt',
nextQuestion: 'return-reason',
returnInfo: { itemCondition: 'Neuwertig/Originalverpackt' }
},
{
label: 'Beschädigt/Fehldruck',
value: 'Geöffnet/Defekt',
returnInfo: { itemCondition: 'Beschädigt/Fehldruck' }
}
]
}
Product Question Example
{
key: 'delivered-item',
description: 'Welcher Artikel wurde geliefert?',
type: 'product',
nextQuestion: 'item-damaged' // Optional next question
}
Checklist Question Example
{
key: 'package-missing-items',
description: 'Was fehlt?',
type: 'checklist',
options: [
{ label: 'Karton / Umverpackung', value: 'Karton / Umverpackung' },
{ label: 'Ladekabel', value: 'Ladekabel' },
{ label: 'Quickstart-Guide', value: 'Quickstart-Guide' }
],
other: {
label: 'Sonstiges'
}
}
Group Question Example (Nested)
{
key: 'package-complete-group',
type: 'group',
questions: [
{
key: 'package-incomplete-info',
description: 'Fehlende Teile',
type: 'info',
content: ['Item 1', 'Item 2']
},
{
key: 'package-missing-items',
description: 'Was fehlt?',
type: 'checklist',
options: [...]
}
]
}
Question Flow and Branching
Questions support conditional branching via the nextQuestion property:
// Question 1: Item Condition
{
key: 'item-condition',
type: 'select',
options: [
{
value: 'Originalverpackt',
nextQuestion: 'return-reason', // Branch to return reason
},
{
value: 'Geöffnet/Defekt',
// No nextQuestion - ends flow
}
]
}
// Question 2: Return Reason (shown only if OVP selected)
{
key: 'return-reason',
type: 'select',
options: [
{
value: 'Gefällt nicht/Widerruf',
// Ends flow
},
{
value: 'Fehllieferung',
nextQuestion: 'delivered-item', // Branch to product lookup
}
]
}
// Question 3: Delivered Item (shown only if wrong item)
{
key: 'delivered-item',
type: 'product',
description: 'Welcher Artikel wurde geliefert?'
}
Active Question Determination
The ReturnProcessService.activeReturnProcessQuestions() method:
- Starts with all category questions from
CategoryQuestionsregistry - Filters based on previous answers using
nextQuestionlogic - Validates answers using Zod schemas
- Returns only questions that should be shown based on user choices
- Detects cyclic dependencies and throws errors
Example Flow:
// User selects "Originalverpackt" for item-condition
answers = { 'item-condition': 'Originalverpackt' };
// Active questions: ['item-condition', 'return-reason']
// ('return-reason' shown because option has nextQuestion)
// User selects "Fehllieferung" for return-reason
answers = {
'item-condition': 'Originalverpackt',
'return-reason': 'Fehllieferung'
};
// Active questions: ['item-condition', 'return-reason', 'delivered-item']
// ('delivered-item' shown because 'Fehllieferung' option has nextQuestion)
Category Question Registry
Questions are organized by category in the CategoryQuestions registry (from @isa/oms/data-access):
const CategoryQuestions: Record<ProductCategory, ReturnProcessQuestion[]> = {
'Buch/Kalender': bookCalendarQuestions,
'Ton-/Datenträger': tonDatentraegerQuestions,
'Spielwaren/Puzzle': nonbookQuestions,
'Sonstiges und Non-Book': nonbookQuestions,
'Andere Elektronische Geräte': elektronischeGeraeteQuestions,
'Tolino': tolinoQuestions,
'unknown': []
};
API Reference
ReturnProcessComponent
Selector: oms-feature-return-process
Inputs: None (uses route parameter)
Computed Signals:
processId: Signal<number | null>- The current process ID from routereturnProcesses: Signal<ReturnProcess[]>- Filtered processes for current IDcanContinueToSummary: Signal<boolean>- True when all questions answered and eligiblecanReturn: WritableSignal<boolean>- Backend validation result for all processes
Methods: None (template-driven navigation)
Template Actions:
location.back()- Navigate to previous page
ReturnProcessItemComponent
Selector: oms-feature-return-process-item
Inputs:
returnProcessId: number(required) - Unique return process identifier
Computed Signals:
returnProcess: Signal<ReturnProcess | undefined>- The current process entityprogress: Signal<{ answered: number; total: number } | undefined>- Question progresseligibleForReturn: Signal<EligibleForReturn | undefined>- Frontend eligibility checkeligibleForReturnState: Signal<EligibleForReturnState>- Combined frontend/backend stateeligibleForReturnMessage: Signal<string | undefined>- Eligibility explanation message
Writable Signals:
canReturn: WritableSignal<CanReturn | undefined>- Backend validation result
Lifecycle:
- Constructor Effect - Automatically validates eligibility on process changes
ReturnProcessQuestionsComponent
Selector: oms-feature-return-process-questions
Inputs:
returnProcessId: number(required) - Return process identifier
Computed Signals:
returnProcess: Signal<ReturnProcess | undefined>- Current processquestions: Signal<ReturnProcessQuestion[]>- Active questions for display
ReturnProcessSelectQuestionComponent
Selector: oms-feature-return-process-select-question
Inputs:
returnProcessId: number(required)question: ReturnProcessSelectQuestion(required)
Form Control:
control: FormControl<string | undefined>- Reactive form control with required validation
Lifecycle:
- Constructor - Sets up value change subscription and answer synchronization
ReturnProcessProductQuestionComponent
Selector: oms-feature-return-process-product-question
Inputs:
returnProcessId: number(required)question: ReturnProcessProductQuestion(required)
Form Control:
control: FormControl<string>- EAN input with validation (required + EAN format)
Signals:
product: WritableSignal<Product | undefined>- Found product datastatus: WritableSignal<{ fetching: boolean; hasResult?: boolean }>- Search status
Methods:
check(): void- Validates EAN and searches for productresetProduct(): void- Clears product and answeronScan(value: string | null): void- Handles barcode scanner input
RxJS Methods:
check: RxMethod<void>- Observable-based product search with error handling
ReturnProcessChecklistQuestionComponent
Selector: oms-feature-return-process-checklist-question
Inputs:
returnProcessId: number(required)question: ReturnProcessChecklistQuestion(required)
Computed Signals:
currentAnswer: Signal<ReturnProcessChecklistAnswer>- Current answer from store
Linked Signals:
options: LinkedSignal<string[] | undefined>- Selected checkbox valuesother: LinkedSignal<string | undefined>- Optional text input
Methods:
updateAnswer(answer: ReturnProcessChecklistAnswer): void- Persists answer to store
Effects:
valueChangesEffect- Watches signal changes and updates store
ReturnProcessInfoQuestionComponent
Selector: oms-feature-return-process-info-question
Inputs:
question: ReturnProcessInfoQuestion(required)
No Methods or Signals (Display-only component)
Usage Examples
Complete Return Process Flow
import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';
import { ReturnProcessStore, Receipt, ReceiptItem } from '@isa/oms/data-access';
@Component({
selector: 'app-return-workflow',
template: `
<button (click)="startReturnProcess()">
Start Return
</button>
`
})
export class ReturnWorkflowComponent {
#returnProcessStore = inject(ReturnProcessStore);
#router = inject(Router);
async startReturnProcess(): Promise<void> {
// Sample data
const receipt: Receipt = {
id: 12345,
printedDate: '2024-01-15',
// ... other receipt properties
};
const items: ReceiptItem[] = [
{
id: 1,
product: {
ean: '9783123456789',
name: 'Sample Book',
category: 'Buch/Kalender'
},
quantity: 1,
// ... other item properties
}
];
// Generate unique process ID (typically tab ID or timestamp)
const processId = Date.now();
try {
// Initialize return process in store
this.#returnProcessStore.startProcess({
processId,
returns: [
{
receipt,
items: items.map(item => ({
receiptItem: item,
quantity: 1,
category: 'Buch/Kalender'
}))
}
]
});
// Navigate to return process page
await this.#router.navigate(['/return-process', processId]);
} catch (error) {
console.error('Failed to start return process:', error);
}
}
}
Accessing Return Process State
import { Component, computed, inject } from '@angular/core';
import { ReturnProcessStore, ReturnProcessService } from '@isa/oms/data-access';
@Component({
selector: 'app-process-monitor',
template: `
<div>
<h3>Return Process Status</h3>
<p>Total Processes: {{ processCount() }}</p>
<p>Completed: {{ completedCount() }}</p>
@for (process of processes(); track process.id) {
<div>
<p>Process {{ process.id }}</p>
<p>Progress: {{ getProgress(process) }}</p>
<p>Eligible: {{ isEligible(process) }}</p>
</div>
}
</div>
`
})
export class ProcessMonitorComponent {
#returnProcessStore = inject(ReturnProcessStore);
#returnProcessService = inject(ReturnProcessService);
processes = this.#returnProcessStore.entities;
processCount = computed(() => this.processes().length);
completedCount = computed(() => {
return this.processes().filter(p => {
const progress = this.#returnProcessService
.returnProcessQuestionsProgress(p);
return progress?.answered === progress?.total;
}).length;
});
getProgress(process: ReturnProcess): string {
const progress = this.#returnProcessService
.returnProcessQuestionsProgress(process);
if (!progress) return 'N/A';
return `${progress.answered}/${progress.total}`;
}
isEligible(process: ReturnProcess): boolean {
const result = this.#returnProcessService.eligibleForReturn(process);
return result?.state === 'eligible';
}
}
Programmatic Answer Setting
import { Component, inject } from '@angular/core';
import { ReturnProcessStore } from '@isa/oms/data-access';
@Component({
selector: 'app-auto-answer',
template: '...'
})
export class AutoAnswerComponent {
#returnProcessStore = inject(ReturnProcessStore);
autoFillAnswers(processId: number): void {
// Set select answer
this.#returnProcessStore.setAnswer(
processId,
'item-condition',
'Originalverpackt'
);
// Set checklist answer
this.#returnProcessStore.setAnswer(
processId,
'package-missing-items',
{
options: ['Ladekabel', 'Quickstart-Guide'],
other: 'Custom missing item'
}
);
// Set product answer
this.#returnProcessStore.setAnswer(
processId,
'delivered-item',
{
ean: '9783123456789',
name: 'Sample Product',
catalogProductNumber: '12345',
// ... other product properties
}
);
// Remove answer
this.#returnProcessStore.removeAnswer(
processId,
'item-condition'
);
}
}
Testing Question Eligibility
import { Component, inject, signal } from '@angular/core';
import {
ReturnProcessService,
ReturnProcess,
EligibleForReturnState
} from '@isa/oms/data-access';
@Component({
selector: 'app-eligibility-check',
template: `
<div>
<h3>Eligibility Status</h3>
@if (eligibility(); as result) {
@switch (result.state) {
@case ('eligible') {
<p class="text-green-600">✓ Eligible for return</p>
}
@case ('not-eligible') {
<p class="text-red-600">✗ Not eligible: {{ result.reason }}</p>
}
@case ('unknown') {
<p class="text-gray-600">⏳ Checking eligibility...</p>
}
}
}
</div>
`
})
export class EligibilityCheckComponent {
#returnProcessService = inject(ReturnProcessService);
process = signal<ReturnProcess>({
id: 1,
processId: 100,
receiptId: 12345,
productCategory: 'Tolino',
quantity: 1,
receiptDate: '2024-01-15',
receiptItem: { /* ... */ },
answers: {
'item-condition': 'Originalverpackt',
'return-reason': 'Gefällt nicht/Widerruf',
'display-damaged': 'Nein',
'device-power': 'Ja'
}
});
eligibility = computed(() => {
return this.#returnProcessService.eligibleForReturn(this.process());
});
}
Routing and Navigation
Route Configuration
The library exports a routes array that defines three routes:
// libs/oms/feature/return-process/src/lib/routes.ts
export const routes: Route[] = [
{
path: '',
component: ReturnProcessComponent
},
{
path: 'summary',
loadChildren: () =>
import('@isa/oms/feature/return-summary').then((feat) => feat.routes)
},
{
path: 'review',
loadChildren: () =>
import('@isa/oms/feature/return-review').then((feat) => feat.routes)
}
];
Integration in Main App
// apps/isa-app/src/app/app-routing.module.ts
import { Route } from '@angular/router';
export const routes: Route[] = [
{
path: 'return-process/:id',
loadChildren: () =>
import('@isa/oms/feature/return-process').then((m) => m.routes)
},
// ... other routes
];
Navigation Flow
- Start - External component navigates to
/return-process/:id - Questions - User answers questions on main route (
/return-process/:id) - Summary - Click "Weiter" navigates to
/return-process/:id/summary - Review - Additional route for final review at
/return-process/:id/review
Process ID Management
The library uses injectTabId() from @isa/core/tabs to retrieve the process ID:
// In ReturnProcessComponent
processId = injectTabId(); // Returns Signal<number | null>
This integrates with the tab system to support multiple concurrent return processes.
Navigation Guards
The "Weiter" (Continue) button is conditionally displayed:
@if (canContinueToSummary() && canReturn()) {
<div class="text-right">
<a routerLink="summary" uiButton color="brand">
Weiter
</a>
</div>
}
Conditions for Navigation:
- All processes must exist
- All questions must be answered
- Frontend eligibility must be "eligible"
- Backend validation must pass
State Management
ReturnProcessStore Integration
The library uses ReturnProcessStore from @isa/oms/data-access for state management:
Store Features:
- Entity Management - Stores
ReturnProcessentities with normalized structure - Session Persistence - Automatically persists to IndexedDB via
IDBStorageProvider - Computed Next ID - Generates unique IDs for new processes
- Answer Management - Methods for setting/removing answers
- Orphan Cleanup - Automatically removes entities when tabs close
Key Methods:
// Start new return process
ReturnProcessStore.startProcess(params: StartProcess): void
// Set answer for a question
ReturnProcessStore.setAnswer<T>(id: number, question: string, answer: T): void
// Remove answer
ReturnProcessStore.removeAnswer(id: number, question: string): void
// Remove processes by ID
ReturnProcessStore.removeAllEntitiesByProcessId(...processIds: number[]): void
// Access entities
ReturnProcessStore.entities: Signal<ReturnProcess[]>
ReturnProcessStore.entityMap: Signal<Record<number, ReturnProcess>>
Answer Structure
Answers are stored as a key-value map with question keys:
interface ReturnProcess {
id: number;
processId: number;
receiptId: number;
receiptItem: ReceiptItem;
receiptDate: string | undefined;
productCategory: string;
quantity: number;
answers: Record<string, unknown>; // Question answers
returnReceipt?: Receipt; // Set when process completed
}
Example Answer Data:
{
'item-condition': 'Originalverpackt',
'return-reason': 'Fehllieferung',
'delivered-item': {
ean: '9783123456789',
name: 'Sample Book',
catalogProductNumber: '12345'
},
'package-missing-items': {
options: ['Ladekabel'],
other: 'Custom item'
}
}
State Synchronization
Components use Angular signals and effects for reactive state updates:
// Example from ReturnProcessSelectQuestionComponent
constructor() {
// Listen to form changes and update store
this.control.valueChanges
.pipe(takeUntilDestroyed())
.subscribe((value) => {
if (this.control.valid) {
this.#returnProcessStore.setAnswer(
this.returnProcessId(),
this.question().key,
value
);
} else {
this.#returnProcessStore.removeAnswer(
this.returnProcessId(),
this.question().key
);
}
});
// Sync store changes back to form
effect(() => {
const value = this.#returnProcessStore
.entityMap()[this.returnProcessId()]
?.answers[this.question().key];
if (value !== this.control.value) {
this.control.setValue(value as string);
}
});
}
Session Persistence
All return process data is automatically persisted to IndexedDB:
- Automatic Save - Every
setAnswer()andremoveAnswer()triggerssaveToStorage() - Restore on Load - State automatically restored when user returns
- Tab Cleanup - Orphaned processes removed via
onInithook
Storage Provider:
withStorage('return-process', IDBStorageProvider)
Validation and Eligibility
Frontend Validation (Schema-Based)
All answers are validated using Zod schemas (defined in @isa/oms/data-access):
// Example schemas
const ReturnProcessQuestionSchema = {
'item-condition': z.enum(['Originalverpackt', 'Geöffnet/Defekt']),
'return-reason': z.enum(['Gefällt nicht/Widerruf', 'Fehllieferung']),
'display-damaged': z.enum(['Ja', 'Nein']),
'delivered-item': z.object({
ean: z.string(),
name: z.string(),
catalogProductNumber: z.string().optional(),
// ... more fields
}).passthrough(),
'package-missing-items': z.object({
options: z.array(z.enum([...])).min(1).optional(),
other: z.string().optional()
})
};
Form-Level Validation
Individual question components use reactive forms with validators:
// Select Question - Required validator
control = new FormControl<string | undefined>(undefined, [
Validators.required
]);
// Product Question - EAN validator
control = new FormControl<string>('', [
Validators.required,
eanValidator // Custom EAN format validator from @isa/utils/ean-validation
]);
Eligibility Determination
Eligibility is checked at two levels:
1. Frontend Eligibility (ReturnProcessService)
eligibleForReturn(returnProcess: ReturnProcess): EligibleForReturn | undefined {
// Returns undefined if questions not answered
// Returns { state, reason } based on category-specific logic
switch (returnProcess.productCategory) {
case 'Andere Elektronische Geräte':
return isElektronischeGeraeteEligibleForReturn(returnProcess, questions);
case 'Ton-/Datenträger':
return isTonDatentraegerEligibleForReturn(returnProcess, questions);
case 'Tolino':
return isTolinoEligibleForReturn(returnProcess, questions);
default:
return { state: 'eligible' };
}
}
Eligibility States:
const EligibleForReturnState = {
NotEligible: 'not-eligible',
Eligible: 'eligible',
Unknown: 'unknown'
};
Example Category Logic (Tolino):
// Not eligible if display damaged
if (answers['display-damaged'] === 'Ja') {
return {
state: 'not-eligible',
reason: 'Display beschädigt - Rückgabe nicht möglich'
};
}
// Not eligible if device doesn't power on
if (answers['device-power'] === 'Nein') {
return {
state: 'not-eligible',
reason: 'Gerät schaltet nicht ein - Rückgabe nicht möglich'
};
}
// Eligible if all checks pass
return { state: 'eligible' };
2. Backend Validation (ReturnCanReturnService)
// In ReturnProcessItemComponent
constructor() {
effect(() => {
const returnProcess = this.returnProcess();
untracked(async () => {
if (returnProcess) {
this.canReturn.set(undefined);
try {
const canReturnResponse = await this.#returnCanReturnService
.canReturn(returnProcess);
this.canReturn.set(canReturnResponse);
} catch (error) {
this.#logger.error('Failed to validate return process', error);
this.canReturn.set(undefined);
}
}
});
});
}
Backend Response:
interface CanReturn {
result: boolean; // Can return or not
message?: string; // Explanation message
}
Combined Eligibility Logic
The final eligibility state combines frontend and backend validation:
eligibleForReturnState = computed(() => {
const frontendEligible = this.eligibleForReturn()?.state;
const backendCanReturn = this.canReturn()?.result;
// Still validating
if (backendCanReturn === undefined) {
return 'unknown';
}
// Both must be true for eligibility
if (backendCanReturn === true && frontendEligible === 'eligible') {
return 'eligible';
}
return 'not-eligible';
});
Progress Calculation
Question progress is calculated considering nested questions:
returnProcessQuestionsProgress(returnProcess: ReturnProcess) {
const questions = getReturnProcessQuestions(returnProcess);
const activeQuestions = this.activeReturnProcessQuestions(returnProcess);
// Count answered questions (including group sub-questions)
const answered = activeQuestions.reduce((acc, q) => {
if (q.type === 'group') {
return acc + q.questions.filter(
subQ => subQ.key in returnProcess.answers
).length;
}
return q.key in returnProcess.answers ? acc + 1 : acc;
}, 0);
// Calculate total possible questions (longest path)
const total = calculateLongestQuestionDepth(questions, returnProcess.answers);
return { answered, total };
}
Testing
The library uses Jest for testing (migration to Vitest pending).
Running Tests
# Run all tests for this library
npx nx test oms-feature-return-process --skip-nx-cache
# Run with coverage
npx nx test oms-feature-return-process --code-coverage --skip-nx-cache
# Run in watch mode
npx nx test oms-feature-return-process --watch
Test Structure
Current test coverage includes:
- Component Tests - Basic component instantiation and template rendering
- Integration Tests - Component interactions with stores and services
- E2E Attributes - Verification of
data-whatanddata-whichattributes
Example Test
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ReturnProcessComponent } from './return-process.component';
describe('ReturnProcessComponent', () => {
let component: ReturnProcessComponent;
let fixture: ComponentFixture<ReturnProcessComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ReturnProcessComponent],
providers: [
// Mock providers
]
}).compileComponents();
fixture = TestBed.createComponent(ReturnProcessComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should have E2E attribute on back button', () => {
const backButton = fixture.nativeElement.querySelector('[data-what="back-navigation"]');
expect(backButton).toBeTruthy();
});
});
E2E Testing Attributes
All interactive elements include E2E testing attributes:
<!-- Back navigation -->
<button data-what="back-navigation" (click)="location.back()">
zurück
</button>
<!-- Summary navigation -->
<a data-what="summary-navigation" routerLink="summary">
Weiter
</a>
<!-- Loading spinner -->
<ui-icon-button
data-what="load-spinner"
data-which="can-return"
[pending]="true">
</ui-icon-button>
Testing Best Practices
- Mock Dependencies - Use TestBed to provide mock services
- Verify E2E Attributes - Always test presence of
data-whatanddata-which - Test User Flows - Simulate complete question flows
- Validate State Changes - Ensure store updates correctly
- Test Error States - Verify error handling and display
Architecture Notes
Component Hierarchy
ReturnProcessComponent (Main orchestrator)
└── ReturnProcessItemComponent (Per-item wrapper)
├── ReturnProductInfoComponent (Product display)
└── ReturnProcessQuestionsComponent (Question container)
└── ReturnProcessQuestionsRendererComponent (Recursive renderer)
├── ReturnProcessSelectQuestionComponent
├── ReturnProcessProductQuestionComponent
├── ReturnProcessChecklistQuestionComponent
├── ReturnProcessInfoQuestionComponent
└── ReturnProcessQuestionsRendererComponent (Recursive for groups)
Data Flow
┌─────────────────────────────────────────────────────────────┐
│ User Interaction │
└────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Question Component (e.g., Select) │
│ - FormControl validation │
│ - User input collection │
└────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ ReturnProcessStore │
│ - setAnswer(id, key, value) │
│ - Persist to IndexedDB │
└────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ ReturnProcessService │
│ - activeReturnProcessQuestions() → Filter visible Qs │
│ - eligibleForReturn() → Frontend validation │
│ - returnProcessQuestionsProgress() → Progress calc │
└────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ ReturnCanReturnService (Backend) │
│ - canReturn() → Backend validation via API │
└────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Component Signals Update │
│ - eligibleForReturnState │
│ - canContinueToSummary │
│ - progress │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ UI Re-renders │
│ - Show/hide questions │
│ - Update progress bar │
│ - Enable/disable navigation │
└─────────────────────────────────────────────────────────────┘
Design Patterns
- Presenter Pattern - Components are thin presenters, business logic in services
- Reactive State - Signals and computed values for automatic UI updates
- Recursive Rendering - Question renderer supports nested question groups
- Strategy Pattern - Different question types use different rendering strategies
- Command Pattern - Store methods (
setAnswer,removeAnswer) encapsulate actions - Observer Pattern - Effects watch signals and trigger side effects
Key Architectural Decisions
1. Two-Level Validation
Decision: Implement both frontend and backend validation
Rationale:
- Frontend validation provides immediate feedback
- Backend validation ensures business rules and data integrity
- Combined approach prevents invalid returns while maintaining UX
Trade-off: Additional complexity and API calls, but improved reliability
2. Recursive Question Rendering
Decision: Use recursive component for nested question groups
Rationale:
- Tolino category requires nested questions (Group type)
- Recursive rendering handles arbitrary nesting depth
- Single renderer component reduces code duplication
Trade-off: Slightly more complex than flat rendering, but handles all cases
3. Signal-Based State Management
Decision: Use Angular signals instead of RxJS observables for local state
Rationale:
- Simpler mental model for component state
- Automatic change detection without manual subscriptions
- Better performance with fine-grained reactivity
Trade-off: Mixing signals and observables can be confusing
4. Session Persistence via IndexedDB
Decision: Persist all return process data to IndexedDB
Rationale:
- Preserve user progress across browser sessions
- Handle page refreshes gracefully
- Support multi-tab workflows
Trade-off: Storage size limitations, async complexity
Known Limitations
- No Undo/Redo - Once answered, questions can only be re-answered, not undone
- Linear Question Flow - Questions follow a tree structure, no arbitrary jumps
- Single Product Category - Each process item has one category, no mixing
- Backend Dependency - Cannot complete process without backend validation
- No Draft Saving - Process must be completed or abandoned, no "save for later"
Future Enhancements
Potential improvements identified:
- Answer History - Track answer changes for audit trail
- Conditional Validation - More complex validation rules based on multiple answers
- Question Templates - Reusable question patterns across categories
- Multi-step Progress - Show step-by-step progress instead of single bar
- Answer Pre-fill - Automatically suggest answers based on previous returns
- Offline Support - Queue backend validation for later when offline
- Migration to Vitest - Update testing framework to match newer libraries
Dependencies
Required Angular Libraries
@angular/core^20.1.2 - Angular framework@angular/common^20.1.2 - Common Angular directives@angular/forms^20.1.2 - Reactive forms@angular/router^20.1.2 - Routing and navigation
Required Internal Libraries
OMS Domain
@isa/oms/data-access- Core business logic, stores, and services@isa/oms/shared/product-info- Product information display component
UI Components
@isa/ui/buttons- Button and icon button components@isa/ui/input-controls- Form controls (chips, checkbox, textarea, text field)@isa/ui/progress-bar- Progress visualization
Shared Components
@isa/shared/scanner- Barcode scanner integration@isa/shared/product-image- Product image directive@isa/shared/product-router-link- Product navigation directive
Core Utilities
@isa/core/tabs- Tab management and process ID injection@isa/core/logging- Centralized logging service@isa/icons- Icon library
Catalogue
@isa/catalogue/data-access- Product search service
Utilities
@isa/utils/ean-validation- EAN format validation
External Dependencies
@ngrx/signals^18.1.2 - Signal-based state management@ngrx/operators^18.1.2 - RxJS operators for NgRx@ng-icons/core^30.3.0 - Icon componentszod^3.24.1 - Schema validationrxjs^7.8.1 - Reactive programminglodash^4.17.21 - Utility functions (isEqual)
Generated API Clients
@generated/swagger/oms-api- OMS API client for backend communication
Path Alias
Import from: @isa/oms/feature/return-process
License
Internal ISA Frontend library - not for external distribution.