Files

201 lines
5.0 KiB
TypeScript
Raw Permalink Normal View History

/**
* 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<string, typeof rows>();
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<Record<string, number>>((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();
});