mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-31 09:37:15 +01:00
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:
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from './get-order-type-feature.helper';
|
export * from './get-order-type-feature.helper';
|
||||||
|
export * from './has-order-type-feature.helper';
|
||||||
export * from './checkout-analysis.helpers';
|
export * from './checkout-analysis.helpers';
|
||||||
export * from './checkout-business-logic.helpers';
|
export * from './checkout-business-logic.helpers';
|
||||||
export * from './checkout-data.helpers';
|
export * from './checkout-data.helpers';
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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({
|
@Component({
|
||||||
selector: 'checkout-order-confirmation-addresses',
|
selector: 'checkout-order-confirmation-addresses',
|
||||||
templateUrl: './order-confirmation-addresses.component.html',
|
templateUrl: './order-confirmation-addresses.component.html',
|
||||||
styleUrls: ['./order-confirmation-addresses.component.css'],
|
styleUrls: ['./order-confirmation-addresses.component.css'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
<h1>Order Confirmation Header</h1>
|
<h1 class="text-isa-neutral-900 isa-text-subtitle-1-regular">
|
||||||
|
Prämienausgabe abgeschlossen
|
||||||
|
</h1>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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({
|
@Component({
|
||||||
selector: 'checkout-order-confirmation-item-list-item',
|
selector: 'checkout-order-confirmation-item-list-item',
|
||||||
templateUrl: './order-confirmation-item-list-item.component.html',
|
templateUrl: './order-confirmation-item-list-item.component.html',
|
||||||
styleUrls: ['./order-confirmation-item-list-item.component.css'],
|
styleUrls: ['./order-confirmation-item-list-item.component.css'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [],
|
imports: [ConfirmationListItemActionCardComponent],
|
||||||
})
|
})
|
||||||
export class OrderConfirmationItemListItemComponent {}
|
export class OrderConfirmationItemListItemComponent {
|
||||||
|
item = input.required<DisplayOrderItem>();
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
:host {
|
||||||
|
@apply block w-full;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 { 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({
|
@Component({
|
||||||
selector: 'checkout-order-confirmation-item-list',
|
selector: 'checkout-order-confirmation-item-list',
|
||||||
templateUrl: './order-confirmation-item-list.component.html',
|
templateUrl: './order-confirmation-item-list.component.html',
|
||||||
styleUrls: ['./order-confirmation-item-list.component.css'],
|
styleUrls: ['./order-confirmation-item-list.component.css'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
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 ?? []);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
:host {
|
||||||
|
@apply block w-full text-isa-neutral-900 mt-[1.42rem];
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ export class RewardOrderConfirmationComponent {
|
|||||||
return param ? param.split('+').map((strId) => parseInt(strId, 10)) : [];
|
return param ? param.split('+').map((strId) => parseInt(strId, 10)) : [];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
orders = this.#store.orders;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
effect(() => {
|
effect(() => {
|
||||||
const tabId = this.#tabId() || undefined;
|
const tabId = this.#tabId() || undefined;
|
||||||
@@ -54,5 +56,9 @@ export class RewardOrderConfirmationComponent {
|
|||||||
this.#store.patch({ tabId, orderIds });
|
this.#store.patch({ tabId, orderIds });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
effect(() => {
|
||||||
|
console.log('Orders to display:', this.orders());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { OmsMetadataService } from '@isa/oms/data-access';
|
||||||
|
import { hasOrderTypeFeature, OrderType } from '@isa/checkout/data-access';
|
||||||
import {
|
import {
|
||||||
patchState,
|
patchState,
|
||||||
signalStore,
|
signalStore,
|
||||||
@@ -42,6 +47,57 @@ export const OrderConfiramtionStore = signalStore(
|
|||||||
return orders?.filter((order) => orderIds.includes(order.id!));
|
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) => ({
|
withMethods((store) => ({
|
||||||
patch(partial: Partial<OrderConfiramtionState>) {
|
patch(partial: Partial<OrderConfiramtionState>) {
|
||||||
patchState(store, partial);
|
patchState(store, partial);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,2 +1,8 @@
|
|||||||
|
export {
|
||||||
|
areAddresseesEqual,
|
||||||
|
areBranchesEqual,
|
||||||
|
deduplicateAddressees,
|
||||||
|
deduplicateBranches,
|
||||||
|
} from './deduplicate-addressees.helper';
|
||||||
export * from './get-customer-name.component';
|
export * from './get-customer-name.component';
|
||||||
export * from './get-primary-bonus-card.helper';
|
export * from './get-primary-bonus-card.helper';
|
||||||
|
|||||||
@@ -1,26 +1,32 @@
|
|||||||
import { EntitySchema, QuantityUnitTypeSchema } from '@isa/common/data-access';
|
import { EntitySchema, QuantityUnitTypeSchema } from '@isa/common/data-access';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { PriceSchema } from './price.schema';
|
import { PriceSchema } from './price.schema';
|
||||||
import { ProductSchema } from './product.schema';
|
import { ProductSchema } from './product.schema';
|
||||||
import { PromotionSchema } from './promotion.schema';
|
import { PromotionSchema } from './promotion.schema';
|
||||||
|
|
||||||
// Forward declaration for circular reference
|
// Forward declaration for circular reference
|
||||||
export const DisplayOrderItemSchema: z.ZodType<any> = z
|
export const DisplayOrderItemSchema = z
|
||||||
.object({
|
.object({
|
||||||
buyerComment: z.string().describe('Buyer comment').optional(),
|
buyerComment: z.string().describe('Buyer comment').optional(),
|
||||||
description: z.string().describe('Description text').optional(),
|
description: z.string().describe('Description text').optional(),
|
||||||
features: z.record(z.string().describe('Features'), z.string()).optional(),
|
features: z.record(z.string().describe('Features'), z.string()).optional(),
|
||||||
order: z.lazy(() => z.any()).describe('Order').optional(), // Circular reference to DisplayOrder
|
order: z
|
||||||
orderDate: z.string().describe('Order date').optional(),
|
.lazy(() => z.any())
|
||||||
orderItemNumber: z.string().describe('OrderItem number').optional(),
|
.describe('Order')
|
||||||
price: PriceSchema.describe('Price information').optional(),
|
.optional(), // Circular reference to DisplayOrder
|
||||||
product: ProductSchema.describe('Product').optional(),
|
orderDate: z.string().describe('Order date').optional(),
|
||||||
promotion: PromotionSchema.describe('Promotion information').optional(),
|
orderItemNumber: z.string().describe('OrderItem number').optional(),
|
||||||
quantity: z.number().describe('Quantity').optional(),
|
price: PriceSchema.describe('Price information').optional(),
|
||||||
quantityUnit: z.string().describe('Quantity unit').optional(),
|
product: ProductSchema.describe('Product').optional(),
|
||||||
quantityUnitType: QuantityUnitTypeSchema.describe('QuantityUnit type'),
|
promotion: PromotionSchema.describe('Promotion information').optional(),
|
||||||
subsetItems: z.array(z.lazy(() => z.any())).describe('Subset items').optional(), // Circular reference to DisplayOrderItemSubset
|
quantity: z.number().describe('Quantity').optional(),
|
||||||
})
|
quantityUnit: z.string().describe('Quantity unit').optional(),
|
||||||
.extend(EntitySchema.shape);
|
quantityUnitType: QuantityUnitTypeSchema.describe('QuantityUnit type'),
|
||||||
|
subsetItems: z
|
||||||
export type DisplayOrderItem = z.infer<typeof DisplayOrderItemSchema>;
|
.array(z.lazy(() => z.any()))
|
||||||
|
.describe('Subset items')
|
||||||
|
.optional(), // Circular reference to DisplayOrderItemSubset
|
||||||
|
})
|
||||||
|
.extend(EntitySchema.shape);
|
||||||
|
|
||||||
|
export type DisplayOrderItem = z.infer<typeof DisplayOrderItemSchema>;
|
||||||
|
|||||||
Reference in New Issue
Block a user