From c6d90ea262d95cabc6ef3717abd0b6c81e5b65bc Mon Sep 17 00:00:00 2001 From: Mahdi Date: Thu, 6 Aug 2026 17:17:56 +0330 Subject: [PATCH] Fix SMS tenant copy target matching; add list-tenants helper. Co-authored-by: Cursor --- package.json | 3 +- prisma/copy-sms-between-tenants.ts | 112 ++++++++++++++++++++++------- prisma/list-tenants.ts | 57 +++++++++++++++ 3 files changed, 145 insertions(+), 27 deletions(-) create mode 100644 prisma/list-tenants.ts diff --git a/package.json b/package.json index 7898718..557c77d 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,8 @@ "prisma:seed-khanegi-defaults": "ts-node --transpile-only prisma/seed-khanegi-default-snapshots.ts", "prisma:ensure-pilot-sms-tokens": "ts-node --transpile-only prisma/ensure-pilot-sms-tokens.ts", "prisma:import-sms-from-product": "ts-node --transpile-only prisma/import-sms-from-product.ts", - "prisma:copy-sms-between-tenants": "ts-node --transpile-only prisma/copy-sms-between-tenants.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" }, "prisma": { "seed": "ts-node --transpile-only prisma/seed.ts" diff --git a/prisma/copy-sms-between-tenants.ts b/prisma/copy-sms-between-tenants.ts index 3b85a55..203da14 100644 --- a/prisma/copy-sms-between-tenants.ts +++ b/prisma/copy-sms-between-tenants.ts @@ -44,41 +44,71 @@ async function findTenantByDomain(domainRaw: string) { const host = normalizeHost(domainRaw); if (!host) throw new Error('domain is empty'); + const byId = env('SMS_COPY_TO_TENANT_ID') || env('SMS_COPY_FROM_TENANT_ID'); + // Prefer explicit ids when provided via dedicated envs in main(). + const candidates = new Set([host]); if (host.startsWith('app.')) candidates.add(host.slice(4)); else candidates.add(`app.${host}`); const tenants = await prisma.tenant.findMany({ where: { isActive: true } }); - const hit = tenants.find((t) => { - const set = new Set(); - const primary = normalizeHost(t.domain); - if (primary) set.add(primary); + + // 1) Exact primary domain + let hit = tenants.find((t) => candidates.has(normalizeHost(t.domain))); + if (hit) return hit; + + // 2) Exact match in domains[] only (no nodeServer fuzzy) + hit = tenants.find((t) => { for (const d of readDomains(t.domains)) { - const n = normalizeHost(d); - if (n) set.add(n); - } - for (const c of candidates) { - if (set.has(c)) return true; - } - const node = normalizeHost(t.nodeServer); - for (const c of candidates) { - if (node.includes(c)) return true; - } - const appUrl = normalizeHost(t.appUrl); - for (const c of candidates) { - if (appUrl === c) return true; + if (candidates.has(normalizeHost(d))) return true; } return false; }); + if (hit) return hit; - if (!hit) { - const sample = tenants - .filter((t) => /ayg|montzar|آیگ|ایگ/i.test(`${t.domain} ${t.nameFa} ${t.nodeServer}`)) - .map((t) => ({ id: t.id, domain: t.domain, nameFa: t.nameFa, nodeServer: t.nodeServer })); - console.error('Nearby tenants:', sample); - throw new Error(`No active tenant matched domain: ${host}`); + // 3) Exact appUrl host + hit = tenants.find((t) => candidates.has(normalizeHost(t.appUrl))); + if (hit) return hit; + + const sample = tenants + .filter((t) => /ayg|montzar|آیگ|ایگ/i.test(`${t.domain} ${t.nameFa} ${t.nodeServer} ${t.appUrl}`)) + .map((t) => ({ + id: t.id, + domain: t.domain, + nameFa: t.nameFa, + nodeServer: t.nodeServer, + appUrl: t.appUrl, + domains: t.domains, + })); + console.error('Nearby tenants:', sample); + throw new Error(`No active tenant matched domain exactly: ${host}`); +} + +async function findTenant(opts: { + domain?: string; + tenantId?: string; + nameFa?: string; +}) { + if (opts.tenantId) { + const t = await prisma.tenant.findUnique({ where: { id: opts.tenantId } }); + if (!t) throw new Error(`Tenant id not found: ${opts.tenantId}`); + return t; } - return hit; + if (opts.nameFa) { + const name = opts.nameFa.trim(); + const rows = await prisma.tenant.findMany({ + where: { isActive: true, nameFa: { contains: name } }, + }); + if (rows.length === 1) return rows[0]; + if (rows.length === 0) throw new Error(`No tenant nameFa contains: ${name}`); + console.error( + 'Multiple name matches:', + rows.map((t) => ({ id: t.id, nameFa: t.nameFa, domain: t.domain })), + ); + throw new Error(`Ambiguous nameFa: ${name} (use SMS_COPY_TO_TENANT_ID)`); + } + if (!opts.domain) throw new Error('domain/tenantId/nameFa required'); + return findTenantByDomain(opts.domain); } async function main() { @@ -88,8 +118,38 @@ async function main() { const withHistory = env('SMS_COPY_HISTORY', '1') !== '0'; const addDomainAlias = env('SMS_COPY_ADD_ALIAS', '1') !== '0'; - const fromTenant = await findTenantByDomain(fromDomain); - const toTenant = await findTenantByDomain(toDomain); + // If a previous run wrongly attached app.aygingold.ir onto k.montzar.ir, strip it first + // unless source and target are intentionally the same. + const stripWrongAlias = env('SMS_COPY_STRIP_ALIAS_FROM', 'k.montzar.ir'); + if (stripWrongAlias && normalizeHost(stripWrongAlias) !== normalizeHost(toDomain)) { + const maybe = await prisma.tenant.findFirst({ + where: { domain: normalizeHost(stripWrongAlias) }, + }); + if (maybe) { + const hostTo = normalizeHost(toDomain); + const current = readDomains(maybe.domains).map(normalizeHost).filter(Boolean); + if (current.includes(hostTo)) { + const next = current.filter((d) => d !== hostTo); + console.log(`Stripping alias ${hostTo} from ${maybe.domain} domains:`, current, '=>', next); + if (!dryRun) { + await prisma.tenant.update({ + where: { id: maybe.id }, + data: { domains: next }, + }); + } + } + } + } + + const fromTenant = await findTenant({ + tenantId: env('SMS_COPY_FROM_TENANT_ID') || undefined, + domain: fromDomain, + }); + const toTenant = await findTenant({ + tenantId: env('SMS_COPY_TO_TENANT_ID') || undefined, + nameFa: env('SMS_COPY_TO_NAME_FA') || undefined, + domain: env('SMS_COPY_TO_TENANT_ID') || env('SMS_COPY_TO_NAME_FA') ? undefined : toDomain, + }); const fromAccount = await prisma.smsAccount.findUnique({ where: { tenantId: fromTenant.id }, diff --git a/prisma/list-tenants.ts b/prisma/list-tenants.ts new file mode 100644 index 0000000..3fb5cc6 --- /dev/null +++ b/prisma/list-tenants.ts @@ -0,0 +1,57 @@ +/** + * List SuperAdmin tenants (filter optional). + * + * SMS_LIST_Q=aygin npx ts-node --transpile-only prisma/list-tenants.ts + */ +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +function env(name: string, fallback = '') { + return String(process.env[name] ?? fallback).trim(); +} + +async function main() { + const q = env('SMS_LIST_Q').toLowerCase(); + const rows = await prisma.tenant.findMany({ + orderBy: [{ product: 'asc' }, { nameFa: 'asc' }], + include: { smsAccount: true }, + }); + + const filtered = q + ? rows.filter((t) => { + const blob = `${t.id} ${t.nameFa} ${t.nameEn || ''} ${t.domain} ${t.slug} ${t.nodeServer} ${t.appUrl || ''} ${JSON.stringify(t.domains)}`.toLowerCase(); + return blob.includes(q) || blob.includes('آیگ') || blob.includes('ایگ') || blob.includes('montzar'); + }) + : rows; + + for (const t of filtered) { + console.log( + JSON.stringify( + { + id: t.id, + nameFa: t.nameFa, + product: t.product, + domain: t.domain, + domains: t.domains, + appUrl: t.appUrl, + nodeServer: t.nodeServer, + balanceRials: t.smsAccount?.balanceRials ?? null, + pricePerPartRials: t.smsAccount?.pricePerPartRials ?? null, + }, + null, + 2, + ), + ); + } + console.log(`count=${filtered.length}`); +} + +main() + .catch((e) => { + console.error(e); + process.exitCode = 1; + }) + .finally(async () => { + await prisma.$disconnect(); + });