Merge branch 'develop' into feature/5047-5053-Design-und-Funktionsweise-Drucker-Dialog

This commit is contained in:
Lorenz Hilpert
2025-05-21 14:39:05 +02:00
21 changed files with 811 additions and 320 deletions

View File

@@ -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 } {

View File

@@ -108,7 +108,7 @@ describe('internalCalculateLongestQuestionDepth', () => {
[ReturnProcessQuestionKey.ItemCondition]: ItemConditionAnswer.Damaged,
};
const expectedDepth = 8;
const expectedDepth = 9;
const result = helpers.calculateLongestQuestionDepth(
tolinoQuestions,

View File

@@ -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 };
};

View File

@@ -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);
}

View File

@@ -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';

View File

@@ -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(';');
};

View File

@@ -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: {

View File

@@ -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

View File

@@ -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 =

View File

@@ -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(

View File

@@ -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(),

View File

@@ -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.

View 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();
}
});
});

View File

@@ -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,
},
];

View File

@@ -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',
},
},
],
},

View File

@@ -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;
}
}
});
}

View File

@@ -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';

View File

@@ -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';

View File

@@ -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(),
// }),
// );
// });
// });
// });

View File

@@ -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;
}
}

View File

@@ -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';