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
@@ -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