Templates belong to a shop; claiming copies their terms into a customer-owned snapshot so a later edit or disable cannot rewrite a held coupon. Claim stock is taken with a guarded decrement after locking the template, and a unique (user, template) index makes a duplicate claim a 409. Deleting a template leaves claimed snapshots standing via ON DELETE SET NULL. Checkout accepts at most one owned coupon per generated shop order, locks the selected coupons by primary key after the SKU locks, and resolves eligibility and the discount server-side (ownership, shop, status, window, converted threshold). The realized discount and coupon id land on the order, and a pending-payment cancellation restores the coupon in the same transaction as stock. The shared contract gains the coupon types, claim/list/manage methods, and the checkout coupon map; the fixed-data adapter implements the same surface. Surfaces (shop-admin management, mall coupon pages, checkout selection) and seeding still follow in tasks 3.1-4.2.
118 lines
3.5 KiB
TypeScript
118 lines
3.5 KiB
TypeScript
import { createApi } from "@vmall/shared";
|
|
import type { ApiClient } from "@vmall/shared";
|
|
import { createMockApi } from "~/mock/api";
|
|
|
|
/**
|
|
* Domains the live backend serves. Every other domain stays on the fixed-data
|
|
* adapter, so a domain can be migrated - or rolled back - by editing this list
|
|
* alone. See openspec/changes/replace-mock-api-wave-1/design.md.
|
|
*/
|
|
type LiveDomain =
|
|
| "auth"
|
|
| "account"
|
|
| "catalog"
|
|
| "currency"
|
|
| "content"
|
|
| "shops"
|
|
| "brands"
|
|
| "cart"
|
|
| "orders"
|
|
| "shipments"
|
|
| "invoices"
|
|
| "addresses"
|
|
| "coupons";
|
|
|
|
/**
|
|
* Explicit per-domain method picks rather than a string allowlist: indexing
|
|
* `ApiClient` by a union of method names would need an unsafe cast, and this
|
|
* keeps the compiler checking that every picked key exists.
|
|
*/
|
|
const LIVE_PICKS = {
|
|
auth: (a: ApiClient) => ({ register: a.register, login: a.login, me: a.me }),
|
|
account: (a: ApiClient) => ({ getAccountSummary: a.getAccountSummary }),
|
|
catalog: (a: ApiClient) => ({
|
|
listProducts: a.listProducts,
|
|
getProduct: a.getProduct,
|
|
listCategories: a.listCategories,
|
|
}),
|
|
currency: (a: ApiClient) => ({ listCurrencies: a.listCurrencies, convert: a.convert }),
|
|
content: (a: ApiClient) => ({ getHomeContent: a.getHomeContent }),
|
|
shops: (a: ApiClient) => ({ listShops: a.listShops, getShop: a.getShop }),
|
|
brands: (a: ApiClient) => ({ listBrands: a.listBrands }),
|
|
cart: (a: ApiClient) => ({
|
|
getCart: a.getCart,
|
|
addCartItem: a.addCartItem,
|
|
updateCartItem: a.updateCartItem,
|
|
removeCartItem: a.removeCartItem,
|
|
}),
|
|
orders: (a: ApiClient) => ({
|
|
checkout: a.checkout,
|
|
listMyOrders: a.listMyOrders,
|
|
getOrder: a.getOrder,
|
|
cancelOrder: a.cancelOrder,
|
|
payOrder: a.payOrder,
|
|
}),
|
|
shipments: (a: ApiClient) => ({
|
|
confirmDelivered: a.confirmDelivered,
|
|
listMyShipments: a.listMyShipments,
|
|
}),
|
|
invoices: (a: ApiClient) => ({
|
|
requestInvoice: a.requestInvoice,
|
|
listMyInvoices: a.listMyInvoices,
|
|
}),
|
|
addresses: (a: ApiClient) => ({
|
|
listMyAddresses: a.listMyAddresses,
|
|
createAddress: a.createAddress,
|
|
updateAddress: a.updateAddress,
|
|
deleteAddress: a.deleteAddress,
|
|
setDefaultAddress: a.setDefaultAddress,
|
|
}),
|
|
coupons: (a: ApiClient) => ({
|
|
listShopCouponTemplates: a.listShopCouponTemplates,
|
|
listMyCoupons: a.listMyCoupons,
|
|
claimCoupon: a.claimCoupon,
|
|
}),
|
|
} satisfies Record<LiveDomain, (a: ApiClient) => Partial<ApiClient>>;
|
|
|
|
const KNOWN_DOMAINS = Object.keys(LIVE_PICKS) as LiveDomain[];
|
|
|
|
/** Fallback when runtimeConfig supplies no list; keep in step with nuxt.config.ts. */
|
|
const DEFAULT_LIVE_DOMAINS: LiveDomain[] = [
|
|
"catalog",
|
|
"currency",
|
|
"content",
|
|
"shops",
|
|
"brands",
|
|
"auth",
|
|
"account",
|
|
"cart",
|
|
"orders",
|
|
"shipments",
|
|
"invoices",
|
|
"addresses",
|
|
"coupons",
|
|
];
|
|
|
|
export default defineNuxtPlugin(() => {
|
|
const config = useRuntimeConfig();
|
|
const mock = createMockApi();
|
|
const live = createApi({
|
|
baseUrl: config.public.apiBase as string,
|
|
getToken: () => (import.meta.client ? localStorage.getItem("vmall.token") : null),
|
|
});
|
|
|
|
const configured = config.public.liveDomains as string[] | undefined;
|
|
const liveDomains = (configured ?? DEFAULT_LIVE_DOMAINS).filter((domain): domain is LiveDomain =>
|
|
KNOWN_DOMAINS.includes(domain as LiveDomain),
|
|
);
|
|
|
|
// The fixed-data adapter is the base object, so an unmigrated domain cannot
|
|
// regress and a live domain can be rolled back by removing one entry.
|
|
let api: ApiClient = mock;
|
|
for (const domain of liveDomains) {
|
|
api = { ...api, ...LIVE_PICKS[domain](live) };
|
|
}
|
|
|
|
return { provide: { api } };
|
|
});
|