import { Prisma, type Company } from "@prisma/client" import { ZodError } from "zod" import { prisma } from "@/lib/prisma" import { companyFormSchema, companyInputSchema, type CompanyCommunicationChannels, type CompanyFormValues, type CompanyStateRegistrationTypeOption, } from "@/lib/schemas/company" export type NormalizedCompany = CompanyFormValues & { id: string provisioningCode: string | null createdAt: string updatedAt: string } type CompanyCreatePayload = Omit // Local representation of the DB enum to avoid relying on Prisma enum exports type DbCompanyStateRegistrationType = "STANDARD" | "EXEMPT" | "SIMPLES" function asDbStateRegistrationType(value: unknown): DbCompanyStateRegistrationType | undefined { return value === "STANDARD" || value === "EXEMPT" || value === "SIMPLES" ? value : undefined } function slugify(value?: string | null): string { if (!value) return "" const ascii = value .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") .replace(/[^\w\s-]/g, "") const collapsed = ascii.trim().replace(/[_\s]+/g, "-") const sanitized = collapsed.replace(/-+/g, "-").toLowerCase() return sanitized.replace(/^-+|-+$/g, "") } function ensureSlugValue( inputSlug: string | null | undefined, fallbackName: string | null | undefined, fallbackId?: string ): string { const slugFromInput = slugify(inputSlug) if (slugFromInput) return slugFromInput const slugFromName = slugify(fallbackName) if (slugFromName) return slugFromName if (fallbackId) { const slugFromId = slugify(fallbackId) if (slugFromId) return slugFromId return fallbackId.toLowerCase() } return "" } const STATE_REGISTRATION_TYPE_TO_PRISMA: Record< CompanyStateRegistrationTypeOption, DbCompanyStateRegistrationType > = { standard: "STANDARD", exempt: "EXEMPT", simples: "SIMPLES", } const STATE_REGISTRATION_TYPE_FROM_PRISMA: Record< DbCompanyStateRegistrationType, CompanyStateRegistrationTypeOption > = { STANDARD: "standard", EXEMPT: "exempt", SIMPLES: "simples", } const JSON_NULL_VALUE = Prisma.JsonNull as unknown as Prisma.InputJsonValue const COMPANY_LEGACY_SELECT = { id: true, tenantId: true, name: true, slug: true, isAvulso: true, createdAt: true, updatedAt: true, } satisfies Prisma.CompanySelect type LegacyCompanyRow = Prisma.CompanyGetPayload<{ select: typeof COMPANY_LEGACY_SELECT }> function isMissingProvisioningCodeColumn(error: unknown): boolean { if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2022") { const meta = error.meta as Record | undefined const metaColumn = typeof meta?.column === "string" ? meta.column : typeof meta?.column_name === "string" ? meta.column_name : typeof meta?.model === "string" ? meta.model : null if (metaColumn && metaColumn.toLowerCase().includes("provisioningcode")) return true if (error.message.toLowerCase().includes("provisioningcode")) return true } return false } function isMissingCompanyTable(error: unknown): boolean { if (error instanceof Prisma.PrismaClientKnownRequestError) { if (error.code === "P2021") return true const message = error.message.toLowerCase() if (message.includes("table") && message.includes("company") && message.includes("does not exist")) { return true } } return false } export async function safeCompanyFindMany(args: Prisma.CompanyFindManyArgs): Promise { try { return await prisma.company.findMany(args) } catch (error) { if (isMissingCompanyTable(error)) { return [] } if (!isMissingProvisioningCodeColumn(error)) { throw error } if (args.select || args.include) { throw error } const legacyRows = (await prisma.company.findMany({ ...args, select: COMPANY_LEGACY_SELECT, })) as LegacyCompanyRow[] return legacyRows.map( (row) => ({ id: row.id, tenantId: row.tenantId, name: row.name, slug: row.slug, provisioningCode: "", isAvulso: Boolean(row.isAvulso), contractedHoursPerMonth: null, cnpj: null, domain: null, phone: null, description: null, address: null, legalName: null, tradeName: null, stateRegistration: null, stateRegistrationType: null, primaryCnae: null, timezone: null, businessHours: null, supportEmail: null, billingEmail: null, contactPreferences: null, clientDomains: null, communicationChannels: null, fiscalAddress: null, hasBranches: false, regulatedEnvironments: null, privacyPolicyAccepted: false, privacyPolicyReference: null, privacyPolicyMetadata: null, contacts: null, locations: null, contracts: null, sla: null, tags: null, customFields: null, notes: null, createdAt: row.createdAt, updatedAt: row.updatedAt, }) as Company ) } } export function formatZodError(error: ZodError) { return error.issues.map((issue) => ({ path: issue.path.join("."), message: issue.message, })) } function sanitizeDomain(value?: string | null) { if (!value) return null const trimmed = value.trim().toLowerCase() return trimmed.length === 0 ? null : trimmed } function sanitizePhone(value?: string | null) { if (!value) return null const trimmed = value.trim() return trimmed.length === 0 ? null : trimmed } function normalizeChannels( channels?: Partial | null ): CompanyCommunicationChannels { const ensure = (values?: string[]) => Array.from(new Set((values ?? []).map((value) => value.trim()).filter(Boolean))) return { supportEmails: ensure(channels?.supportEmails).map((email) => email.toLowerCase()), billingEmails: ensure(channels?.billingEmails).map((email) => email.toLowerCase()), whatsappNumbers: ensure(channels?.whatsappNumbers), phones: ensure(channels?.phones), portals: ensure(channels?.portals), } } function mergeChannelsWithPrimary( payload: CompanyFormValues, base?: CompanyCommunicationChannels ): CompanyCommunicationChannels { const channels = normalizeChannels(base ?? payload.communicationChannels) const supportEmails = new Set(channels.supportEmails) const billingEmails = new Set(channels.billingEmails) const phones = new Set(channels.phones) if (payload.supportEmail) supportEmails.add(payload.supportEmail.toLowerCase()) if (payload.billingEmail) billingEmails.add(payload.billingEmail.toLowerCase()) if (payload.phone) phones.add(payload.phone) return { supportEmails: Array.from(supportEmails), billingEmails: Array.from(billingEmails), whatsappNumbers: channels.whatsappNumbers, phones: Array.from(phones), portals: channels.portals, } } export function sanitizeCompanyInput(input: unknown, tenantId: string): CompanyFormValues { const parsed = companyInputSchema.safeParse(input) if (!parsed.success) { throw parsed.error } const raw = parsed.data const normalizedName = raw.name?.trim() ?? "" const normalizedSlug = ensureSlugValue(raw.slug, normalizedName, raw.slug ?? raw.name ?? undefined) const cnpjDigits = typeof raw.cnpj === "string" ? raw.cnpj.replace(/\D/g, "").slice(0, 14) : null const normalizedContacts = (raw.contacts ?? []).map((contact) => ({ ...contact, email: contact.email?.trim().toLowerCase(), phone: sanitizePhone(contact.phone), whatsapp: sanitizePhone(contact.whatsapp), preference: Array.from(new Set(contact.preference ?? [])), })) const normalizedLocations = (raw.locations ?? []).map((location) => ({ ...location, responsibleContactId: location.responsibleContactId ?? null, serviceWindow: location.serviceWindow ?? { mode: "inherit", periods: [] }, })) const normalizedContracts = (raw.contracts ?? []).map((contract) => ({ ...contract, scope: Array.from(new Set(contract.scope ?? [])), })) const normalizedTags = Array.from( new Set((raw.tags ?? []).map((tag) => tag.trim()).filter(Boolean)) ) const normalizedCustomFields = (raw.customFields ?? []).map((field) => ({ ...field, label: field.label.trim(), key: field.key.trim(), })) const normalized: CompanyFormValues = companyFormSchema.parse({ ...raw, tenantId, name: normalizedName, slug: normalizedSlug, legalName: raw.legalName?.trim() ?? null, tradeName: raw.tradeName?.trim() ?? null, cnpj: cnpjDigits && cnpjDigits.length === 14 ? cnpjDigits : null, stateRegistration: raw.stateRegistration?.trim() ?? null, primaryCnae: raw.primaryCnae?.trim() ?? null, description: raw.description?.trim() ?? null, domain: sanitizeDomain(raw.domain), phone: sanitizePhone(raw.phone), address: raw.address?.trim() ?? null, communicationChannels: normalizeChannels(raw.communicationChannels), supportEmail: raw.supportEmail?.trim().toLowerCase() ?? null, billingEmail: raw.billingEmail?.trim().toLowerCase() ?? null, clientDomains: Array.from( new Set((raw.clientDomains ?? []).map((domain) => domain.trim().toLowerCase()).filter(Boolean)) ), fiscalAddress: raw.fiscalAddress ?? null, regulatedEnvironments: Array.from(new Set(raw.regulatedEnvironments ?? [])), contacts: normalizedContacts, locations: normalizedLocations, contracts: normalizedContracts, businessHours: raw.businessHours ?? null, sla: raw.sla ?? null, tags: normalizedTags, customFields: normalizedCustomFields, notes: raw.notes?.trim() ?? null, privacyPolicy: raw.privacyPolicy ? { accepted: raw.privacyPolicy.accepted ?? false, reference: raw.privacyPolicy.reference ?? null, metadata: raw.privacyPolicy.metadata, } : { accepted: false, reference: null, }, }) return normalized } export function buildCompanyData(payload: CompanyFormValues, tenantId: string): CompanyCreatePayload { const stateRegistrationType = payload.stateRegistrationType ? STATE_REGISTRATION_TYPE_TO_PRISMA[payload.stateRegistrationType as CompanyStateRegistrationTypeOption] : null const communicationChannels = mergeChannelsWithPrimary(payload) const privacyPolicyMetadata = payload.privacyPolicy?.metadata ?? null const data: CompanyCreatePayload = { tenantId, name: payload.name.trim(), slug: payload.slug.trim(), isAvulso: payload.isAvulso ?? false, contractedHoursPerMonth: payload.contractedHoursPerMonth ?? null, cnpj: payload.cnpj ?? null, domain: payload.domain ?? null, phone: payload.phone ?? null, description: payload.description ?? null, address: payload.address ?? null, legalName: payload.legalName ?? null, tradeName: payload.tradeName ?? null, stateRegistration: payload.stateRegistration ?? null, stateRegistrationType, primaryCnae: payload.primaryCnae ?? null, timezone: payload.businessHours?.timezone ?? null, businessHours: payload.businessHours ?? JSON_NULL_VALUE, supportEmail: payload.supportEmail ?? null, billingEmail: payload.billingEmail ?? null, contactPreferences: payload.contactPreferences || payload.supportEmail || payload.billingEmail ? ({ ...payload.contactPreferences, supportEmail: payload.supportEmail ?? null, billingEmail: payload.billingEmail ?? null, } satisfies Prisma.InputJsonValue) : JSON_NULL_VALUE, clientDomains: payload.clientDomains, communicationChannels, fiscalAddress: payload.fiscalAddress ?? JSON_NULL_VALUE, hasBranches: payload.hasBranches ?? false, regulatedEnvironments: payload.regulatedEnvironments, privacyPolicyAccepted: payload.privacyPolicy?.accepted ?? false, privacyPolicyReference: payload.privacyPolicy?.reference ?? null, privacyPolicyMetadata: privacyPolicyMetadata ? (privacyPolicyMetadata as Prisma.InputJsonValue) : JSON_NULL_VALUE, contacts: payload.contacts, locations: payload.locations, contracts: payload.contracts, sla: payload.sla ?? JSON_NULL_VALUE, tags: payload.tags, customFields: payload.customFields, notes: payload.notes ?? null, } return data } export function normalizeCompany(company: Company): NormalizedCompany { const communicationChannels = normalizeChannels( company.communicationChannels as CompanyCommunicationChannels | null | undefined ) const normalizedName = (company.name ?? "").trim() const normalizedSlug = ensureSlugValue(company.slug, normalizedName || company.name, company.id) const base: CompanyFormValues = { tenantId: company.tenantId, name: normalizedName, slug: normalizedSlug, legalName: company.legalName, tradeName: company.tradeName, cnpj: company.cnpj, stateRegistration: company.stateRegistration, stateRegistrationType: (() => { const t = asDbStateRegistrationType(company.stateRegistrationType) return t ? STATE_REGISTRATION_TYPE_FROM_PRISMA[t] : undefined })(), primaryCnae: company.primaryCnae, description: company.description, domain: company.domain, phone: company.phone, address: company.address, contractedHoursPerMonth: company.contractedHoursPerMonth, businessHours: (company.businessHours as CompanyFormValues["businessHours"]) ?? null, communicationChannels, supportEmail: company.supportEmail, billingEmail: company.billingEmail, contactPreferences: (company.contactPreferences as CompanyFormValues["contactPreferences"]) ?? undefined, clientDomains: (company.clientDomains as string[] | null) ?? [], fiscalAddress: (company.fiscalAddress as CompanyFormValues["fiscalAddress"]) ?? null, hasBranches: Boolean(company.hasBranches), regulatedEnvironments: (company.regulatedEnvironments as string[] | null) ?? [], privacyPolicy: { accepted: Boolean(company.privacyPolicyAccepted), reference: company.privacyPolicyReference ?? null, metadata: company.privacyPolicyMetadata ? (company.privacyPolicyMetadata as Record) : undefined, }, contacts: (company.contacts as CompanyFormValues["contacts"]) ?? [], locations: (company.locations as CompanyFormValues["locations"]) ?? [], contracts: (company.contracts as CompanyFormValues["contracts"]) ?? [], sla: (company.sla as CompanyFormValues["sla"]) ?? null, tags: (company.tags as string[] | null) ?? [], customFields: (company.customFields as CompanyFormValues["customFields"]) ?? [], notes: company.notes ?? null, isAvulso: Boolean(company.isAvulso), } const payload = companyFormSchema.parse({ ...base, communicationChannels: mergeChannelsWithPrimary(base, communicationChannels), }) return { ...payload, id: company.id, provisioningCode: company.provisioningCode && company.provisioningCode.trim().length > 0 ? company.provisioningCode : null, createdAt: company.createdAt.toISOString(), updatedAt: company.updatedAt.toISOString(), } } export async function fetchCompaniesByTenant(tenantId: string): Promise { return safeCompanyFindMany({ where: { tenantId }, orderBy: { name: "asc" }, }) } export async function fetchCompanyById(id: string): Promise { const rows = await safeCompanyFindMany({ where: { id }, take: 1, }) return rows[0] ?? null }