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.
125 lines
3.7 KiB
TypeScript
125 lines
3.7 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"
|
|
| "points";
|
|
|
|
/**
|
|
* 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,
|
|
}),
|
|
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[];
|
|
|
|
/** 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",
|
|
"points",
|
|
];
|
|
|
|
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 } };
|
|
});
|