#!/usr/bin/env node /** * Scaffold Torbat Pay frontend module (MVP payment-14.0–14.5). * Run: node scripts/scaffold-payment-module.mjs */ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.resolve(__dirname, ".."); const MOD = path.join(ROOT, "modules", "payment"); const APP = path.join(ROOT, "app", "payment"); function write(rel, content) { const full = path.isAbsolute(rel) ? rel : path.join(ROOT, rel); fs.mkdirSync(path.dirname(full), { recursive: true }); fs.writeFileSync(full, content.replace(/\r\n/g, "\n"), "utf8"); console.log("write", path.relative(ROOT, full)); } const ROUTES = [ { route: "hub", feature: "hub", title: "هاب تربت‌پی" }, { route: "dashboard", feature: "dashboard", title: "داشبورد اجرایی" }, { route: "overview", feature: "overview", title: "نمای کلی پرداخت" }, { route: "kpis", feature: "kpis", title: "شاخص‌های کلیدی" }, { route: "transactions", feature: "transactions", title: "تراکنش‌های اخیر" }, { route: "transactions/[id]", feature: "transactionDetail", title: "جزئیات تراکنش" }, { route: "ledger", feature: "ledger", title: "دفتر کل پرداخت" }, { route: "psp", feature: "psp", title: "مدیریت درگاه‌ها" }, { route: "psp/health", feature: "gatewayHealth", title: "سلامت درگاه" }, { route: "providers", feature: "providers", title: "تخصیص ارائه‌دهنده" }, { route: "merchants", feature: "merchants", title: "حساب‌های پذیرنده" }, { route: "merchants/[id]", feature: "merchantDetail", title: "جزئیات پذیرنده" }, { route: "merchants/approvals", feature: "merchantApprovals", title: "تأیید پذیرنده" }, { route: "requests", feature: "requests", title: "درخواست‌های پرداخت" }, { route: "requests/[id]", feature: "requestDetail", title: "جزئیات درخواست / چک‌اوت" }, { route: "callbacks", feature: "callbacks", title: "مانیتور کال‌بک" }, { route: "verification", feature: "verification", title: "مانیتور تأیید" }, { route: "audit", feature: "audit", title: "لاگ حسابرسی" }, { route: "bundles", feature: "bundles", title: "باندل‌ها" }, { route: "feature-toggles", feature: "featureToggles", title: "کلیدهای ویژگی" }, { route: "capabilities", feature: "capabilities", title: "قابلیت‌ها" }, { route: "settings", feature: "settings", title: "تنظیمات پرداخت" }, { route: "metrics", feature: "metrics", title: "متریک‌ها" }, { route: "monitoring", feature: "monitoring", title: "مانیتورینگ" }, { route: "api-explorer", feature: "apiExplorer", title: "کاوشگر API" }, { route: "openapi", feature: "openapi", title: "مشاهده OpenAPI" }, ]; // --- core module files --- write("modules/payment/types/index.ts", `export type PaymentRecord = Record & { id?: string; status?: string }; export type HealthResponse = { status: string; service: string; version: string }; export type CapabilitiesResponse = { service: string; version: string; phase: string; workspace_status?: string; default_payment_mode?: string; active_bundles?: string[]; feature_toggles?: Record; payment_sources_supported?: string[]; payment_sources_reserved?: string[]; contract_versions?: Record; }; export type MetricsResponse = { service: string; version: string; phase: string; metrics: Record; }; export type PermissionsCatalogResponse = { permissions: string[] }; `); write("modules/payment/constants/permissions.ts", `export const PAYMENT_VIEW = "payment.view"; export const PAYMENT_MANAGE = "payment.manage"; export const PAYMENT_AUDIT_VIEW = "payment.audit.view"; export const PAYMENT_SETTINGS_MANAGE = "payment.settings.manage"; export const PAYMENT_PSP_VIEW = "payment.psp_connections.view"; export const PAYMENT_PSP_MANAGE = "payment.psp_connections.manage"; export const PAYMENT_MERCHANT_VIEW = "payment.merchant_accounts.view"; export const PAYMENT_MERCHANT_MANAGE = "payment.merchant_accounts.manage"; export const PAYMENT_REQUEST_VIEW = "payment.requests.view"; export const PAYMENT_REQUEST_CREATE = "payment.requests.create"; export const PAYMENT_TX_VIEW = "payment.transactions.view"; export const PAYMENT_LEDGER_VIEW = "payment.ledger.view"; export const ALL_PAYMENT_PERMISSIONS = [ PAYMENT_VIEW, PAYMENT_MANAGE, PAYMENT_AUDIT_VIEW, PAYMENT_SETTINGS_MANAGE, PAYMENT_PSP_VIEW, PAYMENT_PSP_MANAGE, PAYMENT_MERCHANT_VIEW, PAYMENT_MERCHANT_MANAGE, PAYMENT_REQUEST_VIEW, PAYMENT_REQUEST_CREATE, PAYMENT_TX_VIEW, PAYMENT_LEDGER_VIEW, ] as const; `); write("modules/payment/constants/portals.ts", `import type { LucideIcon } from "lucide-react"; import { LayoutDashboard, CreditCard, Building2, Settings, Shield, Activity, BarChart3, Wallet, Plug, FileText, BookOpen, KeyRound, Gauge, ScrollText, Network, Eye, } from "lucide-react"; export type PortalNavItem = { href: string; label: string; bundle?: string | null }; export type PortalNavGroup = { id: string; label: string; icon: LucideIcon; href?: string; bundle?: string | null; items?: PortalNavItem[]; }; export const PAYMENT_PORTAL = { id: "payment" as const, label: "تربت‌پی", description: "پلتفرم پرداخت سازمانی — BYO-PSP و پذیرنده", basePath: "/payment", icon: CreditCard, }; export const PAYMENT_NAV: PortalNavGroup[] = [ { id: "dashboards", label: "داشبوردها", icon: LayoutDashboard, items: [ { href: "/payment/dashboard", label: "داشبورد اجرایی" }, { href: "/payment/overview", label: "نمای کلی" }, { href: "/payment/kpis", label: "شاخص‌های کلیدی" }, ], }, { id: "operations", label: "عملیات پرداخت", icon: Wallet, items: [ { href: "/payment/requests", label: "درخواست‌های پرداخت" }, { href: "/payment/transactions", label: "تراکنش‌ها" }, { href: "/payment/ledger", label: "دفتر کل" }, { href: "/payment/callbacks", label: "کال‌بک‌ها" }, { href: "/payment/verification", label: "تأیید پرداخت" }, ], }, { id: "psp", label: "درگاه‌ها (PSP)", icon: Plug, bundle: "payment.byo_psp.basic", items: [ { href: "/payment/psp", label: "مدیریت درگاه", bundle: "payment.byo_psp.basic" }, { href: "/payment/psp/health", label: "سلامت درگاه", bundle: "payment.byo_psp.basic" }, { href: "/payment/providers", label: "تخصیص ارائه‌دهنده" }, ], }, { id: "merchants", label: "پذیرندگان", icon: Building2, bundle: "payment.torbat_pay.merchant", items: [ { href: "/payment/merchants", label: "حساب‌ها", bundle: "payment.torbat_pay.merchant" }, { href: "/payment/merchants/approvals", label: "تأییدها", bundle: "payment.torbat_pay.merchant" }, ], }, { id: "licensing", label: "مجوز و باندل", icon: KeyRound, items: [ { href: "/payment/bundles", label: "باندل‌ها" }, { href: "/payment/feature-toggles", label: "کلیدهای ویژگی" }, { href: "/payment/capabilities", label: "قابلیت‌ها" }, ], }, { id: "ops", label: "عملیات پلتفرم", icon: Activity, items: [ { href: "/payment/monitoring", label: "مانیتورینگ" }, { href: "/payment/metrics", label: "متریک‌ها" }, { href: "/payment/audit", label: "حسابرسی" }, { href: "/payment/settings", label: "تنظیمات" }, ], }, { id: "dev", label: "توسعه‌دهنده", icon: Network, items: [ { href: "/payment/api-explorer", label: "کاوشگر API" }, { href: "/payment/openapi", label: "OpenAPI" }, ], }, ]; export function navForPayment() { return PAYMENT_NAV; } export function filterNavByBundles( nav: PortalNavGroup[], activeBundles: string[] | undefined ) { const bundles = new Set(activeBundles ?? []); return nav .map((g) => { if (g.bundle && !bundles.has(g.bundle)) return null; const items = g.items?.filter((i) => !i.bundle || bundles.has(i.bundle)); if (g.items && (!items || items.length === 0)) return null; return { ...g, items }; }) .filter(Boolean) as PortalNavGroup[]; } export const MVP_ROUTE_HREFS = [ "/payment/hub", "/payment/dashboard", "/payment/overview", "/payment/kpis", "/payment/transactions", "/payment/ledger", "/payment/psp", "/payment/psp/health", "/payment/providers", "/payment/merchants", "/payment/merchants/approvals", "/payment/requests", "/payment/callbacks", "/payment/verification", "/payment/audit", "/payment/bundles", "/payment/feature-toggles", "/payment/capabilities", "/payment/settings", "/payment/metrics", "/payment/monitoring", "/payment/api-explorer", "/payment/openapi", ] as const; `); write( "modules/payment/services/payment-api.ts", `/** * Torbat Pay API client — real backend only (MVP 14.0–14.5). * Browser → /api/payment BFF; SSR → direct upstream :8012 */ import { getValidAccessToken, refreshSession } from "@/lib/auth"; import type { CapabilitiesResponse, HealthResponse, MetricsResponse, PaymentRecord, PermissionsCatalogResponse, } from "@/modules/payment/types"; const PAYMENT_BASE = typeof window !== "undefined" ? "/api/payment" : process.env.PAYMENT_SERVICE_URL || process.env.NEXT_PUBLIC_PAYMENT_API_URL || "http://localhost:8012"; export class PaymentApiError extends Error { constructor( public status: number, public code: string, message: string, public details?: unknown ) { super(message); this.name = "PaymentApiError"; } } type RequestOptions = RequestInit & { tenantId: string; auth?: boolean; idempotencyKey?: string; }; async function request(path: string, options: RequestOptions): Promise { const { tenantId, auth = true, idempotencyKey, 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 (idempotencyKey) headers.set("Idempotency-Key", idempotencyKey); if (auth) { const token = await getValidAccessToken(); if (token) headers.set("Authorization", \`Bearer \${token}\`); } const url = path.startsWith("http") ? path : \`\${PAYMENT_BASE.replace(/\\/$/, "")}\${path.startsWith("/") ? path : \`/\${path}\`}\`; let res: Response; try { res = await fetch(url, { ...init, headers, cache: "no-store" }); } catch (err) { const detail = err instanceof Error ? err.message : "network_error"; throw new Error(\`اتصال به سرویس تربت‌پی برقرار نشد — \${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, cache: "no-store" }); } } if (!res.ok) { const body = await res.json().catch(() => ({})); const detail = body?.error ?? body?.detail ?? {}; throw new PaymentApiError( res.status, detail?.code || (typeof detail === "string" ? detail : "unknown_error"), detail?.message || (typeof detail === "string" ? detail : res.statusText), detail ); } if (res.status === 204) return undefined as T; return res.json() as Promise; } function crud(basePath: string) { return { list: (tenantId: string) => request(basePath, { 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), }), }; } export const paymentApi = { health: (): Promise => fetch(\`\${PAYMENT_BASE}/health\`).then((r) => r.json()), capabilities: (tenantId: string) => request("/capabilities", { tenantId }), metrics: (tenantId?: string) => tenantId ? request("/metrics", { tenantId }) : fetch(\`\${PAYMENT_BASE}/metrics\`).then((r) => r.json()), openapi: async (): Promise> => { const r = await fetch(\`\${PAYMENT_BASE}/openapi.json\`); if (!r.ok) throw new PaymentApiError(r.status, "openapi_error", "OpenAPI در دسترس نیست"); return r.json(); }, permissions: { catalog: (tenantId: string) => request("/api/v1/permissions/catalog", { tenantId }), }, audit: { list: (tenantId: string) => request("/api/v1/audit", { tenantId }), }, workspaces: crud("/api/v1/payment-workspaces"), bundleDefinitions: crud("/api/v1/bundle-definitions"), tenantBundles: crud("/api/v1/tenant-bundles"), featureToggles: crud("/api/v1/feature-toggles"), providerAssignments: crud("/api/v1/provider-assignments"), pspRegistrations: crud("/api/v1/psp-provider-registrations"), creditRegistrations: crud("/api/v1/credit-provider-registrations"), configurations: crud("/api/v1/configurations"), settings: crud("/api/v1/settings"), pspConnections: { ...crud("/api/v1/psp-connections"), test: (tenantId: string, id: string) => request>(\`/api/v1/psp-connections/\${id}/test\`, { tenantId, method: "POST", body: "{}", }), }, pspRouting: crud("/api/v1/psp-routing-policies"), merchants: { list: (tenantId: string) => request("/api/v1/merchant-accounts", { tenantId }), get: (tenantId: string, id: string) => request(\`/api/v1/merchant-accounts/\${id}\`, { tenantId }), create: (tenantId: string, body: Record) => request("/api/v1/merchant-accounts", { tenantId, method: "POST", body: JSON.stringify(body), }), submit: (tenantId: string, id: string) => request(\`/api/v1/merchant-accounts/\${id}/submit\`, { tenantId, method: "POST", body: "{}" }), activate: (tenantId: string, id: string) => request(\`/api/v1/merchant-accounts/\${id}/activate\`, { tenantId, method: "POST", body: "{}" }), suspend: (tenantId: string, id: string) => request(\`/api/v1/merchant-accounts/\${id}/suspend\`, { tenantId, method: "POST", body: "{}" }), close: (tenantId: string, id: string) => request(\`/api/v1/merchant-accounts/\${id}/close\`, { tenantId, method: "POST", body: "{}" }), }, settlementProfiles: crud("/api/v1/merchant-settlement-profiles"), requests: { list: (tenantId: string) => request("/api/v1/payment-requests", { tenantId }), get: (tenantId: string, id: string) => request(\`/api/v1/payment-requests/\${id}\`, { tenantId }), create: (tenantId: string, body: Record, idempotencyKey: string) => request("/api/v1/payment-requests", { tenantId, method: "POST", body: JSON.stringify(body), idempotencyKey, }), cancel: (tenantId: string, id: string) => request(\`/api/v1/payment-requests/\${id}/cancel\`, { tenantId, method: "POST", body: "{}" }), verify: (tenantId: string, id: string, body?: Record) => request(\`/api/v1/payment-requests/\${id}/verify\`, { tenantId, method: "POST", body: JSON.stringify(body ?? { status: "paid" }), }), }, transactions: { list: (tenantId: string) => request("/api/v1/payment-transactions", { tenantId }), get: (tenantId: string, id: string) => request(\`/api/v1/payment-transactions/\${id}\`, { tenantId }), bySource: (tenantId: string, service?: string, refId?: string) => { const sp = new URLSearchParams(); if (service) sp.set("service", service); if (refId) sp.set("ref_id", refId); const q = sp.toString(); return request(\`/api/v1/payment-transactions/by-source\${q ? \`?\${q}\` : ""}\`, { tenantId, }); }, }, ledger: { list: (tenantId: string) => request("/api/v1/payment-ledger-entries", { tenantId }), }, }; export const PAYMENT_API_CATALOG = [ { method: "GET", path: "/health", phase: "14.0" }, { method: "GET", path: "/capabilities", phase: "14.0" }, { method: "GET", path: "/metrics", phase: "14.0" }, { method: "GET|POST", path: "/api/v1/payment-workspaces", phase: "14.0" }, { method: "GET|POST", path: "/api/v1/tenant-bundles", phase: "14.0" }, { method: "GET|POST", path: "/api/v1/feature-toggles", phase: "14.0" }, { method: "GET|POST", path: "/api/v1/psp-connections", phase: "14.1" }, { method: "POST", path: "/api/v1/psp-connections/{id}/test", phase: "14.1" }, { method: "GET|POST", path: "/api/v1/merchant-accounts", phase: "14.2" }, { method: "POST", path: "/api/v1/payment-requests", phase: "14.3" }, { method: "POST", path: "/api/v1/payment-requests/{id}/verify", phase: "14.4" }, { method: "GET", path: "/api/v1/payment-transactions", phase: "14.5" }, { method: "GET", path: "/api/v1/payment-ledger-entries", phase: "14.5" }, ] as const; ` );