TorbatYar/frontend/scripts/gen-hospitality-api.mjs
Mortezakoohjani 065c053c16 Add complete Torbat Food hospitality frontend connected to backend APIs.
Introduces modules/hospitality with 54 routes, BFF proxy, capability-gated nav, CRUD factory, and architecture validation docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-26 20:54:26 +03:30

327 lines
14 KiB
JavaScript

#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
const RESOURCES = [
["venues", true, true],
["branches", true, true],
["dining-areas", false, false],
["tables", false, false],
["menus", true, true],
["menu-categories", false, false],
["menu-items", false, false],
["allergens", false, false],
["modifier-groups", false, false],
["modifier-options", false, false],
["availability-windows", false, false],
["menu-media-refs", false, false],
["qr-codes", false, false],
["qr-menu-sessions", false, false],
["carts", false, false],
["cart-lines", false, false],
["pos-registers", false, false],
["pos-ticket-lines", false, false],
["pos-payments", false, false],
["pos-discounts", false, false],
["pos-tax-lines", false, false],
["pos-floor-plans", false, false],
["pos-stations", false, false],
["kitchen-stations", false, false],
["connector-registrations", false, false],
["connector-dispatches", false, false],
["analytics-reports", false, false],
["bundle-definitions", false, false],
["roles", false, false],
["permissions", false, false],
["configurations", true, false],
["events", false, false],
];
function kebabToProp(s) {
return s.split("-").map((p, i) => (i === 0 ? p : p[0].toUpperCase() + p.slice(1))).join("");
}
let apiBlocks = "";
for (const [p, hasUpdate, hasDelete] of RESOURCES) {
const prop = kebabToProp(p);
let block = ` ${prop}: {
list: (tenantId: string, params?: ListParams) =>
request<Record<string, unknown>[]>(\`/api/v1/${p}\${qs(params)}\`, { tenantId }),
get: (tenantId: string, id: string) =>
request<Record<string, unknown>>(\`/api/v1/${p}/\${id}\`, { tenantId }),
create: (tenantId: string, body: Record<string, unknown>) =>
request<Record<string, unknown>>(\`/api/v1/${p}\`, {
tenantId,
method: "POST",
body: JSON.stringify(body),
}),`;
if (hasUpdate) {
block += `
update: (tenantId: string, id: string, body: Record<string, unknown>) =>
request<Record<string, unknown>>(\`/api/v1/${p}/\${id}\`, {
tenantId,
method: "PATCH",
body: JSON.stringify(body),
}),`;
}
if (hasDelete) {
block += `
remove: (tenantId: string, id: string) =>
request<Record<string, unknown>>(\`/api/v1/${p}/\${id}/delete\`, { tenantId, method: "POST" }),`;
}
block += `
},`;
apiBlocks += block + "\n";
}
const header = `/**
* Hospitality Service API client — Torbat Food (real backend only).
* Browser → /api/hospitality BFF; SSR → direct upstream :8009
*/
import { getValidAccessToken, refreshSession } from "@/lib/auth";
import type { CapabilitiesResponse, HealthResponse, ListParams } from "@/modules/hospitality/types";
const HOSPITALITY_BASE =
typeof window !== "undefined"
? "/api/hospitality"
: process.env.HOSPITALITY_SERVICE_URL ||
process.env.NEXT_PUBLIC_HOSPITALITY_API_URL ||
"http://localhost:8009";
export class HospitalityApiError extends Error {
constructor(
public status: number,
public code: string,
message: string
) {
super(message);
this.name = "HospitalityApiError";
}
}
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
: \`\${HOSPITALITY_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 HospitalityApiError(
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 | 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 action(tenantId: string, apiPath: string, body?: Record<string, unknown>) {
return request<Record<string, unknown>>(apiPath, {
tenantId,
method: "POST",
body: body ? JSON.stringify(body) : undefined,
});
}
export const hospitalityApi = {
health: (): Promise<HealthResponse> =>
fetch(\`\${HOSPITALITY_BASE}/health\`).then((r) => r.json()),
capabilities: (): Promise<CapabilitiesResponse> =>
fetch(\`\${HOSPITALITY_BASE}/capabilities\`).then((r) => r.json()),
${apiBlocks}
tenantBundles: {
list: (tenantId: string, params?: ListParams) =>
request<Record<string, unknown>[]>(\`/api/v1/tenant-bundles\${qs(params)}\`, { tenantId }),
get: (tenantId: string, id: string) =>
request<Record<string, unknown>>(\`/api/v1/tenant-bundles/\${id}\`, { tenantId }),
activate: (tenantId: string, body: Record<string, unknown>) =>
action(tenantId, "/api/v1/tenant-bundles/activate", body),
deactivate: (tenantId: string, id: string) =>
action(tenantId, \`/api/v1/tenant-bundles/\${id}/deactivate\`),
},
featureToggles: {
list: (tenantId: string, params?: ListParams) =>
request<Record<string, unknown>[]>(\`/api/v1/feature-toggles\${qs(params)}\`, { tenantId }),
get: (tenantId: string, id: string) =>
request<Record<string, unknown>>(\`/api/v1/feature-toggles/\${id}\`, { tenantId }),
upsert: (tenantId: string, body: Record<string, unknown>) =>
action(tenantId, "/api/v1/feature-toggles/upsert", body),
},
menuItemModifiers: {
list: (tenantId: string, params?: ListParams) =>
request<Record<string, unknown>[]>(\`/api/v1/menu-item-modifiers\${qs(params)}\`, { tenantId }),
link: (tenantId: string, body: Record<string, unknown>) =>
action(tenantId, "/api/v1/menu-item-modifiers/link", body),
},
menuItemAllergens: {
list: (tenantId: string, params?: ListParams) =>
request<Record<string, unknown>[]>(\`/api/v1/menu-item-allergens\${qs(params)}\`, { tenantId }),
link: (tenantId: string, body: Record<string, unknown>) =>
action(tenantId, "/api/v1/menu-item-allergens/link", body),
},
menuLocalizations: {
list: (tenantId: string, params?: ListParams) =>
request<Record<string, unknown>[]>(\`/api/v1/menu-localizations\${qs(params)}\`, { tenantId }),
get: (tenantId: string, id: string) =>
request<Record<string, unknown>>(\`/api/v1/menu-localizations/\${id}\`, { tenantId }),
upsert: (tenantId: string, body: Record<string, unknown>) =>
action(tenantId, "/api/v1/menu-localizations/upsert", body),
},
settings: {
list: (tenantId: string, params?: ListParams) =>
request<Record<string, unknown>[]>(\`/api/v1/settings\${qs(params)}\`, { tenantId }),
get: (tenantId: string, id: string) =>
request<Record<string, unknown>>(\`/api/v1/settings/\${id}\`, { tenantId }),
upsert: (tenantId: string, body: Record<string, unknown>) =>
action(tenantId, "/api/v1/settings/upsert", body),
},
reservations: {
list: (tenantId: string, params?: ListParams) =>
request<Record<string, unknown>[]>(\`/api/v1/reservations\${qs(params)}\`, { tenantId }),
get: (tenantId: string, id: string) =>
request<Record<string, unknown>>(\`/api/v1/reservations/\${id}\`, { tenantId }),
create: (tenantId: string, body: Record<string, unknown>) =>
action(tenantId, "/api/v1/reservations", body),
updateStatus: (tenantId: string, id: string, body: Record<string, unknown>) =>
action(tenantId, \`/api/v1/reservations/\${id}/status\`, body),
},
tableAssignments: {
list: (tenantId: string, params?: ListParams) =>
request<Record<string, unknown>[]>(\`/api/v1/table-assignments\${qs(params)}\`, { tenantId }),
get: (tenantId: string, id: string) =>
request<Record<string, unknown>>(\`/api/v1/table-assignments/\${id}\`, { tenantId }),
create: (tenantId: string, body: Record<string, unknown>) =>
action(tenantId, "/api/v1/table-assignments", body),
release: (tenantId: string, id: string) =>
action(tenantId, \`/api/v1/table-assignments/\${id}/release\`),
},
serviceRequests: {
list: (tenantId: string, params?: ListParams) =>
request<Record<string, unknown>[]>(\`/api/v1/service-requests\${qs(params)}\`, { tenantId }),
get: (tenantId: string, id: string) =>
request<Record<string, unknown>>(\`/api/v1/service-requests/\${id}\`, { tenantId }),
create: (tenantId: string, body: Record<string, unknown>) =>
action(tenantId, "/api/v1/service-requests", body),
updateStatus: (tenantId: string, id: string, body: Record<string, unknown>) =>
action(tenantId, \`/api/v1/service-requests/\${id}/status\`, body),
},
waitlist: {
list: (tenantId: string, params?: ListParams) =>
request<Record<string, unknown>[]>(\`/api/v1/waitlist\${qs(params)}\`, { tenantId }),
get: (tenantId: string, id: string) =>
request<Record<string, unknown>>(\`/api/v1/waitlist/\${id}\`, { tenantId }),
create: (tenantId: string, body: Record<string, unknown>) =>
action(tenantId, "/api/v1/waitlist", body),
updateStatus: (tenantId: string, id: string, body: Record<string, unknown>) =>
action(tenantId, \`/api/v1/waitlist/\${id}/status\`, body),
},
posShifts: {
list: (tenantId: string, params?: ListParams) =>
request<Record<string, unknown>[]>(\`/api/v1/pos-shifts\${qs(params)}\`, { tenantId }),
get: (tenantId: string, id: string) =>
request<Record<string, unknown>>(\`/api/v1/pos-shifts/\${id}\`, { tenantId }),
create: (tenantId: string, body: Record<string, unknown>) =>
action(tenantId, "/api/v1/pos-shifts", body),
close: (tenantId: string, id: string) =>
action(tenantId, \`/api/v1/pos-shifts/\${id}/close\`),
},
posTickets: {
list: (tenantId: string, params?: ListParams) =>
request<Record<string, unknown>[]>(\`/api/v1/pos-tickets\${qs(params)}\`, { tenantId }),
get: (tenantId: string, id: string) =>
request<Record<string, unknown>>(\`/api/v1/pos-tickets/\${id}\`, { tenantId }),
create: (tenantId: string, body: Record<string, unknown>) =>
action(tenantId, "/api/v1/pos-tickets", body),
void: (tenantId: string, id: string) =>
action(tenantId, \`/api/v1/pos-tickets/\${id}/void\`),
},
qrOrderingSessions: {
list: (tenantId: string, params?: ListParams) =>
request<Record<string, unknown>[]>(\`/api/v1/qr-ordering-sessions\${qs(params)}\`, { tenantId }),
get: (tenantId: string, id: string) =>
request<Record<string, unknown>>(\`/api/v1/qr-ordering-sessions/\${id}\`, { tenantId }),
create: (tenantId: string, body: Record<string, unknown>) =>
action(tenantId, "/api/v1/qr-ordering-sessions", body),
submit: (tenantId: string, id: string) =>
action(tenantId, \`/api/v1/qr-ordering-sessions/\${id}/submit\`),
},
kitchenTickets: {
list: (tenantId: string, params?: ListParams) =>
request<Record<string, unknown>[]>(\`/api/v1/kitchen-tickets\${qs(params)}\`, { tenantId }),
get: (tenantId: string, id: string) =>
request<Record<string, unknown>>(\`/api/v1/kitchen-tickets/\${id}\`, { tenantId }),
create: (tenantId: string, body: Record<string, unknown>) =>
action(tenantId, "/api/v1/kitchen-tickets", body),
updateStatus: (tenantId: string, id: string, body: Record<string, unknown>) =>
action(tenantId, \`/api/v1/kitchen-tickets/\${id}/status\`, body),
},
kitchenTicketItems: {
list: (tenantId: string, params?: ListParams) =>
request<Record<string, unknown>[]>(\`/api/v1/kitchen-ticket-items\${qs(params)}\`, { tenantId }),
get: (tenantId: string, id: string) =>
request<Record<string, unknown>>(\`/api/v1/kitchen-ticket-items/\${id}\`, { tenantId }),
create: (tenantId: string, body: Record<string, unknown>) =>
action(tenantId, "/api/v1/kitchen-ticket-items", body),
updateStatus: (tenantId: string, id: string, body: Record<string, unknown>) =>
action(tenantId, \`/api/v1/kitchen-ticket-items/\${id}/status\`, body),
},
analyticsSnapshots: {
list: (tenantId: string, params?: ListParams) =>
request<Record<string, unknown>[]>(\`/api/v1/analytics-snapshots\${qs(params)}\`, { tenantId }),
get: (tenantId: string, id: string) =>
request<Record<string, unknown>>(\`/api/v1/analytics-snapshots/\${id}\`, { tenantId }),
refresh: (tenantId: string, body: Record<string, unknown>) =>
action(tenantId, "/api/v1/analytics-snapshots/refresh", body),
},
};
`;
fs.writeFileSync(path.join(ROOT, "modules/hospitality/services/hospitality-api.ts"), header);
console.log("Generated hospitality-api.ts");