import { Injectable, NotFoundException } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; type UiSetting = { key: string; value: string }; @Injectable() export class GetUserService { constructor(private readonly prisma: PrismaService) {} async resolveByHost(hostHeader?: string, domainQuery?: string) { const host = this.normalizeHost(domainQuery || hostHeader || ''); if (!host) { throw new NotFoundException('domain required'); } const tenants = await this.prisma.tenant.findMany({ where: { isActive: true }, }); const tenant = tenants.find((item) => this.matchesHost(item, host)); if (!tenant) { throw new NotFoundException('tenant not found for domain'); } const ui = (tenant.appUiConfig as Record | null) || null; const isHamkarUi = tenant.product === 'hamkar' && ui?.kind === 'hamkar'; const hamkarBrand = isHamkarUi ? ui?.brand : null; const khanegiColors = ui?.app?.theme?.colors; const uiSettings = isHamkarUi && ui?.settingsManaged === true ? this.buildHamkarUiSettings(ui) : []; // Preserve named CSS modes from live registry (e.g. limangold / Montpar). // Fallback to notes marker if JSON field missing on older rows. const noteMode = tenant.notes?.match(/registryMode:([^\s|]+)/)?.[1] || null const registryMode = (typeof ui?.registryMode === 'string' && ui.registryMode.trim()) || noteMode || null const themeMode = registryMode && registryMode !== 'dynamicMode' ? registryMode : 'dynamicMode' const color = hamkarBrand?.primaryColor || khanegiColors?.primary || tenant.primaryColor || null const secondary = hamkarBrand?.secondaryColor || khanegiColors?.secondary || tenant.secondaryColor || null const background = hamkarBrand?.backgroundColor || khanegiColors?.background || tenant.backgroundColor || null const branding = { id: tenant.id, name: tenant.nameEn || tenant.slug, name_fa: tenant.nameFa, name_en: tenant.nameEn, avatar: tenant.avatarUrl, color, secondary_color: secondary, background_color: background, node_server: tenant.nodeServer, node_ws: tenant.nodeWs, app_url: tenant.appUrl, admin_url: tenant.adminUrl, product: tenant.product, theme: themeMode, mode: themeMode, app_ui_config: tenant.product === 'khanegi' ? this.stripKhanegiSaMeta(ui) : undefined, /** Laravel-compatible overlay; Hamkar merges only when present */ ui_settings: uiSettings.length ? uiSettings : undefined, }; return { ...branding, user: { ...branding }, }; } private stripKhanegiSaMeta(ui: Record | null) { if (!ui || typeof ui !== 'object') return ui; const { defaultSnapshot: _snap, _backfill, ...rest } = ui; return rest; } private buildHamkarUiSettings(ui: Record): UiSetting[] { const settings = ui?.settings; if (!settings || typeof settings !== 'object') return []; const out: UiSetting[] = []; for (const key of ['pricing_box_theme', 'price_font_size', 'orderUI'] as const) { const value = settings[key]; if (value == null || value === '') continue; out.push({ key, value: String(value) }); } return out; } private matchesHost( tenant: { domain: string; domains: unknown; }, host: string, ) { const candidates = new Set(); for (const value of [tenant.domain, ...this.readDomains(tenant.domains)]) { const normalized = this.normalizeHost(value); if (!normalized) continue; candidates.add(normalized); candidates.add(normalized.replace(/^www\./, '')); candidates.add(`www.${normalized.replace(/^www\./, '')}`); } return candidates.has(host) || candidates.has(host.replace(/^www\./, '')); } private readDomains(raw: unknown): string[] { if (!raw) return []; if (Array.isArray(raw)) return raw.map(String); if (typeof raw === 'string') { try { const parsed = JSON.parse(raw); return Array.isArray(parsed) ? parsed.map(String) : []; } catch { return []; } } return []; } private normalizeHost(value: string) { return value .trim() .toLowerCase() .replace(/^https?:\/\//, '') .split('/')[0] .split(':')[0]; } }