Introduces modules/hospitality with 54 routes, BFF proxy, capability-gated nav, CRUD factory, and architecture validation docs. Co-authored-by: Cursor <cursoragent@cursor.com>
327 lines
14 KiB
JavaScript
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");
|