/** * Sports Center Service API client — real backend only. * Browser → /api/sports-center BFF; SSR → direct upstream :8006 */ import { getValidAccessToken, refreshSession } from "@/lib/auth"; import type { CapabilitiesResponse, HealthResponse, ListParams } from "@/modules/sports-center/types"; const SPORTS_CENTER_BASE = typeof window !== "undefined" ? "/api/sports-center" : process.env.SPORTS_CENTER_SERVICE_URL || process.env.NEXT_PUBLIC_SPORTS_CENTER_API_URL || "http://localhost:8006"; export class SportsCenterApiError extends Error { constructor( public status: number, public code: string, message: string ) { super(message); this.name = "SportsCenterApiError"; } } 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 : `${SPORTS_CENTER_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 SportsCenterApiError( 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?: ListParams) { 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 action(tenantId: string, apiPath: string, body?: Record) { return request>(apiPath, { tenantId, method: "POST", body: body ? JSON.stringify(body) : undefined, }); } function crud(basePath: string, opts?: { upsert?: boolean }) { return { list: (tenantId: string, params?: ListParams) => request[]>(`${basePath}${qs(params)}`, { 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), }), remove: (tenantId: string, id: string) => action(tenantId, `${basePath}/${id}/delete`), upsert: opts?.upsert ? (tenantId: string, body: Record) => request>(basePath, { tenantId, method: "PUT", body: JSON.stringify(body), }) : undefined, }; } function createOnly(basePath: string) { return { list: (tenantId: string, params?: ListParams) => request[]>(`${basePath}${qs(params)}`, { tenantId }), create: (tenantId: string, body: Record) => request>(basePath, { tenantId, method: "POST", body: JSON.stringify(body), }), }; } function postOnly(basePath: string) { return { list: async (_tenantId: string, _params?: ListParams) => [] as Record[], create: (tenantId: string, body: Record) => request>(basePath, { tenantId, method: "POST", body: JSON.stringify(body), }), }; } function listOnly(basePath: string) { return { list: (tenantId: string, params?: ListParams) => request[]>(`${basePath}${qs(params)}`, { tenantId }), }; } export const sportsCenterApi = { health: (): Promise => fetch(`${SPORTS_CENTER_BASE}/health`).then((r) => r.json()), capabilities: (): Promise => fetch(`${SPORTS_CENTER_BASE}/capabilities`).then((r) => r.json()), // Foundation sportsCenters: crud("/api/v1/sports-centers"), branches: crud("/api/v1/branches"), sports: crud("/api/v1/sports"), membershipTypes: crud("/api/v1/membership-types"), memberships: { ...crud("/api/v1/memberships"), assignMember: (tenantId: string, id: string, body: Record) => action(tenantId, `/api/v1/memberships/${id}/assign-member`, body), activate: (tenantId: string, id: string, body?: Record) => action(tenantId, `/api/v1/memberships/${id}/activate`, body), suspend: (tenantId: string, id: string, body?: Record) => action(tenantId, `/api/v1/memberships/${id}/suspend`, body), cancel: (tenantId: string, id: string, body?: Record) => action(tenantId, `/api/v1/memberships/${id}/cancel`, body), freeze: (tenantId: string, id: string, body: Record) => action(tenantId, `/api/v1/memberships/${id}/freeze`, body), unfreeze: (tenantId: string, id: string, body?: Record) => action(tenantId, `/api/v1/memberships/${id}/unfreeze`, body), }, coaches: crud("/api/v1/coaches"), roles: crud("/api/v1/roles"), permissions: crud("/api/v1/permissions"), facilities: crud("/api/v1/facilities"), courts: crud("/api/v1/courts"), rooms: crud("/api/v1/rooms"), lockerRooms: crud("/api/v1/locker-rooms"), lockers: { ...crud("/api/v1/lockers"), assign: (tenantId: string, id: string, body: Record) => action(tenantId, `/api/v1/lockers/${id}/assign`, body), release: (tenantId: string, id: string, body?: Record) => action(tenantId, `/api/v1/lockers/${id}/release`, body), }, deviceProviders: crud("/api/v1/device-providers"), devices: { ...crud("/api/v1/devices"), connect: (tenantId: string, id: string) => action(tenantId, `/api/v1/devices/${id}/connect`), disconnect: (tenantId: string, id: string) => action(tenantId, `/api/v1/devices/${id}/disconnect`), }, attendanceGateways: crud("/api/v1/attendance-gateways"), configurations: crud("/api/v1/configurations"), events: crud("/api/v1/events"), settings: { list: (tenantId: string, params?: ListParams) => request[]>(`/api/v1/settings${qs(params)}`, { tenantId }), upsert: (tenantId: string, body: Record) => request>("/api/v1/settings", { tenantId, method: "POST", body: JSON.stringify(body), }), remove: (tenantId: string, id: string) => action(tenantId, `/api/v1/settings/${id}/delete`), }, // Catalog sportCategories: crud("/api/v1/sport-categories"), ageGroups: crud("/api/v1/age-groups"), pricingModels: crud("/api/v1/pricing-models"), membershipPackages: crud("/api/v1/membership-packages"), membershipPlans: crud("/api/v1/membership-plans"), membershipRules: crud("/api/v1/membership-rules"), renewalPolicies: crud("/api/v1/renewal-policies"), freezingRules: crud("/api/v1/freezing-rules"), expirationPolicies: crud("/api/v1/expiration-policies"), // Members members: crud("/api/v1/members"), familyMembers: crud("/api/v1/family-members"), emergencyContacts: crud("/api/v1/emergency-contacts"), medicalInformation: { upsert: (tenantId: string, body: Record) => request>("/api/v1/medical-information", { tenantId, method: "PUT", body: JSON.stringify(body), }), byMember: (tenantId: string, memberId: string) => request>(`/api/v1/medical-information/by-member/${memberId}`, { tenantId }), list: (tenantId: string, params?: ListParams) => request[]>(`/api/v1/members${qs(params)}`, { tenantId }), create: (tenantId: string, body: Record) => request>("/api/v1/medical-information", { tenantId, method: "PUT", body: JSON.stringify(body), }), }, membershipCards: { ...createOnly("/api/v1/membership-cards"), revoke: (tenantId: string, id: string) => action(tenantId, `/api/v1/membership-cards/${id}/revoke`), }, digitalMemberships: { list: (tenantId: string, params?: ListParams) => request[]>(`/api/v1/digital-memberships${qs(params)}`, { tenantId }), create: (tenantId: string, body: Record) => request>("/api/v1/digital-memberships", { tenantId, method: "POST", body: JSON.stringify(body), }), update: (tenantId: string, id: string, body: Record) => request>(`/api/v1/digital-memberships/${id}`, { tenantId, method: "PATCH", body: JSON.stringify(body), }), }, waivers: { ...createOnly("/api/v1/waivers"), sign: (tenantId: string, id: string, body?: Record) => action(tenantId, `/api/v1/waivers/${id}/sign`, body), }, memberDocuments: createOnly("/api/v1/member-documents"), // Staff staffCertificates: createOnly("/api/v1/staff-certificates"), staffSkills: createOnly("/api/v1/staff-skills"), staffWorkingHours: createOnly("/api/v1/staff-working-hours"), staffAvailability: createOnly("/api/v1/staff-availability"), staffAssignments: createOnly("/api/v1/staff-assignments"), // Booking equipment: createOnly("/api/v1/equipment"), sessions: createOnly("/api/v1/sessions"), bookings: { create: (tenantId: string, body: Record) => request>("/api/v1/bookings", { tenantId, method: "POST", body: JSON.stringify(body), }), confirm: (tenantId: string, id: string) => action(tenantId, `/api/v1/bookings/${id}/confirm`), cancel: (tenantId: string, id: string, body?: Record) => action(tenantId, `/api/v1/bookings/${id}/cancel`, body), list: (tenantId: string, params?: ListParams) => request[]>(`/api/v1/sessions${qs(params)}`, { tenantId }), }, waitingList: createOnly("/api/v1/waiting-list"), resourceReservations: createOnly("/api/v1/resource-reservations"), // Attendance checkIn: (tenantId: string, body: Record) => action(tenantId, "/api/v1/check-in", body), checkOut: (tenantId: string, body: Record) => action(tenantId, "/api/v1/check-out", body), attendanceRecords: createOnly("/api/v1/attendance-records"), accessLogs: listOnly("/api/v1/access-logs"), // Training trainingPrograms: createOnly("/api/v1/training-programs"), exercises: postOnly("/api/v1/exercises"), workoutPlans: postOnly("/api/v1/workout-plans"), workoutAssignments: postOnly("/api/v1/workout-assignments"), trainingGoals: postOnly("/api/v1/training-goals"), measurements: postOnly("/api/v1/measurements"), progressRecords: postOnly("/api/v1/progress-records"), nutritionPlans: postOnly("/api/v1/nutrition-plans"), assessments: postOnly("/api/v1/assessments"), performanceRecords: postOnly("/api/v1/performance-records"), bodyCompositions: postOnly("/api/v1/body-compositions"), // Competitions competitions: { list: (tenantId: string, params?: ListParams) => request[]>(`/api/v1/competitions${qs(params)}`, { tenantId }), create: (tenantId: string, body: Record) => request>("/api/v1/competitions", { tenantId, method: "POST", body: JSON.stringify(body), }), update: (tenantId: string, id: string, body: Record) => request>(`/api/v1/competitions/${id}`, { tenantId, method: "PATCH", body: JSON.stringify(body), }), }, competitionTeams: createOnly("/api/v1/competition-teams"), competitionGroups: createOnly("/api/v1/competition-groups"), competitionRegistrations: { create: (tenantId: string, body: Record) => request>("/api/v1/competition-registrations", { tenantId, method: "POST", body: JSON.stringify(body), }), confirm: (tenantId: string, id: string) => action(tenantId, `/api/v1/competition-registrations/${id}/confirm`), list: (tenantId: string, params?: ListParams) => request[]>(`/api/v1/competition-registrations${qs(params)}`, { tenantId }), }, competitionMatches: createOnly("/api/v1/competition-matches"), competitionResults: createOnly("/api/v1/competition-results"), competitionRankings: listOnly("/api/v1/competition-rankings"), competitionMedals: createOnly("/api/v1/competition-medals"), competitionJudges: createOnly("/api/v1/competition-judges"), competitionCertificates: createOnly("/api/v1/competition-certificates"), };