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