/** * 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(); 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(); });