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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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