Fix SMS tenant copy target matching; add list-tenants helper.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+2
-1
@@ -28,7 +28,8 @@
|
|||||||
"prisma:seed-khanegi-defaults": "ts-node --transpile-only prisma/seed-khanegi-default-snapshots.ts",
|
"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: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: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": {
|
"prisma": {
|
||||||
"seed": "ts-node --transpile-only prisma/seed.ts"
|
"seed": "ts-node --transpile-only prisma/seed.ts"
|
||||||
|
|||||||
@@ -44,41 +44,71 @@ async function findTenantByDomain(domainRaw: string) {
|
|||||||
const host = normalizeHost(domainRaw);
|
const host = normalizeHost(domainRaw);
|
||||||
if (!host) throw new Error('domain is empty');
|
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<string>([host]);
|
const candidates = new Set<string>([host]);
|
||||||
if (host.startsWith('app.')) candidates.add(host.slice(4));
|
if (host.startsWith('app.')) candidates.add(host.slice(4));
|
||||||
else candidates.add(`app.${host}`);
|
else candidates.add(`app.${host}`);
|
||||||
|
|
||||||
const tenants = await prisma.tenant.findMany({ where: { isActive: true } });
|
const tenants = await prisma.tenant.findMany({ where: { isActive: true } });
|
||||||
const hit = tenants.find((t) => {
|
|
||||||
const set = new Set<string>();
|
// 1) Exact primary domain
|
||||||
const primary = normalizeHost(t.domain);
|
let hit = tenants.find((t) => candidates.has(normalizeHost(t.domain)));
|
||||||
if (primary) set.add(primary);
|
if (hit) return hit;
|
||||||
|
|
||||||
|
// 2) Exact match in domains[] only (no nodeServer fuzzy)
|
||||||
|
hit = tenants.find((t) => {
|
||||||
for (const d of readDomains(t.domains)) {
|
for (const d of readDomains(t.domains)) {
|
||||||
const n = normalizeHost(d);
|
if (candidates.has(normalizeHost(d))) return true;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
if (hit) return hit;
|
||||||
|
|
||||||
|
// 3) Exact appUrl host
|
||||||
|
hit = tenants.find((t) => candidates.has(normalizeHost(t.appUrl)));
|
||||||
|
if (hit) return hit;
|
||||||
|
|
||||||
if (!hit) {
|
|
||||||
const sample = tenants
|
const sample = tenants
|
||||||
.filter((t) => /ayg|montzar|آیگ|ایگ/i.test(`${t.domain} ${t.nameFa} ${t.nodeServer}`))
|
.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 }));
|
.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);
|
console.error('Nearby tenants:', sample);
|
||||||
throw new Error(`No active tenant matched domain: ${host}`);
|
throw new Error(`No active tenant matched domain exactly: ${host}`);
|
||||||
}
|
}
|
||||||
return hit;
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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() {
|
async function main() {
|
||||||
@@ -88,8 +118,38 @@ async function main() {
|
|||||||
const withHistory = env('SMS_COPY_HISTORY', '1') !== '0';
|
const withHistory = env('SMS_COPY_HISTORY', '1') !== '0';
|
||||||
const addDomainAlias = env('SMS_COPY_ADD_ALIAS', '1') !== '0';
|
const addDomainAlias = env('SMS_COPY_ADD_ALIAS', '1') !== '0';
|
||||||
|
|
||||||
const fromTenant = await findTenantByDomain(fromDomain);
|
// If a previous run wrongly attached app.aygingold.ir onto k.montzar.ir, strip it first
|
||||||
const toTenant = await findTenantByDomain(toDomain);
|
// 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({
|
const fromAccount = await prisma.smsAccount.findUnique({
|
||||||
where: { tenantId: fromTenant.id },
|
where: { tenantId: fromTenant.id },
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user