import type { AccountSummary, Aftersale, AftersaleApplyBody, AftersaleArbitration, AftersaleDetail, AftersaleMessage, AftersaleMessageBody, AftersaleReturnTrackingBody, AftersaleStatus, Address, AddressBookEntry, AuthTokens, Brand, BrandInput, Cart, Category, ContentInputByKind, ContentKind, Coupon, CouponTemplate, CouponTemplateInput, Currency, Favorite, FavoriteKind, FlashSaleItem, FlashSaleItemInput, FlashSaleSession, FlashSaleSessionInput, GroupBuyIntent, GroupBuyingActivity, GroupBuyingActivityInput, GroupBuyingActivityView, HomeContent, IntegralOrder, IntegralProduct, IntegralProductInput, Invoice, InvoiceKind, LocalizedText, Order, OrderStatus, Paged, Product, ProductStatus, PublicFlashSaleSession, RedeemPointsBody, Shipment, Shop, ShopFlashSaleSession, ShopProfile, ShopProfileInput, ShopProfileSelfInput, Sku, User, } from "./types"; export interface ApiClientOptions { baseUrl: string; getToken?: () => string | null; } export class ApiError extends Error { code: string; status: number; constructor(status: number, code: string, message: string) { super(message); this.status = status; this.code = code; } } // ---- request bodies ---- export interface ProductListQuery { page?: number; per_page?: number; category_id?: string; brand_id?: string; q?: string; shop_id?: string; /** `price` orders by lowest active SKU price; `sales` by units sold. */ sort?: "price" | "sales"; order?: "asc" | "desc"; } export interface ProductUpsertBody { category_id?: string | null; brand_id?: string | null; slug: string; name: LocalizedText; description?: LocalizedText; images?: string[]; } export interface SkuUpsertBody { sku_code: string; attributes?: Record; price_minor: number; currency: string; stock: number; active?: boolean; } export interface ShipmentItemBody { order_item_id: string; qty: number; } export interface AddressInput extends Address { is_default?: boolean; } export interface CurrencyUpsertBody { code: string; name: LocalizedText; symbol: string; exponent: number; rate_to_base: string; enabled: boolean; } export interface ShopProductQuery { page?: number; per_page?: number; status?: ProductStatus; } export interface ShopOrderQuery { page?: number; per_page?: number; status?: OrderStatus; } export interface ConvertResult { amount_minor: number; currency: string; } export interface FavoriteListQuery { kind: FavoriteKind; target_id?: string; page?: number; per_page?: number; } type Query = Record; async function request( opts: ApiClientOptions, method: string, path: string, body?: unknown, query?: Query, ): Promise { const url = new URL(opts.baseUrl + path); if (query) { for (const [k, v] of Object.entries(query)) { if (v !== undefined && v !== null && v !== "") url.searchParams.set(k, String(v)); } } const headers: Record = { "content-type": "application/json" }; const token = opts.getToken?.(); if (token) headers.authorization = `Bearer ${token}`; const res = await fetch(url.toString(), { method, headers, body: body === undefined ? undefined : JSON.stringify(body), }); if (!res.ok) { let code = "UNKNOWN"; let message = res.statusText; try { const data = (await res.json()) as { error?: { code?: string; message?: string } }; code = data?.error?.code ?? code; message = data?.error?.message ?? message; } catch { /* non-JSON error body */ } throw new ApiError(res.status, code, message); } if (res.status === 204) return undefined as T; return (await res.json()) as T; } export interface ApiClient { register(email: string, password: string, displayName: string): Promise; login(email: string, password: string): Promise; me(): Promise; /** The signed-in customer's own money and points balances. */ getAccountSummary(): Promise; listProducts(q?: ProductListQuery): Promise>; getProduct(idOrSlug: string): Promise; listCategories(): Promise; listBrands(): Promise; listCurrencies(): Promise; convert(amountMinor: number, from: string, to: string): Promise; getCart(): Promise; addCartItem(skuId: string, qty: number): Promise; updateCartItem(skuId: string, qty: number): Promise; removeCartItem(skuId: string): Promise; checkout( shippingAddress: Address, currency: string, /** At most one owned coupon per generated shop order. */ couponByShop?: Record, /** Optional group-buying intent for one activity SKU at quantity 1. */ groupBuy?: GroupBuyIntent, ): Promise; listMyOrders(page?: number): Promise>; getOrder(id: string): Promise; cancelOrder(id: string): Promise; payOrder(id: string): Promise; confirmDelivered(shipmentId: string): Promise; listMyShipments(): Promise; requestInvoice( orderId: string, title: string, taxNo: string | null, kind: InvoiceKind, ): Promise; listMyInvoices(): Promise; /** Public home-page marketing content: banners, promos, quick links, floor adverts. */ getHomeContent(): Promise; /** Public store directory: active shops with whatever profile they have. */ listShops(): Promise; getShop(slug: string): Promise; listMyAddresses(): Promise; createAddress(address: AddressInput): Promise; updateAddress(id: string, address: AddressInput): Promise; deleteAddress(id: string): Promise; setDefaultAddress(id: string): Promise; listFavorites(q: FavoriteListQuery): Promise>; addProductFavorite(productId: string): Promise; removeProductFavorite(productId: string): Promise; addShopFavorite(shopId: string): Promise; removeShopFavorite(shopId: string): Promise; /** Public: coupons a shopper could claim from this shop right now. */ listShopCouponTemplates(shopId: string): Promise; listMyCoupons(): Promise; claimCoupon(templateId: string): Promise; applyForAftersale(body: AftersaleApplyBody): Promise; listMyAftersales(): Promise; getAftersale(id: string): Promise; cancelAftersale(id: string): Promise; reopenAftersale(id: string): Promise; submitAftersaleReturnTracking( id: string, body: AftersaleReturnTrackingBody, ): Promise; addAftersaleMessage(id: string, body: AftersaleMessageBody): Promise; /** Public points catalog: published products only. */ listPointsProducts(): Promise; listMyRedemptions(): Promise; redeemPoints(body: RedeemPointsBody): Promise; /** Public: active flash-sale sessions with their purchasable items. */ listFlashSales(): Promise; /** Public: active group-buying activities with their open groups. */ listGroupBuyingActivities(): Promise; shop: { getMyShop(): Promise; /** Merchant self-write of the own shop profile; scores stay platform-owned. */ updateMyProfile(body: ShopProfileSelfInput): Promise; listMyProducts(q?: ShopProductQuery): Promise>; getProduct(id: string): Promise; createProduct(body: ProductUpsertBody): Promise; updateProduct(id: string, body: ProductUpsertBody): Promise; publish(id: string): Promise; unpublish(id: string): Promise; upsertSku(productId: string, body: SkuUpsertBody): Promise; listOrders(q?: ShopOrderQuery): Promise>; getOrder(id: string): Promise; createShipment( orderId: string, carrier: string, trackingNo: string, items: ShipmentItemBody[], ): Promise; listShipments(): Promise; markShipped(id: string): Promise; listInvoices(): Promise; issueInvoice(id: string): Promise; listCouponTemplates(): Promise; createCouponTemplate(body: CouponTemplateInput): Promise; updateCouponTemplate(id: string, body: CouponTemplateInput): Promise; deleteCouponTemplate(id: string): Promise; listFlashSales(): Promise; createFlashSale(body: FlashSaleSessionInput): Promise; updateFlashSale(id: string, body: FlashSaleSessionInput): Promise; deleteFlashSale(id: string): Promise; addFlashSaleItem(sessionId: string, body: FlashSaleItemInput): Promise; updateFlashSaleItem(id: string, body: FlashSaleItemInput): Promise; deleteFlashSaleItem(id: string): Promise; listGroupBuyingActivities(): Promise; createGroupBuyingActivity(body: GroupBuyingActivityInput): Promise; updateGroupBuyingActivity( id: string, body: GroupBuyingActivityInput, ): Promise; deleteGroupBuyingActivity(id: string): Promise; listAftersales(status?: AftersaleStatus): Promise; getAftersale(id: string): Promise; approveAftersale(id: string): Promise; rejectAftersale(id: string): Promise; confirmAftersaleReceipt(id: string): Promise; refundAftersale(id: string): Promise; addAftersaleMessage(id: string, body: AftersaleMessageBody): Promise; }; admin: { listUsers(page?: number): Promise>; setUserRole(id: string, role: string, shopId: string | null): Promise; listShops(): Promise; createShop(name: LocalizedText, slug: string): Promise; setShopStatus(id: string, status: "active" | "suspended"): Promise; listOrders(page?: number): Promise>; listCurrencies(): Promise; upsertCurrency(body: CurrencyUpsertBody): Promise; setRate(code: string, rateToBase: string): Promise; /** Every content kind, inactive rows included. */ getContent(): Promise; /** Replaces one kind from an ordered list; positions follow the array order. */ replaceContent( kind: K, items: ContentInputByKind[K], ): Promise; listAftersales(status?: AftersaleStatus): Promise; getAftersale(id: string): Promise; arbitrateAftersale(id: string, outcome: AftersaleArbitration): Promise; }; } export function createApi(opts: ApiClientOptions): ApiClient { const r = (m: string, p: string, b?: unknown, q?: Query): Promise => request(opts, m, p, b, q); return { register: (email, password, displayName) => r("POST", "/auth/register", { email, password, display_name: displayName }), login: (email, password) => r("POST", "/auth/login", { email, password }), me: () => r("GET", "/auth/me"), getAccountSummary: () => r("GET", "/me/stats"), listProducts: (q = {}) => r("GET", "/products", undefined, { ...q }), getProduct: (idOrSlug) => r("GET", `/products/${idOrSlug}`), listCategories: () => r("GET", "/categories"), listBrands: () => r("GET", "/brands"), listCurrencies: () => r("GET", "/currencies"), convert: (amountMinor, from, to) => r("GET", "/currencies/convert", undefined, { amount_minor: amountMinor, from, to }), getCart: () => r("GET", "/cart"), addCartItem: (skuId, qty) => r("POST", "/cart/items", { sku_id: skuId, qty }), updateCartItem: (skuId, qty) => r("PUT", `/cart/items/${skuId}`, { qty }), removeCartItem: (skuId) => r("DELETE", `/cart/items/${skuId}`), checkout: (shippingAddress, currency, couponByShop = {}, groupBuy) => r("POST", "/orders/checkout", { shipping_address: shippingAddress, currency, coupon_by_shop: couponByShop, group_buy: groupBuy ?? null, }), listMyOrders: (page = 1) => r("GET", "/orders", undefined, { page }), getOrder: (id) => r("GET", `/orders/${id}`), cancelOrder: (id) => r("POST", `/orders/${id}/cancel`), payOrder: (id) => r("POST", `/orders/${id}/pay`), confirmDelivered: (shipmentId) => r("POST", `/shipments/${shipmentId}/confirm-delivered`), listMyShipments: () => r("GET", "/shipments"), requestInvoice: (orderId, title, taxNo, kind) => r("POST", `/orders/${orderId}/invoice`, { title, tax_no: taxNo, kind }), listMyInvoices: () => r("GET", "/invoices"), getHomeContent: () => r("GET", "/content/home"), listShops: () => r("GET", "/shops"), getShop: (slug) => r("GET", `/shops/${slug}`), listMyAddresses: () => r("GET", "/addresses"), createAddress: (address) => r("POST", "/addresses", address), updateAddress: (id, address) => r("PUT", `/addresses/${id}`, address), deleteAddress: (id) => r("DELETE", `/addresses/${id}`), setDefaultAddress: (id) => r("POST", `/addresses/${id}/default`), listFavorites: (q) => r("GET", "/favorites", undefined, { ...q }), addProductFavorite: (productId) => r("PUT", `/favorites/products/${productId}`), removeProductFavorite: (productId) => r("DELETE", `/favorites/products/${productId}`), addShopFavorite: (shopId) => r("PUT", `/favorites/shops/${shopId}`), removeShopFavorite: (shopId) => r("DELETE", `/favorites/shops/${shopId}`), listShopCouponTemplates: (shopId) => r("GET", `/shops/${shopId}/coupon-templates`), listMyCoupons: () => r("GET", "/me/coupons"), claimCoupon: (templateId) => r("POST", "/me/coupons", { template_id: templateId }), applyForAftersale: (body) => r("POST", "/aftersales", body), listMyAftersales: () => r("GET", "/aftersales"), getAftersale: (id) => r("GET", `/aftersales/${id}`), cancelAftersale: (id) => r("POST", `/aftersales/${id}/cancel`), reopenAftersale: (id) => r("POST", `/aftersales/${id}/reopen`), submitAftersaleReturnTracking: (id, body) => r("POST", `/aftersales/${id}/return-tracking`, body), addAftersaleMessage: (id, body) => r("POST", `/aftersales/${id}/messages`, body), listPointsProducts: () => r("GET", "/points/products"), listMyRedemptions: () => r("GET", "/points/redemptions"), redeemPoints: (body) => r("POST", "/points/redemptions", body), listFlashSales: () => r("GET", "/flash-sales"), listGroupBuyingActivities: () => r("GET", "/group-buying/activities"), shop: { getMyShop: () => r("GET", "/shop/profile"), updateMyProfile: (body) => r("PUT", "/shop/profile", body), listMyProducts: (q = {}) => r("GET", "/shop/products", undefined, { ...q }), getProduct: (id) => r("GET", `/shop/products/${id}`), createProduct: (body) => r("POST", "/shop/products", body), updateProduct: (id, body) => r("PUT", `/shop/products/${id}`, body), publish: (id) => r("POST", `/shop/products/${id}/publish`), unpublish: (id) => r("POST", `/shop/products/${id}/unpublish`), upsertSku: (productId, body) => r("POST", `/shop/products/${productId}/skus`, body), listOrders: (q = {}) => r("GET", "/shop/orders", undefined, { ...q }), getOrder: (id) => r("GET", `/shop/orders/${id}`), createShipment: (orderId, carrier, trackingNo, items) => r("POST", `/shop/orders/${orderId}/shipments`, { carrier, tracking_no: trackingNo, items, }), listShipments: () => r("GET", "/shop/shipments"), markShipped: (id) => r("POST", `/shop/shipments/${id}/ship`), listInvoices: () => r("GET", "/shop/invoices"), issueInvoice: (id) => r("POST", `/shop/invoices/${id}/issue`), listCouponTemplates: () => r("GET", "/shop/coupon-templates"), createCouponTemplate: (body) => r("POST", "/shop/coupon-templates", body), updateCouponTemplate: (id, body) => r("PUT", `/shop/coupon-templates/${id}`, body), deleteCouponTemplate: (id) => r("DELETE", `/shop/coupon-templates/${id}`), listFlashSales: () => r("GET", "/shop/flash-sales"), createFlashSale: (body) => r("POST", "/shop/flash-sales", body), updateFlashSale: (id, body) => r("PUT", `/shop/flash-sales/${id}`, body), deleteFlashSale: (id) => r("DELETE", `/shop/flash-sales/${id}`), addFlashSaleItem: (sessionId, body) => r("POST", `/shop/flash-sales/${sessionId}/items`, body), updateFlashSaleItem: (id, body) => r("PUT", `/shop/flash-sale-items/${id}`, body), deleteFlashSaleItem: (id) => r("DELETE", `/shop/flash-sale-items/${id}`), listGroupBuyingActivities: () => r("GET", "/shop/group-buying-activities"), createGroupBuyingActivity: (body) => r("POST", "/shop/group-buying-activities", body), updateGroupBuyingActivity: (id, body) => r("PUT", `/shop/group-buying-activities/${id}`, body), deleteGroupBuyingActivity: (id) => r("DELETE", `/shop/group-buying-activities/${id}`), listAftersales: (status) => r("GET", "/shop/aftersales", undefined, { status }), getAftersale: (id) => r("GET", `/shop/aftersales/${id}`), approveAftersale: (id) => r("POST", `/shop/aftersales/${id}/approve`), rejectAftersale: (id) => r("POST", `/shop/aftersales/${id}/reject`), confirmAftersaleReceipt: (id) => r("POST", `/shop/aftersales/${id}/confirm-receipt`), refundAftersale: (id) => r("POST", `/shop/aftersales/${id}/refund`), addAftersaleMessage: (id, body) => r("POST", `/shop/aftersales/${id}/messages`, body), }, admin: { listUsers: (page = 1) => r("GET", "/admin/users", undefined, { page }), setUserRole: (id, role, shopId) => r("PUT", `/admin/users/${id}/role`, { role, shop_id: shopId }), listShops: () => r("GET", "/admin/shops"), createShop: (name, slug) => r("POST", "/admin/shops", { name, slug }), setShopStatus: (id, status) => r("PUT", `/admin/shops/${id}/status`, { status }), listOrders: (page = 1) => r("GET", "/admin/orders", undefined, { page }), listCurrencies: () => r("GET", "/admin/currencies"), upsertCurrency: (body) => r("POST", "/admin/currencies", body), setRate: (code, rateToBase) => r("PUT", `/admin/currencies/${code}/rate`, { rate_to_base: rateToBase, }), getContent: () => r("GET", "/admin/content"), replaceContent: (kind, items) => r("PUT", `/admin/content/${kind}`, items), setShopProfile: (id, body) => r("PUT", `/admin/shops/${id}/profile`, body), /** Replaces the whole ordered brand list. */ getBrands: () => r("GET", "/brands"), replaceBrands: (items) => r("PUT", "/admin/brands", items), listAftersales: (status) => r("GET", "/admin/aftersales", undefined, { status }), getAftersale: (id) => r("GET", `/admin/aftersales/${id}`), arbitrateAftersale: (id, outcome) => r("POST", `/admin/aftersales/${id}/arbitrate`, { outcome }), listPointsProducts: () => r("GET", "/admin/points/products"), createPointsProduct: (body) => r("POST", "/admin/points/products", body), updatePointsProduct: (id, body) => r("PUT", `/admin/points/products/${id}`, body), setPointsProductPublished: (id, published) => r("POST", `/admin/points/products/${id}/${published ? "publish" : "unpublish"}`), listPointsRedemptions: (page = 1) => r("GET", "/admin/points/orders", undefined, { page }), fulfillRedemption: (id) => r("POST", `/admin/points/orders/${id}/fulfill`), cancelRedemption: (id) => r("POST", `/admin/points/orders/${id}/cancel`), }, }; }