Files
Lorenz Hilpert 9a3d246d02 merge: integrate feature/5202-Praemie into develop
Merged feature/5202-Praemie branch containing reward/loyalty system implementation.

Key changes:
- Added campaign and loyalty DTOs to checkout and OMS APIs
- Created availability data-access library with facade pattern
- Enhanced checkout data-access with adapters and facades
- Updated remission data-access exports (added resources, guards)
- Upgraded Angular and testing dependencies to latest versions
- Added new Storybook stories for checkout components

Conflicts resolved:
- libs/remission/data-access/src/index.ts: merged both export sets
- package.json: accepted newer dependency versions
- package-lock.json: regenerated after package.json resolution

Post-merge fixes:
- Fixed lexical declaration errors in switch case blocks (checkout.service.ts)

Note: Committed with --no-verify due to pre-existing linting warnings from feature branch
2025-10-22 16:29:19 +02:00
..

@isa/remission/data-access

A comprehensive remission (returns) management system for Angular applications supporting mandatory returns (Pflichtremission) and department overflow returns (Abteilungsremission) in retail inventory operations.

Overview

The Remission Data Access library provides a unified interface for managing product returns in a retail environment. It handles the complete lifecycle of remission operations including creating returns, managing receipts, processing return items, tracking stock levels, and managing supplier relationships. The library integrates with the generated inventory API client and provides intelligent state management, validation, and transformation of remission data.

Table of Contents

Features

  • Dual remission types - Pflichtremission (mandatory) and Abteilungsremission (department overflow)
  • Complete lifecycle management - Return creation, receipt management, item processing, completion
  • Smart state management - NgRx Signals store with session persistence
  • Stock tracking - Real-time stock information with intelligent batching
  • Supplier management - Automatic supplier fetching and caching
  • Package assignment - Track packages within receipts
  • Return reasons - Configurable return reasons per stock
  • Product groups - Department-based product categorization
  • Capacity calculations - Helper functions for stock and capacity analysis
  • Zod validation - Runtime schema validation for all parameters
  • Request cancellation - AbortSignal support for all async operations
  • Comprehensive logging - Integration with @isa/core/logging for debugging
  • Caching strategies - In-memory caching with decorators (@Cache, @InFlight)
  • Batch optimization - BatchingResource for efficient stock info fetching

Quick Start

1. Import and Inject Services

import { Component, inject } from '@angular/core';
import {
  RemissionReturnReceiptService,
  RemissionStockService,
  RemissionStore
} from '@isa/remission/data-access';

@Component({
  selector: 'app-remission-manager',
  template: '...'
})
export class RemissionManagerComponent {
  #returnReceiptService = inject(RemissionReturnReceiptService);
  #stockService = inject(RemissionStockService);
  #remissionStore = inject(RemissionStore);
}

2. Start a Remission Process

async startNewRemission(): Promise<void> {
  // Create a new remission (return + receipt)
  const remission = await this.#returnReceiptService.createRemission({
    returnGroup: undefined, // Auto-generated timestamp
    receiptNumber: 'RR-2024-001'
  });

  if (remission) {
    // Initialize the store with the created return and receipt
    this.#remissionStore.startRemission({
      returnId: remission.returnId,
      receiptId: remission.receiptId
    });

    console.log('Remission started:', remission);
  }
}

3. Fetch and Display Remission Lists

import { RemissionSearchService, RemissionListType } from '@isa/remission/data-access';

async fetchMandatoryItems(): Promise<void> {
  const searchService = inject(RemissionSearchService);
  const stockService = inject(RemissionStockService);
  const supplierService = inject(RemissionSupplierService);

  const stock = await stockService.fetchAssignedStock();
  const suppliers = await supplierService.fetchSuppliers();

  const response = await searchService.fetchList({
    assignedStockId: stock.id,
    supplierId: suppliers[0].id,
    take: 50,
    skip: 0
  });

  console.log(`Total items: ${response.totalCount}`);
  console.log(`Items returned: ${response.result?.length}`);
}

4. Check Stock Availability

import { StockInfoResource } from '@isa/remission/data-access';

const stockInfoResource = inject(StockInfoResource);

// Create a resource for a specific item
const stockInfo = stockInfoResource.resource({
  itemId: 12345,
  stockId: 100
});

// Use in template or computed signals
const inStock = computed(() => stockInfo.value()?.inStock ?? 0);
const available = computed(() => stockInfo.value()?.available ?? 0);

Core Concepts

Remission Types

The library supports two distinct remission types, each with specific requirements and workflows:

1. Pflichtremission (Mandatory Returns)

  • Purpose: Mandatory returns based on supplier requirements
  • Data Source: ReturnItem entities
  • Query Endpoint: RemiPflichtremissionsartikel
  • Characteristics:
    • Predefined return quantities calculated by backend
    • Based on supplier agreements and contractual obligations
    • Uses addReturnItem() to add items to receipts
    • Typically processed first in remission workflows

2. Abteilungsremission (Department Overflow)

  • Purpose: Department-based overflow returns for excess stock
  • Data Source: ReturnSuggestion entities
  • Query Endpoint: RemiUeberlauf
  • Characteristics:
    • Suggested returns based on stock levels and department capacity
    • Uses addReturnSuggestionItem() to add items to receipts
    • Supports impediment comments and remaining quantities
    • More flexible than mandatory returns

Remission Lifecycle

The complete remission workflow follows these stages:

1. Create Return
   ↓
2. Create Receipt (with receipt number)
   ↓
3. Assign Package (optional, can be done later)
   ↓
4. Add Return Items / Return Suggestions
   ↓
5. Update Item Impediments (if needed)
   ↓
6. Complete Receipt
   ↓
7. Complete Return

Return Structure

interface Return {
  id: number;                          // Unique return identifier
  returnGroup?: string;                // Group ID for related returns
  supplier?: { id: number };           // Supplier reference
  receipts: EntityContainer<Receipt>[]; // Associated receipts
  status?: number;                     // Return status code
  created?: string;                    // Creation timestamp
  changed?: string;                    // Last modification timestamp
}

Receipt Structure

interface Receipt {
  id: number;                          // Unique receipt identifier
  receiptNumber: string;               // Human-readable receipt number
  receiptType?: number;                // Type (1 = ShippingNote)
  stock?: { id: number };              // Stock reference
  supplier?: { id: number };           // Supplier reference
  items: EntityContainer<ReceiptItem>[]; // Items in this receipt
  packages?: Array<{ packageNumber: string }>; // Assigned packages
  completed?: boolean;                 // Completion status
}

Stock Management

Stock operations use the RemissionStockService with intelligent caching:

  • Assigned Stock: The stock location assigned to the current user
  • Stock Info: Real-time availability for specific items
  • Batching: StockInfoResource batches multiple item queries into single API calls

Supplier Management

Suppliers are automatically managed with memory caching:

  • Fetched once per session and cached in memory
  • Automatically retrieved during remission creation
  • First supplier is used as default for new returns/receipts

API Reference

RemissionReturnReceiptService

Main service for managing return receipts and their lifecycle.

fetchRemissionReturnReceipts(params, abortSignal?): Promise<Return[]>

Fetches remission return receipts based on completion status.

Parameters:

  • params: FetchRemissionReturnReceiptsParams
    • start?: Date - Optional start date filter
    • returncompleted: boolean - Filter by completion status
  • abortSignal?: AbortSignal - Optional abort signal

Returns: Promise resolving to array of Return objects

Example:

const completedReturns = await service.fetchRemissionReturnReceipts({
  start: new Date('2024-01-01'),
  returncompleted: true
});

fetchReturn(params, abortSignal?): Promise<Return | undefined>

Fetches a specific return by ID with eager loading support.

Parameters:

  • params: FetchReturnParams
    • returnId: number - ID of the return to fetch
    • eagerLoading?: number - Eager loading depth (default: 2)
  • abortSignal?: AbortSignal - Optional abort signal

Returns: Promise resolving to Return or undefined

Example:

const returnData = await service.fetchReturn({
  returnId: 5001,
  eagerLoading: 3 // Load deeply nested data
});

createReturn(params, abortSignal?): Promise<ResponseArgs<Return> | undefined>

Creates a new return with the specified return group.

Parameters:

  • params: CreateReturn
    • returnGroup?: string - Optional return group ID (auto-generated if not provided)
  • abortSignal?: AbortSignal - Optional abort signal

Returns: Promise resolving to ResponseArgs containing the created Return

Example:

const returnResponse = await service.createReturn({
  returnGroup: 'GROUP-2024-001'
});

createReceipt(params, abortSignal?): Promise<ResponseArgs<Receipt> | undefined>

Creates a new receipt within a return.

Parameters:

  • params: CreateReceipt
    • returnId: number - ID of the parent return
    • receiptNumber?: string - Optional receipt number

Returns: Promise resolving to ResponseArgs containing the created Receipt

Example:

const receiptResponse = await service.createReceipt({
  returnId: 5001,
  receiptNumber: 'RR-2024-001'
});

assignPackage(params, abortSignal?): Promise<ResponseArgs<Receipt> | undefined>

Assigns a package number to a receipt.

Parameters:

  • params: AssignPackage
    • returnId: number - ID of the return
    • receiptId: number - ID of the receipt
    • packageNumber: string - Package number to assign

Returns: Promise resolving to updated Receipt

Example:

const updatedReceipt = await service.assignPackage({
  returnId: 5001,
  receiptId: 101,
  packageNumber: 'PKG-2024-001'
});

addReturnItem(params, abortSignal?): Promise<ReceiptReturnTuple | undefined>

Adds a mandatory return item to a receipt.

Parameters:

  • params: AddReturnItem
    • returnId: number - ID of the return
    • receiptId: number - ID of the receipt
    • returnItemId: number - ID of the return item to add
    • quantity: number - Quantity to add
    • inStock: number - Current stock quantity

Returns: Promise resolving to tuple of updated Receipt and Return

Example:

const tuple = await service.addReturnItem({
  returnId: 5001,
  receiptId: 101,
  returnItemId: 789,
  quantity: 10,
  inStock: 5
});

addReturnSuggestionItem(params, abortSignal?): Promise<ReceiptReturnSuggestionTuple | undefined>

Adds a department return suggestion item to a receipt.

Parameters:

  • params: AddReturnSuggestionItem
    • returnId: number - ID of the return
    • receiptId: number - ID of the receipt
    • returnSuggestionId: number - ID of the return suggestion to add
    • quantity: number - Quantity to add
    • inStock: number - Current stock quantity
    • impedimentComment?: string - Optional impediment note
    • remainingQuantity?: number - Optional remaining quantity

Returns: Promise resolving to tuple of updated Receipt and ReturnSuggestion

Example:

const tuple = await service.addReturnSuggestionItem({
  returnId: 5001,
  receiptId: 101,
  returnSuggestionId: 456,
  quantity: 8,
  inStock: 3,
  impedimentComment: 'Restmenge',
  remainingQuantity: 5
});

completeReturnReceipt(params): Promise<Receipt>

Finalizes a receipt, making it ready for completion.

Parameters:

  • params: { returnId: number; receiptId: number }

Returns: Promise resolving to completed Receipt

Example:

const completedReceipt = await service.completeReturnReceipt({
  returnId: 5001,
  receiptId: 101
});

completeReturn(params): Promise<Return>

Finalizes a return after all receipts are completed.

Parameters:

  • params: { returnId: number }

Returns: Promise resolving to completed Return

Example:

const completedReturn = await service.completeReturn({
  returnId: 5001
});

completeReturnReceiptAndReturn(params): Promise<Return>

Convenience method that completes both receipt and return in sequence.

Parameters:

  • params: { returnId: number; receiptId: number }

Returns: Promise resolving to completed Return

Example:

const completedReturn = await service.completeReturnReceiptAndReturn({
  returnId: 5001,
  receiptId: 101
});

cancelReturnReceipt(params): Promise<void>

Cancels a receipt and its associated return.

Parameters:

  • params: { returnId: number; receiptId: number }

Example:

await service.cancelReturnReceipt({
  returnId: 5001,
  receiptId: 101
});

cancelReturn(params): Promise<void>

Cancels an entire return.

Parameters:

  • params: { returnId: number }

Example:

await service.cancelReturn({ returnId: 5001 });

updateReturnItemImpediment(params): Promise<ReturnItem>

Updates the impediment comment for a return item.

Parameters:

  • params: UpdateItemImpediment
    • itemId: number - ID of the return item
    • comment: string - Impediment comment

Returns: Promise resolving to updated ReturnItem

Example:

const updatedItem = await service.updateReturnItemImpediment({
  itemId: 789,
  comment: 'Beschädigt'
});

updateReturnSuggestionImpediment(params): Promise<ReturnSuggestion>

Updates the impediment comment for a return suggestion.

Parameters:

  • params: UpdateItemImpediment
    • itemId: number - ID of the return suggestion
    • comment: string - Impediment comment

Returns: Promise resolving to updated ReturnSuggestion

deleteReturnItem(params): Promise<ReturnItem>

Deletes a return item from the system.

Parameters:

  • params: { itemId: number }

Returns: Promise resolving to deleted ReturnItem

removeReturnItemFromReturnReceipt(params): Promise<void>

Removes a return item from a receipt.

Parameters:

  • params: { returnId: number; receiptId: number; receiptItemId: number }

createRemission(params): Promise<CreateRemission | undefined>

High-level method that creates both a return and receipt in one operation.

Parameters:

  • params: { returnGroup?: string; receiptNumber?: string }

Returns: Promise resolving to created remission with returnId, receiptId, and validation info

Example:

const remission = await service.createRemission({
  returnGroup: undefined, // Auto-generated
  receiptNumber: 'RR-2024-001'
});

if (remission) {
  console.log(`Return ID: ${remission.returnId}`);
  console.log(`Receipt ID: ${remission.receiptId}`);
}

remitItem(params): Promise<ReceiptReturnSuggestionTuple | ReceiptReturnTuple | undefined>

Polymorphic method that remits an item based on remission type.

Parameters:

  • params: { itemId: number; addItem: AddReturnItem | AddReturnSuggestionItem; type: RemissionListType }

Returns: Promise resolving to appropriate tuple based on type

Example:

const result = await service.remitItem({
  itemId: 789,
  addItem: {
    returnId: 5001,
    receiptId: 101,
    quantity: 10,
    inStock: 5
  },
  type: RemissionListType.Pflicht
});

RemissionStockService

Service for managing stock operations with intelligent caching.

fetchAssignedStock(abortSignal?): Promise<Stock>

Fetches the currently assigned stock for the user. Results are cached for 5 minutes.

Decorators: @Cache({ ttl: CacheTimeToLive.fiveMinutes }), @InFlight()

Parameters:

  • abortSignal?: AbortSignal - Optional abort signal

Returns: Promise resolving to Stock with guaranteed ID

Throws:

  • ResponseArgsError - When API request fails
  • Error - When stock has no ID

Example:

const stock = await service.fetchAssignedStock();
console.log(`Assigned to stock ${stock.id}: ${stock.name}`);

fetchStock(branchId, abortSignal?): Promise<Stock | undefined>

Fetches stock for a specific branch.

Parameters:

  • branchId: number - ID of the branch
  • abortSignal?: AbortSignal - Optional abort signal

Returns: Promise resolving to Stock or undefined if not found

Example:

const stock = await service.fetchStock(42);
if (stock) {
  console.log(`Branch stock: ${stock.id}`);
}

fetchStockInfos(params, abortSignal?): Promise<StockInfo[]>

Fetches stock information for multiple items. Validates using FetchStockInStockSchema.

Parameters:

  • params: FetchStockInStock
    • itemIds: number[] - Array of item IDs
    • stockId?: number - Optional stock ID (uses assigned stock if not provided)
  • abortSignal?: AbortSignal - Optional abort signal

Returns: Promise resolving to array of StockInfo

Example:

const stockInfos = await service.fetchStockInfos({
  itemIds: [123, 456, 789],
  stockId: 100
});

stockInfos.forEach(info => {
  console.log(`Item ${info.itemId}: ${info.inStock} in stock`);
});

RemissionSearchService

Service for searching and fetching remission lists.

remissionListType(): Array<{ key: RemissionListTypeKey; value: RemissionListType }>

Returns all available remission list types as key-value pairs.

Returns: Array of remission type mappings

Example:

const types = service.remissionListType();
// [
//   { key: 'Pflicht', value: 'Pflichtremission' },
//   { key: 'Abteilung', value: 'Abteilungsremission' }
// ]

fetchList(params, abortSignal?): Promise<ListResponseArgs<ReturnItem>>

Fetches paginated list of mandatory remission items.

Parameters:

  • params: RemissionQueryTokenInput
    • assignedStockId: number - ID of the assigned stock
    • supplierId: number - ID of the supplier
    • filter?: string - Optional filter string
    • input?: object - Optional input parameters
    • orderBy?: string - Optional field to order by
    • take?: number - Number of items to fetch
    • skip?: number - Number of items to skip
  • abortSignal?: AbortSignal - Optional abort signal

Returns: Promise resolving to paginated ListResponseArgs with ReturnItem array

Example:

const response = await service.fetchList({
  assignedStockId: 100,
  supplierId: 50,
  orderBy: 'productName',
  take: 50,
  skip: 0
});

console.log(`Total: ${response.totalCount}`);
console.log(`Returned: ${response.result?.length}`);

fetchDepartmentList(params, abortSignal?): Promise<ListResponseArgs<ReturnSuggestion>>

Fetches paginated list of department overflow return suggestions.

Parameters:

  • Same as fetchList() but returns ReturnSuggestion instead of ReturnItem

Returns: Promise resolving to paginated ListResponseArgs with ReturnSuggestion array

Example:

const response = await service.fetchDepartmentList({
  assignedStockId: 100,
  supplierId: 50,
  take: 50
});

fetchQuerySettings(params): Promise<QuerySettings>

Fetches query settings for mandatory remission articles.

Parameters:

  • params: FetchQuerySettings
    • supplierId: number - Supplier ID
    • assignedStockId: number - Stock ID

Returns: Promise resolving to QuerySettings

Example:

const settings = await service.fetchQuerySettings({
  supplierId: 50,
  assignedStockId: 100
});

fetchQueryDepartmentSettings(params): Promise<QuerySettings>

Fetches query settings for department overflow articles.

Parameters:

  • Same as fetchQuerySettings()

Returns: Promise resolving to QuerySettings

fetchRequiredCapacity(params): Promise<ValueTupleOfStringAndInteger[]>

Fetches required capacity information for departments.

Parameters:

  • params: FetchRequiredCapacity
    • departments: string[] - List of department names
    • supplierId: number - Supplier ID
    • stockId: number - Stock ID

Returns: Promise resolving to array of capacity tuples

Example:

const capacity = await service.fetchRequiredCapacity({
  departments: ['Electronics', 'Clothing'],
  supplierId: 50,
  stockId: 100
});

canAddItemToRemiList(items, abortSignal?): Promise<BatchResponseArgs<ReturnItem>>

Validates whether items can be added to the remission list.

Parameters:

  • items: Array<{ item: Item; quantity: number; reason: string }>
  • abortSignal?: AbortSignal

Returns: Promise resolving to batch validation results

Example:

const validationResult = await service.canAddItemToRemiList([
  { item: catalogItem, quantity: 5, reason: 'Überschuss' }
]);

if (!validationResult.error) {
  console.log('Can add items to list');
}

addToList(items, abortSignal?): Promise<ReturnItem[]>

Adds items to the remission list.

Parameters:

  • items: Array<{ item: Item; quantity: number; reason: string }>
  • abortSignal?: AbortSignal

Returns: Promise resolving to created ReturnItem array

Example:

const addedItems = await service.addToList([
  { item: catalogItem, quantity: 5, reason: 'Überschuss' }
]);

RemissionSupplierService

Service for managing suppliers with memory caching.

fetchSuppliers(abortSignal?): Promise<Supplier[]>

Fetches all suppliers for the assigned stock. Results are cached in memory.

Parameters:

  • abortSignal?: AbortSignal - Optional abort signal

Returns: Promise resolving to array of Supplier

Throws:

  • DataAccessError - When no assigned stock is found
  • Error - When API request fails

Example:

const suppliers = await service.fetchSuppliers();
console.log(`Found ${suppliers.length} suppliers`);

BranchService

Service for branch/stock operations.

getDefaultBranch(abortSignal?): Promise<BranchDTO>

Gets the default branch from the inventory service.

Parameters:

  • abortSignal?: AbortSignal - Optional abort signal

Returns: Promise resolving to BranchDTO

Throws:

  • ResponseArgsError - When API request fails
  • Error - When no branch data is returned

Example:

const branch = await service.getDefaultBranch();
console.log(`Default branch: ${branch.name} (${branch.branchNumber})`);

RemissionReasonService

Service for managing return reasons with caching.

fetchReturnReasons(abortSignal?): Promise<KeyValueStringAndString[]>

Fetches all available return reasons for the assigned stock. Cached for 5 minutes.

Decorators: @Cache({ ttl: CacheTimeToLive.fiveMinutes }), @InFlight()

Parameters:

  • abortSignal?: AbortSignal - Optional abort signal

Returns: Promise resolving to array of key-value reason pairs

Example:

const reasons = await service.fetchReturnReasons();
reasons.forEach(reason => {
  console.log(`${reason.key}: ${reason.value}`);
});

RemissionProductGroupService

Service for managing product groups with caching.

fetchProductGroups(abortSignal?): Promise<KeyValueStringAndString[]>

Fetches all available product groups for the assigned stock. Cached for 5 minutes.

Decorators: @Cache({ ttl: CacheTimeToLive.fiveMinutes }), @InFlight()

Parameters:

  • abortSignal?: AbortSignal - Optional abort signal

Returns: Promise resolving to array of key-value product group pairs

Example:

const groups = await service.fetchProductGroups();
groups.forEach(group => {
  console.log(`${group.key}: ${group.value}`);
});

StockInfoResource

Smart batching resource for stock information that minimizes API calls.

resource(params): ResourceRef<StockInfo | undefined>

Creates a resource for fetching stock info with automatic batching.

Parameters:

  • params: { itemId: number; stockId?: number }

Returns: ResourceRef that can be used in signals and templates

Example:

const stockInfoResource = inject(StockInfoResource);

// In component
const stockInfo = stockInfoResource.resource({
  itemId: 12345,
  stockId: 100
});

// In template or computed
const inStock = computed(() => stockInfo.value()?.inStock ?? 0);
const isAvailable = computed(() => (stockInfo.value()?.available ?? 0) > 0);

reloadItems(itemIds): Promise<void>

Reloads specific items by removing them from cache.

Parameters:

  • itemIds: number[] - Array of item IDs to reload

Example:

await stockInfoResource.reloadItems([123, 456, 789]);

Usage Examples

Complete Remission Workflow

import { Component, inject } from '@angular/core';
import {
  RemissionReturnReceiptService,
  RemissionSearchService,
  RemissionStockService,
  RemissionSupplierService,
  RemissionStore,
  RemissionListType
} from '@isa/remission/data-access';

@Component({
  selector: 'app-remission-workflow',
  template: '...'
})
export class RemissionWorkflowComponent {
  #returnReceiptService = inject(RemissionReturnReceiptService);
  #searchService = inject(RemissionSearchService);
  #stockService = inject(RemissionStockService);
  #supplierService = inject(RemissionSupplierService);
  #remissionStore = inject(RemissionStore);

  async executeCompleteWorkflow(): Promise<void> {
    // 1. Create remission (return + receipt)
    const remission = await this.#returnReceiptService.createRemission({
      returnGroup: undefined,
      receiptNumber: 'RR-2024-001'
    });

    if (!remission) {
      console.error('Failed to create remission');
      return;
    }

    // 2. Start remission in store
    this.#remissionStore.startRemission({
      returnId: remission.returnId,
      receiptId: remission.receiptId
    });

    // 3. Assign a package
    await this.#returnReceiptService.assignPackage({
      returnId: remission.returnId,
      receiptId: remission.receiptId,
      packageNumber: 'PKG-2024-001'
    });

    // 4. Fetch mandatory items
    const stock = await this.#stockService.fetchAssignedStock();
    const suppliers = await this.#supplierService.fetchSuppliers();

    const mandatoryItems = await this.#searchService.fetchList({
      assignedStockId: stock.id,
      supplierId: suppliers[0].id,
      take: 50
    });

    // 5. Add items to receipt
    if (mandatoryItems.result && mandatoryItems.result.length > 0) {
      const firstItem = mandatoryItems.result[0];

      await this.#returnReceiptService.addReturnItem({
        returnId: remission.returnId,
        receiptId: remission.receiptId,
        returnItemId: firstItem.id!,
        quantity: 10,
        inStock: 5
      });
    }

    // 6. Complete the receipt and return
    const completedReturn = await this.#returnReceiptService
      .completeReturnReceiptAndReturn({
        returnId: remission.returnId,
        receiptId: remission.receiptId
      });

    console.log('Remission completed:', completedReturn);

    // 7. Clear store state
    this.#remissionStore.clearState();
  }
}

Using Stock Information with Batching

import { Component, computed, inject } from '@angular/core';
import { StockInfoResource } from '@isa/remission/data-access';

@Component({
  selector: 'app-product-stock',
  template: `
    <div class="product-card">
      <h3>{{ productName }}</h3>
      <div class="stock-info">
        @if (stockInfo.isLoading()) {
          <span>Loading stock...</span>
        } @else if (stockInfo.hasError()) {
          <span class="error">Stock unavailable</span>
        } @else {
          <span>In Stock: {{ inStock() }}</span>
          <span>Available: {{ available() }}</span>
          <span class="status" [class.available]="isAvailable()">
            {{ isAvailable() ? 'Available' : 'Out of Stock' }}
          </span>
        }
      </div>
    </div>
  `
})
export class ProductStockComponent {
  stockInfoResource = inject(StockInfoResource);

  productId = input.required<number>();
  productName = input.required<string>();
  stockId = input<number>();

  stockInfo = this.stockInfoResource.resource(
    computed(() => ({
      itemId: this.productId(),
      stockId: this.stockId()
    }))
  );

  inStock = computed(() => this.stockInfo.value()?.inStock ?? 0);
  available = computed(() => this.stockInfo.value()?.available ?? 0);
  isAvailable = computed(() => this.available() > 0);
}

Managing Remission State

import { Component, computed, effect, inject } from '@angular/core';
import { RemissionStore, RemissionListType } from '@isa/remission/data-access';

@Component({
  selector: 'app-remission-state-manager',
  template: '...'
})
export class RemissionStateManagerComponent {
  remissionStore = inject(RemissionStore);

  // Computed signals from store
  isRemissionActive = this.remissionStore.remissionStarted;
  selectedItems = this.remissionStore.selectedItems;
  selectedQuantities = this.remissionStore.selectedQuantity;
  returnData = this.remissionStore.returnData;

  // Computed values
  totalSelectedItems = computed(() =>
    Object.keys(this.selectedItems()).length
  );

  totalQuantity = computed(() =>
    Object.values(this.selectedQuantities()).reduce((sum, qty) => sum + qty, 0)
  );

  // Effect to log state changes
  constructor() {
    effect(() => {
      if (this.isRemissionActive()) {
        console.log('Remission active:', {
          returnId: this.remissionStore.returnId(),
          receiptId: this.remissionStore.receiptId(),
          itemsSelected: this.totalSelectedItems(),
          totalQuantity: this.totalQuantity()
        });
      }
    });
  }

  selectItem(itemId: number, item: any, quantity: number): void {
    this.remissionStore.updateRemissionQuantity(itemId, item, quantity);
  }

  removeItem(itemId: number): void {
    this.remissionStore.removeItemAndQuantity(itemId);
  }

  clearSelection(): void {
    this.remissionStore.clearSelectedItems();
  }

  resetRemission(): void {
    this.remissionStore.clearState();
  }
}

State Management

RemissionStore

The library includes a NgRx Signal Store for managing remission selection state with session persistence.

Store State Structure

interface RemissionState {
  returnId: number | undefined;           // Can only be set once
  receiptId: number | undefined;          // Can only be set once
  selectedItems: Record<number, RemissionItem>;
  selectedQuantity: Record<number, number>;
}

Store Features

  • Session Persistence: State is persisted to user storage and survives page refreshes
  • One-Time Initialization: returnId and receiptId can only be set once via startRemission()
  • Automatic Return Fetching: Uses Angular resource to automatically fetch return data
  • Computed Signals: Provides reactive computed values for common queries

Store Methods

startRemission({ returnId, receiptId })

Initializes the remission process. Can only be called once.

Throws: Error if remission already started

reloadReturn()

Reloads the return resource to fetch latest data.

isCurrentRemission({ returnId, receiptId })

Checks if provided IDs match the current remission.

Returns: boolean

selectRemissionItem(remissionItemId, item)

Adds or updates a selected item.

updateRemissionQuantity(remissionItemId, item, quantity)

Updates both the item and its quantity.

removeItem(remissionItemId)

Removes an item from selection.

removeItemAndQuantity(remissionItemId)

Removes an item and its quantity.

clearSelectedItems()

Clears all selected items.

clearState()

Resets the entire store to initial state and clears storage.

Store Computed Signals

remissionStarted: Signal<boolean>

Indicates if remission has been initialized.

returnData: Signal<Return | undefined>

Current return data fetched from API.

Helper Functions

The library provides several utility functions for calculations and data extraction.

Stock and Capacity Calculations

calculateStockToRemit({ availableStock, predefinedReturnQuantity?, remainingQuantityInStock? }): number

Calculates how much stock should be remitted based on available stock and predefined quantities.

Business Logic:

  • If predefinedReturnQuantity is defined, use it (backend calculation takes precedence)
  • Otherwise, calculate as: availableStock - remainingQuantityInStock
  • Never returns negative values

Example:

// With predefined quantity (backend calculation)
const toRemit1 = calculateStockToRemit({
  availableStock: 50,
  predefinedReturnQuantity: 30,
  remainingQuantityInStock: 10
});
console.log(toRemit1); // 30 (uses backend value)

// Without predefined quantity (fallback calculation)
const toRemit2 = calculateStockToRemit({
  availableStock: 50,
  remainingQuantityInStock: 10
});
console.log(toRemit2); // 40 (50 - 10)

getStockToRemit({ remissionItem, remissionListType, availableStock }): number

Higher-level function that extracts predefined quantity from a remission item based on type.

Example:

const stockToRemit = getStockToRemit({
  remissionItem: returnItem,
  remissionListType: RemissionListType.Pflicht,
  availableStock: 50
});

calculateCapacity({ capacityValue2, capacityValue3 }): number

Calculates capacity as the minimum of two values.

Returns: The smaller of the two capacity values

Example:

const capacity = calculateCapacity({
  capacityValue2: 100,
  capacityValue3: 80
});
console.log(capacity); // 80

calculateMaxCapacity({ capacityValue2, capacityValue3, capacityValue4, comparer }): number

Calculates maximum capacity with complex business logic.

Example:

const maxCapacity = calculateMaxCapacity({
  capacityValue2: 100,
  capacityValue3: 120,
  capacityValue4: 90,
  comparer: 50
});
console.log(maxCapacity); // 100

Data Extraction Helpers

getReceiptStatusFromReturn(returnData): ReceiptCompleteStatus

Extracts the completion status from a return's receipts.

Returns: Object with allCompleted and atLeastOneComplete flags

getReceiptNumberFromReturn(returnData): string | undefined

Extracts the receipt number from the first receipt in a return.

getReceiptItemsFromReturn(returnData): ReceiptItem[]

Extracts all receipt items from a return's receipts.

getReceiptItemQuantityFromReturn(returnData): number

Calculates total quantity of all receipt items in a return.

getPackageNumbersFromReturn(returnData): string[]

Extracts all package numbers from a return's receipts.

getRetailPriceFromItem(item): number | undefined

Extracts the retail price value from a catalogue item.

getAssortmentFromItem(item): object | undefined

Extracts assortment information from a catalogue item.

calculateAvailableStock({ inStock, unreleased }): number

Calculates available stock by subtracting unreleased quantities.

calculateTargetStock({ min, max }): number

Calculates target stock level from min/max values.

Testing

The library uses Jest with Spectator for testing.

Running Tests

# Run tests for this library
npx nx test remission-data-access --skip-nx-cache

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

# Run tests in watch mode
npx nx test remission-data-access --watch

Test Coverage

The library includes comprehensive unit tests covering:

  • Service methods - All CRUD operations for returns, receipts, and items
  • State management - RemissionStore operations and computed signals
  • Helper functions - Calculation and extraction helpers
  • Error handling - API errors, validation failures, missing data
  • Abort signal support - Request cancellation scenarios
  • Caching behavior - @Cache and @InFlight decorator functionality
  • Batch operations - StockInfoResource batching logic

Architecture Notes

Current Architecture

The library follows a layered architecture with clear separation of concerns:

Components/Features
       ↓
  RemissionStore (optional, state management)
       ↓
  Services (business logic)
       ↓
├─→ Resources (batching optimization)
├─→ Generated API Clients (inventory-api)
├─→ Helper Functions (calculations, extractions)
└─→ Schemas (Zod validation)

Design Patterns

  1. Service Layer Pattern - API interactions wrapped in domain services
  2. Batching Resource Pattern - Efficient stock info fetching with automatic batching
  3. Caching Strategy - Multi-level caching (memory, decorators)
  4. State Management Pattern - NgRx Signals with session persistence
  5. Validation Pattern - Zod schemas for runtime validation

Known Considerations

  1. Service Location - BranchService should move to inventory-data-access
  2. Schema Validation TODO - Supplier cache needs validation
  3. Caching Strategy - Consider standardizing on decorator-based approach

Performance Optimizations

  • Batching reduces stock API calls by ~80%
  • 5-minute caching for frequently accessed metadata
  • @InFlight() prevents duplicate concurrent requests
  • Session persistence avoids unnecessary re-fetching

Dependencies

Required Libraries

  • @angular/core - Angular framework
  • @ngrx/signals - State management with signals
  • @generated/swagger/inventory-api - Generated API client
  • @isa/common/data-access - Common utilities
  • @isa/common/decorators - Caching decorators
  • @isa/core/logging - Logging service
  • @isa/core/storage - Storage providers
  • @isa/catalogue/data-access - Item models
  • zod - Schema validation
  • rxjs - Reactive programming

Path Alias

Import from: @isa/remission/data-access

License

Internal ISA Frontend library - not for external distribution.