/** * Inventory Khanegi tenants for SMS sync rollout. * * Groups by nodeServer host (multiple versions may share one physical server). * Prints TSV + summary JSON. Use on the SuperAdmin host: * * npm run prisma:inventory-khanegi-sms * SMS_INVENTORY_FORMAT=json npm run prisma:inventory-khanegi-sms * SMS_INVENTORY_OUT=/tmp/khanegi-sms-inventory.json npm run prisma:inventory-khanegi-sms */ import * as fs from 'fs'; import { PrismaClient, ProductType } from '@prisma/client'; const prisma = new PrismaClient(); function env(name: string, fallback = '') { return String(process.env[name] ?? fallback).trim(); } function hostFromUrl(url?: string | null) { if (!url) return '(unknown-host)'; return ( String(url) .trim() .toLowerCase() .replace(/^https?:\/\//, '') .split('/')[0] .split(':')[0] || '(unknown-host)' ); } type SyncStatus = | 'no_sms_account' | 'imported' | 'imported_marked' | 'empty_default' | 'inactive'; function classify(row: { isActive: boolean; hasAccount: boolean; smsSyncedAt: Date | null; balanceRials: number | null; ledgerCount: number; sendLogCount: number; }): SyncStatus { if (!row.isActive) return 'inactive'; if (!row.hasAccount) return 'no_sms_account'; if (row.smsSyncedAt) return 'imported_marked'; if ( (row.balanceRials ?? 0) === 0 && row.ledgerCount === 0 && row.sendLogCount === 0 ) { return 'empty_default'; } return 'imported'; } async function main() { const format = (env('SMS_INVENTORY_FORMAT', 'tsv') || 'tsv').toLowerCase(); const outPath = env('SMS_INVENTORY_OUT'); const tenants = await prisma.tenant.findMany({ where: { product: ProductType.khanegi }, orderBy: [{ nodeServer: 'asc' }, { domain: 'asc' }], include: { smsAccount: { include: { _count: { select: { ledgers: true, sendLogs: true } }, }, }, }, }); const rows = tenants.map((t) => { const acc = t.smsAccount; const ledgerCount = acc?._count.ledgers ?? 0; const sendLogCount = acc?._count.sendLogs ?? 0; const balanceRials = acc?.balanceRials ?? null; const status = classify({ isActive: t.isActive, hasAccount: !!acc, smsSyncedAt: t.smsSyncedAt, balanceRials, ledgerCount, sendLogCount, }); return { id: t.id, nameFa: t.nameFa, domain: t.domain, domains: t.domains, appUrl: t.appUrl, adminUrl: t.adminUrl, nodeServer: t.nodeServer, serverHost: hostFromUrl(t.nodeServer), isActive: t.isActive, smsSyncedAt: t.smsSyncedAt?.toISOString() ?? null, hasSmsAccount: !!acc, balanceRials, pricePerPartRials: acc?.pricePerPartRials ?? null, isBlocked: acc?.isBlocked ?? null, autoBlockBelowRials: acc?.autoBlockBelowRials ?? null, ledgerCount, sendLogCount, syncStatus: status, nextAction: status === 'inactive' ? 'skip_inactive' : status === 'no_sms_account' || status === 'empty_default' ? 'import_then_enable_remote' : status === 'imported' || status === 'imported_marked' ? 'enable_remote_if_not_yet' : 'review', }; }); const byHost = new Map(); for (const row of rows) { const list = byHost.get(row.serverHost) || []; list.push(row); byHost.set(row.serverHost, list); } const summary = { total: rows.length, byStatus: rows.reduce>((acc, r) => { acc[r.syncStatus] = (acc[r.syncStatus] || 0) + 1; return acc; }, {}), multiVersionHosts: [...byHost.entries()] .filter(([, list]) => list.length > 1) .map(([host, list]) => ({ host, count: list.length, domains: list.map((r) => r.domain), })), generatedAt: new Date().toISOString(), }; const payload = { summary, rows }; if (outPath) { fs.writeFileSync(outPath, JSON.stringify(payload, null, 2), 'utf8'); console.error(`Wrote ${outPath}`); } if (format === 'json') { console.log(JSON.stringify(payload, null, 2)); } else { console.log( [ 'serverHost', 'domain', 'nameFa', 'syncStatus', 'nextAction', 'balanceRials', 'pricePerPartRials', 'isBlocked', 'smsSyncedAt', 'ledgerCount', 'sendLogCount', 'appUrl', 'nodeServer', ].join('\t'), ); for (const r of rows) { console.log( [ r.serverHost, r.domain, r.nameFa, r.syncStatus, r.nextAction, r.balanceRials ?? '', r.pricePerPartRials ?? '', r.isBlocked ?? '', r.smsSyncedAt ?? '', r.ledgerCount, r.sendLogCount, r.appUrl ?? '', r.nodeServer, ].join('\t'), ); } console.error(JSON.stringify(summary, null, 2)); } } main() .catch((e) => { console.error(e); process.exitCode = 1; }) .finally(async () => { await prisma.$disconnect(); });