feat(checkout): add branch selection to reward catalog - Add new select-branch-dropdown library with BranchDropdownComponent and SelectedBranchDropdownComponent for branch selection - Extend DropdownButtonComponent with filter and option subcomponents - Integrate branch selection into reward catalog page - Add BranchesResource for fetching available branches - Update CheckoutMetadataService with branch selection persistence - Add comprehensive tests for dropdown components Related work items: #5464
@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
- Quick Start
- Core Concepts
- API Reference
- Usage Examples
- State Management
- Helper Functions
- Testing
- Architecture Notes
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: FetchRemissionReturnReceiptsParamsstart?: Date- Optional start date filterreturncompleted: 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: FetchReturnParamsreturnId: number- ID of the return to fetcheagerLoading?: 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: CreateReturnreturnGroup?: 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: CreateReceiptreturnId: number- ID of the parent returnreceiptNumber?: 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: AssignPackagereturnId: number- ID of the returnreceiptId: number- ID of the receiptpackageNumber: 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: AddReturnItemreturnId: number- ID of the returnreceiptId: number- ID of the receiptreturnItemId: number- ID of the return item to addquantity: number- Quantity to addinStock: 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: AddReturnSuggestionItemreturnId: number- ID of the returnreceiptId: number- ID of the receiptreturnSuggestionId: number- ID of the return suggestion to addquantity: number- Quantity to addinStock: number- Current stock quantityimpedimentComment?: string- Optional impediment noteremainingQuantity?: 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: UpdateItemImpedimentitemId: number- ID of the return itemcomment: 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: UpdateItemImpedimentitemId: number- ID of the return suggestioncomment: 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 failsError- 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 branchabortSignal?: 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: FetchStockInStockitemIds: number[]- Array of item IDsstockId?: 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: RemissionQueryTokenInputassignedStockId: number- ID of the assigned stocksupplierId: number- ID of the supplierfilter?: string- Optional filter stringinput?: object- Optional input parametersorderBy?: string- Optional field to order bytake?: number- Number of items to fetchskip?: 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: FetchQuerySettingssupplierId: number- Supplier IDassignedStockId: 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: FetchRequiredCapacitydepartments: string[]- List of department namessupplierId: number- Supplier IDstockId: 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 foundError- 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 failsError- 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:
returnIdandreceiptIdcan only be set once viastartRemission() - 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
predefinedReturnQuantityis 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
- Service Layer Pattern - API interactions wrapped in domain services
- Batching Resource Pattern - Efficient stock info fetching with automatic batching
- Caching Strategy - Multi-level caching (memory, decorators)
- State Management Pattern - NgRx Signals with session persistence
- Validation Pattern - Zod schemas for runtime validation
Known Considerations
- Service Location - BranchService should move to inventory-data-access
- Schema Validation TODO - Supplier cache needs validation
- 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 modelszod- Schema validationrxjs- Reactive programming
Path Alias
Import from: @isa/remission/data-access
License
Internal ISA Frontend library - not for external distribution.