mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
fix(checkout): resolve currency constraint violations in price handling - Add ensureCurrencyDefaults() helper to normalize price objects with EUR defaults - Fix currency constraint violation in shopping cart item additions (bug #5405) - Apply price normalization across availability, checkout, and shopping cart services - Update 8 locations: availability.adapter, checkout.service, shopping-cart.service, get-availability-params.adapter, availability-transformers, reward quantity control - Refactor OrderType to @isa/common/data-access for cross-domain reusability - Remove duplicate availability service from catalogue library - Enhance PriceValue and VatValue schemas with proper currency defaults - Add availability-transformers.spec.ts test coverage - Fix QuantityControl fallback from 0 to 1 to prevent invalid state warnings Resolves issue where POST requests to /checkout/v6/store/shoppingcart/{id}/item were sending price objects without required currency/currencySymbol fields, causing 400 Bad Request with 'Currency: Constraint violation: NotNull' error. Related work items: #5405
239 lines
6.9 KiB
TypeScript
239 lines
6.9 KiB
TypeScript
import {
|
|
getOrderTypeFeature,
|
|
OrderType,
|
|
ShoppingCartItem,
|
|
} from '@isa/checkout/data-access';
|
|
import {
|
|
GetAvailabilityInputParams,
|
|
GetSingleItemAvailabilityInputParams,
|
|
} from '../schemas';
|
|
|
|
// TODO: [Adapter Refactoring - Medium Priority] Replace switch with builder pattern
|
|
// Current: 67-line switch statement, 90% duplication (Complexity: 6/10)
|
|
// Target: Fluent builder API with type safety
|
|
//
|
|
// Proposed approach:
|
|
// 1. Create AvailabilityParamsBuilder class:
|
|
// - withItem(catalogProductNumber, ean, quantity, price) // Fix: type-safe price (not any)
|
|
// - withOrderType(orderType)
|
|
// - withShopId(shopId)
|
|
// - build(): GetAvailabilityInputParams | undefined
|
|
//
|
|
// 2. Encapsulate business rules in builder:
|
|
// - requiresShopId check (InStore, Pickup)
|
|
// - Download special case (no quantity)
|
|
// - Validation logic
|
|
//
|
|
// 3. Simplify adapter to:
|
|
// return new AvailabilityParamsBuilder()
|
|
// .withItem(catalogProductNumber, ean, quantity, price)
|
|
// .withOrderType(orderType)
|
|
// .withShopId(targetBranch)
|
|
// .build();
|
|
//
|
|
// Benefits:
|
|
// - Eliminates switch statement duplication
|
|
// - Fixes 'any' type on line 158 (type-safe price parameter)
|
|
// - Fluent API makes intent clear
|
|
// - Easy to add new order types
|
|
// - Encapsulates validation rules
|
|
//
|
|
// Effort: ~3 hours | Impact: Medium | Risk: Low
|
|
// See: complexity-analysis.md (Code Review Section 3, Option 1)
|
|
export class GetAvailabilityParamsAdapter {
|
|
static fromShoppingCartItem(
|
|
item: ShoppingCartItem,
|
|
orderType = getOrderTypeFeature(item.features),
|
|
): GetAvailabilityInputParams | undefined {
|
|
const itemData = this.extractItemData(item);
|
|
if (!itemData) {
|
|
return undefined;
|
|
}
|
|
|
|
const { catalogProductNumber, quantity, targetBranch, ean } = itemData;
|
|
const price = this.preparePriceData(item);
|
|
const baseItems = [
|
|
this.createBaseItem(catalogProductNumber, ean, quantity, price),
|
|
];
|
|
|
|
switch (orderType) {
|
|
case OrderType.InStore:
|
|
if (!targetBranch) {
|
|
return undefined;
|
|
}
|
|
return {
|
|
orderType: OrderType.InStore,
|
|
branchId: targetBranch,
|
|
itemsIds: baseItems.map((item) => item.itemId), // Note: itemsIds is array of numbers
|
|
};
|
|
|
|
case OrderType.Pickup:
|
|
if (!targetBranch) {
|
|
return undefined;
|
|
}
|
|
return {
|
|
orderType: OrderType.Pickup,
|
|
branchId: targetBranch,
|
|
items: baseItems,
|
|
};
|
|
|
|
case OrderType.Delivery:
|
|
return {
|
|
orderType: OrderType.Delivery,
|
|
items: baseItems,
|
|
};
|
|
|
|
case OrderType.DigitalShipping:
|
|
return {
|
|
orderType: OrderType.DigitalShipping,
|
|
items: baseItems,
|
|
};
|
|
|
|
case OrderType.B2BShipping:
|
|
return {
|
|
orderType: OrderType.B2BShipping,
|
|
items: baseItems,
|
|
};
|
|
|
|
case OrderType.Download:
|
|
return {
|
|
orderType: OrderType.Download,
|
|
items: baseItems.map((item) => ({
|
|
itemId: item.itemId,
|
|
ean: item.ean,
|
|
price: item.price,
|
|
// Download doesn't need quantity
|
|
})),
|
|
};
|
|
|
|
default:
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Converts a ShoppingCartItem to single-item availability parameters.
|
|
* Returns params for the convenience method that checks only one item.
|
|
*
|
|
* @param item Shopping cart item to convert
|
|
* @returns Single-item availability params or undefined if data is invalid
|
|
*/
|
|
static fromShoppingCartItemToSingle(
|
|
item: ShoppingCartItem,
|
|
orderType = getOrderTypeFeature(item.features),
|
|
): GetSingleItemAvailabilityInputParams | undefined {
|
|
// Extract common data
|
|
const itemData = this.extractItemData(item);
|
|
if (!itemData) {
|
|
return undefined;
|
|
}
|
|
|
|
const { catalogProductNumber, quantity, targetBranch, ean } = itemData;
|
|
const price = this.preparePriceData(item);
|
|
|
|
// Create the item object
|
|
const itemObj = this.createBaseItem(
|
|
catalogProductNumber,
|
|
ean,
|
|
quantity,
|
|
price,
|
|
);
|
|
|
|
// Build single-item params based on order type
|
|
switch (orderType) {
|
|
case OrderType.InStore:
|
|
if (!targetBranch) {
|
|
return undefined;
|
|
}
|
|
return {
|
|
orderType,
|
|
branchId: targetBranch,
|
|
itemId: itemObj.itemId,
|
|
};
|
|
case OrderType.Pickup:
|
|
if (!targetBranch) {
|
|
return undefined;
|
|
}
|
|
return {
|
|
orderType,
|
|
branchId: targetBranch,
|
|
item: itemObj,
|
|
};
|
|
|
|
case OrderType.Delivery:
|
|
case OrderType.DigitalShipping:
|
|
case OrderType.B2BShipping:
|
|
case OrderType.Download:
|
|
return {
|
|
orderType,
|
|
item: itemObj,
|
|
};
|
|
|
|
default:
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extracts and validates required data from a ShoppingCartItem.
|
|
* @returns Extracted data or undefined if validation fails
|
|
*/
|
|
private static extractItemData(item: ShoppingCartItem) {
|
|
const catalogProductNumber = item.product.catalogProductNumber;
|
|
const quantity = item.quantity;
|
|
const targetBranch = item.destination?.data?.targetBranch?.id;
|
|
const ean = item.product.ean;
|
|
|
|
if (!catalogProductNumber || !ean || !quantity) {
|
|
return undefined;
|
|
}
|
|
|
|
return { catalogProductNumber, quantity, targetBranch, ean };
|
|
}
|
|
|
|
/**
|
|
* Prepares price data from a ShoppingCartItem to match PriceSchema structure.
|
|
* @returns Formatted price object or undefined
|
|
*/
|
|
private static preparePriceData(item: ShoppingCartItem) {
|
|
return item.availability?.price
|
|
? {
|
|
value: item.availability.price.value ?? {
|
|
value: undefined,
|
|
currency: 'EUR',
|
|
currencySymbol: '€',
|
|
},
|
|
vat: item.availability.price.vat ?? {
|
|
value: undefined,
|
|
label: undefined,
|
|
inPercent: undefined,
|
|
vatType: undefined,
|
|
},
|
|
}
|
|
: undefined;
|
|
}
|
|
|
|
/**
|
|
* Creates a base item object for availability requests.
|
|
*
|
|
* TODO: [Next Sprint] Replace `any` type with proper typing
|
|
* - Change parameter type from `price: any` to `price: Price | undefined`
|
|
* - Import: import { Price } from '@isa/common/data-access';
|
|
* - Ensures compile-time type safety for price transformations
|
|
* - Prevents potential runtime errors from invalid price structures
|
|
*/
|
|
private static createBaseItem(
|
|
catalogProductNumber: string | number,
|
|
ean: string,
|
|
quantity: number,
|
|
price: any, // TODO: Replace with `Price | undefined`
|
|
) {
|
|
return {
|
|
itemId: Number(catalogProductNumber),
|
|
ean,
|
|
quantity,
|
|
price,
|
|
};
|
|
}
|
|
}
|