mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Merge branch 'develop' into feature/5047-5053-Design-und-Funktionsweise-Drucker-Dialog
This commit is contained in:
@@ -35,7 +35,7 @@ export function activeReturnProcessQuestions(
|
||||
return internalActiveReturnProcessQuestions(questions, answers).questions;
|
||||
}
|
||||
|
||||
function internalActiveReturnProcessQuestions(
|
||||
export function internalActiveReturnProcessQuestions(
|
||||
questions: ReturnProcessQuestion[],
|
||||
answers: ReturnProcessAnswers,
|
||||
): { questions: ReturnProcessQuestion[]; nextQuestion?: string } {
|
||||
|
||||
@@ -108,7 +108,7 @@ describe('internalCalculateLongestQuestionDepth', () => {
|
||||
[ReturnProcessQuestionKey.ItemCondition]: ItemConditionAnswer.Damaged,
|
||||
};
|
||||
|
||||
const expectedDepth = 8;
|
||||
const expectedDepth = 9;
|
||||
|
||||
const result = helpers.calculateLongestQuestionDepth(
|
||||
tolinoQuestions,
|
||||
|
||||
@@ -2,37 +2,95 @@ import {
|
||||
EligibleForReturn,
|
||||
EligibleForReturnState,
|
||||
ReturnProcess,
|
||||
ReturnProcessQuestion,
|
||||
ReturnProcessQuestionKey,
|
||||
} from '../../models';
|
||||
import { ItemConditionAnswer, YesNoAnswer } from '../../questions';
|
||||
import { parseISO, differenceInCalendarDays } from 'date-fns';
|
||||
import {
|
||||
ItemConditionAnswer,
|
||||
ReturnReasonAnswer,
|
||||
YesNoAnswer,
|
||||
} from '../../questions';
|
||||
import {
|
||||
parseISO,
|
||||
differenceInCalendarDays,
|
||||
differenceInCalendarMonths,
|
||||
} from 'date-fns';
|
||||
|
||||
// TODO: Tolino special cases implementieren (Verschiedene Antwortmöglichkeiten)
|
||||
// #4978
|
||||
export const isTolinoEligibleForReturn = (
|
||||
returnProcess: ReturnProcess,
|
||||
questions: ReturnProcessQuestion[],
|
||||
): EligibleForReturn | undefined => {
|
||||
console.log(returnProcess);
|
||||
const answers = returnProcess.answers;
|
||||
const date = returnProcess.receiptDate;
|
||||
|
||||
// Check if question exists before evaluating its answer
|
||||
if (questions.find((q) => q.key === ReturnProcessQuestionKey.ItemCondition)) {
|
||||
const itemCondition = answers[ReturnProcessQuestionKey.ItemCondition];
|
||||
|
||||
const receiptOlderThan100Days = date
|
||||
? differenceInCalendarDays(new Date(), parseISO(date)) >= 100
|
||||
: undefined;
|
||||
|
||||
if (itemCondition === ItemConditionAnswer.OVP && receiptOlderThan100Days) {
|
||||
return {
|
||||
state: EligibleForReturnState.NotEligible,
|
||||
reason: 'Keine Retoure möglich',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Check if question exists before evaluating its answer
|
||||
if (questions.find((q) => q.key === ReturnProcessQuestionKey.ItemDamaged)) {
|
||||
const itemDamaged = answers[ReturnProcessQuestionKey.ItemDamaged];
|
||||
|
||||
if (itemDamaged === ReturnReasonAnswer.DamagedByCustomer) {
|
||||
return {
|
||||
state: EligibleForReturnState.NotEligible,
|
||||
reason: 'Keine Retoure möglich',
|
||||
};
|
||||
}
|
||||
|
||||
const receiptOlderThan6Months = date
|
||||
? differenceInCalendarMonths(new Date(), parseISO(date)) >= 6
|
||||
: undefined;
|
||||
|
||||
if (
|
||||
itemDamaged === ReturnReasonAnswer.ReceivedDamaged &&
|
||||
receiptOlderThan6Months
|
||||
) {
|
||||
return {
|
||||
state: EligibleForReturnState.NotEligible,
|
||||
reason: 'Keine Retoure möglich',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { state: EligibleForReturnState.Eligible };
|
||||
};
|
||||
|
||||
// #4978
|
||||
export const isElektronischeGeraeteEligibleForReturn = (
|
||||
returnProcess: ReturnProcess,
|
||||
questions: ReturnProcessQuestion[],
|
||||
): EligibleForReturn | undefined => {
|
||||
const answers = returnProcess.answers;
|
||||
const date = returnProcess.receiptDate;
|
||||
const itemCondition = answers[ReturnProcessQuestionKey.ItemCondition];
|
||||
|
||||
const receiptOlderThan100Days = date
|
||||
? differenceInCalendarDays(new Date(), parseISO(date)) >= 100
|
||||
: undefined;
|
||||
// Check if question exists before evaluating its answer
|
||||
if (questions.find((q) => q.key === ReturnProcessQuestionKey.ItemCondition)) {
|
||||
const itemCondition = answers[ReturnProcessQuestionKey.ItemCondition];
|
||||
|
||||
if (itemCondition === ItemConditionAnswer.OVP && receiptOlderThan100Days) {
|
||||
return {
|
||||
state: EligibleForReturnState.NotEligible,
|
||||
reason: 'Keine Retoure möglich',
|
||||
};
|
||||
const receiptOlderThan100Days = date
|
||||
? differenceInCalendarDays(new Date(), parseISO(date)) >= 100
|
||||
: undefined;
|
||||
|
||||
if (itemCondition === ItemConditionAnswer.OVP && receiptOlderThan100Days) {
|
||||
return {
|
||||
state: EligibleForReturnState.NotEligible,
|
||||
reason: 'Keine Retoure möglich',
|
||||
};
|
||||
}
|
||||
}
|
||||
return { state: EligibleForReturnState.Eligible };
|
||||
};
|
||||
@@ -40,18 +98,27 @@ export const isElektronischeGeraeteEligibleForReturn = (
|
||||
// #4978
|
||||
export const isTonDatentraegerEligibleForReturn = (
|
||||
returnProcess: ReturnProcess,
|
||||
questions: ReturnProcessQuestion[],
|
||||
): EligibleForReturn | undefined => {
|
||||
const answers = returnProcess.answers;
|
||||
const itemCondition = answers[ReturnProcessQuestionKey.ItemCondition];
|
||||
const itemDefective = answers[ReturnProcessQuestionKey.ItemDefective];
|
||||
|
||||
// Check if questions exist before evaluating their answers
|
||||
if (
|
||||
itemCondition === ItemConditionAnswer.Damaged &&
|
||||
itemDefective === YesNoAnswer.No
|
||||
questions.find((q) => q.key === ReturnProcessQuestionKey.ItemCondition) &&
|
||||
questions.find((q) => q.key === ReturnProcessQuestionKey.ItemDefective)
|
||||
) {
|
||||
return {
|
||||
state: EligibleForReturnState.NotEligible,
|
||||
reason: 'Keine Retoure möglich',
|
||||
};
|
||||
const itemCondition = answers[ReturnProcessQuestionKey.ItemCondition];
|
||||
const itemDefective = answers[ReturnProcessQuestionKey.ItemDefective];
|
||||
if (
|
||||
itemCondition === ItemConditionAnswer.Damaged &&
|
||||
itemDefective === YesNoAnswer.No
|
||||
) {
|
||||
return {
|
||||
state: EligibleForReturnState.NotEligible,
|
||||
reason: 'Keine Retoure möglich',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { state: EligibleForReturnState.Eligible };
|
||||
};
|
||||
|
||||
@@ -7,9 +7,13 @@ import {
|
||||
ReturnProcessQuestionKey,
|
||||
ReturnProcessQuestionType,
|
||||
} from '../../models';
|
||||
import { activeReturnProcessQuestions } from './active-return-process-questions.helper';
|
||||
import {
|
||||
activeReturnProcessQuestions,
|
||||
internalActiveReturnProcessQuestions,
|
||||
} from './active-return-process-questions.helper';
|
||||
import { allReturnProcessQuestionsAnswered } from './all-return-process-questions-answered.helper';
|
||||
import { ReturnProcessQuestionSchema } from '../../questions';
|
||||
import { ReturnProcessChecklistAnswerSchema } from '../../schemas';
|
||||
|
||||
/**
|
||||
* Union type for parameters accepted by getReturnInfo.
|
||||
@@ -113,7 +117,6 @@ export function getReturnInfo(
|
||||
currentQuestionKey = selectedOption?.nextQuestion;
|
||||
break;
|
||||
}
|
||||
// TODO: Checkliste handling
|
||||
case ReturnProcessQuestionType.Product: {
|
||||
const parseResult = ReturnProcessQuestionSchema[
|
||||
ReturnProcessQuestionKey.DeliveredItem
|
||||
@@ -128,18 +131,50 @@ export function getReturnInfo(
|
||||
currentQuestionKey = question.nextQuestion;
|
||||
break;
|
||||
}
|
||||
case ReturnProcessQuestionType.Checklist: {
|
||||
const parseResult =
|
||||
ReturnProcessChecklistAnswerSchema.passthrough().safeParse(answer);
|
||||
|
||||
if (parseResult.success) {
|
||||
const selectedValues: string[] = [...parseResult.data.options];
|
||||
|
||||
if (parseResult.data.other) {
|
||||
selectedValues.push(parseResult.data.other);
|
||||
}
|
||||
|
||||
assignReturnInfo(returnInfo, {
|
||||
returnDetails: {
|
||||
[question.key]: selectedValues,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
currentQuestionKey = question.nextQuestion;
|
||||
break;
|
||||
}
|
||||
case ReturnProcessQuestionType.Info: {
|
||||
currentQuestionKey = question.nextQuestion;
|
||||
break;
|
||||
}
|
||||
case ReturnProcessQuestionType.Group: {
|
||||
const f = internalActiveReturnProcessQuestions(
|
||||
question.questions,
|
||||
answers,
|
||||
);
|
||||
|
||||
const groupReturnInfo = getReturnInfo({
|
||||
questions: question.questions,
|
||||
questions: f.questions,
|
||||
answers,
|
||||
});
|
||||
if (groupReturnInfo) {
|
||||
assignReturnInfo(returnInfo, groupReturnInfo);
|
||||
}
|
||||
|
||||
currentQuestionKey = f.nextQuestion as ReturnProcessQuestionKey;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unsupported question type: ${question.type}`);
|
||||
throw new Error(`Unsupported question type`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -147,7 +182,39 @@ export function getReturnInfo(
|
||||
return returnInfo;
|
||||
}
|
||||
|
||||
// TODO: Je nachdem wie das Backend die Werte braucht hier drin anpassen
|
||||
/**
|
||||
* Assigns all properties from `source` into `target`, merging `returnDetails` specially.
|
||||
*
|
||||
* @param target - The object to receive assigned properties.
|
||||
* @param source - The object providing properties to assign.
|
||||
*
|
||||
* @remarks
|
||||
* - If `source.returnDetails` is `undefined`, performs a straight shallow merge of all fields.
|
||||
* - Otherwise:
|
||||
* 1. Merges `returnDetails` by combining any existing entries in `target.returnDetails` with
|
||||
* those from `source.returnDetails`.
|
||||
* 2. Shallow-assigns all other properties from `source` into `target`, leaving `returnDetails`
|
||||
* untouched (since it was already merged).
|
||||
*/
|
||||
function assignReturnInfo(target: ReturnInfo, source: ReturnInfo) {
|
||||
Object.assign(target, source);
|
||||
// 1) If there are no returnDetails in source, copy everything at once.
|
||||
if (!source.returnDetails) {
|
||||
Object.assign(target, source);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2) Merge returnDetails:
|
||||
// - Keep existing entries in target.returnDetails (if any)
|
||||
// - Override/add entries from source.returnDetails
|
||||
target.returnDetails = {
|
||||
...(target.returnDetails ?? {}),
|
||||
...source.returnDetails,
|
||||
};
|
||||
|
||||
// 3) Prepare all other fields (excluding returnDetails) for assignment
|
||||
const otherFields: Partial<ReturnInfo> = { ...source };
|
||||
delete otherFields.returnDetails;
|
||||
|
||||
// 4) Shallow-merge the rest of the properties into target
|
||||
Object.assign(target, otherFields);
|
||||
}
|
||||
|
||||
@@ -6,3 +6,4 @@ export * from './get-return-info.helper';
|
||||
export * from './eligible-for-return.helper';
|
||||
export * from './get-return-process-questions.helper';
|
||||
export * from './return-receipt-values-mapping.helper';
|
||||
export * from './return-details-mapping.helper';
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ReturnProcessQuestionKey } from '../../models';
|
||||
|
||||
export const returnDetailsMapping = (
|
||||
returnDetails: Partial<Record<ReturnProcessQuestionKey, unknown>> | undefined,
|
||||
): string | undefined => {
|
||||
if (!returnDetails || Object.keys(returnDetails).length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return Object.entries(returnDetails)
|
||||
.map(([key, value]) => `${key}:${String(value)}`)
|
||||
.join(';');
|
||||
};
|
||||
@@ -3,6 +3,7 @@ import { ReturnReceiptValues } from '../../schemas';
|
||||
import { getReturnProcessQuestions } from './get-return-process-questions.helper';
|
||||
import { getReturnInfo } from './get-return-info.helper';
|
||||
import { PropertyNullOrUndefinedError } from '@isa/common/data-access';
|
||||
import { returnDetailsMapping } from './return-details-mapping.helper';
|
||||
|
||||
export const returnReceiptValuesMapping = (
|
||||
process: ReturnProcess,
|
||||
@@ -22,11 +23,11 @@ export const returnReceiptValuesMapping = (
|
||||
}
|
||||
|
||||
return {
|
||||
quantity: process.receiptItem.quantity.quantity, // TODO: Teilmenge handling implementieren - Aktuell wird die gesamte Quantity genommen
|
||||
quantity: process.receiptItem.quantity.quantity,
|
||||
comment: returnInfo.comment,
|
||||
itemCondition: returnInfo.itemCondition,
|
||||
otherProduct: returnInfo.otherProduct,
|
||||
returnDetails: returnInfo.returnDetails,
|
||||
returnDetails: returnDetailsMapping(returnInfo.returnDetails),
|
||||
returnReason: returnInfo.returnReason,
|
||||
category: process?.receiptItem?.features?.['category'],
|
||||
receiptItem: {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Product } from './product';
|
||||
import { ReturnProcessQuestionKey } from './return-process-question-key';
|
||||
|
||||
/**
|
||||
* Interface representing information collected during a return process.
|
||||
@@ -24,7 +25,7 @@ export interface ReturnInfo {
|
||||
/**
|
||||
* Return details / Rückgabedetails
|
||||
*/
|
||||
returnDetails?: string;
|
||||
returnDetails?: Partial<Record<ReturnProcessQuestionKey, unknown>>;
|
||||
|
||||
/**
|
||||
* Return reason / Rückgabegrund
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
export const ReturnProcessQuestionKey = {
|
||||
ItemCondition: 'item_condition',
|
||||
ReturnReason: 'return_reason',
|
||||
DeliveredItem: 'delivered_item',
|
||||
ItemDefective: 'item_defective',
|
||||
DisplayCondition: 'display_condition',
|
||||
DevicePower: 'device_power',
|
||||
PackageComplete: 'package_complete',
|
||||
ItemCondition: 'Artikelzustand',
|
||||
ReturnReason: 'Rückgabegrund',
|
||||
DeliveredItem: 'Gelieferter Artikel',
|
||||
ItemDefective: 'Kleinere Mängel',
|
||||
DisplayDamaged: 'Display beschädigt',
|
||||
DevicePower: 'Gerät einschaltbar',
|
||||
PackageComplete: 'Verpackung vollständig',
|
||||
PackageCompleteGroup: 'package_complete_group',
|
||||
PackageIncompleteInfo: 'info',
|
||||
PackageIncomplete: 'package_incomplete',
|
||||
CaseCondition: 'case_condition',
|
||||
UsbPort: 'usb_port',
|
||||
ItemDamaged: 'item_damaged',
|
||||
PackageMissingItems: 'Fehlende Artikel',
|
||||
CaseDamaged: 'Gehäuse beschädigt',
|
||||
UsbPortDamaged: 'USB-Anschluss beschädigt',
|
||||
ItemDamaged: 'Beschädigungs Ursache',
|
||||
} as const;
|
||||
|
||||
export type ReturnProcessQuestionKey =
|
||||
|
||||
@@ -11,24 +11,24 @@ import { ReturnProcessQuestionKey } from '../models';
|
||||
describe('Constants', () => {
|
||||
describe('Return Process Answers', () => {
|
||||
it('should define ItemConditionAnswer values', () => {
|
||||
expect(ItemConditionAnswer.OVP).toBe('ovp');
|
||||
expect(ItemConditionAnswer.Damaged).toBe('damaged');
|
||||
expect(ItemConditionAnswer.OVP).toBe('Originalverpackt');
|
||||
expect(ItemConditionAnswer.Damaged).toBe('Geöffnet/Defekt');
|
||||
});
|
||||
|
||||
it('should define ReturnReasonAnswer values', () => {
|
||||
expect(ReturnReasonAnswer.Dislike).toBe('dislike');
|
||||
expect(ReturnReasonAnswer.WrongItem).toBe('wrong_item');
|
||||
expect(ReturnReasonAnswer.Dislike).toBe('Gefällt nicht/Widerruf');
|
||||
expect(ReturnReasonAnswer.WrongItem).toBe('Fehllieferung');
|
||||
});
|
||||
|
||||
it('should define YesNoAnswer values', () => {
|
||||
expect(YesNoAnswer.Yes).toBe('yes');
|
||||
expect(YesNoAnswer.No).toBe('no');
|
||||
expect(YesNoAnswer.Yes).toBe('Ja');
|
||||
expect(YesNoAnswer.No).toBe('Nein');
|
||||
});
|
||||
|
||||
it('should define PackageIncompleteAnswer values', () => {
|
||||
expect(PackageIncompleteAnswer.OVP).toBe('ovp');
|
||||
expect(PackageIncompleteAnswer.ChargingCable).toBe('charging_cable');
|
||||
expect(PackageIncompleteAnswer.QuickStartGuide).toBe('quick_start_guide');
|
||||
expect(PackageIncompleteAnswer.OVP).toBe('Karton / Umverpackung');
|
||||
expect(PackageIncompleteAnswer.ChargingCable).toBe('Ladekabel');
|
||||
expect(PackageIncompleteAnswer.QuickStartGuide).toBe('Quickstart-Guide');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -75,11 +75,11 @@ describe('Constants', () => {
|
||||
it('should validate YesNo schemas', () => {
|
||||
const schemas = [
|
||||
ReturnProcessQuestionSchema[ReturnProcessQuestionKey.ItemDefective],
|
||||
ReturnProcessQuestionSchema[ReturnProcessQuestionKey.DisplayCondition],
|
||||
ReturnProcessQuestionSchema[ReturnProcessQuestionKey.DisplayDamaged],
|
||||
ReturnProcessQuestionSchema[ReturnProcessQuestionKey.DevicePower],
|
||||
ReturnProcessQuestionSchema[ReturnProcessQuestionKey.PackageComplete],
|
||||
ReturnProcessQuestionSchema[ReturnProcessQuestionKey.CaseCondition],
|
||||
ReturnProcessQuestionSchema[ReturnProcessQuestionKey.UsbPort],
|
||||
ReturnProcessQuestionSchema[ReturnProcessQuestionKey.CaseDamaged],
|
||||
ReturnProcessQuestionSchema[ReturnProcessQuestionKey.UsbPortDamaged],
|
||||
];
|
||||
|
||||
schemas.forEach((schema) => {
|
||||
@@ -94,7 +94,9 @@ describe('Constants', () => {
|
||||
|
||||
it('should validate PackageIncomplete schema', () => {
|
||||
const schema =
|
||||
ReturnProcessQuestionSchema[ReturnProcessQuestionKey.PackageIncomplete];
|
||||
ReturnProcessQuestionSchema[
|
||||
ReturnProcessQuestionKey.PackageMissingItems
|
||||
];
|
||||
|
||||
// Valid values - with options
|
||||
expect(
|
||||
|
||||
@@ -25,8 +25,8 @@ export type ProductCategory =
|
||||
* - Damaged: Item is damaged or has printing errors
|
||||
*/
|
||||
export const ItemConditionAnswer = {
|
||||
OVP: 'ovp',
|
||||
Damaged: 'damaged',
|
||||
OVP: 'Originalverpackt',
|
||||
Damaged: 'Geöffnet/Defekt',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
@@ -42,10 +42,10 @@ export type ItemConditionAnswer =
|
||||
* - WrongItem: Customer received the wrong item (incorrect delivery)
|
||||
*/
|
||||
export const ReturnReasonAnswer = {
|
||||
Dislike: 'dislike',
|
||||
WrongItem: 'wrong_item',
|
||||
DamagedByCustomer: 'damaged_by_customer',
|
||||
ReceivedDamaged: 'received_damaged',
|
||||
Dislike: 'Gefällt nicht/Widerruf',
|
||||
WrongItem: 'Fehllieferung',
|
||||
DamagedByCustomer: 'Beschädigt durch Kunde',
|
||||
ReceivedDamaged: 'Defekt bei Erhalt',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
@@ -60,8 +60,8 @@ export type ReturnReasonAnswer =
|
||||
* Used for boolean-type questions throughout the return process flow.
|
||||
*/
|
||||
export const YesNoAnswer = {
|
||||
Yes: 'yes',
|
||||
No: 'no',
|
||||
Yes: 'Ja',
|
||||
No: 'Nein',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
@@ -78,9 +78,9 @@ export type YesNoAnswer = (typeof YesNoAnswer)[keyof typeof YesNoAnswer];
|
||||
* - QuickStartGuide: Quick start guide documentation is missing
|
||||
*/
|
||||
export const PackageIncompleteAnswer = {
|
||||
OVP: 'ovp',
|
||||
ChargingCable: 'charging_cable',
|
||||
QuickStartGuide: 'quick_start_guide',
|
||||
OVP: 'Karton / Umverpackung',
|
||||
ChargingCable: 'Ladekabel',
|
||||
QuickStartGuide: 'Quickstart-Guide',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
@@ -120,7 +120,7 @@ export const ReturnProcessQuestionSchema = {
|
||||
YesNoAnswer.Yes,
|
||||
YesNoAnswer.No,
|
||||
]),
|
||||
[ReturnProcessQuestionKey.DisplayCondition]: z.enum([
|
||||
[ReturnProcessQuestionKey.DisplayDamaged]: z.enum([
|
||||
YesNoAnswer.Yes,
|
||||
YesNoAnswer.No,
|
||||
]),
|
||||
@@ -138,7 +138,7 @@ export const ReturnProcessQuestionSchema = {
|
||||
]),
|
||||
[ReturnProcessQuestionKey.PackageCompleteGroup]: z.any(),
|
||||
[ReturnProcessQuestionKey.PackageIncompleteInfo]: z.any(),
|
||||
[ReturnProcessQuestionKey.PackageIncomplete]: z
|
||||
[ReturnProcessQuestionKey.PackageMissingItems]: z
|
||||
.object({
|
||||
options: z
|
||||
.array(
|
||||
@@ -165,11 +165,14 @@ export const ReturnProcessQuestionSchema = {
|
||||
other: z.string(),
|
||||
}),
|
||||
),
|
||||
[ReturnProcessQuestionKey.CaseCondition]: z.enum([
|
||||
[ReturnProcessQuestionKey.CaseDamaged]: z.enum([
|
||||
YesNoAnswer.Yes,
|
||||
YesNoAnswer.No,
|
||||
]),
|
||||
[ReturnProcessQuestionKey.UsbPortDamaged]: z.enum([
|
||||
YesNoAnswer.Yes,
|
||||
YesNoAnswer.No,
|
||||
]),
|
||||
[ReturnProcessQuestionKey.UsbPort]: z.enum([YesNoAnswer.Yes, YesNoAnswer.No]),
|
||||
[ReturnProcessQuestionKey.DeliveredItem]: z
|
||||
.object({
|
||||
catalogProductNumber: z.string().optional(),
|
||||
|
||||
@@ -3,11 +3,7 @@ import {
|
||||
ReturnProcessQuestionKey,
|
||||
ReturnProcessQuestionType,
|
||||
} from '../models';
|
||||
import {
|
||||
ItemConditionAnswer,
|
||||
ReturnReasonAnswer,
|
||||
YesNoAnswer,
|
||||
} from './constants';
|
||||
import { ItemConditionAnswer, ReturnReasonAnswer } from './constants';
|
||||
|
||||
/**
|
||||
* Questions for the return process of other electronic devices.
|
||||
|
||||
185
libs/oms/data-access/src/lib/questions/tolino.spec.ts
Normal file
185
libs/oms/data-access/src/lib/questions/tolino.spec.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import {
|
||||
ReturnProcessQuestion,
|
||||
ReturnProcessQuestionKey,
|
||||
ReturnProcessQuestionType,
|
||||
} from '../models';
|
||||
import { tolinoQuestions } from './tolino';
|
||||
|
||||
describe('tolinoQuestions consistency & flow', () => {
|
||||
let questionMap: Record<string, ReturnProcessQuestion> = {};
|
||||
|
||||
beforeAll(() => {
|
||||
questionMap = {};
|
||||
for (const q of tolinoQuestions) {
|
||||
questionMap[q.key] = q;
|
||||
if (q.type === ReturnProcessQuestionType.Group) {
|
||||
for (const sub of q.questions) {
|
||||
questionMap[sub.key] = sub;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('every Select option defines a valid nextQuestion (or is terminal)', () => {
|
||||
const terminalSelectKeys = [
|
||||
ReturnProcessQuestionKey.ItemDamaged,
|
||||
ReturnProcessQuestionKey.ReturnReason,
|
||||
];
|
||||
for (const q of tolinoQuestions) {
|
||||
if (q.type === ReturnProcessQuestionType.Select) {
|
||||
for (const opt of q.options) {
|
||||
if (opt.nextQuestion == null) {
|
||||
// allowed only on the two terminal selects
|
||||
expect(terminalSelectKeys).toContain(q.key);
|
||||
} else {
|
||||
expect(questionMap[opt.nextQuestion]).toBeDefined();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('Info and Checklist questions inside groups have nextQuestion', () => {
|
||||
for (const q of tolinoQuestions) {
|
||||
if (q.type === ReturnProcessQuestionType.Group) {
|
||||
for (const sub of q.questions) {
|
||||
if (
|
||||
sub.type === ReturnProcessQuestionType.Info ||
|
||||
sub.type === ReturnProcessQuestionType.Checklist
|
||||
) {
|
||||
const next = (sub as any).nextQuestion;
|
||||
expect(next).toBeDefined();
|
||||
expect(questionMap[next]).toBeDefined();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('no cycles and all paths end at DeliveredItem', () => {
|
||||
const terminal = ReturnProcessQuestionKey.DeliveredItem;
|
||||
const terminalSelects = [
|
||||
ReturnProcessQuestionKey.ItemDamaged,
|
||||
ReturnProcessQuestionKey.ReturnReason,
|
||||
];
|
||||
|
||||
const walk = (key: ReturnProcessQuestionKey, visited: Set<string>) => {
|
||||
expect(visited.has(key)).toBeFalsy();
|
||||
visited.add(key);
|
||||
if (key === terminal) return;
|
||||
|
||||
const q = questionMap[key];
|
||||
switch (q.type) {
|
||||
case ReturnProcessQuestionType.Select:
|
||||
for (const opt of q.options) {
|
||||
if (opt.nextQuestion) {
|
||||
walk(opt.nextQuestion, new Set(visited));
|
||||
} else {
|
||||
// terminal Select must be one of the two
|
||||
expect(terminalSelects).toContain(key);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ReturnProcessQuestionType.Info:
|
||||
case ReturnProcessQuestionType.Product:
|
||||
const next = (q as any).nextQuestion;
|
||||
expect(next).toBeDefined();
|
||||
walk(next, visited);
|
||||
break;
|
||||
case ReturnProcessQuestionType.Checklist:
|
||||
walk((q as any).nextQuestion, visited);
|
||||
break;
|
||||
case ReturnProcessQuestionType.Group:
|
||||
for (const sub of q.questions) {
|
||||
walk(sub.key, new Set(visited));
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
fail(`Unhandled type ${q}`);
|
||||
}
|
||||
};
|
||||
walk(tolinoQuestions[0].key, new Set());
|
||||
});
|
||||
|
||||
it('all keys are unique', () => {
|
||||
const seen = new Set<string>();
|
||||
for (const q of tolinoQuestions) {
|
||||
expect(seen.has(q.key)).toBeFalsy();
|
||||
seen.add(q.key);
|
||||
if (q.type === ReturnProcessQuestionType.Group) {
|
||||
for (const sub of q.questions) {
|
||||
expect(seen.has(sub.key)).toBeFalsy();
|
||||
seen.add(sub.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('each Select option has returnInfo.returnDetails for its key', () => {
|
||||
for (const q of tolinoQuestions) {
|
||||
if (q.type === ReturnProcessQuestionType.Select) {
|
||||
for (const opt of q.options) {
|
||||
expect(opt.returnInfo).toBeDefined();
|
||||
expect(opt.returnInfo!.returnDetails).toHaveProperty(q.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('calculates all possible answer paths ("click dummy")', () => {
|
||||
const paths: any[] = [];
|
||||
const terminalSelects = [
|
||||
ReturnProcessQuestionKey.ItemDamaged,
|
||||
ReturnProcessQuestionKey.ReturnReason,
|
||||
];
|
||||
|
||||
const traverse = (key: ReturnProcessQuestionKey, acc: any) => {
|
||||
const q = questionMap[key];
|
||||
|
||||
if (q.type === ReturnProcessQuestionType.Select) {
|
||||
for (const opt of q.options) {
|
||||
const nextAcc = { ...acc, [key]: opt.value };
|
||||
if (opt.nextQuestion) {
|
||||
traverse(opt.nextQuestion, nextAcc);
|
||||
} else {
|
||||
// terminal branch
|
||||
expect(terminalSelects).toContain(key);
|
||||
paths.push(nextAcc);
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
q.type === ReturnProcessQuestionType.Info ||
|
||||
q.type === ReturnProcessQuestionType.Checklist
|
||||
) {
|
||||
traverse((q as any).nextQuestion, acc);
|
||||
} else if (q.type === ReturnProcessQuestionType.Group) {
|
||||
for (const sub of q.questions) {
|
||||
traverse(sub.key, { ...acc });
|
||||
}
|
||||
} else {
|
||||
// Product (DeliveredItem) terminal
|
||||
paths.push(acc);
|
||||
}
|
||||
};
|
||||
|
||||
traverse(tolinoQuestions[0].key, {});
|
||||
expect(paths.length).toBeGreaterThan(0);
|
||||
|
||||
for (const p of paths) {
|
||||
// always have the initial question answered
|
||||
expect(p).toHaveProperty(ReturnProcessQuestionKey.ItemCondition);
|
||||
|
||||
// must end in exactly one of the two terminal selects
|
||||
const hasReturnReason = Object.prototype.hasOwnProperty.call(
|
||||
p,
|
||||
ReturnProcessQuestionKey.ReturnReason,
|
||||
);
|
||||
const hasItemDamaged = Object.prototype.hasOwnProperty.call(
|
||||
p,
|
||||
ReturnProcessQuestionKey.ItemDamaged,
|
||||
);
|
||||
expect(hasReturnReason || hasItemDamaged).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -10,26 +10,6 @@ import {
|
||||
YesNoAnswer,
|
||||
} from './constants';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* The question flow for Tolino devices is more complex than other product types
|
||||
* due to the detailed assessment needed for electronic devices:
|
||||
*
|
||||
* 1. Item condition assessment (original packaging or opened/damaged)
|
||||
* 2. Device functionality verification (can it power on?)
|
||||
* 3. Package completeness check
|
||||
* 4. Detailed condition assessment (case, display, USB port)
|
||||
* 5. Return reason inquiry
|
||||
*
|
||||
* This comprehensive flow helps accurately determine return eligibility and
|
||||
* properly document the condition of returned devices for appropriate processing.
|
||||
*
|
||||
* 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[] = [
|
||||
{
|
||||
key: ReturnProcessQuestionKey.ItemCondition,
|
||||
@@ -39,13 +19,25 @@ export const tolinoQuestions: ReturnProcessQuestion[] = [
|
||||
{
|
||||
label: 'Originalverpackt',
|
||||
value: ItemConditionAnswer.OVP,
|
||||
returnInfo: { itemCondition: 'Originalverpackt' },
|
||||
nextQuestion: ReturnProcessQuestionKey.ReturnReason,
|
||||
returnInfo: {
|
||||
itemCondition: 'Originalverpackt',
|
||||
returnDetails: {
|
||||
[ReturnProcessQuestionKey.ItemCondition]: ItemConditionAnswer.OVP,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Geöffnet/Beschädigt',
|
||||
label: 'Geöffnet/Defekt',
|
||||
value: ItemConditionAnswer.Damaged,
|
||||
nextQuestion: ReturnProcessQuestionKey.DevicePower,
|
||||
returnInfo: { itemCondition: 'Geöffnet/Beschädigt' },
|
||||
returnInfo: {
|
||||
itemCondition: 'Geöffnet/Defekt',
|
||||
returnDetails: {
|
||||
[ReturnProcessQuestionKey.ItemCondition]:
|
||||
ItemConditionAnswer.Damaged,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -58,12 +50,21 @@ export const tolinoQuestions: ReturnProcessQuestion[] = [
|
||||
label: 'Ja',
|
||||
value: YesNoAnswer.Yes,
|
||||
nextQuestion: ReturnProcessQuestionKey.PackageCompleteGroup,
|
||||
returnInfo: {
|
||||
returnDetails: {
|
||||
[ReturnProcessQuestionKey.DevicePower]: YesNoAnswer.Yes,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Nein',
|
||||
value: YesNoAnswer.No,
|
||||
nextQuestion: ReturnProcessQuestionKey.PackageCompleteGroup,
|
||||
returnInfo: { returnDetails: 'Artikel defekt' },
|
||||
returnInfo: {
|
||||
returnDetails: {
|
||||
[ReturnProcessQuestionKey.DevicePower]: YesNoAnswer.No,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -79,12 +80,22 @@ export const tolinoQuestions: ReturnProcessQuestion[] = [
|
||||
{
|
||||
label: 'Ja',
|
||||
value: YesNoAnswer.Yes,
|
||||
nextQuestion: ReturnProcessQuestionKey.CaseCondition,
|
||||
nextQuestion: ReturnProcessQuestionKey.ItemDefective,
|
||||
returnInfo: {
|
||||
returnDetails: {
|
||||
[ReturnProcessQuestionKey.PackageComplete]: YesNoAnswer.Yes,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Nein',
|
||||
value: YesNoAnswer.No,
|
||||
nextQuestion: ReturnProcessQuestionKey.PackageIncompleteInfo,
|
||||
returnInfo: {
|
||||
returnDetails: {
|
||||
[ReturnProcessQuestionKey.PackageComplete]: YesNoAnswer.No,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -97,10 +108,10 @@ export const tolinoQuestions: ReturnProcessQuestion[] = [
|
||||
'2.) Ladekabel',
|
||||
'3.) Quickstart-Guide',
|
||||
],
|
||||
nextQuestion: ReturnProcessQuestionKey.PackageIncomplete,
|
||||
nextQuestion: ReturnProcessQuestionKey.PackageMissingItems,
|
||||
},
|
||||
{
|
||||
key: ReturnProcessQuestionKey.PackageIncomplete,
|
||||
key: ReturnProcessQuestionKey.PackageMissingItems,
|
||||
description: 'Was fehlt?',
|
||||
type: ReturnProcessQuestionType.Checklist,
|
||||
options: [
|
||||
@@ -120,43 +131,115 @@ export const tolinoQuestions: ReturnProcessQuestion[] = [
|
||||
other: {
|
||||
label: 'Sonstiges',
|
||||
},
|
||||
nextQuestion: ReturnProcessQuestionKey.CaseCondition,
|
||||
nextQuestion: ReturnProcessQuestionKey.ItemDefective,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: ReturnProcessQuestionKey.CaseCondition,
|
||||
key: ReturnProcessQuestionKey.ItemDefective,
|
||||
description: 'Hat das Gerät optische oder technische Mängel?',
|
||||
type: ReturnProcessQuestionType.Select,
|
||||
options: [
|
||||
{
|
||||
label: 'Ja',
|
||||
value: YesNoAnswer.Yes,
|
||||
nextQuestion: ReturnProcessQuestionKey.CaseDamaged,
|
||||
returnInfo: {
|
||||
returnDetails: {
|
||||
[ReturnProcessQuestionKey.ItemDefective]: YesNoAnswer.Yes,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Nein',
|
||||
value: YesNoAnswer.No,
|
||||
nextQuestion: ReturnProcessQuestionKey.ItemDamaged,
|
||||
returnInfo: {
|
||||
returnDetails: {
|
||||
[ReturnProcessQuestionKey.ItemDefective]: YesNoAnswer.No,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: ReturnProcessQuestionKey.CaseDamaged,
|
||||
description: 'Hat das Gehäuse Mängel oder ist zerkratzt?',
|
||||
type: ReturnProcessQuestionType.Select,
|
||||
options: [
|
||||
{
|
||||
label: 'Ja',
|
||||
value: YesNoAnswer.Yes,
|
||||
nextQuestion: ReturnProcessQuestionKey.DisplayCondition,
|
||||
returnInfo: { itemCondition: 'Gehäuse Mängel' },
|
||||
nextQuestion: ReturnProcessQuestionKey.DisplayDamaged,
|
||||
returnInfo: {
|
||||
returnDetails: {
|
||||
[ReturnProcessQuestionKey.CaseDamaged]: YesNoAnswer.Yes,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Nein',
|
||||
value: YesNoAnswer.No,
|
||||
nextQuestion: ReturnProcessQuestionKey.UsbPort,
|
||||
nextQuestion: ReturnProcessQuestionKey.DisplayDamaged,
|
||||
returnInfo: {
|
||||
returnDetails: {
|
||||
[ReturnProcessQuestionKey.CaseDamaged]: YesNoAnswer.No,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: ReturnProcessQuestionKey.DisplayCondition,
|
||||
key: ReturnProcessQuestionKey.DisplayDamaged,
|
||||
description: 'Hat das Display Kratzer oder ist gebrochen?',
|
||||
type: ReturnProcessQuestionType.Select,
|
||||
options: [
|
||||
{
|
||||
label: 'Ja',
|
||||
value: YesNoAnswer.Yes,
|
||||
nextQuestion: ReturnProcessQuestionKey.ItemDamaged,
|
||||
returnInfo: { itemCondition: 'Beschädigt' },
|
||||
nextQuestion: ReturnProcessQuestionKey.UsbPortDamaged,
|
||||
returnInfo: {
|
||||
returnDetails: {
|
||||
[ReturnProcessQuestionKey.DisplayDamaged]: YesNoAnswer.Yes,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Nein',
|
||||
value: YesNoAnswer.No,
|
||||
nextQuestion: ReturnProcessQuestionKey.UsbPort,
|
||||
nextQuestion: ReturnProcessQuestionKey.UsbPortDamaged,
|
||||
returnInfo: {
|
||||
returnDetails: {
|
||||
[ReturnProcessQuestionKey.DisplayDamaged]: YesNoAnswer.No,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: ReturnProcessQuestionKey.UsbPortDamaged,
|
||||
description: 'Funktioniert die USB Buchse?',
|
||||
type: ReturnProcessQuestionType.Select,
|
||||
options: [
|
||||
{
|
||||
label: 'Ja',
|
||||
value: YesNoAnswer.Yes,
|
||||
nextQuestion: ReturnProcessQuestionKey.ItemDamaged,
|
||||
returnInfo: {
|
||||
returnDetails: {
|
||||
[ReturnProcessQuestionKey.UsbPortDamaged]: YesNoAnswer.No,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Nein',
|
||||
value: YesNoAnswer.No,
|
||||
nextQuestion: ReturnProcessQuestionKey.ItemDamaged,
|
||||
returnInfo: {
|
||||
returnDetails: {
|
||||
[ReturnProcessQuestionKey.UsbPortDamaged]: YesNoAnswer.Yes,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -169,26 +252,24 @@ export const tolinoQuestions: ReturnProcessQuestion[] = [
|
||||
{
|
||||
label: 'Ja',
|
||||
value: ReturnReasonAnswer.DamagedByCustomer,
|
||||
returnInfo: {
|
||||
returnReason: 'Geöffnet/Defekt',
|
||||
returnDetails: {
|
||||
[ReturnProcessQuestionKey.ItemDamaged]:
|
||||
ReturnReasonAnswer.DamagedByCustomer,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Nein',
|
||||
value: ReturnReasonAnswer.ReceivedDamaged,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: ReturnProcessQuestionKey.UsbPort,
|
||||
description: 'Funktioniert die USB Buchse?',
|
||||
type: ReturnProcessQuestionType.Select,
|
||||
options: [
|
||||
{
|
||||
label: 'Ja',
|
||||
value: YesNoAnswer.Yes,
|
||||
nextQuestion: ReturnProcessQuestionKey.ReturnReason,
|
||||
},
|
||||
{
|
||||
label: 'Nein',
|
||||
value: YesNoAnswer.No,
|
||||
returnInfo: {
|
||||
returnReason: 'Geöffnet/Defekt',
|
||||
returnDetails: {
|
||||
[ReturnProcessQuestionKey.ItemDamaged]:
|
||||
ReturnReasonAnswer.ReceivedDamaged,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -200,11 +281,30 @@ export const tolinoQuestions: ReturnProcessQuestion[] = [
|
||||
{
|
||||
label: 'Gefällt nicht/Widerruf',
|
||||
value: ReturnReasonAnswer.Dislike,
|
||||
returnInfo: {
|
||||
returnReason: 'Gefällt nicht/Widerruf',
|
||||
returnDetails: {
|
||||
[ReturnProcessQuestionKey.ReturnReason]: ReturnReasonAnswer.Dislike,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Fehllieferung',
|
||||
value: ReturnReasonAnswer.WrongItem,
|
||||
nextQuestion: ReturnProcessQuestionKey.DeliveredItem,
|
||||
returnInfo: {
|
||||
returnReason: 'Fehllieferung',
|
||||
returnDetails: {
|
||||
[ReturnProcessQuestionKey.ReturnReason]:
|
||||
ReturnReasonAnswer.WrongItem,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: ReturnProcessQuestionKey.DeliveredItem,
|
||||
description: 'Welcher Artikel wurde geliefert?',
|
||||
type: ReturnProcessQuestionType.Product,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -32,14 +32,13 @@ export const tonDatentraegerQuestions: ReturnProcessQuestion[] = [
|
||||
{
|
||||
label: 'Versiegelt/Originalverpackt',
|
||||
value: ItemConditionAnswer.OVP,
|
||||
returnInfo: { itemCondition: 'Versiegelt/Originalverpackt' },
|
||||
returnInfo: { itemCondition: 'Originalverpackt' },
|
||||
nextQuestion: ReturnProcessQuestionKey.ReturnReason,
|
||||
},
|
||||
{
|
||||
label: 'Geöffnet',
|
||||
value: ItemConditionAnswer.Damaged,
|
||||
nextQuestion: ReturnProcessQuestionKey.ItemDefective,
|
||||
returnInfo: { itemCondition: 'Geöffnet' },
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -51,12 +50,18 @@ export const tonDatentraegerQuestions: ReturnProcessQuestion[] = [
|
||||
{
|
||||
label: 'Ja',
|
||||
value: YesNoAnswer.Yes,
|
||||
returnInfo: { itemCondition: 'Defekt' },
|
||||
returnInfo: {
|
||||
itemCondition: 'Geöffnet/Defekt',
|
||||
returnReason: 'Geöffnet/Defekt',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Nein',
|
||||
value: YesNoAnswer.No,
|
||||
returnInfo: { itemCondition: 'Ok' },
|
||||
returnInfo: {
|
||||
itemCondition: 'Geöffnet/Ok',
|
||||
returnReason: 'Geöffnet/Ok',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -168,12 +168,15 @@ export class ReturnProcessService {
|
||||
|
||||
switch (returnProcess.productCategory) {
|
||||
case ProductCategory.ElektronischeGeraete:
|
||||
return isElektronischeGeraeteEligibleForReturn(returnProcess);
|
||||
return isElektronischeGeraeteEligibleForReturn(
|
||||
returnProcess,
|
||||
questions,
|
||||
);
|
||||
case ProductCategory.Software:
|
||||
case ProductCategory.TonDatentraeger:
|
||||
return isTonDatentraegerEligibleForReturn(returnProcess);
|
||||
return isTonDatentraegerEligibleForReturn(returnProcess, questions);
|
||||
case ProductCategory.Tolino:
|
||||
return isTolinoEligibleForReturn(returnProcess);
|
||||
return isTolinoEligibleForReturn(returnProcess, questions);
|
||||
}
|
||||
|
||||
return { state: EligibleForReturnState.Eligible };
|
||||
@@ -193,17 +196,30 @@ export class ReturnProcessService {
|
||||
returnProcess: ReturnProcess,
|
||||
): boolean {
|
||||
return questions.every((q) => {
|
||||
if (q.type === ReturnProcessQuestionType.Checklist) {
|
||||
// Validate Checklist answers: must have options selected or 'other' text filled
|
||||
const answer = ReturnProcessChecklistAnswerSchema.optional().parse(
|
||||
returnProcess.answers[q.key],
|
||||
);
|
||||
return (
|
||||
(answer && answer.options?.length > 0) || (answer && !!answer.other)
|
||||
);
|
||||
} else {
|
||||
// For other types, simply check if an answer exists for the question key
|
||||
return q.key in returnProcess.answers;
|
||||
switch (q.type) {
|
||||
case ReturnProcessQuestionType.Checklist: {
|
||||
// Validate Checklist answers: must have options selected or 'other' text filled
|
||||
const answer = ReturnProcessChecklistAnswerSchema.optional().parse(
|
||||
returnProcess.answers[q.key],
|
||||
);
|
||||
return Boolean(
|
||||
answer && (answer.options?.length > 0 || answer.other),
|
||||
);
|
||||
}
|
||||
|
||||
case ReturnProcessQuestionType.Info: {
|
||||
return true; // Info questions are always considered answered
|
||||
}
|
||||
|
||||
case ReturnProcessQuestionType.Group: {
|
||||
// Group: nicht q.key selbst, sondern alle Unter-Fragen prüfen
|
||||
return this._areAllQuestionsAnswered(q.questions, returnProcess);
|
||||
}
|
||||
|
||||
default: {
|
||||
// For other types, simply check if an answer exists for the question key
|
||||
return q.key in returnProcess.answers;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// TODO: Unit Tests wieder reinnehmen wenn printReceiptsService nicht mehr auf die Alte ISA App zugreift durch den alten ModalService
|
||||
|
||||
// import { byText } from '@ngneat/spectator';
|
||||
// import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
|
||||
// import { MockDirective } from 'ng-mocks';
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// TODO: Unit Tests wieder reinnehmen wenn printReceiptsService nicht mehr auf die Alte ISA App zugreift durch den alten ModalService
|
||||
|
||||
// import { signal } from '@angular/core';
|
||||
// import { Location } from '@angular/common';
|
||||
// import { RouterLink } from '@angular/router';
|
||||
|
||||
@@ -1,177 +1,180 @@
|
||||
import { createRoutingFactory, Spectator } from '@ngneat/spectator/jest';
|
||||
import { ReturnSummaryItemComponent } from './return-summary-item.component';
|
||||
import { MockComponents, MockProvider } from 'ng-mocks';
|
||||
import { ReturnProductInfoComponent } from '@isa/oms/shared/product-info';
|
||||
import {
|
||||
Product,
|
||||
ReturnProcess,
|
||||
ReturnProcessService,
|
||||
} from '@isa/oms/data-access';
|
||||
import { NgIcon } from '@ng-icons/core';
|
||||
import { IconButtonComponent } from '@isa/ui/buttons';
|
||||
import { Router } from '@angular/router';
|
||||
// TODO: Unit Tests wieder reinnehmen wenn printReceiptsService nicht mehr auf die Alte ISA App zugreift durch den alten ModalService
|
||||
|
||||
/**
|
||||
* Creates a mock ReturnProcess with default values that can be overridden
|
||||
*/
|
||||
function createMockReturnProcess(
|
||||
partial: Partial<ReturnProcess>,
|
||||
): ReturnProcess {
|
||||
return {
|
||||
id: 1,
|
||||
processId: 1,
|
||||
productCategory: 'Electronics',
|
||||
answers: {},
|
||||
receiptId: 123,
|
||||
receiptItem: {
|
||||
id: 321,
|
||||
product: {
|
||||
name: 'Test Product',
|
||||
},
|
||||
},
|
||||
...partial,
|
||||
} as ReturnProcess;
|
||||
}
|
||||
// import { createRoutingFactory, Spectator } from '@ngneat/spectator/jest';
|
||||
// import { ReturnSummaryItemComponent } from './return-summary-item.component';
|
||||
// import { MockComponents, MockProvider } from 'ng-mocks';
|
||||
// import { ReturnProductInfoComponent } from '@isa/oms/shared/product-info';
|
||||
// import {
|
||||
// Product,
|
||||
// ReturnProcess,
|
||||
// ReturnProcessQuestionKey,
|
||||
// ReturnProcessService,
|
||||
// } from '@isa/oms/data-access';
|
||||
// import { NgIcon } from '@ng-icons/core';
|
||||
// import { IconButtonComponent } from '@isa/ui/buttons';
|
||||
// import { Router } from '@angular/router';
|
||||
|
||||
describe('ReturnSummaryItemComponent', () => {
|
||||
let spectator: Spectator<ReturnSummaryItemComponent>;
|
||||
let returnProcessService: jest.Mocked<ReturnProcessService>;
|
||||
// /**
|
||||
// * Creates a mock ReturnProcess with default values that can be overridden
|
||||
// */
|
||||
// function createMockReturnProcess(
|
||||
// partial: Partial<ReturnProcess>,
|
||||
// ): ReturnProcess {
|
||||
// return {
|
||||
// id: 1,
|
||||
// processId: 1,
|
||||
// productCategory: 'Electronics',
|
||||
// answers: {},
|
||||
// receiptId: 123,
|
||||
// receiptItem: {
|
||||
// id: 321,
|
||||
// product: {
|
||||
// name: 'Test Product',
|
||||
// },
|
||||
// },
|
||||
// ...partial,
|
||||
// } as ReturnProcess;
|
||||
// }
|
||||
|
||||
const createComponent = createRoutingFactory({
|
||||
component: ReturnSummaryItemComponent,
|
||||
declarations: MockComponents(
|
||||
ReturnProductInfoComponent,
|
||||
NgIcon,
|
||||
IconButtonComponent,
|
||||
),
|
||||
providers: [
|
||||
MockProvider(ReturnProcessService, { getReturnInfo: jest.fn() }),
|
||||
],
|
||||
shallow: true,
|
||||
});
|
||||
// describe('ReturnSummaryItemComponent', () => {
|
||||
// let spectator: Spectator<ReturnSummaryItemComponent>;
|
||||
// let returnProcessService: jest.Mocked<ReturnProcessService>;
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createComponent({
|
||||
props: {
|
||||
returnProcess: createMockReturnProcess({}),
|
||||
},
|
||||
});
|
||||
returnProcessService = spectator.inject(ReturnProcessService);
|
||||
spectator.detectChanges();
|
||||
});
|
||||
// const createComponent = createRoutingFactory({
|
||||
// component: ReturnSummaryItemComponent,
|
||||
// declarations: MockComponents(
|
||||
// ReturnProductInfoComponent,
|
||||
// NgIcon,
|
||||
// IconButtonComponent,
|
||||
// ),
|
||||
// providers: [
|
||||
// MockProvider(ReturnProcessService, { getReturnInfo: jest.fn() }),
|
||||
// ],
|
||||
// shallow: true,
|
||||
// });
|
||||
|
||||
describe('Component Creation', () => {
|
||||
it('should create the component', () => {
|
||||
expect(spectator.component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
// beforeEach(() => {
|
||||
// spectator = createComponent({
|
||||
// props: {
|
||||
// returnProcess: createMockReturnProcess({}),
|
||||
// },
|
||||
// });
|
||||
// returnProcessService = spectator.inject(ReturnProcessService);
|
||||
// spectator.detectChanges();
|
||||
// });
|
||||
|
||||
describe('Return Information Display', () => {
|
||||
const mockReturnInfo = {
|
||||
itemCondition: 'itemCondition',
|
||||
comment: 'comment',
|
||||
returnDetails: 'returnDetails',
|
||||
returnReason: 'returnReason',
|
||||
otherProduct: {
|
||||
ean: 'ean',
|
||||
} as Product,
|
||||
};
|
||||
// describe('Component Creation', () => {
|
||||
// it('should create the component', () => {
|
||||
// expect(spectator.component).toBeTruthy();
|
||||
// });
|
||||
// });
|
||||
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(returnProcessService, 'getReturnInfo')
|
||||
.mockReturnValue(mockReturnInfo);
|
||||
spectator.setInput('returnProcess', createMockReturnProcess({ id: 2 }));
|
||||
spectator.detectChanges();
|
||||
});
|
||||
// describe('Return Information Display', () => {
|
||||
// const mockReturnInfo = {
|
||||
// itemCondition: 'itemCondition',
|
||||
// comment: 'comment',
|
||||
// returnDetails: { [ReturnProcessQuestionKey.CaseDamaged]: 'no' },
|
||||
// returnReason: 'returnReason',
|
||||
// otherProduct: {
|
||||
// ean: 'ean',
|
||||
// } as Product,
|
||||
// };
|
||||
|
||||
it('should provide correct return information array', () => {
|
||||
// Arrange
|
||||
const expectedInfos = [
|
||||
'itemCondition',
|
||||
'returnReason',
|
||||
'returnDetails',
|
||||
'comment',
|
||||
'Geliefert wurde: ean',
|
||||
];
|
||||
// beforeEach(() => {
|
||||
// jest
|
||||
// .spyOn(returnProcessService, 'getReturnInfo')
|
||||
// .mockReturnValue(mockReturnInfo);
|
||||
// spectator.setInput('returnProcess', createMockReturnProcess({ id: 2 }));
|
||||
// spectator.detectChanges();
|
||||
// });
|
||||
|
||||
// Act
|
||||
const actualInfos = spectator.component.returnInfos();
|
||||
// it('should provide correct return information array', () => {
|
||||
// // Arrange
|
||||
// const expectedInfos = [
|
||||
// 'itemCondition',
|
||||
// 'returnReason',
|
||||
// 'returnDetails',
|
||||
// 'comment',
|
||||
// 'Geliefert wurde: ean',
|
||||
// ];
|
||||
|
||||
// Assert
|
||||
expect(actualInfos).toEqual(expectedInfos);
|
||||
expect(actualInfos.length).toBe(5);
|
||||
});
|
||||
// // Act
|
||||
// const actualInfos = spectator.component.returnInfos();
|
||||
|
||||
it('should render return info items with correct content', () => {
|
||||
// Arrange
|
||||
const expectedInfos = [
|
||||
'itemCondition',
|
||||
'returnReason',
|
||||
'returnDetails',
|
||||
'comment',
|
||||
'Geliefert wurde: ean',
|
||||
];
|
||||
// // Assert
|
||||
// expect(actualInfos).toEqual(expectedInfos);
|
||||
// expect(actualInfos.length).toBe(5);
|
||||
// });
|
||||
|
||||
// Act
|
||||
spectator.detectComponentChanges();
|
||||
const listItems = spectator.queryAll(
|
||||
'[data-what="list-item"][data-which="return-info"]',
|
||||
);
|
||||
// it('should render return info items with correct content', () => {
|
||||
// // Arrange
|
||||
// const expectedInfos = [
|
||||
// 'itemCondition',
|
||||
// 'returnReason',
|
||||
// 'returnDetails',
|
||||
// 'comment',
|
||||
// 'Geliefert wurde: ean',
|
||||
// ];
|
||||
|
||||
// Assert
|
||||
expect(listItems.length).toBe(expectedInfos.length);
|
||||
listItems.forEach((item, index) => {
|
||||
expect(item).toHaveText(expectedInfos[index]);
|
||||
expect(item).toHaveAttribute('data-info-index', index.toString());
|
||||
});
|
||||
});
|
||||
// // Act
|
||||
// spectator.detectComponentChanges();
|
||||
// const listItems = spectator.queryAll(
|
||||
// '[data-what="list-item"][data-which="return-info"]',
|
||||
// );
|
||||
|
||||
it('should handle undefined return info gracefully', () => {
|
||||
// Arrange
|
||||
returnProcessService.getReturnInfo.mockReturnValue(undefined);
|
||||
spectator.setInput('returnProcess', createMockReturnProcess({ id: 3 }));
|
||||
spectator.detectChanges();
|
||||
// // Assert
|
||||
// expect(listItems.length).toBe(expectedInfos.length);
|
||||
// listItems.forEach((item, index) => {
|
||||
// expect(item).toHaveText(expectedInfos[index]);
|
||||
// expect(item).toHaveAttribute('data-info-index', index.toString());
|
||||
// });
|
||||
// });
|
||||
|
||||
// Act
|
||||
const infos = spectator.component.returnInfos();
|
||||
// it('should handle undefined return info gracefully', () => {
|
||||
// // Arrange
|
||||
// returnProcessService.getReturnInfo.mockReturnValue(undefined);
|
||||
// spectator.setInput('returnProcess', createMockReturnProcess({ id: 3 }));
|
||||
// spectator.detectChanges();
|
||||
|
||||
// Assert
|
||||
expect(infos).toEqual([]);
|
||||
const listItems = spectator.queryAll(
|
||||
'[data-what="list-item"][data-which="return-info"]',
|
||||
);
|
||||
expect(listItems.length).toBe(0);
|
||||
});
|
||||
});
|
||||
// // Act
|
||||
// const infos = spectator.component.returnInfos();
|
||||
|
||||
describe('Navigation', () => {
|
||||
it('should render edit button with correct attributes', () => {
|
||||
// Assert
|
||||
const editButton = spectator.query(
|
||||
'[data-what="button"][data-which="edit-return-item"]',
|
||||
);
|
||||
expect(editButton).toExist();
|
||||
});
|
||||
// // Assert
|
||||
// expect(infos).toEqual([]);
|
||||
// const listItems = spectator.queryAll(
|
||||
// '[data-what="list-item"][data-which="return-info"]',
|
||||
// );
|
||||
// expect(listItems.length).toBe(0);
|
||||
// });
|
||||
// });
|
||||
|
||||
it('should navigate back when edit button is clicked', () => {
|
||||
// Arrange
|
||||
const editButton = spectator.query(
|
||||
'[data-what="button"][data-which="edit-return-item"]',
|
||||
);
|
||||
// describe('Navigation', () => {
|
||||
// it('should render edit button with correct attributes', () => {
|
||||
// // Assert
|
||||
// const editButton = spectator.query(
|
||||
// '[data-what="button"][data-which="edit-return-item"]',
|
||||
// );
|
||||
// expect(editButton).toExist();
|
||||
// });
|
||||
|
||||
// Act
|
||||
if (editButton) {
|
||||
spectator.click(editButton);
|
||||
}
|
||||
// it('should navigate back when edit button is clicked', () => {
|
||||
// // Arrange
|
||||
// const editButton = spectator.query(
|
||||
// '[data-what="button"][data-which="edit-return-item"]',
|
||||
// );
|
||||
|
||||
// Assert
|
||||
expect(spectator.inject(Router).navigate).toHaveBeenCalledWith(
|
||||
['..'],
|
||||
expect.objectContaining({
|
||||
relativeTo: expect.anything(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
// // Act
|
||||
// if (editButton) {
|
||||
// spectator.click(editButton);
|
||||
// }
|
||||
|
||||
// // Assert
|
||||
// expect(spectator.inject(Router).navigate).toHaveBeenCalledWith(
|
||||
// ['..'],
|
||||
// expect.objectContaining({
|
||||
// relativeTo: expect.anything(),
|
||||
// }),
|
||||
// );
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
EligibleForReturn,
|
||||
EligibleForReturnState,
|
||||
ReturnProcess,
|
||||
ReturnProcessQuestionKey,
|
||||
ReturnProcessService,
|
||||
} from '@isa/oms/data-access';
|
||||
import { ReturnProductInfoComponent } from '@isa/oms/shared/product-info';
|
||||
@@ -86,17 +87,10 @@ export class ReturnSummaryItemComponent {
|
||||
});
|
||||
|
||||
/**
|
||||
* Computes a list of formatted information strings related to the return process.
|
||||
* Computed array of info strings from the current return process.
|
||||
*
|
||||
* This computed signal processes the current return process data and extracts
|
||||
* relevant information to display to the user, including:
|
||||
* - Item condition (if available)
|
||||
* - Return reason (if available)
|
||||
* - Additional return details (if available)
|
||||
* - Customer comments (if available)
|
||||
* - Information about product mismatches (if a different product was delivered)
|
||||
*
|
||||
* @returns An array of string descriptions ready to be displayed in the UI
|
||||
* Gathers top-level fields (itemCondition, returnReason, comment, otherProduct.ean)
|
||||
* plus all entries from `returnDetails`, then deduplicates.
|
||||
*/
|
||||
returnInfos = computed<string[]>(() => {
|
||||
const returnProcess = this.returnProcess();
|
||||
@@ -112,19 +106,17 @@ export class ReturnSummaryItemComponent {
|
||||
comment,
|
||||
itemCondition,
|
||||
otherProduct,
|
||||
returnDetails,
|
||||
returnReason,
|
||||
returnDetails,
|
||||
} = returnInfo;
|
||||
|
||||
if (itemCondition) {
|
||||
// push basic fields if present
|
||||
if (itemCondition && returnProcess.productCategory !== 'Tolino') {
|
||||
result.push(itemCondition);
|
||||
}
|
||||
if (returnReason) {
|
||||
if (returnReason && returnProcess.productCategory !== 'Tolino') {
|
||||
result.push(returnReason);
|
||||
}
|
||||
if (returnDetails) {
|
||||
result.push(returnDetails);
|
||||
}
|
||||
if (comment) {
|
||||
result.push(comment);
|
||||
}
|
||||
@@ -132,10 +124,43 @@ export class ReturnSummaryItemComponent {
|
||||
result.push(`Geliefert wurde: ${otherProduct.ean}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
// append formatted returnDetails entries
|
||||
if (returnDetails) {
|
||||
result.push(...this._mapReturnDetails(returnDetails));
|
||||
}
|
||||
|
||||
// remove duplicates while preserving order
|
||||
return Array.from(new Set(result));
|
||||
});
|
||||
|
||||
navigateBack() {
|
||||
this.#router.navigate(['..'], { relativeTo: this.#activatedRoute });
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a `returnDetails` object into an array of formatted "key: value" strings.
|
||||
*
|
||||
* @param returnDetails - Partial record of question keys to arbitrary values.
|
||||
* @returns An array where each entry is formatted as "key: value", with arrays joined by ", ".
|
||||
*/
|
||||
private _mapReturnDetails(
|
||||
returnDetails: Partial<Record<ReturnProcessQuestionKey, unknown>>,
|
||||
): string[] {
|
||||
const entries = Object.entries(returnDetails) as [
|
||||
ReturnProcessQuestionKey,
|
||||
unknown,
|
||||
][];
|
||||
const mapped: string[] = [];
|
||||
|
||||
for (const [key, value] of entries) {
|
||||
// flatten arrays into comma-separated strings, or just stringify other types
|
||||
const valString = Array.isArray(value)
|
||||
? value.map((v) => String(v)).join(', ')
|
||||
: String(value);
|
||||
|
||||
mapped.push(`${key}: ${valString}`);
|
||||
}
|
||||
|
||||
return mapped;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// TODO: Unit Tests wieder reinnehmen wenn printReceiptsService nicht mehr auf die Alte ISA App zugreift durch den alten ModalService
|
||||
|
||||
// import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
|
||||
// import { MockComponent } from 'ng-mocks';
|
||||
// import { ReturnSummaryComponent } from './return-summary.component';
|
||||
|
||||
Reference in New Issue
Block a user