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,219 @@
import { describe, it, expect } from 'vitest';
import { hasOrderTypeFeature } from './has-order-type-feature.helper';
import { OrderType } from '../models';
describe('hasOrderTypeFeature', () => {
describe('when features is undefined', () => {
it('should return false', () => {
// Act
const result = hasOrderTypeFeature(undefined, [OrderType.Delivery]);
// Assert
expect(result).toBe(false);
});
});
describe('when features is empty', () => {
it('should return false', () => {
// Arrange
const features = {};
// Act
const result = hasOrderTypeFeature(features, [OrderType.Delivery]);
// Assert
expect(result).toBe(false);
});
});
describe('when orderType feature is not present', () => {
it('should return false', () => {
// Arrange
const features = { someOtherFeature: 'value' };
// Act
const result = hasOrderTypeFeature(features, [OrderType.Delivery]);
// Assert
expect(result).toBe(false);
});
});
describe('when orderType feature is invalid', () => {
it('should return false', () => {
// Arrange
const features = { orderType: 'InvalidOrderType' };
// Act
const result = hasOrderTypeFeature(features, [OrderType.Delivery]);
// Assert
expect(result).toBe(false);
});
});
describe('when orderType feature matches one of the provided types', () => {
it('should return true for Delivery in delivery types', () => {
// Arrange
const features = { orderType: OrderType.Delivery };
// Act
const result = hasOrderTypeFeature(features, [
OrderType.Delivery,
OrderType.DigitalShipping,
OrderType.B2BShipping,
]);
// Assert
expect(result).toBe(true);
});
it('should return true for DigitalShipping in delivery types', () => {
// Arrange
const features = { orderType: OrderType.DigitalShipping };
// Act
const result = hasOrderTypeFeature(features, [
OrderType.Delivery,
OrderType.DigitalShipping,
OrderType.B2BShipping,
]);
// Assert
expect(result).toBe(true);
});
it('should return true for B2BShipping in delivery types', () => {
// Arrange
const features = { orderType: OrderType.B2BShipping };
// Act
const result = hasOrderTypeFeature(features, [
OrderType.Delivery,
OrderType.DigitalShipping,
OrderType.B2BShipping,
]);
// Assert
expect(result).toBe(true);
});
it('should return true for InStore in branch types', () => {
// Arrange
const features = { orderType: OrderType.InStore };
// Act
const result = hasOrderTypeFeature(features, [
OrderType.InStore,
OrderType.Pickup,
]);
// Assert
expect(result).toBe(true);
});
it('should return true for Pickup in branch types', () => {
// Arrange
const features = { orderType: OrderType.Pickup };
// Act
const result = hasOrderTypeFeature(features, [
OrderType.InStore,
OrderType.Pickup,
]);
// Assert
expect(result).toBe(true);
});
it('should return true for Download when checked alone', () => {
// Arrange
const features = { orderType: OrderType.Download };
// Act
const result = hasOrderTypeFeature(features, [OrderType.Download]);
// Assert
expect(result).toBe(true);
});
});
describe('when orderType feature does not match any provided types', () => {
it('should return false for Delivery when checking branch types', () => {
// Arrange
const features = { orderType: OrderType.Delivery };
// Act
const result = hasOrderTypeFeature(features, [
OrderType.InStore,
OrderType.Pickup,
]);
// Assert
expect(result).toBe(false);
});
it('should return false for InStore when checking delivery types', () => {
// Arrange
const features = { orderType: OrderType.InStore };
// Act
const result = hasOrderTypeFeature(features, [
OrderType.Delivery,
OrderType.DigitalShipping,
OrderType.B2BShipping,
]);
// Assert
expect(result).toBe(false);
});
it('should return false for Download when checking delivery types', () => {
// Arrange
const features = { orderType: OrderType.Download };
// Act
const result = hasOrderTypeFeature(features, [
OrderType.Delivery,
OrderType.DigitalShipping,
OrderType.B2BShipping,
]);
// Assert
expect(result).toBe(false);
});
});
describe('when checking against empty order types array', () => {
it('should return false', () => {
// Arrange
const features = { orderType: OrderType.Delivery };
// Act
const result = hasOrderTypeFeature(features, []);
// Assert
expect(result).toBe(false);
});
});
describe('when checking against all order types', () => {
it('should return true for any valid order type', () => {
// Arrange
const features = { orderType: OrderType.Delivery };
// Act
const result = hasOrderTypeFeature(features, [
OrderType.InStore,
OrderType.Pickup,
OrderType.Delivery,
OrderType.DigitalShipping,
OrderType.B2BShipping,
OrderType.Download,
]);
// Assert
expect(result).toBe(true);
});
});
});

View File

@@ -0,0 +1,38 @@
import { OrderType } from '../models';
import { getOrderTypeFeature } from './get-order-type-feature.helper';
/**
* Checks if the order type feature in the provided features record matches any of the specified order types.
*
* @param features - Record containing feature flags with an 'orderType' key
* @param orderTypes - Array of order types to check against
* @returns true if the feature's order type is one of the provided types, false otherwise
*
* @example
* ```typescript
* // Check if order type is a delivery type
* const isDelivery = hasOrderTypeFeature(features, [
* OrderType.Delivery,
* OrderType.DigitalShipping,
* OrderType.B2BShipping
* ]);
*
* // Check if order type requires a target branch
* const hasTargetBranch = hasOrderTypeFeature(features, [
* OrderType.InStore,
* OrderType.Pickup
* ]);
* ```
*/
export function hasOrderTypeFeature(
features: Record<string, string> | undefined,
orderTypes: readonly OrderType[],
): boolean {
const orderType = getOrderTypeFeature(features);
if (!orderType) {
return false;
}
return orderTypes.includes(orderType);
}

View File

@@ -1,4 +1,5 @@
export * from './get-order-type-feature.helper';
export * from './has-order-type-feature.helper';
export * from './checkout-analysis.helpers';
export * from './checkout-business-logic.helpers';
export * from './checkout-data.helpers';

View File

@@ -1 +1,46 @@
<h1>Order Confirmation Addresses</h1>
<div class="flex items-start gap-[2.5rem_5rem] self-stretch flex-wrap">
@for (payer of payers(); track payer) {
@if (payer.address) {
<div>
<h3 class="isa-text-body-1-regular">Rechnugsadresse</h3>
<div class="isa-text-body-1-bold mt-1">
{{ getCustomerName(payer) }}
</div>
<shared-address
class="isa-text-body-1-bold"
[address]="payer.address"
></shared-address>
</div>
}
}
@if (hasDeliveryOrderTypeFeature()) {
@for (shippingAddress of shippingAddresses(); track shippingAddress) {
@if (shippingAddress.address) {
<div>
<h3 class="isa-text-body-1-regular">Lieferadresse</h3>
<div class="isa-text-body-1-bold mt-1">
{{ getCustomerName(shippingAddress) }}
</div>
<shared-address
class="isa-text-body-1-bold"
[address]="shippingAddress.address"
></shared-address>
</div>
}
}
}
@if (hasTargetBranchFeature()) {
@for (branch of targetBranches(); track branch) {
<div>
<h3 class="isa-text-body-1-regular">Abholfiliale</h3>
<div class="isa-text-body-1-bold mt-1">
{{ branch.name }}
</div>
<shared-address
class="isa-text-body-1-bold"
[address]="branch.address"
></shared-address>
</div>
}
}
</div>

View File

@@ -1,10 +1,27 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { AddressComponent } from '@isa/shared/address';
import { OrderConfiramtionStore } from '../reward-order-confirmation.store';
import { getCustomerName } from '@isa/crm/data-access';
@Component({
selector: 'checkout-order-confirmation-addresses',
templateUrl: './order-confirmation-addresses.component.html',
styleUrls: ['./order-confirmation-addresses.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [],
imports: [AddressComponent],
})
export class OrderConfirmationAddressesComponent {}
export class OrderConfirmationAddressesComponent {
getCustomerName = getCustomerName;
#store = inject(OrderConfiramtionStore);
payers = this.#store.payers;
shippingAddresses = this.#store.shippingAddresses;
hasDeliveryOrderTypeFeature = this.#store.hasDeliveryOrderTypeFeature;
targetBranches = this.#store.targetBranches;
hasTargetBranchFeature = this.#store.hasTargetBranchFeature;
}

View File

@@ -1 +1,3 @@
<h1>Order Confirmation Header</h1>
<h1 class="text-isa-neutral-900 isa-text-subtitle-1-regular">
Prämienausgabe abgeschlossen
</h1>

View File

@@ -1 +1,5 @@
<h1>Order Confirmation Item List Item</h1>
<div>Product Info</div>
<checkout-confirmation-list-item-action-card
[item]="item()"
></checkout-confirmation-list-item-action-card>
<div>Destination Info</div>

View File

@@ -1,10 +1,14 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
import { DisplayOrderItem } from '@isa/oms/data-access';
import { ConfirmationListItemActionCardComponent } from './confirmation-list-item-action-card/confirmation-list-item-action-card.component';
@Component({
selector: 'checkout-order-confirmation-item-list-item',
templateUrl: './order-confirmation-item-list-item.component.html',
styleUrls: ['./order-confirmation-item-list-item.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [],
imports: [ConfirmationListItemActionCardComponent],
})
export class OrderConfirmationItemListItemComponent {}
export class OrderConfirmationItemListItemComponent {
item = input.required<DisplayOrderItem>();
}

View File

@@ -1 +1,13 @@
<h1>Order Confirmation Item List</h1>
<div
class="flex p-2 items-center gap-[0.625rem] self-stretch rounded-2xl bg-isa-neutral-200 w-full isa-text-body-2-bold"
>
<ng-icon [name]="orderTypeIcon()" size="1.5rem"></ng-icon>
<div>
{{ orderType() }}
</div>
</div>
@for (item of items(); track item.id) {
<checkout-order-confirmation-item-list-item
[item]="item"
></checkout-order-confirmation-item-list-item>
}

View File

@@ -1,12 +1,58 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
computed,
input,
} from '@angular/core';
import { OrderConfirmationItemListItemComponent } from './order-confirmation-item-list-item/order-confirmation-item-list-item.component';
import { DisplayOrder, DisplayOrderItem } from '@isa/oms/data-access';
import { getOrderTypeFeature, OrderType } from '@isa/checkout/data-access';
import { NgIcon, provideIcons } from '@ng-icons/core';
import {
isaDeliveryVersand,
isaDeliveryRuecklage2,
isaDeliveryRuecklage1,
} from '@isa/icons';
@Component({
selector: 'checkout-order-confirmation-item-list',
templateUrl: './order-confirmation-item-list.component.html',
styleUrls: ['./order-confirmation-item-list.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [OrderConfirmationItemListItemComponent],
imports: [OrderConfirmationItemListItemComponent, NgIcon],
providers: [
provideIcons({
isaDeliveryVersand,
isaDeliveryRuecklage2,
isaDeliveryRuecklage1,
}),
],
})
export class OrderConfirmationItemListComponent {}
export class OrderConfirmationItemListComponent {
order = input.required<DisplayOrder>();
orderType = computed(() => {
return getOrderTypeFeature(this.order().features);
});
orderTypeIcon = computed(() => {
const orderType = this.orderType();
if (OrderType.Delivery === orderType) {
return 'isaDeliveryVersand';
}
if (OrderType.Pickup === orderType) {
return 'isaDeliveryRuecklage2';
}
if (OrderType.InStore === orderType) {
return 'isaDeliveryRuecklage1';
}
return 'isaDeliveryVersand';
});
items = computed(() => this.order().items ?? []);
}

View File

@@ -0,0 +1,3 @@
:host {
@apply block w-full text-isa-neutral-900 mt-[1.42rem];
}

View File

@@ -1 +1,12 @@
<checkout-order-confirmation-header></checkout-order-confirmation-header>
<div
class="bg-isa-white p-6 rounded-2xl flex flex-col gap-6 items-start self-stretch"
>
<checkout-order-confirmation-header></checkout-order-confirmation-header>
<div class="w-full h-[0.0625rem] bg-[#DEE2E6]"></div>
<checkout-order-confirmation-addresses></checkout-order-confirmation-addresses>
@for (order of orders(); track order.id) {
<checkout-order-confirmation-item-list
[order]="order"
></checkout-order-confirmation-item-list>
}
</div>

View File

@@ -45,6 +45,8 @@ export class RewardOrderConfirmationComponent {
return param ? param.split('+').map((strId) => parseInt(strId, 10)) : [];
});
orders = this.#store.orders;
constructor() {
effect(() => {
const tabId = this.#tabId() || undefined;
@@ -54,5 +56,9 @@ export class RewardOrderConfirmationComponent {
this.#store.patch({ tabId, orderIds });
});
});
effect(() => {
console.log('Orders to display:', this.orders());
});
}
}

View File

@@ -1,5 +1,10 @@
import { computed, inject, resource } from '@angular/core';
import { computed, inject } from '@angular/core';
import {
deduplicateAddressees,
deduplicateBranches,
} from '@isa/crm/data-access';
import { OmsMetadataService } from '@isa/oms/data-access';
import { hasOrderTypeFeature, OrderType } from '@isa/checkout/data-access';
import {
patchState,
signalStore,
@@ -42,6 +47,57 @@ export const OrderConfiramtionStore = signalStore(
return orders?.filter((order) => orderIds.includes(order.id!));
}),
})),
withComputed((state) => ({
payers: computed(() => {
const orders = state.orders();
if (!orders) {
return undefined;
}
const buyers = orders.map((order) => order.buyer);
return deduplicateAddressees(buyers);
}),
shippingAddresses: computed(() => {
const orders = state.orders();
if (!orders) {
return undefined;
}
const addresses = orders.map((order) => order.shippingAddress);
return deduplicateAddressees(addresses);
}),
targetBranches: computed(() => {
const orders = state.orders();
if (!orders) {
return undefined;
}
const branches = orders.map((order) => order.targetBranch);
return deduplicateBranches(branches);
}),
hasDeliveryOrderTypeFeature: computed(() => {
const orders = state.orders();
if (!orders || orders.length === 0) {
return false;
}
return orders.some((order) => {
return hasOrderTypeFeature(order.features, [
OrderType.Delivery,
OrderType.DigitalShipping,
OrderType.B2BShipping,
]);
});
}),
hasTargetBranchFeature: computed(() => {
const orders = state.orders();
if (!orders || orders.length === 0) {
return false;
}
return orders.some((order) => {
return hasOrderTypeFeature(order.features, [
OrderType.InStore,
OrderType.Pickup,
]);
});
}),
})),
withMethods((store) => ({
patch(partial: Partial<OrderConfiramtionState>) {
patchState(store, partial);

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

View File

@@ -1,26 +1,32 @@
import { EntitySchema, QuantityUnitTypeSchema } from '@isa/common/data-access';
import { z } from 'zod';
import { PriceSchema } from './price.schema';
import { ProductSchema } from './product.schema';
import { PromotionSchema } from './promotion.schema';
// Forward declaration for circular reference
export const DisplayOrderItemSchema: z.ZodType<any> = z
.object({
buyerComment: z.string().describe('Buyer comment').optional(),
description: z.string().describe('Description text').optional(),
features: z.record(z.string().describe('Features'), z.string()).optional(),
order: z.lazy(() => z.any()).describe('Order').optional(), // Circular reference to DisplayOrder
orderDate: z.string().describe('Order date').optional(),
orderItemNumber: z.string().describe('OrderItem number').optional(),
price: PriceSchema.describe('Price information').optional(),
product: ProductSchema.describe('Product').optional(),
promotion: PromotionSchema.describe('Promotion information').optional(),
quantity: z.number().describe('Quantity').optional(),
quantityUnit: z.string().describe('Quantity unit').optional(),
quantityUnitType: QuantityUnitTypeSchema.describe('QuantityUnit type'),
subsetItems: z.array(z.lazy(() => z.any())).describe('Subset items').optional(), // Circular reference to DisplayOrderItemSubset
})
.extend(EntitySchema.shape);
export type DisplayOrderItem = z.infer<typeof DisplayOrderItemSchema>;
import { EntitySchema, QuantityUnitTypeSchema } from '@isa/common/data-access';
import { z } from 'zod';
import { PriceSchema } from './price.schema';
import { ProductSchema } from './product.schema';
import { PromotionSchema } from './promotion.schema';
// Forward declaration for circular reference
export const DisplayOrderItemSchema = z
.object({
buyerComment: z.string().describe('Buyer comment').optional(),
description: z.string().describe('Description text').optional(),
features: z.record(z.string().describe('Features'), z.string()).optional(),
order: z
.lazy(() => z.any())
.describe('Order')
.optional(), // Circular reference to DisplayOrder
orderDate: z.string().describe('Order date').optional(),
orderItemNumber: z.string().describe('OrderItem number').optional(),
price: PriceSchema.describe('Price information').optional(),
product: ProductSchema.describe('Product').optional(),
promotion: PromotionSchema.describe('Promotion information').optional(),
quantity: z.number().describe('Quantity').optional(),
quantityUnit: z.string().describe('Quantity unit').optional(),
quantityUnitType: QuantityUnitTypeSchema.describe('QuantityUnit type'),
subsetItems: z
.array(z.lazy(() => z.any()))
.describe('Subset items')
.optional(), // Circular reference to DisplayOrderItemSubset
})
.extend(EntitySchema.shape);
export type DisplayOrderItem = z.infer<typeof DisplayOrderItemSchema>;