/** * Apply Niazi-style modular brand colors to Khanegi tenants * that are NOT locked newTheme (Aygin, new.gholaktala.ir, …). * * Colors (from app.niazitala.ir): * primary #0a1b37 (هدر) * secondary #e0be7f (اکسنت) * background #ffffff (پس‌زمینه) * * Also forces uiVariant=modular for non-protected Khanegi rows. * * Dry-run: * KHANEGI_BRAND_DRY_RUN=1 npm run prisma:bulk-khanegi-modular-brand * * Apply: * npm run prisma:bulk-khanegi-modular-brand * * Optional source override: * KHANEGI_BRAND_SOURCE_DOMAIN=app.niazitala.ir npm run prisma:bulk-khanegi-modular-brand */ import { KhanegiUiVariant, PrismaClient, ProductType } from '@prisma/client'; import { isForcedKhanegiNewTheme } from './khanegi-variant-locks'; const prisma = new PrismaClient(); const FALLBACK_BRAND = { primaryColor: '#0a1b37', secondaryColor: '#e0be7f', backgroundColor: '#ffffff', }; 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]; } function isProtectedNewThemeTenant(domain: string, domains: unknown, slug: string) { // Aygin + explicit locks (e.g. new.gholaktala.ir) return isForcedKhanegiNewTheme(domain, domains, slug); } function bumpAppVersion(current?: string | null): string { const v = String(current || '1').trim() || '1'; if (/^\d+$/.test(v)) return String(Number(v) + 1); const semver = v.match(/^(\d+)\.(\d+)\.(\d+)$/); if (semver) return `${semver[1]}.${semver[2]}.${Number(semver[3]) + 1}`; return `${Date.now()}`; } async function resolveBrand() { const sourceDomain = env('KHANEGI_BRAND_SOURCE_DOMAIN', 'app.niazitala.ir') || 'app.niazitala.ir'; const host = normalizeHost(sourceDomain); const tenants = await prisma.tenant.findMany({ where: { product: ProductType.khanegi, isActive: true }, select: { id: true, domain: true, domains: true, primaryColor: true, secondaryColor: true, backgroundColor: true, }, }); const source = 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 (!source) { console.warn( `Source tenant not found for ${host}; using hardcoded Niazi palette.`, ); return { ...FALLBACK_BRAND, sourceDomain: host, sourceFound: false }; } return { primaryColor: source.primaryColor || FALLBACK_BRAND.primaryColor, secondaryColor: source.secondaryColor || FALLBACK_BRAND.secondaryColor, backgroundColor: source.backgroundColor || FALLBACK_BRAND.backgroundColor, sourceDomain: host, sourceFound: true, sourceId: source.id, }; } async function main() { const dryRun = env('KHANEGI_BRAND_DRY_RUN') === '1'; const brand = await resolveBrand(); const tenants = await prisma.tenant.findMany({ where: { product: ProductType.khanegi, isActive: true }, select: { id: true, domain: true, domains: true, slug: true, nameFa: true, uiVariant: true, primaryColor: true, secondaryColor: true, backgroundColor: true, appVersion: true, }, orderBy: { domain: 'asc' }, }); const targets = tenants.filter( (t) => !isProtectedNewThemeTenant(t.domain, t.domains, t.slug), ); const skippedNewTheme = tenants.filter((t) => isProtectedNewThemeTenant(t.domain, t.domains, t.slug), ); console.log( JSON.stringify( { dryRun, brand: { primaryColor: brand.primaryColor, secondaryColor: brand.secondaryColor, backgroundColor: brand.backgroundColor, sourceDomain: brand.sourceDomain, sourceFound: brand.sourceFound, }, totalKhanegi: tenants.length, targets: targets.length, skippedNewTheme: skippedNewTheme.map((t) => ({ domain: t.domain, uiVariant: t.uiVariant, })), }, null, 2, ), ); let updated = 0; for (const t of targets) { const next = { uiVariant: KhanegiUiVariant.modular, primaryColor: brand.primaryColor, secondaryColor: brand.secondaryColor, backgroundColor: brand.backgroundColor, appVersion: bumpAppVersion(t.appVersion), }; const changed = t.uiVariant !== next.uiVariant || t.primaryColor !== next.primaryColor || t.secondaryColor !== next.secondaryColor || t.backgroundColor !== next.backgroundColor; console.log( JSON.stringify({ domain: t.domain, nameFa: t.nameFa, changed, before: { uiVariant: t.uiVariant, primaryColor: t.primaryColor, secondaryColor: t.secondaryColor, backgroundColor: t.backgroundColor, appVersion: t.appVersion, }, after: next, }), ); if (!changed || dryRun) continue; await prisma.tenant.update({ where: { id: t.id }, data: next, }); updated += 1; } // Ensure locked newTheme tenants stay newTheme for (const t of skippedNewTheme) { if (t.uiVariant === KhanegiUiVariant.newTheme) continue; console.log( JSON.stringify({ domain: t.domain, action: dryRun ? 'would_force_newTheme' : 'force_newTheme', before: t.uiVariant, }), ); if (dryRun) continue; await prisma.tenant.update({ where: { id: t.id }, data: { uiVariant: KhanegiUiVariant.newTheme, appVersion: bumpAppVersion(t.appVersion), }, }); } console.log(JSON.stringify({ dryRun, updated, done: true })); } main() .catch((err) => { console.error(err); process.exitCode = 1; }) .finally(async () => { await prisma.$disconnect(); });