Files

@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

  • 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-what and data-which attributes 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:

  1. Static (Primary Receipt) - Immediately loads the receipt specified in the URL route parameter (receiptId)
  2. 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:

  1. API Validation - canReturn endpoint checks business rules
  2. Category Selection - Product category must be selected before return
  3. Available Quantity - Items with 0 available quantity cannot be returned
  4. 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:

  1. Groups selected items by receipt ID
  2. Collects selected quantities and categories for each item
  3. Passes data to ReturnProcessStore to initialize the return workflow
  4. 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 receipt
  • customerReceiptsResource - 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 ../../process relative 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 store
  • buyer(): Buyer | undefined - Buyer information from receipt
  • referenceId(): number | undefined - Customer reference ID for navigation
  • name(): 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 operations
  • itemCount(): number - Total number of items (handles both array and count)
  • selectableItems(): ReceiptItem[] - Items eligible for return selection
  • allSelected(): 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 uiExpandableTrigger directive
  • 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 store
  • canReturn(): { result: boolean; message?: string } - Return eligibility from API
  • canReturnReceiptItem(): boolean - Checks if item is in returnable items list
  • canReturnMessage(): string - Explanation message for return eligibility
  • quantity(): number - Original quantity from order
  • availableQuantity(): 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 store
  • selectedQuantity(): number - Selected quantity from store
  • quantityDropdownValues(): number[] - Array of selectable quantities (1 to available)
  • productCategory(): ProductCategory - Selected category from store
  • selectable(): 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 store
  • setQuantity(quantity: number): void - Updates quantity in store
  • setSelected(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 name
  • shippingName(): 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]);
// 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

  1. Lazy Loading - Customer receipt history loads on-demand
  2. Expandable Sections - Full receipt details load only when expanded
  3. OnPush Change Detection - All components use ChangeDetectionStrategy.OnPush
  4. Resource Caching - Resources automatically cache and deduplicate requests
  5. Computed Signals - Efficient reactive computations with memoization
  6. Track By - Optimized @for loops with track expressions

Integration Points

Dependencies on OMS Data Access:

  • ReturnDetailsStore - Primary state management
  • ReturnProcessStore - Return process orchestration
  • ReturnDetailsService - API integration
  • ReceiptItem, Receipt, ReceiptListItem - Data models
  • canReturnReceiptItem(), 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:

  • ReturnDetailsStore is provided at component level in ReturnDetailsComponent
  • 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 rendering
  • zod - Schema validation
  • lodash - 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

  1. Mock ReturnDetailsStore - Always provide mock store in tests
  2. Test E2E Attributes - Verify data-what and data-which attributes exist
  3. Test User Interactions - Verify click handlers, form inputs, navigation
  4. Test Computed Properties - Ensure reactive computations work correctly
  5. Test Error States - Verify error messages and disabled states
  6. 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.