/** * Torbat Pay API client — real backend only (MVP 14.0–14.5). * Browser → /api/payment BFF; SSR → direct upstream :8012 */ import { getValidAccessToken, refreshSession } from "@/lib/auth"; import type { CapabilitiesResponse, HealthResponse, MetricsResponse, PaymentRecord, PermissionsCatalogResponse, } from "@/modules/payment/types"; const PAYMENT_BASE = typeof window !== "undefined" ? "/api/payment" : process.env.PAYMENT_SERVICE_URL || process.env.NEXT_PUBLIC_PAYMENT_API_URL || "http://localhost:8012"; export class PaymentApiError extends Error { constructor( public status: number, public code: string, message: string, public details?: unknown ) { super(message); this.name = "PaymentApiError"; } } type RequestOptions = RequestInit & { tenantId: string; auth?: boolean; idempotencyKey?: string; }; async function request(path: string, options: RequestOptions): Promise { const { tenantId, auth = true, idempotencyKey, headers: customHeaders, ...init } = options; const headers = new Headers(customHeaders); if (!headers.has("Content-Type") && init.body) headers.set("Content-Type", "application/json"); headers.set("X-Tenant-ID", tenantId); if (idempotencyKey) headers.set("Idempotency-Key", idempotencyKey); if (auth) { const token = await getValidAccessToken(); if (token) headers.set("Authorization", `Bearer ${token}`); } const url = path.startsWith("http") ? path : `${PAYMENT_BASE.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`; let res: Response; try { res = await fetch(url, { ...init, headers, cache: "no-store" }); } catch (err) { const detail = err instanceof Error ? err.message : "network_error"; throw new Error(`اتصال به سرویس تربت‌پی برقرار نشد — ${detail}`); } if (res.status === 401 && auth) { const refreshed = await refreshSession({ force: true }); if (refreshed) { headers.set("Authorization", `Bearer ${refreshed}`); res = await fetch(url, { ...init, headers, cache: "no-store" }); } } if (!res.ok) { const body = await res.json().catch(() => ({})); const detail = body?.error ?? body?.detail ?? {}; throw new PaymentApiError( res.status, detail?.code || (typeof detail === "string" ? detail : "unknown_error"), detail?.message || (typeof detail === "string" ? detail : res.statusText), detail ); } if (res.status === 204) return undefined as T; return res.json() as Promise; } function crud(basePath: string) { return { list: (tenantId: string) => request(basePath, { tenantId }), get: (tenantId: string, id: string) => request(`${basePath}/${id}`, { tenantId }), create: (tenantId: string, body: Record) => request(basePath, { tenantId, method: "POST", body: JSON.stringify(body) }), update: (tenantId: string, id: string, body: Record) => request(`${basePath}/${id}`, { tenantId, method: "PATCH", body: JSON.stringify(body), }), }; } export const paymentApi = { health: (): Promise => fetch(`${PAYMENT_BASE}/health`).then((r) => r.json()), capabilities: (tenantId: string) => request("/capabilities", { tenantId }), metrics: (tenantId?: string) => tenantId ? request("/metrics", { tenantId }) : fetch(`${PAYMENT_BASE}/metrics`).then((r) => r.json()), openapi: async (): Promise> => { const r = await fetch(`${PAYMENT_BASE}/openapi.json`); if (!r.ok) throw new PaymentApiError(r.status, "openapi_error", "OpenAPI در دسترس نیست"); return r.json(); }, permissions: { catalog: (tenantId: string) => request("/api/v1/permissions/catalog", { tenantId }), }, audit: { list: (tenantId: string) => request("/api/v1/audit", { tenantId }), }, workspaces: crud("/api/v1/payment-workspaces"), bundleDefinitions: crud("/api/v1/bundle-definitions"), tenantBundles: crud("/api/v1/tenant-bundles"), featureToggles: crud("/api/v1/feature-toggles"), providerAssignments: crud("/api/v1/provider-assignments"), pspRegistrations: crud("/api/v1/psp-provider-registrations"), creditRegistrations: crud("/api/v1/credit-provider-registrations"), configurations: crud("/api/v1/configurations"), settings: crud("/api/v1/settings"), pspConnections: { ...crud("/api/v1/psp-connections"), test: (tenantId: string, id: string) => request>(`/api/v1/psp-connections/${id}/test`, { tenantId, method: "POST", body: "{}", }), }, pspRouting: crud("/api/v1/psp-routing-policies"), merchants: { list: (tenantId: string) => request("/api/v1/merchant-accounts", { tenantId }), get: (tenantId: string, id: string) => request(`/api/v1/merchant-accounts/${id}`, { tenantId }), create: (tenantId: string, body: Record) => request("/api/v1/merchant-accounts", { tenantId, method: "POST", body: JSON.stringify(body), }), submit: (tenantId: string, id: string) => request(`/api/v1/merchant-accounts/${id}/submit`, { tenantId, method: "POST", body: "{}" }), activate: (tenantId: string, id: string) => request(`/api/v1/merchant-accounts/${id}/activate`, { tenantId, method: "POST", body: "{}" }), suspend: (tenantId: string, id: string) => request(`/api/v1/merchant-accounts/${id}/suspend`, { tenantId, method: "POST", body: "{}" }), close: (tenantId: string, id: string) => request(`/api/v1/merchant-accounts/${id}/close`, { tenantId, method: "POST", body: "{}" }), }, settlementProfiles: crud("/api/v1/merchant-settlement-profiles"), requests: { list: (tenantId: string) => request("/api/v1/payment-requests", { tenantId }), get: (tenantId: string, id: string) => request(`/api/v1/payment-requests/${id}`, { tenantId }), create: (tenantId: string, body: Record, idempotencyKey: string) => request("/api/v1/payment-requests", { tenantId, method: "POST", body: JSON.stringify(body), idempotencyKey, }), cancel: (tenantId: string, id: string) => request(`/api/v1/payment-requests/${id}/cancel`, { tenantId, method: "POST", body: "{}" }), verify: (tenantId: string, id: string, body?: Record) => request(`/api/v1/payment-requests/${id}/verify`, { tenantId, method: "POST", body: JSON.stringify(body ?? { status: "paid" }), }), }, transactions: { list: (tenantId: string) => request("/api/v1/payment-transactions", { tenantId }), get: (tenantId: string, id: string) => request(`/api/v1/payment-transactions/${id}`, { tenantId }), bySource: (tenantId: string, service?: string, refId?: string) => { const sp = new URLSearchParams(); if (service) sp.set("service", service); if (refId) sp.set("ref_id", refId); const q = sp.toString(); return request(`/api/v1/payment-transactions/by-source${q ? `?${q}` : ""}`, { tenantId, }); }, }, ledger: { list: (tenantId: string) => request("/api/v1/payment-ledger-entries", { tenantId }), }, }; export const PAYMENT_API_CATALOG = [ { method: "GET", path: "/health", phase: "14.0" }, { method: "GET", path: "/capabilities", phase: "14.0" }, { method: "GET", path: "/metrics", phase: "14.0" }, { method: "GET|POST", path: "/api/v1/payment-workspaces", phase: "14.0" }, { method: "GET|POST", path: "/api/v1/tenant-bundles", phase: "14.0" }, { method: "GET|POST", path: "/api/v1/feature-toggles", phase: "14.0" }, { method: "GET|POST", path: "/api/v1/psp-connections", phase: "14.1" }, { method: "POST", path: "/api/v1/psp-connections/{id}/test", phase: "14.1" }, { method: "GET|POST", path: "/api/v1/merchant-accounts", phase: "14.2" }, { method: "POST", path: "/api/v1/payment-requests", phase: "14.3" }, { method: "POST", path: "/api/v1/payment-requests/{id}/verify", phase: "14.4" }, { method: "GET", path: "/api/v1/payment-transactions", phase: "14.5" }, { method: "GET", path: "/api/v1/payment-ledger-entries", phase: "14.5" }, ] as const;