Enhance documentation for return process components and schemas.

- 📚 **Docs**: Added detailed comments for return process questions and validation logic
- 📚 **Docs**: Improved documentation for return process service methods
- 📚 **Docs**: Updated schemas with descriptions for clarity
This commit is contained in:
Lorenz Hilpert
2025-04-11 19:21:18 +02:00
parent 4885a523ab
commit beeba1004e
9 changed files with 201 additions and 43 deletions

View File

@@ -1,3 +1,18 @@
/**
* OMS Data Access Library
*
* This library provides services and stores for interacting with Order Management System (OMS) data.
* It includes functionality for searching, viewing, and processing returns in the system.
*
* Core components:
* - Models: Type definitions for data structures
* - Services: API integrations for data retrieval and manipulation
* - Stores: State management for OMS-related data
* - Schemas: Validation schemas for ensuring data integrity
* - Return Process: Question flows and validation for return processing
* - Error handling: Specialized error types for OMS operations
*/
export * from './lib/errors';
export * from './lib/models';
export * from './lib/schemas';

View File

@@ -1,3 +1,17 @@
/**
* Questions Module Index
*
* This module exports all components of the return process question system:
* - Constants: Enum values used throughout the question system
* - Types: Type definitions for validators and other components
* - Validators: Generic validation utilities
* - Category-specific modules: Questions and validation logic for each product category
* - Registry: Mappings that connect product categories with their questions and validators
*
* The questions system is organized by product category, with each category having its own
* set of questions and validation logic to determine return eligibility.
*/
// Export constants
export * from './constants';

View File

@@ -14,6 +14,10 @@ import {
/**
* Questions for the return process of Tolino devices.
* This array defines the sequence and logic flow of questions presented to users
* when processing Tolino device returns in the system.
* Each question has a unique key, descriptive text, question type, and possible options
* with their corresponding next question in the flow.
*/
export const tolinoQuestions: ReturnProcessQuestion[] = [
{
@@ -132,9 +136,15 @@ export const tolinoQuestions: ReturnProcessQuestion[] = [
];
/**
* Validates the answers for the Tolino return process.
* @param answers - A record of answers keyed by question keys.
* @returns The eligibility state for the return process.
* Validates the answers for the Tolino return process and determines return eligibility.
*
* The validation logic follows these rules:
* 1. If item is in original packaging, it's always eligible for return
* 2. If item is damaged, it requires at least one defect to be eligible
* 3. If no valid condition is provided, the item is not eligible
*
* @param {ReturnProcessAnswers} answers - A record of answers keyed by question keys
* @returns {EligibleForReturn} Object containing eligibility state and reason if applicable
*/
export function validateTolinoQuestions(
answers: ReturnProcessAnswers,

View File

@@ -10,6 +10,10 @@ import { ItemConditionValue, ItemDefectiveValue } from './constants';
/**
* Questions for the return process of Ton-/Datenträger (audio/data carriers).
* This array defines the sequence and logic flow of questions presented to users
* when processing audio or data carrier returns in the system.
* Each question has a unique key, descriptive text, question type, and possible options
* with their corresponding next question in the flow.
*/
export const tonDatentraegerQuestions: ReturnProcessQuestion[] = [
{
@@ -46,9 +50,16 @@ export const tonDatentraegerQuestions: ReturnProcessQuestion[] = [
];
/**
* Validates the answers for the Ton-/Datenträger return process.
* @param answers - A record of answers keyed by question keys.
* @returns The eligibility state for the return process.
* Validates the answers for the Ton-/Datenträger (audio/data carriers) return process and determines return eligibility.
*
* The validation logic follows these rules:
* 1. If the item is in original packaging/sealed, it's always eligible for return
* 2. If the item has been opened, it's only eligible if it's defective
* 3. If the item has been opened but is not defective, it's not eligible for return
* 4. If invalid or incomplete answers are provided, the item is not eligible
*
* @param {ReturnProcessAnswers} answers - A record of answers keyed by question keys
* @returns {EligibleForReturn} Object containing eligibility state and reason if applicable
*/
export function validateTonDatentraegerQuestions(
answers: ReturnProcessAnswers,

View File

@@ -9,14 +9,29 @@ import {
import { CategoryQuestions, CategoryQuestionValidators } from './questions';
import { KeyValue } from '@angular/common';
/**
* Service responsible for managing the return process workflow.
* Handles questions, validation, and eligibility determination for product returns.
*/
@Injectable({ providedIn: 'root' })
export class ReturnProcessService {
/**
* Gets all available product categories that have defined question sets.
*
* @returns {KeyValue<string, string>[]} Array of key-value pairs representing available categories.
*/
availableCategories(): KeyValue<string, string>[] {
return Object.keys(CategoryQuestions).map((key) => {
return { key, value: key };
});
}
/**
* Retrieves questions applicable to a specific return process based on product category.
*
* @param {ReturnProcess} process - The return process containing product information.
* @returns {ReturnProcessQuestion[] | undefined} Questions for the category or undefined if no matching category found.
*/
returnProcessQuestions(
process: ReturnProcess,
): ReturnProcessQuestion[] | undefined {
@@ -29,6 +44,12 @@ export class ReturnProcessService {
return undefined;
}
/**
* Gets the validator function for a specific return process based on product category.
*
* @param {ReturnProcess} process - The return process containing product information.
* @returns {((answers: Record<string, unknown>) => EligibleForReturn) | undefined} Validator function or undefined if no matching category found.
*/
returnProcessQuestionValidator(
process: ReturnProcess,
): ((answers: Record<string, unknown>) => EligibleForReturn) | undefined {
@@ -41,6 +62,13 @@ export class ReturnProcessService {
return undefined;
}
/**
* Gets active questions in the return process based on previously provided answers.
* Handles question branching logic and detects cyclic dependencies.
*
* @param {ReturnProcess} process - The return process containing answers and product information.
* @returns {ReturnProcessQuestion[] | undefined} Active questions in the process or undefined if no questions apply.
*/
activeReturnProcessQuestions(
process: ReturnProcess,
): ReturnProcessQuestion[] | undefined {
@@ -87,10 +115,11 @@ export class ReturnProcessService {
}
/**
* Returns the progress of the return process questions
* Returns a tuple with the current question index and the total number of questions
* @param product Which questions to ask
* @param answers Answers to previous questions
* Calculates the progress of the return process questions.
* Returns a tuple with the current question index and the total number of questions.
*
* @param {ReturnProcess} returnProcess - The return process containing answers and product information.
* @returns {{ answered: number; total: number } | undefined} Progress information or undefined if no questions apply.
*/
returnProcessQuestionsProgress(
returnProcess: ReturnProcess,
@@ -161,6 +190,13 @@ export class ReturnProcessService {
return { answered, total: totalCount };
}
/**
* Determines whether a product is eligible for return based on provided answers.
* Validates all questions have been answered and applies category-specific validation rules.
*
* @param {ReturnProcess} returnProcess - The return process containing answers and product information.
* @returns {EligibleForReturn} Object indicating eligibility state and reason.
*/
eligibleForReturn(returnProcess: ReturnProcess): EligibleForReturn {
const questions = this.activeReturnProcessQuestions(returnProcess);

View File

@@ -1,5 +1,15 @@
import { patchState, signalStore, withComputed, withHooks, withMethods } from '@ngrx/signals';
import { withEntities, setAllEntities, updateEntity } from '@ngrx/signals/entities';
import {
patchState,
signalStore,
withComputed,
withHooks,
withMethods,
} from '@ngrx/signals';
import {
withEntities,
setAllEntities,
updateEntity,
} from '@ngrx/signals/entities';
import { IDBStorageProvider, withStorage } from '@isa/core/storage';
import { computed, effect, inject } from '@angular/core';
import { ProcessService } from '@isa/core/process';
@@ -75,7 +85,10 @@ export const ReturnProcessStore = signalStore(
const entity = store.entityMap()[id];
if (entity) {
const answers = { ...entity.answers, [question]: answer };
patchState(store, updateEntity({ id: entity.id, changes: { answers } }));
patchState(
store,
updateEntity({ id: entity.id, changes: { answers } }),
);
store.storeState();
}
},
@@ -91,7 +104,10 @@ export const ReturnProcessStore = signalStore(
if (entity) {
const answers = { ...entity.answers };
delete answers[question];
patchState(store, updateEntity({ id: entity.id, changes: { answers } }));
patchState(
store,
updateEntity({ id: entity.id, changes: { answers } }),
);
store.storeState();
}
},
@@ -106,7 +122,13 @@ export const ReturnProcessStore = signalStore(
setProductCategory: (id: number, category: string | undefined) => {
const entity = store.entityMap()[id];
if (entity) {
patchState(store, updateEntity({ id: entity.id, changes: { productCategory: category } }));
patchState(
store,
updateEntity({
id: entity.id,
changes: { productCategory: category },
}),
);
store.storeState();
}
},
@@ -131,7 +153,9 @@ export const ReturnProcessStore = signalStore(
const nextId = store.nextId();
const returnableItems = params.items.filter((item) =>
item.actions?.some((a) => a.key === 'canReturn' && coerceBooleanProperty(a.value)),
item.actions?.some(
(a) => a.key === 'canReturn' && coerceBooleanProperty(a.value),
),
);
if (returnableItems.length === 0) {
@@ -167,12 +191,18 @@ export const ReturnProcessStore = signalStore(
})),
withHooks((store, processService = inject(ProcessService)) => ({
/**
* Lifecycle hook that runs when the store is initialized.
* Sets up an effect to clean up orphaned entities that are no longer associated with active processes.
*/
onInit() {
effect(() => {
const processIds = processService.ids();
const entities = store.entities().find((entity) => !processIds.includes(entity.processId));
if (entities) {
store.removeAllEntitiesByProcessId(entities.processId);
const orphanedEntity = store
.entities()
.find((entity) => !processIds.includes(entity.processId));
if (orphanedEntity) {
store.removeAllEntitiesByProcessId(orphanedEntity.processId);
}
});
},

View File

@@ -1,4 +1,10 @@
import { patchState, signalStore, type, withHooks, withMethods } from '@ngrx/signals';
import {
patchState,
signalStore,
type,
withHooks,
withMethods,
} from '@ngrx/signals';
import { rxMethod } from '@ngrx/signals/rxjs-interop';
import {
addEntity,
@@ -61,8 +67,16 @@ export const ReturnSearchStore = signalStore(
getEntity(processId: number): ReturnSearchEntity | undefined {
return store.entities().find((e) => e.processId === processId);
},
removeAllEntitiesByProcessId(processId: number) {
const entities = store.entities().filter((entity) => entity.processId !== processId);
/**
* Removes all entities associated with a specific process ID.
*
* @param {number} processId - The unique identifier of the process whose entities should be removed.
* @returns {void}
*/
removeAllEntitiesByProcessId(processId: number): void {
const entities = store
.entities()
.filter((entity) => entity.processId !== processId);
patchState(store, setAllEntities(entities, config));
},
})),
@@ -128,7 +142,9 @@ export const ReturnSearchStore = signalStore(
changes: {
status: ReturnSearchStatus.Success,
hits: response.hits,
items: entityItems ? [...entityItems, ...response.result] : response.result,
items: entityItems
? [...entityItems, ...response.result]
: response.result,
},
},
config,
@@ -145,7 +161,13 @@ export const ReturnSearchStore = signalStore(
* @param {number} options.processId - The unique identifier of the search process.
* @param {unknown} options.error - The error encountered.
*/
handleSearchError({ processId, error }: { processId: number; error: unknown }) {
handleSearchError({
processId,
error,
}: {
processId: number;
error: unknown;
}) {
console.error(error);
patchState(
store,
@@ -185,12 +207,10 @@ export const ReturnSearchStore = signalStore(
switchMap(({ processId, query, cb }) =>
returnSearchService
.search(
QueryTokenSchema.parse(
QueryTokenSchema.parse({
...query,
skip: store.getEntity(processId)?.items?.length ?? 0,
}),
),
QueryTokenSchema.parse({
...query,
skip: store.getEntity(processId)?.items?.length ?? 0,
}),
)
.pipe(
tapResponse(
@@ -212,9 +232,11 @@ export const ReturnSearchStore = signalStore(
onInit() {
effect(() => {
const processIds = processService.ids();
const entities = store.entities().find((entity) => !processIds.includes(entity.processId));
if (entities) {
store.removeAllEntitiesByProcessId(entities.processId);
const orphanedEntity = store
.entities()
.find((entity) => !processIds.includes(entity.processId));
if (orphanedEntity) {
store.removeAllEntitiesByProcessId(orphanedEntity.processId);
}
});
},

View File

@@ -1,7 +1,15 @@
import { z } from 'zod';
/**
* Schema for validating parameters when fetching return details.
* Ensures that a valid numeric receipt ID is provided.
*/
export const FetchReturnDetailsSchema = z.object({
receiptId: z.number(),
receiptId: z.number(), // The unique identifier for the receipt
});
/**
* Type representing the parameters required for fetching return details.
* Generated from the Zod schema for type safety.
*/
export type FetchReturnDetails = z.infer<typeof FetchReturnDetailsSchema>;

View File

@@ -1,18 +1,30 @@
import { z } from 'zod';
/**
* Schema for defining order-by parameters in queries.
* Used for sorting search results.
*/
export const OrderBySchema = z.object({
by: z.string(),
label: z.string(),
desc: z.boolean(),
selected: z.boolean(),
by: z.string(), // Field name to sort by
label: z.string(), // Display label for the sort option
desc: z.boolean(), // Whether sorting is descending
selected: z.boolean(), // Whether this sort option is currently selected
});
/**
* Schema for validating and parsing query tokens.
* Used for search operations to ensure consistent query structure.
*/
export const QueryTokenSchema = z.object({
filter: z.record(z.any()).default({}),
input: z.record(z.any()).default({}),
orderBy: z.array(OrderBySchema).default([]),
skip: z.number().default(0),
take: z.number().default(25),
filter: z.record(z.any()).default({}), // Filter criteria as key-value pairs
input: z.record(z.any()).default({}), // Input values for the query
orderBy: z.array(OrderBySchema).default([]), // Sorting parameters
skip: z.number().default(0), // Number of items to skip (for pagination)
take: z.number().default(25), // Number of items to take (page size)
});
/**
* Type representing the structure of a query token input.
* Generated from the Zod schema for type safety.
*/
export type QueryTokenInput = z.infer<typeof QueryTokenSchema>;