Enhance catalog caching and pricing utilities
- Introduced a new interface, LegacyCatalogCachePayload, to improve cache handling in CatalogCacheService, allowing for better normalization of cached values. - Updated the getDisplayPrice function to utilize a centralized price formatting utility, getCatalogProductPriceDisplay, for consistent price rendering across the application. - Refactored order preparation logic in useCheckout to leverage a new domain helper, prepareOrderFromCart, streamlining SKU extraction and payload formatting. - Added CatalogPriceInfo interface to standardize pricing display information across the frontend and backend.
This commit is contained in:
parent
833ff24645
commit
c497eae763
@ -17,6 +17,12 @@ export interface CatalogCacheOptions<T> {
|
||||
) => CacheDependencies | Promise<CacheDependencies | undefined> | undefined;
|
||||
}
|
||||
|
||||
interface LegacyCatalogCachePayload<T> {
|
||||
value: T;
|
||||
__catalogCache?: boolean;
|
||||
dependencies?: CacheDependencies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Catalog cache service
|
||||
*
|
||||
@ -188,13 +194,12 @@ export class CatalogCacheService {
|
||||
const allowNull = options?.allowNull ?? false;
|
||||
|
||||
// Check Redis cache first
|
||||
const cached = await this.cache.get<T>(key);
|
||||
const cached = await this.cache.get<T | LegacyCatalogCachePayload<T>>(key);
|
||||
|
||||
if (cached !== null) {
|
||||
if (allowNull || cached !== null) {
|
||||
this.metrics[bucket].hits++;
|
||||
return cached;
|
||||
}
|
||||
const normalized = await this.normalizeCachedValue(key, cached, ttlSeconds);
|
||||
this.metrics[bucket].hits++;
|
||||
return normalized;
|
||||
}
|
||||
|
||||
// Check for in-flight request (prevents thundering herd)
|
||||
@ -243,6 +248,43 @@ export class CatalogCacheService {
|
||||
return fetchPromise;
|
||||
}
|
||||
|
||||
private async normalizeCachedValue<T>(
|
||||
key: string,
|
||||
cached: T | LegacyCatalogCachePayload<T>,
|
||||
ttlSeconds: number | null
|
||||
): Promise<T> {
|
||||
if (this.isLegacyCatalogCachePayload<T>(cached)) {
|
||||
const ttlArg = ttlSeconds === null ? undefined : ttlSeconds;
|
||||
const normalizedValue = cached.value;
|
||||
|
||||
if (ttlArg === undefined) {
|
||||
await this.cache.set(key, normalizedValue);
|
||||
} else {
|
||||
await this.cache.set(key, normalizedValue, ttlArg);
|
||||
}
|
||||
|
||||
if (cached.dependencies) {
|
||||
await this.storeDependencies(key, cached.dependencies);
|
||||
await this.linkDependencies(key, cached.dependencies);
|
||||
}
|
||||
|
||||
return normalizedValue;
|
||||
}
|
||||
|
||||
return cached;
|
||||
}
|
||||
|
||||
private isLegacyCatalogCachePayload<T>(
|
||||
payload: unknown
|
||||
): payload is LegacyCatalogCachePayload<T> {
|
||||
if (!payload || typeof payload !== "object") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const record = payload as Record<string, unknown>;
|
||||
return record.__catalogCache === true && Object.prototype.hasOwnProperty.call(record, "value");
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate catalog entries by product IDs
|
||||
* Returns true if any entries were invalidated, false if no matches found
|
||||
|
||||
@ -21,7 +21,7 @@ export type OrderItem = CatalogProductBase & {
|
||||
itemClass?: string;
|
||||
};
|
||||
|
||||
export interface OrderConfiguration {
|
||||
export interface OrderSummaryConfiguration {
|
||||
label: string;
|
||||
value: string;
|
||||
important?: boolean;
|
||||
@ -45,7 +45,7 @@ export interface EnhancedOrderSummaryProps {
|
||||
planDescription?: string;
|
||||
|
||||
// Configuration details
|
||||
configurations?: OrderConfiguration[];
|
||||
configurations?: OrderSummaryConfiguration[];
|
||||
|
||||
// Additional information
|
||||
infoLines?: string[];
|
||||
|
||||
@ -35,7 +35,7 @@ export type {
|
||||
export type {
|
||||
EnhancedOrderSummaryProps,
|
||||
OrderItem,
|
||||
OrderConfiguration,
|
||||
OrderSummaryConfiguration,
|
||||
OrderTotals,
|
||||
} from "./base/EnhancedOrderSummary";
|
||||
export type { ConfigurationStepProps, StepValidation } from "./base/ConfigurationStep";
|
||||
|
||||
@ -3,35 +3,15 @@
|
||||
* These are UI-specific formatting helpers, not business logic
|
||||
*/
|
||||
|
||||
import type { CatalogProductBase } from "@customer-portal/domain/catalog";
|
||||
import {
|
||||
getCatalogProductPriceDisplay,
|
||||
type CatalogProductBase,
|
||||
type CatalogPriceInfo,
|
||||
} from "@customer-portal/domain/catalog";
|
||||
|
||||
export interface PriceInfo {
|
||||
display: string;
|
||||
monthly: number | null;
|
||||
oneTime: number | null;
|
||||
currency: string;
|
||||
}
|
||||
// Re-export domain type for compatibility
|
||||
export type PriceInfo = CatalogPriceInfo;
|
||||
|
||||
export function getDisplayPrice(item: CatalogProductBase): PriceInfo | null {
|
||||
const monthlyPrice = item.monthlyPrice ?? null;
|
||||
const oneTimePrice = item.oneTimePrice ?? null;
|
||||
const currency = "JPY";
|
||||
|
||||
if (monthlyPrice === null && oneTimePrice === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let display = "";
|
||||
if (monthlyPrice !== null && monthlyPrice > 0) {
|
||||
display = `¥${monthlyPrice.toLocaleString()}/month`;
|
||||
} else if (oneTimePrice !== null && oneTimePrice > 0) {
|
||||
display = `¥${oneTimePrice.toLocaleString()} (one-time)`;
|
||||
}
|
||||
|
||||
return {
|
||||
display,
|
||||
monthly: monthlyPrice,
|
||||
oneTime: oneTimePrice,
|
||||
currency,
|
||||
};
|
||||
return getCatalogProductPriceDisplay(item);
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@ import { useActiveSubscriptions } from "@/features/subscriptions/hooks/useSubscr
|
||||
import {
|
||||
ORDER_TYPE,
|
||||
orderWithSkuValidationSchema,
|
||||
prepareOrderFromCart,
|
||||
type CheckoutCart,
|
||||
} from "@customer-portal/domain/orders";
|
||||
import { CheckoutParamsService } from "@/features/checkout/services/checkout-params.service";
|
||||
@ -150,31 +151,15 @@ export function useCheckout() {
|
||||
// Debug logging to check cart contents
|
||||
console.log("[DEBUG] Cart data:", cart);
|
||||
console.log("[DEBUG] Cart items:", cart.items);
|
||||
|
||||
const uniqueSkus = Array.from(
|
||||
new Set(
|
||||
cart.items
|
||||
.map(item => item.sku)
|
||||
.filter((sku): sku is string => typeof sku === "string" && sku.trim().length > 0)
|
||||
)
|
||||
);
|
||||
|
||||
console.log("[DEBUG] Extracted SKUs from cart:", uniqueSkus);
|
||||
|
||||
if (uniqueSkus.length === 0) {
|
||||
throw new Error("No products selected for order. Please go back and select products.");
|
||||
}
|
||||
|
||||
// Validate cart before submission
|
||||
await checkoutService.validateCart(cart);
|
||||
|
||||
const orderData = {
|
||||
orderType,
|
||||
skus: uniqueSkus,
|
||||
...(Object.keys(cart.configuration).length > 0
|
||||
? { configurations: cart.configuration }
|
||||
: {}),
|
||||
};
|
||||
// Use domain helper to prepare order data
|
||||
// This encapsulates SKU extraction and payload formatting
|
||||
const orderData = prepareOrderFromCart(cart, orderType);
|
||||
|
||||
console.log("[DEBUG] Extracted SKUs from cart:", orderData.skus);
|
||||
|
||||
const currentUserId = useAuthStore.getState().user?.id;
|
||||
if (currentUserId) {
|
||||
|
||||
@ -50,6 +50,17 @@ export interface PricingTier {
|
||||
originalPrice?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Standardized pricing display info
|
||||
* Used for consistent price rendering across Frontend and BFF
|
||||
*/
|
||||
export interface CatalogPriceInfo {
|
||||
display: string;
|
||||
monthly: number | null;
|
||||
oneTime: number | null;
|
||||
currency: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Catalog filtering options
|
||||
*/
|
||||
|
||||
@ -8,7 +8,9 @@ import {
|
||||
type SimCatalogCollection,
|
||||
type VpnCatalogCollection,
|
||||
type InternetPlanTemplate,
|
||||
type CatalogProductBase,
|
||||
} from "./schema";
|
||||
import type { CatalogPriceInfo } from "./contract";
|
||||
|
||||
/**
|
||||
* Empty catalog defaults shared by portal and BFF.
|
||||
@ -156,3 +158,30 @@ export function enrichInternetPlanMetadata(plan: InternetPlanCatalogItem): Inter
|
||||
|
||||
export const internetPlanCollectionSchema = internetPlanCatalogItemSchema.array();
|
||||
|
||||
/**
|
||||
* Calculates display price information for a catalog item
|
||||
* Centralized logic for price formatting
|
||||
*/
|
||||
export function getCatalogProductPriceDisplay(item: CatalogProductBase): CatalogPriceInfo | null {
|
||||
const monthlyPrice = item.monthlyPrice ?? null;
|
||||
const oneTimePrice = item.oneTimePrice ?? null;
|
||||
const currency = "JPY";
|
||||
|
||||
if (monthlyPrice === null && oneTimePrice === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let display = "";
|
||||
if (monthlyPrice !== null && monthlyPrice > 0) {
|
||||
display = `¥${monthlyPrice.toLocaleString()}/month`;
|
||||
} else if (oneTimePrice !== null && oneTimePrice > 0) {
|
||||
display = `¥${oneTimePrice.toLocaleString()} (one-time)`;
|
||||
}
|
||||
|
||||
return {
|
||||
display,
|
||||
monthly: monthlyPrice,
|
||||
oneTime: oneTimePrice,
|
||||
currency,
|
||||
};
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ import {
|
||||
type CreateOrderRequest,
|
||||
type OrderSelections,
|
||||
} from "./schema";
|
||||
import { ORDER_TYPE } from "./contract";
|
||||
import { ORDER_TYPE, type CheckoutCart, type OrderTypeValue } from "./contract";
|
||||
|
||||
export function buildOrderConfigurations(selections: OrderSelections): OrderConfigurations {
|
||||
const normalizedSelections = orderSelectionsSchema.parse(selections);
|
||||
@ -41,3 +41,38 @@ export function createOrderRequest(payload: {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform CheckoutCart into CreateOrderRequest
|
||||
* Handles SKU extraction, validation, and payload formatting
|
||||
*
|
||||
* @throws Error if no products are selected
|
||||
*/
|
||||
export function prepareOrderFromCart(
|
||||
cart: CheckoutCart,
|
||||
orderType: OrderTypeValue
|
||||
): CreateOrderRequest {
|
||||
const uniqueSkus = Array.from(
|
||||
new Set(
|
||||
cart.items
|
||||
.map(item => item.sku)
|
||||
.filter((sku): sku is string => typeof sku === "string" && sku.trim().length > 0)
|
||||
)
|
||||
);
|
||||
|
||||
if (uniqueSkus.length === 0) {
|
||||
throw new Error("No products selected for order. Please go back and select products.");
|
||||
}
|
||||
|
||||
// Note: Zod validation of the final structure should happen at the boundary or via schema.parse
|
||||
// This function focuses on the structural transformation logic.
|
||||
|
||||
const orderData: CreateOrderRequest = {
|
||||
orderType,
|
||||
skus: uniqueSkus,
|
||||
...(Object.keys(cart.configuration).length > 0
|
||||
? { configurations: cart.configuration }
|
||||
: {}),
|
||||
};
|
||||
|
||||
return orderData;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user