mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-31 09:37:15 +01:00
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:
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user