mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
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:
committed by
Nino Righi
parent
334436c737
commit
100cbb5020
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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) ?? {};
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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})?$/;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
7
libs/checkout/data-access/src/lib/constants.ts
Normal file
7
libs/checkout/data-access/src/lib/constants.ts
Normal 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';
|
||||
15
libs/checkout/data-access/src/lib/facades/branch.facade.ts
Normal file
15
libs/checkout/data-access/src/lib/facades/branch.facade.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
3
libs/checkout/data-access/src/lib/facades/index.ts
Normal file
3
libs/checkout/data-access/src/lib/facades/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './branch.facade';
|
||||
export * from './purchase-options.facade';
|
||||
export * from './shopping-cart.facade';
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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> = {},
|
||||
|
||||
@@ -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;
|
||||
3
libs/checkout/data-access/src/lib/models/campaign.ts
Normal file
3
libs/checkout/data-access/src/lib/models/campaign.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { CampaignDTO } from '@generated/swagger/checkout-api';
|
||||
|
||||
export type Campaign = CampaignDTO;
|
||||
11
libs/checkout/data-access/src/lib/models/gender.ts
Normal file
11
libs/checkout/data-access/src/lib/models/gender.ts
Normal 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;
|
||||
@@ -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';
|
||||
|
||||
3
libs/checkout/data-access/src/lib/models/loyalty.ts
Normal file
3
libs/checkout/data-access/src/lib/models/loyalty.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { LoyaltyDTO } from '@generated/swagger/checkout-api';
|
||||
|
||||
export type Loyalty = LoyaltyDTO;
|
||||
@@ -0,0 +1,2 @@
|
||||
export type OLAAvailability =
|
||||
import('@generated/swagger/checkout-api').OLAAvailabilityDTO;
|
||||
10
libs/checkout/data-access/src/lib/models/order-type.ts
Normal file
10
libs/checkout/data-access/src/lib/models/order-type.ts
Normal 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];
|
||||
3
libs/checkout/data-access/src/lib/models/price.ts
Normal file
3
libs/checkout/data-access/src/lib/models/price.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { PriceDTO } from '@generated/swagger/checkout-api';
|
||||
|
||||
export type Price = PriceDTO;
|
||||
3
libs/checkout/data-access/src/lib/models/promotion.ts
Normal file
3
libs/checkout/data-access/src/lib/models/promotion.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { PromotionDTO } from '@generated/swagger/checkout-api';
|
||||
|
||||
export type Promotion = PromotionDTO;
|
||||
@@ -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];
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { ShoppingCartDTO } from '@generated/swagger/checkout-api';
|
||||
|
||||
export type ShoppingCart = ShoppingCartDTO;
|
||||
@@ -0,0 +1,3 @@
|
||||
import { UpdateShoppingCartItemDTO } from '@generated/swagger/checkout-api';
|
||||
|
||||
export type UpdateShoppingCartItem = UpdateShoppingCartItemDTO;
|
||||
16
libs/checkout/data-access/src/lib/models/vat-type.ts
Normal file
16
libs/checkout/data-access/src/lib/models/vat-type.ts
Normal 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;
|
||||
@@ -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
|
||||
>;
|
||||
229
libs/checkout/data-access/src/lib/schemas/base-schemas.ts
Normal file
229
libs/checkout/data-access/src/lib/schemas/base-schemas.ts
Normal 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();
|
||||
@@ -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
|
||||
>;
|
||||
5
libs/checkout/data-access/src/lib/schemas/index.ts
Normal file
5
libs/checkout/data-access/src/lib/schemas/index.ts
Normal 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';
|
||||
@@ -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
|
||||
>;
|
||||
@@ -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;
|
||||
};
|
||||
30
libs/checkout/data-access/src/lib/services/branch.service.ts
Normal file
30
libs/checkout/data-access/src/lib/services/branch.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
3
libs/checkout/data-access/src/lib/services/index.ts
Normal file
3
libs/checkout/data-access/src/lib/services/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './branch.service';
|
||||
export * from './checkout-metadata.service';
|
||||
export * from './shopping-cart.service';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
17
libs/crm/data-access/src/lib/facades/customer.facade.ts
Normal file
17
libs/crm/data-access/src/lib/facades/customer.facade.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user