import type { Address, AuthTokens, Cart, Category, ContentInputByKind, ContentKind, Currency, HomeContent, Invoice, InvoiceKind, LocalizedText, Order, OrderStatus, Paged, Product, ProductStatus, Shipment, Shop, ShopProfile, ShopProfileInput, 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; q?: string; shop_id?: string; /** `price` orders by each product's lowest active SKU price. */ sort?: "price"; order?: "asc" | "desc"; } export interface ProductUpsertBody { category_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 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; } 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; listProducts(q?: ProductListQuery): Promise>; getProduct(idOrSlug: string): Promise; listCategories(): 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): 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; shop: { getMyShop(): 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; }; 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; }; } 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"), listProducts: (q = {}) => r("GET", "/products", undefined, { ...q }), getProduct: (idOrSlug) => r("GET", `/products/${idOrSlug}`), listCategories: () => r("GET", "/categories"), 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) => r("POST", "/orders/checkout", { shipping_address: shippingAddress, currency }), 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}`), shop: { getMyShop: () => r("GET", "/shop/profile"), 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`), }, 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), }, }; }