feat: Implement return process questions and validators for Tolino categories

- Added questions and validation logic for the Tolino return process.
- Introduced Ton-/Datenträger return process questions and validation.
- Created a registry to map product categories to their respective questions and validators.
- Developed unit tests for the new return process questions and validators.
- Removed deprecated return process questions and validators to streamline the codebase.
This commit is contained in:
Lorenz Hilpert
2025-04-11 19:07:26 +02:00
parent afff1ea8fd
commit 4885a523ab
21 changed files with 1210 additions and 553 deletions

View File

@@ -3,6 +3,11 @@ export const ReturnProcessQuestionKey = {
ReturnReason: 'return_reason',
DeliveredItem: 'delivered_item',
ItemDefective: 'item_defective',
DisplayCondition: 'display_condition',
DevicePower: 'device_power',
PackageComplete: 'package_complete',
CaseCondition: 'case_condition',
UsbPort: 'usb_port',
} as const;
export type ReturnProcessQuestionKey =

View File

@@ -0,0 +1,73 @@
import {
validateBookCalendarQuestions,
bookCalendarQuestions,
} from './book-calendar';
import { EligibleForReturnState, ReturnProcessQuestionKey } from '../models';
import { ProductCategory } from './constants';
import { CategoryQuestions, CategoryQuestionValidators } from './registry';
describe('Book Calendar Return Process', () => {
describe('validateBookCalendarQuestions', () => {
it('should return Eligible when item is ovp and return reason is dislike', () => {
const answers = {
[ReturnProcessQuestionKey.ItemCondition]: 'ovp',
[ReturnProcessQuestionKey.ReturnReason]: 'dislike',
};
const result = validateBookCalendarQuestions(answers);
expect(result).toEqual({ state: EligibleForReturnState.Eligible });
});
it('should return Eligible when item is ovp, return reason is wrong_item and delivered item is provided', () => {
const answers = {
[ReturnProcessQuestionKey.ItemCondition]: 'ovp',
[ReturnProcessQuestionKey.ReturnReason]: 'wrong_item',
[ReturnProcessQuestionKey.DeliveredItem]: 'someProduct',
};
const result = validateBookCalendarQuestions(answers);
expect(result).toEqual({ state: EligibleForReturnState.Eligible });
});
it('should return Pending when item is ovp, return reason is wrong_item but delivered item is missing', () => {
const answers = {
[ReturnProcessQuestionKey.ItemCondition]: 'ovp',
[ReturnProcessQuestionKey.ReturnReason]: 'wrong_item',
};
const result = validateBookCalendarQuestions(answers);
expect(result).toEqual({
state: EligibleForReturnState.Pending,
reason: 'Missing delivered item',
});
});
it('should return Eligible when item is damaged', () => {
const answers = {
[ReturnProcessQuestionKey.ItemCondition]: 'damaged',
};
const result = validateBookCalendarQuestions(answers);
expect(result).toEqual({ state: EligibleForReturnState.Eligible });
});
it('should return NotEligible for any invalid answers', () => {
const answers = {};
const result = validateBookCalendarQuestions(answers);
expect(result).toEqual({
state: EligibleForReturnState.NotEligible,
reason: 'Invalid answers',
});
});
});
describe('Registry mappings', () => {
it('should map BookCalendar category to book calendar questions', () => {
expect(CategoryQuestions[ProductCategory.BookCalendar]).toBe(
bookCalendarQuestions,
);
});
it('should map BookCalendar category to book calendar validator', () => {
expect(CategoryQuestionValidators[ProductCategory.BookCalendar]).toBe(
validateBookCalendarQuestions,
);
});
});
});

View File

@@ -0,0 +1,72 @@
import {
ReturnProcessAnswers,
ReturnProcessQuestion,
ReturnProcessQuestionKey,
ReturnProcessQuestionType,
EligibleForReturn,
EligibleForReturnState,
} from '../models';
import { ItemConditionValue, ReturnReasonValue } from './constants';
import { validateOvpReturn } from './validators';
/**
* Questions for the return process of books and calendars.
*/
export const bookCalendarQuestions: ReturnProcessQuestion[] = [
{
key: ReturnProcessQuestionKey.ItemCondition,
description: 'Wie ist der Artikelzustand?',
type: ReturnProcessQuestionType.Select,
options: [
{
label: 'Neuwertig/Originalverpackt',
value: ItemConditionValue.OriginalPackaging,
nextQuestion: ReturnProcessQuestionKey.ReturnReason,
},
{ label: 'Beschädigt/Fehldruck', value: ItemConditionValue.Damaged },
],
},
{
key: ReturnProcessQuestionKey.ReturnReason,
description: 'Warum möchtest du den Artikel zurücksenden?',
type: ReturnProcessQuestionType.Select,
options: [
{ label: 'Gefällt nicht/Wiederruf', value: ReturnReasonValue.Dislike },
{
label: 'Fehllieferung',
value: ReturnReasonValue.WrongItem,
nextQuestion: ReturnProcessQuestionKey.DeliveredItem,
},
],
},
{
key: ReturnProcessQuestionKey.DeliveredItem,
description: 'Welcher Artikel wurde geliefert?',
type: ReturnProcessQuestionType.Product,
},
];
/**
* Validates the answers for the book and calendar return process.
* @param answers - A record of answers keyed by question keys.
* @returns The eligibility state for the return process.
*/
export function validateBookCalendarQuestions(
answers: ReturnProcessAnswers,
): EligibleForReturn {
if (
answers[ReturnProcessQuestionKey.ItemCondition] ===
ItemConditionValue.OriginalPackaging
) {
return validateOvpReturn(answers);
} else if (
answers[ReturnProcessQuestionKey.ItemCondition] ===
ItemConditionValue.Damaged
) {
return { state: EligibleForReturnState.Eligible };
}
return {
state: EligibleForReturnState.NotEligible,
reason: 'Invalid answers',
};
}

View File

@@ -0,0 +1,35 @@
/**
* Constants for product categories used in the return process.
*/
export const enum ProductCategory {
BookCalendar = 'Buch/Kalender',
TonDatentraeger = 'Ton-/Datenträger',
SpielwarenPuzzle = 'Spielwaren/Puzzle',
SonstigesNonbook = 'Sonstiges und Nonbook',
ElektronischeGeraete = 'Andere Elektronische Geräte',
Tolino = 'Tolino', // Added Tolino category
}
/**
* Constants for item condition values.
*/
export const enum ItemConditionValue {
OriginalPackaging = 'ovp',
Damaged = 'damaged',
}
/**
* Constants for return reason values.
*/
export const enum ReturnReasonValue {
Dislike = 'dislike',
WrongItem = 'wrong_item',
}
/**
* Constants for item defective values.
*/
export const enum ItemDefectiveValue {
Yes = 'yes',
No = 'no',
}

View File

@@ -0,0 +1,86 @@
import {
validateElektronischeGeraeteQuestions,
elektronischeGeraeteQuestions,
} from './elektronische-geraete';
import { EligibleForReturnState, ReturnProcessQuestionKey } from '../models';
import { ProductCategory } from './constants';
import { CategoryQuestions, CategoryQuestionValidators } from './registry';
describe('Elektronische Geräte Return Process', () => {
describe('validateElektronischeGeraeteQuestions', () => {
it('should return Eligible when item is ovp and return reason is dislike', () => {
const answers = {
[ReturnProcessQuestionKey.ItemCondition]: 'ovp',
[ReturnProcessQuestionKey.ReturnReason]: 'dislike',
};
const result = validateElektronischeGeraeteQuestions(answers);
expect(result).toEqual({ state: EligibleForReturnState.Eligible });
});
it('should return Eligible when item is ovp, return reason is wrong_item and delivered item provided', () => {
const answers = {
[ReturnProcessQuestionKey.ItemCondition]: 'ovp',
[ReturnProcessQuestionKey.ReturnReason]: 'wrong_item',
[ReturnProcessQuestionKey.DeliveredItem]: 'item123',
};
const result = validateElektronischeGeraeteQuestions(answers);
expect(result).toEqual({ state: EligibleForReturnState.Eligible });
});
it('should return Pending when delivered item is missing for wrong_item reason', () => {
const answers = {
[ReturnProcessQuestionKey.ItemCondition]: 'ovp',
[ReturnProcessQuestionKey.ReturnReason]: 'wrong_item',
};
const result = validateElektronischeGeraeteQuestions(answers);
expect(result).toEqual({
state: EligibleForReturnState.Pending,
reason: 'Missing delivered item',
});
});
it('should return Eligible when damaged and item is defective', () => {
const answers = {
[ReturnProcessQuestionKey.ItemCondition]: 'damaged',
[ReturnProcessQuestionKey.ItemDefective]: 'yes',
};
const result = validateElektronischeGeraeteQuestions(answers);
expect(result).toEqual({ state: EligibleForReturnState.Eligible });
});
it('should return NotEligible when damaged and item is not defective', () => {
const answers = {
[ReturnProcessQuestionKey.ItemCondition]: 'damaged',
[ReturnProcessQuestionKey.ItemDefective]: 'no',
};
const result = validateElektronischeGeraeteQuestions(answers);
expect(result).toEqual({
state: EligibleForReturnState.NotEligible,
reason: 'Item not defective',
});
});
it('should return NotEligible for invalid answers', () => {
const answers = {};
const result = validateElektronischeGeraeteQuestions(answers);
expect(result).toEqual({
state: EligibleForReturnState.NotEligible,
reason: 'Invalid answers',
});
});
});
describe('Registry mappings', () => {
it('should map ElektronischeGeraete category to elektronische-geraete questions', () => {
expect(CategoryQuestions[ProductCategory.ElektronischeGeraete]).toBe(
elektronischeGeraeteQuestions,
);
});
it('should map ElektronischeGeraete category to elektronische-geraete validator', () => {
expect(
CategoryQuestionValidators[ProductCategory.ElektronischeGeraete],
).toBe(validateElektronischeGeraeteQuestions);
});
});
});

View File

@@ -0,0 +1,106 @@
import {
ReturnProcessAnswers,
ReturnProcessQuestion,
ReturnProcessQuestionKey,
ReturnProcessQuestionType,
EligibleForReturn,
EligibleForReturnState,
} from '../models';
import {
ItemConditionValue,
ReturnReasonValue,
ItemDefectiveValue,
} from './constants';
import { validateOvpReturn } from './validators';
/**
* Questions for the return process of other electronic devices.
*/
export const elektronischeGeraeteQuestions: ReturnProcessQuestion[] = [
{
key: ReturnProcessQuestionKey.ItemCondition,
description: 'Wie ist der Artikelzustand?',
type: ReturnProcessQuestionType.Select,
options: [
{
label: 'Originalverpackt',
value: ItemConditionValue.OriginalPackaging,
nextQuestion: ReturnProcessQuestionKey.ReturnReason,
},
{
label: 'Geöffnet / Beschädigt',
value: ItemConditionValue.Damaged,
nextQuestion: ReturnProcessQuestionKey.ItemDefective,
},
],
},
{
key: ReturnProcessQuestionKey.ReturnReason,
description: 'Warum möchtest du den Artikel zurückgeben?',
type: ReturnProcessQuestionType.Select,
options: [
{ label: 'Gefällt nicht/Wiederruf', value: ReturnReasonValue.Dislike },
{
label: 'Fehllieferung',
value: ReturnReasonValue.WrongItem,
nextQuestion: ReturnProcessQuestionKey.DeliveredItem,
},
],
},
{
key: ReturnProcessQuestionKey.ItemDefective,
description: 'Ist der Artikel defekt?',
type: ReturnProcessQuestionType.Select,
options: [
{
label: 'Ja',
value: ItemDefectiveValue.Yes,
},
{
label: 'Nein',
value: ItemDefectiveValue.No,
},
],
},
{
key: ReturnProcessQuestionKey.DeliveredItem,
description: 'Welcher Artikel wurde geliefert?',
type: ReturnProcessQuestionType.Product,
},
];
/**
* Validates the answers for the return process of other electronic devices.
* @param answers - A record of answers keyed by question keys.
* @returns The eligibility state for the return process.
*/
export function validateElektronischeGeraeteQuestions(
answers: ReturnProcessAnswers,
): EligibleForReturn {
if (
answers[ReturnProcessQuestionKey.ItemCondition] ===
ItemConditionValue.OriginalPackaging
) {
return validateOvpReturn(answers);
} else if (
answers[ReturnProcessQuestionKey.ItemCondition] ===
ItemConditionValue.Damaged
) {
if (
answers[ReturnProcessQuestionKey.ItemDefective] === ItemDefectiveValue.Yes
) {
return { state: EligibleForReturnState.Eligible };
} else if (
answers[ReturnProcessQuestionKey.ItemDefective] === ItemDefectiveValue.No
) {
return {
state: EligibleForReturnState.NotEligible,
reason: 'Item not defective',
};
}
}
return {
state: EligibleForReturnState.NotEligible,
reason: 'Invalid answers',
};
}

View File

@@ -0,0 +1,17 @@
// Export constants
export * from './constants';
// Export types
export * from './types';
// Export validators
export * from './validators';
// Export category-specific questions and validators
export * from './book-calendar';
export * from './ton-datentraeger';
export * from './nonbook';
export * from './elektronische-geraete';
// Export registry
export * from './registry';

View File

@@ -0,0 +1,82 @@
import { validateNonbookQuestions, nonbookQuestions } from './nonbook';
import { EligibleForReturnState, ReturnProcessQuestionKey } from '../models';
import { ProductCategory } from './constants';
import { CategoryQuestions, CategoryQuestionValidators } from './registry';
describe('Nonbook Return Process', () => {
describe('validateNonbookQuestions', () => {
it('should return Eligible when item is ovp and return reason is dislike', () => {
const answers = {
[ReturnProcessQuestionKey.ItemCondition]: 'ovp',
[ReturnProcessQuestionKey.ReturnReason]: 'dislike',
};
const result = validateNonbookQuestions(answers);
expect(result).toEqual({ state: EligibleForReturnState.Eligible });
});
it('should return Eligible when item is ovp, return reason is wrong_item and delivered item is provided', () => {
const answers = {
[ReturnProcessQuestionKey.ItemCondition]: 'ovp',
[ReturnProcessQuestionKey.ReturnReason]: 'wrong_item',
[ReturnProcessQuestionKey.DeliveredItem]: 'product',
};
const result = validateNonbookQuestions(answers);
expect(result).toEqual({ state: EligibleForReturnState.Eligible });
});
it('should return Pending when delivered item is missing for wrong_item reason', () => {
const answers = {
[ReturnProcessQuestionKey.ItemCondition]: 'ovp',
[ReturnProcessQuestionKey.ReturnReason]: 'wrong_item',
};
const result = validateNonbookQuestions(answers);
expect(result).toEqual({
state: EligibleForReturnState.Pending,
reason: 'Missing delivered item',
});
});
it('should return Eligible when item is damaged', () => {
const answers = {
[ReturnProcessQuestionKey.ItemCondition]: 'damaged',
};
const result = validateNonbookQuestions(answers);
expect(result).toEqual({ state: EligibleForReturnState.Eligible });
});
it('should return NotEligible for invalid answers', () => {
const answers = {};
const result = validateNonbookQuestions(answers);
expect(result).toEqual({
state: EligibleForReturnState.NotEligible,
reason: 'Invalid answers',
});
});
});
describe('Registry mappings', () => {
it('should map SpielwarenPuzzle category to nonbook questions', () => {
expect(CategoryQuestions[ProductCategory.SpielwarenPuzzle]).toBe(
nonbookQuestions,
);
});
it('should map SonstigesNonbook category to nonbook questions', () => {
expect(CategoryQuestions[ProductCategory.SonstigesNonbook]).toBe(
nonbookQuestions,
);
});
it('should map SpielwarenPuzzle category to nonbook validator', () => {
expect(CategoryQuestionValidators[ProductCategory.SpielwarenPuzzle]).toBe(
validateNonbookQuestions,
);
});
it('should map SonstigesNonbook category to nonbook validator', () => {
expect(CategoryQuestionValidators[ProductCategory.SonstigesNonbook]).toBe(
validateNonbookQuestions,
);
});
});
});

View File

@@ -0,0 +1,72 @@
import {
ReturnProcessAnswers,
ReturnProcessQuestion,
ReturnProcessQuestionKey,
ReturnProcessQuestionType,
EligibleForReturn,
EligibleForReturnState,
} from '../models';
import { ItemConditionValue, ReturnReasonValue } from './constants';
import { validateOvpReturn } from './validators';
/**
* Questions for the return process of Spielwaren/Puzzle and other nonbook items.
*/
export const nonbookQuestions: ReturnProcessQuestion[] = [
{
key: ReturnProcessQuestionKey.ItemCondition,
description: 'Wie ist der Artikelzustand?',
type: ReturnProcessQuestionType.Select,
options: [
{
label: 'Neuwertig/Originalverpackt',
value: ItemConditionValue.OriginalPackaging,
nextQuestion: ReturnProcessQuestionKey.ReturnReason,
},
{ label: 'Beschädigt/Fehldruck', value: ItemConditionValue.Damaged },
],
},
{
key: ReturnProcessQuestionKey.ReturnReason,
description: 'Warum möchtest du den Artikel zurückgeben?',
type: ReturnProcessQuestionType.Select,
options: [
{ label: 'Gefällt nicht/Wiederruf', value: ReturnReasonValue.Dislike },
{
label: 'Fehllieferung',
value: ReturnReasonValue.WrongItem,
nextQuestion: ReturnProcessQuestionKey.DeliveredItem,
},
],
},
{
key: ReturnProcessQuestionKey.DeliveredItem,
description: 'Welcher Artikel wurde geliefert?',
type: ReturnProcessQuestionType.Product,
},
];
/**
* Validates the answers for the nonbook return process.
* @param answers - A record of answers keyed by question keys.
* @returns The eligibility state for the return process.
*/
export function validateNonbookQuestions(
answers: ReturnProcessAnswers,
): EligibleForReturn {
if (
answers[ReturnProcessQuestionKey.ItemCondition] ===
ItemConditionValue.OriginalPackaging
) {
return validateOvpReturn(answers);
} else if (
answers[ReturnProcessQuestionKey.ItemCondition] ===
ItemConditionValue.Damaged
) {
return { state: EligibleForReturnState.Eligible };
}
return {
state: EligibleForReturnState.NotEligible,
reason: 'Invalid answers',
};
}

View File

@@ -0,0 +1,81 @@
import { CategoryQuestions, CategoryQuestionValidators } from './registry';
import { ProductCategory } from './constants';
import {
bookCalendarQuestions,
validateBookCalendarQuestions,
} from './book-calendar';
import {
tonDatentraegerQuestions,
validateTonDatentraegerQuestions,
} from './ton-datentraeger';
import { nonbookQuestions, validateNonbookQuestions } from './nonbook';
import {
elektronischeGeraeteQuestions,
validateElektronischeGeraeteQuestions,
} from './elektronische-geraete';
describe('Category Registry', () => {
describe('CategoryQuestions mappings', () => {
it('should map each category to its respective questions array', () => {
// Test all category mappings
expect(CategoryQuestions[ProductCategory.BookCalendar]).toBe(
bookCalendarQuestions,
);
expect(CategoryQuestions[ProductCategory.TonDatentraeger]).toBe(
tonDatentraegerQuestions,
);
expect(CategoryQuestions[ProductCategory.SpielwarenPuzzle]).toBe(
nonbookQuestions,
);
expect(CategoryQuestions[ProductCategory.SonstigesNonbook]).toBe(
nonbookQuestions,
);
expect(CategoryQuestions[ProductCategory.ElektronischeGeraete]).toBe(
elektronischeGeraeteQuestions,
);
});
it('should provide the same nonbook questions for both nonbook categories', () => {
// Both nonbook categories should point to the same question array
expect(CategoryQuestions[ProductCategory.SpielwarenPuzzle]).toBe(
CategoryQuestions[ProductCategory.SonstigesNonbook],
);
});
});
describe('CategoryQuestionValidators mappings', () => {
it('should map each category to its respective validator function', () => {
// Test all validator mappings
expect(CategoryQuestionValidators[ProductCategory.BookCalendar]).toBe(
validateBookCalendarQuestions,
);
expect(CategoryQuestionValidators[ProductCategory.TonDatentraeger]).toBe(
validateTonDatentraegerQuestions,
);
expect(CategoryQuestionValidators[ProductCategory.SpielwarenPuzzle]).toBe(
validateNonbookQuestions,
);
expect(CategoryQuestionValidators[ProductCategory.SonstigesNonbook]).toBe(
validateNonbookQuestions,
);
expect(
CategoryQuestionValidators[ProductCategory.ElektronischeGeraete],
).toBe(validateElektronischeGeraeteQuestions);
});
it('should provide the same validator for both nonbook categories', () => {
// Both nonbook categories should point to the same validator function
expect(CategoryQuestionValidators[ProductCategory.SpielwarenPuzzle]).toBe(
CategoryQuestionValidators[ProductCategory.SonstigesNonbook],
);
});
});
it('should have matching keys in both CategoryQuestions and CategoryQuestionValidators', () => {
// Make sure both registries have the same set of keys
const questionKeys = Object.keys(CategoryQuestions);
const validatorKeys = Object.keys(CategoryQuestionValidators);
expect(questionKeys.sort()).toEqual(validatorKeys.sort());
});
});

View File

@@ -0,0 +1,47 @@
import { ProductCategory } from './constants';
import { QuestionValidator } from './types';
import { ReturnProcessQuestion } from '../models';
import {
bookCalendarQuestions,
validateBookCalendarQuestions,
} from './book-calendar';
import {
tonDatentraegerQuestions,
validateTonDatentraegerQuestions,
} from './ton-datentraeger';
import { nonbookQuestions, validateNonbookQuestions } from './nonbook';
import {
elektronischeGeraeteQuestions,
validateElektronischeGeraeteQuestions,
} from './elektronische-geraete';
import { tolinoQuestions, validateTolinoQuestions } from './tolino';
/**
* A mapping of categories to their respective return process questions.
*/
export const CategoryQuestions: Record<
ProductCategory,
ReturnProcessQuestion[]
> = {
[ProductCategory.BookCalendar]: bookCalendarQuestions,
[ProductCategory.TonDatentraeger]: tonDatentraegerQuestions,
[ProductCategory.SpielwarenPuzzle]: nonbookQuestions,
[ProductCategory.SonstigesNonbook]: nonbookQuestions,
[ProductCategory.ElektronischeGeraete]: elektronischeGeraeteQuestions,
[ProductCategory.Tolino]: tolinoQuestions,
};
/**
* A mapping of categories to their respective validation functions.
*/
export const CategoryQuestionValidators: Record<
ProductCategory,
QuestionValidator
> = {
[ProductCategory.BookCalendar]: validateBookCalendarQuestions,
[ProductCategory.TonDatentraeger]: validateTonDatentraegerQuestions,
[ProductCategory.SpielwarenPuzzle]: validateNonbookQuestions,
[ProductCategory.SonstigesNonbook]: validateNonbookQuestions,
[ProductCategory.ElektronischeGeraete]: validateElektronischeGeraeteQuestions,
[ProductCategory.Tolino]: validateTolinoQuestions,
};

View File

@@ -0,0 +1,61 @@
import { validateTolinoQuestions, tolinoQuestions } from './tolino';
import { EligibleForReturnState, ReturnProcessQuestionKey } from '../models';
import { ProductCategory } from './constants';
import { CategoryQuestions, CategoryQuestionValidators } from './registry';
describe('Tolino Return Process', () => {
describe('validateTolinoQuestions', () => {
it('should return Eligible when item is ovp', () => {
const answers = {
[ReturnProcessQuestionKey.ItemCondition]: 'ovp',
};
const result = validateTolinoQuestions(answers);
expect(result).toEqual({ state: EligibleForReturnState.Eligible });
});
it('should return Eligible when damaged and has significant defects', () => {
const answers = {
[ReturnProcessQuestionKey.ItemCondition]: 'damaged',
[ReturnProcessQuestionKey.DevicePower]: 'yes',
};
const result = validateTolinoQuestions(answers);
expect(result).toEqual({ state: EligibleForReturnState.Eligible });
});
it('should return NotEligible when damaged but no significant defects', () => {
const answers = {
[ReturnProcessQuestionKey.ItemCondition]: 'damaged',
[ReturnProcessQuestionKey.DevicePower]: 'no',
[ReturnProcessQuestionKey.PackageComplete]: 'no',
[ReturnProcessQuestionKey.CaseCondition]: 'no',
[ReturnProcessQuestionKey.UsbPort]: 'no',
};
const result = validateTolinoQuestions(answers);
expect(result).toEqual({
state: EligibleForReturnState.NotEligible,
reason: 'No significant defects found',
});
});
it('should return NotEligible for invalid answers', () => {
const answers = {};
const result = validateTolinoQuestions(answers);
expect(result).toEqual({
state: EligibleForReturnState.NotEligible,
reason: 'Invalid answers',
});
});
});
describe('Registry mappings', () => {
it('should map Tolino category to tolino questions', () => {
expect(CategoryQuestions[ProductCategory.Tolino]).toBe(tolinoQuestions);
});
it('should map Tolino category to tolino validator', () => {
expect(CategoryQuestionValidators[ProductCategory.Tolino]).toBe(
validateTolinoQuestions,
);
});
});
});

View File

@@ -0,0 +1,171 @@
import {
ReturnProcessAnswers,
ReturnProcessQuestion,
ReturnProcessQuestionKey,
ReturnProcessQuestionType,
EligibleForReturn,
EligibleForReturnState,
} from '../models';
import {
ItemConditionValue,
ItemDefectiveValue,
ReturnReasonValue,
} from './constants';
/**
* Questions for the return process of Tolino devices.
*/
export const tolinoQuestions: ReturnProcessQuestion[] = [
{
key: ReturnProcessQuestionKey.ItemCondition,
description: 'Wie ist der Artikelzustand?',
type: ReturnProcessQuestionType.Select,
options: [
{
label: 'Originalverpackt',
value: ItemConditionValue.OriginalPackaging,
},
{
label: 'Geöffnet/Beschädigt',
value: ItemConditionValue.Damaged,
nextQuestion: ReturnProcessQuestionKey.DevicePower,
},
],
},
{
key: ReturnProcessQuestionKey.DevicePower,
description: 'Lässt sich das Gerät einschalten?',
type: ReturnProcessQuestionType.Select,
options: [
{
label: 'Ja',
value: ItemDefectiveValue.No,
nextQuestion: ReturnProcessQuestionKey.PackageComplete,
},
{
label: 'Nein',
value: ItemDefectiveValue.Yes,
},
],
},
{
key: ReturnProcessQuestionKey.PackageComplete,
description: 'Ist die Verpackung vollständig?',
type: ReturnProcessQuestionType.Select,
options: [
{
label: 'Ja',
value: ItemDefectiveValue.No,
nextQuestion: ReturnProcessQuestionKey.ReturnReason,
},
{
label: 'Nein',
value: ItemDefectiveValue.Yes,
nextQuestion: ReturnProcessQuestionKey.CaseCondition,
},
],
},
{
key: ReturnProcessQuestionKey.CaseCondition,
description: 'Hat das Gehäuse Mängel oder ist zerkratzt?',
type: ReturnProcessQuestionType.Select,
options: [
{
label: 'Ja',
value: ItemDefectiveValue.Yes,
nextQuestion: ReturnProcessQuestionKey.DisplayCondition,
},
{
label: 'Nein',
value: ItemDefectiveValue.No,
nextQuestion: ReturnProcessQuestionKey.UsbPort,
},
],
},
{
key: ReturnProcessQuestionKey.DisplayCondition,
description: 'Hat das Display Mängel oder ist gebrochen?',
type: ReturnProcessQuestionType.Select,
options: [
{
label: 'Ja',
value: ItemDefectiveValue.Yes,
},
{
label: 'Nein',
value: ItemDefectiveValue.No,
nextQuestion: ReturnProcessQuestionKey.UsbPort,
},
],
},
{
key: ReturnProcessQuestionKey.UsbPort,
description: 'Funktioniert die USB Buchse?',
type: ReturnProcessQuestionType.Select,
options: [
{
label: 'Ja',
value: ItemDefectiveValue.No,
nextQuestion: ReturnProcessQuestionKey.ReturnReason,
},
{
label: 'Nein',
value: ItemDefectiveValue.Yes,
},
],
},
{
key: ReturnProcessQuestionKey.ReturnReason,
description: 'Warum wird der Artikel zurückgegeben?',
type: ReturnProcessQuestionType.Select,
options: [
{
label: 'Gefällt nicht/Wiederruf',
value: ReturnReasonValue.Dislike,
},
{
label: 'Fehllieferung',
value: ReturnReasonValue.WrongItem,
},
],
},
];
/**
* Validates the answers for the Tolino return process.
* @param answers - A record of answers keyed by question keys.
* @returns The eligibility state for the return process.
*/
export function validateTolinoQuestions(
answers: ReturnProcessAnswers,
): EligibleForReturn {
if (
answers[ReturnProcessQuestionKey.ItemCondition] ===
ItemConditionValue.OriginalPackaging
) {
return { state: EligibleForReturnState.Eligible };
} else if (
answers[ReturnProcessQuestionKey.ItemCondition] ===
ItemConditionValue.Damaged
) {
if (
answers[ReturnProcessQuestionKey.DevicePower] ===
ItemDefectiveValue.Yes ||
answers[ReturnProcessQuestionKey.PackageComplete] ===
ItemDefectiveValue.Yes ||
answers[ReturnProcessQuestionKey.CaseCondition] ===
ItemDefectiveValue.Yes ||
answers[ReturnProcessQuestionKey.UsbPort] === ItemDefectiveValue.Yes
) {
return { state: EligibleForReturnState.Eligible };
}
return {
state: EligibleForReturnState.NotEligible,
reason: 'No significant defects found',
};
}
return {
state: EligibleForReturnState.NotEligible,
reason: 'Invalid answers',
};
}

View File

@@ -0,0 +1,63 @@
import {
validateTonDatentraegerQuestions,
tonDatentraegerQuestions,
} from './ton-datentraeger';
import { EligibleForReturnState, ReturnProcessQuestionKey } from '../models';
import { ProductCategory } from './constants';
import { CategoryQuestions, CategoryQuestionValidators } from './registry';
describe('Ton-/Datenträger Return Process', () => {
describe('validateTonDatentraegerQuestions', () => {
it('should return Eligible when item is ovp', () => {
const answers = {
[ReturnProcessQuestionKey.ItemCondition]: 'ovp',
};
const result = validateTonDatentraegerQuestions(answers);
expect(result).toEqual({ state: EligibleForReturnState.Eligible });
});
it('should return Eligible when damaged and item is defective', () => {
const answers = {
[ReturnProcessQuestionKey.ItemCondition]: 'damaged',
[ReturnProcessQuestionKey.ItemDefective]: 'yes',
};
const result = validateTonDatentraegerQuestions(answers);
expect(result).toEqual({ state: EligibleForReturnState.Eligible });
});
it('should return NotEligible when damaged and item is not defective', () => {
const answers = {
[ReturnProcessQuestionKey.ItemCondition]: 'damaged',
[ReturnProcessQuestionKey.ItemDefective]: 'no',
};
const result = validateTonDatentraegerQuestions(answers);
expect(result).toEqual({
state: EligibleForReturnState.NotEligible,
reason: 'Item not defective',
});
});
it('should return NotEligible with invalid answers if keys are missing', () => {
const answers = {};
const result = validateTonDatentraegerQuestions(answers);
expect(result).toEqual({
state: EligibleForReturnState.NotEligible,
reason: 'Invalid answers',
});
});
});
describe('Registry mappings', () => {
it('should map TonDatentraeger category to ton-datentraeger questions', () => {
expect(CategoryQuestions[ProductCategory.TonDatentraeger]).toBe(
tonDatentraegerQuestions,
);
});
it('should map TonDatentraeger category to ton-datentraeger validator', () => {
expect(CategoryQuestionValidators[ProductCategory.TonDatentraeger]).toBe(
validateTonDatentraegerQuestions,
);
});
});
});

View File

@@ -0,0 +1,89 @@
import {
ReturnProcessAnswers,
ReturnProcessQuestion,
ReturnProcessQuestionKey,
ReturnProcessQuestionType,
EligibleForReturn,
EligibleForReturnState,
} from '../models';
import { ItemConditionValue, ItemDefectiveValue } from './constants';
/**
* Questions for the return process of Ton-/Datenträger (audio/data carriers).
*/
export const tonDatentraegerQuestions: ReturnProcessQuestion[] = [
{
key: ReturnProcessQuestionKey.ItemCondition,
description: 'Wie ist der Artikelzustand?',
type: ReturnProcessQuestionType.Select,
options: [
{
label: 'Versiegelt/Originalverpackt',
value: ItemConditionValue.OriginalPackaging,
},
{
label: 'Geöffnet',
value: ItemConditionValue.Damaged,
nextQuestion: ReturnProcessQuestionKey.ItemDefective,
},
],
},
{
key: ReturnProcessQuestionKey.ItemDefective,
description: 'Ist der Artikel Defekt?',
type: ReturnProcessQuestionType.Select,
options: [
{
label: 'Ja',
value: ItemDefectiveValue.Yes,
},
{
label: 'Nein',
value: ItemDefectiveValue.No,
},
],
},
];
/**
* Validates the answers for the Ton-/Datenträger return process.
* @param answers - A record of answers keyed by question keys.
* @returns The eligibility state for the return process.
*/
export function validateTonDatentraegerQuestions(
answers: ReturnProcessAnswers,
): EligibleForReturn {
// Check if the item is in original condition
if (
answers[ReturnProcessQuestionKey.ItemCondition] ===
ItemConditionValue.OriginalPackaging
) {
return { state: EligibleForReturnState.Eligible };
}
// Check if the item is damaged
else if (
answers[ReturnProcessQuestionKey.ItemCondition] ===
ItemConditionValue.Damaged
) {
// Check if the item is defective
if (
answers[ReturnProcessQuestionKey.ItemDefective] === ItemDefectiveValue.Yes
) {
return { state: EligibleForReturnState.Eligible };
}
// Check if the item is not defective
else if (
answers[ReturnProcessQuestionKey.ItemDefective] === ItemDefectiveValue.No
) {
return {
state: EligibleForReturnState.NotEligible,
reason: 'Item not defective',
};
}
}
return {
state: EligibleForReturnState.NotEligible,
reason: 'Invalid answers',
};
}

View File

@@ -0,0 +1,9 @@
import { ReturnProcessAnswers, EligibleForReturn } from '../models';
/**
* Type definition for question validator functions.
* These functions take answer records and return eligibility results.
*/
export type QuestionValidator = (
answers: ReturnProcessAnswers,
) => EligibleForReturn;

View File

@@ -0,0 +1,54 @@
import { validateOvpReturn } from './validators';
import { EligibleForReturnState, ReturnProcessQuestionKey } from '../models';
describe('Shared Validators', () => {
describe('validateOvpReturn', () => {
it('should return Eligible when return reason is dislike', () => {
const answers = {
[ReturnProcessQuestionKey.ReturnReason]: 'dislike',
};
const result = validateOvpReturn(answers);
expect(result).toEqual({ state: EligibleForReturnState.Eligible });
});
it('should return Eligible when return reason is wrong_item and delivered item is provided', () => {
const answers = {
[ReturnProcessQuestionKey.ReturnReason]: 'wrong_item',
[ReturnProcessQuestionKey.DeliveredItem]: 'someProduct',
};
const result = validateOvpReturn(answers);
expect(result).toEqual({ state: EligibleForReturnState.Eligible });
});
it('should return Pending when return reason is wrong_item but delivered item is missing', () => {
const answers = {
[ReturnProcessQuestionKey.ReturnReason]: 'wrong_item',
};
const result = validateOvpReturn(answers);
expect(result).toEqual({
state: EligibleForReturnState.Pending,
reason: 'Missing delivered item',
});
});
it('should return NotEligible for invalid or unspecified return reasons', () => {
// Test with empty answers
const emptyAnswers = {};
const emptyResult = validateOvpReturn(emptyAnswers);
expect(emptyResult).toEqual({
state: EligibleForReturnState.NotEligible,
reason: 'Invalid answers',
});
// Test with invalid return reason
const invalidAnswers = {
[ReturnProcessQuestionKey.ReturnReason]: 'not_a_valid_reason',
};
const invalidResult = validateOvpReturn(invalidAnswers);
expect(invalidResult).toEqual({
state: EligibleForReturnState.NotEligible,
reason: 'Invalid answers',
});
});
});
});

View File

@@ -0,0 +1,36 @@
import {
ReturnProcessAnswers,
ReturnProcessQuestionKey,
EligibleForReturn,
EligibleForReturnState,
} from '../models';
import { ReturnReasonValue, ItemDefectiveValue } from './constants';
/**
* Helper function to validate return eligibility for items in original condition.
* @param answers - A record of answers keyed by question keys.
* @returns The eligibility state for the return process.
*/
export function validateOvpReturn(
answers: ReturnProcessAnswers,
): EligibleForReturn {
if (
answers[ReturnProcessQuestionKey.ReturnReason] === ReturnReasonValue.Dislike
) {
return { state: EligibleForReturnState.Eligible };
} else if (
answers[ReturnProcessQuestionKey.ReturnReason] ===
ReturnReasonValue.WrongItem
) {
return answers[ReturnProcessQuestionKey.DeliveredItem]
? { state: EligibleForReturnState.Eligible }
: {
state: EligibleForReturnState.Pending,
reason: 'Missing delivered item',
};
}
return {
state: EligibleForReturnState.NotEligible,
reason: 'Invalid answers',
};
}

View File

@@ -1,238 +0,0 @@
import {
bookCalendarQuestions,
nonbookQuestions,
tonDatentraegerQuestions,
andereElektronischeGeraete,
validateBookCalendarQuestions,
validateNonbookQuestions,
validateTonDatentraegerQuestions,
validateAndereElektronischeGeraeteQuestions,
CategoryQuestions,
CategoryQuestionValidators,
} from './return-process-questions';
import { EligibleForReturnState, ReturnProcessQuestionKey } from './models';
describe('ReturnProcessQuestions Validators', () => {
describe('validateBookCalendarQuestions', () => {
it('should return Eligible when item is ovp and return reason is dislike', () => {
const answers = {
[ReturnProcessQuestionKey.ItemCondition]: 'ovp',
[ReturnProcessQuestionKey.ReturnReason]: 'dislike',
};
const result = validateBookCalendarQuestions(answers);
expect(result).toEqual({ state: EligibleForReturnState.Eligible });
});
it('should return Eligible when item is ovp, return reason is wrong_item and delivered item is provided', () => {
const answers = {
[ReturnProcessQuestionKey.ItemCondition]: 'ovp',
[ReturnProcessQuestionKey.ReturnReason]: 'wrong_item',
[ReturnProcessQuestionKey.DeliveredItem]: 'someProduct',
};
const result = validateBookCalendarQuestions(answers);
expect(result).toEqual({ state: EligibleForReturnState.Eligible });
});
it('should return Pending when item is ovp, return reason is wrong_item but delivered item is missing', () => {
const answers = {
[ReturnProcessQuestionKey.ItemCondition]: 'ovp',
[ReturnProcessQuestionKey.ReturnReason]: 'wrong_item',
};
const result = validateBookCalendarQuestions(answers);
expect(result).toEqual({
state: EligibleForReturnState.Pending,
reason: 'Missing delivered item',
});
});
it('should return Eligible when item is damaged', () => {
const answers = {
[ReturnProcessQuestionKey.ItemCondition]: 'damaged',
};
const result = validateBookCalendarQuestions(answers);
expect(result).toEqual({ state: EligibleForReturnState.Eligible });
});
it('should return NotEligible for any invalid answers', () => {
const answers = {};
const result = validateBookCalendarQuestions(answers);
expect(result).toEqual({
state: EligibleForReturnState.NotEligible,
reason: 'Invalid answers',
});
});
});
describe('validateTonDatentraegerQuestions', () => {
it('should return Eligible when item is ovp', () => {
const answers = {
[ReturnProcessQuestionKey.ItemCondition]: 'ovp',
};
const result = validateTonDatentraegerQuestions(answers);
expect(result).toEqual({ state: EligibleForReturnState.Eligible });
});
it('should return Eligible when damaged and item is defective', () => {
const answers = {
[ReturnProcessQuestionKey.ItemCondition]: 'damaged',
[ReturnProcessQuestionKey.ItemDefective]: 'yes',
};
const result = validateTonDatentraegerQuestions(answers);
expect(result).toEqual({ state: EligibleForReturnState.Eligible });
});
it('should return NotEligible when damaged and item is not defective', () => {
const answers = {
[ReturnProcessQuestionKey.ItemCondition]: 'damaged',
[ReturnProcessQuestionKey.ItemDefective]: 'no',
};
const result = validateTonDatentraegerQuestions(answers);
expect(result).toEqual({
state: EligibleForReturnState.NotEligible,
reason: 'Item not defective',
});
});
it('should return NotEligible with invalid answers if keys are missing', () => {
const answers = {};
const result = validateTonDatentraegerQuestions(answers);
expect(result).toEqual({
state: EligibleForReturnState.NotEligible,
reason: 'Invalid answers',
});
});
});
describe('validateNonbookQuestions', () => {
it('should return Eligible when item is ovp and return reason is dislike', () => {
const answers = {
[ReturnProcessQuestionKey.ItemCondition]: 'ovp',
[ReturnProcessQuestionKey.ReturnReason]: 'dislike',
};
const result = validateNonbookQuestions(answers);
expect(result).toEqual({ state: EligibleForReturnState.Eligible });
});
it('should return Eligible when item is ovp, return reason is wrong_item and delivered item is provided', () => {
const answers = {
[ReturnProcessQuestionKey.ItemCondition]: 'ovp',
[ReturnProcessQuestionKey.ReturnReason]: 'wrong_item',
[ReturnProcessQuestionKey.DeliveredItem]: 'product',
};
const result = validateNonbookQuestions(answers);
expect(result).toEqual({ state: EligibleForReturnState.Eligible });
});
it('should return Pending when delivered item is missing for wrong_item reason', () => {
const answers = {
[ReturnProcessQuestionKey.ItemCondition]: 'ovp',
[ReturnProcessQuestionKey.ReturnReason]: 'wrong_item',
};
const result = validateNonbookQuestions(answers);
expect(result).toEqual({
state: EligibleForReturnState.Pending,
reason: 'Missing delivered item',
});
});
it('should return Eligible when item is damaged', () => {
const answers = {
[ReturnProcessQuestionKey.ItemCondition]: 'damaged',
};
const result = validateNonbookQuestions(answers);
expect(result).toEqual({ state: EligibleForReturnState.Eligible });
});
it('should return NotEligible for invalid answers', () => {
const answers = {};
const result = validateNonbookQuestions(answers);
expect(result).toEqual({
state: EligibleForReturnState.NotEligible,
reason: 'Invalid answers',
});
});
});
describe('validateAndereElektronischeGeraeteQuestions', () => {
it('should return Eligible when item is ovp and return reason is dislike', () => {
const answers = {
[ReturnProcessQuestionKey.ItemCondition]: 'ovp',
[ReturnProcessQuestionKey.ReturnReason]: 'dislike',
};
const result = validateAndereElektronischeGeraeteQuestions(answers);
expect(result).toEqual({ state: EligibleForReturnState.Eligible });
});
it('should return Eligible when item is ovp, return reason is wrong_item and delivered item provided', () => {
const answers = {
[ReturnProcessQuestionKey.ItemCondition]: 'ovp',
[ReturnProcessQuestionKey.ReturnReason]: 'wrong_item',
[ReturnProcessQuestionKey.DeliveredItem]: 'item123',
};
const result = validateAndereElektronischeGeraeteQuestions(answers);
expect(result).toEqual({ state: EligibleForReturnState.Eligible });
});
it('should return Pending when delivered item is missing for wrong_item reason', () => {
const answers = {
[ReturnProcessQuestionKey.ItemCondition]: 'ovp',
[ReturnProcessQuestionKey.ReturnReason]: 'wrong_item',
};
const result = validateAndereElektronischeGeraeteQuestions(answers);
expect(result).toEqual({
state: EligibleForReturnState.Pending,
reason: 'Missing delivered item',
});
});
it('should return Eligible when damaged and item is defective', () => {
const answers = {
[ReturnProcessQuestionKey.ItemCondition]: 'damaged',
[ReturnProcessQuestionKey.ItemDefective]: 'yes',
};
const result = validateAndereElektronischeGeraeteQuestions(answers);
expect(result).toEqual({ state: EligibleForReturnState.Eligible });
});
it('should return NotEligible when damaged and item is not defective', () => {
const answers = {
[ReturnProcessQuestionKey.ItemCondition]: 'damaged',
[ReturnProcessQuestionKey.ItemDefective]: 'no',
};
const result = validateAndereElektronischeGeraeteQuestions(answers);
expect(result).toEqual({
state: EligibleForReturnState.NotEligible,
reason: 'Item not defective',
});
});
it('should return NotEligible for invalid answers', () => {
const answers = {};
const result = validateAndereElektronischeGeraeteQuestions(answers);
expect(result).toEqual({
state: EligibleForReturnState.NotEligible,
reason: 'Invalid answers',
});
});
});
describe('CategoryQuestions and Validators mapping', () => {
it('should map each category to its questions', () => {
expect(CategoryQuestions['Buch/Kalender']).toBe(bookCalendarQuestions);
expect(CategoryQuestions['Ton-/Datenträger']).toBe(tonDatentraegerQuestions);
expect(CategoryQuestions['Spielwaren/Puzzle']).toBe(nonbookQuestions);
expect(CategoryQuestions['Sonstiges und Nonbook']).toBe(nonbookQuestions);
expect(CategoryQuestions['Andere Elektronische Geräte']).toBe(andereElektronischeGeraete);
});
it('should map each category to its validator', () => {
expect(CategoryQuestionValidators['Buch/Kalender']).toBe(validateBookCalendarQuestions);
expect(CategoryQuestionValidators['Ton-/Datenträger']).toBe(validateTonDatentraegerQuestions);
expect(CategoryQuestionValidators['Spielwaren/Puzzle']).toBe(validateNonbookQuestions);
expect(CategoryQuestionValidators['Sonstiges und Nonbook']).toBe(validateNonbookQuestions);
expect(CategoryQuestionValidators['Andere Elektronische Geräte']).toBe(
validateAndereElektronischeGeraeteQuestions,
);
});
});
});

View File

@@ -1,299 +0,0 @@
import {
EligibleForReturn,
EligibleForReturnState,
ReturnProcessAnswers,
ReturnProcessQuestion,
ReturnProcessQuestionKey,
ReturnProcessQuestionType,
} from './models';
/**
* Questions for the return process of books and calendars.
*/
export const bookCalendarQuestions: ReturnProcessQuestion[] = [
{
key: ReturnProcessQuestionKey.ItemCondition,
description: 'Wie ist der Artikelzustand?',
type: ReturnProcessQuestionType.Select,
options: [
{
label: 'Neuwertig/Originalverpackt',
value: 'ovp',
nextQuestion: ReturnProcessQuestionKey.ReturnReason,
},
{ label: 'Beschädigt/Fehldruck', value: 'damaged' },
],
},
{
key: ReturnProcessQuestionKey.ReturnReason,
description: 'Warum möchtest du den Artikel zurücksenden?',
type: ReturnProcessQuestionType.Select,
options: [
{ label: 'Gefällt nicht/Wiederruf', value: 'dislike' },
{
label: 'Fehllieferung',
value: 'wrong_item',
nextQuestion: ReturnProcessQuestionKey.DeliveredItem,
},
],
},
{
key: ReturnProcessQuestionKey.DeliveredItem,
description: 'Welcher Artikel wurde geliefert?',
type: ReturnProcessQuestionType.Product,
},
];
/**
* Helper function to validate return eligibility for items in original condition.
* @param answers - A record of answers keyed by question keys.
* @returns The eligibility state for the return process.
*/
function validateOvpReturn(answers: ReturnProcessAnswers): EligibleForReturn {
if (answers[ReturnProcessQuestionKey.ReturnReason] === 'dislike') {
return { state: EligibleForReturnState.Eligible };
} else if (answers[ReturnProcessQuestionKey.ReturnReason] === 'wrong_item') {
return answers[ReturnProcessQuestionKey.DeliveredItem]
? { state: EligibleForReturnState.Eligible }
: { state: EligibleForReturnState.Pending, reason: 'Missing delivered item' };
}
return { state: EligibleForReturnState.NotEligible, reason: 'Invalid answers' };
}
/**
* Validates the answers for the book and calendar return process.
* @param answers - A record of answers keyed by question keys.
* @returns The eligibility state for the return process.
*/
export function validateBookCalendarQuestions(answers: ReturnProcessAnswers): EligibleForReturn {
if (answers[ReturnProcessQuestionKey.ItemCondition] === 'ovp') {
return validateOvpReturn(answers);
} else if (answers[ReturnProcessQuestionKey.ItemCondition] === 'damaged') {
return { state: EligibleForReturnState.Eligible };
}
return { state: EligibleForReturnState.NotEligible, reason: 'Invalid answers' };
}
/**
* Questions for the return process of Ton-/Datenträger (audio/data carriers).
*/
export const tonDatentraegerQuestions: ReturnProcessQuestion[] = [
{
key: ReturnProcessQuestionKey.ItemCondition,
description: 'Wie ist der Artikelzustand?',
type: ReturnProcessQuestionType.Select,
options: [
{
label: 'Versiegelt/Originalverpackt',
value: 'ovp',
},
{
label: 'Geöffnet',
value: 'damaged',
nextQuestion: ReturnProcessQuestionKey.ItemDefective,
},
],
},
{
key: ReturnProcessQuestionKey.ItemDefective,
description: 'Ist der Artikel Defekt?',
type: ReturnProcessQuestionType.Select,
options: [
{
label: 'Ja',
value: 'yes',
},
{
label: 'Nein',
value: 'no',
},
],
},
];
/**
* Validates the answers for the Ton-/Datenträger return process.
* @param answers - A record of answers keyed by question keys.
* @returns The eligibility state for the return process.
*/
export function validateTonDatentraegerQuestions(answers: ReturnProcessAnswers): EligibleForReturn {
// Check if the item is in original condition
if (answers[ReturnProcessQuestionKey.ItemCondition] === 'ovp') {
return { state: EligibleForReturnState.Eligible };
}
// Check if the item is damaged
else if (answers[ReturnProcessQuestionKey.ItemCondition] === 'damaged') {
// Check if the item is defective
if (answers[ReturnProcessQuestionKey.ItemDefective] === 'yes') {
return { state: EligibleForReturnState.Eligible };
}
// Check if the item is not defective
else if (answers[ReturnProcessQuestionKey.ItemDefective] === 'no') {
return { state: EligibleForReturnState.NotEligible, reason: 'Item not defective' };
}
}
return { state: EligibleForReturnState.NotEligible, reason: 'Invalid answers' };
}
/**
* Questions for the return process of Spielwaren/Puzzle and other nonbook items.
*/
export const nonbookQuestions: ReturnProcessQuestion[] = [
{
key: ReturnProcessQuestionKey.ItemCondition,
description: 'Wie ist der Artikelzustand?',
type: ReturnProcessQuestionType.Select,
options: [
{
label: 'Neuwertig/Originalverpackt',
value: 'ovp',
nextQuestion: ReturnProcessQuestionKey.ReturnReason,
},
{ label: 'Beschädigt/Fehldruck', value: 'damaged' },
],
},
{
key: ReturnProcessQuestionKey.ReturnReason,
description: 'Warum möchtest du den Artikel zurückgeben?',
type: ReturnProcessQuestionType.Select,
options: [
{ label: 'Gefällt nicht/Wiederruf', value: 'dislike' },
{
label: 'Fehllieferung',
value: 'wrong_item',
nextQuestion: ReturnProcessQuestionKey.DeliveredItem,
},
],
},
{
key: ReturnProcessQuestionKey.DeliveredItem,
description: 'Welcher Artikel wurde geliefert?',
type: ReturnProcessQuestionType.Product,
},
];
/**
* Validates the answers for the nonbook return process.
* @param answers - A record of answers keyed by question keys.
* @returns The eligibility state for the return process.
*/
export function validateNonbookQuestions(answers: ReturnProcessAnswers): EligibleForReturn {
if (answers[ReturnProcessQuestionKey.ItemCondition] === 'ovp') {
return validateOvpReturn(answers);
} else if (answers[ReturnProcessQuestionKey.ItemCondition] === 'damaged') {
return { state: EligibleForReturnState.Eligible };
}
return { state: EligibleForReturnState.NotEligible, reason: 'Invalid answers' };
}
/**
* Questions for the return process of other electronic devices.
*/
export const andereElektronischeGeraete: ReturnProcessQuestion[] = [
{
key: ReturnProcessQuestionKey.ItemCondition,
description: 'Wie ist der Artikelzustand?',
type: ReturnProcessQuestionType.Select,
options: [
{
label: 'Originalverpackt',
value: 'ovp',
nextQuestion: ReturnProcessQuestionKey.ReturnReason,
},
{
label: 'Geöffnet / Beschädigt',
value: 'damaged',
nextQuestion: ReturnProcessQuestionKey.ItemDefective,
},
],
},
{
key: ReturnProcessQuestionKey.ReturnReason,
description: 'Warum möchtest du den Artikel zurückgeben?',
type: ReturnProcessQuestionType.Select,
options: [
{ label: 'Gefällt nicht/Wiederruf', value: 'dislike' },
{
label: 'Fehllieferung',
value: 'wrong_item',
nextQuestion: ReturnProcessQuestionKey.DeliveredItem,
},
],
},
{
key: ReturnProcessQuestionKey.ItemDefective,
description: 'Ist der Artikel defekt?',
type: ReturnProcessQuestionType.Select,
options: [
{
label: 'Ja',
value: 'yes',
},
{
label: 'Nein',
value: 'no',
},
],
},
{
key: ReturnProcessQuestionKey.DeliveredItem,
description: 'Welcher Artikel wurde geliefert?',
type: ReturnProcessQuestionType.Product,
},
];
/**
* Validates the answers for the return process of other electronic devices.
* @param answers - A record of answers keyed by question keys.
* @returns The eligibility state for the return process.
*/
export function validateAndereElektronischeGeraeteQuestions(
answers: ReturnProcessAnswers,
): EligibleForReturn {
if (answers[ReturnProcessQuestionKey.ItemCondition] === 'ovp') {
return validateOvpReturn(answers);
} else if (answers[ReturnProcessQuestionKey.ItemCondition] === 'damaged') {
if (answers[ReturnProcessQuestionKey.ItemDefective] === 'yes') {
return { state: EligibleForReturnState.Eligible };
} else if (answers[ReturnProcessQuestionKey.ItemDefective] === 'no') {
return { state: EligibleForReturnState.NotEligible, reason: 'Item not defective' };
}
}
return { state: EligibleForReturnState.NotEligible, reason: 'Invalid answers' };
}
/**
* A mapping of categories to their respective return process questions.
*/
export const CategoryQuestions = {
'Buch/Kalender': bookCalendarQuestions,
'Ton-/Datenträger': tonDatentraegerQuestions,
'Spielwaren/Puzzle': nonbookQuestions,
'Sonstiges und Nonbook': nonbookQuestions,
'Andere Elektronische Geräte': andereElektronischeGeraete,
} as const;
/**
* A type alias for representing the keys of the CategoryQuestions object.
*
* This type is used to ensure that the keys of the CategoryQuestions object
* and the references to them remain in sync. Any updates to the keys defined
* in CategoryQuestions will automatically be reflected in CategoryQuestionsKey,
* thereby helping to maintain consistency across the codebase.
*/
type CategoryQuestionsKey = keyof typeof CategoryQuestions;
/**
* A mapping of categories to their respective validation functions.
*/
export const CategoryQuestionValidators: Record<
CategoryQuestionsKey,
(answers: ReturnProcessAnswers) => EligibleForReturn
> = {
'Buch/Kalender': validateBookCalendarQuestions,
'Ton-/Datenträger': validateTonDatentraegerQuestions,
'Spielwaren/Puzzle': validateNonbookQuestions,
'Sonstiges und Nonbook': validateNonbookQuestions,
'Andere Elektronische Geräte': validateAndereElektronischeGeraeteQuestions,
};

View File

@@ -6,7 +6,7 @@ import {
ReturnProcessQuestion,
ReturnProcessQuestionType,
} from './models';
import { CategoryQuestions, CategoryQuestionValidators } from './return-process-questions';
import { CategoryQuestions, CategoryQuestionValidators } from './questions';
import { KeyValue } from '@angular/common';
@Injectable({ providedIn: 'root' })
@@ -17,11 +17,14 @@ export class ReturnProcessService {
});
}
returnProcessQuestions(process: ReturnProcess): ReturnProcessQuestion[] | undefined {
const category = process.productCategory || process.receiptItem.features?.['category'];
returnProcessQuestions(
process: ReturnProcess,
): ReturnProcessQuestion[] | undefined {
const category =
process.productCategory || process.receiptItem.features?.['category'];
if (category) {
return CategoryQuestions[category];
return CategoryQuestions[category as keyof typeof CategoryQuestions];
}
return undefined;
}
@@ -29,7 +32,8 @@ export class ReturnProcessService {
returnProcessQuestionValidator(
process: ReturnProcess,
): ((answers: Record<string, unknown>) => EligibleForReturn) | undefined {
const category = process.productCategory || process.receiptItem.features?.['category'];
const category =
process.productCategory || process.receiptItem.features?.['category'];
if (category) {
return CategoryQuestionValidators[category];
@@ -37,7 +41,9 @@ export class ReturnProcessService {
return undefined;
}
activeReturnProcessQuestions(process: ReturnProcess): ReturnProcessQuestion[] | undefined {
activeReturnProcessQuestions(
process: ReturnProcess,
): ReturnProcessQuestion[] | undefined {
const questions = this.returnProcessQuestions(process);
if (!questions) {
@@ -60,10 +66,14 @@ export class ReturnProcessService {
result.push(question);
if (question.type === ReturnProcessQuestionType.Select) {
const option = question.options.find((o) => o.value === process.answers[question.key]);
const option = question.options.find(
(o) => o.value === process.answers[question.key],
);
questionKey = option?.nextQuestion;
} else if (question.type === ReturnProcessQuestionType.Product) {
questionKey = process.answers[question.key] ? question.nextQuestion : undefined;
questionKey = process.answers[question.key]
? question.nextQuestion
: undefined;
} else {
console.error('Unknown question type', question);
break;
@@ -92,7 +102,10 @@ export class ReturnProcessService {
}
const visited = new Set<string>();
function computeLongestPath(questions: ReturnProcessQuestion[], startKey: string) {
function computeLongestPath(
questions: ReturnProcessQuestion[],
startKey: string,
) {
if (visited.has(startKey)) return 0;
visited.add(startKey);
@@ -101,16 +114,30 @@ export class ReturnProcessService {
if (!key) return 0;
const question = questions.find((q) => q.key === key);
if (!question) return 0;
if (question.type === ReturnProcessQuestionType.Select && question.options) {
if (
question.type === ReturnProcessQuestionType.Select &&
question.options
) {
if (returnProcess.answers[question.key]) {
const chosen = question.options.find(
(o) => o.value === returnProcess.answers[question.key],
);
return 1 + findDepth(chosen?.nextQuestion);
}
return 1 + Math.max(...question.options.map((o) => findDepth(o.nextQuestion)));
} else if (question.type === ReturnProcessQuestionType.Product && question.nextQuestion) {
return 1 + (returnProcess.answers[question.key] ? findDepth(question.nextQuestion) : 0);
return (
1 +
Math.max(...question.options.map((o) => findDepth(o.nextQuestion)))
);
} else if (
question.type === ReturnProcessQuestionType.Product &&
question.nextQuestion
) {
return (
1 +
(returnProcess.answers[question.key]
? findDepth(question.nextQuestion)
: 0)
);
}
return 1;
};
@@ -141,15 +168,23 @@ export class ReturnProcessService {
return { state: EligibleForReturnState.Pending };
}
const everyQuestionAnswered = questions.every((q) => q.key in returnProcess.answers);
const everyQuestionAnswered = questions.every(
(q) => q.key in returnProcess.answers,
);
if (!everyQuestionAnswered) {
return { state: EligibleForReturnState.Pending, reason: 'Not all questions answered' };
return {
state: EligibleForReturnState.Pending,
reason: 'Not all questions answered',
};
}
const validator = this.returnProcessQuestionValidator(returnProcess);
if (!validator) {
return { state: EligibleForReturnState.Pending, reason: 'No validator found' };
return {
state: EligibleForReturnState.Pending,
reason: 'No validator found',
};
}
return validator(returnProcess.answers);