TorbatYar/frontend/modules/delivery/services/delivery-api.ts
Mortezakoohjani 7978970783 feat(delivery): add Torbat Driver frontend module with real API wiring.
Ship the delivery platform UI under modules/delivery with thin app routes, BFF proxy, CRUD for phase 10.0-10.1 APIs, and capability-gated future screens.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 12:02:01 +03:30

248 lines
8.6 KiB
TypeScript

/**
* Delivery Platform API client — Torbat Driver (real backend only).
* Browser → /api/delivery BFF; SSR → direct upstream :8007
*/
import { getValidAccessToken, refreshSession } from "@/lib/auth";
import type {
CapabilitiesResponse,
DriverListResponse,
HealthResponse,
ListParams,
MetricsResponse,
PaginatedList,
PermissionsCatalogResponse,
} from "@/modules/delivery/types";
const DELIVERY_BASE =
typeof window !== "undefined"
? "/api/delivery"
: process.env.DELIVERY_SERVICE_URL ||
process.env.NEXT_PUBLIC_DELIVERY_API_URL ||
"http://localhost:8007";
export class DeliveryApiError extends Error {
constructor(
public status: number,
public code: string,
message: string
) {
super(message);
this.name = "DeliveryApiError";
}
}
type RequestOptions = RequestInit & { tenantId: string; auth?: boolean };
async function request<T>(path: string, options: RequestOptions): Promise<T> {
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
: `${DELIVERY_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 DeliveryApiError(
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<T>;
}
function qs(params?: Record<string, string | number | boolean | undefined | null>) {
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 crudResource(basePath: string) {
return {
list: (tenantId: string, params?: ListParams) =>
request<Record<string, unknown>[]>(`${basePath}${qs(params)}`, { tenantId }),
get: (tenantId: string, id: string) =>
request<Record<string, unknown>>(`${basePath}/${id}`, { tenantId }),
create: (tenantId: string, body: Record<string, unknown>) =>
request<Record<string, unknown>>(basePath, {
tenantId,
method: "POST",
body: JSON.stringify(body),
}),
update: (tenantId: string, id: string, body: Record<string, unknown>) =>
request<Record<string, unknown>>(`${basePath}/${id}`, {
tenantId,
method: "PATCH",
body: JSON.stringify(body),
}),
remove: (tenantId: string, id: string) =>
request<Record<string, unknown>>(`${basePath}/${id}/delete`, {
tenantId,
method: "POST",
}),
};
}
export const deliveryApi = {
health: (): Promise<HealthResponse> =>
fetch(`${DELIVERY_BASE}/health`).then((r) => r.json()),
capabilities: (): Promise<CapabilitiesResponse> =>
fetch(`${DELIVERY_BASE}/capabilities`).then((r) => r.json()),
metrics: (): Promise<MetricsResponse> =>
fetch(`${DELIVERY_BASE}/metrics`).then((r) => r.json()),
permissions: {
catalog: (tenantId: string) =>
request<PermissionsCatalogResponse>(`/api/v1/permissions/catalog`, { tenantId }),
},
organizations: crudResource("/api/v1/organizations"),
hubs: crudResource("/api/v1/hubs"),
externalProviders: crudResource("/api/v1/external-providers"),
routingEngines: crudResource("/api/v1/routing-engines"),
configurations: crudResource("/api/v1/configurations"),
settings: {
list: (tenantId: string, params?: ListParams) =>
request<Record<string, unknown>[]>(`/api/v1/settings${qs(params)}`, { tenantId }),
upsert: (tenantId: string, body: Record<string, unknown>) =>
request<Record<string, unknown>>(`/api/v1/settings`, {
tenantId,
method: "PUT",
body: JSON.stringify(body),
}),
},
audit: {
list: (
tenantId: string,
params: { entity_type: string; entity_id: string; limit?: number }
) => request<Record<string, unknown>[]>(`/api/v1/audit${qs(params)}`, { tenantId }),
},
drivers: {
list: (tenantId: string, params?: ListParams) =>
request<DriverListResponse | Record<string, unknown>[]>(
`/api/v1/drivers${qs(params)}`,
{ tenantId }
).then(normalizeDriverList),
get: (tenantId: string, id: string) =>
request<Record<string, unknown>>(`/api/v1/drivers/${id}`, { tenantId }),
create: (tenantId: string, body: Record<string, unknown>) =>
request<Record<string, unknown>>(`/api/v1/drivers`, {
tenantId,
method: "POST",
body: JSON.stringify(body),
}),
update: (tenantId: string, id: string, body: Record<string, unknown>) =>
request<Record<string, unknown>>(`/api/v1/drivers/${id}`, {
tenantId,
method: "PATCH",
body: JSON.stringify(body),
}),
remove: (tenantId: string, id: string) =>
request<Record<string, unknown>>(`/api/v1/drivers/${id}/delete`, {
tenantId,
method: "POST",
}),
activate: (tenantId: string, id: string, body: Record<string, unknown>) =>
lifecycleAction(tenantId, id, "activate", body),
suspend: (tenantId: string, id: string, body: Record<string, unknown>) =>
lifecycleAction(tenantId, id, "suspend", body),
resume: (tenantId: string, id: string, body: Record<string, unknown>) =>
lifecycleAction(tenantId, id, "resume", body),
deactivate: (tenantId: string, id: string, body: Record<string, unknown>) =>
lifecycleAction(tenantId, id, "deactivate", body),
block: (tenantId: string, id: string, body: Record<string, unknown>) =>
lifecycleAction(tenantId, id, "block", body),
unblock: (tenantId: string, id: string, body: Record<string, unknown>) =>
lifecycleAction(tenantId, id, "unblock", body),
archive: (tenantId: string, id: string, body: Record<string, unknown>) =>
lifecycleAction(tenantId, id, "archive", body),
lifecycle: (tenantId: string, id: string) =>
request<Record<string, unknown>[]>(`/api/v1/drivers/${id}/lifecycle`, { tenantId }),
credentials: {
list: (tenantId: string, driverId: string) =>
request<Record<string, unknown>[]>(`/api/v1/drivers/${driverId}/credentials`, {
tenantId,
}),
create: (tenantId: string, driverId: string, body: Record<string, unknown>) =>
request<Record<string, unknown>>(`/api/v1/drivers/${driverId}/credentials`, {
tenantId,
method: "POST",
body: JSON.stringify(body),
}),
},
documents: {
list: (tenantId: string, driverId: string) =>
request<Record<string, unknown>[]>(`/api/v1/drivers/${driverId}/documents`, { tenantId }),
create: (tenantId: string, driverId: string, body: Record<string, unknown>) =>
request<Record<string, unknown>>(`/api/v1/drivers/${driverId}/documents`, {
tenantId,
method: "POST",
body: JSON.stringify(body),
}),
},
},
};
function lifecycleAction(
tenantId: string,
id: string,
action: string,
body: Record<string, unknown>
) {
return request<Record<string, unknown>>(`/api/v1/drivers/${id}/${action}`, {
tenantId,
method: "POST",
body: JSON.stringify(body),
});
}
function normalizeDriverList(
data: DriverListResponse | Record<string, unknown>[]
): PaginatedList<Record<string, unknown>> {
if (Array.isArray(data)) {
return { items: data, total: data.length, page: 1, page_size: data.length };
}
return {
items: data.items ?? [],
total: data.total ?? 0,
page: data.page ?? 1,
page_size: data.page_size ?? 20,
};
}