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
244 lines
8.0 KiB
TypeScript
244 lines
8.0 KiB
TypeScript
import { PriceDTO, Price } from '@generated/swagger/checkout-api';
|
|
import { Availability as AvaAvailability } from '@isa/availability/data-access';
|
|
import { ensureCurrencyDefaults } from '@isa/common/data-access';
|
|
import { Availability, AvailabilityType } from '../schemas';
|
|
|
|
/**
|
|
* Availability data from catalogue-api (raw response)
|
|
*/
|
|
export interface CatalogueAvailabilityResponse {
|
|
availabilityType: AvailabilityType;
|
|
ssc: number;
|
|
sscText: string;
|
|
supplier: { id: number };
|
|
isPrebooked: boolean;
|
|
estimatedShippingDate: string;
|
|
price: number;
|
|
lastRequest: string;
|
|
// Optional fields for shipping
|
|
estimatedDelivery?: { start?: string; stop?: string };
|
|
logistician?: { id: number };
|
|
supplierProductNumber?: string;
|
|
supplierInfo?: string;
|
|
priceMaintained?: boolean;
|
|
inStock?: number;
|
|
orderDeadline?: string;
|
|
}
|
|
|
|
/**
|
|
* Adapter for converting catalogue-api availability responses to checkout-api format.
|
|
*
|
|
* Handles:
|
|
* - Structure mapping between different API representations
|
|
* - Price object creation with VAT
|
|
* - Supplier and logistician reference wrapping
|
|
*/
|
|
export class AvailabilityAdapter {
|
|
private static readonly ADAPTER_NAME = 'AvailabilityAdapter';
|
|
|
|
/**
|
|
* Converts catalogue-api availability to checkout-api AvailabilityDTO.
|
|
*
|
|
* @param catalogueAvailability - Raw availability from catalogue service
|
|
* @param originalPrice - Original price to preserve (if different from new price)
|
|
* @returns AvailabilityDTO compatible with checkout-api
|
|
*/
|
|
static toCheckoutFormat(
|
|
catalogueAvailability: CatalogueAvailabilityResponse,
|
|
originalPrice?: number,
|
|
): Availability {
|
|
const availability: Availability = {
|
|
availabilityType: catalogueAvailability.availabilityType,
|
|
ssc: catalogueAvailability.ssc?.toString(),
|
|
sscText: catalogueAvailability.sscText,
|
|
supplier: {
|
|
id: catalogueAvailability.supplier.id,
|
|
data: {
|
|
id: catalogueAvailability.supplier.id,
|
|
},
|
|
// Explicitly omit externalReference to avoid TypeScript errors
|
|
// (generated DTOs require externalStatus when externalReference is present)
|
|
},
|
|
isPrebooked: catalogueAvailability.isPrebooked,
|
|
estimatedShippingDate: catalogueAvailability.estimatedShippingDate,
|
|
lastRequest: catalogueAvailability.lastRequest,
|
|
// Use original price if provided, otherwise use new price
|
|
price: {
|
|
value: {
|
|
value: originalPrice ?? catalogueAvailability.price,
|
|
currency: 'EUR',
|
|
currencySymbol: '€',
|
|
},
|
|
},
|
|
};
|
|
|
|
// Add optional fields if present
|
|
if (catalogueAvailability.estimatedDelivery) {
|
|
availability.estimatedDelivery = catalogueAvailability.estimatedDelivery;
|
|
}
|
|
|
|
if (catalogueAvailability.logistician) {
|
|
availability.logistician = {
|
|
id: catalogueAvailability.logistician.id,
|
|
data: {
|
|
id: catalogueAvailability.logistician.id,
|
|
},
|
|
// Explicitly omit externalReference to avoid TypeScript errors
|
|
// (generated DTOs require externalStatus when externalReference is present)
|
|
};
|
|
}
|
|
|
|
if (catalogueAvailability.supplierProductNumber) {
|
|
availability.supplierProductNumber =
|
|
catalogueAvailability.supplierProductNumber;
|
|
}
|
|
|
|
if (catalogueAvailability.supplierInfo) {
|
|
availability.supplierInfo = catalogueAvailability.supplierInfo;
|
|
}
|
|
|
|
if (catalogueAvailability.inStock !== undefined) {
|
|
availability.inStock = catalogueAvailability.inStock;
|
|
}
|
|
|
|
return availability;
|
|
}
|
|
|
|
/**
|
|
* Converts availability-api Availability to checkout-api AvailabilityDTO.
|
|
*
|
|
* Handles mapping between different API representations:
|
|
* - status → availabilityType
|
|
* - qty → inStock (preserves quantity information)
|
|
* - Simple IDs → Entity containers for logistician/supplier
|
|
* - Preserves common fields (price, ssc, dates, etc.)
|
|
*
|
|
* @param availability - Availability from availability-api service
|
|
* @returns AvailabilityDTO compatible with checkout-api
|
|
*/
|
|
static fromAvailabilityApi(availability: AvaAvailability): Availability {
|
|
const checkoutAvailability: Availability = {
|
|
availabilityType: availability.status,
|
|
ssc: availability.ssc,
|
|
sscText: availability.sscText,
|
|
isPrebooked: availability.isPrebooked,
|
|
price: ensureCurrencyDefaults(availability.price),
|
|
estimatedShippingDate: availability.at,
|
|
lastRequest: availability.requested,
|
|
};
|
|
|
|
// Map qty to inStock (preserve quantity information)
|
|
if (availability.qty !== undefined) {
|
|
checkoutAvailability.inStock = availability.qty;
|
|
}
|
|
|
|
// Convert logistician ID to entity container
|
|
if (availability.logisticianId) {
|
|
checkoutAvailability.logistician = {
|
|
id: availability.logisticianId,
|
|
data: {
|
|
id: availability.logisticianId,
|
|
},
|
|
// Explicitly omit externalReference to avoid TypeScript errors
|
|
// (generated DTOs require externalStatus when externalReference is present)
|
|
};
|
|
}
|
|
|
|
// Convert supplier ID to entity container
|
|
if (availability.supplierId) {
|
|
checkoutAvailability.supplier = {
|
|
id: availability.supplierId,
|
|
data: {
|
|
id: availability.supplierId,
|
|
},
|
|
// Explicitly omit externalReference to avoid TypeScript errors
|
|
// (generated DTOs require externalStatus when externalReference is present)
|
|
};
|
|
}
|
|
|
|
// Map supplier string to supplierInfo (alternative to supplierId)
|
|
if (availability.supplier) {
|
|
checkoutAvailability.supplierInfo = availability.supplier;
|
|
}
|
|
|
|
// Optional fields
|
|
if (availability.estimatedDelivery) {
|
|
checkoutAvailability.estimatedDelivery = availability.estimatedDelivery;
|
|
}
|
|
|
|
if (availability.supplierProductNumber) {
|
|
checkoutAvailability.supplierProductNumber =
|
|
availability.supplierProductNumber;
|
|
}
|
|
|
|
if (availability.requestReference) {
|
|
checkoutAvailability.requestReference = availability.requestReference;
|
|
}
|
|
|
|
return checkoutAvailability;
|
|
}
|
|
|
|
/**
|
|
* Converts PriceDTO to Price format for shopping cart operations.
|
|
*
|
|
* PriceDTO format (nested):
|
|
* - value: { value?: number, currency?: string, ... }
|
|
* - vat: { value?: number, inPercent?: number, vatType?: VATType, ... }
|
|
*
|
|
* Price format (flat):
|
|
* - value: number (required)
|
|
* - vatInPercent?: number
|
|
* - vatType: VATType (required)
|
|
* - vatValue?: number
|
|
* - currency?: string
|
|
*
|
|
* @param priceDTO - PriceDTO from shopping cart item
|
|
* @returns Price in flat format, or undefined if input is invalid
|
|
*/
|
|
static convertPriceDTOToPrice(priceDTO?: PriceDTO): Price | undefined {
|
|
if (!priceDTO) {
|
|
return undefined;
|
|
}
|
|
|
|
const value = priceDTO.value?.value;
|
|
const vatType = priceDTO.vat?.vatType;
|
|
|
|
// Both value and vatType are required for Price
|
|
if (value === undefined || value === null || !vatType) {
|
|
return undefined;
|
|
}
|
|
|
|
return {
|
|
value,
|
|
vatType,
|
|
vatInPercent: priceDTO.vat?.inPercent,
|
|
vatValue: priceDTO.vat?.value,
|
|
currency: priceDTO.value?.currency,
|
|
currencySymbol: priceDTO.value?.currencySymbol,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Type guard for catalogue availability response
|
|
*/
|
|
static isValidCatalogueResponse(
|
|
value: unknown,
|
|
): value is CatalogueAvailabilityResponse {
|
|
if (typeof value !== 'object' || value === null) return false;
|
|
|
|
return (
|
|
'availabilityType' in value &&
|
|
typeof value.availabilityType === 'number' &&
|
|
'ssc' in value &&
|
|
typeof value.ssc === 'number' &&
|
|
'sscText' in value &&
|
|
typeof value.sscText === 'string' &&
|
|
'supplier' in value &&
|
|
typeof value.supplier === 'object' &&
|
|
value.supplier !== null &&
|
|
'id' in value.supplier &&
|
|
typeof value.supplier.id === 'number'
|
|
);
|
|
}
|
|
}
|