Files
ISA-Frontend/libs/availability/data-access/src/lib/adapters/get-availability-params.adapter.ts
Lorenz Hilpert bfd151dd84 Merged PR 1989: fix(checkout): resolve currency constraint violations in price handling
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
2025-10-28 10:34:39 +00:00

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,
};
}
}