From 3fa24ac96519037faa1762439423abca07710992 Mon Sep 17 00:00:00 2001 From: Mahdi Date: Mon, 3 Aug 2026 20:51:33 +0330 Subject: [PATCH] Add Hamkar default appearance snapshot seed script and builder support. Co-authored-by: Cursor --- package.json | 3 +- prisma/lib/appearance-builders.ts | 38 +++- prisma/seed-hamkar-default-snapshots.ts | 231 ++++++++++++++++++++++++ 3 files changed, 270 insertions(+), 2 deletions(-) create mode 100644 prisma/seed-hamkar-default-snapshots.ts diff --git a/package.json b/package.json index e7020d2..5268825 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "prisma:deploy": "prisma migrate deploy", "prisma:seed": "ts-node --transpile-only prisma/seed.ts", "prisma:import-users": "ts-node --transpile-only prisma/import-users.ts", - "prisma:backfill-appearance": "ts-node --transpile-only prisma/backfill-appearance-from-registry.ts" + "prisma:backfill-appearance": "ts-node --transpile-only prisma/backfill-appearance-from-registry.ts", + "prisma:seed-hamkar-defaults": "ts-node --transpile-only prisma/seed-hamkar-default-snapshots.ts" }, "prisma": { "seed": "ts-node --transpile-only prisma/seed.ts" diff --git a/prisma/lib/appearance-builders.ts b/prisma/lib/appearance-builders.ts index 72280e0..8664009 100644 --- a/prisma/lib/appearance-builders.ts +++ b/prisma/lib/appearance-builders.ts @@ -40,8 +40,21 @@ export function buildHamkarUiFromBrand(input: { price_font_size?: string orderUI?: string } + defaultSnapshot?: { + registryMode?: string | null + brand?: { + primaryColor?: string | null + secondaryColor?: string | null + backgroundColor?: string | null + } + settings?: { + pricing_box_theme?: string + price_font_size?: string + orderUI?: string + } + } | null }) { - return { + const base = { kind: 'hamkar' as const, settingsManaged: Boolean(input.settingsManaged), registryMode: input.registryMode || 'dynamicMode', @@ -57,6 +70,29 @@ export function buildHamkarUiFromBrand(input: { orderUI: String(input.settings?.orderUI || 'old'), }, } + + if (!input.defaultSnapshot) return base + + return { + ...base, + defaultSnapshot: { + registryMode: input.defaultSnapshot.registryMode || base.registryMode, + brand: { + primaryColor: input.defaultSnapshot.brand?.primaryColor ?? null, + secondaryColor: input.defaultSnapshot.brand?.secondaryColor ?? null, + backgroundColor: input.defaultSnapshot.brand?.backgroundColor ?? null, + }, + settings: { + pricing_box_theme: String( + input.defaultSnapshot.settings?.pricing_box_theme || base.settings.pricing_box_theme, + ), + price_font_size: String( + input.defaultSnapshot.settings?.price_font_size || base.settings.price_font_size, + ), + orderUI: String(input.defaultSnapshot.settings?.orderUI || base.settings.orderUI), + }, + }, + } } export function buildKhanegiUiFromColors(input: { diff --git a/prisma/seed-hamkar-default-snapshots.ts b/prisma/seed-hamkar-default-snapshots.ts new file mode 100644 index 0000000..cdb7cbb --- /dev/null +++ b/prisma/seed-hamkar-default-snapshots.ts @@ -0,0 +1,231 @@ +/** + * Seed Hamkar defaultSnapshot from users.json (original registry look). + * Optionally restore current appearance to that snapshot (--restore). + * + * Usage: + * npx ts-node --transpile-only prisma/seed-hamkar-default-snapshots.ts + * npx ts-node --transpile-only prisma/seed-hamkar-default-snapshots.ts --restore + * npx ts-node --transpile-only prisma/seed-hamkar-default-snapshots.ts --restore --domain=montpar.ir + */ +import * as fs from 'fs'; +import * as path from 'path'; +import { PrismaClient, ProductType } from '@prisma/client'; +import { buildHamkarUiFromBrand } from './lib/appearance-builders'; + +const prisma = new PrismaClient(); + +type SourceUser = { + id: number; + name?: string | null; + name_fa?: string | null; + domain?: string | null; + domains?: string | string[] | null; + api_origin?: string | null; + mode?: string | null; + theme?: string | null; + color?: string | null; + secondary_color?: string | null; + background_color?: string | null; + deleted_at?: string | null; +}; + +function parseArgs() { + const restore = process.argv.includes('--restore'); + const domainArg = process.argv.find((a) => a.startsWith('--domain=')); + const domain = domainArg ? domainArg.slice('--domain='.length).trim().toLowerCase() : null; + return { restore, domain }; +} + +function parseDomains(raw: SourceUser['domains']): string[] { + if (!raw) return []; + try { + const value = typeof raw === 'string' ? JSON.parse(raw) : raw; + if (!Array.isArray(value)) return []; + return value + .map((item) => String(item).trim().toLowerCase()) + .filter(Boolean) + .map((item) => item.replace(/^https?:\/\//, '').split('/')[0]); + } catch { + return []; + } +} + +function primaryDomain(row: SourceUser): string | null { + const fromField = row.domain?.trim().toLowerCase(); + if (fromField) return fromField.replace(/^https?:\/\//, '').split('/')[0]; + const fromOrigin = row.api_origin?.trim().toLowerCase(); + if (fromOrigin) return fromOrigin.replace(/^https?:\/\//, '').split('/')[0]; + const domains = parseDomains(row.domains); + return domains[0] || null; +} + +function emptyToNull(v: string | null | undefined) { + const s = String(v ?? '').trim(); + if (!s || s === 'null' || s === 'undefined') return null; + return s; +} + +async function main() { + const { restore, domain: onlyDomain } = parseArgs(); + const file = path.join(__dirname, 'data', 'users.json'); + if (!fs.existsSync(file)) { + throw new Error(`Missing ${file}`); + } + + const rows = JSON.parse(fs.readFileSync(file, 'utf8')) as SourceUser[]; + const byDomain = new Map(); + for (const row of rows) { + if (row.deleted_at) continue; + const d = primaryDomain(row); + if (!d) continue; + if (!byDomain.has(d)) byDomain.set(d, row); + for (const extra of parseDomains(row.domains)) { + if (!byDomain.has(extra)) byDomain.set(extra, row); + } + } + + const tenants = await prisma.tenant.findMany({ + where: { product: ProductType.hamkar }, + }); + + const filtered = onlyDomain + ? tenants.filter((tenant) => { + const keys = [ + tenant.domain?.toLowerCase(), + ...((Array.isArray(tenant.domains) ? tenant.domains : []) as string[]).map((d) => + String(d).toLowerCase(), + ), + ]; + return keys.includes(onlyDomain); + }) + : tenants; + + let updated = 0; + let restored = 0; + let skipped = 0; + + for (const tenant of filtered) { + const domainKeys = [ + tenant.domain?.toLowerCase(), + ...((Array.isArray(tenant.domains) ? tenant.domains : []) as string[]).map((d) => + String(d).toLowerCase(), + ), + ].filter(Boolean) as string[]; + + const source = + domainKeys.map((d) => byDomain.get(d)).find(Boolean) || + null; + + const noteMode = + tenant.notes?.match(/registryMode:([^\s|]+)/)?.[1] || + tenant.notes?.match(/(?:^|\|)\s*mode:([^\s|]+)/)?.[1] || + null; + + const registryMode = + (source?.mode && String(source.mode).trim()) || + (source?.theme && String(source.theme).trim()) || + noteMode || + 'dynamicMode'; + + const brandPrimary = emptyToNull(source?.color) ?? emptyToNull(tenant.primaryColor); + const brandSecondary = + emptyToNull(source?.secondary_color) ?? emptyToNull(tenant.secondaryColor); + const brandBackground = + emptyToNull(source?.background_color) ?? emptyToNull(tenant.backgroundColor); + + const existing = + tenant.appUiConfig && + typeof tenant.appUiConfig === 'object' && + (tenant.appUiConfig as any).kind === 'hamkar' + ? (tenant.appUiConfig as Record) + : null; + + const defaultSnapshot = { + registryMode, + brand: { + primaryColor: brandPrimary, + secondaryColor: brandSecondary, + backgroundColor: brandBackground, + }, + settings: { + pricing_box_theme: String(existing?.settings?.pricing_box_theme || '1'), + price_font_size: String(existing?.settings?.price_font_size || 'بزرگ'), + orderUI: String(existing?.settings?.orderUI || 'old'), + }, + }; + + // Never overwrite an existing defaultSnapshot (immutable factory look) + const keepSnapshot = + existing?.defaultSnapshot && typeof existing.defaultSnapshot === 'object' + ? existing.defaultSnapshot + : defaultSnapshot; + + const nextUi = buildHamkarUiFromBrand({ + primaryColor: restore + ? keepSnapshot.brand?.primaryColor + : existing?.brand?.primaryColor ?? brandPrimary, + secondaryColor: restore + ? keepSnapshot.brand?.secondaryColor + : existing?.brand?.secondaryColor ?? brandSecondary, + backgroundColor: restore + ? keepSnapshot.brand?.backgroundColor + : existing?.brand?.backgroundColor ?? brandBackground, + settingsManaged: Boolean(existing?.settingsManaged), + registryMode: restore + ? keepSnapshot.registryMode + : existing?.registryMode || registryMode, + settings: restore ? keepSnapshot.settings : existing?.settings, + }); + + const appUiConfig = { + ...nextUi, + defaultSnapshot: keepSnapshot, + settings: nextUi.settings, + }; + + const unchanged = + JSON.stringify(existing) === JSON.stringify(appUiConfig) && !restore; + if (unchanged) { + skipped += 1; + continue; + } + + await prisma.tenant.update({ + where: { id: tenant.id }, + data: { + appUiConfig: appUiConfig as any, + ...(restore + ? { + primaryColor: keepSnapshot.brand?.primaryColor ?? null, + secondaryColor: keepSnapshot.brand?.secondaryColor ?? null, + backgroundColor: keepSnapshot.brand?.backgroundColor ?? null, + } + : {}), + }, + }); + + updated += 1; + if (restore) restored += 1; + console.log( + `${restore ? 'restored' : 'seeded'}: ${tenant.domain} → default=${keepSnapshot.registryMode}`, + ); + } + + console.log({ + tenants: filtered.length, + updated, + restored, + skipped, + restore, + domain: onlyDomain, + }); +} + +main() + .catch((err) => { + console.error(err); + process.exitCode = 1; + }) + .finally(async () => { + await prisma.$disconnect(); + });