refactor: remove validation functions and tests for return processes

- Deleted validation functions for electronic devices, nonbook items, and ton/datentraeger.
- Removed associated test files for these validations.
- Updated question definitions to use new constants for item conditions and return reasons.
- Refactored return process service to utilize schema validation instead of custom validators.
- Adjusted HTML templates to reflect changes in eligibility state handling.
This commit is contained in:
Lorenz Hilpert
2025-04-15 21:39:50 +02:00
parent a608d77ab5
commit d615efd806
22 changed files with 452 additions and 825 deletions

View File

@@ -12,7 +12,7 @@ import {
CheckboxAppearance,
} from '@isa/ui/input-controls';
import { provideAnimations } from '@angular/platform-browser/animations';
import { Component, importProvidersFrom } from '@angular/core';
import { importProvidersFrom } from '@angular/core';
interface ChecklistStoryProps {
values: string[];

View File

@@ -1,7 +1,6 @@
export const EligibleForReturnState = {
NotEligible: 'not-eligible',
Eligible: 'eligible',
Pending: 'pending',
} as const;
export type EligibleForReturnState =

View File

@@ -1,73 +0,0 @@
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

@@ -1,16 +1,20 @@
import {
ReturnProcessAnswers,
ReturnProcessQuestion,
ReturnProcessQuestionKey,
ReturnProcessQuestionType,
EligibleForReturn,
EligibleForReturnState,
} from '../models';
import { ItemConditionValue, ReturnReasonValue } from './constants';
import { validateOvpReturn } from './validators';
import { ItemConditionAnswer, ReturnReasonAnswer } from './constants';
/**
* Questions for the return process of books and calendars.
*
* This array defines the flow of questions presented to users when processing
* book or calendar returns. The questions follow a branching logic based on
* the user's answers, guiding them through the appropriate return process.
*
* The flow typically starts with checking the item condition, then asks about
* the return reason, and may request additional information depending on the
* reason selected.
*/
export const bookCalendarQuestions: ReturnProcessQuestion[] = [
{
@@ -20,10 +24,10 @@ export const bookCalendarQuestions: ReturnProcessQuestion[] = [
options: [
{
label: 'Neuwertig/Originalverpackt',
value: ItemConditionValue.OriginalPackaging,
value: ItemConditionAnswer.OVP,
nextQuestion: ReturnProcessQuestionKey.ReturnReason,
},
{ label: 'Beschädigt/Fehldruck', value: ItemConditionValue.Damaged },
{ label: 'Beschädigt/Fehldruck', value: ItemConditionAnswer.Damaged },
],
},
{
@@ -31,10 +35,10 @@ export const bookCalendarQuestions: ReturnProcessQuestion[] = [
description: 'Warum möchtest du den Artikel zurücksenden?',
type: ReturnProcessQuestionType.Select,
options: [
{ label: 'Gefällt nicht/Wiederruf', value: ReturnReasonValue.Dislike },
{ label: 'Gefällt nicht/Wiederruf', value: ReturnReasonAnswer.Dislike },
{
label: 'Fehllieferung',
value: ReturnReasonValue.WrongItem,
value: ReturnReasonAnswer.WrongItem,
nextQuestion: ReturnProcessQuestionKey.DeliveredItem,
},
],
@@ -45,28 +49,3 @@ export const bookCalendarQuestions: ReturnProcessQuestion[] = [
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,146 @@
import {
ItemConditionAnswer,
ReturnReasonAnswer,
YesNoAnswer,
PackageIncompleteAnswer,
ReturnProcessQuestionSchema,
ProductCategory
} from './constants';
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');
});
it('should define ReturnReasonAnswer values', () => {
expect(ReturnReasonAnswer.Dislike).toBe('dislike');
expect(ReturnReasonAnswer.WrongItem).toBe('wrong_item');
});
it('should define YesNoAnswer values', () => {
expect(YesNoAnswer.Yes).toBe('yes');
expect(YesNoAnswer.No).toBe('no');
});
it('should define PackageIncompleteAnswer values', () => {
expect(PackageIncompleteAnswer.OVP).toBe('ovp');
expect(PackageIncompleteAnswer.ChargingCable).toBe('charging_cable');
expect(PackageIncompleteAnswer.QuickStartGuide).toBe('quick_start_guide');
});
});
describe('ProductCategory', () => {
it('should define all product categories', () => {
expect(ProductCategory.BookCalendar).toBe('Buch/Kalender');
expect(ProductCategory.TonDatentraeger).toBe('Ton-/Datenträger');
expect(ProductCategory.SpielwarenPuzzle).toBe('Spielwaren/Puzzle');
expect(ProductCategory.SonstigesNonbook).toBe('Sonstiges und Nonbook');
expect(ProductCategory.ElektronischeGeraete).toBe('Andere Elektronische Geräte');
expect(ProductCategory.Tolino).toBe('Tolino');
});
});
describe('ReturnProcessQuestionSchema', () => {
it('should validate ItemCondition schema', () => {
const schema = ReturnProcessQuestionSchema[ReturnProcessQuestionKey.ItemCondition];
// Valid values
expect(schema.safeParse(ItemConditionAnswer.OVP).success).toBe(true);
expect(schema.safeParse(ItemConditionAnswer.Damaged).success).toBe(true);
// Invalid values
expect(schema.safeParse('invalid').success).toBe(false);
expect(schema.safeParse('').success).toBe(false);
expect(schema.safeParse(null).success).toBe(false);
});
it('should validate ReturnReason schema', () => {
const schema = ReturnProcessQuestionSchema[ReturnProcessQuestionKey.ReturnReason];
// Valid values
expect(schema.safeParse(ReturnReasonAnswer.Dislike).success).toBe(true);
expect(schema.safeParse(ReturnReasonAnswer.WrongItem).success).toBe(true);
// Invalid values
expect(schema.safeParse('invalid').success).toBe(false);
});
it('should validate YesNo schemas', () => {
const schemas = [
ReturnProcessQuestionSchema[ReturnProcessQuestionKey.ItemDefective],
ReturnProcessQuestionSchema[ReturnProcessQuestionKey.DisplayCondition],
ReturnProcessQuestionSchema[ReturnProcessQuestionKey.DevicePower],
ReturnProcessQuestionSchema[ReturnProcessQuestionKey.PackageComplete],
ReturnProcessQuestionSchema[ReturnProcessQuestionKey.CaseCondition],
ReturnProcessQuestionSchema[ReturnProcessQuestionKey.UsbPort],
];
schemas.forEach(schema => {
// Valid values
expect(schema.safeParse(YesNoAnswer.Yes).success).toBe(true);
expect(schema.safeParse(YesNoAnswer.No).success).toBe(true);
// Invalid values
expect(schema.safeParse('maybe').success).toBe(false);
});
});
it('should validate PackageIncomplete schema', () => {
const schema = ReturnProcessQuestionSchema[ReturnProcessQuestionKey.PackageIncomplete];
// Valid values - with options
expect(schema.safeParse({
options: [PackageIncompleteAnswer.OVP],
other: '',
}).success).toBe(true);
// Valid values - with multiple options
expect(schema.safeParse({
options: [
PackageIncompleteAnswer.OVP,
PackageIncompleteAnswer.ChargingCable
],
other: 'Some other missing part',
}).success).toBe(true);
// Valid values - with only other
expect(schema.safeParse({
options: [],
other: 'Something else is missing',
}).success).toBe(true);
// Invalid values
expect(schema.safeParse({}).success).toBe(false);
expect(schema.safeParse({ options: [] }).success).toBe(false);
expect(schema.safeParse({ options: ['invalid'] }).success).toBe(false);
});
it('should validate DeliveredItem schema', () => {
const schema = ReturnProcessQuestionSchema[ReturnProcessQuestionKey.DeliveredItem];
// Valid values
expect(schema.safeParse({
ean: '1234567890123',
format: 'Book',
formatDetails: 'Hardcover',
name: 'Test Book',
}).success).toBe(true);
// Invalid values - missing required fields
expect(schema.safeParse({
ean: '1234567890123',
format: 'Book',
}).success).toBe(false);
// Invalid values - wrong types
expect(schema.safeParse({
ean: 1234567890123,
format: 'Book',
formatDetails: 'Hardcover',
}).success).toBe(false);
});
});
});

View File

@@ -1,3 +1,6 @@
import { z } from 'zod';
import { ReturnProcessQuestionKey } from '../models';
/**
* Constants for product categories used in the return process.
*/
@@ -11,34 +14,158 @@ export const enum ProductCategory {
}
/**
* Constants for item condition values.
* Constant object for item condition answers in the return process.
* - OVP: Original packaging/as new condition
* - Damaged: Item is damaged or has printing errors
*/
export const enum ItemConditionValue {
OriginalPackaging = 'ovp',
Damaged = 'damaged',
}
export const ItemConditionAnswer = {
OVP: 'ovp',
Damaged: 'damaged',
} as const;
/**
* Constants for return reason values.
* Type representing valid item condition answers derived from the ItemConditionAnswer constant.
* Used for type checking and autocompletion when handling item condition responses.
*/
export const enum ReturnReasonValue {
Dislike = 'dislike',
WrongItem = 'wrong_item',
}
export type ItemConditionAnswer =
(typeof ItemConditionAnswer)[keyof typeof ItemConditionAnswer];
/**
* Constants for item defective values.
* Constant object for return reason answers in the return process.
* - Dislike: Customer doesn't like the item or wants to withdraw purchase
* - WrongItem: Customer received the wrong item (incorrect delivery)
*/
export const enum ItemDefectiveValue {
Yes = 'yes',
No = 'no',
}
export const ReturnReasonAnswer = {
Dislike: 'dislike',
WrongItem: 'wrong_item',
} as const;
export const enum PackageIncompleteValue {
// Karton/Umverpackung
Ovp = 'ovp',
// Ladekabel
CharchingCable = 'charching_cable',
// Quickstart Guide
QuickstartGuide = 'quickstart_guide',
}
/**
* Type representing valid return reason answers derived from the ReturnReasonAnswer constant.
* Used for type checking and autocompletion when handling return reason responses.
*/
export type ReturnReasonAnswer =
(typeof ReturnReasonAnswer)[keyof typeof ReturnReasonAnswer];
/**
* Constant object for yes/no answers in the return process.
* Used for boolean-type questions throughout the return process flow.
*/
export const YesNoAnswer = {
Yes: 'yes',
No: 'no',
} as const;
/**
* Type representing valid yes/no answers derived from the YesNoAnswer constant.
* Used for type checking and autocompletion when handling boolean responses.
*/
export type YesNoAnswer = (typeof YesNoAnswer)[keyof typeof YesNoAnswer];
/**
* Constant object for package incomplete answers in the return process.
* Specifies which parts of the package are missing during a return.
* - OVP: Original packaging/box is missing
* - ChargingCable: Charging cable is missing
* - QuickStartGuide: Quick start guide documentation is missing
*/
export const PackageIncompleteAnswer = {
OVP: 'ovp',
ChargingCable: 'charging_cable',
QuickStartGuide: 'quick_start_guide',
} as const;
/**
* Type representing valid package incomplete answers derived from the PackageIncompleteAnswer constant.
* Used for type checking and autocompletion when handling package incomplete responses.
*/
export type PackageIncompleteAnswer =
(typeof PackageIncompleteAnswer)[keyof typeof PackageIncompleteAnswer];
/**
* Zod schema definitions for validating return process question answers.
* Each schema corresponds to a specific question key from ReturnProcessQuestionKey
* and defines the expected format and allowed values for that question's answer.
*/
export const ReturnProcessQuestionSchema = {
[ReturnProcessQuestionKey.ItemCondition]: z.enum([
ItemConditionAnswer.OVP,
ItemConditionAnswer.Damaged,
]),
[ReturnProcessQuestionKey.ReturnReason]: z.enum([
ReturnReasonAnswer.Dislike,
ReturnReasonAnswer.WrongItem,
]),
[ReturnProcessQuestionKey.ItemDefective]: z.enum([
YesNoAnswer.Yes,
YesNoAnswer.No,
]),
[ReturnProcessQuestionKey.DisplayCondition]: z.enum([
YesNoAnswer.Yes,
YesNoAnswer.No,
]),
[ReturnProcessQuestionKey.DevicePower]: z.enum([
YesNoAnswer.Yes,
YesNoAnswer.No,
]),
[ReturnProcessQuestionKey.PackageComplete]: z.enum([
YesNoAnswer.Yes,
YesNoAnswer.No,
]),
[ReturnProcessQuestionKey.PackageIncomplete]: z
.object({
options: z
.array(
z.enum([
PackageIncompleteAnswer.OVP,
PackageIncompleteAnswer.ChargingCable,
PackageIncompleteAnswer.QuickStartGuide,
]),
)
.min(1),
other: z.string().optional(),
})
.or(
z.object({
options: z
.array(
z.enum([
PackageIncompleteAnswer.OVP,
PackageIncompleteAnswer.ChargingCable,
PackageIncompleteAnswer.QuickStartGuide,
]),
)
.optional(),
other: z.string(),
}),
),
[ReturnProcessQuestionKey.CaseCondition]: z.enum([
YesNoAnswer.Yes,
YesNoAnswer.No,
]),
[ReturnProcessQuestionKey.UsbPort]: z.enum([YesNoAnswer.Yes, YesNoAnswer.No]),
[ReturnProcessQuestionKey.DeliveredItem]: z.object({
ean: z.string(),
format: z.string(),
formatDetails: z.string(),
additionalName: z.string().optional(),
catalogProductNumber: z.string().optional(),
contributors: z.string().optional(),
edition: z.string().optional(),
formatDetail: z.string().optional(),
locale: z.string().optional(),
productGroup: z.string().optional(),
manufacturer: z.string().optional(),
name: z.string().optional(),
productGroupDetails: z.string().optional(),
publicationDate: z.string().optional(),
serial: z.string().optional(),
}),
} as const;
/**
* Type representing any valid schema from ReturnProcessQuestionSchema.
* Used for type checking when working with question schemas dynamically.
*/
export type ReturnProcessQuestionSchema =
(typeof ReturnProcessQuestionSchema)[keyof typeof ReturnProcessQuestionSchema];

View File

@@ -1,86 +0,0 @@
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

@@ -1,20 +1,26 @@
import {
ReturnProcessAnswers,
ReturnProcessQuestion,
ReturnProcessQuestionKey,
ReturnProcessQuestionType,
EligibleForReturn,
EligibleForReturnState,
} from '../models';
import {
ItemConditionValue,
ReturnReasonValue,
ItemDefectiveValue,
ItemConditionAnswer,
ReturnReasonAnswer,
YesNoAnswer,
} from './constants';
import { validateOvpReturn } from './validators';
/**
* Questions for the return process of other electronic devices.
*
* This array defines the sequence and branching logic of questions presented to users
* when processing electronic device returns. The question flow adapts dynamically
* based on the customer's responses to guide them through the appropriate return path.
*
* The question sequence typically includes:
* 1. Item condition assessment (original packaging or opened/damaged)
* 2. Return reason inquiry for items in original packaging
* 3. Defect verification for opened/damaged items
* 4. Collection of information about incorrectly delivered items when applicable
*/
export const elektronischeGeraeteQuestions: ReturnProcessQuestion[] = [
{
@@ -24,12 +30,12 @@ export const elektronischeGeraeteQuestions: ReturnProcessQuestion[] = [
options: [
{
label: 'Originalverpackt',
value: ItemConditionValue.OriginalPackaging,
value: ItemConditionAnswer.OVP,
nextQuestion: ReturnProcessQuestionKey.ReturnReason,
},
{
label: 'Geöffnet / Beschädigt',
value: ItemConditionValue.Damaged,
value: ItemConditionAnswer.Damaged,
nextQuestion: ReturnProcessQuestionKey.ItemDefective,
},
],
@@ -39,10 +45,10 @@ export const elektronischeGeraeteQuestions: ReturnProcessQuestion[] = [
description: 'Warum möchtest du den Artikel zurückgeben?',
type: ReturnProcessQuestionType.Select,
options: [
{ label: 'Gefällt nicht/Wiederruf', value: ReturnReasonValue.Dislike },
{ label: 'Gefällt nicht/Wiederruf', value: ReturnReasonAnswer.Dislike },
{
label: 'Fehllieferung',
value: ReturnReasonValue.WrongItem,
value: ReturnReasonAnswer.WrongItem,
nextQuestion: ReturnProcessQuestionKey.DeliveredItem,
},
],
@@ -54,11 +60,11 @@ export const elektronischeGeraeteQuestions: ReturnProcessQuestion[] = [
options: [
{
label: 'Ja',
value: ItemDefectiveValue.Yes,
value: YesNoAnswer.Yes,
},
{
label: 'Nein',
value: ItemDefectiveValue.No,
value: YesNoAnswer.No,
},
],
},
@@ -68,39 +74,3 @@ export const elektronischeGeraeteQuestions: ReturnProcessQuestion[] = [
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

@@ -15,12 +15,6 @@
// 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';

View File

@@ -1,82 +0,0 @@
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

@@ -1,16 +1,20 @@
import {
ReturnProcessAnswers,
ReturnProcessQuestion,
ReturnProcessQuestionKey,
ReturnProcessQuestionType,
EligibleForReturn,
EligibleForReturnState,
} from '../models';
import { ItemConditionValue, ReturnReasonValue } from './constants';
import { validateOvpReturn } from './validators';
import { ItemConditionAnswer, ReturnReasonAnswer } from './constants';
/**
* Questions for the return process of Spielwaren/Puzzle and other nonbook items.
*
* This array defines the sequence and logic flow of questions presented to users
* when processing returns for toys, puzzles, and other nonbook items. The questions
* follow a branching structure that adapts based on the customer's responses.
*
* The question flow generally starts with item condition assessment, followed by
* return reason inquiry, and may include additional questions about the delivered
* item when applicable.
*/
export const nonbookQuestions: ReturnProcessQuestion[] = [
{
@@ -20,10 +24,10 @@ export const nonbookQuestions: ReturnProcessQuestion[] = [
options: [
{
label: 'Neuwertig/Originalverpackt',
value: ItemConditionValue.OriginalPackaging,
value: ItemConditionAnswer.OVP,
nextQuestion: ReturnProcessQuestionKey.ReturnReason,
},
{ label: 'Beschädigt/Fehldruck', value: ItemConditionValue.Damaged },
{ label: 'Beschädigt/Fehldruck', value: ItemConditionAnswer.Damaged },
],
},
{
@@ -31,10 +35,10 @@ export const nonbookQuestions: ReturnProcessQuestion[] = [
description: 'Warum möchtest du den Artikel zurückgeben?',
type: ReturnProcessQuestionType.Select,
options: [
{ label: 'Gefällt nicht/Wiederruf', value: ReturnReasonValue.Dislike },
{ label: 'Gefällt nicht/Wiederruf', value: ReturnReasonAnswer.Dislike },
{
label: 'Fehllieferung',
value: ReturnReasonValue.WrongItem,
value: ReturnReasonAnswer.WrongItem,
nextQuestion: ReturnProcessQuestionKey.DeliveredItem,
},
],
@@ -45,28 +49,3 @@ export const nonbookQuestions: ReturnProcessQuestion[] = [
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

@@ -1,81 +0,0 @@
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

@@ -1,23 +1,20 @@
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';
import { bookCalendarQuestions } from './book-calendar';
import { tonDatentraegerQuestions } from './ton-datentraeger';
import { nonbookQuestions } from './nonbook';
import { elektronischeGeraeteQuestions } from './elektronische-geraete';
import { tolinoQuestions } from './tolino';
/**
* A mapping of categories to their respective return process questions.
* A mapping of product categories to their respective return process questions.
* This registry centrally connects each product category with its specific
* question flow, allowing the application to dynamically load the appropriate
* questions based on the product being returned.
*
* Each category maps to an array of ReturnProcessQuestion objects that define
* the questions, their sequence, and the branching logic for that category.
*/
export const CategoryQuestions: Record<
ProductCategory,
@@ -30,18 +27,3 @@ export const CategoryQuestions: Record<
[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

@@ -1,28 +0,0 @@
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 });
});
});
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

@@ -1,22 +1,32 @@
import {
ReturnProcessAnswers,
ReturnProcessQuestion,
ReturnProcessQuestionKey,
ReturnProcessQuestionType,
EligibleForReturn,
EligibleForReturnState,
} from '../models';
import {
ItemConditionValue,
ItemDefectiveValue,
PackageIncompleteValue,
ReturnReasonValue,
ItemConditionAnswer,
PackageIncompleteAnswer,
ReturnReasonAnswer,
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.
*/
@@ -28,11 +38,11 @@ export const tolinoQuestions: ReturnProcessQuestion[] = [
options: [
{
label: 'Originalverpackt',
value: ItemConditionValue.OriginalPackaging,
value: ItemConditionAnswer.OVP,
},
{
label: 'Geöffnet/Beschädigt',
value: ItemConditionValue.Damaged,
value: ItemConditionAnswer.Damaged,
nextQuestion: ReturnProcessQuestionKey.DevicePower,
},
],
@@ -44,12 +54,13 @@ export const tolinoQuestions: ReturnProcessQuestion[] = [
options: [
{
label: 'Ja',
value: ItemDefectiveValue.No,
value: YesNoAnswer.Yes,
nextQuestion: ReturnProcessQuestionKey.PackageComplete,
},
{
label: 'Nein',
value: ItemDefectiveValue.Yes,
value: YesNoAnswer.No,
nextQuestion: ReturnProcessQuestionKey.PackageComplete,
},
],
},
@@ -60,13 +71,13 @@ export const tolinoQuestions: ReturnProcessQuestion[] = [
options: [
{
label: 'Ja',
value: ItemDefectiveValue.No,
nextQuestion: ReturnProcessQuestionKey.PackageIncomplete,
value: YesNoAnswer.Yes,
nextQuestion: ReturnProcessQuestionKey.CaseCondition,
},
{
label: 'Nein',
value: ItemDefectiveValue.Yes,
nextQuestion: ReturnProcessQuestionKey.CaseCondition,
value: YesNoAnswer.No,
nextQuestion: ReturnProcessQuestionKey.PackageIncomplete,
},
],
},
@@ -77,15 +88,15 @@ export const tolinoQuestions: ReturnProcessQuestion[] = [
options: [
{
label: 'Karton/Umverpackung',
value: PackageIncompleteValue.Ovp,
value: PackageIncompleteAnswer.OVP,
},
{
label: 'Ladekabel',
value: PackageIncompleteValue.CharchingCable,
value: PackageIncompleteAnswer.ChargingCable,
},
{
label: 'Quickstart Guide',
value: PackageIncompleteValue.QuickstartGuide,
value: PackageIncompleteAnswer.QuickStartGuide,
},
],
other: {
@@ -100,12 +111,12 @@ export const tolinoQuestions: ReturnProcessQuestion[] = [
options: [
{
label: 'Ja',
value: ItemDefectiveValue.Yes,
value: YesNoAnswer.Yes,
nextQuestion: ReturnProcessQuestionKey.DisplayCondition,
},
{
label: 'Nein',
value: ItemDefectiveValue.No,
value: YesNoAnswer.No,
nextQuestion: ReturnProcessQuestionKey.UsbPort,
},
],
@@ -117,11 +128,12 @@ export const tolinoQuestions: ReturnProcessQuestion[] = [
options: [
{
label: 'Ja',
value: ItemDefectiveValue.Yes,
value: YesNoAnswer.Yes,
nextQuestion: ReturnProcessQuestionKey.UsbPort,
},
{
label: 'Nein',
value: ItemDefectiveValue.No,
value: YesNoAnswer.No,
nextQuestion: ReturnProcessQuestionKey.UsbPort,
},
],
@@ -133,12 +145,12 @@ export const tolinoQuestions: ReturnProcessQuestion[] = [
options: [
{
label: 'Ja',
value: ItemDefectiveValue.No,
value: YesNoAnswer.Yes,
nextQuestion: ReturnProcessQuestionKey.ReturnReason,
},
{
label: 'Nein',
value: ItemDefectiveValue.Yes,
value: YesNoAnswer.No,
},
],
},
@@ -149,23 +161,12 @@ export const tolinoQuestions: ReturnProcessQuestion[] = [
options: [
{
label: 'Gefällt nicht/Wiederruf',
value: ReturnReasonValue.Dislike,
value: ReturnReasonAnswer.Dislike,
},
{
label: 'Fehllieferung',
value: ReturnReasonValue.WrongItem,
value: ReturnReasonAnswer.WrongItem,
},
],
},
];
/**
* Validates the answers for the Tolino return process and determines return eligibility.
* @param {ReturnProcessAnswers} answers - A record of answers keyed by question keys
* @returns {EligibleForReturn} Object containing eligibility state and reason if applicable
*/
export function validateTolinoQuestions(
answers: ReturnProcessAnswers,
): EligibleForReturn {
return { state: EligibleForReturnState.Eligible };
}

View File

@@ -1,63 +0,0 @@
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

@@ -6,12 +6,19 @@ import {
EligibleForReturn,
EligibleForReturnState,
} from '../models';
import { ItemConditionValue, ItemDefectiveValue } from './constants';
import { ItemConditionAnswer, YesNoAnswer } from './constants';
/**
* Questions for the return process of Ton-/Datenträger (audio/data carriers).
* This array defines the sequence and logic flow of questions presented to users
* when processing audio or data carrier returns in the system.
*
* The question flow for audio/data carriers is specifically designed to handle
* the unique return policies that apply to these products:
* - Sealed items are typically eligible for return
* - Opened items may be eligible if they're defective
* - Opened, non-defective items might be subject to different return policies
*
* Each question has a unique key, descriptive text, question type, and possible options
* with their corresponding next question in the flow.
*/
@@ -23,11 +30,11 @@ export const tonDatentraegerQuestions: ReturnProcessQuestion[] = [
options: [
{
label: 'Versiegelt/Originalverpackt',
value: ItemConditionValue.OriginalPackaging,
value: ItemConditionAnswer.OVP,
},
{
label: 'Geöffnet',
value: ItemConditionValue.Damaged,
value: ItemConditionAnswer.Damaged,
nextQuestion: ReturnProcessQuestionKey.ItemDefective,
},
],
@@ -39,62 +46,12 @@ export const tonDatentraegerQuestions: ReturnProcessQuestion[] = [
options: [
{
label: 'Ja',
value: ItemDefectiveValue.Yes,
value: YesNoAnswer.Yes,
},
{
label: 'Nein',
value: ItemDefectiveValue.No,
value: YesNoAnswer.No,
},
],
},
];
/**
* Validates the answers for the Ton-/Datenträger (audio/data carriers) return process and determines return eligibility.
*
* The validation logic follows these rules:
* 1. If the item is in original packaging/sealed, it's always eligible for return
* 2. If the item has been opened, it's only eligible if it's defective
* 3. If the item has been opened but is not defective, it's not eligible for return
* 4. If invalid or incomplete answers are provided, the item is not eligible
*
* @param {ReturnProcessAnswers} answers - A record of answers keyed by question keys
* @returns {EligibleForReturn} Object containing eligibility state and reason if applicable
*/
export function validateTonDatentraegerQuestions(
answers: ReturnProcessAnswers,
): 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

@@ -1,9 +0,0 @@
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

@@ -1,54 +0,0 @@
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

@@ -1,36 +0,0 @@
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

@@ -6,13 +6,14 @@ import {
ReturnProcessQuestion,
ReturnProcessQuestionType,
} from './models';
import { CategoryQuestions, CategoryQuestionValidators } from './questions';
import { CategoryQuestions, ReturnProcessQuestionSchema } from './questions';
import { KeyValue } from '@angular/common';
import { ReturnProcessChecklistAnswerSchema } from './schemas';
/**
* Service responsible for managing the return process workflow.
* Handles questions, validation, and eligibility determination for product returns.
* Uses Zod schemas for data validation throughout the return process.
*/
@Injectable({ providedIn: 'root' })
export class ReturnProcessService {
@@ -45,32 +46,19 @@ export class ReturnProcessService {
return undefined;
}
/**
* Gets the validator function for a specific return process based on product category.
*
* @param {ReturnProcess} process - The return process containing product information.
* @returns {((answers: Record<string, unknown>) => EligibleForReturn) | undefined} Validator function or undefined if no matching category found.
*/
returnProcessQuestionValidator(
process: ReturnProcess,
): ((answers: Record<string, unknown>) => EligibleForReturn) | undefined {
const category =
process.productCategory || process.receiptItem.features?.['category'];
if (category) {
return CategoryQuestionValidators[
category as keyof typeof CategoryQuestionValidators
];
}
return undefined;
}
/**
* Gets active questions in the return process based on previously provided answers.
* Handles question branching logic and detects cyclic dependencies.
*
* This method:
* 1. Determines which questions to show based on previous answers
* 2. Validates answers using Zod schemas to ensure data integrity
* 3. Handles different question types (Select, Product, Checklist)
* 4. Follows the branching logic defined by nextQuestion properties
*
* @param {ReturnProcess} process - The return process containing answers and product information.
* @returns {ReturnProcessQuestion[] | undefined} Active questions in the process or undefined if no questions apply.
* @throws {Error} If cyclic question dependencies are detected in the question flow.
*/
activeReturnProcessQuestions(
process: ReturnProcess,
@@ -87,8 +75,7 @@ export class ReturnProcessService {
while (questionKey) {
if (visited.has(questionKey)) {
console.error('Cyclic question dependency detected', questionKey);
break;
throw new Error('Cyclic question dependency detected');
}
visited.add(questionKey);
@@ -96,6 +83,14 @@ export class ReturnProcessService {
if (question) {
result.push(question);
const schema = ReturnProcessQuestionSchema[question.key];
const parseResult = schema.safeParse(process.answers[question.key]);
if (!parseResult.success) {
break;
}
if (question.type === ReturnProcessQuestionType.Select) {
const option = question.options.find(
(o) => o.value === process.answers[question.key],
@@ -106,11 +101,11 @@ export class ReturnProcessService {
? question.nextQuestion
: undefined;
} else if (question.type === ReturnProcessQuestionType.Checklist) {
const answer = ReturnProcessChecklistAnswerSchema.parse(
const answer = ReturnProcessChecklistAnswerSchema.optional().parse(
process.answers[question.key],
);
if (answer.options?.length || answer.other) {
if ((answer && answer.options?.length) || (answer && answer.other)) {
questionKey = question.nextQuestion;
} else {
questionKey = undefined;
@@ -205,37 +200,47 @@ export class ReturnProcessService {
/**
* Determines whether a product is eligible for return based on provided answers.
* Validates all questions have been answered and applies category-specific validation rules.
* Uses schema validation to verify answers and determines eligibility status.
*
* The method has been refactored to:
* 1. Use schema validation instead of custom validator functions
* 2. Return undefined if questions are not answered completely
* 3. Always return eligible status for complete and validated answers
*
* @param {ReturnProcess} returnProcess - The return process containing answers and product information.
* @returns {EligibleForReturn} Object indicating eligibility state and reason.
* @returns {EligibleForReturn | undefined} Object indicating eligibility state or undefined if answers incomplete.
*/
eligibleForReturn(returnProcess: ReturnProcess): EligibleForReturn {
eligibleForReturn(
returnProcess: ReturnProcess,
): EligibleForReturn | undefined {
const questions = this.activeReturnProcessQuestions(returnProcess);
if (!questions) {
return { state: EligibleForReturnState.Pending };
return undefined;
}
const everyQuestionAnswered = questions.every(
(q) => q.key in returnProcess.answers,
);
if (!everyQuestionAnswered) {
return {
state: EligibleForReturnState.Pending,
reason: 'Not all questions answered',
};
return undefined;
}
const validator = this.returnProcessQuestionValidator(returnProcess);
const allQuestionsAnswered = questions.every((q) => {
if (q.type === ReturnProcessQuestionType.Checklist) {
const answer = ReturnProcessChecklistAnswerSchema.optional().parse(
returnProcess.answers[q.key],
);
return (answer && answer.options?.length) || (answer && answer.other);
} else {
return q.key in returnProcess.answers;
}
});
if (!validator) {
return {
state: EligibleForReturnState.Pending,
reason: 'No validator found',
};
if (!allQuestionsAnswered) {
return undefined;
}
return validator(returnProcess.answers);
return { state: EligibleForReturnState.Eligible };
}
}

View File

@@ -12,7 +12,7 @@
</div>
<div
class="return-process-item-body border-b-isa-neutral-300"
[class.border-b]="eligible.state !== 'pending'"
[class.border-b]="eligible"
>
<div class="return-process-item-body__product">
<img
@@ -43,7 +43,7 @@
[returnProcessId]="returnProcessId()"
></oms-feature-return-process-questions>
</div>
@switch (eligible.state) {
@switch (eligible?.state) {
@case ('eligible') {
<div
class="text-isa-accent-green isa-text-body-2-bold flex items-center gap-1"