feat(mall): claim and redeem shop coupons from the storefront
The product page loads a shop's claimable coupons through the shared client and claims them in place; the buyer coupon list reads owned snapshots with their claimed/redeemed/expired state. Checkout offers each shop order at most one owned coupon and submits only the choice; the pay page and order detail render the server-persisted discount and reduced total, never a client figure. Coupon fixtures leave the pages; the fixed-data adapter still serves the coupon domain as the rollback path. The demo seed creates two deterministic templates and claims them for the demo customer.
This commit is contained in:
@@ -173,6 +173,34 @@ for (const def of SHOPS) {
|
||||
ownerTokens.set(def.slug, await ensureShop(def));
|
||||
}
|
||||
|
||||
// 2b. shop coupons: deterministic, idempotent by localized title.
|
||||
const DEMO_COUPONS = [
|
||||
{ title: { en: "$5 off over $59", zh: "满 59 减 5" }, amount_minor: 500, threshold_minor: 5900, stock: 200 },
|
||||
{ title: { en: "$15 off over $199", zh: "满 199 减 15" }, amount_minor: 1500, threshold_minor: 19900, stock: 100 },
|
||||
];
|
||||
let demoShopId = "";
|
||||
{
|
||||
const owner = ownerTokens.get("demo-store");
|
||||
const profile = await call("GET", "/shop/profile", { token: owner });
|
||||
if (profile.status !== 200) fail("coupon shop profile", profile);
|
||||
demoShopId = profile.data.id;
|
||||
|
||||
const listed = await call("GET", "/shop/coupon-templates", { token: owner });
|
||||
if (listed.status !== 200) fail("list coupon templates", listed);
|
||||
const known = new Set(listed.data.map((t) => t.title.en));
|
||||
const starts_at = new Date(Date.now() - 24 * 3600 * 1000).toISOString();
|
||||
const ends_at = new Date(Date.now() + 365 * 24 * 3600 * 1000).toISOString();
|
||||
for (const coupon of DEMO_COUPONS) {
|
||||
if (known.has(coupon.title.en)) continue;
|
||||
const res = await call("POST", "/shop/coupon-templates", {
|
||||
token: owner,
|
||||
body: { ...coupon, currency: "USD", enabled: true, starts_at, ends_at },
|
||||
});
|
||||
if (res.status !== 201) fail(`seed coupon ${coupon.title.en}`, res);
|
||||
}
|
||||
console.log(`coupons ready: ${DEMO_COUPONS.length}`);
|
||||
}
|
||||
|
||||
// 3. demo customer
|
||||
await call("POST", "/auth/register", {
|
||||
body: { email: "customer@vmall.local", password: "customer123", display_name: "Demo Customer" },
|
||||
@@ -194,6 +222,19 @@ await call("POST", "/auth/register", {
|
||||
if (r.status !== 200 && r.status !== 201) fail("seed address", r);
|
||||
}
|
||||
}
|
||||
|
||||
// Claim the demo shop's coupons so the buyer starts with live ones.
|
||||
const claimable = (await call("GET", `/shops/${demoShopId}/coupon-templates`)).data;
|
||||
if (Array.isArray(claimable)) {
|
||||
for (const template of claimable) {
|
||||
const res = await call("POST", "/me/coupons", {
|
||||
token: customerToken,
|
||||
body: { template_id: template.id },
|
||||
});
|
||||
// 409 means a previous seed run already claimed it.
|
||||
if (res.status !== 201 && res.status !== 409) fail(`claim coupon ${template.id}`, res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. categories (reference data from the migrations)
|
||||
|
||||
Reference in New Issue
Block a user