feat(mall): live flash-sale page and activity price snapshots
The flash-sale page loads active sessions and their items through the shared client with live sale price, remaining activity stock, sell-through, and a countdown, and adds the SKU to the existing cart so checkout resolves the authoritative price. Payment and order detail tag and render the snapshotted activity unit price. The flash-sale fixtures leave the page; the fixed-data adapter still serves the domain as the rollback path.
This commit is contained in:
@@ -15,6 +15,10 @@ import type {
|
||||
Coupon,
|
||||
CouponTemplate,
|
||||
CouponTemplateInput,
|
||||
FlashSaleItem,
|
||||
FlashSaleItemInput,
|
||||
FlashSaleSession,
|
||||
FlashSaleSessionInput,
|
||||
HomeContent,
|
||||
IntegralOrder,
|
||||
IntegralProduct,
|
||||
@@ -23,6 +27,7 @@ import type {
|
||||
InvoiceKind,
|
||||
Order,
|
||||
Product,
|
||||
PublicFlashSaleSession,
|
||||
RedeemPointsBody,
|
||||
Shipment,
|
||||
ShopProfile,
|
||||
@@ -43,9 +48,11 @@ import {
|
||||
USER_STATS,
|
||||
MOCK_ADDRESSES,
|
||||
defaultAddress,
|
||||
lowestSku,
|
||||
mockConvertMinor,
|
||||
productById,
|
||||
searchMockProducts,
|
||||
seckillProducts,
|
||||
seedOrders,
|
||||
storeById,
|
||||
} from "./data";
|
||||
@@ -150,6 +157,46 @@ function seedPointsProducts(): IntegralProduct[] {
|
||||
}));
|
||||
}
|
||||
|
||||
/** One synthesized active session covering the fixed seckill fixtures. */
|
||||
function mockFlashSales(): PublicFlashSaleSession[] {
|
||||
const now = Date.now();
|
||||
const items = seckillProducts().map((entry, index) => {
|
||||
const sku = lowestSku(entry.product);
|
||||
const stock = sku?.stock ?? 100;
|
||||
const sold = Math.round((stock * entry.soldPct) / 100);
|
||||
return {
|
||||
id: `fsi${index + 1}`,
|
||||
session_id: "fs1",
|
||||
sku_id: sku?.id ?? "",
|
||||
product_id: entry.product.id,
|
||||
product_slug: entry.product.slug,
|
||||
product_name: entry.product.name,
|
||||
image: entry.product.images[0] ?? null,
|
||||
sku_code: sku?.sku_code ?? "",
|
||||
sale_price_minor: entry.seckillPriceMinor,
|
||||
currency: sku?.currency ?? BASE_CURRENCY,
|
||||
original_price_minor: sku?.price_minor ?? entry.seckillPriceMinor,
|
||||
original_currency: sku?.currency ?? BASE_CURRENCY,
|
||||
reserved_stock: Math.max(stock - sold, 0),
|
||||
sold_count: sold,
|
||||
per_customer_limit: 2,
|
||||
};
|
||||
});
|
||||
return [
|
||||
{
|
||||
id: "fs1",
|
||||
shop_id: MOCK_STORES[0].id,
|
||||
label: { en: "Flash sale", zh: "限时秒杀" },
|
||||
starts_at: new Date(now - 3_600_000).toISOString(),
|
||||
ends_at: new Date(now + 6 * 3_600_000).toISOString(),
|
||||
enabled: true,
|
||||
created_at: "2026-01-01T00:00:00.000Z",
|
||||
updated_at: "2026-01-01T00:00:00.000Z",
|
||||
items,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function initialState(): MockState {
|
||||
const persisted = loadPersisted();
|
||||
// Coupons and points are session-only, so a restored snapshot re-seeds them.
|
||||
@@ -397,6 +444,7 @@ export function createMockApi(): ApiClient {
|
||||
unit_price_minor: i.unit_price_minor,
|
||||
qty: i.qty,
|
||||
image: i.image,
|
||||
flash_sale_item_id: null,
|
||||
})),
|
||||
shipping_address: shippingAddress,
|
||||
created_at: new Date().toISOString(),
|
||||
@@ -630,6 +678,8 @@ export function createMockApi(): ApiClient {
|
||||
state.pointsProducts.filter((p) => p.published).map((p) => ({ ...p })),
|
||||
),
|
||||
|
||||
listFlashSales: () => Promise.resolve(mockFlashSales()),
|
||||
|
||||
listMyRedemptions: () => Promise.resolve(state.redemptions.map((o) => ({ ...o }))),
|
||||
|
||||
redeemPoints: (body: RedeemPointsBody) => {
|
||||
@@ -693,6 +743,13 @@ export function createMockApi(): ApiClient {
|
||||
createCouponTemplate: (_body: CouponTemplateInput) => unsupported(),
|
||||
updateCouponTemplate: (_id: string, _body: CouponTemplateInput) => unsupported(),
|
||||
deleteCouponTemplate: (_id: string) => unsupported(),
|
||||
listFlashSales: () => unsupported(),
|
||||
createFlashSale: (_body: FlashSaleSessionInput) => unsupported(),
|
||||
updateFlashSale: (_id: string, _body: FlashSaleSessionInput) => unsupported(),
|
||||
deleteFlashSale: (_id: string) => unsupported(),
|
||||
addFlashSaleItem: (_sessionId: string, _body: FlashSaleItemInput) => unsupported(),
|
||||
updateFlashSaleItem: (_id: string, _body: FlashSaleItemInput) => unsupported(),
|
||||
deleteFlashSaleItem: (_id: string) => unsupported(),
|
||||
},
|
||||
admin: {
|
||||
listUsers: () => unsupported(),
|
||||
|
||||
Reference in New Issue
Block a user