/** * Loyalty Platform Service API client — real backend only (Phase 7.0–7.6). * Browser → /api/loyalty BFF; SSR → direct upstream. */ import { getValidAccessToken, refreshSession } from "@/lib/auth"; import type { AuditEntry, Campaign, CampaignApplication, CampaignApplicationPage, HealthResponse, ListParams, Member, MembershipLifecycleEvent, MembershipTier, LoyaltyProgram, PointAccount, PointBalance, PointLedgerEntry, PointLedgerPage, ReferralAttribution, ReferralAttributionPage, ReferralCode, ReferralCodePage, ReferralProgram, Reward, RewardRedemption, RewardRedemptionPage, WalletAccount, WalletBalance, WalletLedgerEntry, WalletLedgerPage, WalletTransferResult, } from "@/modules/loyalty/types"; const LOYALTY_BASE = typeof window !== "undefined" ? "/api/loyalty" : process.env.LOYALTY_SERVICE_URL || process.env.NEXT_PUBLIC_LOYALTY_API_URL || "http://localhost:8004"; export class LoyaltyApiError extends Error { constructor( public status: number, public code: string, message: string ) { super(message); this.name = "LoyaltyApiError"; } } type RequestOptions = RequestInit & { tenantId: string; auth?: boolean }; async function request(path: string, options: RequestOptions): Promise { const { tenantId, auth = true, 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 (auth) { const token = await getValidAccessToken(); if (token) headers.set("Authorization", `Bearer ${token}`); } const url = path.startsWith("http") ? path : `${LOYALTY_BASE.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`; let res: Response; try { res = await fetch(url, { ...init, headers }); } catch (err) { const detail = err instanceof Error ? err.message : "network_error"; throw new Error(`اتصال به سرویس وفاداری برقرار نشد (${url}) — ${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 }); } } if (!res.ok) { const body = await res.json().catch(() => ({})); throw new LoyaltyApiError( res.status, body?.error?.code || body?.detail || "unknown_error", body?.error?.message || body?.detail || res.statusText ); } if (res.status === 204) return undefined as T; return res.json() as Promise; } function qs(params?: Record) { if (!params) return ""; const sp = new URLSearchParams(); Object.entries(params).forEach(([k, v]) => { if (v !== undefined && v !== null && v !== "") sp.set(k, String(v)); }); const s = sp.toString(); return s ? `?${s}` : ""; } function apiPath(segment: string) { return `/api/v1${segment.startsWith("/") ? segment : `/${segment}`}`; } const ZERO_UUID = "00000000-0000-0000-0000-000000000000"; export type LedgerListParams = { offset?: number; limit?: number; entry_type?: string; q?: string; }; export type WalletLedgerListParams = { offset?: number; limit?: number; entry_type?: string; }; export const loyaltyApi = { health: () => request("/health", { tenantId: ZERO_UUID, auth: false }), programs: { list: (tenantId: string, params?: ListParams) => request(`${apiPath("/programs")}${qs(params)}`, { tenantId }), get: (tenantId: string, id: string) => request(apiPath(`/programs/${id}`), { tenantId }), create: (tenantId: string, body: Record) => request(apiPath("/programs"), { tenantId, method: "POST", body: JSON.stringify(body), }), update: (tenantId: string, id: string, body: Record) => request(apiPath(`/programs/${id}`), { tenantId, method: "PATCH", body: JSON.stringify(body), }), delete: (tenantId: string, id: string) => request(apiPath(`/programs/${id}/delete`), { tenantId, method: "POST", }), }, tiers: { list: (tenantId: string, params?: ListParams) => request(`${apiPath("/tiers")}${qs(params)}`, { tenantId }), get: (tenantId: string, id: string) => request(apiPath(`/tiers/${id}`), { tenantId }), create: (tenantId: string, body: Record) => request(apiPath("/tiers"), { tenantId, method: "POST", body: JSON.stringify(body), }), update: (tenantId: string, id: string, body: Record) => request(apiPath(`/tiers/${id}`), { tenantId, method: "PATCH", body: JSON.stringify(body), }), delete: (tenantId: string, id: string) => request(apiPath(`/tiers/${id}/delete`), { tenantId, method: "POST", }), }, members: { list: (tenantId: string, params?: ListParams) => request(`${apiPath("/members")}${qs(params)}`, { tenantId }), get: (tenantId: string, id: string) => request(apiPath(`/members/${id}`), { tenantId }), create: (tenantId: string, body: Record) => request(apiPath("/members"), { tenantId, method: "POST", body: JSON.stringify(body), }), update: (tenantId: string, id: string, body: Record) => request(apiPath(`/members/${id}`), { tenantId, method: "PATCH", body: JSON.stringify(body), }), enroll: (tenantId: string, id: string, body: Record) => request(apiPath(`/members/${id}/enroll`), { tenantId, method: "POST", body: JSON.stringify(body), }), activate: (tenantId: string, id: string, body: Record) => request(apiPath(`/members/${id}/activate`), { tenantId, method: "POST", body: JSON.stringify(body), }), renew: (tenantId: string, id: string, body: Record) => request(apiPath(`/members/${id}/renew`), { tenantId, method: "POST", body: JSON.stringify(body), }), freeze: (tenantId: string, id: string, body: Record) => request(apiPath(`/members/${id}/freeze`), { tenantId, method: "POST", body: JSON.stringify(body), }), resume: (tenantId: string, id: string, body: Record) => request(apiPath(`/members/${id}/resume`), { tenantId, method: "POST", body: JSON.stringify(body), }), cancel: (tenantId: string, id: string, body: Record) => request(apiPath(`/members/${id}/cancel`), { tenantId, method: "POST", body: JSON.stringify(body), }), expire: (tenantId: string, id: string, body: Record) => request(apiPath(`/members/${id}/expire`), { tenantId, method: "POST", body: JSON.stringify(body), }), transfer: (tenantId: string, id: string, body: Record) => request(apiPath(`/members/${id}/transfer`), { tenantId, method: "POST", body: JSON.stringify(body), }), lifecycle: (tenantId: string, id: string, limit?: number) => request( `${apiPath(`/members/${id}/lifecycle`)}${qs({ limit })}`, { tenantId } ), delete: (tenantId: string, id: string) => request(apiPath(`/members/${id}/delete`), { tenantId, method: "POST" }), }, pointAccounts: { list: (tenantId: string, params?: ListParams) => request(`${apiPath("/point-accounts")}${qs(params)}`, { tenantId }), get: (tenantId: string, id: string) => request(apiPath(`/point-accounts/${id}`), { tenantId }), create: (tenantId: string, body: Record) => request(apiPath("/point-accounts"), { tenantId, method: "POST", body: JSON.stringify(body), }), update: (tenantId: string, id: string, body: Record) => request(apiPath(`/point-accounts/${id}`), { tenantId, method: "PATCH", body: JSON.stringify(body), }), delete: (tenantId: string, id: string) => request(apiPath(`/point-accounts/${id}/delete`), { tenantId, method: "POST", }), balance: (tenantId: string, id: string) => request(apiPath(`/point-accounts/${id}/balance`), { tenantId }), ledger: (tenantId: string, id: string, params?: LedgerListParams) => request(`${apiPath(`/point-accounts/${id}/ledger`)}${qs(params)}`, { tenantId, }), earn: (tenantId: string, id: string, body: Record) => request(apiPath(`/point-accounts/${id}/earn`), { tenantId, method: "POST", body: JSON.stringify(body), }), redeem: (tenantId: string, id: string, body: Record) => request(apiPath(`/point-accounts/${id}/redeem`), { tenantId, method: "POST", body: JSON.stringify(body), }), adjust: (tenantId: string, id: string, body: Record) => request(apiPath(`/point-accounts/${id}/adjust`), { tenantId, method: "POST", body: JSON.stringify(body), }), expirePoints: (tenantId: string, id: string, body: Record) => request(apiPath(`/point-accounts/${id}/expire-points`), { tenantId, method: "POST", body: JSON.stringify(body), }), }, rewards: { list: (tenantId: string, params?: ListParams) => request(`${apiPath("/rewards")}${qs(params)}`, { tenantId }), get: (tenantId: string, id: string) => request(apiPath(`/rewards/${id}`), { tenantId }), create: (tenantId: string, body: Record) => request(apiPath("/rewards"), { tenantId, method: "POST", body: JSON.stringify(body), }), update: (tenantId: string, id: string, body: Record) => request(apiPath(`/rewards/${id}`), { tenantId, method: "PATCH", body: JSON.stringify(body), }), delete: (tenantId: string, id: string) => request(apiPath(`/rewards/${id}/delete`), { tenantId, method: "POST" }), redeem: (tenantId: string, id: string, body: Record) => request(apiPath(`/rewards/${id}/redeem`), { tenantId, method: "POST", body: JSON.stringify(body), }), redemptions: (tenantId: string, id: string, params?: ListParams) => request( `${apiPath(`/rewards/${id}/redemptions`)}${qs(params)}`, { tenantId } ), }, redemptions: { list: (tenantId: string, params?: ListParams) => request(`${apiPath("/redemptions")}${qs(params)}`, { tenantId }), get: (tenantId: string, id: string) => request(apiPath(`/redemptions/${id}`), { tenantId }), fulfill: (tenantId: string, id: string) => request(apiPath(`/redemptions/${id}/fulfill`), { tenantId, method: "POST", }), cancel: (tenantId: string, id: string, body: Record) => request(apiPath(`/redemptions/${id}/cancel`), { tenantId, method: "POST", body: JSON.stringify(body), }), }, campaigns: { list: (tenantId: string, params?: ListParams) => request(`${apiPath("/campaigns")}${qs(params)}`, { tenantId }), get: (tenantId: string, id: string) => request(apiPath(`/campaigns/${id}`), { tenantId }), create: (tenantId: string, body: Record) => request(apiPath("/campaigns"), { tenantId, method: "POST", body: JSON.stringify(body), }), update: (tenantId: string, id: string, body: Record) => request(apiPath(`/campaigns/${id}`), { tenantId, method: "PATCH", body: JSON.stringify(body), }), delete: (tenantId: string, id: string) => request(apiPath(`/campaigns/${id}/delete`), { tenantId, method: "POST" }), schedule: (tenantId: string, id: string) => request(apiPath(`/campaigns/${id}/schedule`), { tenantId, method: "POST" }), activate: (tenantId: string, id: string) => request(apiPath(`/campaigns/${id}/activate`), { tenantId, method: "POST" }), pause: (tenantId: string, id: string) => request(apiPath(`/campaigns/${id}/pause`), { tenantId, method: "POST" }), resume: (tenantId: string, id: string) => request(apiPath(`/campaigns/${id}/resume`), { tenantId, method: "POST" }), expire: (tenantId: string, id: string) => request(apiPath(`/campaigns/${id}/expire`), { tenantId, method: "POST" }), rules: (tenantId: string, id: string, body: Record) => request(apiPath(`/campaigns/${id}/rules`), { tenantId, method: "POST", body: JSON.stringify(body), }), apply: (tenantId: string, id: string, body: Record) => request(apiPath(`/campaigns/${id}/apply`), { tenantId, method: "POST", body: JSON.stringify(body), }), applications: (tenantId: string, id: string, params?: ListParams) => request( `${apiPath(`/campaigns/${id}/applications`)}${qs(params)}`, { tenantId } ), }, referralPrograms: { list: (tenantId: string, params?: ListParams) => request(`${apiPath("/referral-programs")}${qs(params)}`, { tenantId }), get: (tenantId: string, id: string) => request(apiPath(`/referral-programs/${id}`), { tenantId }), create: (tenantId: string, body: Record) => request(apiPath("/referral-programs"), { tenantId, method: "POST", body: JSON.stringify(body), }), update: (tenantId: string, id: string, body: Record) => request(apiPath(`/referral-programs/${id}`), { tenantId, method: "PATCH", body: JSON.stringify(body), }), delete: (tenantId: string, id: string) => request(apiPath(`/referral-programs/${id}/delete`), { tenantId, method: "POST", }), }, referralCodes: { create: (tenantId: string, body: Record) => request(apiPath("/referral-codes"), { tenantId, method: "POST", body: JSON.stringify(body), }), list: (tenantId: string, params?: ListParams & { referral_program_id?: string }) => request(`${apiPath("/referral-codes")}${qs(params)}`, { tenantId }), get: (tenantId: string, id: string) => request(apiPath(`/referral-codes/${id}`), { tenantId }), revoke: (tenantId: string, id: string) => request(apiPath(`/referral-codes/${id}/revoke`), { tenantId, method: "POST", }), }, referrals: { attribute: (tenantId: string, body: Record) => request(apiPath("/referrals/attribute"), { tenantId, method: "POST", body: JSON.stringify(body), }), list: (tenantId: string, params?: ListParams & { referral_program_id?: string }) => request(`${apiPath("/referrals")}${qs(params)}`, { tenantId }), get: (tenantId: string, id: string) => request(apiPath(`/referrals/${id}`), { tenantId }), convert: (tenantId: string, id: string) => request(apiPath(`/referrals/${id}/convert`), { tenantId, method: "POST", }), reward: (tenantId: string, id: string) => request(apiPath(`/referrals/${id}/reward`), { tenantId, method: "POST", }), cancel: (tenantId: string, id: string, body: Record) => request(apiPath(`/referrals/${id}/cancel`), { tenantId, method: "POST", body: JSON.stringify(body), }), }, wallets: { list: (tenantId: string, params?: ListParams) => request(`${apiPath("/wallets")}${qs(params)}`, { tenantId }), get: (tenantId: string, id: string) => request(apiPath(`/wallets/${id}`), { tenantId }), create: (tenantId: string, body: Record) => request(apiPath("/wallets"), { tenantId, method: "POST", body: JSON.stringify(body), }), update: (tenantId: string, id: string, body: Record) => request(apiPath(`/wallets/${id}`), { tenantId, method: "PATCH", body: JSON.stringify(body), }), delete: (tenantId: string, id: string) => request(apiPath(`/wallets/${id}/delete`), { tenantId, method: "POST" }), balance: (tenantId: string, id: string) => request(apiPath(`/wallets/${id}/balance`), { tenantId }), ledger: (tenantId: string, id: string, params?: WalletLedgerListParams) => request(`${apiPath(`/wallets/${id}/ledger`)}${qs(params)}`, { tenantId }), credit: (tenantId: string, id: string, body: Record) => request(apiPath(`/wallets/${id}/credit`), { tenantId, method: "POST", body: JSON.stringify(body), }), debit: (tenantId: string, id: string, body: Record) => request(apiPath(`/wallets/${id}/debit`), { tenantId, method: "POST", body: JSON.stringify(body), }), adjust: (tenantId: string, id: string, body: Record) => request(apiPath(`/wallets/${id}/adjust`), { tenantId, method: "POST", body: JSON.stringify(body), }), transfer: (tenantId: string, id: string, body: Record) => request(apiPath(`/wallets/${id}/transfer`), { tenantId, method: "POST", body: JSON.stringify(body), }), }, audit: { list: ( tenantId: string, entityType: string, entityId: string, params?: { limit?: number } ) => request( `${apiPath("/audit")}${qs({ entity_type: entityType, entity_id: entityId, ...params })}`, { tenantId } ), }, }; /** Fetch all pages client-side (backend has no total count on plain list endpoints). */ export async function loyaltyFetchAll( fetchPage: (params: ListParams) => Promise, pageSize = 500 ): Promise { const all: T[] = []; let page = 1; for (;;) { const batch = await fetchPage({ page, page_size: pageSize }); all.push(...batch); if (batch.length < pageSize) break; page += 1; if (page > 50) break; } return all; }