feat(checkout): complete reward order confirmation with reusable product info component

- Extract reusable ProductInfoComponent from ProductInfoRedemptionComponent
- Implement order confirmation item list with product, action card, and destination info
- Add completed shopping carts tracking to checkout metadata service
- Create Storybook stories for product info component variants
- Update checkout completion orchestrator to store shopping cart data
- Extract COMPLETED_SHOPPING_CARTS_METADATA_KEY constant for consistency
This commit is contained in:
Lorenz Hilpert
2025-10-21 22:18:16 +02:00
parent ee2d9ba43a
commit a92f72f767
21 changed files with 555 additions and 72 deletions

View File

@@ -0,0 +1,130 @@
import {
type Meta,
type StoryObj,
applicationConfig,
moduleMetadata,
} from '@storybook/angular';
import { ProductInfoComponent } from '@isa/checkout/shared/product-info';
import { provideProductImageUrl } from '@isa/shared/product-image';
import { provideProductRouterLinkBuilder } from '@isa/shared/product-router-link';
import { provideRouter } from '@angular/router';
const mockProduct = {
ean: '9783498007706',
name: 'Die Assistentin',
contributors: 'Wahl, Caroline',
};
const meta: Meta<ProductInfoComponent> = {
title: 'checkout/shared/product-info/ProductInfo',
component: ProductInfoComponent,
decorators: [
applicationConfig({
providers: [
provideRouter([{ path: ':ean', component: ProductInfoComponent }]),
],
}),
moduleMetadata({
providers: [
provideProductImageUrl('https://produktbilder-test.paragon-data.net'),
provideProductRouterLinkBuilder((ean: string) => ean),
],
}),
],
};
export default meta;
type Story = StoryObj<ProductInfoComponent>;
export const BasicWithoutContent: Story = {
args: {
item: mockProduct,
nameSize: 'medium',
},
argTypes: {
item: { control: 'object' },
nameSize: {
control: { type: 'radio' },
options: ['small', 'medium', 'large'],
},
},
};
export const SmallNameSize: Story = {
args: {
item: mockProduct,
nameSize: 'small',
},
};
export const MediumNameSize: Story = {
args: {
item: mockProduct,
nameSize: 'medium',
},
};
export const LargeNameSize: Story = {
args: {
item: mockProduct,
nameSize: 'large',
},
};
export const WithLesepunkte: Story = {
args: {
item: mockProduct,
nameSize: 'medium',
},
render: (args) => ({
props: args,
template: `
<checkout-product-info [item]="item" [nameSize]="nameSize">
<div class="isa-text-body-2-regular">
<span class="isa-text-body-2-bold">150</span> Lesepunkte
</div>
</checkout-product-info>
`,
}),
};
export const WithManufacturer: Story = {
args: {
item: mockProduct,
nameSize: 'medium',
},
render: (args) => ({
props: args,
template: `
<checkout-product-info [item]="item" [nameSize]="nameSize">
<div class="isa-text-body-2-regular text-neutral-600">
Rowohlt Taschenbuch
</div>
</checkout-product-info>
`,
}),
};
export const WithMultipleRows: Story = {
args: {
item: mockProduct,
nameSize: 'medium',
},
render: (args) => ({
props: args,
template: `
<checkout-product-info [item]="item" [nameSize]="nameSize">
<div class="isa-text-body-2-regular">
<span class="isa-text-body-2-bold">150</span> Lesepunkte
</div>
<div class="isa-text-body-2-regular text-neutral-600">
Rowohlt Taschenbuch
</div>
<div class="isa-text-body-2-regular text-neutral-600">
Erschienen: 01. Januar 2023
</div>
</checkout-product-info>
`,
}),
};

View File

@@ -9,3 +9,6 @@ export const CHECKOUT_REWARD_SHOPPING_CART_ID_METADATA_KEY =
export const CHECKOUT_REWARD_SELECTION_POPUP_OPENED_STATE_KEY =
'checkout-data-access.checkoutRewardSelectionPopupOpenedState';
export const COMPLETED_SHOPPING_CARTS_METADATA_KEY =
'checkout-data-access.completedShoppingCarts';

View File

@@ -4,9 +4,11 @@ import {
CHECKOUT_REWARD_SELECTION_POPUP_OPENED_STATE_KEY,
CHECKOUT_REWARD_SHOPPING_CART_ID_METADATA_KEY,
CHECKOUT_SHOPPING_CART_ID_METADATA_KEY,
COMPLETED_SHOPPING_CARTS_METADATA_KEY,
SELECTED_BRANCH_METADATA_KEY,
} from '../constants';
import z from 'zod';
import { ShoppingCart } from '../models';
@Injectable({ providedIn: 'root' })
export class CheckoutMetadataService {
@@ -74,4 +76,20 @@ export class CheckoutMetadataService {
this.#tabService.entityMap(),
);
}
getCompletedShoppingCarts(tabId: number): ShoppingCart[] | undefined {
return getMetadataHelper(
tabId,
COMPLETED_SHOPPING_CARTS_METADATA_KEY,
z.array(z.any()).optional(),
this.#tabService.entityMap(),
);
}
addCompletedShoppingCart(tabId: number, shoppingCart: ShoppingCart) {
const existingCarts = this.getCompletedShoppingCarts(tabId) || [];
this.#tabService.patchTabMetadata(tabId, {
[COMPLETED_SHOPPING_CARTS_METADATA_KEY]: [...existingCarts, shoppingCart],
});
}
}

View File

@@ -1,5 +1,15 @@
<div>Product Info</div>
<checkout-product-info class="grow" [item]="productItem()" [nameSize]="'small'">
<div class="isa-text-body-2-regular" data-what="product-points">
<span class="isa-text-body-2-bold">{{ points() }}</span>
Lesepunkte
</div>
<div class="isa-text-body-2-bold">{{ item()?.quantity }} x</div>
</checkout-product-info>
<checkout-confirmation-list-item-action-card
class="w-[24.5rem]"
[item]="item()"
></checkout-confirmation-list-item-action-card>
<div>Destination Info</div>
<checkout-destination-info
class="max-w-[22.9rem] grow"
[shoppingCartItem]="shoppingCartItem()"
></checkout-destination-info>

View File

@@ -1,14 +1,74 @@
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
computed,
inject,
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';
import {
ProductInfoComponent,
ProductInfoItem,
DestinationInfoComponent,
} from '@isa/checkout/shared/product-info';
import { OrderConfiramtionStore } from '../../reward-order-confirmation.store';
@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: [ConfirmationListItemActionCardComponent],
imports: [
ConfirmationListItemActionCardComponent,
ProductInfoComponent,
DestinationInfoComponent,
],
})
export class OrderConfirmationItemListItemComponent {
#orderConfiramtionStore = inject(OrderConfiramtionStore);
item = input.required<DisplayOrderItem>();
productItem = computed<ProductInfoItem>(() => {
const product = this.item().product;
return {
contributors: product?.contributors ?? '',
ean: product?.ean ?? '',
name: product?.name ?? '',
};
});
points = computed(() => {
const shoppingCart = this.#orderConfiramtionStore.shoppingCart();
if (!shoppingCart) {
return 0;
}
const item = this.item();
const shoppingCartItem = shoppingCart.items.find(
(scItem) =>
scItem?.data?.product?.catalogProductNumber ===
item.product?.catalogProductNumber,
)?.data;
return shoppingCartItem?.loyalty?.value ?? 0;
});
shoppingCartItem = computed(() => {
const shoppingCart = this.#orderConfiramtionStore.shoppingCart();
const item = this.item();
if (!shoppingCart) {
return undefined;
}
return shoppingCart.items.find(
(scItem) =>
scItem?.data?.product?.catalogProductNumber ===
item.product?.catalogProductNumber,
)?.data;
});
}

View File

@@ -1,3 +1,3 @@
:host {
@apply block w-full;
@apply flex flex-col gap-6 w-full;
}

View File

@@ -6,6 +6,7 @@
{{ orderType() }}
</div>
</div>
@for (item of items(); track item.id) {
<checkout-order-confirmation-item-list-item
[item]="item"

View File

@@ -6,7 +6,7 @@ import {
} 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 {
@@ -14,6 +14,7 @@ import {
isaDeliveryRuecklage2,
isaDeliveryRuecklage1,
} from '@isa/icons';
import { DisplayOrder } from '@isa/oms/data-access';
@Component({
selector: 'checkout-order-confirmation-item-list',

View File

@@ -12,8 +12,6 @@ import { OrderConfirmationAddressesComponent } from './order-confirmation-addres
import { OrderConfirmationHeaderComponent } from './order-confirmation-header/order-confirmation-header.component';
import { OrderConfirmationItemListComponent } from './order-confirmation-item-list/order-confirmation-item-list.component';
import { ActivatedRoute } from '@angular/router';
import { JsonPipe } from '@angular/common';
import { OmsMetadataService } from '@isa/oms/data-access';
import { TabService } from '@isa/core/tabs';
import { OrderConfiramtionStore } from './reward-order-confirmation.store';

View File

@@ -4,7 +4,11 @@ import {
deduplicateBranches,
} from '@isa/crm/data-access';
import { OmsMetadataService } from '@isa/oms/data-access';
import { hasOrderTypeFeature, OrderType } from '@isa/checkout/data-access';
import {
CheckoutMetadataService,
hasOrderTypeFeature,
OrderType,
} from '@isa/checkout/data-access';
import {
patchState,
signalStore,
@@ -28,6 +32,7 @@ export const OrderConfiramtionStore = signalStore(
withState(initialState),
withProps(() => ({
_omsMetadataService: inject(OmsMetadataService),
_checkoutMetadataService: inject(CheckoutMetadataService),
})),
withComputed((state) => ({
orders: computed(() => {
@@ -44,10 +49,28 @@ export const OrderConfiramtionStore = signalStore(
const orders = state._omsMetadataService.getDisplayOrders(tabId);
return orders?.filter((order) => orderIds.includes(order.id!));
return orders?.filter(
(order) => order.id !== undefined && orderIds.includes(order.id),
);
}),
})),
withComputed((state) => ({
shoppingCart: computed(() => {
const tabId = state.tabId();
const orders = state.orders();
if (!tabId) {
return undefined;
}
if (!orders || !orders.length) {
return undefined;
}
const completedCarts =
state._checkoutMetadataService.getCompletedShoppingCarts(tabId);
return completedCarts?.find(
(cart) => cart.id === orders[0].shoppingCartId,
);
}),
payers: computed(() => {
const orders = state.orders();
if (!orders) {

View File

@@ -2,8 +2,12 @@ import { inject, Injectable } from '@angular/core';
import {
ShoppingCartFacade,
CompleteCrmOrderParams,
CheckoutMetadataService,
} from '@isa/checkout/data-access';
import { OrderCreationFacade, OmsMetadataService } from '@isa/oms/data-access';
import {
OrderCreationFacade,
OmsMetadataService,
} from '@isa/oms/data-access';
import { logger } from '@isa/core/logging';
import type { Order } from '@isa/checkout/data-access';
@@ -34,6 +38,7 @@ export class CheckoutCompletionOrchestratorService {
#shoppingCartFacade = inject(ShoppingCartFacade);
#orderCreationFacade = inject(OrderCreationFacade);
#omsMetadataService = inject(OmsMetadataService);
#checkoutMetadataService = inject(CheckoutMetadataService);
/**
* Complete checkout with CRM data and create orders.
@@ -69,12 +74,27 @@ export class CheckoutCompletionOrchestratorService {
abortSignal,
);
const shoppingCart = await this.#shoppingCartFacade.getShoppingCart(
params.shoppingCartId,
abortSignal,
);
const orders =
await this.#orderCreationFacade.createOrdersFromCheckout(checkoutId);
// Step 2: Update OMS metadata with created orders
if (tabId && orders.length > 0) {
this.#omsMetadataService.addDisplayOrders(tabId, orders);
if (tabId && shoppingCart) {
this.#checkoutMetadataService.addCompletedShoppingCart(
tabId,
shoppingCart,
);
}
if (tabId && orders.length > 0 && shoppingCart) {
this.#omsMetadataService.addDisplayOrders(
tabId,
orders,
shoppingCart.id,
);
}
this.#logger.info(

View File

@@ -1,3 +1,4 @@
export * from './lib/destination-info/destination-info.component';
export * from './lib/product-info/product-info.component';
export * from './lib/product-info/product-info-redemption.component';
export * from './lib/stock-info/stock-info.component';

View File

@@ -1,32 +1,12 @@
@let prd = item().product;
@let rPoints = points();
@if (prd) {
<div class="grid grid-cols-[auto,1fr] gap-6 items-start">
<div>
<img
sharedProductRouterLink
sharedProductImage
[ean]="prd.ean"
[alt]="prd.name"
class="checkout-product-info-redemption__image w-14"
data-what="product-image"
/>
<checkout-product-info [item]="baseItem()" [nameSize]="nameSize()">
<div class="isa-text-body-2-regular" data-what="product-points">
<span class="isa-text-body-2-bold">{{ rPoints }}</span>
Lesepunkte
</div>
<div class="flex flex-1 flex-col gap-1">
<div class="isa-text-body-2-bold">{{ prd.contributors }}</div>
<div
[class.isa-text-body-2-regular]="orientation() === 'horizontal'"
[class.isa-text-subtitle-1-regular]="orientation() === 'vertical'"
>
{{ prd.name }}
</div>
<div class="isa-text-body-2-regular">
<span class="isa-text-body-2-bold">{{ rPoints }}</span>
Lesepunkte
</div>
</div>
</div>
</checkout-product-info>
<div
class="flex flex-1 flex-col gap-2"
[class.ml-20]="orientation() === 'vertical'"
@@ -39,11 +19,12 @@
></shared-product-format>
<div
class="flex items-center gap-1 isa-text-body-2-regular text-neutral-600"
data-what="product-manufacturer-ean"
>
<span class="truncate">{{ prd.manufacturer }}</span>
<span class="shrink-0">| {{ prd.ean }}</span>
</div>
<div class="isa-text-body-2-regular text-neutral-600">
<div class="isa-text-body-2-regular text-neutral-600" data-what="product-publication-date">
{{ prd.publicationDate | date: 'dd. MMMM yyyy' }}
</div>
</div>

View File

@@ -6,13 +6,15 @@ import {
} from '@angular/core';
import { Product as CatProduct } from '@isa/catalogue/data-access';
import { Product as CheckoutProduct } from '@isa/checkout/data-access';
import { ProductImageDirective } from '@isa/shared/product-image';
import { ProductRouterLinkDirective } from '@isa/shared/product-router-link';
import { ProductFormatComponent } from '@isa/shared/product-foramt';
import { DatePipe } from '@angular/common';
import { Loyalty } from '@isa/checkout/data-access';
import {
ProductInfoComponent,
ProductInfoItem,
} from './product-info.component';
export type ProductInfoItem =
export type ProductInfoRedemptionItem =
| {
product: Pick<
CatProduct,
@@ -46,18 +48,13 @@ export type ProductInfoItem =
styleUrls: ['./product-info-redemption.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
ProductImageDirective,
ProductRouterLinkDirective,
ProductFormatComponent,
DatePipe,
],
imports: [ProductInfoComponent, ProductFormatComponent, DatePipe],
host: {
'[class]': '[orientation()]',
},
})
export class ProductInfoRedemptionComponent {
item = input.required<ProductInfoItem>();
item = input.required<ProductInfoRedemptionItem>();
orientation = input<'horizontal' | 'vertical'>('vertical');
@@ -73,4 +70,17 @@ export class ProductInfoRedemptionComponent {
return 0;
});
nameSize = computed(() => {
return this.orientation() === 'horizontal' ? 'small' : 'medium';
});
baseItem = computed<ProductInfoItem>(() => {
const item = this.item();
return {
ean: item.product.ean ?? '',
name: item.product.name ?? '',
contributors: item.product.contributors ?? '',
};
});
}

View File

@@ -0,0 +1,3 @@
:host {
@apply block text-neutral-900;
}

View File

@@ -0,0 +1,23 @@
@let i = item();
<div class="grid grid-cols-[auto,1fr] gap-6 items-start">
<div>
<img
sharedProductRouterLink
sharedProductImage
[ean]="i.ean"
[alt]="i.name"
class="checkout-product-info__image max-w-14 max-h-[5.125rem] object-contain"
data-what="product-image"
/>
</div>
<div class="flex flex-1 flex-col gap-1">
<div class="isa-text-body-2-bold" data-what="product-contributors">
{{ i.contributors }}
</div>
<div [class]="nameSizeClass()" data-what="product-name">
{{ i.name }}
</div>
<ng-content />
</div>
</div>

View File

@@ -0,0 +1,145 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { describe, it, expect, beforeEach } from 'vitest';
import { ProductInfoComponent, ProductInfoItem } from './product-info.component';
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
import { provideProductImageUrl } from '@isa/shared/product-image';
import { provideProductRouterLinkBuilder } from '@isa/shared/product-router-link';
import { provideRouter } from '@angular/router';
describe('ProductInfoComponent', () => {
let component: ProductInfoComponent;
let fixture: ComponentFixture<ProductInfoComponent>;
const mockProduct: ProductInfoItem = {
ean: '9783161484100',
name: 'Test Product Name',
contributors: 'Test Author',
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ProductInfoComponent],
providers: [
provideRouter([]),
provideProductImageUrl('https://test.example.com'),
provideProductRouterLinkBuilder((ean: string) => `/product/${ean}`),
],
});
fixture = TestBed.createComponent(ProductInfoComponent);
component = fixture.componentInstance;
});
it('should create', () => {
fixture.componentRef.setInput('item', mockProduct);
fixture.detectChanges();
expect(component).toBeTruthy();
});
it('should have item input', () => {
fixture.componentRef.setInput('item', mockProduct);
expect(component.item()).toEqual(mockProduct);
});
it('should have default nameSize of medium', () => {
fixture.componentRef.setInput('item', mockProduct);
expect(component.nameSize()).toBe('medium');
});
describe('nameSizeClass computation', () => {
beforeEach(() => {
fixture.componentRef.setInput('item', mockProduct);
});
it('should return isa-text-body-2-regular for small size', () => {
fixture.componentRef.setInput('nameSize', 'small');
expect(component.nameSizeClass()).toBe('isa-text-body-2-regular');
});
it('should return isa-text-subtitle-1-regular for medium size', () => {
fixture.componentRef.setInput('nameSize', 'medium');
expect(component.nameSizeClass()).toBe('isa-text-subtitle-1-regular');
});
it('should return isa-text-heading-3-regular for large size', () => {
fixture.componentRef.setInput('nameSize', 'large');
expect(component.nameSizeClass()).toBe('isa-text-heading-3-regular');
});
});
describe('template rendering', () => {
beforeEach(() => {
fixture.componentRef.setInput('item', mockProduct);
fixture.detectChanges();
});
it('should render product contributors', () => {
const contributorsEl: DebugElement = fixture.debugElement.query(
By.css('[data-what="product-contributors"]')
);
expect(contributorsEl).toBeTruthy();
expect(contributorsEl.nativeElement.textContent.trim()).toBe(
'Test Author'
);
});
it('should render product name with correct size class', () => {
const nameEl: DebugElement = fixture.debugElement.query(
By.css('[data-what="product-name"]')
);
expect(nameEl).toBeTruthy();
expect(nameEl.nativeElement.textContent.trim()).toBe('Test Product Name');
expect(nameEl.nativeElement.classList.contains('isa-text-subtitle-1-regular')).toBe(true);
});
it('should have E2E attribute on product image', () => {
const imageEl: DebugElement = fixture.debugElement.query(
By.css('[data-what="product-image"]')
);
expect(imageEl).toBeTruthy();
});
it('should project ng-content', () => {
const customContentFixture = TestBed.createComponent(ProductInfoComponent);
customContentFixture.componentRef.setInput('item', mockProduct);
const div = document.createElement('div');
div.className = 'custom-content';
div.textContent = 'Custom projected content';
customContentFixture.nativeElement.appendChild(div);
customContentFixture.detectChanges();
const projectedContent = customContentFixture.nativeElement.querySelector('.custom-content');
expect(projectedContent).toBeTruthy();
expect(projectedContent.textContent).toBe('Custom projected content');
});
});
describe('name size variations', () => {
beforeEach(() => {
fixture.componentRef.setInput('item', mockProduct);
});
it('should apply small name size class', () => {
fixture.componentRef.setInput('nameSize', 'small');
fixture.detectChanges();
const nameEl: DebugElement = fixture.debugElement.query(
By.css('[data-what="product-name"]')
);
expect(nameEl.nativeElement.classList.contains('isa-text-body-2-regular')).toBe(true);
});
it('should apply large name size class', () => {
fixture.componentRef.setInput('nameSize', 'large');
fixture.detectChanges();
const nameEl: DebugElement = fixture.debugElement.query(
By.css('[data-what="product-name"]')
);
expect(nameEl.nativeElement.classList.contains('isa-text-heading-3-regular')).toBe(true);
});
});
});

View File

@@ -0,0 +1,44 @@
import {
ChangeDetectionStrategy,
Component,
computed,
input,
} from '@angular/core';
import { ProductImageDirective } from '@isa/shared/product-image';
import { ProductRouterLinkDirective } from '@isa/shared/product-router-link';
export type ProductInfoItem = {
ean: string;
name: string;
contributors: string;
};
export type ProductNameSize = 'small' | 'medium' | 'large';
@Component({
selector: 'checkout-product-info',
templateUrl: './product-info.component.html',
styleUrls: ['./product-info.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [ProductImageDirective, ProductRouterLinkDirective],
})
export class ProductInfoComponent {
item = input.required<ProductInfoItem>();
nameSize = input<ProductNameSize>('medium');
nameSizeClass = computed(() => {
const size = this.nameSize();
switch (size) {
case 'small':
return 'isa-text-body-2-regular';
case 'medium':
return 'isa-text-subtitle-1-regular';
case 'large':
return 'isa-text-heading-3-regular';
default:
return 'isa-text-subtitle-1-regular';
}
});
}

View File

@@ -1,19 +1,19 @@
export * from './display-addressee.schema';
export * from './display-branch.schema';
export * from './display-logistician.schema';
export * from './display-order-item.schema';
export * from './display-order-payment.schema';
export * from './display-order.schema';
export * from './environment-channel.schema';
export * from './fetch-return-details.schema';
export * from './linked-record.schema';
export * from './order-type.schema';
export * from './price.schema';
export * from './product.schema';
export * from './promotion.schema';
export * from './query-token.schema';
export * from './return-process-question-answer.schema';
export * from './return-receipt-values.schema';
export * from './shipping-type.schema';
export * from './terms-of-delivery.schema';
export * from './type-of-delivery.schema';
export * from './display-addressee.schema';
export * from './display-branch.schema';
export * from './display-logistician.schema';
export * from './display-order-item.schema';
export * from './display-order-payment.schema';
export * from './display-order.schema';
export * from './environment-channel.schema';
export * from './fetch-return-details.schema';
export * from './linked-record.schema';
export * from './order-type.schema';
export * from './price.schema';
export * from './product.schema';
export * from './promotion.schema';
export * from './query-token.schema';
export * from './return-process-question-answer.schema';
export * from './return-receipt-values.schema';
export * from './shipping-type.schema';
export * from './terms-of-delivery.schema';
export * from './type-of-delivery.schema';

View File

@@ -8,19 +8,28 @@ import z from 'zod';
export class OmsMetadataService {
#tabService = inject(TabService);
getDisplayOrders(tabId: number): DisplayOrder[] | undefined {
getDisplayOrders(tabId: number) {
return getMetadataHelper(
tabId,
OMS_DISPLAY_ORDERS_KEY,
z.array(DisplayOrderSchema).optional(),
z
.array(DisplayOrderSchema.extend({ shoppingCartId: z.number() }))
.optional(),
this.#tabService.entityMap(),
);
}
addDisplayOrders(tabId: number, orders: DisplayOrder[]) {
addDisplayOrders(
tabId: number,
orders: DisplayOrder[],
shoppingCartId: number,
) {
const existingOrders = this.getDisplayOrders(tabId) || [];
this.#tabService.patchTabMetadata(tabId, {
[OMS_DISPLAY_ORDERS_KEY]: [...existingOrders, ...orders],
[OMS_DISPLAY_ORDERS_KEY]: [
...existingOrders,
...orders.map((order) => ({ ...order, shoppingCartId })),
],
});
}