TorbatYar/frontend/scripts/scaffold-payment-module.mjs
Mortezakoohjani 4451b32a33 feat(payment-frontend): ship Torbat Pay admin portal for MVP 14.0-14.5
Add payment module, BFF proxy, hub/dashboard/ops/PSP/merchant screens wired to real APIs, and mark payment-frontend complete in project status.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 18:35:07 +03:30

479 lines
18 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* Scaffold Torbat Pay frontend module (MVP payment-14.014.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<string, unknown> & { 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<string, boolean>;
payment_sources_supported?: string[];
payment_sources_reserved?: string[];
contract_versions?: Record<string, string>;
};
export type MetricsResponse = {
service: string;
version: string;
phase: string;
metrics: Record<string, unknown>;
};
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.014.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<T>(path: string, options: RequestOptions): Promise<T> {
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<T>;
}
function crud(basePath: string) {
return {
list: (tenantId: string) => request<PaymentRecord[]>(basePath, { tenantId }),
get: (tenantId: string, id: string) => request<PaymentRecord>(\`\${basePath}/\${id}\`, { tenantId }),
create: (tenantId: string, body: Record<string, unknown>) =>
request<PaymentRecord>(basePath, { tenantId, method: "POST", body: JSON.stringify(body) }),
update: (tenantId: string, id: string, body: Record<string, unknown>) =>
request<PaymentRecord>(\`\${basePath}/\${id}\`, {
tenantId,
method: "PATCH",
body: JSON.stringify(body),
}),
};
}
export const paymentApi = {
health: (): Promise<HealthResponse> => fetch(\`\${PAYMENT_BASE}/health\`).then((r) => r.json()),
capabilities: (tenantId: string) =>
request<CapabilitiesResponse>("/capabilities", { tenantId }),
metrics: (tenantId?: string) =>
tenantId
? request<MetricsResponse>("/metrics", { tenantId })
: fetch(\`\${PAYMENT_BASE}/metrics\`).then((r) => r.json()),
openapi: async (): Promise<Record<string, unknown>> => {
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<PermissionsCatalogResponse>("/api/v1/permissions/catalog", { tenantId }),
},
audit: {
list: (tenantId: string) => request<PaymentRecord[]>("/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<Record<string, unknown>>(\`/api/v1/psp-connections/\${id}/test\`, {
tenantId,
method: "POST",
body: "{}",
}),
},
pspRouting: crud("/api/v1/psp-routing-policies"),
merchants: {
list: (tenantId: string) => request<PaymentRecord[]>("/api/v1/merchant-accounts", { tenantId }),
get: (tenantId: string, id: string) =>
request<PaymentRecord>(\`/api/v1/merchant-accounts/\${id}\`, { tenantId }),
create: (tenantId: string, body: Record<string, unknown>) =>
request<PaymentRecord>("/api/v1/merchant-accounts", {
tenantId,
method: "POST",
body: JSON.stringify(body),
}),
submit: (tenantId: string, id: string) =>
request<PaymentRecord>(\`/api/v1/merchant-accounts/\${id}/submit\`, { tenantId, method: "POST", body: "{}" }),
activate: (tenantId: string, id: string) =>
request<PaymentRecord>(\`/api/v1/merchant-accounts/\${id}/activate\`, { tenantId, method: "POST", body: "{}" }),
suspend: (tenantId: string, id: string) =>
request<PaymentRecord>(\`/api/v1/merchant-accounts/\${id}/suspend\`, { tenantId, method: "POST", body: "{}" }),
close: (tenantId: string, id: string) =>
request<PaymentRecord>(\`/api/v1/merchant-accounts/\${id}/close\`, { tenantId, method: "POST", body: "{}" }),
},
settlementProfiles: crud("/api/v1/merchant-settlement-profiles"),
requests: {
list: (tenantId: string) => request<PaymentRecord[]>("/api/v1/payment-requests", { tenantId }),
get: (tenantId: string, id: string) =>
request<PaymentRecord>(\`/api/v1/payment-requests/\${id}\`, { tenantId }),
create: (tenantId: string, body: Record<string, unknown>, idempotencyKey: string) =>
request<PaymentRecord>("/api/v1/payment-requests", {
tenantId,
method: "POST",
body: JSON.stringify(body),
idempotencyKey,
}),
cancel: (tenantId: string, id: string) =>
request<PaymentRecord>(\`/api/v1/payment-requests/\${id}/cancel\`, { tenantId, method: "POST", body: "{}" }),
verify: (tenantId: string, id: string, body?: Record<string, unknown>) =>
request<PaymentRecord>(\`/api/v1/payment-requests/\${id}/verify\`, {
tenantId,
method: "POST",
body: JSON.stringify(body ?? { status: "paid" }),
}),
},
transactions: {
list: (tenantId: string) => request<PaymentRecord[]>("/api/v1/payment-transactions", { tenantId }),
get: (tenantId: string, id: string) =>
request<PaymentRecord>(\`/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<PaymentRecord[]>(\`/api/v1/payment-transactions/by-source\${q ? \`?\${q}\` : ""}\`, {
tenantId,
});
},
},
ledger: {
list: (tenantId: string) => request<PaymentRecord[]>("/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;
`
);