Files
api/prisma/bulk-khanegi-modular-brand.ts
MahdiandCursor dd4850d9fa fix: lock new.gholaktala.ir as Khanegi newTheme (userTheme)
Correct prior modular/Hamkar misclassification. Protect locked newTheme domains from bulk-modular brand overrides.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-11 17:53:04 +03:30

228 lines
6.0 KiB
TypeScript

/**
* 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<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 (!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();
});