import type { Db } from "./db.ts"; import { callCoreFn, RpcCallError } from "./rpc.ts"; import type { RpcEnvelope } from "./http_errors.ts"; import { mapRpcToStatus } from "./http_errors.ts"; export type Company = { id: number; code: string; name: string; parent_id: number | null; kind: "principal" | "sub"; status: "activo" | "inactivo"; tenant_id?: number | null; registro_patronal?: string; razon_social?: string; nombre_comercial?: string; rfc?: string; regimen_fiscal?: string; clase_riesgo?: string; domicilio_fiscal?: string; codigo_postal?: string; ciudad?: string; estado?: string; telefono?: string; email?: string; representante_legal?: string; giro?: string; created_at?: string; parent_code?: string | null; parent_name?: string | null; worker_count?: number; }; export type CompanyProfileInput = { name?: string; code?: string; status?: string; parent_id?: number | null; registro_patronal?: string; razon_social?: string; nombre_comercial?: string; rfc?: string; regimen_fiscal?: string; clase_riesgo?: string; domicilio_fiscal?: string; codigo_postal?: string; ciudad?: string; estado?: string; telefono?: string; email?: string; representante_legal?: string; giro?: string; }; const CODE_RE = /^[A-Z0-9][A-Z0-9_-]{1,15}$/; const RFC_RE = /^[A-ZÑ&]{3,4}\d{6}[A-Z0-9]{3}$/; function trimText(value: unknown): string { return (value ?? "").toString().trim(); } function normRfc(value: unknown): string { return trimText(value).toUpperCase().replace(/\s+/g, ""); } function envelopeError(env: RpcEnvelope): { error: string; status: number } { return { error: env.message, status: mapRpcToStatus(String(env.code)) }; } export async function listCompanies(database: Db, tenantId?: number | null): Promise { const env = await callCoreFn<{ companies: Company[] }>(database, "core.fn_company_list", { tenant_id: tenantId ?? null, }); if (!env.ok) throw new RpcCallError(env); return (env.data?.companies ?? []) as Company[]; } export async function companyById(database: Db, id: number): Promise { const env = await callCoreFn<{ company: Company }>(database, "core.fn_company_get", { id }); if (!env.ok) return undefined; return env.data?.company as Company | undefined; } export async function companyByCode(database: Db, code: string): Promise { const env = await callCoreFn<{ company: Company }>(database, "core.fn_company_get_by_code", { code: normCompanyCode(code), }); if (!env.ok) return undefined; return env.data?.company as Company | undefined; } export async function principalCompany(database: Db, tenantId?: number | null): Promise { const companies = await listCompanies(database, tenantId); return companies.find((c) => c.kind === "principal"); } export function normCompanyCode(value: string | null | undefined): string { return (value ?? "").toString().trim().toUpperCase().replace(/\s+/g, ""); } export function companyCodeFromName(name: string): string { const slug = name .normalize("NFD") .replace(/\p{M}/gu, "") .toUpperCase() .replace(/[^A-Z0-9]+/g, "") .slice(0, 12); return slug || "EMP"; } export async function nextCompanyCode(database: Db, name: string): Promise { const env = await callCoreFn<{ code: string }>(database, "core.fn_next_company_code", { name }); if (!env.ok) throw new RpcCallError(env); return String(env.data?.code ?? companyCodeFromName(name)); } export async function resolveCompany( database: Db, body: { company_id?: number | null; hire_type?: string | null }, ): Promise { if (body.company_id) { const byId = await companyById(database, Number(body.company_id)); if (byId) return byId; } const code = normCompanyCode(body.hire_type); if (code) return await companyByCode(database, code); return undefined; } export function validateCompanyCode(code: string): string | null { if (!CODE_RE.test(code)) { return "Código de 2 a 16 caracteres (letras, números, _ o -)"; } return null; } export function validateCompanyProfile( input: CompanyProfileInput, opts: { requireLegal?: boolean } = {}, ): string | null { const rfc = normRfc(input.rfc); if (rfc && !RFC_RE.test(rfc)) { return "RFC inválido (formato mexicano de 12 o 13 caracteres)"; } if (opts.requireLegal) { if (!trimText(input.razon_social) && !trimText(input.name) && !trimText(input.nombre_comercial)) { return "Indique razón social o nombre comercial"; } } const clase = trimText(input.clase_riesgo).toUpperCase(); if (clase && !["I", "II", "III", "IV", "V"].includes(clase)) { return "Clase de riesgo IMSS debe ser I, II, III, IV o V"; } return null; } export function normalizeCompanyProfile(input: CompanyProfileInput, fallbackName = "") { const nombreComercial = trimText(input.nombre_comercial) || trimText(input.name) || fallbackName; const razonSocial = trimText(input.razon_social) || nombreComercial || fallbackName; const name = trimText(input.name) || nombreComercial || razonSocial || fallbackName; return { name, nombre_comercial: nombreComercial, razon_social: razonSocial, rfc: normRfc(input.rfc), regimen_fiscal: trimText(input.regimen_fiscal), registro_patronal: trimText(input.registro_patronal).toUpperCase(), clase_riesgo: trimText(input.clase_riesgo).toUpperCase(), domicilio_fiscal: trimText(input.domicilio_fiscal), codigo_postal: trimText(input.codigo_postal), ciudad: trimText(input.ciudad), estado: trimText(input.estado), telefono: trimText(input.telefono), email: trimText(input.email).toLowerCase(), representante_legal: trimText(input.representante_legal), giro: trimText(input.giro), }; } export async function createSubcompany( database: Db, input: CompanyProfileInput, tenantId?: number | null, ): Promise<{ company?: Company; error?: string; status?: number }> { const profileErr = validateCompanyProfile(input, { requireLegal: true }); if (profileErr) return { error: profileErr, status: 400 }; const env = await callCoreFn<{ company: Company }>(database, "core.fn_company_create", { ...input, tenant_id: tenantId ?? null, }); if (!env.ok) return envelopeError(env); return { company: env.data?.company as Company }; } export async function updateCompany( database: Db, id: number, input: CompanyProfileInput, tenantId?: number | null, ): Promise<{ company?: Company; error?: string; status?: 400 | 404 }> { const env = await callCoreFn<{ company: Company }>(database, "core.fn_company_update", { id, tenant_id: tenantId ?? null, ...input, }); if (!env.ok) { const st = mapRpcToStatus(String(env.code)); return { error: env.message, status: st === 404 ? 404 : 400 }; } return { company: env.data?.company as Company }; }