@isa/oms/feature/return-details
A comprehensive Angular feature library for displaying receipt details and managing product returns in the Order Management System (OMS). Provides an interactive interface for viewing receipt information, selecting items for return, configuring return quantities and product categories, and initiating return processes.
Overview
The Return Details feature library implements a sophisticated receipt visualization and return management interface. It displays detailed receipt information including customer data, order history, product items, and pricing. The library enables users to select items for return, specify quantities, categorize products, and validate return eligibility in real-time. It supports both primary receipt views and lazy-loaded customer receipt history for comprehensive return workflows.
Table of Contents
- Features
- Quick Start
- Core Concepts
- Component API Reference
- Usage Examples
- Routing and Navigation
- Architecture Notes
- Dependencies
- Testing
- Best Practices
Features
- Receipt Details Display - Comprehensive receipt visualization with buyer, shipping, and order information
- Customer Receipt History - Lazy-loaded customer purchase history based on email address
- Item Selection - Interactive item selection with checkbox controls for return processing
- Quantity Management - Dropdown-based quantity selection for partial returns
- Product Categorization - Category selection for each return item (required for processing)
- Return Eligibility Validation - Real-time validation of item return eligibility with API integration
- Expandable Sections - Collapsible order details for improved UX and information density
- Customer Navigation - Quick access to customer details, orders, and history
- Batch Operations - Select/deselect all items in a receipt with single action
- Available Quantity Tracking - Displays remaining returnable quantity (accounts for previous returns)
- Return Process Integration - Seamless handoff to return process workflow with selected items
- E2E Testing Support - Comprehensive
data-whatanddata-whichattributes throughout
Quick Start
1. Import Routes
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: 'return-details',
loadChildren: () =>
import('@isa/oms/feature/return-details').then((m) => m.routes),
},
];
2. Navigate to Receipt Details
import { Component, inject } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-return-search',
template: '...',
})
export class ReturnSearchComponent {
#router = inject(Router);
#route = inject(ActivatedRoute);
viewReceiptDetails(receiptId: number): void {
this.#router.navigate(['return-details', receiptId], {
relativeTo: this.#route,
});
}
}
3. Component Usage in Custom Context
import { Component } from '@angular/core';
import { ReturnDetailsComponent } from '@isa/oms/feature/return-details';
import { ReturnDetailsStore } from '@isa/oms/data-access';
@Component({
selector: 'app-custom-return-view',
template: `
<oms-feature-return-details></oms-feature-return-details>
`,
imports: [ReturnDetailsComponent],
providers: [ReturnDetailsStore], // Required for component state management
})
export class CustomReturnViewComponent {}
Core Concepts
Return Details Store
The library relies on ReturnDetailsStore from @isa/oms/data-access for state management. The store provides:
- Receipt entities - Normalized receipt storage with entity management
- Item selection state - Tracks selected item IDs across receipts
- Quantity mapping - Maps item IDs to selected return quantities
- Category mapping - Maps item IDs to selected product categories
- Return eligibility - Caches and manages return eligibility checks
- Resources - Provides reactive resources for receipt and eligibility data
Receipt Loading Strategy
The library uses two loading strategies:
- Static (Primary Receipt) - Immediately loads the receipt specified in the URL route parameter (
receiptId) - Lazy (Customer History) - Loads additional customer receipts on-demand based on the buyer's email address
This approach optimizes performance by loading only essential data initially, then expanding to show related receipts when available.
Item Return Eligibility
Return eligibility is determined through multiple checks:
- API Validation -
canReturnendpoint checks business rules - Category Selection - Product category must be selected before return
- Available Quantity - Items with 0 available quantity cannot be returned
- Receipt Item Actions - Item metadata includes return eligibility flags
The UI reflects these checks with disabled states, error messages, and loading indicators.
Product Categories
Each return item must be categorized before processing. Available categories are provided by ReturnDetailsService.availableCategories():
const availableCategories: Array<{ key: ProductCategory; value: string }> = [
{ key: ProductCategory.Book, value: 'Buch' },
{ key: ProductCategory.Media, value: 'Medien' },
{ key: ProductCategory.Other, value: 'Sonstiges' },
// ... additional categories
];
Return Process Initiation
When users click "Rückgabe starten" (Start Return), the component:
- Groups selected items by receipt ID
- Collects selected quantities and categories for each item
- Passes data to
ReturnProcessStoreto initialize the return workflow - Navigates to the return process route
Component API Reference
ReturnDetailsComponent (Main Component)
The routed component that orchestrates the return details view.
Selector: oms-feature-return-details
Route Parameter:
receiptId: number- The ID of the receipt to display (parsed from route params)
Computed Properties:
receiptId(): number- Parsed receipt ID from route parameters (throws if missing)canStartProcess(): boolean- Returns true if items are selected and process ID exists
Resources:
receiptResource- Reactive resource loading the primary receiptcustomerReceiptsResource- Reactive resource loading customer's receipt history by email
Methods:
startProcess(): void- Initiates the return process with selected items and navigates to process view
Navigation:
- Back button: Calls
location.back()to return to previous view - Start return button: Navigates to
../../processrelative route
Example:
// Component handles route parameter automatically
// Route: /return-details/12345
// receiptId() === 12345
// User clicks "Rückgabe starten"
// Component groups items by receipt, navigates to process
ReturnDetailsStaticComponent
Displays the primary receipt details (non-expandable, always visible).
Selector: oms-feature-return-details-static
Inputs:
| Input | Type | Required | Description |
|---|---|---|---|
receipt |
Receipt |
Yes | The full receipt object to display |
Template Structure:
- Header with customer information and navigation
- Expandable order details section
- Order group toolbar (date, receipt number, select all)
- List of receipt items with return controls
Example:
@Component({
template: `
<oms-feature-return-details-static
[receipt]="receipt()"
></oms-feature-return-details-static>
`,
})
export class MyComponent {
receipt = signal<Receipt>({
id: 123,
receiptNumber: 'R-2024-00123',
printedDate: '2024-01-15T10:30:00Z',
items: [/* ... */],
buyer: {/* ... */},
});
}
ReturnDetailsLazyComponent
Displays secondary receipts from customer history (expandable, lazy-loaded).
Selector: oms-feature-return-details-lazy
Inputs:
| Input | Type | Required | Description |
|---|---|---|---|
receipt |
ReceiptListItem |
Yes | Simplified receipt object from customer history |
Computed Properties:
receiptId(): number | undefined- Returns receipt ID only when expanded (triggers lazy loading)
Resources:
receiptResource- Loads full receipt details when expanded
Behavior:
- Renders collapsed by default (shows only order group toolbar)
- Fetches full receipt data when user expands the section
- Displays full item list and details once loaded
Example:
@Component({
template: `
@for (receipt of customerReceipts(); track receipt.id) {
<oms-feature-return-details-lazy
[receipt]="receipt"
></oms-feature-return-details-lazy>
}
`,
})
export class MyComponent {
customerReceipts = signal<ReceiptListItem[]>([/* ... */]);
}
ReturnDetailsHeaderComponent
Displays customer information with navigation menu.
Selector: oms-feature-return-details-header
Inputs:
| Input | Type | Required | Description |
|---|---|---|---|
receiptId |
number |
Yes | The receipt ID for store lookup |
Computed Properties:
receipt(): Receipt- Full receipt from storebuyer(): Buyer | undefined- Buyer information from receiptreferenceId(): number | undefined- Customer reference ID for navigationname(): string- Formatted customer name (handles individual and organization names)
Menu Actions:
- Kundendetails - Navigate to customer details page
- Bestellungen - Navigate to customer orders page
- Historie - Navigate to customer history page
E2E Attributes:
[data-what="button"][data-which="customer-actions"]- Customer menu trigger[data-what="heading"][data-which="customer-name"]- Customer name display[data-what="menu-item"][data-which="customer-details"]- Menu items
Example:
@Component({
template: `
<oms-feature-return-details-header
[receiptId]="12345"
></oms-feature-return-details-header>
`,
})
export class MyComponent {}
ReturnDetailsOrderGroupComponent
Displays receipt summary toolbar with item count, date, and batch selection.
Selector: oms-feature-return-details-order-group
Inputs:
| Input | Type | Required | Description |
|---|---|---|---|
receipt |
ReceiptInput |
Yes | Receipt with id, printedDate, receiptNumber, items |
Computed Properties:
receiptId(): number- Receipt ID for store operationsitemCount(): number- Total number of items (handles both array and count)selectableItems(): ReceiptItem[]- Items eligible for return selectionallSelected(): boolean- True if all selectable items are selected
Methods:
selectOrUnselectAll(): void- Toggles selection state for all items in receipt
Behavior:
- Displays as expandable trigger when used with
uiExpandableTriggerdirective - Shows chevron icon when expandable context is available
- Batch select/deselect button only visible when selectable items exist
Example:
@Component({
template: `
<ng-container uiExpandable>
<oms-feature-return-details-order-group
uiExpandableTrigger
[receipt]="receipt()"
></oms-feature-return-details-order-group>
<div *uiExpanded>
<!-- Expanded content -->
</div>
</ng-container>
`,
})
export class MyComponent {
receipt = signal({
id: 123,
printedDate: '2024-01-15T10:30:00Z',
receiptNumber: 'R-2024-00123',
items: [/* ... */],
});
}
ReturnDetailsOrderGroupItemComponent
Displays individual product item with pricing, details, and return status.
Selector: oms-feature-return-details-order-group-item
Inputs:
| Input | Type | Required | Description |
|---|---|---|---|
item |
ReceiptItem |
Yes | The receipt item with product details and pricing |
Computed Properties:
selected(): boolean- Item selection state from storecanReturn(): { result: boolean; message?: string }- Return eligibility from APIcanReturnReceiptItem(): boolean- Checks if item is in returnable items listcanReturnMessage(): string- Explanation message for return eligibilityquantity(): number- Original quantity from orderavailableQuantity(): number- Remaining returnable quantity
Template Features:
- Product image with router link to product details
- Product title, contributors, and pricing information
- Product format icon and details (manufacturer, EAN, publication date)
- Return controls (quantity, category, checkbox)
- Warning messages for non-returnable items
- Partial return notification
E2E Attributes:
[data-what="return-item-row"][data-which="<EAN>"]- Item row container[data-what="product-image"][data-which="<EAN>"]- Product image[data-what="product-name"][data-which="<EAN>"]- Product name[data-what="product-price"][data-which="<EAN>"]- Price display
Example:
@Component({
template: `
<oms-feature-return-details-order-group-item
[item]="receiptItem()"
class="border-b border-isa-neutral-300"
></oms-feature-return-details-order-group-item>
`,
})
export class MyComponent {
receiptItem = signal<ReceiptItem>({
id: 456,
product: {
ean: '9783123456789',
name: 'Example Book',
contributors: 'John Author',
format: 'HC',
formatDetail: 'Hardcover',
manufacturer: 'Example Publisher',
publicationDate: '2024-01-01',
},
price: {
value: { value: 29.99, currency: 'EUR' },
vat: { inPercent: 7 },
},
});
}
ReturnDetailsOrderGroupItemControlsComponent
Interactive controls for quantity selection, category assignment, and item selection.
Selector: oms-feature-return-details-order-group-item-controls
Inputs:
| Input | Type | Required | Description |
|---|---|---|---|
item |
ReceiptItem |
Yes | The receipt item for control configuration |
Computed Properties:
selected(): boolean- Selection state from storeselectedQuantity(): number- Selected quantity from storequantityDropdownValues(): number[]- Array of selectable quantities (1 to available)productCategory(): ProductCategory- Selected category from storeselectable(): boolean- True if item can be selected (eligibility passed)canReturnReceiptItem(): boolean- Return eligibility flag
Resources:
canReturnResource- Reactive resource for return eligibility validation
Methods:
setProductCategory(category: ProductCategory): void- Updates category in storesetQuantity(quantity: number): void- Updates quantity in storesetSelected(selected: boolean): void- Updates selection state in store
UI Elements:
- Quantity Dropdown - Only visible when available quantity > 1
- Category Dropdown - Required for all returnable items
- Selection Checkbox - Bullet-style checkbox for return selection
- Loading Spinner - Displayed during eligibility validation
E2E Attributes:
[data-what="return-item-checkbox"][data-which="<EAN>"]- Selection checkbox[data-what="load-spinner"][data-which="can-return"]- Loading indicator
Example:
@Component({
template: `
<oms-feature-return-details-order-group-item-controls
[item]="receiptItem()"
></oms-feature-return-details-order-group-item-controls>
`,
})
export class MyComponent {
receiptItem = signal<ReceiptItem>({
id: 456,
product: { ean: '9783123456789', /* ... */ },
/* ... */
});
}
ReturnDetailsDataComponent
Displays minimal receipt metadata (date and type) in collapsed view.
Selector: oms-feature-return-details-data
Inputs:
| Input | Type | Required | Description |
|---|---|---|---|
receipt |
ReceiptInput |
Yes | Receipt with printedDate and receiptType |
Template Structure:
- Receipt date with time (formatted:
dd.MM.yyyy | HH:mm Uhr) - Receipt type (translated to German)
Example:
@Component({
template: `
<oms-feature-return-details-data
*uiCollapsed
[receipt]="receipt()"
></oms-feature-return-details-data>
`,
})
export class MyComponent {
receipt = signal({
printedDate: '2024-01-15T10:30:00Z',
receiptType: 'Invoice',
});
}
ReturnDetailsOrderGroupDataComponent
Displays comprehensive receipt metadata including buyer and shipping information.
Selector: oms-feature-return-details-order-group-data
Inputs:
| Input | Type | Required | Description |
|---|---|---|---|
receipt |
ReceiptInput |
Yes | Receipt with buyer, shipping, order, printedDate, receiptType |
Computed Properties:
buyerName(): string- Formatted buyer full nameshippingName(): string- Formatted shipping recipient full name
Template Structure:
- Receipt date and type
- Order number (Vorgang-ID)
- Order date
- Buyer address (if available)
- Shipping address (if available)
Example:
@Component({
template: `
<oms-feature-return-details-order-group-data
*uiExpanded
[receipt]="receipt()"
></oms-feature-return-details-order-group-data>
`,
})
export class MyComponent {
receipt = signal<Receipt>({
printedDate: '2024-01-15T10:30:00Z',
receiptType: 'Invoice',
order: {
data: {
orderNumber: 'ORD-2024-00123',
orderDate: '2024-01-14T15:00:00Z',
},
},
buyer: {
firstName: 'John',
lastName: 'Doe',
address: {
street: 'Main St',
streetNumber: '123',
zipCode: '12345',
city: 'Berlin',
},
},
shipping: {
firstName: 'Jane',
lastName: 'Smith',
address: {
street: 'Second Ave',
streetNumber: '456',
zipCode: '54321',
city: 'Munich',
},
},
});
}
Usage Examples
Basic Receipt Details View
import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';
@Component({
selector: 'app-receipt-list',
template: `
<div class="receipt-list">
@for (receipt of receipts(); track receipt.id) {
<button
(click)="viewDetails(receipt.id)"
class="receipt-item"
>
{{ receipt.receiptNumber }} - {{ receipt.printedDate | date }}
</button>
}
</div>
`,
})
export class ReceiptListComponent {
#router = inject(Router);
receipts = signal([
{ id: 101, receiptNumber: 'R-2024-00101', printedDate: '2024-01-15' },
{ id: 102, receiptNumber: 'R-2024-00102', printedDate: '2024-01-16' },
]);
viewDetails(receiptId: number): void {
// Navigate to return details with receipt ID
this.#router.navigate(['/oms/return-details', receiptId]);
}
}
Programmatic Item Selection
import { Component, inject, effect } from '@angular/core';
import { ReturnDetailsStore } from '@isa/oms/data-access';
@Component({
selector: 'app-auto-select-returns',
template: `
<oms-feature-return-details></oms-feature-return-details>
`,
providers: [ReturnDetailsStore],
})
export class AutoSelectReturnsComponent {
#store = inject(ReturnDetailsStore);
constructor() {
// Automatically select all eligible items when loaded
effect(() => {
const returnableItems = this.#store.returnableItems();
if (returnableItems.length > 0) {
const itemIds = returnableItems.map((item) => item.id);
this.#store.addSelectedItems(itemIds);
}
});
}
}
Custom Return Process Handler
import { Component, inject } from '@angular/core';
import { ReturnDetailsStore, ReturnProcessStore } from '@isa/oms/data-access';
import { Router } from '@angular/router';
@Component({
selector: 'app-custom-return-handler',
template: `
<oms-feature-return-details></oms-feature-return-details>
<button
uiButton
color="brand"
[disabled]="!canProcess()"
(click)="processReturn()"
>
Start Custom Return Process
</button>
`,
providers: [ReturnDetailsStore],
})
export class CustomReturnHandlerComponent {
#returnDetailsStore = inject(ReturnDetailsStore);
#returnProcessStore = inject(ReturnProcessStore);
#router = inject(Router);
canProcess = computed(() => {
const selectedItems = this.#returnDetailsStore.selectedItems();
const categories = this.#returnDetailsStore.itemCategoryMap();
// Ensure all selected items have categories
return selectedItems.every((item) =>
categories[item.id] !== ProductCategory.Unknown
);
});
processReturn(): void {
const selectedItems = this.#returnDetailsStore.selectedItems();
const quantities = this.#returnDetailsStore.selectedQuantityMap();
const categories = this.#returnDetailsStore.itemCategoryMap();
const receipts = this.#returnDetailsStore.receiptsEntityMap();
// Group items by receipt
const itemsByReceipt = groupBy(selectedItems, (item) => item.receipt?.id);
const returns = Object.entries(itemsByReceipt).map(([receiptId, items]) => ({
receipt: receipts[Number(receiptId)],
items: items.map((item) => ({
receiptItem: item,
quantity: quantities[item.id],
category: categories[item.id],
})),
}));
// Initialize custom return process
this.#returnProcessStore.startProcess({
processId: Date.now().toString(),
returns,
});
// Navigate to custom handler
this.#router.navigate(['/custom-return-process']);
}
}
Monitoring Return Eligibility
import { Component, inject, effect } from '@angular/core';
import { ReturnDetailsStore } from '@isa/oms/data-access';
@Component({
selector: 'app-eligibility-monitor',
template: `
<oms-feature-return-details></oms-feature-return-details>
<div class="eligibility-summary">
<p>Total Items: {{ totalItems() }}</p>
<p>Returnable: {{ returnableItems().length }}</p>
<p>Selected: {{ selectedItems().length }}</p>
</div>
`,
providers: [ReturnDetailsStore],
})
export class EligibilityMonitorComponent {
#store = inject(ReturnDetailsStore);
totalItems = computed(() => {
const items = this.#store.items();
return Object.keys(items).length;
});
returnableItems = this.#store.returnableItems;
selectedItems = this.#store.selectedItems;
constructor() {
// Log when eligibility changes
effect(() => {
const returnable = this.returnableItems();
console.log('Returnable items:', returnable.length);
returnable.forEach((item) => {
const canReturn = this.#store.getCanReturn(() => item)();
console.log(`Item ${item.id}:`, canReturn);
});
});
}
}
Filtering Customer Receipt History
import { Component, computed, signal } from '@angular/core';
import { ReturnDetailsLazyComponent } from '@isa/oms/feature/return-details';
@Component({
selector: 'app-filtered-receipt-history',
template: `
<input
type="text"
[(ngModel)]="searchTerm"
placeholder="Filter receipts..."
/>
@for (receipt of filteredReceipts(); track receipt.id) {
<oms-feature-return-details-lazy
[receipt]="receipt"
></oms-feature-return-details-lazy>
}
`,
imports: [ReturnDetailsLazyComponent, FormsModule],
})
export class FilteredReceiptHistoryComponent {
searchTerm = signal('');
customerReceipts = signal<ReceiptListItem[]>([/* ... */]);
filteredReceipts = computed(() => {
const term = this.searchTerm().toLowerCase();
if (!term) return this.customerReceipts();
return this.customerReceipts().filter((receipt) =>
receipt.receiptNumber.toLowerCase().includes(term)
);
});
}
Routing and Navigation
Route Configuration
The library exports a routes constant for lazy loading:
// libs/oms/feature/return-details/src/lib/routes.ts
import { Routes } from '@angular/router';
import { ReturnDetailsComponent } from './return-details.component';
export const routes: Routes = [
{ path: ':receiptId', component: ReturnDetailsComponent }
];
Integration in Parent Routes
// App routing configuration
import { Routes } from '@angular/router';
export const omsRoutes: Routes = [
{
path: 'oms',
children: [
{
path: 'return-details',
loadChildren: () =>
import('@isa/oms/feature/return-details').then((m) => m.routes),
},
{
path: 'process',
loadChildren: () =>
import('@isa/oms/feature/return-process').then((m) => m.routes),
},
],
},
];
Navigation Patterns
From Search Results
// Navigate to receipt details from search
this.#router.navigate(['/oms/return-details', receiptId]);
Back to Search
// User clicks back button - uses Location.back()
this.location.back();
To Return Process
// After selecting items, navigate to process (relative navigation)
this.#router.navigate(['../../', 'process'], {
relativeTo: this._activatedRoute,
});
// Resolves to: /oms/process (assuming current: /oms/return-details/123)
To Customer Details
// From header menu, navigate to customer page
const referenceId = 12345;
const timestamp = Date.now();
this.#router.navigate(['/kunde', timestamp, 'customer', 'search', referenceId]);
Route Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
receiptId |
number |
Yes | The ID of the receipt to display |
Parameter Parsing:
// Component automatically parses receiptId from route
receiptId = computed<number>(() => {
const params = this.params();
if (params) {
return z.coerce.number().parse(params['receiptId']);
}
throw new Error('No receiptId found in route params');
});
Tab-Based Navigation
The component uses injectTabId() from @isa/core/tabs for process identification:
private processId = injectTabId();
// Used to track return process across navigation
This enables multiple concurrent return processes in different browser tabs.
Architecture Notes
Component Hierarchy
ReturnDetailsComponent (Routed, provides ReturnDetailsStore)
├── Back Button (Location.back())
├── ReturnDetailsStaticComponent (Primary receipt, always visible)
│ ├── ReturnDetailsHeaderComponent (Customer info + navigation menu)
│ ├── Expandable Section
│ │ ├── ReturnDetailsOrderGroupDataComponent (Expanded: full details)
│ │ └── ReturnDetailsDataComponent (Collapsed: minimal info)
│ ├── ReturnDetailsOrderGroupComponent (Toolbar: date, count, select all)
│ └── ReturnDetailsOrderGroupItemComponent[] (Product items)
│ └── ReturnDetailsOrderGroupItemControlsComponent (Quantity, category, checkbox)
├── ProgressBar (While loading customer receipts)
├── ReturnDetailsLazyComponent[] (Customer receipt history, lazy-loaded)
│ ├── ReturnDetailsOrderGroupComponent (Expandable trigger)
│ └── Expandable Section
│ ├── ProgressBar (While loading full receipt)
│ ├── ReturnDetailsOrderGroupDataComponent (Full details)
│ └── ReturnDetailsOrderGroupItemComponent[] (Product items)
└── Start Return Button (Fixed bottom-right, initiates process)
State Management Architecture
ReturnDetailsStore (NgRx Signals)
├── Receipt Entities (Normalized receipts by ID)
├── Item Entities (Normalized items by ID)
├── Selection State
│ ├── selectedItemIds: Set<number>
│ ├── selectedQuantityMap: Record<itemId, quantity>
│ └── itemCategoryMap: Record<itemId, ProductCategory>
├── Return Eligibility Cache
│ └── canReturnResource(item) → { result: boolean, message?: string }
└── Computed Values
├── selectedItems(): ReceiptItem[]
├── returnableItems(): ReceiptItem[]
└── availableQuantityMap(): Record<itemId, availableQty>
Resource-Based Data Loading
The library uses Angular's resource() API for reactive data fetching:
// Primary receipt - loads immediately
receiptResource = this.#store.receiptResource(this.receiptId);
// Customer receipts - loads based on buyer email
customerReceiptsResource = resource({
params: this.receiptResource.value,
loader: async ({ params, abortSignal }) => {
const email = params.buyer?.communicationDetails?.email;
if (!email) return [];
return await this.#returnDetailsService.fetchReceiptsByEmail(
{ email },
abortSignal
);
},
});
// Lazy receipt - loads only when expanded
receiptResource = this.#store.receiptResource(this.receiptId);
// receiptId computed from expandable state
Performance Optimizations
- Lazy Loading - Customer receipt history loads on-demand
- Expandable Sections - Full receipt details load only when expanded
- OnPush Change Detection - All components use
ChangeDetectionStrategy.OnPush - Resource Caching - Resources automatically cache and deduplicate requests
- Computed Signals - Efficient reactive computations with memoization
- Track By - Optimized
@forloops withtrackexpressions
Integration Points
Dependencies on OMS Data Access:
ReturnDetailsStore- Primary state managementReturnProcessStore- Return process orchestrationReturnDetailsService- API integrationReceiptItem,Receipt,ReceiptListItem- Data modelscanReturnReceiptItem(),getReceiptItemAction()- Helper functions
Dependencies on Shared UI:
@isa/ui/buttons- Button components@isa/ui/expandable- Collapsible sections@isa/ui/input-controls- Dropdowns and checkboxes@isa/ui/item-rows- Product display layout@isa/ui/toolbar- Receipt toolbar@isa/ui/progress-bar- Loading indicators@isa/ui/menu- Customer navigation menu
Dependencies on Shared Features:
@isa/shared/product-image- Product image loading@isa/shared/product-router-link- Product detail navigation
Known Architectural Considerations
1. Store Provider Scope (High Priority)
Current State:
ReturnDetailsStoreis provided at component level inReturnDetailsComponent- Store state is scoped to the component instance
- Navigation away destroys store state
Implication:
- Users cannot navigate back and preserve selection state
- Multi-receipt workflows require reselection on return
Recommendation:
- Consider route-level provider for persistent state during session
- Implement state restoration from session storage if needed
2. Customer Receipt Loading Strategy (Medium Priority)
Current State:
- All customer receipts load simultaneously after primary receipt
- No pagination or incremental loading
- Large receipt histories could impact performance
Recommendation:
- Implement virtual scrolling for receipt lists
- Add pagination controls for customers with many receipts
- Consider limiting initial load to recent receipts
3. Return Eligibility Validation Timing (Low Priority)
Current State:
- Eligibility checks trigger when category changes
- Each category change triggers API call
- No debouncing or batching
Potential Improvement:
- Batch eligibility checks for multiple items
- Debounce category changes to reduce API calls
- Pre-fetch eligibility for all items on load
Dependencies
Required Libraries
@angular/core- Angular framework (v20.1.2)@angular/router- Routing and navigation@angular/common- Common Angular utilities (DatePipe, Location, etc.)@angular/forms- FormsModule for ngModel@isa/oms/data-access- OMS data access layer and state management@isa/core/tabs- Tab ID management@isa/core/logging- Logging service@isa/ui/buttons- Button components@isa/ui/expandable- Expandable section directives@isa/ui/input-controls- Input components (checkbox, dropdown)@isa/ui/item-rows- Item display layout@isa/ui/toolbar- Toolbar component@isa/ui/progress-bar- Progress indicators@isa/ui/menu- Menu components@isa/shared/product-image- Product image directive@isa/shared/product-router-link- Product navigation@isa/icons- Icon library@ng-icons/core- Icon renderingzod- Schema validationlodash- Utility functions (groupBy)
Path Alias
Import from: @isa/oms/feature/return-details
Peer Dependencies
The library requires the following services to be available through dependency injection:
ReturnDetailsStore(must be provided at component or route level)ReturnProcessStore(for return process integration)ReturnDetailsService(from@isa/oms/data-access)
Testing
The library uses Jest with Spectator for testing.
Running Tests
# Run tests for this library
npx nx test oms-feature-return-details --skip-nx-cache
# Run tests with coverage
npx nx test oms-feature-return-details --code-coverage --skip-nx-cache
# Run tests in watch mode
npx nx test oms-feature-return-details --watch
Test Structure
The library includes unit tests for key components:
- ReturnDetailsHeaderComponent - Customer information and navigation menu
- ReturnDetailsOrderGroupItemControlsComponent - Item control interactions
Example Test (ReturnDetailsHeaderComponent)
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import { ReturnDetailsHeaderComponent } from './return-details-header.component';
import { ReturnDetailsStore } from '@isa/oms/data-access';
import { signal } from '@angular/core';
describe('ReturnDetailsHeaderComponent', () => {
let spectator: Spectator<ReturnDetailsHeaderComponent>;
const createComponent = createComponentFactory({
component: ReturnDetailsHeaderComponent,
});
const getReceiptMock = signal<Receipt>({
id: 12345,
items: [],
buyer: {
reference: { id: 12345 },
firstName: 'John',
lastName: 'Doe',
buyerNumber: '123456',
},
});
const storeMock = {
getReceipt: () => getReceiptMock,
};
beforeEach(() => {
spectator = createComponent({
props: {
receiptId: 12345,
},
providers: [
{
provide: ReturnDetailsStore,
useValue: storeMock,
},
],
});
});
it('should create', () => {
expect(spectator.component).toBeTruthy();
});
describe('[data-what="button"][data-which="customer-actions"]', () => {
it('should be disabled if referenceId() returns undefined', () => {
jest.spyOn(spectator.component, 'referenceId').mockReturnValue(undefined);
spectator.detectComponentChanges();
const button = spectator.query(
'[data-what="button"][data-which="customer-actions"]'
);
expect(button).toBeTruthy();
expect(button).toBeDisabled();
});
it('should be enabled if referenceId() returns a value', () => {
jest.spyOn(spectator.component, 'referenceId').mockReturnValue(12345);
spectator.detectComponentChanges();
const button = spectator.query(
'[data-what="button"][data-which="customer-actions"]'
);
expect(button).toBeTruthy();
expect(button).not.toBeDisabled();
});
});
});
Testing Best Practices
- Mock ReturnDetailsStore - Always provide mock store in tests
- Test E2E Attributes - Verify
data-whatanddata-whichattributes exist - Test User Interactions - Verify click handlers, form inputs, navigation
- Test Computed Properties - Ensure reactive computations work correctly
- Test Error States - Verify error messages and disabled states
- Test Loading States - Verify progress indicators during data fetching
E2E Testing Support
All interactive elements include data-what and data-which attributes for automated testing:
// Product item row
<ui-item-row data-what="return-item-row" [attr.data-which]="item.product.ean">
// Selection checkbox
<input
type="checkbox"
data-what="return-item-checkbox"
[attr.data-which]="item().product.ean"
/>
// Customer actions button
<button
data-what="button"
data-which="customer-actions"
>
// Menu items
<a data-what="menu-item" data-which="customer-details">
Best Practices
State Management
Do:
// Use store for all selection state
this.#store.addSelectedItems([itemId]);
this.#store.setQuantity(itemId, quantity);
this.#store.setProductCategory(itemId, category);
Don't:
// Don't manage selection state locally
this.selectedItems.push(item); // ❌ Wrong
Component Inputs
Do:
// Use required inputs with proper typing
item = input.required<ReceiptItem>();
receipt = input.required<Receipt>();
Don't:
// Don't use optional inputs when data is required
@Input() item?: ReceiptItem; // ❌ Old pattern
Resource Loading
Do:
// Use resources for async data with proper error handling
customerReceiptsResource = resource({
params: this.receiptResource.value,
loader: async ({ params, abortSignal }) => {
try {
return await this.service.fetch(params, abortSignal);
} catch (error) {
this.#logger.error('Failed to fetch', error);
return [];
}
},
});
Don't:
// Don't use manual subscription management
ngOnInit() {
this.service.fetch().subscribe(data => {
this.receipts = data; // ❌ Old pattern
});
}
Expandable Sections
Do:
// Use expandable directives correctly
<ng-container uiExpandable #expandable="uiExpandable">
<div uiExpandableTrigger>Click to expand</div>
<div *uiExpanded>Expanded content</div>
<div *uiCollapsed>Collapsed content</div>
</ng-container>
Don't:
// Don't mix expandable patterns
<div uiExpandable *uiExpanded> // ❌ Wrong nesting
Navigation
Do:
// Use relative navigation for known routes
this.#router.navigate(['../../process'], {
relativeTo: this._activatedRoute,
});
// Use Location.back() for browser back
this.location.back();
Don't:
// Don't use hardcoded absolute paths
this.#router.navigate(['/oms/return/process']); // ❌ Brittle
E2E Attributes
Do:
// Always include data-what and data-which for interactive elements
<button
data-what="button"
data-which="start-return"
(click)="startProcess()"
>
// Use dynamic data-which for item-specific elements
<div
data-what="product-item"
[attr.data-which]="item.product.ean"
>
Don't:
// Don't omit E2E attributes
<button (click)="startProcess()"> // ❌ Missing attributes
Logging
Do:
// Use logger with context
#logger = logger(() => ({
component: 'ReturnDetailsComponent',
receiptId: this.receiptId(),
}));
this.#logger.error('Failed to load receipt', error);
Don't:
// Don't use console.log
console.log('Error:', error); // ❌ No context, no production logging
Error Handling
Do:
// Handle errors gracefully with fallbacks
try {
return await this.service.fetch(email, abortSignal);
} catch (error) {
this.#logger.error('Failed to fetch customer receipts', error);
return []; // Return empty array as fallback
}
Don't:
// Don't let errors propagate unhandled
return await this.service.fetch(email); // ❌ No error handling
Performance
Do:
// Use OnPush change detection
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
})
// Use track expressions in @for loops
@for (item of items(); track item.id) {
<app-item [item]="item"></app-item>
}
// Lazy load expensive operations
receiptId = computed(() => {
if (!this.expandable().expanded()) return undefined;
return this.receipt().id;
});
Don't:
// Don't use default change detection
@Component({
changeDetection: ChangeDetectionStrategy.Default, // ❌
})
// Don't use @for without track
@for (item of items()) { // ❌ Poor performance
License
Internal ISA Frontend library - not for external distribution.