/** * Communication Platform API — Torbat Communication (real backend only). * Browser → /api/communication BFF; SSR → direct upstream :8005 */ import { getValidAccessToken, refreshSession } from "@/lib/auth"; import type { CapabilitiesRawResponse, HealthResponse, MetricsResponse, MonitoringMetrics, MonitoringStats, CommunicationListParams, } from "@/modules/communication/types"; const COMM_BASE = typeof window !== "undefined" ? "/api/communication" : process.env.COMMUNICATION_SERVICE_URL || process.env.NEXT_PUBLIC_COMMUNICATION_API_URL || "http://localhost:8005"; export class CommunicationApiError extends Error { constructor( public status: number, public code: string, message: string ) { super(message); this.name = "CommunicationApiError"; } } 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) { let token = await getValidAccessToken(); if (!token) { token = await refreshSession({ force: true }); } if (!token) { throw new CommunicationApiError( 401, "unauthorized", "توکن احراز هویت یافت نشد. لطفاً دوباره وارد شوید." ); } headers.set("Authorization", `Bearer ${token}`); } const url = path.startsWith("http") ? path : `${COMM_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 CommunicationApiError( 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?: CommunicationListParams) { 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}` : ""; } export const communicationApi = { health: (): Promise => fetch(`${COMM_BASE}/health`).then((r) => r.json()), healthReady: (): Promise<{ status: string; database: string }> => fetch(`${COMM_BASE}/health/ready`).then((r) => r.json()), capabilities: (): Promise => fetch(`${COMM_BASE}/capabilities`).then((r) => r.json()), metrics: (): Promise => fetch(`${COMM_BASE}/metrics`).then((r) => r.json()), providers: { list: (tenantId: string) => request[]>("/api/v1/providers", { tenantId }), get: (tenantId: string, id: string) => request>(`/api/v1/providers/${id}`, { tenantId }), create: (tenantId: string, body: Record) => request>("/api/v1/providers", { tenantId, method: "POST", body: JSON.stringify(body), }), update: (tenantId: string, id: string, body: Record) => request>(`/api/v1/providers/${id}`, { tenantId, method: "PATCH", body: JSON.stringify(body), }), remove: (tenantId: string, id: string) => request(`/api/v1/providers/${id}`, { tenantId, method: "DELETE" }), status: (tenantId: string) => request[]>("/api/v1/providers/status", { tenantId }), balance: (tenantId: string, id: string) => request>(`/api/v1/providers/${id}/balance`, { tenantId }), healthCheck: (tenantId: string, id: string) => request>(`/api/v1/providers/${id}/health`, { tenantId, method: "POST", }), senders: { list: (tenantId: string) => request[]>("/api/v1/providers/senders/list", { tenantId }), create: (tenantId: string, body: Record) => request>("/api/v1/providers/senders", { tenantId, method: "POST", body: JSON.stringify(body), }), }, }, templates: { list: (tenantId: string) => request[]>("/api/v1/templates", { tenantId }), get: (tenantId: string, id: string) => request>(`/api/v1/templates/${id}`, { tenantId }), create: (tenantId: string, body: Record) => request>("/api/v1/templates", { tenantId, method: "POST", body: JSON.stringify(body), }), submit: (tenantId: string, id: string) => request>(`/api/v1/templates/${id}/submit`, { tenantId, method: "POST", }), approve: (tenantId: string, id: string) => request>(`/api/v1/templates/${id}/approve`, { tenantId, method: "POST", }), reject: (tenantId: string, id: string, reason?: string) => request>( `/api/v1/templates/${id}/reject${reason ? qs({ reason }) : ""}`, { tenantId, method: "POST" } ), preview: (tenantId: string, id: string, variables: Record) => request<{ rendered: string; subject?: string }>(`/api/v1/templates/${id}/preview`, { tenantId, method: "POST", body: JSON.stringify({ variables }), }), }, contacts: { manual: { list: (tenantId: string) => request[]>("/api/v1/contacts/manual", { tenantId }), create: (tenantId: string, body: Record) => request>("/api/v1/contacts/manual", { tenantId, method: "POST", body: JSON.stringify(body), }), importCsv: (tenantId: string, body: { csv_text: string }) => request[]>("/api/v1/contacts/manual/import-csv", { tenantId, method: "POST", body: JSON.stringify(body), }), }, sources: { list: (tenantId: string) => request[]>("/api/v1/contacts/sources", { tenantId }), create: (tenantId: string, body: Record) => request>("/api/v1/contacts/sources", { tenantId, method: "POST", body: JSON.stringify(body), }), }, resolve: (tenantId: string, body: Record) => request[]>("/api/v1/contacts/resolve", { tenantId, method: "POST", body: JSON.stringify(body), }), }, messages: { send: (tenantId: string, body: Record) => request[]>("/api/v1/messages/send", { tenantId, method: "POST", body: JSON.stringify(body), }), list: (tenantId: string, params?: CommunicationListParams) => request[]>(`/api/v1/messages${qs(params)}`, { tenantId }), get: (tenantId: string, id: string) => request>(`/api/v1/messages/${id}`, { tenantId }), stats: (tenantId: string) => request>("/api/v1/messages/stats", { tenantId }), timeline: (tenantId: string, id: string) => request[]>(`/api/v1/messages/${id}/timeline`, { tenantId }), cancel: (tenantId: string, id: string) => request>(`/api/v1/messages/${id}/cancel`, { tenantId, method: "POST", }), queue: { process: (tenantId: string, limit = 20) => request<{ processed: number }>(`/api/v1/messages/queue/process${qs({ limit })}`, { tenantId, method: "POST", }), deadLetters: (tenantId: string) => request[]>("/api/v1/messages/queue/dead-letters", { tenantId }), stats: (tenantId: string) => request>("/api/v1/messages/queue/stats", { tenantId }), }, }, otp: { request: (tenantId: string, body: Record) => request>("/api/v1/otp/request", { tenantId, method: "POST", body: JSON.stringify(body), }), verify: (tenantId: string, body: Record) => request>("/api/v1/otp/verify", { tenantId, method: "POST", body: JSON.stringify(body), }), }, monitoring: { stats: (tenantId: string) => request("/api/v1/monitoring/stats", { tenantId }), metrics: (tenantId: string) => request("/api/v1/monitoring/metrics", { tenantId }), }, }; export function normalizeCapabilities(raw: CapabilitiesRawResponse) { const features: Record = {}; for (const f of raw.features ?? []) features[f] = true; for (const ch of raw.channels ?? []) { features[`channel_${ch.channel}`] = ch.status === "active"; } return { ...raw, commercial_product: "Torbat Communication", phase: "8", features, }; }