From 425eb3c7141528cae75b815d523d67b1d4fbee11 Mon Sep 17 00:00:00 2001 From: Mahdi Date: Mon, 10 Aug 2026 11:42:31 +0330 Subject: [PATCH] feat: bulk apply Niazi modular brand colors to Khanegi tenants Add prisma:bulk-khanegi-modular-brand (skip Aygin/newTheme). Palette: header #0a1b37, accent #e0be7f, bg #ffffff; bumps appVersion. Co-authored-by: Cursor --- package.json | 1 + prisma/bulk-khanegi-modular-brand.ts | 237 +++++++++++++++++++++++++++ 2 files changed, 238 insertions(+) create mode 100644 prisma/bulk-khanegi-modular-brand.ts diff --git a/package.json b/package.json index dc4a3dc..2a96ce2 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "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:bulk-khanegi-modular-brand": "ts-node --transpile-only prisma/bulk-khanegi-modular-brand.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" }, diff --git a/prisma/bulk-khanegi-modular-brand.ts b/prisma/bulk-khanegi-modular-brand.ts new file mode 100644 index 0000000..00a2a90 --- /dev/null +++ b/prisma/bulk-khanegi-modular-brand.ts @@ -0,0 +1,237 @@ +/** + * Apply Niazi-style modular brand colors to all Khanegi tenants + * that are NOT newTheme (Aygin stays newTheme). + * + * Colors (from app.niazitala.ir): + * primary #0a1b37 (هدر) + * secondary #e0be7f (اکسنت) + * background #ffffff (پس‌زمینه) + * + * Also forces uiVariant=modular for non-Aygin 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'; + +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 isAyginTenant(domain: string, domains: unknown, slug: string) { + const hosts = new Set(); + const primary = normalizeHost(domain); + if (primary) hosts.add(primary); + const extra = Array.isArray(domains) ? domains : []; + for (const d of extra) { + const n = normalizeHost(String(d)); + if (n) hosts.add(n); + } + if (String(slug || '').toLowerCase().includes('aygin')) return true; + for (const h of hosts) { + if (h.includes('aygingold') || h === 'app.aygingold.ir') return true; + } + return false; +} + +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) => !isAyginTenant(t.domain, t.domains, t.slug), + ); + const skippedAygin = tenants.filter((t) => + isAyginTenant(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, + skippedAygin: skippedAygin.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 Aygin stays newTheme (do not touch colors unless variant wrong) + for (const t of skippedAygin) { + 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(); + });