Files

195 lines
4.7 KiB
TypeScript
Raw Permalink Normal View History

/**
* Bulk-copy SMS price + deposit (charge) info from one Khanegi tenant to all others.
* Does NOT change balance / block / auto-block / history.
*
* Default source: k.montzar.ir
*
* SMS_BULK_SOURCE_DOMAIN=k.montzar.ir \
* SMS_BULK_DRY_RUN=1 \
* npm run prisma:bulk-khanegi-sms-defaults
*
* Apply:
* SMS_BULK_SOURCE_DOMAIN=k.montzar.ir \
* npm run prisma:bulk-khanegi-sms-defaults
*/
import { PrismaClient, ProductType } from '@prisma/client';
const prisma = new PrismaClient();
function env(name: string, fallback = '') {
return String(process.env[name] ?? fallback).trim();
}
function normalizeHost(value?: string | null) {
if (!value) return '';
return String(value)
.trim()
.toLowerCase()
.replace(/^https?:\/\//, '')
.split('/')[0]
.split(':')[0];
}
async function findTenantByDomain(domain: string) {
const host = normalizeHost(domain);
const tenants = await prisma.tenant.findMany({
where: { product: ProductType.khanegi, isActive: true },
include: { smsAccount: true },
});
const hit = tenants.find((t) => {
const candidates = new Set<string>();
const primary = normalizeHost(t.domain);
if (primary) candidates.add(primary);
const extra = Array.isArray(t.domains) ? t.domains : [];
for (const d of extra) {
const n = normalizeHost(String(d));
if (n) candidates.add(n);
}
return candidates.has(host);
});
if (!hit) throw new Error(`Source tenant not found for domain: ${host}`);
if (!hit.smsAccount) throw new Error(`Source tenant has no SMS account: ${host}`);
return hit;
}
async function main() {
const sourceDomain = env('SMS_BULK_SOURCE_DOMAIN', 'k.montzar.ir') || 'k.montzar.ir';
const dryRun = env('SMS_BULK_DRY_RUN') === '1';
const includeSource = env('SMS_BULK_INCLUDE_SOURCE') === '1';
const source = await findTenantByDomain(sourceDomain);
const src = source.smsAccount!;
const payload = {
pricePerPartRials: src.pricePerPartRials,
chargeCard: src.chargeCard,
chargeIban: src.chargeIban,
chargeHolder: src.chargeHolder,
chargeInstruction: src.chargeInstruction,
};
console.log(
JSON.stringify(
{
dryRun,
source: {
id: source.id,
domain: source.domain,
nameFa: source.nameFa,
...payload,
},
},
null,
2,
),
);
const targets = await prisma.tenant.findMany({
where: {
product: ProductType.khanegi,
isActive: true,
...(includeSource ? {} : { NOT: { id: source.id } }),
},
include: { smsAccount: true },
orderBy: { domain: 'asc' },
});
let updated = 0;
let created = 0;
let skipped = 0;
for (const t of targets) {
if (!t.smsAccount) {
if (dryRun) {
console.log(`[dry] would CREATE sms account for ${t.domain}`);
created += 1;
continue;
}
await prisma.smsAccount.create({
data: {
tenantId: t.id,
balanceRials: 0,
isBlocked: false,
...payload,
alertCriticalRials: 10000,
alertWarningRials: 100000,
},
});
console.log(`CREATE ${t.domain}`);
created += 1;
continue;
}
const same =
t.smsAccount.pricePerPartRials === payload.pricePerPartRials &&
(t.smsAccount.chargeCard || null) === (payload.chargeCard || null) &&
(t.smsAccount.chargeIban || null) === (payload.chargeIban || null) &&
(t.smsAccount.chargeHolder || null) === (payload.chargeHolder || null) &&
(t.smsAccount.chargeInstruction || null) === (payload.chargeInstruction || null);
if (same) {
skipped += 1;
continue;
}
if (dryRun) {
console.log(
`[dry] would UPDATE ${t.domain} price=${payload.pricePerPartRials} card=${payload.chargeCard || ''}`,
);
updated += 1;
continue;
}
await prisma.smsAccount.update({
where: { id: t.smsAccount.id },
data: payload,
});
console.log(`UPDATE ${t.domain}`);
updated += 1;
}
if (!dryRun) {
await prisma.auditLog.create({
data: {
action: 'sms.bulk_khanegi_defaults',
entityType: 'sms_account',
entityId: src.id,
tenantId: source.id,
after: {
sourceDomain: source.domain,
payload,
updated,
created,
skipped,
},
},
});
}
console.log(
JSON.stringify(
{
ok: true,
dryRun,
updated,
created,
skipped,
totalTargets: targets.length,
},
null,
2,
),
);
}
main()
.catch((e) => {
console.error(e);
process.exitCode = 1;
})
.finally(async () => {
await prisma.$disconnect();
});