Add bulk copy of Khanegi SMS price and charge info from pilot.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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<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();
|
||||
});
|
||||
Reference in New Issue
Block a user