Files
ISA-Frontend/libs/checkout/data-access/src/lib/adapters/availability.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

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'
);
}
}