Merged PR 1958: Refactoring Checkout: Migration von prozess-basierter zu warenkorb-basierter Architektur mit neuer Data-Access-Library und verbesserter Typsicherheit

refactor(checkout): migrate purchase options to shopping cart-based architecture
Replace processId with shoppingCartId in purchase options modal and related components
Add new checkout data-access library with facades, services, and schemas
Update PurchaseOptionsService to use new checkout facade pattern
Migrate state management from process-based to shopping cart-based approach
Update selectors and store to handle shoppingCartId instead of processId
Improve type safety with Zod schemas for checkout operations
Add proper error handling and logging throughout checkout services
Update article details and checkout review components to use new patterns
BREAKING CHANGE: Purchase options modal now requires shoppingCartId instead of processId

Related work items: #5350
This commit is contained in:
Lorenz Hilpert
2025-09-25 09:27:05 +00:00
committed by Nino Righi
parent 334436c737
commit 100cbb5020
52 changed files with 6686 additions and 4963 deletions

View File

@@ -25,7 +25,10 @@
"/libs/catalogue/data-access/src/lib/models",
"/libs/common/data-access/src/lib/models",
"/libs/common/data-access/src/lib/error",
"/libs/oms/data-access/src/lib/errors/return-process"
"/libs/oms/data-access/src/lib/errors/return-process",
"/libs/checkout/data-access/src/lib/schemas",
"/libs/checkout/data-access/src/lib/models",
"/libs/checkout/data-access/src/lib/facades"
],
"github.copilot.chat.commitMessageGeneration.instructions": [
{

View File

@@ -142,7 +142,6 @@ export class ApplicationServiceAdapter extends ApplicationService {
patchProcessData(processId: number, data: Record<string, unknown>): void {
const currentProcess = this.#tabService.entityMap()[processId];
const currentData: TabMetadata =
(currentProcess?.metadata?.['process_data'] as TabMetadata) ?? {};

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,33 +1,48 @@
import { ItemType, PriceDTO, PriceValueDTO, VATValueDTO } from '@generated/swagger/checkout-api';
import { OrderType, PurchaseOption } from './store';
export const PURCHASE_OPTIONS: PurchaseOption[] = [
'in-store',
'pickup',
'delivery',
'dig-delivery',
'b2b-delivery',
'download',
];
export const DELIVERY_PURCHASE_OPTIONS: PurchaseOption[] = ['delivery', 'dig-delivery', 'b2b-delivery'];
export const PURCHASE_OPTION_TO_ORDER_TYPE: { [purchaseOption: string]: OrderType } = {
'in-store': 'Rücklage',
pickup: 'Abholung',
delivery: 'Versand',
'dig-delivery': 'Versand',
'b2b-delivery': 'Versand',
};
export const GIFT_CARD_TYPE = 66560 as ItemType;
export const DEFAULT_PRICE_DTO: PriceDTO = { value: { value: undefined }, vat: { vatType: 0 } };
export const DEFAULT_PRICE_VALUE: PriceValueDTO = { value: 0, currency: 'EUR' };
export const DEFAULT_VAT_VALUE: VATValueDTO = { value: 0 };
export const GIFT_CARD_MAX_PRICE = 200;
export const PRICE_PATTERN = /^\d+(,\d{1,2})?$/;
import {
ItemType,
PriceDTO,
PriceValueDTO,
VATValueDTO,
} from '@generated/swagger/checkout-api';
import { PurchaseOption } from './store';
import { OrderType } from '@isa/checkout/data-access';
export const PURCHASE_OPTIONS: PurchaseOption[] = [
'in-store',
'pickup',
'delivery',
'dig-delivery',
'b2b-delivery',
'download',
];
export const DELIVERY_PURCHASE_OPTIONS: PurchaseOption[] = [
'delivery',
'dig-delivery',
'b2b-delivery',
];
export const PURCHASE_OPTION_TO_ORDER_TYPE: {
[purchaseOption: string]: OrderType;
} = {
'in-store': 'Rücklage',
'pickup': 'Abholung',
'delivery': 'Versand',
'dig-delivery': 'Versand',
'b2b-delivery': 'Versand',
};
export const GIFT_CARD_TYPE = 66560 as ItemType;
export const DEFAULT_PRICE_DTO: PriceDTO = {
value: { value: undefined },
vat: { vatType: 0 },
};
export const DEFAULT_PRICE_VALUE: PriceValueDTO = { value: 0, currency: 'EUR' };
export const DEFAULT_VAT_VALUE: VATValueDTO = { value: 0 };
export const GIFT_CARD_MAX_PRICE = 200;
export const PRICE_PATTERN = /^\d+(,\d{1,2})?$/;

View File

@@ -1,145 +1,181 @@
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, TrackByFunction, HostBinding } from '@angular/core';
import { UiModalRef } from '@ui/modal';
import { PurchaseOptionsModalData } from './purchase-options-modal.data';
import { PurchaseOptionsListHeaderComponent } from './purchase-options-list-header';
import { PurchaseOptionsListItemComponent } from './purchase-options-list-item';
import { CommonModule } from '@angular/common';
import { Subject, zip } from 'rxjs';
import {
DeliveryPurchaseOptionTileComponent,
DownloadPurchaseOptionTileComponent,
InStorePurchaseOptionTileComponent,
PickupPurchaseOptionTileComponent,
} from './purchase-options-tile';
import { isGiftCard, Item, PurchaseOption, PurchaseOptionsStore } from './store';
import { delay, map, shareReplay, skip, switchMap, takeUntil, tap } from 'rxjs/operators';
import { KeyValueDTOOfStringAndString } from '@generated/swagger/cat-search-api';
import { provideComponentStore } from '@ngrx/component-store';
@Component({
selector: 'shared-purchase-options-modal',
templateUrl: 'purchase-options-modal.component.html',
styleUrls: ['purchase-options-modal.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [provideComponentStore(PurchaseOptionsStore)],
imports: [
CommonModule,
PurchaseOptionsListHeaderComponent,
PurchaseOptionsListItemComponent,
DeliveryPurchaseOptionTileComponent,
InStorePurchaseOptionTileComponent,
PickupPurchaseOptionTileComponent,
DownloadPurchaseOptionTileComponent,
],
})
export class PurchaseOptionsModalComponent implements OnInit, OnDestroy {
get type() {
return this._uiModalRef.data.type;
}
@HostBinding('attr.data-loading')
get fetchingData() {
return this.store.fetchingAvailabilities.length > 0;
}
items$ = this.store.items$;
hasPrice$ = this.items$.pipe(
switchMap((items) =>
items.map((item) => {
let isArchive = false;
const features = item?.features as KeyValueDTOOfStringAndString[];
// Ticket #4074 analog zu Ticket #2244
// Ob Archivartikel kann nur über Kaufoptionen herausgefunden werden, nicht über Ändern im Warenkorb da am ShoppingCartItem das Archivartikel Feature fehlt
if (!!features && Array.isArray(features)) {
isArchive = !!features?.find((feature) => feature?.enabled === true && feature?.key === 'ARC');
}
return zip(
this.store
?.getPrice$(item?.id)
.pipe(
map((price) =>
isArchive
? !!price?.value?.value && price?.vat !== undefined && price?.vat?.value !== undefined
: !!price?.value?.value,
),
),
);
}),
),
switchMap((hasPrices) => hasPrices),
map((hasPrices) => {
const containsItemWithNoPrice = hasPrices?.filter((hasPrice) => hasPrice === false) ?? [];
return containsItemWithNoPrice?.length === 0;
}),
);
purchasingOptions$ = this.store.getPurchaseOptionsInAvailabilities$;
isDownloadOnly$ = this.purchasingOptions$.pipe(
map((purchasingOptions) => purchasingOptions.length === 1 && purchasingOptions[0] === 'download'),
);
isGiftCardOnly$ = this.store.items$.pipe(map((items) => items.every((item) => isGiftCard(item, this.store.type))));
hasDownload$ = this.purchasingOptions$.pipe(map((purchasingOptions) => purchasingOptions.includes('download')));
canContinue$ = this.store.canContinue$;
private _onDestroy$ = new Subject<void>();
saving = false;
constructor(
private _uiModalRef: UiModalRef<string, PurchaseOptionsModalData>,
public store: PurchaseOptionsStore,
) {
this.store.initialize(this._uiModalRef.data);
}
ngOnInit(): void {
this.items$.pipe(takeUntil(this._onDestroy$), skip(1), delay(100)).subscribe((items) => {
if (items.length === 0) {
this._uiModalRef.close();
return;
}
if (this._uiModalRef.data?.preSelectOption?.option) {
this.store.setPurchaseOption(this._uiModalRef.data?.preSelectOption?.option);
}
});
}
ngOnDestroy(): void {
this._onDestroy$.next();
this._onDestroy$.complete();
}
itemTrackBy: TrackByFunction<Item> = (_, item) => item.id;
showOption(option: PurchaseOption): boolean {
return this._uiModalRef.data?.preSelectOption?.showOptionOnly
? this._uiModalRef.data?.preSelectOption?.option === option
: true;
}
async save(action: string) {
if (this.saving) {
return;
}
this.saving = true;
try {
await this.store.save();
if (this.store.items.length === 0) {
this._uiModalRef.close(action);
}
} catch (error) {
console.error(error);
}
this.saving = false;
}
}
import {
Component,
ChangeDetectionStrategy,
OnInit,
OnDestroy,
TrackByFunction,
HostBinding,
} from '@angular/core';
import { UiModalRef } from '@ui/modal';
import { PurchaseOptionsModalContext } from './purchase-options-modal.data';
import { PurchaseOptionsListHeaderComponent } from './purchase-options-list-header';
import { PurchaseOptionsListItemComponent } from './purchase-options-list-item';
import { CommonModule } from '@angular/common';
import { Subject, zip } from 'rxjs';
import {
DeliveryPurchaseOptionTileComponent,
DownloadPurchaseOptionTileComponent,
InStorePurchaseOptionTileComponent,
PickupPurchaseOptionTileComponent,
} from './purchase-options-tile';
import {
isGiftCard,
Item,
PurchaseOption,
PurchaseOptionsStore,
} from './store';
import {
delay,
map,
shareReplay,
skip,
switchMap,
takeUntil,
tap,
} from 'rxjs/operators';
import { KeyValueDTOOfStringAndString } from '@generated/swagger/cat-search-api';
import { provideComponentStore } from '@ngrx/component-store';
@Component({
selector: 'shared-purchase-options-modal',
templateUrl: 'purchase-options-modal.component.html',
styleUrls: ['purchase-options-modal.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [provideComponentStore(PurchaseOptionsStore)],
imports: [
CommonModule,
PurchaseOptionsListHeaderComponent,
PurchaseOptionsListItemComponent,
DeliveryPurchaseOptionTileComponent,
InStorePurchaseOptionTileComponent,
PickupPurchaseOptionTileComponent,
DownloadPurchaseOptionTileComponent,
],
})
export class PurchaseOptionsModalComponent implements OnInit, OnDestroy {
get type() {
return this._uiModalRef.data.type;
}
@HostBinding('attr.data-loading')
get fetchingData() {
return this.store.fetchingAvailabilities.length > 0;
}
items$ = this.store.items$;
hasPrice$ = this.items$.pipe(
switchMap((items) =>
items.map((item) => {
let isArchive = false;
const features = item?.features as KeyValueDTOOfStringAndString[];
// Ticket #4074 analog zu Ticket #2244
// Ob Archivartikel kann nur über Kaufoptionen herausgefunden werden, nicht über Ändern im Warenkorb da am ShoppingCartItem das Archivartikel Feature fehlt
if (!!features && Array.isArray(features)) {
isArchive = !!features?.find(
(feature) => feature?.enabled === true && feature?.key === 'ARC',
);
}
return zip(
this.store
?.getPrice$(item?.id)
.pipe(
map((price) =>
isArchive
? !!price?.value?.value &&
price?.vat !== undefined &&
price?.vat?.value !== undefined
: !!price?.value?.value,
),
),
);
}),
),
switchMap((hasPrices) => hasPrices),
map((hasPrices) => {
const containsItemWithNoPrice =
hasPrices?.filter((hasPrice) => hasPrice === false) ?? [];
return containsItemWithNoPrice?.length === 0;
}),
);
purchasingOptions$ = this.store.getPurchaseOptionsInAvailabilities$;
isDownloadOnly$ = this.purchasingOptions$.pipe(
map(
(purchasingOptions) =>
purchasingOptions.length === 1 && purchasingOptions[0] === 'download',
),
);
isGiftCardOnly$ = this.store.items$.pipe(
map((items) => items.every((item) => isGiftCard(item, this.store.type))),
);
hasDownload$ = this.purchasingOptions$.pipe(
map((purchasingOptions) => purchasingOptions.includes('download')),
);
canContinue$ = this.store.canContinue$;
private _onDestroy$ = new Subject<void>();
saving = false;
constructor(
private _uiModalRef: UiModalRef<string, PurchaseOptionsModalContext>,
public store: PurchaseOptionsStore,
) {
this.store.initialize(this._uiModalRef.data);
}
ngOnInit(): void {
this.items$
.pipe(takeUntil(this._onDestroy$), skip(1), delay(100))
.subscribe((items) => {
if (items.length === 0) {
this._uiModalRef.close();
return;
}
if (this._uiModalRef.data?.preSelectOption?.option) {
this.store.setPurchaseOption(
this._uiModalRef.data?.preSelectOption?.option,
);
}
});
}
ngOnDestroy(): void {
this._onDestroy$.next();
this._onDestroy$.complete();
}
itemTrackBy: TrackByFunction<Item> = (_, item) => item.id;
showOption(option: PurchaseOption): boolean {
return this._uiModalRef.data?.preSelectOption?.showOptionOnly
? this._uiModalRef.data?.preSelectOption?.option === option
: true;
}
async save(action: string) {
if (this.saving) {
return;
}
this.saving = true;
try {
await this.store.save();
if (this.store.items.length === 0) {
this._uiModalRef.close(action);
}
} catch (error) {
console.error(error);
}
this.saving = false;
}
}

View File

@@ -1,12 +1,28 @@
import { ItemDTO } from '@generated/swagger/cat-search-api';
import { ShoppingCartItemDTO, BranchDTO } from '@generated/swagger/checkout-api';
import { ActionType, PurchaseOption } from './store';
export interface PurchaseOptionsModalData {
processId: number;
type: ActionType;
items: Array<ItemDTO | ShoppingCartItemDTO>;
pickupBranch?: BranchDTO;
inStoreBranch?: BranchDTO;
preSelectOption?: { option: PurchaseOption; showOptionOnly?: boolean };
}
import { ItemDTO } from '@generated/swagger/cat-search-api';
import {
ShoppingCartItemDTO,
BranchDTO,
} from '@generated/swagger/checkout-api';
import { Customer } from '@isa/crm/data-access';
import { ActionType, PurchaseOption } from './store';
export interface PurchaseOptionsModalData {
tabId: number;
shoppingCartId: number;
type: ActionType;
items: Array<ItemDTO | ShoppingCartItemDTO>;
pickupBranch?: BranchDTO;
inStoreBranch?: BranchDTO;
preSelectOption?: { option: PurchaseOption; showOptionOnly?: boolean };
}
export interface PurchaseOptionsModalContext {
shoppingCartId: number;
type: ActionType;
items: Array<ItemDTO | ShoppingCartItemDTO>;
selectedCustomer?: Customer;
selectedBranch?: BranchDTO;
pickupBranch?: BranchDTO;
inStoreBranch?: BranchDTO;
preSelectOption?: { option: PurchaseOption; showOptionOnly?: boolean };
}

View File

@@ -1,16 +1,48 @@
import { Injectable } from '@angular/core';
import { UiModalRef, UiModalService } from '@ui/modal';
import { PurchaseOptionsModalComponent } from './purchase-options-modal.component';
import { PurchaseOptionsModalData } from './purchase-options-modal.data';
@Injectable({ providedIn: 'root' })
export class PurchaseOptionsModalService {
constructor(private _uiModal: UiModalService) {}
open(data: PurchaseOptionsModalData): UiModalRef<string, PurchaseOptionsModalData> {
return this._uiModal.open<string, PurchaseOptionsModalData>({
content: PurchaseOptionsModalComponent,
data,
});
}
}
import { Injectable, inject } from '@angular/core';
import { UiModalRef, UiModalService } from '@ui/modal';
import { PurchaseOptionsModalComponent } from './purchase-options-modal.component';
import {
PurchaseOptionsModalData,
PurchaseOptionsModalContext,
} from './purchase-options-modal.data';
import {
SelectedCustomerFacade,
CustomerFacade,
Customer,
} from '@isa/crm/data-access';
@Injectable({ providedIn: 'root' })
export class PurchaseOptionsModalService {
#uiModal = inject(UiModalService);
#selectedCustomerFacade = inject(SelectedCustomerFacade);
#customerFacade = inject(CustomerFacade);
async open(
data: PurchaseOptionsModalData,
): Promise<UiModalRef<string, PurchaseOptionsModalData>> {
const context: PurchaseOptionsModalContext = {
...data,
};
context.selectedCustomer = await this.#getSelectedCustomer(data);
return this.#uiModal.open<string, PurchaseOptionsModalContext>({
content: PurchaseOptionsModalComponent,
data: context,
});
}
#getSelectedCustomer({
tabId,
}: {
tabId: number;
}): Promise<Customer | undefined> {
const customerId = this.#selectedCustomerFacade.get(tabId);
if (!customerId) {
return Promise.resolve(undefined);
}
return this.#customerFacade.fetchCustomer({ customerId });
}
}

View File

@@ -1,158 +1,180 @@
import { PriceDTO } from '@generated/swagger/availability-api';
import { ItemDTO } from '@generated/swagger/cat-search-api';
import { AvailabilityDTO, OLAAvailabilityDTO, ShoppingCartItemDTO } from '@generated/swagger/checkout-api';
import { GIFT_CARD_TYPE } from '../constants';
import {
ActionType,
Item,
ItemData,
ItemPayloadWithSourceId,
OrderType,
PurchaseOption,
} from './purchase-options.types';
export function isItemDTO(item: any, type: ActionType): item is ItemDTO {
return type === 'add';
}
export function isItemDTOArray(items: any, type: ActionType): items is ItemDTO[] {
return type === 'add';
}
export function isShoppingCartItemDTO(item: any, type: ActionType): item is ShoppingCartItemDTO {
return type === 'update';
}
export function isShoppingCartItemDTOArray(items: any, type: ActionType): items is ShoppingCartItemDTO[] {
return type === 'update';
}
export function mapToItemData(item: Item, type: ActionType): ItemData {
const price: PriceDTO = {};
if (isItemDTO(item, type)) {
price.value = item?.catalogAvailability?.price?.value ?? {};
price.vat = item?.catalogAvailability?.price?.vat ?? {};
return {
ean: item.product.ean,
itemId: item.id,
price,
sourceId: item.id,
quantity: item.quantity ?? 1,
};
} else {
price.value = item?.unitPrice?.value ?? {};
price.vat = item?.unitPrice?.vat ?? {};
return {
ean: item.product.ean,
itemId: Number(item.product.catalogProductNumber),
price,
sourceId: item.id,
quantity: item.quantity ?? 1,
};
}
}
export function isDownload(item: Item): boolean {
return item.product.format === 'DL' || item.product.format === 'EB';
}
export function isGiftCard(item: Item, type: ActionType): boolean {
if (isItemDTO(item, type)) {
return item?.type === GIFT_CARD_TYPE;
} else {
return item?.itemType === GIFT_CARD_TYPE;
}
}
export function isArchive(item: Item, type: ActionType): boolean {
if (isItemDTO(item, type)) {
return item?.features?.some((f) => f.key === 'ARC');
} else {
return !!item?.features?.['ARC'];
}
}
export function mapToItemPayload({
item,
quantity,
availability,
type,
}: {
item: ItemDTO | ShoppingCartItemDTO;
quantity: number;
availability: AvailabilityDTO;
type: ActionType;
}): ItemPayloadWithSourceId {
return {
availabilities: [mapToOlaAvailability({ item, quantity, availability, type })],
id: String(getCatalogId(item, type)),
sourceId: item.id,
};
}
export function getCatalogId(item: ItemDTO | ShoppingCartItemDTO, type: ActionType): number | string {
return isItemDTO(item, type) ? item.id : item.product.catalogProductNumber;
}
export function mapToOlaAvailability({
availability,
item,
quantity,
type,
}: {
availability: AvailabilityDTO;
item: ItemDTO | ShoppingCartItemDTO;
quantity: number;
type: ActionType;
}): OLAAvailabilityDTO {
return {
status: availability?.availabilityType,
at: availability?.estimatedShippingDate,
ean: item?.product?.ean,
itemId: Number(getCatalogId(item, type)),
format: item?.product?.format,
isPrebooked: availability?.isPrebooked,
logisticianId: availability?.logistician?.id,
price: availability?.price,
qty: quantity,
ssc: availability?.ssc,
sscText: availability?.sscText,
supplierId: availability?.supplier?.id,
supplierProductNumber: availability?.supplierProductNumber,
};
}
export function getOrderTypeForPurchaseOption(purchaseOption: PurchaseOption): OrderType | undefined {
switch (purchaseOption) {
case 'delivery':
case 'dig-delivery':
case 'b2b-delivery':
return 'Versand';
case 'pickup':
return 'Abholung';
case 'in-store':
return 'Rücklage';
case 'download':
return 'Download';
default:
return undefined;
}
}
export function getPurchaseOptionForOrderType(orderType: OrderType): PurchaseOption | undefined {
switch (orderType) {
case 'Versand':
return 'delivery';
case 'Abholung':
return 'pickup';
case 'Rücklage':
return 'in-store';
case 'Download':
return 'download';
default:
return undefined;
}
}
import { PriceDTO } from '@generated/swagger/availability-api';
import { ItemDTO } from '@generated/swagger/cat-search-api';
import {
AvailabilityDTO,
OLAAvailabilityDTO,
ShoppingCartItemDTO,
} from '@generated/swagger/checkout-api';
import { GIFT_CARD_TYPE } from '../constants';
import {
ActionType,
Item,
ItemData,
ItemPayloadWithSourceId,
PurchaseOption,
} from './purchase-options.types';
import { OrderType } from '@isa/checkout/data-access';
export function isItemDTO(item: any, type: ActionType): item is ItemDTO {
return type === 'add';
}
export function isItemDTOArray(
items: any,
type: ActionType,
): items is ItemDTO[] {
return type === 'add';
}
export function isShoppingCartItemDTO(
item: any,
type: ActionType,
): item is ShoppingCartItemDTO {
return type === 'update';
}
export function isShoppingCartItemDTOArray(
items: any,
type: ActionType,
): items is ShoppingCartItemDTO[] {
return type === 'update';
}
export function mapToItemData(item: Item, type: ActionType): ItemData {
const price: PriceDTO = {};
if (isItemDTO(item, type)) {
price.value = item?.catalogAvailability?.price?.value ?? {};
price.vat = item?.catalogAvailability?.price?.vat ?? {};
return {
ean: item.product.ean,
itemId: item.id,
price,
sourceId: item.id,
quantity: item.quantity ?? 1,
};
} else {
price.value = item?.unitPrice?.value ?? {};
price.vat = item?.unitPrice?.vat ?? {};
return {
ean: item.product.ean,
itemId: Number(item.product.catalogProductNumber),
price,
sourceId: item.id,
quantity: item.quantity ?? 1,
};
}
}
export function isDownload(item: Item): boolean {
return item.product.format === 'DL' || item.product.format === 'EB';
}
export function isGiftCard(item: Item, type: ActionType): boolean {
if (isItemDTO(item, type)) {
return item?.type === GIFT_CARD_TYPE;
} else {
return item?.itemType === GIFT_CARD_TYPE;
}
}
export function isArchive(item: Item, type: ActionType): boolean {
if (isItemDTO(item, type)) {
return item?.features?.some((f) => f.key === 'ARC');
} else {
return !!item?.features?.['ARC'];
}
}
export function mapToItemPayload({
item,
quantity,
availability,
type,
}: {
item: ItemDTO | ShoppingCartItemDTO;
quantity: number;
availability: AvailabilityDTO;
type: ActionType;
}): ItemPayloadWithSourceId {
return {
availabilities: [
mapToOlaAvailability({ item, quantity, availability, type }),
],
id: String(getCatalogId(item, type)),
sourceId: item.id,
};
}
export function getCatalogId(
item: ItemDTO | ShoppingCartItemDTO,
type: ActionType,
): number | string {
return isItemDTO(item, type) ? item.id : item.product.catalogProductNumber;
}
export function mapToOlaAvailability({
availability,
item,
quantity,
type,
}: {
availability: AvailabilityDTO;
item: ItemDTO | ShoppingCartItemDTO;
quantity: number;
type: ActionType;
}): OLAAvailabilityDTO {
return {
status: availability?.availabilityType,
at: availability?.estimatedShippingDate,
ean: item?.product?.ean,
itemId: Number(getCatalogId(item, type)),
format: item?.product?.format,
isPrebooked: availability?.isPrebooked,
logisticianId: availability?.logistician?.id,
price: availability?.price,
qty: quantity,
ssc: availability?.ssc,
sscText: availability?.sscText,
supplierId: availability?.supplier?.id,
supplierProductNumber: availability?.supplierProductNumber,
};
}
export function getOrderTypeForPurchaseOption(
purchaseOption: PurchaseOption,
): OrderType | undefined {
switch (purchaseOption) {
case 'delivery':
case 'dig-delivery':
case 'b2b-delivery':
return 'Versand';
case 'pickup':
return 'Abholung';
case 'in-store':
return 'Rücklage';
case 'download':
return 'Download';
default:
return undefined;
}
}
export function getPurchaseOptionForOrderType(
orderType: OrderType,
): PurchaseOption | undefined {
switch (orderType) {
case 'Versand':
return 'delivery';
case 'Abholung':
return 'pickup';
case 'Rücklage':
return 'in-store';
case 'Download':
return 'download';
default:
return undefined;
}
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,178 +1,240 @@
import { Injectable } from '@angular/core';
import { DomainAvailabilityService } from '@domain/availability';
import { DomainCheckoutService } from '@domain/checkout';
import {
AddToShoppingCartDTO,
AvailabilityDTO,
EntityDTOContainerOfDestinationDTO,
ItemPayload,
ItemsResult,
ShoppingCartDTO,
UpdateShoppingCartItemDTO,
} from '@generated/swagger/checkout-api';
import { Observable } from 'rxjs';
import { map, shareReplay, take } from 'rxjs/operators';
import { Branch, ItemData } from './purchase-options.types';
import { memorize } from '@utils/common';
import { AuthService } from '@core/auth';
import { ApplicationService } from '@core/application';
import { DomainOmsService } from '@domain/oms';
@Injectable({ providedIn: 'root' })
export class PurchaseOptionsService {
constructor(
private _availabilityService: DomainAvailabilityService,
private _checkoutService: DomainCheckoutService,
private _omsService: DomainOmsService,
private _auth: AuthService,
private _app: ApplicationService,
) {}
getVats$() {
return this._omsService.getVATs();
}
getSelectedBranchForProcess(processId: number): Observable<Branch> {
return this._app.getSelectedBranch$(processId).pipe(take(1), shareReplay(1));
}
getCustomerFeatures(processId: number): Observable<Record<string, string>> {
return this._checkoutService.getCustomerFeatures({ processId }).pipe(take(1), shareReplay(1));
}
@memorize()
fetchDefaultBranch(): Observable<Branch> {
return this.getBranch({ branchNumber: this._auth.getClaimByKey('branch_no') }).pipe(take(1), shareReplay(1));
}
fetchPickupAvailability(item: ItemData, quantity: number, branch: Branch): Observable<AvailabilityDTO> {
return this._availabilityService
.getPickUpAvailability({
branch,
quantity,
item,
})
.pipe(map((res) => (Array.isArray(res) ? res[0] : undefined)));
}
fetchInStoreAvailability(item: ItemData, quantity: number, branch: Branch): Observable<AvailabilityDTO> {
return this._availabilityService.getTakeAwayAvailability({
item,
quantity,
branch,
});
}
fetchDeliveryAvailability(item: ItemData, quantity: number): Observable<AvailabilityDTO> {
return this._availabilityService.getDeliveryAvailability({
item,
quantity,
});
}
fetchDigDeliveryAvailability(item: ItemData, quantity: number): Observable<AvailabilityDTO> {
return this._availabilityService.getDigDeliveryAvailability({
item,
quantity,
});
}
fetchB2bDeliveryAvailability(item: ItemData, quantity: number): Observable<AvailabilityDTO> {
return this._availabilityService.getB2bDeliveryAvailability({
item,
quantity,
});
}
fetchDownloadAvailability(item: ItemData): Observable<AvailabilityDTO> {
return this._availabilityService.getDownloadAvailability({
item,
});
}
isAvailable(availability: AvailabilityDTO): boolean {
return this._availabilityService.isAvailable({ availability });
}
fetchCanAdd(processId: number, orderType: string, payload: ItemPayload[]): Observable<ItemsResult[]> {
return this._checkoutService.canAddItems({
processId,
orderType,
payload,
});
}
removeItemFromShoppingCart(processId: number, shoppingCartItemId: number): Promise<ShoppingCartDTO> {
return this._checkoutService
.updateItemInShoppingCart({
processId,
shoppingCartItemId,
update: {
availability: null,
quantity: 0,
},
})
.toPromise();
}
getInStoreDestination(branch: Branch): EntityDTOContainerOfDestinationDTO {
return {
data: { target: 1, targetBranch: { id: branch.id } },
};
}
getPickupDestination(branch: Branch): EntityDTOContainerOfDestinationDTO {
return {
data: { target: 1, targetBranch: { id: branch.id } },
};
}
getDeliveryDestination(availability: AvailabilityDTO): EntityDTOContainerOfDestinationDTO {
return {
data: { target: 2, logistician: availability?.logistician },
};
}
getDownloadDestination(availability: AvailabilityDTO): EntityDTOContainerOfDestinationDTO {
return {
data: { target: 16, logistician: availability?.logistician },
};
}
addItemToShoppingCart(processId: number, items: AddToShoppingCartDTO[]) {
return this._checkoutService.addItemToShoppingCart({
processId,
items,
});
}
updateItemInShoppingCart(processId: number, shoppingCartItemId: number, payload: UpdateShoppingCartItemDTO) {
return this._checkoutService.updateItemInShoppingCart({
processId,
shoppingCartItemId,
update: payload,
});
}
@memorize({ comparer: (_) => true })
getBranches(): Observable<Branch[]> {
return this._availabilityService.getBranches().pipe(
map((branches) => {
return branches.filter((branch) => branch.isShippingEnabled == true);
}),
shareReplay(1),
);
}
getBranch(params: { id: number }): Observable<Branch>;
getBranch(params: { branchNumber: string }): Observable<Branch>;
getBranch(params: { id: number; branchNumber: string }): Observable<Branch>;
getBranch(params: { id?: number; branchNumber?: string }): Observable<Branch> {
return this.getBranches().pipe(
map((branches) => {
const branch = branches.find((branch) => branch.id == params.id || branch.branchNumber == params.branchNumber);
return branch;
}),
);
}
}
import { inject, Injectable } from '@angular/core';
import { DomainAvailabilityService } from '@domain/availability';
import { DomainCheckoutService } from '@domain/checkout';
import {
AddToShoppingCartDTO,
AvailabilityDTO,
EntityDTOContainerOfDestinationDTO,
ItemPayload,
ItemsResult,
ShoppingCartDTO,
UpdateShoppingCartItemDTO,
} from '@generated/swagger/checkout-api';
import { Observable } from 'rxjs';
import { map, shareReplay, take } from 'rxjs/operators';
import { Branch, ItemData } from './purchase-options.types';
import { memorize } from '@utils/common';
import { AuthService } from '@core/auth';
import { ApplicationService } from '@core/application';
import { DomainOmsService } from '@domain/oms';
import { OrderType, PurchaseOptionsFacade } from '@isa/checkout/data-access';
@Injectable({ providedIn: 'root' })
export class PurchaseOptionsService {
#purchaseOptionsFacade = inject(PurchaseOptionsFacade);
constructor(
private _availabilityService: DomainAvailabilityService,
private _checkoutService: DomainCheckoutService,
private _omsService: DomainOmsService,
private _auth: AuthService,
private _app: ApplicationService,
) {}
getVats$() {
return this._omsService.getVATs();
}
getSelectedBranchForProcess(processId: number): Observable<Branch> {
return this._app
.getSelectedBranch$(processId)
.pipe(take(1), shareReplay(1));
}
getCustomerFeatures(processId: number): Observable<Record<string, string>> {
return this._checkoutService
.getCustomerFeatures({ processId })
.pipe(take(1), shareReplay(1));
}
@memorize()
fetchDefaultBranch(): Observable<Branch> {
return this.getBranch({
branchNumber: this._auth.getClaimByKey('branch_no'),
}).pipe(take(1), shareReplay(1));
}
fetchPickupAvailability(
item: ItemData,
quantity: number,
branch: Branch,
): Observable<AvailabilityDTO> {
return this._availabilityService
.getPickUpAvailability({
branch,
quantity,
item,
})
.pipe(map((res) => (Array.isArray(res) ? res[0] : undefined)));
}
fetchInStoreAvailability(
item: ItemData,
quantity: number,
branch: Branch,
): Observable<AvailabilityDTO> {
return this._availabilityService.getTakeAwayAvailability({
item,
quantity,
branch,
});
}
fetchDeliveryAvailability(
item: ItemData,
quantity: number,
): Observable<AvailabilityDTO> {
return this._availabilityService.getDeliveryAvailability({
item,
quantity,
});
}
fetchDigDeliveryAvailability(
item: ItemData,
quantity: number,
): Observable<AvailabilityDTO> {
return this._availabilityService.getDigDeliveryAvailability({
item,
quantity,
});
}
fetchB2bDeliveryAvailability(
item: ItemData,
quantity: number,
): Observable<AvailabilityDTO> {
return this._availabilityService.getB2bDeliveryAvailability({
item,
quantity,
});
}
fetchDownloadAvailability(item: ItemData): Observable<AvailabilityDTO> {
return this._availabilityService.getDownloadAvailability({
item,
});
}
isAvailable(availability: AvailabilityDTO): boolean {
return this._availabilityService.isAvailable({ availability });
}
fetchCanAdd(
shoppingCartId: number,
orderType: OrderType,
payload: ItemPayload[],
customerFeatures: Record<string, string>,
): Promise<ItemsResult[]> {
return this.#purchaseOptionsFacade.canAddItems({
shoppingCartId,
payload: payload.map((p) => ({
...p,
customerFeatures: customerFeatures,
orderType: orderType,
})),
});
}
removeItemFromShoppingCart(
shoppingCartId: number,
shoppingCartItemId: number,
): Promise<ShoppingCartDTO> {
const shoppingCart = this.#purchaseOptionsFacade.removeItem({
shoppingCartId,
shoppingCartItemId,
});
return shoppingCart;
}
getInStoreDestination(branch: Branch): EntityDTOContainerOfDestinationDTO {
return {
data: { target: 1, targetBranch: { id: branch.id } },
};
}
getPickupDestination(branch: Branch): EntityDTOContainerOfDestinationDTO {
return {
data: { target: 1, targetBranch: { id: branch.id } },
};
}
getDeliveryDestination(
availability: AvailabilityDTO,
): EntityDTOContainerOfDestinationDTO {
return {
data: { target: 2, logistician: availability?.logistician },
};
}
getDownloadDestination(
availability: AvailabilityDTO,
): EntityDTOContainerOfDestinationDTO {
return {
data: { target: 16, logistician: availability?.logistician },
};
}
async addItemToShoppingCart(
shoppingCartId: number,
items: AddToShoppingCartDTO[],
) {
const shoppingCart = await this.#purchaseOptionsFacade.addItem({
shoppingCartId,
items,
});
console.log('added item to cart', { shoppingCart });
this._checkoutService.updateProcessCount(
this._app.activatedProcessId,
shoppingCart,
);
return shoppingCart;
}
async updateItemInShoppingCart(
shoppingCartId: number,
shoppingCartItemId: number,
payload: UpdateShoppingCartItemDTO,
) {
const shoppingCart = await this.#purchaseOptionsFacade.updateItem({
shoppingCartId,
shoppingCartItemId,
values: payload,
});
console.log('updated item in cart', { shoppingCart });
this._checkoutService.updateProcessCount(
this._app.activatedProcessId,
shoppingCart,
);
}
@memorize({ comparer: (_) => true })
getBranches(): Observable<Branch[]> {
return this._availabilityService.getBranches().pipe(
map((branches) => {
return branches.filter((branch) => branch.isShippingEnabled == true);
}),
shareReplay(1),
);
}
getBranch(params: { id: number }): Observable<Branch>;
getBranch(params: { branchNumber: string }): Observable<Branch>;
getBranch(params: { id: number; branchNumber: string }): Observable<Branch>;
getBranch(params: {
id?: number;
branchNumber?: string;
}): Observable<Branch> {
return this.getBranches().pipe(
map((branches) => {
const branch = branches.find(
(branch) =>
branch.id == params.id ||
branch.branchNumber == params.branchNumber,
);
return branch;
}),
);
}
}

View File

@@ -1,38 +1,38 @@
import { PriceDTO } from '@generated/swagger/checkout-api';
import {
ActionType,
Availability,
Branch,
CanAdd,
FetchingAvailability,
Item,
PurchaseOption,
} from './purchase-options.types';
export interface PurchaseOptionsState {
type: ActionType;
processId: number;
items: Item[];
availabilities: Availability[];
canAddResults: CanAdd[];
purchaseOption: PurchaseOption;
selectedItemIds: number[];
prices: { [itemId: number]: PriceDTO };
defaultBranch: Branch;
pickupBranch: Branch;
inStoreBranch: Branch;
customerFeatures: Record<string, string>;
fetchingAvailabilities: Array<FetchingAvailability>;
}
import { PriceDTO } from '@generated/swagger/checkout-api';
import {
ActionType,
Availability,
Branch,
CanAdd,
FetchingAvailability,
Item,
PurchaseOption,
} from './purchase-options.types';
export interface PurchaseOptionsState {
shoppingCartId: number;
type: ActionType;
items: Item[];
availabilities: Availability[];
canAddResults: CanAdd[];
purchaseOption: PurchaseOption;
selectedItemIds: number[];
prices: { [itemId: number]: PriceDTO };
defaultBranch: Branch;
pickupBranch: Branch;
inStoreBranch: Branch;
customerFeatures: Record<string, string>;
fetchingAvailabilities: Array<FetchingAvailability>;
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,37 +1,56 @@
import { ItemData as AvailabilityItemData } from '@domain/availability';
import { ItemDTO } from '@generated/swagger/cat-search-api';
import { AvailabilityDTO, BranchDTO, ItemPayload, ShoppingCartItemDTO } from '@generated/swagger/checkout-api';
export type ActionType = 'add' | 'update';
export type PurchaseOption =
| 'delivery'
| 'dig-delivery'
| 'b2b-delivery'
| 'pickup'
| 'in-store'
| 'download'
| 'catalog';
export type OrderType = 'Rücklage' | 'Abholung' | 'Versand' | 'Download';
export type ItemDTOWithQuantity = ItemDTO & { quantity?: number };
export type Item = ItemDTOWithQuantity | ShoppingCartItemDTO;
export type Branch = BranchDTO;
export type Availability = {
itemId: number;
purchaseOption: PurchaseOption;
data: AvailabilityDTO & { priceMaintained?: boolean; orderDeadline?: string; firstDayOfSale?: string };
ean?: string;
};
export type ItemData = AvailabilityItemData & { sourceId: number; quantity: number };
export type ItemPayloadWithSourceId = ItemPayload & { sourceId: number };
export type CanAdd = { itemId: number; purchaseOption: PurchaseOption; canAdd: boolean; message?: string };
export type FetchingAvailability = { id: string; itemId: number; purchaseOption?: PurchaseOption };
import { ItemData as AvailabilityItemData } from '@domain/availability';
import { ItemDTO } from '@generated/swagger/cat-search-api';
import {
AvailabilityDTO,
BranchDTO,
ItemPayload,
ShoppingCartItemDTO,
} from '@generated/swagger/checkout-api';
export type ActionType = 'add' | 'update';
export type PurchaseOption =
| 'delivery'
| 'dig-delivery'
| 'b2b-delivery'
| 'pickup'
| 'in-store'
| 'download'
| 'catalog';
export type ItemDTOWithQuantity = ItemDTO & { quantity?: number };
export type Item = ItemDTOWithQuantity | ShoppingCartItemDTO;
export type Branch = BranchDTO;
export type Availability = {
itemId: number;
purchaseOption: PurchaseOption;
data: AvailabilityDTO & {
priceMaintained?: boolean;
orderDeadline?: string;
firstDayOfSale?: string;
};
ean?: string;
};
export type ItemData = AvailabilityItemData & {
sourceId: number;
quantity: number;
};
export type ItemPayloadWithSourceId = ItemPayload & { sourceId: number };
export type CanAdd = {
itemId: number;
purchaseOption: PurchaseOption;
canAdd: boolean;
message?: string;
};
export type FetchingAvailability = {
id: string;
itemId: number;
purchaseOption?: PurchaseOption;
};

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,185 +1,237 @@
import { Injectable } from '@angular/core';
import { UntypedFormGroup } from '@angular/forms';
import { ApplicationService } from '@core/application';
import { DomainCheckoutService } from '@domain/checkout';
import { ComponentStore } from '@ngrx/component-store';
import { tapResponse } from '@ngrx/operators';
import { NotificationChannel, PayerDTO, ShoppingCartDTO, ShoppingCartItemDTO } from '@generated/swagger/checkout-api';
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { BehaviorSubject, Subject } from 'rxjs';
import { first, map, switchMap, takeUntil, tap, withLatestFrom } from 'rxjs/operators';
export interface CheckoutReviewState {
payer: PayerDTO;
shoppingCart: ShoppingCartDTO;
shoppingCartItems: ShoppingCartItemDTO[];
fetching: boolean;
}
@Injectable()
export class CheckoutReviewStore extends ComponentStore<CheckoutReviewState> {
orderCompleted = new Subject<void>();
get shoppingCart() {
return this.get((s) => s.shoppingCart);
}
set shoppingCart(shoppingCart: ShoppingCartDTO) {
this.patchState({ shoppingCart });
}
readonly shoppingCart$ = this.select((s) => s.shoppingCart);
get shoppingCartItems() {
return this.get((s) => s.shoppingCartItems);
}
set shoppingCartItems(shoppingCartItems: ShoppingCartItemDTO[]) {
this.patchState({ shoppingCartItems });
}
readonly shoppingCartItems$ = this.select((s) => s.shoppingCartItems);
get fetching() {
return this.get((s) => s.fetching);
}
set fetching(fetching: boolean) {
this.patchState({ fetching });
}
readonly fetching$ = this.select((s) => s.fetching);
customerFeatures$ = this._application.activatedProcessId$.pipe(
takeUntil(this.orderCompleted),
switchMap((processId) => this._domainCheckoutService.getCustomerFeatures({ processId })),
);
payer$ = this._application.activatedProcessId$.pipe(
takeUntil(this.orderCompleted),
switchMap((processId) => this._domainCheckoutService.getPayer({ processId })),
);
buyer$ = this._application.activatedProcessId$.pipe(
takeUntil(this.orderCompleted),
switchMap((processId) => this._domainCheckoutService.getBuyer({ processId })),
);
showBillingAddress$ = this.shoppingCartItems$.pipe(
withLatestFrom(this.customerFeatures$),
map(
([items, customerFeatures]) =>
items.some(
(item) =>
item.features?.orderType === 'Versand' ||
item.features?.orderType === 'B2B-Versand' ||
item.features?.orderType === 'DIG-Versand',
) || !!customerFeatures?.b2b,
),
);
checkNotificationChannelControl$ = new BehaviorSubject<boolean>(true);
notificationChannelLoading$ = new Subject<boolean>();
notificationsControl: UntypedFormGroup;
constructor(
private _domainCheckoutService: DomainCheckoutService,
private _application: ApplicationService,
private _uiModal: UiModalService,
) {
super({ payer: undefined, shoppingCart: undefined, shoppingCartItems: [], fetching: false });
}
loadShoppingCart = this.effect(($) =>
$.pipe(
tap(() => (this.fetching = true)),
withLatestFrom(this._application.activatedProcessId$),
switchMap(([_, processId]) => {
return this._domainCheckoutService.getShoppingCart({ processId, latest: true }).pipe(
tapResponse(
(shoppingCart) => {
const shoppingCartItems = shoppingCart?.items?.map((item) => item.data) || [];
this.patchState({
shoppingCart,
shoppingCartItems,
});
},
(err) => {},
() => {},
),
);
}),
tap(() => (this.fetching = false)),
),
);
async onNotificationChange(notificationChannels?: NotificationChannel[]) {
this.notificationChannelLoading$.next(true);
try {
const control = this.notificationsControl?.getRawValue();
const notificationChannel = notificationChannels
? (notificationChannels.reduce((val, current) => val | current, 0) as NotificationChannel)
: control?.notificationChannel?.selected || 0;
const processId = await this._application.activatedProcessId$.pipe(first()).toPromise();
const email = control?.notificationChannel?.email;
const mobile = control?.notificationChannel?.mobile;
// Check if E-Mail and Mobilnumber is available if E-Mail or SMS checkbox is active
if (notificationChannel === 3 && (!email || !mobile)) {
this.checkNotificationChannelControl$.next(false);
} else if (notificationChannel === 2 && !mobile) {
this.checkNotificationChannelControl$.next(false);
} else if (notificationChannel === 1 && !email) {
this.checkNotificationChannelControl$.next(false);
} else {
this.checkNotificationChannelControl$.next(true);
}
// NotificationChannel nur speichern, wenn Haken und Value gesetzt
let setNotificationChannel = 0;
if ((notificationChannel & 1) === 1 && email) {
setNotificationChannel += 1;
}
if ((notificationChannel & 2) === 2 && mobile) {
setNotificationChannel += 2;
}
if (notificationChannel > 0) {
this.setCommunicationDetails({ processId, notificationChannel, email, mobile });
}
this._domainCheckoutService.setNotificationChannels({
processId,
notificationChannels: (setNotificationChannel as NotificationChannel) || 0,
});
} catch (error) {
this._uiModal.open({
content: UiErrorModalComponent,
data: error,
title: 'Fehler beim setzen des Benachrichtigungskanals',
});
}
this.notificationChannelLoading$.next(false);
}
setCommunicationDetails({
processId,
notificationChannel,
email,
mobile,
}: {
processId: number;
notificationChannel: number;
email: string;
mobile: string;
}) {
const emailValid = this.notificationsControl?.get('notificationChannel')?.get('email')?.valid;
const mobileValid = this.notificationsControl?.get('notificationChannel')?.get('mobile')?.valid;
if (notificationChannel === 3 && emailValid && mobileValid) {
this._domainCheckoutService.setBuyerCommunicationDetails({ processId, email, mobile });
} else if (notificationChannel === 1 && emailValid) {
this._domainCheckoutService.setBuyerCommunicationDetails({ processId, email });
} else if (notificationChannel === 2 && mobileValid) {
this._domainCheckoutService.setBuyerCommunicationDetails({ processId, mobile });
}
}
}
import { Injectable } from '@angular/core';
import { UntypedFormGroup } from '@angular/forms';
import { ApplicationService } from '@core/application';
import { DomainCheckoutService } from '@domain/checkout';
import { ComponentStore } from '@ngrx/component-store';
import { tapResponse } from '@ngrx/operators';
import {
NotificationChannel,
PayerDTO,
ShoppingCartDTO,
ShoppingCartItemDTO,
} from '@generated/swagger/checkout-api';
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { BehaviorSubject, Subject } from 'rxjs';
import {
first,
map,
switchMap,
takeUntil,
tap,
withLatestFrom,
} from 'rxjs/operators';
export interface CheckoutReviewState {
payer: PayerDTO;
shoppingCart: ShoppingCartDTO;
shoppingCartItems: ShoppingCartItemDTO[];
fetching: boolean;
}
@Injectable()
export class CheckoutReviewStore extends ComponentStore<CheckoutReviewState> {
orderCompleted = new Subject<void>();
get shoppingCart() {
return this.get((s) => s.shoppingCart);
}
set shoppingCart(shoppingCart: ShoppingCartDTO) {
this.patchState({ shoppingCart });
}
readonly shoppingCart$ = this.select((s) => s.shoppingCart);
get shoppingCartItems() {
return this.get((s) => s.shoppingCartItems);
}
set shoppingCartItems(shoppingCartItems: ShoppingCartItemDTO[]) {
this.patchState({ shoppingCartItems });
}
readonly shoppingCartItems$ = this.select((s) => s.shoppingCartItems);
get fetching() {
return this.get((s) => s.fetching);
}
set fetching(fetching: boolean) {
this.patchState({ fetching });
}
readonly fetching$ = this.select((s) => s.fetching);
customerFeatures$ = this._application.activatedProcessId$.pipe(
takeUntil(this.orderCompleted),
switchMap((processId) =>
this._domainCheckoutService.getCustomerFeatures({ processId }),
),
);
payer$ = this._application.activatedProcessId$.pipe(
takeUntil(this.orderCompleted),
switchMap((processId) =>
this._domainCheckoutService.getPayer({ processId }),
),
);
buyer$ = this._application.activatedProcessId$.pipe(
takeUntil(this.orderCompleted),
switchMap((processId) =>
this._domainCheckoutService.getBuyer({ processId }),
),
);
showBillingAddress$ = this.shoppingCartItems$.pipe(
withLatestFrom(this.customerFeatures$),
map(
([items, customerFeatures]) =>
items.some(
(item) =>
item.features?.orderType === 'Versand' ||
item.features?.orderType === 'B2B-Versand' ||
item.features?.orderType === 'DIG-Versand',
) || !!customerFeatures?.b2b,
),
);
checkNotificationChannelControl$ = new BehaviorSubject<boolean>(true);
notificationChannelLoading$ = new Subject<boolean>();
notificationsControl: UntypedFormGroup;
constructor(
private _domainCheckoutService: DomainCheckoutService,
private _application: ApplicationService,
private _uiModal: UiModalService,
) {
super({
payer: undefined,
shoppingCart: undefined,
shoppingCartItems: [],
fetching: false,
});
}
loadShoppingCart = this.effect(($) =>
$.pipe(
tap(() => (this.fetching = true)),
withLatestFrom(this._application.activatedProcessId$),
switchMap(([_, processId]) => {
return this._domainCheckoutService
.getShoppingCart({ processId, latest: true })
.pipe(
tapResponse(
(shoppingCart) => {
console.log('Loaded shopping cart', { shoppingCart });
const shoppingCartItems =
shoppingCart?.items?.map((item) => item.data) || [];
this.patchState({
shoppingCart,
shoppingCartItems,
});
},
(err) => {},
() => {},
),
);
}),
tap(() => (this.fetching = false)),
),
);
async onNotificationChange(notificationChannels?: NotificationChannel[]) {
this.notificationChannelLoading$.next(true);
try {
const control = this.notificationsControl?.getRawValue();
const notificationChannel = notificationChannels
? (notificationChannels.reduce(
(val, current) => val | current,
0,
) as NotificationChannel)
: control?.notificationChannel?.selected || 0;
const processId = await this._application.activatedProcessId$
.pipe(first())
.toPromise();
const email = control?.notificationChannel?.email;
const mobile = control?.notificationChannel?.mobile;
// Check if E-Mail and Mobilnumber is available if E-Mail or SMS checkbox is active
if (notificationChannel === 3 && (!email || !mobile)) {
this.checkNotificationChannelControl$.next(false);
} else if (notificationChannel === 2 && !mobile) {
this.checkNotificationChannelControl$.next(false);
} else if (notificationChannel === 1 && !email) {
this.checkNotificationChannelControl$.next(false);
} else {
this.checkNotificationChannelControl$.next(true);
}
// NotificationChannel nur speichern, wenn Haken und Value gesetzt
let setNotificationChannel = 0;
if ((notificationChannel & 1) === 1 && email) {
setNotificationChannel += 1;
}
if ((notificationChannel & 2) === 2 && mobile) {
setNotificationChannel += 2;
}
if (notificationChannel > 0) {
this.setCommunicationDetails({
processId,
notificationChannel,
email,
mobile,
});
}
this._domainCheckoutService.setNotificationChannels({
processId,
notificationChannels:
(setNotificationChannel as NotificationChannel) || 0,
});
} catch (error) {
this._uiModal.open({
content: UiErrorModalComponent,
data: error,
title: 'Fehler beim setzen des Benachrichtigungskanals',
});
}
this.notificationChannelLoading$.next(false);
}
setCommunicationDetails({
processId,
notificationChannel,
email,
mobile,
}: {
processId: number;
notificationChannel: number;
email: string;
mobile: string;
}) {
const emailValid = this.notificationsControl
?.get('notificationChannel')
?.get('email')?.valid;
const mobileValid = this.notificationsControl
?.get('notificationChannel')
?.get('mobile')?.valid;
if (notificationChannel === 3 && emailValid && mobileValid) {
this._domainCheckoutService.setBuyerCommunicationDetails({
processId,
email,
mobile,
});
} else if (notificationChannel === 1 && emailValid) {
this._domainCheckoutService.setBuyerCommunicationDetails({
processId,
email,
});
} else if (notificationChannel === 2 && mobileValid) {
this._domainCheckoutService.setBuyerCommunicationDetails({
processId,
mobile,
});
}
}
}

View File

@@ -21,9 +21,7 @@
(click)="$event?.preventDefault(); $event?.stopPropagation()"
>
<shared-icon icon="shopping-cart-bold" [size]="22"></shared-icon>
<span class="shopping-cart-count-label ml-2">{{
cartItemCount$ | async
}}</span>
<span class="shopping-cart-count-label ml-2">{{ cartCount() }}</span>
</button>
}
</a>

View File

@@ -63,6 +63,14 @@ export class ShellProcessBarItemComponent
return 'count' in pdata;
});
cartCount = computed(() => {
const tab = this.tab();
const pdata = tab.metadata?.process_data as { count?: number };
return pdata?.count ?? 0;
});
currentLocationUrlTree = computed(() => {
const tab = this.tab();
const current = tab.location.locations[tab.location.current];
@@ -112,7 +120,6 @@ export class ShellProcessBarItemComponent
this.initQueryParams$();
this.initIsActive$();
this.initShowCloseButton$();
this.initCartItemCount$();
}
scrollIntoView() {
@@ -171,15 +178,6 @@ export class ShellProcessBarItemComponent
}
}
initCartItemCount$() {
this.cartItemCount$ = this.process$.pipe(
switchMap((process) =>
this._checkout?.getShoppingCart({ processId: process?.id }),
),
map((cart) => cart?.items?.length ?? 0),
);
}
ngOnDestroy() {
this._process$.complete();
}

View File

@@ -1,3 +1,6 @@
export * from './lib/store';
export * from './lib/helpers';
export * from './lib/models';
export * from './lib/facades';
export * from './lib/models';
export * from './lib/schemas';
export * from './lib/store';
export * from './lib/helpers';
export * from './lib/services';

View File

@@ -0,0 +1,7 @@
export const SELECTED_BRANCH_METADATA_KEY = 'CHECKOUT_SELECTED_BRANCH_ID';
export const CHECKOUT_SHOPPING_CART_ID_METADATA_KEY =
'CHECKOUT_SHOPPING_CART_ID';
export const CHECKOUT_REWARD_SHOPPING_CART_ID_METADATA_KEY =
'CHECKOUT_REWARD_SHOPPING_CART_ID';

View File

@@ -0,0 +1,15 @@
import { Injectable, inject } from '@angular/core';
import { CheckoutMetadataService } from '../services/checkout-metadata.service';
@Injectable({ providedIn: 'root' })
export class BranchFacade {
#checkoutMetadataService = inject(CheckoutMetadataService);
getSelectedBranchId(tabId: number): number | undefined {
return this.#checkoutMetadataService.getSelectedBranchId(tabId);
}
setSelectedBranchId(tabId: number, branchId: number | undefined): void {
this.#checkoutMetadataService.setSelectedBranchId(tabId, branchId);
}
}

View File

@@ -0,0 +1,3 @@
export * from './branch.facade';
export * from './purchase-options.facade';
export * from './shopping-cart.facade';

View File

@@ -0,0 +1,36 @@
import { inject, Injectable } from '@angular/core';
import { ShoppingCartService } from '../services';
import {
AddItemToShoppingCartParams,
CanAddItemsToShoppingCartParams,
RemoveShoppingCartItemParams,
UpdateShoppingCartItemParams,
} from '../schemas';
@Injectable({ providedIn: 'root' })
export class PurchaseOptionsFacade {
#shoppingCartService = inject(ShoppingCartService);
get(shoppingCartId: number, abortSignal?: AbortSignal) {
return this.#shoppingCartService.getShoppingCart(
shoppingCartId,
abortSignal,
);
}
canAddItems(params: CanAddItemsToShoppingCartParams) {
return this.#shoppingCartService.canAddItems(params);
}
addItem(params: AddItemToShoppingCartParams) {
return this.#shoppingCartService.addItem(params);
}
updateItem(params: UpdateShoppingCartItemParams) {
return this.#shoppingCartService.updateItem(params);
}
removeItem(params: RemoveShoppingCartItemParams) {
return this.#shoppingCartService.removeItem(params);
}
}

View File

@@ -0,0 +1,14 @@
import { inject, Injectable } from '@angular/core';
import { ShoppingCartService } from '../services';
@Injectable({ providedIn: 'root' })
export class ShoppingCartFacade {
#shoppingCartService = inject(ShoppingCartService);
getShoppingCart(shoppingCartId: number, abortSignal?: AbortSignal) {
return this.#shoppingCartService.getShoppingCart(
shoppingCartId,
abortSignal,
);
}
}

View File

@@ -1,10 +1,4 @@
export const OrderType = {
Pickup: 'Abholung',
Delivery: 'Versand',
InStore: 'Rücklage',
} as const;
export type OrderType = (typeof OrderType)[keyof typeof OrderType];
import { OrderType } from '../models';
export function getOrderTypeFeature(
features: Record<string, string> = {},

View File

@@ -0,0 +1,18 @@
import { AvailabilityType as GeneratedAvailabilityType } from '@generated/swagger/checkout-api';
export type AvailabilityType = GeneratedAvailabilityType;
// Helper constants for easier usage
export const AvailabilityType = {
Unknown: 0,
InStock: 1,
OutOfStock: 2,
PreOrder: 32,
BackOrder: 256,
Discontinued: 512,
OnRequest: 1024,
SpecialOrder: 2048,
DigitalDelivery: 4096,
PartialStock: 8192,
ExpectedDelivery: 16384,
} as const;

View File

@@ -0,0 +1,3 @@
import { CampaignDTO } from '@generated/swagger/checkout-api';
export type Campaign = CampaignDTO;

View File

@@ -0,0 +1,11 @@
import { Gender as GeneratedGender } from '@generated/swagger/checkout-api';
export type Gender = GeneratedGender;
// Helper constants for easier usage
export const Gender = {
Unknown: 0,
Male: 1,
Female: 2,
Other: 4,
} as const;

View File

@@ -1,7 +1,18 @@
export * from './availability';
export * from './checkout-item';
export * from './checkout';
export * from './destination';
export * from './shipping-address';
export * from './shipping-target';
export * from './shopping-cart-item';
export * from './availability-type';
export * from './availability';
export * from './campaign';
export * from './checkout-item';
export * from './checkout';
export * from './destination';
export * from './gender';
export * from './loyalty';
export * from './ola-availability';
export * from './order-type';
export * from './price';
export * from './promotion';
export * from './shipping-address';
export * from './shipping-target';
export * from './shopping-cart-item';
export * from './shopping-cart';
export * from './update-shopping-cart-item';
export * from './vat-type';

View File

@@ -0,0 +1,3 @@
import { LoyaltyDTO } from '@generated/swagger/checkout-api';
export type Loyalty = LoyaltyDTO;

View File

@@ -0,0 +1,2 @@
export type OLAAvailability =
import('@generated/swagger/checkout-api').OLAAvailabilityDTO;

View File

@@ -0,0 +1,10 @@
export const OrderType = {
InStore: 'Rücklage',
Pickup: 'Abholung',
Delivery: 'Versand',
DigitalShipping: 'DIG-Versand',
B2BShipping: 'B2B-Versand',
Download: 'Download',
} as const;
export type OrderType = (typeof OrderType)[keyof typeof OrderType];

View File

@@ -0,0 +1,3 @@
import { PriceDTO } from '@generated/swagger/checkout-api';
export type Price = PriceDTO;

View File

@@ -0,0 +1,3 @@
import { PromotionDTO } from '@generated/swagger/checkout-api';
export type Promotion = PromotionDTO;

View File

@@ -1,8 +1,13 @@
export const ShippingTarget = {
None: 0,
Branch: 1,
Delivery: 2,
} as const;
export type ShippingTarget =
(typeof ShippingTarget)[keyof typeof ShippingTarget];
// Helper constants for easier usage
export const ShippingTarget = {
None: 0,
Branch: 1,
Delivery: 2,
PickupPoint: 4,
PostOffice: 8,
Locker: 16,
Workplace: 32,
} as const;
export type ShippingTarget =
(typeof ShippingTarget)[keyof typeof ShippingTarget];

View File

@@ -0,0 +1,3 @@
import { ShoppingCartDTO } from '@generated/swagger/checkout-api';
export type ShoppingCart = ShoppingCartDTO;

View File

@@ -0,0 +1,3 @@
import { UpdateShoppingCartItemDTO } from '@generated/swagger/checkout-api';
export type UpdateShoppingCartItem = UpdateShoppingCartItemDTO;

View File

@@ -0,0 +1,16 @@
import { VATType as GeneratedVATType } from '@generated/swagger/checkout-api';
export type VATType = GeneratedVATType;
// Helper constants for easier usage
export const VATType = {
Unknown: 0,
Standard: 1,
Reduced: 2,
Zero: 4,
Exempt: 8,
ReverseCharge: 16,
IntraCommunity: 32,
Export: 64,
Margin: 128,
} as const;

View File

@@ -0,0 +1,34 @@
import { z } from 'zod';
import {
EntityContainerSchema,
AvailabilityDTOSchema,
CampaignDTOSchema,
LoyaltyDTOSchema,
ProductDTOSchema,
PromotionDTOSchema,
PriceSchema,
EntityDTOContainerOfDestinationDTOSchema,
ItemTypeSchema,
} from './base-schemas';
const AddToShoppingCartDTOSchema = z.object({
availability: AvailabilityDTOSchema,
campaign: CampaignDTOSchema,
destination: EntityDTOContainerOfDestinationDTOSchema,
itemType: ItemTypeSchema,
loyalty: LoyaltyDTOSchema,
product: ProductDTOSchema,
promotion: PromotionDTOSchema,
quantity: z.number().int().positive(),
retailPrice: PriceSchema,
shopItemId: z.number().int().positive().optional(),
});
export const AddItemToShoppingCartParamsSchema = z.object({
shoppingCartId: z.number().int().positive(),
items: z.array(AddToShoppingCartDTOSchema).min(1),
});
export type AddItemToShoppingCartParams = z.infer<
typeof AddItemToShoppingCartParamsSchema
>;

View File

@@ -0,0 +1,229 @@
import { z } from 'zod';
import { AvailabilityType, Gender, ShippingTarget, VATType } from '../models';
import { OrderType } from '../models';
// ItemType from generated API - it's a numeric bitwise enum
export const ItemTypeSchema = z.number().optional();
// Enum schemas based on generated swagger types
export const AvailabilityTypeSchema = z.nativeEnum(AvailabilityType).optional();
export const ShippingTargetSchema = z.nativeEnum(ShippingTarget).optional();
export const VATTypeSchema = z.nativeEnum(VATType).optional();
export const GenderSchema = z.nativeEnum(Gender).optional();
export const OrderTypeSchema = z.nativeEnum(OrderType).optional();
// Base schemas for nested objects
export const DateRangeSchema = z
.object({
start: z.string().optional(),
stop: z.string().optional(),
})
.optional();
const _EntityContainerSchema = z
.object({
id: z.number().optional(),
})
.optional();
export const EntityContainerSchema = (schema: z.ZodTypeAny) =>
_EntityContainerSchema.and(
z.object({
data: schema,
}),
);
export const PriceValueSchema = z
.object({
currency: z.string().optional(),
currencySymbol: z.string().optional(),
value: z.number().optional(),
})
.optional();
export const VATValueSchema = z
.object({
inPercent: z.number().optional(),
label: z.string().optional(),
value: z.number().optional(),
vatType: VATTypeSchema,
})
.optional();
export const AddressSchema = z
.object({
street: z.string().optional(),
streetNumber: z.string().optional(),
postalCode: z.string().optional(),
city: z.string().optional(),
country: z.string().optional(),
additionalInfo: z.string().optional(),
})
.optional();
export const CommunicationDetailsSchema = z
.object({
email: z.string().optional(),
phone: z.string().optional(),
mobile: z.string().optional(),
fax: z.string().optional(),
})
.optional();
export const OrganisationSchema = z
.object({
name: z.string().optional(),
taxNumber: z.string().optional(),
})
.optional();
export const ShippingAddressSchema = z
.object({
id: z.number().optional(),
address: AddressSchema,
communicationDetails: CommunicationDetailsSchema,
firstName: z.string().optional(),
gender: GenderSchema,
lastName: z.string().optional(),
locale: z.string().optional(),
organisation: OrganisationSchema,
title: z.string().optional(),
})
.optional();
// DTO Schemas based on generated API types
export const TouchedBaseSchema = z.object({
id: z.number().optional(),
createdAt: z.string().optional(),
modifiedAt: z.string().optional(),
});
export const PriceDTOSchema = z
.object({
id: z.number().optional(),
createdAt: z.string().optional(),
modifiedAt: z.string().optional(),
value: PriceValueSchema,
vat: VATValueSchema,
})
.optional();
export const PriceSchema = z
.object({
currency: z.string().optional(),
currencySymbol: z.string().optional(),
validFrom: z.string().optional(),
value: z.number(),
vatInPercent: z.number().optional(),
vatType: VATTypeSchema,
vatValue: z.number().optional(),
})
.optional();
export const CampaignDTOSchema = z
.object({
id: z.number().optional(),
createdAt: z.string().optional(),
modifiedAt: z.string().optional(),
code: z.string().optional(),
label: z.string().optional(),
type: z.string().optional(),
value: z.number().optional(),
})
.optional();
export const PromotionDTOSchema = z
.object({
id: z.number().optional(),
createdAt: z.string().optional(),
modifiedAt: z.string().optional(),
code: z.string().optional(),
label: z.string().optional(),
type: z.string().optional(),
value: z.number().optional(),
})
.optional();
export const LoyaltyDTOSchema = z
.object({
id: z.number().optional(),
createdAt: z.string().optional(),
modifiedAt: z.string().optional(),
code: z.string().optional(),
label: z.string().optional(),
type: z.string().optional(),
value: z.number().optional(),
})
.optional();
export const ProductDTOSchema = z
.object({
id: z.number().optional(),
createdAt: z.string().optional(),
modifiedAt: z.string().optional(),
additionalName: z.string().optional(),
catalogProductNumber: z.string().optional(),
contributors: z.string().optional(),
ean: z.string().optional(),
edition: z.string().optional(),
format: z.string().optional(),
formatDetail: z.string().optional(),
locale: z.string().optional(),
manufacturer: z.string().optional(),
name: z.string().optional(),
productGroup: z.string().optional(),
productGroupDetails: z.string().optional(),
publicationDate: z.string().optional(),
serial: z.string().optional(),
supplierProductNumber: z.string().optional(),
volume: z.string().optional(),
})
.optional();
export const AvailabilityDTOSchema = z
.object({
id: z.number().optional(),
createdAt: z.string().optional(),
modifiedAt: z.string().optional(),
availabilityType: AvailabilityTypeSchema,
estimatedDelivery: DateRangeSchema,
estimatedShippingDate: z.string().optional(),
inStock: z.number().optional(),
isPrebooked: z.boolean().optional(),
lastRequest: z.string().optional(),
price: PriceDTOSchema,
requestReference: z.string().optional(),
ssc: z.string().optional(),
sscText: z.string().optional(),
supplierInfo: z.string().optional(),
supplierProductNumber: z.string().optional(),
supplierSSC: z.string().optional(),
supplierSSCText: z.string().optional(),
supplyChannel: z.string().optional(),
})
.optional();
export const DestinationDTOSchema = z
.object({
id: z.number().optional(),
createdAt: z.string().optional(),
modifiedAt: z.string().optional(),
address: AddressSchema,
communicationDetails: CommunicationDetailsSchema,
firstName: z.string().optional(),
gender: GenderSchema,
lastName: z.string().optional(),
locale: z.string().optional(),
organisation: OrganisationSchema,
title: z.string().optional(),
target: ShippingTargetSchema,
})
.optional();
export const EntityDTOContainerOfDestinationDTOSchema = z
.object({
id: z.number().optional(),
data: DestinationDTOSchema,
})
.optional();

View File

@@ -0,0 +1,58 @@
import { ItemPayload } from '@generated/swagger/checkout-api';
import { z } from 'zod';
import { OrderTypeSchema } from './base-schemas';
const CanAddPriceSchema = z.object({
value: z
.object({
value: z.number().optional(),
currency: z.string().optional(),
currencySymbol: z.string().optional(),
})
.optional(),
vat: z
.object({
inPercent: z.number().optional(),
label: z.string().optional(),
value: z.number().optional(),
vatType: z.number().optional(),
})
.optional(),
});
const CanAddOLAAvailabilitySchema = z.object({
altAt: z.string().optional(),
at: z.string().optional(),
ean: z.string().optional(),
format: z.string().optional(),
isPrebooked: z.boolean().optional(),
itemId: z.number().int().optional(),
logistician: z.string().optional(),
logisticianId: z.number().int().optional(),
preferred: z.number().int().optional(),
price: CanAddPriceSchema.optional(),
qty: z.number().int().optional(),
shop: z.number().int().optional(),
ssc: z.string().optional(),
sscText: z.string().optional(),
status: z.number().int(),
supplier: z.string().optional(),
supplierId: z.number().int().optional(),
supplierProductNumber: z.string().optional(),
});
const CanAddItemPayloadSchema = z.object({
availabilities: z.array(CanAddOLAAvailabilitySchema),
customerFeatures: z.record(z.string()),
orderType: OrderTypeSchema,
id: z.string(),
});
export const CanAddItemsToShoppingCartParamsSchema = z.object({
shoppingCartId: z.number().int().positive(),
payload: z.array(CanAddItemPayloadSchema).min(1),
});
export type CanAddItemsToShoppingCartParams = z.infer<
typeof CanAddItemsToShoppingCartParamsSchema
>;

View File

@@ -0,0 +1,5 @@
export * from './add-item-to-shopping-cart-params.schema';
export * from './base-schemas';
export * from './can-add-items-to-shopping-cart-params.schema';
export * from './remove-shopping-cart-item-params.schema';
export * from './update-shopping-cart-item-params.schema';

View File

@@ -0,0 +1,10 @@
import { z } from 'zod';
export const RemoveShoppingCartItemParamsSchema = z.object({
shoppingCartId: z.number().int().positive(),
shoppingCartItemId: z.number().int().positive(),
});
export type RemoveShoppingCartItemParams = z.infer<
typeof RemoveShoppingCartItemParamsSchema
>;

View File

@@ -0,0 +1,35 @@
import { z } from 'zod';
import {
EntityContainerSchema,
AvailabilityDTOSchema,
CampaignDTOSchema,
LoyaltyDTOSchema,
PromotionDTOSchema,
PriceSchema,
EntityDTOContainerOfDestinationDTOSchema,
} from './base-schemas';
import { UpdateShoppingCartItem } from '../models';
const UpdateShoppingCartItemParamsValueSchema = z.object({
availability: AvailabilityDTOSchema,
buyerComment: z.string().optional(),
campaign: CampaignDTOSchema,
destination: EntityDTOContainerOfDestinationDTOSchema,
loyalty: LoyaltyDTOSchema,
promotion: PromotionDTOSchema,
quantity: z.number().int().positive().optional(),
retailPrice: PriceSchema,
specialComment: z.string().optional(),
});
export const UpdateShoppingCartItemParamsSchema = z.object({
shoppingCartId: z.number().int().positive(),
shoppingCartItemId: z.number().int().positive(),
values: UpdateShoppingCartItemParamsValueSchema,
});
export type UpdateShoppingCartItemParams = {
shoppingCartId: number;
shoppingCartItemId: number;
values: UpdateShoppingCartItem;
};

View File

@@ -0,0 +1,30 @@
import { inject, Injectable } from '@angular/core';
import { StoreCheckoutBranchService } from '@generated/swagger/checkout-api';
import { ResponseArgsError, takeUntilAborted } from '@isa/common/data-access';
import { logger } from '@isa/core/logging';
import { firstValueFrom } from 'rxjs';
import { Cache, CacheTimeToLive, InFlight } from '@isa/common/decorators';
@Injectable({ providedIn: 'root' })
export class BranchService {
#logger = logger(() => ({ service: 'BranchService' }));
#branchService = inject(StoreCheckoutBranchService);
@Cache({ ttl: CacheTimeToLive.fiveMinutes })
@InFlight()
async fetchBranches(abortSignal?: AbortSignal) {
let req$ = this.#branchService.StoreCheckoutBranchGetBranches({});
if (abortSignal) {
req$ = req$.pipe(takeUntilAborted(abortSignal));
}
const res = await firstValueFrom(req$);
if (res.error) {
const error = new ResponseArgsError(res);
this.#logger.error('Failed to fetch branches', error);
throw error;
}
}
}

View File

@@ -0,0 +1,58 @@
import { Injectable, inject } from '@angular/core';
import { TabService, getMetadataHelper } from '@isa/core/tabs';
import {
CHECKOUT_REWARD_SHOPPING_CART_ID_METADATA_KEY,
CHECKOUT_SHOPPING_CART_ID_METADATA_KEY,
SELECTED_BRANCH_METADATA_KEY,
} from '../constants';
import z from 'zod';
@Injectable({ providedIn: 'root' })
export class CheckoutMetadataService {
#tabService = inject(TabService);
setSelectedBranchId(tabId: number, branchId: number | undefined) {
this.#tabService.patchTabMetadata(tabId, {
[SELECTED_BRANCH_METADATA_KEY]: branchId,
});
}
getSelectedBranchId(tabId: number): number | undefined {
return getMetadataHelper(
tabId,
SELECTED_BRANCH_METADATA_KEY,
z.number().optional(),
this.#tabService.entities(),
);
}
setShoppingCartId(tabId: number, shoppingCartId: number | undefined) {
this.#tabService.patchTabMetadata(tabId, {
CHECKOUT_SHOPPING_CART_ID_METADATA_KEY: shoppingCartId,
});
}
getShoppingCartId(tabId: number): number | undefined {
return getMetadataHelper(
tabId,
CHECKOUT_SHOPPING_CART_ID_METADATA_KEY,
z.number().optional(),
this.#tabService.entities(),
);
}
setRewardShoppingCartId(tabId: number, shoppingCartId: number | undefined) {
this.#tabService.patchTabMetadata(tabId, {
CHECKOUT_REWARD_SHOPPING_CART_ID_METADATA_KEY: shoppingCartId,
});
}
getRewardShoppingCartId(tabId: number): number | undefined {
return getMetadataHelper(
tabId,
CHECKOUT_REWARD_SHOPPING_CART_ID_METADATA_KEY,
z.number().optional(),
this.#tabService.entities(),
);
}
}

View File

@@ -0,0 +1,3 @@
export * from './branch.service';
export * from './checkout-metadata.service';
export * from './shopping-cart.service';

View File

@@ -0,0 +1,163 @@
import { inject, Injectable } from '@angular/core';
import {
ItemsResult,
StoreCheckoutShoppingCartService,
ItemPayload,
AddToShoppingCartDTO,
UpdateShoppingCartItemDTO,
} from '@generated/swagger/checkout-api';
import {
AddItemToShoppingCartParams,
AddItemToShoppingCartParamsSchema,
CanAddItemsToShoppingCartParams,
CanAddItemsToShoppingCartParamsSchema,
RemoveShoppingCartItemParams,
RemoveShoppingCartItemParamsSchema,
UpdateShoppingCartItemParams,
UpdateShoppingCartItemParamsSchema,
} from '../schemas';
import { ShoppingCart } from '../models';
import { ResponseArgsError, takeUntilAborted } from '@isa/common/data-access';
import { firstValueFrom } from 'rxjs';
import { logger } from '@isa/core/logging';
@Injectable({ providedIn: 'root' })
export class ShoppingCartService {
#logger = logger(() => ({ service: 'ShoppingCartService' }));
#storeCheckoutShoppingCartService = inject(StoreCheckoutShoppingCartService);
async createShoppingCart(): Promise<ShoppingCart> {
const req$ =
this.#storeCheckoutShoppingCartService.StoreCheckoutShoppingCartCreateShoppingCart();
const res = await firstValueFrom(req$);
if (res.error) {
const err = new ResponseArgsError(res);
this.#logger.error('Failed to create shopping cart', err);
throw err;
}
return res.result as ShoppingCart;
}
async getShoppingCart(
shoppingCartId: number,
abortSignal?: AbortSignal,
): Promise<ShoppingCart | undefined> {
let req$ =
this.#storeCheckoutShoppingCartService.StoreCheckoutShoppingCartGetShoppingCart(
{
shoppingCartId,
},
);
if (abortSignal) {
req$ = req$.pipe(takeUntilAborted(abortSignal));
}
const res = await firstValueFrom(req$);
if (res.error) {
const err = new ResponseArgsError(res);
this.#logger.error('Failed to fetch shopping cart', err);
throw err;
}
return res.result;
}
async canAddItems(
params: CanAddItemsToShoppingCartParams,
): Promise<ItemsResult[]> {
const parsed = CanAddItemsToShoppingCartParamsSchema.parse(params);
const req$ =
this.#storeCheckoutShoppingCartService.StoreCheckoutShoppingCartCanAddItems(
{
shoppingCartId: parsed.shoppingCartId,
payload: parsed.payload as ItemPayload[],
},
);
const res = await firstValueFrom(req$);
if (res.error) {
const err = new ResponseArgsError(res);
this.#logger.error(
'Failed to check if items can be added to shopping cart',
err,
);
throw err;
}
return res.result as unknown as ItemsResult[];
}
async addItem(params: AddItemToShoppingCartParams): Promise<ShoppingCart> {
const parsed = AddItemToShoppingCartParamsSchema.parse(params);
const req$ =
this.#storeCheckoutShoppingCartService.StoreCheckoutShoppingCartAddItemToShoppingCart(
{
shoppingCartId: parsed.shoppingCartId,
items: parsed.items as AddToShoppingCartDTO[],
},
);
const res = await firstValueFrom(req$);
if (res.error) {
const err = new ResponseArgsError(res);
this.#logger.error('Failed to add item to shopping cart', err);
throw err;
}
return res.result as ShoppingCart;
}
async updateItem(
params: UpdateShoppingCartItemParams,
): Promise<ShoppingCart> {
const parsed = UpdateShoppingCartItemParamsSchema.parse(params);
const req$ =
this.#storeCheckoutShoppingCartService.StoreCheckoutShoppingCartUpdateShoppingCartItem(
{
shoppingCartId: parsed.shoppingCartId,
shoppingCartItemId: parsed.shoppingCartItemId,
values: parsed.values as UpdateShoppingCartItemDTO,
},
);
const res = await firstValueFrom(req$);
if (res.error) {
const err = new ResponseArgsError(res);
this.#logger.error('Failed to update shopping cart item', err);
throw err;
}
return res.result as ShoppingCart;
}
async removeItem(
params: RemoveShoppingCartItemParams,
): Promise<ShoppingCart> {
const parsed = RemoveShoppingCartItemParamsSchema.parse(params);
const req$ =
this.#storeCheckoutShoppingCartService.StoreCheckoutShoppingCartDeleteShoppingCartItemAvailability(
{
shoppingCartId: parsed.shoppingCartId,
shoppingCartItemId: parsed.shoppingCartItemId,
},
);
const res = await firstValueFrom(req$);
if (res.error) {
const err = new ResponseArgsError(res);
this.#logger.error('Failed to remove item from shopping cart', err);
throw err;
}
return res.result as ShoppingCart;
}
}

View File

@@ -84,10 +84,14 @@ export function Cache<T extends (...args: any[]) => any>(
throw new Error('Cache map not initialized properly');
}
const argsWithoutAbortSignal = args.filter(
(arg) => !(arg instanceof AbortSignal),
) as Parameters<T>;
// Generate cache key
const key = options.keyGenerator
? options.keyGenerator(...args)
: JSON.stringify(args);
? options.keyGenerator(...argsWithoutAbortSignal)
: JSON.stringify(argsWithoutAbortSignal);
// Check cache first
const cached = instanceCache.get(key);

View File

@@ -85,9 +85,13 @@ export function InFlight<T extends (...args: any[]) => Promise<any>>(
throw new Error('In-flight map not initialized properly');
}
const argsWithoutAbortSignal = args.filter(
(arg) => !(arg instanceof AbortSignal),
) as Parameters<T>;
const key = options.keyGenerator
? options.keyGenerator(...args)
: JSON.stringify(args);
? options.keyGenerator(...argsWithoutAbortSignal)
: JSON.stringify(argsWithoutAbortSignal);
const existingRequest = instanceMap.get(key);
if (existingRequest) {

View File

@@ -0,0 +1,17 @@
import { inject, Injectable } from '@angular/core';
import { CrmSearchService } from '../services/crm-search.service';
import { FetchCustomerInput } from '../schemas';
import { Customer } from '../models';
@Injectable({ providedIn: 'root' })
export class CustomerFacade {
#customerService = inject(CrmSearchService);
async fetchCustomer(
params: FetchCustomerInput,
abortSignal?: AbortSignal,
): Promise<Customer | undefined> {
const res = await this.#customerService.fetchCustomer(params, abortSignal);
return res.result;
}
}

View File

@@ -1,2 +1,3 @@
export * from './selected-customer-id.facade';
export * from './customer-cards.facade';
export * from './customer-cards.facade';
export * from './customer.facade';
export * from './selected-customer-id.facade';