feat(api): points mall with atomic redemption and admin fulfillment

Platform-owned points products and a redemption order lifecycle kept separate
from cash orders. Redeeming locks the published product, reserves stock, creates
the order, debits points through the archived customer-accounts ledger with the
order as reference, and snapshots the line in one transaction; a failure leaves
no order, no stock change, and no ledger entry. Fulfilment moves only from
pending_fulfillment, and customers see only their own redemptions.

Demo seeding credits the demo customer through the same guarded credit path
with reason seed, once, so no balance is ever written absolutely.

Surfaces (admin console, mall points page) and product seeding follow.
This commit is contained in:
2026-09-18 12:24:46 +00:00
parent cc07ac3230
commit 9c2b705880
19 changed files with 1400 additions and 10 deletions
+8 -1
View File
@@ -20,7 +20,8 @@ type LiveDomain =
| "shipments"
| "invoices"
| "addresses"
| "coupons";
| "coupons"
| "points";
/**
* Explicit per-domain method picks rather than a string allowlist: indexing
@@ -72,6 +73,11 @@ const LIVE_PICKS = {
listMyCoupons: a.listMyCoupons,
claimCoupon: a.claimCoupon,
}),
points: (a: ApiClient) => ({
listPointsProducts: a.listPointsProducts,
listMyRedemptions: a.listMyRedemptions,
redeemPoints: a.redeemPoints,
}),
} satisfies Record<LiveDomain, (a: ApiClient) => Partial<ApiClient>>;
const KNOWN_DOMAINS = Object.keys(LIVE_PICKS) as LiveDomain[];
@@ -91,6 +97,7 @@ const DEFAULT_LIVE_DOMAINS: LiveDomain[] = [
"invoices",
"addresses",
"coupons",
"points",
];
export default defineNuxtPlugin(() => {