mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
feat(checkout): add reward order confirmation feature with schema migrations
- Add new reward-order-confirmation feature library with components and store - Implement checkout completion orchestrator service for order finalization - Migrate checkout/oms/crm models to Zod schemas for better type safety - Add order creation facade and display order schemas - Update shopping cart facade with order completion flow - Add comprehensive tests for shopping cart facade - Update routing to include order confirmation page
This commit is contained in:
@@ -1,7 +1,843 @@
|
||||
# oms-data-access
|
||||
# @isa/oms/data-access
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
A comprehensive Order Management System (OMS) data access library for Angular applications providing return processing, receipt management, order creation, and print capabilities.
|
||||
|
||||
## Running unit tests
|
||||
## Overview
|
||||
|
||||
Run `nx test oms-data-access` to execute the unit tests.
|
||||
The OMS Data Access library provides a complete suite of services, stores, and utilities for managing the entire return lifecycle in a retail environment. It integrates with the generated OMS API client and provides intelligent state management, question-based return workflows, validation, and receipt printing capabilities. The library follows domain-driven design principles with clear separation between business logic, state management, and API integration.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Features](#features)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Core Concepts](#core-concepts)
|
||||
- [API Reference](#api-reference)
|
||||
- [Usage Examples](#usage-examples)
|
||||
- [Return Process Workflow](#return-process-workflow)
|
||||
- [Product Categories and Questions](#product-categories-and-questions)
|
||||
- [State Management](#state-management)
|
||||
- [Error Handling](#error-handling)
|
||||
- [Testing](#testing)
|
||||
- [Architecture Notes](#architecture-notes)
|
||||
|
||||
## Features
|
||||
|
||||
- **Return Search** - Advanced receipt search with query builder and filter support
|
||||
- **Return Process Management** - Question-based return workflow with branching logic
|
||||
- **Product Category System** - Category-specific questions and validation rules
|
||||
- **Receipt Management** - Full CRUD operations for receipts and receipt items
|
||||
- **Order Creation** - Create orders from completed checkouts
|
||||
- **Print Integration** - Automated return receipt printing via label printers
|
||||
- **NgRx Signals Stores** - Reactive state management with persistence
|
||||
- **Session & IndexedDB Storage** - Automatic state persistence across browser sessions
|
||||
- **Orphaned Entity Cleanup** - Automatic cleanup when tabs are closed
|
||||
- **Zod Schema Validation** - Runtime validation for all data structures
|
||||
- **AbortSignal Support** - Request cancellation for all async operations
|
||||
- **Comprehensive Logging** - Integration with @isa/core/logging for debugging
|
||||
- **Question Branching Logic** - Dynamic question flow based on previous answers
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Import and Inject Services
|
||||
|
||||
```typescript
|
||||
import { Component, inject } from '@angular/core';
|
||||
import {
|
||||
ReturnSearchService,
|
||||
ReturnDetailsService,
|
||||
ReturnProcessService
|
||||
} from '@isa/oms/data-access';
|
||||
|
||||
@Component({
|
||||
selector: 'app-return-management',
|
||||
template: '...'
|
||||
})
|
||||
export class ReturnManagementComponent {
|
||||
#returnSearchService = inject(ReturnSearchService);
|
||||
#returnDetailsService = inject(ReturnDetailsService);
|
||||
#returnProcessService = inject(ReturnProcessService);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Search for Receipts
|
||||
|
||||
```typescript
|
||||
async searchReceipts(): Promise<void> {
|
||||
const results = await firstValueFrom(
|
||||
this.#returnSearchService.search({
|
||||
input: { qs: '12345' }, // Receipt number or search term
|
||||
filter: { receipt_type: '1;128;1024' },
|
||||
take: 20,
|
||||
skip: 0
|
||||
})
|
||||
);
|
||||
|
||||
console.log(`Found ${results.hits} receipts`);
|
||||
results.result.forEach(receipt => {
|
||||
console.log(`Receipt #${receipt.receiptNumber}: ${receipt.date}`);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Fetch Receipt Details
|
||||
|
||||
```typescript
|
||||
async getReceiptDetails(receiptId: number): Promise<void> {
|
||||
const receipt = await this.#returnDetailsService.fetchReturnDetails({
|
||||
receiptId: receiptId
|
||||
});
|
||||
|
||||
console.log(`Receipt: ${receipt.receiptNumber}`);
|
||||
console.log(`Buyer: ${receipt.buyer.firstName} ${receipt.buyer.lastName}`);
|
||||
console.log(`Items: ${receipt.items.length}`);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Process a Return
|
||||
|
||||
```typescript
|
||||
async processReturn(): Promise<void> {
|
||||
// Start a return process
|
||||
const returnProcess: ReturnProcess = {
|
||||
id: 1,
|
||||
processId: 100,
|
||||
receiptId: 12345,
|
||||
receiptItem: receiptItem,
|
||||
receiptDate: '2025-10-20',
|
||||
answers: {},
|
||||
productCategory: ProductCategory.ElektronischeGeraete,
|
||||
quantity: 1
|
||||
};
|
||||
|
||||
// Get active questions
|
||||
const questions = this.#returnProcessService.activeReturnProcessQuestions(returnProcess);
|
||||
|
||||
// Check eligibility
|
||||
const eligibility = this.#returnProcessService.eligibleForReturn(returnProcess);
|
||||
if (eligibility?.state === EligibleForReturnState.Eligible) {
|
||||
console.log('Item is eligible for return');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Return Process Lifecycle
|
||||
|
||||
The return process follows a structured workflow:
|
||||
|
||||
```
|
||||
1. Search & Selection
|
||||
└─> Search for receipts using ReturnSearchService
|
||||
└─> Select receipt items to return
|
||||
|
||||
2. Return Process Initialization
|
||||
└─> Create ReturnProcess entities via ReturnProcessStore
|
||||
└─> Assign product categories to items
|
||||
|
||||
3. Question Workflow
|
||||
└─> Display category-specific questions
|
||||
└─> Collect answers with validation
|
||||
└─> Follow branching logic based on answers
|
||||
|
||||
4. Eligibility Determination
|
||||
└─> Validate all questions answered
|
||||
└─> Check category-specific business rules
|
||||
└─> Determine if item is eligible for return
|
||||
|
||||
5. Completion
|
||||
└─> Generate return receipts via ReceiptService
|
||||
└─> Print return receipts via PrintReceiptsService
|
||||
└─> Update process with return receipt
|
||||
```
|
||||
|
||||
### Product Categories
|
||||
|
||||
The library supports seven product categories, each with specific return rules:
|
||||
|
||||
```typescript
|
||||
enum ProductCategory {
|
||||
BookCalendar = 'book-calendar', // Books and calendars
|
||||
TonDatentraeger = 'ton-datentraeger', // Audio and data media
|
||||
SpielwarenPuzzle = 'spielwaren-puzzle', // Toys and puzzles
|
||||
SonstigesNonbook = 'sonstiges-nonbook', // Other non-book items
|
||||
ElektronischeGeraete = 'elektronische-geraete', // Electronic devices
|
||||
Tolino = 'tolino', // Tolino e-readers
|
||||
Unknown = 'unknown' // Uncategorized
|
||||
}
|
||||
```
|
||||
|
||||
Each category has:
|
||||
- **Specific questions** - Defined in the CategoryQuestions registry
|
||||
- **Branching logic** - Questions can have nextQuestion properties
|
||||
- **Validation rules** - Category-specific eligibility checks
|
||||
- **Business rules** - Special handling (e.g., sealed packages, electronic devices)
|
||||
|
||||
### Question System Architecture
|
||||
|
||||
Questions follow a tree-like structure with branching:
|
||||
|
||||
```typescript
|
||||
interface ReturnProcessQuestion {
|
||||
key: string; // Unique question identifier
|
||||
type: ReturnProcessQuestionType; // Question type (Select, Checklist, Info, Group, Product)
|
||||
question: string; // Question text to display
|
||||
options?: Array<{ // Available answer options
|
||||
value: string;
|
||||
label: string;
|
||||
nextQuestion?: ReturnProcessQuestion; // Branching to next question
|
||||
}>;
|
||||
questions?: ReturnProcessQuestion[]; // Sub-questions for Group type
|
||||
}
|
||||
```
|
||||
|
||||
**Question Types:**
|
||||
- `Select` - Single choice from options
|
||||
- `Checklist` - Multiple choice with optional "other" text
|
||||
- `Info` - Informational display (auto-answered)
|
||||
- `Group` - Container for multiple related questions
|
||||
- `Product` - Product-specific question
|
||||
|
||||
### State Management with NgRx Signals
|
||||
|
||||
The library provides three signal stores with different persistence strategies:
|
||||
|
||||
#### 1. ReturnSearchStore (Session Storage)
|
||||
```typescript
|
||||
export const ReturnSearchStore = signalStore(
|
||||
{ providedIn: 'root' },
|
||||
withStorage('oms-data-access.return-search-store', SessionStorageProvider),
|
||||
withEntities<ReturnSearchEntity>(config),
|
||||
// ...
|
||||
);
|
||||
```
|
||||
|
||||
- **Purpose**: Manage search results and pagination
|
||||
- **Persistence**: SessionStorage (cleared when tab closes)
|
||||
- **Entity**: ReturnSearchEntity with status, items, hits
|
||||
- **Features**: Paginated search, status tracking (Idle, Pending, Success, Error)
|
||||
|
||||
#### 2. ReturnProcessStore (IndexedDB)
|
||||
```typescript
|
||||
export const ReturnProcessStore = signalStore(
|
||||
{ providedIn: 'root' },
|
||||
withStorage('return-process', IDBStorageProvider),
|
||||
withEntities<ReturnProcess>(),
|
||||
// ...
|
||||
);
|
||||
```
|
||||
|
||||
- **Purpose**: Manage active return processes
|
||||
- **Persistence**: IndexedDB (survives tab close, persistent across sessions)
|
||||
- **Entity**: ReturnProcess with answers, product category, receipt info
|
||||
- **Features**: Answer management, process completion tracking
|
||||
|
||||
#### 3. ReturnDetailsStore (No Persistence)
|
||||
```typescript
|
||||
export const ReturnDetailsStore = signalStore(
|
||||
withState(initialState),
|
||||
withEntities(receiptConfig),
|
||||
// ...
|
||||
);
|
||||
```
|
||||
|
||||
- **Purpose**: Manage receipt details and item selection
|
||||
- **Persistence**: None (in-memory only)
|
||||
- **Features**: Item selection, category/quantity management, canReturn checks
|
||||
|
||||
### Validation with Zod
|
||||
|
||||
All input parameters and API payloads are validated using Zod schemas:
|
||||
|
||||
```typescript
|
||||
import { QueryTokenSchema, ReturnReceiptValuesSchema } from '@isa/oms/data-access';
|
||||
|
||||
// Automatic validation in services
|
||||
const validatedToken = QueryTokenSchema.parse(queryToken);
|
||||
|
||||
// Safe parsing with error handling
|
||||
const result = ReturnReceiptValuesSchema.safeParse(payload);
|
||||
if (!result.success) {
|
||||
console.error('Validation failed:', result.error);
|
||||
}
|
||||
```
|
||||
|
||||
**Key Schemas:**
|
||||
- `QueryTokenSchema` - Search query validation
|
||||
- `FetchReturnDetailsSchema` - Receipt detail fetch params
|
||||
- `ReturnReceiptValuesSchema` - Return submission payload
|
||||
- `ReturnProcessChecklistAnswerSchema` - Checklist answer validation
|
||||
|
||||
## API Reference
|
||||
|
||||
### Services
|
||||
|
||||
#### ReturnSearchService
|
||||
|
||||
Service for searching receipts in the OMS system.
|
||||
|
||||
##### `querySettings(): Observable<QuerySettingsDTO>`
|
||||
|
||||
Fetches query settings for receipt search configuration.
|
||||
|
||||
**Returns:** Observable containing query settings (available filters, sort options)
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
this.#returnSearchService.querySettings().subscribe(settings => {
|
||||
console.log('Available filters:', settings.filters);
|
||||
});
|
||||
```
|
||||
|
||||
##### `search(queryToken: QueryTokenInput): Observable<ListResponseArgs<ReceiptListItem>>`
|
||||
|
||||
Executes a search for receipts based on query parameters.
|
||||
|
||||
**Parameters:**
|
||||
- `queryToken: QueryTokenInput` - Search parameters (validated with Zod)
|
||||
- `input.qs` - Search query string
|
||||
- `filter` - Filter criteria (e.g., receipt_type)
|
||||
- `take` - Number of results to return
|
||||
- `skip` - Number of results to skip (pagination)
|
||||
|
||||
**Returns:** Observable containing list of receipt items with metadata
|
||||
|
||||
**Throws:**
|
||||
- `ReturnParseQueryTokenError` - If query token validation fails
|
||||
- `ReturnSearchSearchError` - If search fails due to API error
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
this.#returnSearchService.search({
|
||||
input: { qs: 'smith@example.com' },
|
||||
filter: { receipt_type: '1;128;1024' },
|
||||
take: 20,
|
||||
skip: 0
|
||||
}).subscribe({
|
||||
next: (results) => console.log(`Found ${results.hits} receipts`),
|
||||
error: (error) => console.error('Search failed:', error)
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### ReturnDetailsService
|
||||
|
||||
Service for managing receipt details and return eligibility.
|
||||
|
||||
##### `fetchReturnDetails(params: FetchReturnDetails, abortSignal?: AbortSignal): Promise<Receipt>`
|
||||
|
||||
Fetches detailed receipt information including items, buyer, and shipping.
|
||||
|
||||
**Parameters:**
|
||||
- `params.receiptId: number` - Receipt ID to fetch (validated with Zod)
|
||||
- `abortSignal?: AbortSignal` - Optional signal to cancel request
|
||||
|
||||
**Returns:** Promise resolving to complete Receipt with items
|
||||
|
||||
**Throws:** Error if receipt not found or API error
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
const receipt = await this.#returnDetailsService.fetchReturnDetails(
|
||||
{ receiptId: 12345 },
|
||||
abortController.signal
|
||||
);
|
||||
|
||||
console.log(`Receipt ${receipt.receiptNumber} has ${receipt.items.length} items`);
|
||||
```
|
||||
|
||||
##### `fetchReceiptsByEmail(params: { email: string }, abortSignal?: AbortSignal): Promise<ReceiptListItem[]>`
|
||||
|
||||
Fetches all receipts associated with an email address.
|
||||
|
||||
**Parameters:**
|
||||
- `params.email: string` - Email address to search for
|
||||
- `abortSignal?: AbortSignal` - Optional signal to cancel request
|
||||
|
||||
**Returns:** Promise resolving to array of receipt list items
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
const receipts = await this.#returnDetailsService.fetchReceiptsByEmail({
|
||||
email: 'customer@example.com'
|
||||
});
|
||||
|
||||
receipts.forEach(r => console.log(`Receipt: ${r.receiptNumber}`));
|
||||
```
|
||||
|
||||
##### `canReturn(params: { receiptItemId: number, quantity: number, category: ProductCategory }, abortSignal?: AbortSignal): Promise<CanReturn>`
|
||||
|
||||
Checks if a specific receipt item can be returned.
|
||||
|
||||
**Parameters:**
|
||||
- `params.receiptItemId: number` - ID of receipt item to check
|
||||
- `params.quantity: number` - Quantity to return
|
||||
- `params.category: ProductCategory` - Product category
|
||||
- `abortSignal?: AbortSignal` - Optional signal to cancel request
|
||||
|
||||
**Returns:** Promise resolving to CanReturn result with eligibility info
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
const canReturn = await this.#returnDetailsService.canReturn({
|
||||
receiptItemId: 456,
|
||||
quantity: 1,
|
||||
category: ProductCategory.ElektronischeGeraete
|
||||
});
|
||||
|
||||
if (canReturn.result) {
|
||||
console.log('Item can be returned');
|
||||
} else {
|
||||
console.log('Return not allowed:', canReturn.message);
|
||||
}
|
||||
```
|
||||
|
||||
##### `availableCategories(): KeyValue<ProductCategory, string>[]`
|
||||
|
||||
Gets all available product categories (excludes Unknown).
|
||||
|
||||
**Returns:** Array of key-value pairs representing categories
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
const categories = this.#returnDetailsService.availableCategories();
|
||||
categories.forEach(cat => {
|
||||
console.log(`${cat.key}: ${cat.value}`);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### ReturnProcessService
|
||||
|
||||
Service for managing the return process workflow including questions and eligibility.
|
||||
|
||||
##### `activeReturnProcessQuestions(process: ReturnProcess): ReturnProcessQuestion[] | undefined`
|
||||
|
||||
Gets active questions based on answers provided so far.
|
||||
|
||||
**Parameters:**
|
||||
- `process: ReturnProcess` - The return process containing answers
|
||||
|
||||
**Returns:** Array of active questions, or undefined if no questions apply
|
||||
|
||||
**Throws:**
|
||||
- `PropertyNullOrUndefinedError` - If questions cannot be found
|
||||
- `Error` - If cyclic question dependencies detected
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
const questions = this.#returnProcessService.activeReturnProcessQuestions(process);
|
||||
|
||||
questions?.forEach(q => {
|
||||
console.log(`Question: ${q.question}`);
|
||||
if (q.type === ReturnProcessQuestionType.Select) {
|
||||
q.options?.forEach(opt => console.log(` - ${opt.label}`));
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
##### `returnProcessQuestionsProgress(returnProcess: ReturnProcess): { answered: number; total: number } | undefined`
|
||||
|
||||
Calculates question progress for the return process.
|
||||
|
||||
**Parameters:**
|
||||
- `returnProcess: ReturnProcess` - The return process to analyze
|
||||
|
||||
**Returns:** Object with answered and total question counts, or undefined
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
const progress = this.#returnProcessService.returnProcessQuestionsProgress(process);
|
||||
if (progress) {
|
||||
console.log(`Progress: ${progress.answered}/${progress.total}`);
|
||||
const percentage = (progress.answered / progress.total) * 100;
|
||||
console.log(`${percentage}% complete`);
|
||||
}
|
||||
```
|
||||
|
||||
##### `eligibleForReturn(returnProcess: ReturnProcess): EligibleForReturn | undefined`
|
||||
|
||||
Determines if the product is eligible for return based on answers.
|
||||
|
||||
**Parameters:**
|
||||
- `returnProcess: ReturnProcess` - The return process with complete answers
|
||||
|
||||
**Returns:** Eligibility status, or undefined if questions incomplete
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
const eligibility = this.#returnProcessService.eligibleForReturn(process);
|
||||
|
||||
switch (eligibility?.state) {
|
||||
case EligibleForReturnState.Eligible:
|
||||
console.log('Item is eligible for return');
|
||||
break;
|
||||
case EligibleForReturnState.NotEligible:
|
||||
console.log('Item is not eligible:', eligibility.reason);
|
||||
break;
|
||||
case EligibleForReturnState.Unknown:
|
||||
console.log('Cannot determine eligibility');
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
##### `getReturnInfo(process: ReturnProcess): ReturnInfo | undefined`
|
||||
|
||||
Retrieves consolidated return information from answers.
|
||||
|
||||
**Parameters:**
|
||||
- `process: ReturnProcess` - The return process
|
||||
|
||||
**Returns:** Consolidated return information or undefined
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
const info = this.#returnProcessService.getReturnInfo(process);
|
||||
if (info) {
|
||||
console.log('Return reason:', info.reason);
|
||||
console.log('Additional notes:', info.notes);
|
||||
}
|
||||
```
|
||||
|
||||
##### `completeReturnProcess(processes: ReturnProcess[]): Promise<Receipt[]>`
|
||||
|
||||
Completes the return process and generates return receipts.
|
||||
|
||||
**Parameters:**
|
||||
- `processes: ReturnProcess[]` - Array of return processes to complete
|
||||
|
||||
**Returns:** Promise resolving to generated return receipts
|
||||
|
||||
**Throws:**
|
||||
- `PropertyNullOrUndefinedError` - If processes is null/undefined
|
||||
- `PropertyIsEmptyError` - If processes array is empty
|
||||
- `ReturnProcessIsNotCompleteError` - If any process has unanswered questions
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
try {
|
||||
const receipts = await this.#returnProcessService.completeReturnProcess([
|
||||
process1,
|
||||
process2
|
||||
]);
|
||||
|
||||
console.log(`Generated ${receipts.length} return receipts`);
|
||||
receipts.forEach(r => console.log(`Return receipt: ${r.receiptNumber}`));
|
||||
} catch (error) {
|
||||
if (error instanceof ReturnProcessIsNotCompleteError) {
|
||||
console.error('Please answer all questions before completing');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### OrderCreationService
|
||||
|
||||
Service for creating orders from checkout.
|
||||
|
||||
##### `createOrdersFromCheckout(checkoutId: number): Promise<Order[]>`
|
||||
|
||||
Creates orders from a completed checkout.
|
||||
|
||||
**Parameters:**
|
||||
- `checkoutId: number` - The checkout ID to create orders from
|
||||
|
||||
**Returns:** Promise resolving to array of created orders
|
||||
|
||||
**Throws:**
|
||||
- `Error` - If checkoutId is invalid
|
||||
- `ResponseArgsError` - If order creation fails
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
const orders = await this.#orderCreationService.createOrdersFromCheckout(12345);
|
||||
orders.forEach(order => {
|
||||
console.log(`Order ${order.orderNumber} created`);
|
||||
});
|
||||
```
|
||||
|
||||
##### `getLogistician(logisticianNumber = '2470', abortSignal?: AbortSignal): Promise<LogisticianDTO>`
|
||||
|
||||
Retrieves logistician information.
|
||||
|
||||
**Parameters:**
|
||||
- `logisticianNumber: string` - Logistician number (defaults to '2470')
|
||||
- `abortSignal?: AbortSignal` - Optional signal to cancel request
|
||||
|
||||
**Returns:** Promise resolving to logistician data
|
||||
|
||||
**Throws:**
|
||||
- `ResponseArgsError` - If request fails
|
||||
- `Error` - If logistician not found
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
const logistician = await this.#orderCreationService.getLogistician('2470');
|
||||
console.log(`Logistician: ${logistician.name}`);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### PrintReceiptsService
|
||||
|
||||
Service for printing return receipts.
|
||||
|
||||
##### `printReturnReceipts({ returnReceiptIds }: { returnReceiptIds: number[] }): Promise<void>`
|
||||
|
||||
Prints return receipts to a label printer.
|
||||
|
||||
**Parameters:**
|
||||
- `returnReceiptIds: number[]` - Array of return receipt IDs to print
|
||||
|
||||
**Returns:** Promise that resolves when printing is complete
|
||||
|
||||
**Throws:** Error if no receipt IDs provided
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
await this.#printReceiptsService.printReturnReceipts({
|
||||
returnReceiptIds: [123, 124, 125]
|
||||
});
|
||||
console.log('Receipts sent to printer');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Facades
|
||||
|
||||
#### OrderCreationFacade
|
||||
|
||||
Simplified interface for order creation operations. Delegates to OrderCreationService.
|
||||
|
||||
**Methods:**
|
||||
- `createOrdersFromCheckout(checkoutId: number): Promise<Order[]>`
|
||||
- `getLogistician(logisticianNumber = '2470', abortSignal?: AbortSignal): Promise<LogisticianDTO>`
|
||||
|
||||
**Note:** This is a pass-through facade. Consider injecting OrderCreationService directly unless future orchestration logic is planned.
|
||||
|
||||
---
|
||||
|
||||
### Stores
|
||||
|
||||
For detailed store usage and examples, see the [Usage Examples](#usage-examples) section.
|
||||
|
||||
#### ReturnSearchStore
|
||||
|
||||
Signal store for managing receipt search state with session persistence.
|
||||
|
||||
**Key Methods:**
|
||||
- `getEntity(processId)` - Get search entity by process ID
|
||||
- `search(params)` - Execute search with rxMethod
|
||||
- `removeAllEntitiesByProcessId(processId)` - Clean up search results
|
||||
|
||||
#### ReturnProcessStore
|
||||
|
||||
Signal store for managing return process entities with IndexedDB persistence.
|
||||
|
||||
**Key Methods:**
|
||||
- `startProcess(params)` - Initialize return process
|
||||
- `setAnswer(id, question, answer)` - Set question answer
|
||||
- `removeAnswer(id, question)` - Remove question answer
|
||||
- `finishProcess(returnReceipts)` - Mark process complete
|
||||
- `removeAllEntitiesByProcessId(...processIds)` - Clean up processes
|
||||
|
||||
#### ReturnDetailsStore
|
||||
|
||||
Signal store for managing receipt details and item selection (in-memory).
|
||||
|
||||
**Key Methods:**
|
||||
- `receiptResource(receiptId)` - Load receipt
|
||||
- `canReturnResource(receiptItem)` - Check return eligibility
|
||||
- `addSelectedItems(itemIds)` - Add items to selection
|
||||
- `setProductCategory(itemId, category)` - Set item category
|
||||
- `setQuantity(itemId, quantity)` - Set return quantity
|
||||
|
||||
---
|
||||
|
||||
### Helper Functions
|
||||
|
||||
#### Return Process Helpers
|
||||
|
||||
Key helper functions exported from `@isa/oms/data-access`:
|
||||
|
||||
- `canReturnReceiptItem(item)` - Check if item is returnable
|
||||
- `getReceiptItemQuantity(item)` - Get item quantity
|
||||
- `getReceiptItemReturnedQuantity(item)` - Get returned quantity
|
||||
- `getReceiptItemProductCategory(item)` - Get item category
|
||||
- `receiptItemHasCategory(item, category)` - Check category match
|
||||
- `activeReturnProcessQuestions(questions, answers)` - Get active questions
|
||||
- `allReturnProcessQuestionsAnswered(params)` - Check completion
|
||||
- `calculateLongestQuestionDepth(questions, answers)` - Calculate question tree depth
|
||||
|
||||
---
|
||||
|
||||
### RxJS Operators
|
||||
|
||||
#### `takeUntilAborted(signal: AbortSignal)`
|
||||
|
||||
RxJS operator that completes the source Observable when an AbortSignal is aborted.
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
import { takeUntilAborted } from '@isa/oms/data-access';
|
||||
|
||||
const abortController = new AbortController();
|
||||
|
||||
observable$.pipe(
|
||||
takeUntilAborted(abortController.signal)
|
||||
).subscribe({
|
||||
next: (data) => console.log('Data:', data),
|
||||
error: (err) => console.error('Aborted or failed')
|
||||
});
|
||||
|
||||
// Cancel request
|
||||
abortController.abort();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Error Classes
|
||||
|
||||
- `OmsError` - Base error class
|
||||
- `ReturnSearchSearchError` - Search failure
|
||||
- `ReturnParseQueryTokenError` - Query validation failure
|
||||
- `CreateReturnProcessError` - Process creation failure
|
||||
- `ReturnProcessIsNotCompleteError` - Incomplete process submission
|
||||
|
||||
For detailed error handling patterns, see the [Error Handling](#error-handling) section.
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Complete Return Workflow
|
||||
|
||||
See the comprehensive workflow example in the full documentation above for a step-by-step guide covering:
|
||||
1. Searching for receipts
|
||||
2. Loading receipt details
|
||||
3. Selecting items for return
|
||||
4. Starting the return process
|
||||
5. Answering questions
|
||||
6. Checking eligibility
|
||||
7. Completing the return
|
||||
|
||||
### Search with Pagination
|
||||
|
||||
Example showing how to implement paginated receipt search with the ReturnSearchStore.
|
||||
|
||||
### Dynamic Question Display
|
||||
|
||||
Example component demonstrating how to render questions dynamically based on type and handle answers.
|
||||
|
||||
## Return Process Workflow
|
||||
|
||||
The return process follows this workflow:
|
||||
|
||||
```
|
||||
Search Receipt → Load Details → Select Items → Start Process
|
||||
→ Answer Questions → Check Eligibility → Complete Return → Print Receipt
|
||||
```
|
||||
|
||||
### Question Branching
|
||||
|
||||
Questions support branching logic through the `nextQuestion` property, allowing different question flows based on previous answers.
|
||||
|
||||
## Product Categories and Questions
|
||||
|
||||
### Supported Categories
|
||||
|
||||
- **BookCalendar** - Books and calendars (2 questions)
|
||||
- **TonDatentraeger** - Audio/data media (3-5 questions)
|
||||
- **SpielwarenPuzzle** - Toys and puzzles (2 questions)
|
||||
- **SonstigesNonbook** - Other non-book items (2 questions)
|
||||
- **ElektronischeGeraete** - Electronic devices (4-8 questions, complex branching)
|
||||
- **Tolino** - E-readers (6-12 questions, most complex)
|
||||
- **Unknown** - Uncategorized (0 questions, manual processing)
|
||||
|
||||
Each category has specific questions and validation rules defined in the `CategoryQuestions` registry.
|
||||
|
||||
## State Management
|
||||
|
||||
### Persistence Strategies
|
||||
|
||||
- **ReturnSearchStore**: SessionStorage (tab-scoped, cleared on close)
|
||||
- **ReturnProcessStore**: IndexedDB (persistent across sessions)
|
||||
- **ReturnDetailsStore**: In-memory (no persistence)
|
||||
|
||||
### Orphaned Entity Cleanup
|
||||
|
||||
All stores automatically clean up entities when tabs are closed using Angular effects and the `@isa/core/tabs` service.
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. Use Zod `safeParse()` for input validation
|
||||
2. Handle service errors with try/catch
|
||||
3. Use Observable error callbacks for streams
|
||||
4. Check store entity status for error states
|
||||
5. Integrate with `@isa/core/logging` for debugging
|
||||
|
||||
### Common Error Types
|
||||
|
||||
- **Validation Errors**: ZodError from schema validation
|
||||
- **API Errors**: ResponseArgsError from API calls
|
||||
- **Business Logic Errors**: Custom OMS error classes
|
||||
- **Process Errors**: ReturnProcessIsNotCompleteError for incomplete submissions
|
||||
|
||||
## Testing
|
||||
|
||||
The library uses **Jest** with **Spectator** for testing.
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run tests
|
||||
npx nx test oms-data-access --skip-nx-cache
|
||||
|
||||
# Run with coverage
|
||||
npx nx test oms-data-access --code-coverage --skip-nx-cache
|
||||
|
||||
# Run in watch mode
|
||||
npx nx test oms-data-access --watch
|
||||
```
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
### Layer Architecture
|
||||
|
||||
```
|
||||
Features → Facades → Services → Stores → Helpers → Generated APIs
|
||||
```
|
||||
|
||||
### Design Patterns
|
||||
|
||||
- **Service Layer Pattern** - Business logic encapsulation
|
||||
- **State Management Pattern** - NgRx Signals with persistence
|
||||
- **Question Registry Pattern** - Category-specific question lookup
|
||||
- **Helper Function Pattern** - Pure transformation functions
|
||||
- **Adapter Pattern** - API DTO to domain model mapping
|
||||
|
||||
### Dependencies
|
||||
|
||||
- `@angular/core` - Angular framework
|
||||
- `@ngrx/signals` - State management
|
||||
- `@generated/swagger/oms-api` - OMS API client
|
||||
- `@isa/core/logging` - Logging infrastructure
|
||||
- `@isa/core/tabs` - Tab management
|
||||
- `@isa/core/storage` - Storage providers
|
||||
- `zod` - Schema validation
|
||||
- `rxjs` - Reactive programming
|
||||
|
||||
### Path Alias
|
||||
|
||||
Import from: `@isa/oms/data-access`
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
Internal ISA Frontend library - not for external distribution.
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*
|
||||
* Core components:
|
||||
* - Models: Type definitions for data structures
|
||||
* - Facades: Simplified interfaces for order creation operations
|
||||
* - Services: API integrations for data retrieval and manipulation
|
||||
* - Stores: State management for OMS-related data
|
||||
* - Schemas: Validation schemas for ensuring data integrity
|
||||
@@ -17,6 +18,7 @@
|
||||
export * from './lib/errors';
|
||||
export * from './lib/questions';
|
||||
export * from './lib/models';
|
||||
export * from './lib/facades';
|
||||
export * from './lib/helpers/return-process';
|
||||
export * from './lib/schemas';
|
||||
export * from './lib/services';
|
||||
|
||||
1
libs/oms/data-access/src/lib/constants.ts
Normal file
1
libs/oms/data-access/src/lib/constants.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const OMS_DISPLAY_ORDERS_KEY = 'OMS-DATA-ACCESS.DISPLAY_ORDERS';
|
||||
1
libs/oms/data-access/src/lib/facades/index.ts
Normal file
1
libs/oms/data-access/src/lib/facades/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { OrderCreationFacade } from './order-creation.facade';
|
||||
@@ -0,0 +1,145 @@
|
||||
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
|
||||
import { OrderCreationFacade } from './order-creation.facade';
|
||||
import { OrderCreationService } from '../services';
|
||||
import { LogisticianDTO } from '@generated/swagger/oms-api';
|
||||
import { Order } from '../models';
|
||||
|
||||
describe('OrderCreationFacade', () => {
|
||||
let spectator: SpectatorService<OrderCreationFacade>;
|
||||
const createService = createServiceFactory({
|
||||
service: OrderCreationFacade,
|
||||
mocks: [OrderCreationService],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createService();
|
||||
});
|
||||
|
||||
describe('createOrdersFromCheckout', () => {
|
||||
it('should delegate to OrderCreationService.createOrdersFromCheckout', async () => {
|
||||
// Arrange
|
||||
const checkoutId = 123;
|
||||
const mockOrders: Order[] = [
|
||||
{ id: 1 } as Order,
|
||||
{ id: 2 } as Order,
|
||||
];
|
||||
const orderCreationService = spectator.inject(OrderCreationService);
|
||||
(orderCreationService.createOrdersFromCheckout as jest.Mock).mockResolvedValue(mockOrders);
|
||||
|
||||
// Act
|
||||
const result = await spectator.service.createOrdersFromCheckout(checkoutId);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(mockOrders);
|
||||
expect(orderCreationService.createOrdersFromCheckout).toHaveBeenCalledWith(
|
||||
checkoutId,
|
||||
);
|
||||
expect(orderCreationService.createOrdersFromCheckout).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should propagate errors from OrderCreationService', async () => {
|
||||
// Arrange
|
||||
const checkoutId = 0;
|
||||
const errorMessage = 'Invalid checkoutId: 0';
|
||||
const orderCreationService = spectator.inject(OrderCreationService);
|
||||
(orderCreationService.createOrdersFromCheckout as jest.Mock).mockRejectedValue(
|
||||
new Error(errorMessage)
|
||||
);
|
||||
|
||||
// Act & Assert
|
||||
await expect(
|
||||
spectator.service.createOrdersFromCheckout(checkoutId),
|
||||
).rejects.toThrow(errorMessage);
|
||||
expect(orderCreationService.createOrdersFromCheckout).toHaveBeenCalledWith(
|
||||
checkoutId,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLogistician', () => {
|
||||
it('should delegate to OrderCreationService.getLogistician with default logisticianNumber', async () => {
|
||||
// Arrange
|
||||
const mockLogistician: LogisticianDTO = {
|
||||
logisticianNumber: '2470',
|
||||
name: 'Default Logistician',
|
||||
} as LogisticianDTO;
|
||||
const orderCreationService = spectator.inject(OrderCreationService);
|
||||
(orderCreationService.getLogistician as jest.Mock).mockResolvedValue(mockLogistician);
|
||||
|
||||
// Act
|
||||
const result = await spectator.service.getLogistician();
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(mockLogistician);
|
||||
expect(orderCreationService.getLogistician).toHaveBeenCalledWith(
|
||||
'2470',
|
||||
undefined,
|
||||
);
|
||||
expect(orderCreationService.getLogistician).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should delegate to OrderCreationService.getLogistician with custom logisticianNumber', async () => {
|
||||
// Arrange
|
||||
const logisticianNumber = '1000';
|
||||
const mockLogistician: LogisticianDTO = {
|
||||
logisticianNumber,
|
||||
name: 'Custom Logistician',
|
||||
} as LogisticianDTO;
|
||||
const orderCreationService = spectator.inject(OrderCreationService);
|
||||
(orderCreationService.getLogistician as jest.Mock).mockResolvedValue(mockLogistician);
|
||||
|
||||
// Act
|
||||
const result = await spectator.service.getLogistician(logisticianNumber);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(mockLogistician);
|
||||
expect(orderCreationService.getLogistician).toHaveBeenCalledWith(
|
||||
logisticianNumber,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('should delegate to OrderCreationService.getLogistician with abortSignal', async () => {
|
||||
// Arrange
|
||||
const logisticianNumber = '2470';
|
||||
const abortSignal = new AbortController().signal;
|
||||
const mockLogistician: LogisticianDTO = {
|
||||
logisticianNumber,
|
||||
name: 'Default Logistician',
|
||||
} as LogisticianDTO;
|
||||
const orderCreationService = spectator.inject(OrderCreationService);
|
||||
(orderCreationService.getLogistician as jest.Mock).mockResolvedValue(mockLogistician);
|
||||
|
||||
// Act
|
||||
const result = await spectator.service.getLogistician(
|
||||
logisticianNumber,
|
||||
abortSignal,
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(mockLogistician);
|
||||
expect(orderCreationService.getLogistician).toHaveBeenCalledWith(
|
||||
logisticianNumber,
|
||||
abortSignal,
|
||||
);
|
||||
});
|
||||
|
||||
it('should propagate errors from OrderCreationService', async () => {
|
||||
// Arrange
|
||||
const errorMessage = 'Logistician 2470 not found';
|
||||
const orderCreationService = spectator.inject(OrderCreationService);
|
||||
(orderCreationService.getLogistician as jest.Mock).mockRejectedValue(
|
||||
new Error(errorMessage)
|
||||
);
|
||||
|
||||
// Act & Assert
|
||||
await expect(spectator.service.getLogistician()).rejects.toThrow(
|
||||
errorMessage,
|
||||
);
|
||||
expect(orderCreationService.getLogistician).toHaveBeenCalledWith(
|
||||
'2470',
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { LogisticianDTO } from '@generated/swagger/oms-api';
|
||||
import { Order } from '../models';
|
||||
import { OrderCreationService } from '../services';
|
||||
|
||||
/**
|
||||
* Facade for order creation operations.
|
||||
*
|
||||
* @remarks
|
||||
* This facade provides a simplified interface for creating orders from checkout
|
||||
* and retrieving logistician information. It delegates to OrderCreationService
|
||||
* for all operations.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class OrderCreationFacade {
|
||||
#orderCreationService = inject(OrderCreationService);
|
||||
|
||||
/**
|
||||
* Creates orders from a checkout.
|
||||
*
|
||||
* @param checkoutId - The ID of the checkout to create orders from
|
||||
* @returns Promise resolving to array of created orders
|
||||
* @throws {Error} If checkoutId is invalid or order creation fails
|
||||
*/
|
||||
async createOrdersFromCheckout(checkoutId: number): Promise<Order[]> {
|
||||
return this.#orderCreationService.createOrdersFromCheckout(checkoutId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves logistician information.
|
||||
*
|
||||
* @param logisticianNumber - The logistician number to retrieve (defaults to '2470')
|
||||
* @param abortSignal - Optional signal to abort the operation
|
||||
* @returns Promise resolving to logistician data
|
||||
* @throws {Error} If logistician is not found or request fails
|
||||
*/
|
||||
async getLogistician(
|
||||
logisticianNumber = '2470',
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<LogisticianDTO> {
|
||||
return this.#orderCreationService.getLogistician(
|
||||
logisticianNumber,
|
||||
abortSignal,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { canReturnReceiptItem } from './can-return-receipt-item.helper';
|
||||
import { ReceiptItem } from '../../models/receipt-item';
|
||||
import { Product } from '../../models/product';
|
||||
import { Product } from '../../schemas/product.schema';
|
||||
import { Quantity } from '../../models/quantity';
|
||||
|
||||
describe('canReturnReceiptItem', () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getReceiptItemAction } from './get-receipt-item-action.helper';
|
||||
import { ReceiptItem } from '../../models/receipt-item';
|
||||
import { Product } from '../../models/product';
|
||||
import { Product } from '../../schemas/product.schema';
|
||||
import { Quantity } from '../../models/quantity';
|
||||
|
||||
describe('getReceiptItemAction', () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getReceiptItemProductCategory } from './get-receipt-item-product-category.helper';
|
||||
import { ReceiptItem } from '../../models/receipt-item';
|
||||
import { Product } from '../../models/product';
|
||||
import { Product } from '../../schemas/product.schema';
|
||||
import { Quantity } from '../../models/quantity';
|
||||
import { ProductCategory } from '../../questions/constants';
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { isEmpty } from 'lodash';
|
||||
import {
|
||||
Product,
|
||||
ReturnInfo,
|
||||
ReturnProcessAnswers,
|
||||
ReturnProcessQuestion,
|
||||
ReturnProcessQuestionKey,
|
||||
ReturnProcessQuestionType,
|
||||
} from '../../models';
|
||||
import { Product } from '../../schemas/product.schema';
|
||||
import {
|
||||
activeReturnProcessQuestions,
|
||||
internalActiveReturnProcessQuestions,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { receiptItemHasCategory } from './receipt-item-has-category.helper';
|
||||
import { ReceiptItem } from '../../models/receipt-item';
|
||||
import { Product } from '../../models/product';
|
||||
import { Product } from '../../schemas/product.schema';
|
||||
import { Quantity } from '../../models/quantity';
|
||||
import { ProductCategory } from '../../questions/constants';
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ export * from './eligible-for-return';
|
||||
export * from './gender';
|
||||
export * from './logistician';
|
||||
export * from './order';
|
||||
export * from './product';
|
||||
export * from './quantity';
|
||||
export * from './receipt-item-list-item';
|
||||
export * from './receipt-item-task-list-item';
|
||||
@@ -21,5 +20,4 @@ export * from './return-process-question';
|
||||
export * from './return-process-status';
|
||||
export * from './return-process';
|
||||
export * from './shipping-address-2';
|
||||
export * from './shipping-type';
|
||||
export * from './task-action-type';
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { ProductDTO } from '@generated/swagger/oms-api';
|
||||
|
||||
export interface Product extends ProductDTO {
|
||||
name: string;
|
||||
contributors: string;
|
||||
catalogProductNumber: string;
|
||||
ean: string;
|
||||
format: string;
|
||||
formatDetail: string;
|
||||
volume: string;
|
||||
manufacturer: string;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ReceiptItemListItemDTO } from '@generated/swagger/oms-api';
|
||||
import { Product } from './product';
|
||||
import { Product } from '../schemas/product.schema';
|
||||
import { Quantity } from './quantity';
|
||||
|
||||
export interface ReceiptItemListItem extends ReceiptItemListItemDTO {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ReceiptItemDTO } from '@generated/swagger/oms-api';
|
||||
import { Product } from './product';
|
||||
import { Product } from '../schemas/product.schema';
|
||||
import { Quantity } from './quantity';
|
||||
|
||||
export interface ReceiptItem extends ReceiptItemDTO {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Product } from './product';
|
||||
import { Product } from '../schemas/product.schema';
|
||||
import { ReturnProcessQuestionKey } from './return-process-question-key';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
export enum ShippingType {
|
||||
NotSet = 0,
|
||||
PostCard = 1,
|
||||
Letter = 2,
|
||||
LargeLetter = 4,
|
||||
BookRate = 8,
|
||||
MerchandiseShipment = 16,
|
||||
Parcel = 32,
|
||||
Palette = 64,
|
||||
MerchandiseShipmentSmall = 128,
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import {
|
||||
AddressSchema,
|
||||
CommunicationDetailsSchema,
|
||||
ExternalReferenceSchema,
|
||||
GenderSchema,
|
||||
OrganisationSchema,
|
||||
} from '@isa/common/data-access';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const DisplayAddresseeSchema = z.object({
|
||||
number: z.string().describe('Number').optional(),
|
||||
locale: z.string().describe('Locale').optional(),
|
||||
gender: GenderSchema.describe('Gender'),
|
||||
title: z.string().describe('Title').optional(),
|
||||
firstName: z.string().describe('First name').optional(),
|
||||
lastName: z.string().describe('Last name').optional(),
|
||||
organisation: OrganisationSchema.describe('Organisation information').optional(),
|
||||
address: AddressSchema.describe('Address').optional(),
|
||||
communicationDetails: CommunicationDetailsSchema.describe('Communication details').optional(),
|
||||
externalReference: ExternalReferenceSchema.describe('External system reference').optional(),
|
||||
});
|
||||
|
||||
export type DisplayAddressee = z.infer<typeof DisplayAddresseeSchema>;
|
||||
@@ -0,0 +1,19 @@
|
||||
import {
|
||||
AddressSchema,
|
||||
CommunicationDetailsSchema,
|
||||
EntitySchema,
|
||||
} from '@isa/common/data-access';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const DisplayBranchSchema = z
|
||||
.object({
|
||||
name: z.string().describe('Name').optional(),
|
||||
key: z.string().describe('Key').optional(),
|
||||
branchNumber: z.string().describe('Branch number').optional(),
|
||||
address: AddressSchema.describe('Address').optional(),
|
||||
communicationDetails: CommunicationDetailsSchema.describe('Communication details').optional(),
|
||||
web: z.string().describe('Web').optional(),
|
||||
})
|
||||
.extend(EntitySchema.shape);
|
||||
|
||||
export type DisplayBranch = z.infer<typeof DisplayBranchSchema>;
|
||||
@@ -0,0 +1,12 @@
|
||||
import { EntitySchema } from '@isa/common/data-access';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const DisplayLogisticianSchema = z
|
||||
.object({
|
||||
name: z.string().describe('Name').optional(),
|
||||
logisticianNumber: z.string().describe('Logistician number').optional(),
|
||||
gln: z.string().describe('Gln').optional(),
|
||||
})
|
||||
.extend(EntitySchema.shape);
|
||||
|
||||
export type DisplayLogistician = z.infer<typeof DisplayLogisticianSchema>;
|
||||
@@ -0,0 +1,26 @@
|
||||
import { EntitySchema, QuantityUnitTypeSchema } from '@isa/common/data-access';
|
||||
import { z } from 'zod';
|
||||
import { PriceSchema } from './price.schema';
|
||||
import { ProductSchema } from './product.schema';
|
||||
import { PromotionSchema } from './promotion.schema';
|
||||
|
||||
// Forward declaration for circular reference
|
||||
export const DisplayOrderItemSchema: z.ZodType<any> = z
|
||||
.object({
|
||||
buyerComment: z.string().describe('Buyer comment').optional(),
|
||||
description: z.string().describe('Description text').optional(),
|
||||
features: z.record(z.string().describe('Features'), z.string()).optional(),
|
||||
order: z.lazy(() => z.any()).describe('Order').optional(), // Circular reference to DisplayOrder
|
||||
orderDate: z.string().describe('Order date').optional(),
|
||||
orderItemNumber: z.string().describe('OrderItem number').optional(),
|
||||
price: PriceSchema.describe('Price information').optional(),
|
||||
product: ProductSchema.describe('Product').optional(),
|
||||
promotion: PromotionSchema.describe('Promotion information').optional(),
|
||||
quantity: z.number().describe('Quantity').optional(),
|
||||
quantityUnit: z.string().describe('Quantity unit').optional(),
|
||||
quantityUnitType: QuantityUnitTypeSchema.describe('QuantityUnit type'),
|
||||
subsetItems: z.array(z.lazy(() => z.any())).describe('Subset items').optional(), // Circular reference to DisplayOrderItemSubset
|
||||
})
|
||||
.extend(EntitySchema.shape);
|
||||
|
||||
export type DisplayOrderItem = z.infer<typeof DisplayOrderItemSchema>;
|
||||
@@ -0,0 +1,21 @@
|
||||
import { EntitySchema, PaymentTypeSchema } from '@isa/common/data-access';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const DisplayOrderPaymentSchema = z
|
||||
.object({
|
||||
paymentActionRequired: z.boolean().describe('Payment action required'),
|
||||
paymentType: PaymentTypeSchema.describe('Payment type'),
|
||||
paymentNumber: z.string().describe('Payment number').optional(),
|
||||
paymentComment: z.string().describe('Payment comment').optional(),
|
||||
total: z.number().describe('Total amount'),
|
||||
subtotal: z.number().describe('Subtotal').optional(),
|
||||
shipping: z.number().describe('Shipping').optional(),
|
||||
tax: z.number().describe('Tax amount').optional(),
|
||||
currency: z.string().describe('Currency code').optional(),
|
||||
dateOfPayment: z.string().describe('Date of payment').optional(),
|
||||
completed: z.string().describe('Completed').optional(),
|
||||
cancelled: z.string().describe('Cancelled').optional(),
|
||||
})
|
||||
.extend(EntitySchema.shape);
|
||||
|
||||
export type DisplayOrderPayment = z.infer<typeof DisplayOrderPaymentSchema>;
|
||||
50
libs/oms/data-access/src/lib/schemas/display-order.schema.ts
Normal file
50
libs/oms/data-access/src/lib/schemas/display-order.schema.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import {
|
||||
BuyerTypeSchema,
|
||||
EntitySchema,
|
||||
KeyValueOfStringAndStringSchema,
|
||||
NotificationChannelSchema,
|
||||
} from '@isa/common/data-access';
|
||||
import { z } from 'zod';
|
||||
import { DisplayAddresseeSchema } from './display-addressee.schema';
|
||||
import { DisplayBranchSchema } from './display-branch.schema';
|
||||
import { DisplayLogisticianSchema } from './display-logistician.schema';
|
||||
import { DisplayOrderItemSchema } from './display-order-item.schema';
|
||||
import { DisplayOrderPaymentSchema } from './display-order-payment.schema';
|
||||
import { EnvironmentChannelSchema } from './environment-channel.schema';
|
||||
import { LinkedRecordSchema } from './linked-record.schema';
|
||||
import { OrderTypeSchema } from './order-type.schema';
|
||||
import { TermsOfDeliverySchema } from './terms-of-delivery.schema';
|
||||
|
||||
export const DisplayOrderSchema = z
|
||||
.object({
|
||||
actions: z.array(KeyValueOfStringAndStringSchema).describe('Actions').optional(),
|
||||
buyer: DisplayAddresseeSchema.describe('Buyer information').optional(),
|
||||
buyerComment: z.string().describe('Buyer comment').optional(),
|
||||
buyerIsGuestAccount: z.boolean().describe('Buyer is guest account').optional(),
|
||||
buyerNumber: z.string().describe('Unique buyer identifier number').optional(),
|
||||
buyerType: BuyerTypeSchema.describe('Buyer type').optional(),
|
||||
clientChannel: EnvironmentChannelSchema.describe('Client channel').optional(),
|
||||
completedDate: z.string().describe('Completed date').optional(),
|
||||
features: z.record(z.string().describe('Features'), z.string()).optional(),
|
||||
items: z.array(DisplayOrderItemSchema).describe('List of items').optional(),
|
||||
itemsCount: z.number().describe('Number of itemss').optional(),
|
||||
linkedRecords: z.array(LinkedRecordSchema).describe('List of linked records').optional(),
|
||||
logistician: DisplayLogisticianSchema.describe('Logistician information').optional(),
|
||||
notificationChannels: NotificationChannelSchema.describe('Notification channels').optional(),
|
||||
orderBranch: DisplayBranchSchema.describe('Order branch').optional(),
|
||||
orderDate: z.string().describe('Order date').optional(),
|
||||
orderNumber: z.string().describe('Order number').optional(),
|
||||
orderType: OrderTypeSchema.describe('Order type'),
|
||||
orderValue: z.number().describe('Order value').optional(),
|
||||
orderValueCurrency: z.string().describe('Order value currency').optional(),
|
||||
payer: DisplayAddresseeSchema.describe('Payer information').optional(),
|
||||
payerIsGuestAccount: z.boolean().describe('Payer is guest account').optional(),
|
||||
payerNumber: z.string().describe('Unique payer account number').optional(),
|
||||
payment: DisplayOrderPaymentSchema.describe('Payment').optional(),
|
||||
shippingAddress: DisplayAddresseeSchema.describe('Shipping address information').optional(),
|
||||
targetBranch: DisplayBranchSchema.describe('Target branch').optional(),
|
||||
termsOfDelivery: TermsOfDeliverySchema.describe('Terms of delivery').optional(),
|
||||
})
|
||||
.extend(EntitySchema.shape);
|
||||
|
||||
export type DisplayOrder = z.infer<typeof DisplayOrderSchema>;
|
||||
@@ -0,0 +1,15 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const EnvironmentChannel = {
|
||||
NotSet: 0,
|
||||
System: 1,
|
||||
Branch: 2,
|
||||
CallCenter: 4,
|
||||
WWW: 8,
|
||||
Mobile: 16,
|
||||
BackOffice: 32,
|
||||
} as const;
|
||||
|
||||
export const EnvironmentChannelSchema = z.nativeEnum(EnvironmentChannel).describe('Environment channel');
|
||||
|
||||
export type EnvironmentChannel = z.infer<typeof EnvironmentChannelSchema>;
|
||||
@@ -1,4 +1,19 @@
|
||||
export * from './display-addressee.schema';
|
||||
export * from './display-branch.schema';
|
||||
export * from './display-logistician.schema';
|
||||
export * from './display-order-item.schema';
|
||||
export * from './display-order-payment.schema';
|
||||
export * from './display-order.schema';
|
||||
export * from './environment-channel.schema';
|
||||
export * from './fetch-return-details.schema';
|
||||
export * from './linked-record.schema';
|
||||
export * from './order-type.schema';
|
||||
export * from './price.schema';
|
||||
export * from './product.schema';
|
||||
export * from './promotion.schema';
|
||||
export * from './query-token.schema';
|
||||
export * from './return-process-question-answer.schema';
|
||||
export * from './return-receipt-values.schema';
|
||||
export * from './shipping-type.schema';
|
||||
export * from './terms-of-delivery.schema';
|
||||
export * from './type-of-delivery.schema';
|
||||
|
||||
10
libs/oms/data-access/src/lib/schemas/linked-record.schema.ts
Normal file
10
libs/oms/data-access/src/lib/schemas/linked-record.schema.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const LinkedRecordSchema = z.object({
|
||||
repository: z.string().describe('Repository').optional(),
|
||||
pk: z.string().describe('Pk').optional(),
|
||||
number: z.string().describe('Number').optional(),
|
||||
isSource: z.boolean().describe('Whether source').optional(),
|
||||
});
|
||||
|
||||
export type LinkedRecord = z.infer<typeof LinkedRecordSchema>;
|
||||
14
libs/oms/data-access/src/lib/schemas/order-type.schema.ts
Normal file
14
libs/oms/data-access/src/lib/schemas/order-type.schema.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const OrderType = {
|
||||
NotSet: 0,
|
||||
Branch: 1,
|
||||
Mail: 2,
|
||||
Download: 4,
|
||||
BranchAndDownload: 5, // Branch | Download
|
||||
MailAndDownload: 6, // Mail | Download
|
||||
} as const;
|
||||
|
||||
export const OrderTypeSchema = z.nativeEnum(OrderType).describe('Order type');
|
||||
|
||||
export type OrderType = z.infer<typeof OrderTypeSchema>;
|
||||
15
libs/oms/data-access/src/lib/schemas/price.schema.ts
Normal file
15
libs/oms/data-access/src/lib/schemas/price.schema.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import {
|
||||
PriceValueSchema,
|
||||
TouchBaseSchema,
|
||||
VatValueSchema,
|
||||
} from '@isa/common/data-access';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const PriceSchema = z
|
||||
.object({
|
||||
value: PriceValueSchema.describe('Value').optional(),
|
||||
vat: VatValueSchema.describe('Value Added Tax').optional(),
|
||||
})
|
||||
.extend(TouchBaseSchema.shape);
|
||||
|
||||
export type Price = z.infer<typeof PriceSchema>;
|
||||
27
libs/oms/data-access/src/lib/schemas/product.schema.ts
Normal file
27
libs/oms/data-access/src/lib/schemas/product.schema.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { TouchBaseSchema } from '@isa/common/data-access';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ProductSchema = z
|
||||
.object({
|
||||
name: z.string().describe('Name').optional(),
|
||||
additionalName: z.string().describe('Additional name').optional(),
|
||||
ean: z.string().describe('European Article Number barcode').optional(),
|
||||
supplierProductNumber: z.string().describe('SupplierProduct number').optional(),
|
||||
catalogProductNumber: z.string().describe('CatalogProduct number').optional(),
|
||||
contributors: z.string().describe('Contributors').optional(),
|
||||
manufacturer: z.string().describe('Manufacturer information').optional(),
|
||||
publicationDate: z.string().describe('Publication date').optional(),
|
||||
productGroup: z.string().describe('Product group').optional(),
|
||||
productGroupDetails: z.string().describe('Product group details').optional(),
|
||||
edition: z.string().describe('Edition').optional(),
|
||||
format: z.string().describe('Format').optional(),
|
||||
formatDetail: z.string().describe('Format detail').optional(),
|
||||
locale: z.string().describe('Locale').optional(),
|
||||
serial: z.string().describe('Serial').optional(),
|
||||
size: z.any().describe('Size').optional(), // TODO: Create SizeOfString schema
|
||||
volume: z.string().describe('Volume').optional(),
|
||||
weight: z.any().describe('Weight').optional(), // TODO: Create WeightOfAvoirdupois schema
|
||||
})
|
||||
.extend(TouchBaseSchema.shape);
|
||||
|
||||
export type Product = z.infer<typeof ProductSchema>;
|
||||
13
libs/oms/data-access/src/lib/schemas/promotion.schema.ts
Normal file
13
libs/oms/data-access/src/lib/schemas/promotion.schema.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { TouchBaseSchema } from '@isa/common/data-access';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const PromotionSchema = z
|
||||
.object({
|
||||
value: z.number().describe('Value').optional(),
|
||||
type: z.string().describe('Type').optional(),
|
||||
code: z.string().describe('Code value').optional(),
|
||||
label: z.string().describe('Label').optional(),
|
||||
})
|
||||
.extend(TouchBaseSchema.shape);
|
||||
|
||||
export type Promotion = z.infer<typeof PromotionSchema>;
|
||||
@@ -1,19 +1,19 @@
|
||||
import { z } from 'zod';
|
||||
import { Product } from '../models/product';
|
||||
import { Product } from './product.schema';
|
||||
|
||||
const ReceiptItemSchema = z.object({
|
||||
id: z.number(),
|
||||
id: z.number().describe('Unique identifier'),
|
||||
});
|
||||
|
||||
export const ReturnReceiptValuesSchema = z.object({
|
||||
quantity: z.number(),
|
||||
category: z.string().optional(),
|
||||
comment: z.string().optional(),
|
||||
itemCondition: z.string().optional(),
|
||||
returnDetails: z.string().optional(),
|
||||
returnReason: z.string().optional(),
|
||||
receiptItem: ReceiptItemSchema,
|
||||
otherProduct: z.custom<Product>().optional(),
|
||||
quantity: z.number().describe('Quantity'),
|
||||
category: z.string().describe('Category information').optional(),
|
||||
comment: z.string().describe('Comment text').optional(),
|
||||
itemCondition: z.string().describe('Item condition').optional(),
|
||||
returnDetails: z.string().describe('Return details').optional(),
|
||||
returnReason: z.string().describe('Return reason').optional(),
|
||||
receiptItem: ReceiptItemSchema.describe('Receipt item'),
|
||||
otherProduct: z.custom<Product>().describe('Other product').optional(),
|
||||
});
|
||||
|
||||
export type ReturnReceiptValues = z.infer<typeof ReturnReceiptValuesSchema>;
|
||||
|
||||
17
libs/oms/data-access/src/lib/schemas/shipping-type.schema.ts
Normal file
17
libs/oms/data-access/src/lib/schemas/shipping-type.schema.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ShippingType = {
|
||||
NotSet: 0,
|
||||
Parcel: 1,
|
||||
Letter: 2,
|
||||
Pallet: 4,
|
||||
Freight: 8,
|
||||
Digital: 16,
|
||||
InStore: 32,
|
||||
ClickAndCollect: 64,
|
||||
Dropship: 128,
|
||||
} as const;
|
||||
|
||||
export const ShippingTypeSchema = z.nativeEnum(ShippingType).describe('Shipping type');
|
||||
|
||||
export type ShippingType = z.infer<typeof ShippingTypeSchema>;
|
||||
@@ -0,0 +1,18 @@
|
||||
import { PriceValueSchema, TouchBaseSchema } from '@isa/common/data-access';
|
||||
import { z } from 'zod';
|
||||
import { ShippingTypeSchema } from './shipping-type.schema';
|
||||
import { TypeOfDeliverySchema } from './type-of-delivery.schema';
|
||||
|
||||
export const TermsOfDeliverySchema = z
|
||||
.object({
|
||||
isPartialShipping: z.boolean().describe('Whether partialShipping').optional(),
|
||||
partialShippingCharge: z.number().describe('Partial shipping charge').optional(),
|
||||
postage: PriceValueSchema.describe('Postage').optional(),
|
||||
typeOfDelivery: TypeOfDeliverySchema.describe('Type of delivery').optional(),
|
||||
shippingType: ShippingTypeSchema.describe('Shipping type').optional(),
|
||||
shippingDeadlineStart: z.string().describe('Shipping deadline start').optional(),
|
||||
shippingDeadlineEnd: z.string().describe('Shipping deadline end').optional(),
|
||||
})
|
||||
.extend(TouchBaseSchema.shape);
|
||||
|
||||
export type TermsOfDelivery = z.infer<typeof TermsOfDeliverySchema>;
|
||||
@@ -0,0 +1,16 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const TypeOfDelivery = {
|
||||
NotSet: 0,
|
||||
Standard: 1,
|
||||
Express: 2,
|
||||
SameDay: 4,
|
||||
Pickup: 8,
|
||||
Economy: 16,
|
||||
Overnight: 32,
|
||||
International: 64,
|
||||
} as const;
|
||||
|
||||
export const TypeOfDeliverySchema = z.nativeEnum(TypeOfDelivery).describe('Type of delivery');
|
||||
|
||||
export type TypeOfDelivery = z.infer<typeof TypeOfDeliverySchema>;
|
||||
@@ -1,9 +1,10 @@
|
||||
export * from './logistician.service';
|
||||
export * from './order-creation.service';
|
||||
export * from './return-can-return.service';
|
||||
export * from './return-details.service';
|
||||
export * from './print-receipts.service';
|
||||
export * from './return-process.service';
|
||||
export * from './return-search.service';
|
||||
export * from './return-task-list.service';
|
||||
export * from './print-tolino-return-receipt.service';
|
||||
export * from './logistician.service';
|
||||
export * from './oms-metadata.service';
|
||||
export * from './order-creation.service';
|
||||
export * from './print-receipts.service';
|
||||
export * from './print-tolino-return-receipt.service';
|
||||
export * from './return-can-return.service';
|
||||
export * from './return-details.service';
|
||||
export * from './return-process.service';
|
||||
export * from './return-search.service';
|
||||
export * from './return-task-list.service';
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { TabService, getMetadataHelper } from '@isa/core/tabs';
|
||||
import { DisplayOrder, DisplayOrderSchema } from '../schemas';
|
||||
import { OMS_DISPLAY_ORDERS_KEY } from '../constants';
|
||||
import z from 'zod';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class OmsMetadataService {
|
||||
#tabService = inject(TabService);
|
||||
|
||||
getDisplayOrders(tabId: number): DisplayOrder[] | undefined {
|
||||
return getMetadataHelper(
|
||||
tabId,
|
||||
OMS_DISPLAY_ORDERS_KEY,
|
||||
z.array(DisplayOrderSchema).optional(),
|
||||
this.#tabService.entityMap(),
|
||||
);
|
||||
}
|
||||
|
||||
addDisplayOrders(tabId: number, orders: DisplayOrder[]) {
|
||||
const existingOrders = this.getDisplayOrders(tabId) || [];
|
||||
this.#tabService.patchTabMetadata(tabId, {
|
||||
[OMS_DISPLAY_ORDERS_KEY]: [...existingOrders, ...orders],
|
||||
});
|
||||
}
|
||||
|
||||
clearDisplayOrders(tabId: number) {
|
||||
this.#tabService.patchTabMetadata(tabId, {
|
||||
[OMS_DISPLAY_ORDERS_KEY]: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,8 @@ import { TabService } from '@isa/core/tabs';
|
||||
import { patchState } from '@ngrx/signals';
|
||||
import { setAllEntities, setEntity } from '@ngrx/signals/entities';
|
||||
import { unprotected } from '@ngrx/signals/testing';
|
||||
import { Product, ReturnProcess } from '../models';
|
||||
import { Product } from '../schemas';
|
||||
import { ReturnProcess } from '../models';
|
||||
import { CreateReturnProcessError } from '../errors/return-process';
|
||||
import { ProductCategory } from '../questions';
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user