mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 12:43:18 +00:00
- Replace remaining budget.ts prepare() with fn_budget_replace and fn_project_update - Migrate excel.ts findExisting, document store, assign, and catalogs to RPC - Add 015-rpc-documents.sql for project/company/worker document metadata - Document RPC envelope conventions in db/README.md - Add http_errors_test and front api-response composables Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
211 lines
6.8 KiB
TypeScript
211 lines
6.8 KiB
TypeScript
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<T>(env: RpcEnvelope<T>): { error: string; status: number } {
|
|
return { error: env.message, status: mapRpcToStatus(String(env.code)) };
|
|
}
|
|
|
|
export async function listCompanies(database: Db, tenantId?: number | null): Promise<Company[]> {
|
|
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<Company | undefined> {
|
|
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<Company | undefined> {
|
|
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<Company | undefined> {
|
|
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<string> {
|
|
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<Company | undefined> {
|
|
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 };
|
|
}
|