feat(checkout): implement reward order confirmation UI

Implement the complete UI for the reward order confirmation page including address displays, order item lists, and supporting helper functions.

Features:
- Add order confirmation addresses component displaying billing, delivery, and pickup branch addresses
- Implement order confirmation item list with order type icons and item details
- Add helper functions for order type feature checking and address/branch deduplication
- Integrate store computed properties for payers, shipping addresses, and target branches
- Apply responsive layout with Tailwind CSS styling
This commit is contained in:
Lorenz Hilpert
2025-10-21 17:39:52 +02:00
parent 5b04a29e17
commit ee2d9ba43a
19 changed files with 1225 additions and 41 deletions

View File

@@ -0,0 +1,498 @@
import { describe, it, expect } from 'vitest';
import {
areAddresseesEqual,
areBranchesEqual,
deduplicateAddressees,
deduplicateBranches,
} from './deduplicate-addressees.helper';
import {
DisplayAddresseeDTO,
DisplayBranchDTO,
AddressDTO,
} from '@generated/swagger/oms-api';
describe('areAddresseesEqual', () => {
const sampleAddress: AddressDTO = {
street: 'Teststraße',
streetNumber: '123',
city: 'Berlin',
zipCode: '10115',
country: 'DEU',
};
const createAddressee = (
firstName: string,
lastName: string,
address?: AddressDTO,
): DisplayAddresseeDTO => ({
firstName,
lastName,
address,
gender: 0, // NotSet
});
it('should return true when both addressees are undefined', () => {
// Act
const result = areAddresseesEqual(undefined, undefined);
// Assert
expect(result).toBe(true);
});
it('should return false when only first addressee is undefined', () => {
// Arrange
const addressee = createAddressee('John', 'Doe', sampleAddress);
// Act
const result = areAddresseesEqual(undefined, addressee);
// Assert
expect(result).toBe(false);
});
it('should return false when only second addressee is undefined', () => {
// Arrange
const addressee = createAddressee('John', 'Doe', sampleAddress);
// Act
const result = areAddresseesEqual(addressee, undefined);
// Assert
expect(result).toBe(false);
});
it('should return true when addressees have same name and address', () => {
// Arrange
const addressee1 = createAddressee('John', 'Doe', sampleAddress);
const addressee2 = createAddressee('John', 'Doe', { ...sampleAddress });
// Act
const result = areAddresseesEqual(addressee1, addressee2);
// Assert
expect(result).toBe(true);
});
it('should return false when addressees have different first names', () => {
// Arrange
const addressee1 = createAddressee('John', 'Doe', sampleAddress);
const addressee2 = createAddressee('Jane', 'Doe', sampleAddress);
// Act
const result = areAddresseesEqual(addressee1, addressee2);
// Assert
expect(result).toBe(false);
});
it('should return false when addressees have different last names', () => {
// Arrange
const addressee1 = createAddressee('John', 'Doe', sampleAddress);
const addressee2 = createAddressee('John', 'Smith', sampleAddress);
// Act
const result = areAddresseesEqual(addressee1, addressee2);
// Assert
expect(result).toBe(false);
});
it('should return false when addressees have different addresses', () => {
// Arrange
const addressee1 = createAddressee('John', 'Doe', sampleAddress);
const addressee2 = createAddressee('John', 'Doe', {
...sampleAddress,
street: 'Other Street',
});
// Act
const result = areAddresseesEqual(addressee1, addressee2);
// Assert
expect(result).toBe(false);
});
it('should return true when both addressees have no address', () => {
// Arrange
const addressee1 = createAddressee('John', 'Doe');
const addressee2 = createAddressee('John', 'Doe');
// Act
const result = areAddresseesEqual(addressee1, addressee2);
// Assert
expect(result).toBe(true);
});
it('should return false when one has address and other does not', () => {
// Arrange
const addressee1 = createAddressee('John', 'Doe', sampleAddress);
const addressee2 = createAddressee('John', 'Doe');
// Act
const result = areAddresseesEqual(addressee1, addressee2);
// Assert
expect(result).toBe(false);
});
it('should handle empty string names', () => {
// Arrange
const addressee1 = createAddressee('', '', sampleAddress);
const addressee2 = createAddressee('', '', sampleAddress);
// Act
const result = areAddresseesEqual(addressee1, addressee2);
// Assert
expect(result).toBe(true);
});
it('should treat undefined names as empty strings', () => {
// Arrange
const addressee1: DisplayAddresseeDTO = {
gender: 0, // NotSet
address: sampleAddress,
};
const addressee2: DisplayAddresseeDTO = {
firstName: '',
lastName: '',
gender: 0, // NotSet
address: sampleAddress,
};
// Act
const result = areAddresseesEqual(addressee1, addressee2);
// Assert
expect(result).toBe(true);
});
});
describe('deduplicateAddressees', () => {
const sampleAddress: AddressDTO = {
street: 'Teststraße',
streetNumber: '123',
city: 'Berlin',
zipCode: '10115',
country: 'DEU',
};
const createAddressee = (
firstName: string,
lastName: string,
address?: AddressDTO,
): DisplayAddresseeDTO => ({
firstName,
lastName,
address,
gender: 0, // NotSet
});
it('should return empty array when input is empty', () => {
// Arrange
const addressees: DisplayAddresseeDTO[] = [];
// Act
const result = deduplicateAddressees(addressees);
// Assert
expect(result).toEqual([]);
});
it('should filter out undefined values', () => {
// Arrange
const addressee = createAddressee('John', 'Doe', sampleAddress);
const addressees = [undefined, addressee, undefined];
// Act
const result = deduplicateAddressees(addressees);
// Assert
expect(result).toHaveLength(1);
expect(result[0]).toEqual(addressee);
});
it('should return single item when array has one addressee', () => {
// Arrange
const addressee = createAddressee('John', 'Doe', sampleAddress);
const addressees = [addressee];
// Act
const result = deduplicateAddressees(addressees);
// Assert
expect(result).toHaveLength(1);
expect(result[0]).toEqual(addressee);
});
it('should return all items when no duplicates exist', () => {
// Arrange
const addressees = [
createAddressee('John', 'Doe', sampleAddress),
createAddressee('Jane', 'Smith', sampleAddress),
createAddressee('Bob', 'Johnson', {
...sampleAddress,
street: 'Other Street',
}),
];
// Act
const result = deduplicateAddressees(addressees);
// Assert
expect(result).toHaveLength(3);
});
it('should remove duplicate addressees keeping first occurrence', () => {
// Arrange
const addressee1 = createAddressee('John', 'Doe', sampleAddress);
const addressee2 = createAddressee('John', 'Doe', { ...sampleAddress });
const addressees = [addressee1, addressee2];
// Act
const result = deduplicateAddressees(addressees);
// Assert
expect(result).toHaveLength(1);
expect(result[0]).toBe(addressee1); // First occurrence kept
});
it('should handle multiple duplicates and keep only first', () => {
// Arrange
const addressee1 = createAddressee('John', 'Doe', sampleAddress);
const addressee2 = createAddressee('John', 'Doe', { ...sampleAddress });
const addressee3 = createAddressee('John', 'Doe', { ...sampleAddress });
const addressees = [addressee1, addressee2, addressee3];
// Act
const result = deduplicateAddressees(addressees);
// Assert
expect(result).toHaveLength(1);
expect(result[0]).toBe(addressee1);
});
it('should not consider different names but same address as duplicates', () => {
// Arrange
const addressees = [
createAddressee('John', 'Doe', sampleAddress),
createAddressee('Jane', 'Doe', sampleAddress),
];
// Act
const result = deduplicateAddressees(addressees);
// Assert
expect(result).toHaveLength(2);
});
it('should not consider same names but different addresses as duplicates', () => {
// Arrange
const addressees = [
createAddressee('John', 'Doe', sampleAddress),
createAddressee('John', 'Doe', {
...sampleAddress,
street: 'Other Street',
}),
];
// Act
const result = deduplicateAddressees(addressees);
// Assert
expect(result).toHaveLength(2);
});
it('should handle complex scenario with mixed duplicates and unique items', () => {
// Arrange
const addressee1 = createAddressee('John', 'Doe', sampleAddress);
const addressee2 = createAddressee('John', 'Doe', { ...sampleAddress }); // Duplicate of 1
const addressee3 = createAddressee('Jane', 'Smith', sampleAddress);
const addressee4 = createAddressee('Jane', 'Smith', { ...sampleAddress }); // Duplicate of 3
const addressee5 = createAddressee('Bob', 'Johnson', sampleAddress);
const addressees = [
addressee1,
addressee2,
addressee3,
addressee4,
addressee5,
undefined,
];
// Act
const result = deduplicateAddressees(addressees);
// Assert
expect(result).toHaveLength(3);
expect(result[0]).toBe(addressee1);
expect(result[1]).toBe(addressee3);
expect(result[2]).toBe(addressee5);
});
it('should preserve order of first occurrences', () => {
// Arrange
const addressee1 = createAddressee('Alice', 'Wonder', sampleAddress);
const addressee2 = createAddressee('Bob', 'Builder', sampleAddress);
const addressee3 = createAddressee('Alice', 'Wonder', { ...sampleAddress }); // Duplicate
const addressees = [addressee1, addressee2, addressee3];
// Act
const result = deduplicateAddressees(addressees);
// Assert
expect(result).toHaveLength(2);
expect(result[0]).toBe(addressee1);
expect(result[1]).toBe(addressee2);
});
});
describe('areBranchesEqual', () => {
const sampleAddress: AddressDTO = {
street: 'Teststraße',
streetNumber: '123',
city: 'Berlin',
zipCode: '10115',
country: 'DEU',
};
const createBranch = (
name: string,
address?: AddressDTO,
): DisplayBranchDTO => ({
name,
address,
});
it('should return true when both branches are undefined', () => {
// Act
const result = areBranchesEqual(undefined, undefined);
// Assert
expect(result).toBe(true);
});
it('should return false when only first branch is undefined', () => {
// Arrange
const branch = createBranch('Branch 1', sampleAddress);
// Act
const result = areBranchesEqual(undefined, branch);
// Assert
expect(result).toBe(false);
});
it('should return true when branches have same name and address', () => {
// Arrange
const branch1 = createBranch('Branch 1', sampleAddress);
const branch2 = createBranch('Branch 1', { ...sampleAddress });
// Act
const result = areBranchesEqual(branch1, branch2);
// Assert
expect(result).toBe(true);
});
it('should return false when branches have different names', () => {
// Arrange
const branch1 = createBranch('Branch 1', sampleAddress);
const branch2 = createBranch('Branch 2', sampleAddress);
// Act
const result = areBranchesEqual(branch1, branch2);
// Assert
expect(result).toBe(false);
});
it('should return false when branches have different addresses', () => {
// Arrange
const branch1 = createBranch('Branch 1', sampleAddress);
const branch2 = createBranch('Branch 1', {
...sampleAddress,
street: 'Other Street',
});
// Act
const result = areBranchesEqual(branch1, branch2);
// Assert
expect(result).toBe(false);
});
});
describe('deduplicateBranches', () => {
const sampleAddress: AddressDTO = {
street: 'Teststraße',
streetNumber: '123',
city: 'Berlin',
zipCode: '10115',
country: 'DEU',
};
const createBranch = (
name: string,
address?: AddressDTO,
): DisplayBranchDTO => ({
name,
address,
});
it('should return empty array when input is empty', () => {
// Arrange
const branches: DisplayBranchDTO[] = [];
// Act
const result = deduplicateBranches(branches);
// Assert
expect(result).toEqual([]);
});
it('should filter out undefined values', () => {
// Arrange
const branch = createBranch('Branch 1', sampleAddress);
const branches = [undefined, branch, undefined];
// Act
const result = deduplicateBranches(branches);
// Assert
expect(result).toHaveLength(1);
expect(result[0]).toEqual(branch);
});
it('should remove duplicate branches keeping first occurrence', () => {
// Arrange
const branch1 = createBranch('Branch 1', sampleAddress);
const branch2 = createBranch('Branch 1', { ...sampleAddress });
const branches = [branch1, branch2];
// Act
const result = deduplicateBranches(branches);
// Assert
expect(result).toHaveLength(1);
expect(result[0]).toBe(branch1);
});
it('should return all items when no duplicates exist', () => {
// Arrange
const branches = [
createBranch('Branch 1', sampleAddress),
createBranch('Branch 2', sampleAddress),
createBranch('Branch 3', {
...sampleAddress,
street: 'Other Street',
}),
];
// Act
const result = deduplicateBranches(branches);
// Assert
expect(result).toHaveLength(3);
});
});

View File

@@ -0,0 +1,207 @@
import {
DisplayAddresseeDTO,
DisplayBranchDTO,
} from '@generated/swagger/oms-api';
/**
* Internal model representing an entity with name and address for comparison.
* This allows for type-safe comparison without casting.
*/
interface EntityWithNameAndAddress {
readonly name: string;
readonly address: unknown;
}
/**
* Converts an addressee-like object to a comparable entity.
*/
function toComparableAddressee(
addressee: Pick<DisplayAddresseeDTO, 'firstName' | 'lastName' | 'address'>,
): EntityWithNameAndAddress {
const firstName = addressee.firstName ?? '';
const lastName = addressee.lastName ?? '';
const name = `${firstName}|${lastName}`.trim();
return {
name,
address: addressee.address ?? null,
};
}
/**
* Converts a branch-like object to a comparable entity.
*/
function toComparableBranch(
branch: Pick<DisplayBranchDTO, 'name' | 'address'>,
): EntityWithNameAndAddress {
return {
name: branch.name ?? '',
address: branch.address ?? null,
};
}
/**
* Compares two entities for equality based on name and address.
*/
function areEntitiesEqual(
entity1: EntityWithNameAndAddress,
entity2: EntityWithNameAndAddress,
): boolean {
if (entity1.name !== entity2.name) {
return false;
}
const address1Str = JSON.stringify(entity1.address);
const address2Str = JSON.stringify(entity2.address);
return address1Str === address2Str;
}
/**
* Compares two DisplayAddresseeDTO objects for equality based on name and address.
* Two addressees are considered equal if they have the same firstName, lastName, and address.
*
* @param addressee1 - First addressee to compare
* @param addressee2 - Second addressee to compare
* @returns true if addressees are equal, false otherwise
*/
export function areAddresseesEqual(
addressee1: DisplayAddresseeDTO | undefined,
addressee2: DisplayAddresseeDTO | undefined,
): boolean {
if (!addressee1 && !addressee2) {
return true;
}
if (!addressee1 || !addressee2) {
return false;
}
return areEntitiesEqual(
toComparableAddressee(addressee1),
toComparableAddressee(addressee2),
);
}
/**
* Compares two DisplayBranchDTO objects for equality based on name and address.
* Two branches are considered equal if they have the same name and address.
*
* @param branch1 - First branch to compare
* @param branch2 - Second branch to compare
* @returns true if branches are equal, false otherwise
*/
export function areBranchesEqual(
branch1: DisplayBranchDTO | undefined,
branch2: DisplayBranchDTO | undefined,
): boolean {
if (!branch1 && !branch2) {
return true;
}
if (!branch1 || !branch2) {
return false;
}
return areEntitiesEqual(
toComparableBranch(branch1),
toComparableBranch(branch2),
);
}
/**
* Type guard to check if an object can be compared as an addressee.
*/
function hasAddresseeShape(
obj: unknown,
): obj is Pick<DisplayAddresseeDTO, 'firstName' | 'lastName' | 'address'> {
return (
obj !== null &&
typeof obj === 'object' &&
('firstName' in obj || 'lastName' in obj || 'address' in obj)
);
}
/**
* Type guard to check if an object can be compared as a branch.
*/
function hasBranchShape(
obj: unknown,
): obj is Pick<DisplayBranchDTO, 'name' | 'address'> {
return (
obj !== null &&
typeof obj === 'object' &&
('name' in obj || 'address' in obj)
);
}
/**
* Removes duplicate addressees from an array, keeping only the first occurrence.
* Two addressees are considered duplicates if they have the same firstName, lastName, and address.
*
* @param addressees - Array of addressees to deduplicate
* @returns Deduplicated array with only first occurrence of each unique addressee
*/
export function deduplicateAddressees<
T extends Pick<DisplayAddresseeDTO, 'firstName' | 'lastName' | 'address'>,
>(addressees: readonly (T | undefined)[]): T[] {
const result: T[] = [];
const seen: T[] = [];
for (const addressee of addressees) {
if (!addressee || !hasAddresseeShape(addressee)) {
continue;
}
const isDuplicate = seen.some((seenAddressee) =>
hasAddresseeShape(seenAddressee)
? areEntitiesEqual(
toComparableAddressee(seenAddressee),
toComparableAddressee(addressee),
)
: false,
);
if (!isDuplicate) {
result.push(addressee);
seen.push(addressee);
}
}
return result;
}
/**
* Removes duplicate branches from an array, keeping only the first occurrence.
* Two branches are considered duplicates if they have the same name and address.
*
* @param branches - Array of branches to deduplicate
* @returns Deduplicated array with only first occurrence of each unique branch
*/
export function deduplicateBranches<
T extends Pick<DisplayBranchDTO, 'name' | 'address'>,
>(branches: readonly (T | undefined)[]): T[] {
const result: T[] = [];
const seen: T[] = [];
for (const branch of branches) {
if (!branch || !hasBranchShape(branch)) {
continue;
}
const isDuplicate = seen.some((seenBranch) =>
hasBranchShape(seenBranch)
? areEntitiesEqual(
toComparableBranch(seenBranch),
toComparableBranch(branch),
)
: false,
);
if (!isDuplicate) {
result.push(branch);
seen.push(branch);
}
}
return result;
}

View File

@@ -1,2 +1,8 @@
export {
areAddresseesEqual,
areBranchesEqual,
deduplicateAddressees,
deduplicateBranches,
} from './deduplicate-addressees.helper';
export * from './get-customer-name.component';
export * from './get-primary-bonus-card.helper';