From 2f0e39edbb4e15f658bf73bfb420065609e76835 Mon Sep 17 00:00:00 2001 From: Mahdi Date: Sat, 8 Aug 2026 16:00:54 +0330 Subject: [PATCH] Add bulk copy of Khanegi SMS price and charge info from pilot. Co-authored-by: Cursor --- package.json | 1 + prisma/bulk-khanegi-sms-defaults.ts | 194 ++++++++++++++++++++++++++++ 2 files changed, 195 insertions(+) create mode 100644 prisma/bulk-khanegi-sms-defaults.ts diff --git a/package.json b/package.json index d1333a3..dc4a3dc 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "prisma:import-sms-from-product": "ts-node --transpile-only prisma/import-sms-from-product.ts", "prisma:import-sms-batch": "ts-node --transpile-only prisma/import-sms-batch.ts", "prisma:inventory-khanegi-sms": "ts-node --transpile-only prisma/inventory-khanegi-sms.ts", + "prisma:bulk-khanegi-sms-defaults": "ts-node --transpile-only prisma/bulk-khanegi-sms-defaults.ts", "prisma:copy-sms-between-tenants": "ts-node --transpile-only prisma/copy-sms-between-tenants.ts", "prisma:list-tenants": "ts-node --transpile-only prisma/list-tenants.ts" }, diff --git a/prisma/bulk-khanegi-sms-defaults.ts b/prisma/bulk-khanegi-sms-defaults.ts new file mode 100644 index 0000000..b8390d5 --- /dev/null +++ b/prisma/bulk-khanegi-sms-defaults.ts @@ -0,0 +1,194 @@ +/** + * 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(); + });