201 lines
5.0 KiB
TypeScript
201 lines
5.0 KiB
TypeScript
/**
|
|
* 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();
|
|
});
|