Initial commit - Horux Despachos NL

This commit is contained in:
2026-05-03 16:47:53 -06:00
commit b00b677c54
647 changed files with 133843 additions and 0 deletions

View File

@@ -0,0 +1,244 @@
-- Migration: 001_initial_schema
-- Description: Full initial DDL for tenant databases (horux_<rfc>)
-- Created: 2026-04-13
-- Tables: rfcs, bancos, cfdis, cfdi_conceptos, conciliaciones, alertas, recordatorios
-- Extensions
CREATE EXTENSION IF NOT EXISTS pg_trgm;
-- Tables
CREATE TABLE IF NOT EXISTS rfcs (
id SERIAL PRIMARY KEY,
rfc VARCHAR(14) UNIQUE NOT NULL,
razon_social VARCHAR(255),
regimen_fiscal VARCHAR(3),
codigo_postal VARCHAR(5)
);
CREATE TABLE IF NOT EXISTS bancos (
id SERIAL PRIMARY KEY,
banco VARCHAR(100) NOT NULL,
terminacion_cuenta VARCHAR(4) NOT NULL,
creado_en TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS cfdis (
id SERIAL PRIMARY KEY,
year VARCHAR(4),
month VARCHAR(2),
type VARCHAR(10),
uuid VARCHAR(36) UNIQUE,
serie VARCHAR(50),
folio VARCHAR(50),
status VARCHAR(20),
fecha_emision TIMESTAMP,
rfc_emisor_id INTEGER REFERENCES rfcs(id),
rfc_emisor VARCHAR(13),
nombre_emisor VARCHAR(255),
rfc_receptor_id INTEGER REFERENCES rfcs(id),
rfc_receptor VARCHAR(13),
nombre_receptor VARCHAR(255),
subtotal NUMERIC(18,4),
subtotal_mxn NUMERIC(18,4),
descuento NUMERIC(18,4),
descuento_mxn NUMERIC(18,4),
total NUMERIC(18,4),
total_mxn NUMERIC(18,4),
saldo_insoluto TEXT,
moneda VARCHAR(3),
tipo_cambio NUMERIC(18,6),
tipo_comprobante VARCHAR(1),
metodo_pago VARCHAR(3),
forma_pago VARCHAR(2),
uso_cfdi VARCHAR(5),
pac VARCHAR(13),
fecha_cert_sat TIMESTAMP,
fecha_cancelacion TIMESTAMP,
uuid_relacionado TEXT,
isr_retencion NUMERIC(18,4),
isr_retencion_mxn NUMERIC(18,4),
iva_traslado NUMERIC(18,4),
iva_traslado_mxn NUMERIC(18,4),
iva_retencion NUMERIC(18,4),
iva_retencion_mxn NUMERIC(18,4),
ieps_traslado NUMERIC(18,4),
ieps_traslado_mxn NUMERIC(18,4),
ieps_retencion NUMERIC(18,4),
ieps_retencion_mxn NUMERIC(18,4),
impuestos_locales_trasladado NUMERIC(18,4),
impuestos_locales_trasladado_mxn NUMERIC(18,4),
impuestos_locales_retenidos NUMERIC(18,4),
impuestos_locales_retenidos_mxn NUMERIC(18,4),
monto_pago NUMERIC(18,4),
monto_pago_mxn NUMERIC(18,4),
fecha_pago_p TIMESTAMP,
num_parcialidad TEXT,
isr_retencion_pago NUMERIC(18,4),
isr_retencion_pago_mxn NUMERIC(18,4),
iva_traslado_pago NUMERIC(18,4),
iva_traslado_pago_mxn NUMERIC(18,4),
iva_retencion_pago NUMERIC(18,4),
iva_retencion_pago_mxn NUMERIC(18,4),
ieps_traslado_pago NUMERIC(18,4),
ieps_traslado_pago_mxn NUMERIC(18,4),
ieps_retencion_pago NUMERIC(18,4),
ieps_retencion_pago_mxn NUMERIC(18,4),
saldo_pendiente NUMERIC(18,4),
saldo_pendiente_mxn NUMERIC(18,4),
fecha_liquidacion TIMESTAMP,
fecha_pago DATE,
fecha_inicial_pago DATE,
fecha_final_pago DATE,
num_dias_pagados NUMERIC(10,2),
num_seguro_social VARCHAR(50),
puesto VARCHAR(255),
salario_base_cot_apor NUMERIC(18,4),
salario_base_cot_apor_mxn NUMERIC(18,4),
salario_diario_integrado NUMERIC(18,4),
salario_diario_integrado_mxn NUMERIC(18,4),
total_percepciones NUMERIC(18,4),
total_percepciones_mxn NUMERIC(18,4),
total_deducciones NUMERIC(18,4),
total_deducciones_mxn NUMERIC(18,4),
imp_retenidos_nomina NUMERIC(18,4),
imp_retenidos_nomina_mxn NUMERIC(18,4),
otras_deducciones_nomina NUMERIC(18,4),
otras_deducciones_nomina_mxn NUMERIC(18,4),
subsidio_causado NUMERIC(18,4),
subsidio_causado_mxn NUMERIC(18,4),
conciliado VARCHAR(50),
id_conciliacion INTEGER,
xml_url TEXT,
pdf_url TEXT,
xml_original TEXT,
last_sat_sync TIMESTAMP,
sat_sync_job_id UUID,
source VARCHAR(20) DEFAULT 'manual',
facturapi_id VARCHAR(50),
regimen_fiscal_emisor VARCHAR(3),
regimen_fiscal_receptor VARCHAR(3),
creado_en TIMESTAMP DEFAULT NOW(),
actualizado_en TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS cfdi_conceptos (
id SERIAL PRIMARY KEY,
cfdi_id INTEGER REFERENCES cfdis(id) ON DELETE CASCADE,
clave_prod_serv VARCHAR(10),
no_identificacion VARCHAR(100),
descripcion TEXT,
cantidad NUMERIC(18,4),
clave_unidad VARCHAR(10),
unidad VARCHAR(100),
valor_unitario NUMERIC(18,4),
valor_unitario_mxn NUMERIC(18,4),
importe NUMERIC(18,4),
importe_mxn NUMERIC(18,4),
descuento NUMERIC(18,4),
descuento_mxn NUMERIC(18,4),
isr_retencion NUMERIC(18,4),
isr_retencion_mxn NUMERIC(18,4),
iva_traslado NUMERIC(18,4),
iva_traslado_mxn NUMERIC(18,4),
iva_retencion NUMERIC(18,4),
iva_retencion_mxn NUMERIC(18,4),
ieps_traslado NUMERIC(18,4),
ieps_traslado_mxn NUMERIC(18,4),
ieps_retencion NUMERIC(18,4),
ieps_retencion_mxn NUMERIC(18,4),
impuestos_locales_trasladado NUMERIC(18,4),
impuestos_locales_trasladado_mxn NUMERIC(18,4),
impuestos_locales_retenidos NUMERIC(18,4),
impuestos_locales_retenidos_mxn NUMERIC(18,4),
total_percepciones NUMERIC(18,4),
total_percepciones_mxn NUMERIC(18,4),
total_deducciones NUMERIC(18,4),
total_deducciones_mxn NUMERIC(18,4),
imp_retenidos_nomina NUMERIC(18,4),
imp_retenidos_nomina_mxn NUMERIC(18,4),
otras_deducciones_nomina NUMERIC(18,4),
otras_deducciones_nomina_mxn NUMERIC(18,4),
subsidio_causado NUMERIC(18,4),
subsidio_causado_mxn NUMERIC(18,4),
creado_en TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS conciliaciones (
id SERIAL PRIMARY KEY,
anio VARCHAR(4) NOT NULL,
mes VARCHAR(2) NOT NULL,
id_cfdi INTEGER NOT NULL UNIQUE REFERENCES cfdis(id),
fecha_de_pago DATE NOT NULL,
id_banco INTEGER NOT NULL REFERENCES bancos(id),
creado_en TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS alertas (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tipo VARCHAR(50) NOT NULL,
titulo VARCHAR(200) NOT NULL,
mensaje TEXT,
prioridad VARCHAR(20) DEFAULT 'media',
fecha_vencimiento TIMESTAMP,
leida BOOLEAN DEFAULT FALSE,
resuelta BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS recordatorios (
id SERIAL PRIMARY KEY,
titulo VARCHAR(200) NOT NULL,
descripcion TEXT,
fecha_limite DATE NOT NULL,
notas TEXT,
completado BOOLEAN DEFAULT FALSE,
privado BOOLEAN DEFAULT FALSE,
creado_por UUID NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- =============================================
-- Columns that may be missing on older tenants
-- (CREATE TABLE IF NOT EXISTS won't add these if the table already existed)
-- =============================================
ALTER TABLE cfdis ADD COLUMN IF NOT EXISTS id_conciliacion INTEGER;
ALTER TABLE cfdis ADD COLUMN IF NOT EXISTS conciliado VARCHAR(50);
ALTER TABLE cfdis ADD COLUMN IF NOT EXISTS source VARCHAR(20) DEFAULT 'manual';
ALTER TABLE cfdis ADD COLUMN IF NOT EXISTS facturapi_id VARCHAR(50);
ALTER TABLE cfdis ADD COLUMN IF NOT EXISTS regimen_fiscal_emisor VARCHAR(3);
ALTER TABLE cfdis ADD COLUMN IF NOT EXISTS regimen_fiscal_receptor VARCHAR(3);
ALTER TABLE cfdis ADD COLUMN IF NOT EXISTS last_sat_sync TIMESTAMP;
ALTER TABLE cfdis ADD COLUMN IF NOT EXISTS sat_sync_job_id UUID;
-- Indexes
CREATE INDEX IF NOT EXISTS idx_cfdis_fecha_emision ON cfdis(fecha_emision DESC);
CREATE INDEX IF NOT EXISTS idx_cfdis_type ON cfdis(type);
CREATE INDEX IF NOT EXISTS idx_cfdis_rfc_emisor ON cfdis(rfc_emisor);
CREATE INDEX IF NOT EXISTS idx_cfdis_rfc_receptor ON cfdis(rfc_receptor);
CREATE INDEX IF NOT EXISTS idx_cfdis_status ON cfdis(status);
CREATE INDEX IF NOT EXISTS idx_cfdis_year_month ON cfdis(year, month);
CREATE INDEX IF NOT EXISTS idx_cfdis_nombre_emisor_trgm ON cfdis USING gin(nombre_emisor gin_trgm_ops);
CREATE INDEX IF NOT EXISTS idx_cfdis_nombre_receptor_trgm ON cfdis USING gin(nombre_receptor gin_trgm_ops);
CREATE INDEX IF NOT EXISTS idx_cfdis_rfc_emisor_id ON cfdis(rfc_emisor_id);
CREATE INDEX IF NOT EXISTS idx_cfdis_rfc_receptor_id ON cfdis(rfc_receptor_id);
CREATE INDEX IF NOT EXISTS idx_cfdi_conceptos_cfdi_id ON cfdi_conceptos(cfdi_id);
CREATE INDEX IF NOT EXISTS idx_cfdi_conceptos_clave ON cfdi_conceptos(clave_prod_serv);
CREATE INDEX IF NOT EXISTS idx_conciliaciones_anio_mes ON conciliaciones(anio, mes);
CREATE INDEX IF NOT EXISTS idx_conciliaciones_id_cfdi ON conciliaciones(id_cfdi);
CREATE INDEX IF NOT EXISTS idx_cfdis_id_conciliacion ON cfdis(id_conciliacion);
-- Deferred FK: cfdis.id_conciliacion -> conciliaciones(id)
-- (cfdis is created before conciliaciones, so this constraint is added after both tables exist)
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'cfdis_id_conciliacion_fkey') THEN
ALTER TABLE cfdis ADD CONSTRAINT cfdis_id_conciliacion_fkey FOREIGN KEY (id_conciliacion) REFERENCES conciliaciones(id);
END IF;
END $$;

View File

@@ -0,0 +1,16 @@
-- 002_create_opiniones_cumplimiento
-- Table for storing SAT Opinión de Cumplimiento PDFs and metadata
CREATE TABLE IF NOT EXISTS opiniones_cumplimiento (
id SERIAL PRIMARY KEY,
rfc VARCHAR(14) NOT NULL,
razon_social VARCHAR(255),
estatus VARCHAR(50) NOT NULL,
folio VARCHAR(50),
cadena_original TEXT,
fecha_consulta TIMESTAMP NOT NULL,
pdf BYTEA NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_opiniones_fecha ON opiniones_cumplimiento(fecha_consulta DESC);

View File

@@ -0,0 +1,36 @@
-- Declaraciones provisionales del tenant: PDFs subidos por el contador con
-- el comprobante de la declaración + opcionalmente el comprobante de pago.
-- Al subir una declaración o un comprobante, el sistema marca como resueltas
-- las alertas correspondientes (decl-XXX o pago-XXX) en la tabla `alertas`.
--
-- Reglas:
-- - 1 declaración tipo='normal' por (año, mes) — UNIQUE parcial
-- - N declaraciones tipo='complementaria' por (año, mes) — sin restricción
-- - `impuestos` es un array de strings: ['IVA', 'ISR', 'IEPS', etc.] que
-- cubre la declaración. Permite saber qué alertas resolver.
CREATE TABLE IF NOT EXISTS declaraciones_provisionales (
id SERIAL PRIMARY KEY,
año INT NOT NULL,
mes INT NOT NULL CHECK (mes BETWEEN 1 AND 12),
tipo VARCHAR(15) NOT NULL CHECK (tipo IN ('normal', 'complementaria')),
impuestos TEXT[] NOT NULL, -- ['IVA', 'ISR', 'IEPS', ...]
pdf_declaracion BYTEA NOT NULL,
pdf_filename VARCHAR(255),
link_pago TEXT,
pdf_pago BYTEA,
pdf_pago_filename VARCHAR(255),
pagado_at TIMESTAMP,
creado_por VARCHAR(255), -- email del user que la subió
notas TEXT,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_declaraciones_periodo ON declaraciones_provisionales(año DESC, mes DESC);
-- Solo 1 declaración tipo='normal' por (año, mes). Las complementarias no
-- tienen restricción de cantidad.
CREATE UNIQUE INDEX IF NOT EXISTS uniq_declaracion_normal_mes
ON declaraciones_provisionales(año, mes)
WHERE tipo = 'normal';

View File

@@ -0,0 +1,11 @@
-- La "liga de pago" de la declaración es un PDF (no un URL). Reemplazamos
-- la columna TEXT por un par BYTEA+filename, consistente con pdf_declaracion
-- y pdf_pago. Si la migración 003 aún no se desplegó en algún ambiente,
-- este ALTER aplica igual (DROP IF EXISTS + ADD COLUMN IF NOT EXISTS).
ALTER TABLE declaraciones_provisionales
DROP COLUMN IF EXISTS link_pago;
ALTER TABLE declaraciones_provisionales
ADD COLUMN IF NOT EXISTS pdf_liga_pago BYTEA,
ADD COLUMN IF NOT EXISTS pdf_liga_pago_filename VARCHAR(255);

View File

@@ -0,0 +1,24 @@
-- Constancia de Situación Fiscal: PDF descargado del portal SAT con Playwright
-- + FIEL. Se descarga automáticamente el 1° de cada mes y al primer upload de
-- FIEL del tenant. Retención 5 años (similar a declaraciones_provisionales).
--
-- `datos` es un JSONB con el shape `ConstanciaSituacionFiscal` del prototipo
-- (domicilio, régimenes activos, actividades, obligaciones, sellos). Se
-- guarda completo para poder re-hidratar la UI sin re-parsear el PDF, y
-- comparar entre consultas (detectar cambios de domicilio/régimen).
CREATE TABLE IF NOT EXISTS constancias_situacion_fiscal (
id SERIAL PRIMARY KEY,
rfc VARCHAR(13) NOT NULL,
id_cif VARCHAR(20),
razon_social TEXT,
estatus_padron VARCHAR(30),
fecha_emision TEXT, -- "GUADALAJARA, JALISCO A 14 DE ABRIL DE 2026" (formato libre del SAT)
datos JSONB NOT NULL, -- shape ConstanciaSituacionFiscal completo
pdf BYTEA NOT NULL,
fecha_consulta TIMESTAMP DEFAULT NOW(),
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_csf_fecha_consulta
ON constancias_situacion_fiscal(fecha_consulta DESC);

View File

@@ -0,0 +1,17 @@
CREATE TABLE IF NOT EXISTS tenant_migrations (
scope varchar(50) NOT NULL,
version int NOT NULL,
name varchar(255),
applied_at timestamptz DEFAULT now(),
PRIMARY KEY (scope, version)
);
INSERT INTO tenant_migrations (scope, version, name)
VALUES
('legacy', 1, '001_initial_schema'),
('legacy', 2, '002_create_opiniones_cumplimiento'),
('legacy', 3, '003_create_declaraciones_provisionales'),
('legacy', 4, '004_declaraciones_liga_pago_pdf'),
('legacy', 5, '005_create_constancias_situacion_fiscal'),
('legacy', 6, '006_tenant_migrations_tracking')
ON CONFLICT (scope, version) DO NOTHING;

View File

@@ -0,0 +1,18 @@
CREATE TABLE IF NOT EXISTS entidades_gestionadas (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tipo varchar(20) NOT NULL,
nombre text NOT NULL,
identificador text,
supervisor_user_id uuid,
active boolean DEFAULT true,
created_at timestamptz DEFAULT now(),
updated_at timestamptz DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_entidades_supervisor ON entidades_gestionadas(supervisor_user_id);
CREATE INDEX IF NOT EXISTS ix_entidades_tipo ON entidades_gestionadas(tipo, active);
CREATE INDEX IF NOT EXISTS ix_entidades_identificador ON entidades_gestionadas(identificador);
INSERT INTO tenant_migrations (scope, version, name)
VALUES ('core', 7, '007_entidades_gestionadas')
ON CONFLICT (scope, version) DO NOTHING;

View File

@@ -0,0 +1,27 @@
CREATE TABLE IF NOT EXISTS carteras (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
supervisor_user_id uuid NOT NULL,
nombre text NOT NULL,
descripcion text,
created_at timestamptz DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_carteras_supervisor ON carteras(supervisor_user_id);
CREATE TABLE IF NOT EXISTS cartera_entidades (
cartera_id uuid NOT NULL REFERENCES carteras(id) ON DELETE CASCADE,
entidad_id uuid NOT NULL REFERENCES entidades_gestionadas(id) ON DELETE CASCADE,
added_at timestamptz DEFAULT now(),
PRIMARY KEY (cartera_id, entidad_id)
);
CREATE TABLE IF NOT EXISTS cartera_auxiliares (
cartera_id uuid NOT NULL REFERENCES carteras(id) ON DELETE CASCADE,
auxiliar_user_id uuid NOT NULL,
added_at timestamptz DEFAULT now(),
PRIMARY KEY (cartera_id, auxiliar_user_id)
);
INSERT INTO tenant_migrations (scope, version, name)
VALUES ('core', 8, '008_carteras')
ON CONFLICT (scope, version) DO NOTHING;

View File

@@ -0,0 +1,10 @@
CREATE TABLE IF NOT EXISTS cliente_accesos (
user_id uuid NOT NULL,
entidad_id uuid NOT NULL REFERENCES entidades_gestionadas(id) ON DELETE CASCADE,
granted_at timestamptz DEFAULT now(),
PRIMARY KEY (user_id, entidad_id)
);
INSERT INTO tenant_migrations (scope, version, name)
VALUES ('core', 9, '009_cliente_accesos')
ON CONFLICT (scope, version) DO NOTHING;

View File

@@ -0,0 +1,13 @@
CREATE TABLE IF NOT EXISTS contribuyentes (
entidad_id uuid PRIMARY KEY REFERENCES entidades_gestionadas(id) ON DELETE CASCADE,
rfc varchar(13) NOT NULL UNIQUE,
regimen_fiscal varchar(3),
codigo_postal varchar(5),
domicilio jsonb
);
CREATE INDEX IF NOT EXISTS ix_contribuyentes_rfc ON contribuyentes(rfc);
INSERT INTO tenant_migrations (scope, version, name)
VALUES ('vertical-contable', 10, '010_contribuyentes')
ON CONFLICT (scope, version) DO NOTHING;

View File

@@ -0,0 +1,23 @@
CREATE TABLE IF NOT EXISTS fiel_contribuyente (
contribuyente_id uuid PRIMARY KEY REFERENCES contribuyentes(entidad_id) ON DELETE CASCADE,
rfc varchar(13) NOT NULL,
cer_data bytea NOT NULL,
key_data bytea NOT NULL,
key_password_enc bytea NOT NULL,
cer_iv bytea NOT NULL,
cer_tag bytea NOT NULL,
key_iv bytea NOT NULL,
key_tag bytea NOT NULL,
password_iv bytea NOT NULL,
password_tag bytea NOT NULL,
serial_number varchar(50),
valid_from timestamptz NOT NULL,
valid_until timestamptz NOT NULL,
is_active boolean DEFAULT true,
uploaded_at timestamptz DEFAULT now(),
updated_at timestamptz DEFAULT now()
);
INSERT INTO tenant_migrations (scope, version, name)
VALUES ('vertical-contable', 11, '011_fiel_per_contribuyente')
ON CONFLICT (scope, version) DO NOTHING;

View File

@@ -0,0 +1,11 @@
CREATE TABLE IF NOT EXISTS facturapi_orgs (
contribuyente_id uuid PRIMARY KEY REFERENCES contribuyentes(entidad_id) ON DELETE CASCADE,
facturapi_org_id text NOT NULL UNIQUE,
csd_uploaded boolean DEFAULT false,
active boolean DEFAULT true,
created_at timestamptz DEFAULT now()
);
INSERT INTO tenant_migrations (scope, version, name)
VALUES ('vertical-contable', 12, '012_facturapi_per_contribuyente')
ON CONFLICT (scope, version) DO NOTHING;

View File

@@ -0,0 +1,7 @@
ALTER TABLE cfdis ADD COLUMN IF NOT EXISTS contribuyente_id uuid REFERENCES contribuyentes(entidad_id);
CREATE INDEX IF NOT EXISTS ix_cfdi_contribuyente ON cfdis(contribuyente_id) WHERE contribuyente_id IS NOT NULL;
INSERT INTO tenant_migrations (scope, version, name)
VALUES ('vertical-contable', 13, '013_cfdi_contribuyente_id')
ON CONFLICT (scope, version) DO NOTHING;

View File

@@ -0,0 +1,52 @@
CREATE TABLE IF NOT EXISTS metricas_mensuales (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
contribuyente_id uuid NOT NULL REFERENCES contribuyentes(entidad_id) ON DELETE CASCADE,
anio smallint NOT NULL,
mes smallint NOT NULL,
regimen_fiscal varchar(3),
formula_version smallint DEFAULT 1,
iva_trasladado_16 numeric(18,2) DEFAULT 0,
iva_trasladado_8 numeric(18,2) DEFAULT 0,
iva_trasladado_0 numeric(18,2) DEFAULT 0,
iva_trasladado_exento numeric(18,2) DEFAULT 0,
iva_trasladado_total numeric(18,2) DEFAULT 0,
iva_acreditable numeric(18,2) DEFAULT 0,
iva_retenido_cobrado numeric(18,2) DEFAULT 0,
iva_retenido_pagado numeric(18,2) DEFAULT 0,
iva_resultado numeric(18,2) DEFAULT 0,
iva_a_favor_mes numeric(18,2) DEFAULT 0,
isr_ingresos_brutos numeric(18,2) DEFAULT 0,
isr_deducciones_autoriz numeric(18,2) DEFAULT 0,
isr_base numeric(18,2) DEFAULT 0,
isr_causado numeric(18,2) DEFAULT 0,
isr_retenido numeric(18,2) DEFAULT 0,
isr_a_pagar numeric(18,2) DEFAULT 0,
ieps_trasladado numeric(18,2) DEFAULT 0,
ieps_acreditable numeric(18,2) DEFAULT 0,
cfdis_emitidos_count int DEFAULT 0,
cfdis_recibidos_count int DEFAULT 0,
cfdis_cancelados_count int DEFAULT 0,
ingresos_devengados numeric(18,2) DEFAULT 0,
ingresos_cobrados numeric(18,2) DEFAULT 0,
egresos_devengados numeric(18,2) DEFAULT 0,
egresos_pagados numeric(18,2) DEFAULT 0,
utilidad_devengada numeric(18,2) DEFAULT 0,
utilidad_realizada numeric(18,2) DEFAULT 0,
flujo_entradas numeric(18,2) DEFAULT 0,
flujo_salidas numeric(18,2) DEFAULT 0,
flujo_neto numeric(18,2) DEFAULT 0,
cxc_saldo_final numeric(18,2) DEFAULT 0,
cxp_saldo_final numeric(18,2) DEFAULT 0,
cxc_cfdis_count int DEFAULT 0,
cxp_cfdis_count int DEFAULT 0,
cerrado boolean DEFAULT false,
computed_at timestamptz DEFAULT now(),
source_max_cfdi_at timestamptz,
UNIQUE (contribuyente_id, anio, mes, regimen_fiscal)
);
CREATE INDEX IF NOT EXISTS ix_metricas_contrib_anio ON metricas_mensuales(contribuyente_id, anio DESC, mes DESC);
CREATE INDEX IF NOT EXISTS ix_metricas_cerrado ON metricas_mensuales(cerrado, computed_at);
INSERT INTO tenant_migrations (scope, version, name)
VALUES ('vertical-contable', 14, '014_metricas_mensuales')
ON CONFLICT (scope, version) DO NOTHING;

View File

@@ -0,0 +1,24 @@
CREATE TABLE IF NOT EXISTS metricas_acumuladas_anuales (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
contribuyente_id uuid NOT NULL REFERENCES contribuyentes(entidad_id) ON DELETE CASCADE,
anio smallint NOT NULL,
regimen_fiscal varchar(3),
formula_version smallint DEFAULT 1,
iva_a_favor_arrastrado numeric(18,2) DEFAULT 0,
iva_a_favor_generado numeric(18,2) DEFAULT 0,
iva_a_favor_aplicado numeric(18,2) DEFAULT 0,
iva_a_favor_saldo numeric(18,2) DEFAULT 0,
ingresos_anuales numeric(18,2) DEFAULT 0,
deducciones_anuales numeric(18,2) DEFAULT 0,
utilidad_anual numeric(18,2) DEFAULT 0,
isr_causado_anual numeric(18,2) DEFAULT 0,
isr_retenido_anual numeric(18,2) DEFAULT 0,
isr_a_pagar_anual numeric(18,2) DEFAULT 0,
cerrado boolean DEFAULT false,
computed_at timestamptz DEFAULT now(),
UNIQUE (contribuyente_id, anio, regimen_fiscal)
);
INSERT INTO tenant_migrations (scope, version, name)
VALUES ('vertical-contable', 15, '015_metricas_acumuladas_anuales')
ON CONFLICT (scope, version) DO NOTHING;

View File

@@ -0,0 +1,19 @@
CREATE TABLE IF NOT EXISTS metricas_por_contraparte_anuales (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
contribuyente_id uuid NOT NULL REFERENCES contribuyentes(entidad_id) ON DELETE CASCADE,
anio smallint NOT NULL,
rfc_contraparte varchar(13) NOT NULL,
nombre_contraparte text,
tipo char(1),
subtotal numeric(18,2),
total numeric(18,2),
cfdis_count int,
concentracion_pct numeric(5,2),
computed_at timestamptz DEFAULT now(),
UNIQUE (contribuyente_id, anio, rfc_contraparte, tipo)
);
CREATE INDEX IF NOT EXISTS ix_metricas_contraparte_top ON metricas_por_contraparte_anuales(contribuyente_id, anio, tipo, total DESC);
INSERT INTO tenant_migrations (scope, version, name)
VALUES ('vertical-contable', 16, '016_metricas_por_contraparte_anuales')
ON CONFLICT (scope, version) DO NOTHING;

View File

@@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS metricas_invalidaciones (
contribuyente_id uuid NOT NULL,
anio smallint NOT NULL,
mes smallint NOT NULL,
reason text,
marcado_at timestamptz DEFAULT now(),
PRIMARY KEY (contribuyente_id, anio, mes)
);
INSERT INTO tenant_migrations (scope, version, name)
VALUES ('vertical-contable', 17, '017_metricas_invalidaciones')
ON CONFLICT (scope, version) DO NOTHING;

View File

@@ -0,0 +1,20 @@
CREATE TABLE IF NOT EXISTS obligaciones_contribuyente (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
contribuyente_id uuid NOT NULL REFERENCES contribuyentes(entidad_id) ON DELETE CASCADE,
catalogo_id text,
nombre text NOT NULL,
fundamento text,
frecuencia text,
fecha_limite text,
categoria text,
activa boolean DEFAULT true,
es_recomendada boolean DEFAULT false,
es_custom boolean DEFAULT false,
created_at timestamptz DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_obligaciones_contrib ON obligaciones_contribuyente(contribuyente_id, activa);
INSERT INTO tenant_migrations (scope, version, name)
VALUES ('vertical-contable', 18, '018_obligaciones_contribuyente')
ON CONFLICT (scope, version) DO NOTHING;

View File

@@ -0,0 +1,8 @@
ALTER TABLE obligaciones_contribuyente ADD COLUMN IF NOT EXISTS completada boolean DEFAULT false;
ALTER TABLE obligaciones_contribuyente ADD COLUMN IF NOT EXISTS completada_at timestamptz;
ALTER TABLE obligaciones_contribuyente ADD COLUMN IF NOT EXISTS completada_por uuid;
ALTER TABLE obligaciones_contribuyente ADD COLUMN IF NOT EXISTS periodo_completado varchar(7); -- "2026-04" (year-month)
INSERT INTO tenant_migrations (scope, version, name)
VALUES ('vertical-contable', 19, '019_obligaciones_completada')
ON CONFLICT (scope, version) DO NOTHING;

View File

@@ -0,0 +1,17 @@
CREATE TABLE IF NOT EXISTS obligacion_periodos (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
obligacion_id uuid NOT NULL REFERENCES obligaciones_contribuyente(id) ON DELETE CASCADE,
periodo varchar(7) NOT NULL,
completada boolean DEFAULT false,
completada_at timestamptz,
completada_por uuid,
notas text,
created_at timestamptz DEFAULT now(),
UNIQUE (obligacion_id, periodo)
);
CREATE INDEX IF NOT EXISTS ix_obligacion_periodos_periodo ON obligacion_periodos(periodo);
INSERT INTO tenant_migrations (scope, version, name)
VALUES ('vertical-contable', 20, '020_obligacion_periodos')
ON CONFLICT (scope, version) DO NOTHING;

View File

@@ -0,0 +1,15 @@
-- Add periodicidad (period type) and monto_pago (payment amount) to declaraciones.
-- periodicidad replaces the assumption that all declarations are monthly.
-- monto_pago = 0 means the declaration results in $0 to pay (auto-mark as paid).
ALTER TABLE declaraciones_provisionales
ADD COLUMN IF NOT EXISTS periodicidad VARCHAR(15) NOT NULL DEFAULT 'mensual'
CHECK (periodicidad IN ('mensual', 'bimestral', 'trimestral', 'semestral', 'anual'));
ALTER TABLE declaraciones_provisionales
ADD COLUMN IF NOT EXISTS monto_pago NUMERIC(15,2);
-- For existing rows that already have a payment proof, backfill pagado_at if null
UPDATE declaraciones_provisionales
SET pagado_at = updated_at
WHERE pdf_pago IS NOT NULL AND pagado_at IS NULL;

View File

@@ -0,0 +1,23 @@
-- Subcarteras: a cartera can be a child of another cartera.
-- Top-level carteras belong to a supervisor (or owner).
-- Subcarteras belong to an auxiliar within a parent cartera.
ALTER TABLE carteras
ADD COLUMN IF NOT EXISTS parent_id uuid REFERENCES carteras(id) ON DELETE CASCADE;
ALTER TABLE carteras
ADD COLUMN IF NOT EXISTS auxiliar_user_id uuid;
-- Allow supervisor_user_id to be NULL for subcarteras (inherited from parent)
ALTER TABLE carteras
ALTER COLUMN supervisor_user_id DROP NOT NULL;
CREATE INDEX IF NOT EXISTS ix_carteras_parent ON carteras(parent_id);
CREATE INDEX IF NOT EXISTS ix_carteras_auxiliar ON carteras(auxiliar_user_id);
-- Track which supervisor an auxiliar reports to (1:1 per auxiliar)
CREATE TABLE IF NOT EXISTS auxiliar_supervisores (
auxiliar_user_id uuid NOT NULL PRIMARY KEY,
supervisor_user_id uuid NOT NULL,
created_at timestamptz DEFAULT now()
);

View File

@@ -0,0 +1,4 @@
-- Bancos belong to individual contribuyentes, not the whole tenant.
-- Used for conciliación per-contribuyente.
ALTER TABLE bancos ADD COLUMN IF NOT EXISTS contribuyente_id uuid;
CREATE INDEX IF NOT EXISTS ix_bancos_contribuyente ON bancos(contribuyente_id);

View File

@@ -0,0 +1,12 @@
-- CFDIs descartados de alertas (ej: discrepancias de régimen que el usuario revisó y decidió ignorar).
-- El descarte se aplica por tipo de alerta y cfdi_id.
CREATE TABLE IF NOT EXISTS cfdi_descartados (
id serial PRIMARY KEY,
cfdi_id integer NOT NULL,
tipo_alerta text NOT NULL, -- e.g. 'discrepancia-regimen'
descartado_por text, -- email or userId
created_at timestamptz DEFAULT now(),
UNIQUE (cfdi_id, tipo_alerta)
);
CREATE INDEX IF NOT EXISTS ix_cfdi_descartados_tipo ON cfdi_descartados(tipo_alerta);

View File

@@ -0,0 +1,18 @@
-- Amplía contribuyentes.regimen_fiscal a TEXT para soportar CSV de múltiples
-- regímenes (ej. "626,605"). La migración 010 original lo declaró varchar(3)
-- asumiendo un solo régimen por contribuyente, pero el código sincroniza CSV
-- desde la CSF (sincronizarDatosFiscales en constancia.service.ts).
--
-- Síntoma antes del fix: el sync falla con "el valor es demasiado largo para
-- el tipo character varying(3)" cuando un contribuyente tiene ≥2 regímenes
-- activos en su CSF, y los campos regimen_fiscal/codigo_postal/domicilio
-- quedan NULL.
--
-- Idempotente: si ya es text (patito tenía parche manual), el ALTER es no-op
-- en términos de filas — Postgres lo resuelve como metadata change.
ALTER TABLE contribuyentes ALTER COLUMN regimen_fiscal TYPE text;
INSERT INTO tenant_migrations (scope, version, name)
VALUES ('vertical-contable', 25, '025_contribuyentes_regimen_fiscal_text')
ON CONFLICT (scope, version) DO NOTHING;

View File

@@ -0,0 +1,47 @@
-- Normaliza el case de cfdis.uuid y elimina duplicados generados por el
-- mismatch case-sensitive entre los dos paths de sync SAT:
-- - source='sat' (XML parser): insertaba UUIDs como venían del XML (lowercase)
-- - source='sat-metadata' (CSV parser): insertaba UUIDs del CSV (UPPERCASE)
-- La constraint UNIQUE(uuid) de Postgres es case-sensitive → ambos convivían
-- como filas distintas, duplicando el CFDI en todas las métricas.
--
-- Estrategia (idempotente — en tenants sin duplicados es no-op):
-- 1. Propagar status=Cancelado de metadata→sat si corresponde (guard de
-- data loss; verificado que en Patito no aplica a ninguna fila, pero la
-- cláusula queda como protección para futuros despachos).
-- 2. Borrar las filas 'sat-metadata' que tengan par 'sat' con el mismo UUID
-- (case-insensitive). Las filas sat-metadata sin par se conservan (son
-- legítimas: CFDIs cancelados sin XML).
-- 3. Normalizar todos los UUIDs restantes a lowercase.
-- El código de saveCfdis/saveMetadata también fue actualizado para (a) matchear
-- case-insensitive, (b) insertar siempre en lowercase.
-- 1. Propagar Cancelado antes de borrar
UPDATE cfdis sat
SET status = 'Cancelado',
fecha_cancelacion = COALESCE(sat.fecha_cancelacion, meta.fecha_cancelacion),
actualizado_en = NOW()
FROM cfdis meta
WHERE LOWER(sat.uuid) = LOWER(meta.uuid)
AND sat.id != meta.id
AND sat.source = 'sat'
AND meta.source = 'sat-metadata'
AND meta.status = 'Cancelado'
AND sat.status != 'Cancelado';
-- 2. Borrar duplicados sat-metadata
DELETE FROM cfdis meta
WHERE meta.source = 'sat-metadata'
AND EXISTS (
SELECT 1 FROM cfdis sat
WHERE LOWER(meta.uuid) = LOWER(sat.uuid)
AND sat.id != meta.id
AND sat.source = 'sat'
);
-- 3. Normalizar a lowercase
UPDATE cfdis SET uuid = LOWER(uuid) WHERE uuid IS NOT NULL AND uuid != LOWER(uuid);
INSERT INTO tenant_migrations (scope, version, name)
VALUES ('vertical-contable', 26, '026_normalize_cfdi_uuid_case')
ON CONFLICT (scope, version) DO NOTHING;

View File

@@ -0,0 +1,22 @@
-- Reemplaza el UNIQUE (uuid) case-sensitive por un índice funcional
-- UNIQUE (LOWER(uuid)), como defensa en profundidad contra duplicados por
-- mismatch de case. El código fuente ya normaliza a lowercase en el insert
-- (saveCfdis y saveMetadata en sat.service.ts), pero este constraint previene
-- que cualquier insert manual o vía futuro path pueda reintroducir el bug.
--
-- Prerequisito: migración 026 ya normalizó todos los UUIDs existentes a
-- lowercase y eliminó duplicados case-insensitive. Si no se aplicó antes, el
-- CREATE UNIQUE INDEX fallará con "could not create unique index" y habrá que
-- correr 026 primero.
--
-- Idempotente: si el índice nuevo ya existe, IF NOT EXISTS lo salta.
ALTER TABLE cfdis DROP CONSTRAINT IF EXISTS cfdis_uuid_key;
CREATE UNIQUE INDEX IF NOT EXISTS cfdis_uuid_lower_key
ON cfdis (LOWER(uuid))
WHERE uuid IS NOT NULL;
INSERT INTO tenant_migrations (scope, version, name)
VALUES ('vertical-contable', 27, '027_cfdi_uuid_unique_case_insensitive')
ON CONFLICT (scope, version) DO NOTHING;

View File

@@ -0,0 +1,26 @@
-- Pestaña "Extras" en /documentos: PDFs libres (acuses SAT, contratos, poderes,
-- estados de cuenta, comprobantes) organizados por contribuyente con categoría
-- de texto libre.
CREATE TABLE IF NOT EXISTS documentos_extras (
id serial PRIMARY KEY,
contribuyente_id uuid REFERENCES contribuyentes(entidad_id) ON DELETE CASCADE,
nombre varchar(255) NOT NULL,
descripcion text,
categoria varchar(100),
pdf bytea NOT NULL,
pdf_filename varchar(255) NOT NULL,
subido_por varchar(255),
created_at timestamptz DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_documentos_extras_contrib
ON documentos_extras(contribuyente_id, created_at DESC)
WHERE contribuyente_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS ix_documentos_extras_categoria
ON documentos_extras(categoria)
WHERE categoria IS NOT NULL;
INSERT INTO tenant_migrations (scope, version, name)
VALUES ('vertical-contable', 28, '028_documentos_extras')
ON CONFLICT (scope, version) DO NOTHING;

View File

@@ -0,0 +1,17 @@
-- #6 Trazabilidad declaración↔obligación: agrega FK a declaraciones_provisionales
-- en obligacion_periodos. ON DELETE SET NULL porque si la declaración se borra
-- el periodo puede seguir completado (el usuario puede haberlo cerrado sin
-- re-subir, o la completitud viene de otra fuente — "marcar manualmente"
-- via UI, etc.). La UI puede mostrar "via Declaración #123" cuando hay FK.
ALTER TABLE obligacion_periodos
ADD COLUMN IF NOT EXISTS declaracion_id integer
REFERENCES declaraciones_provisionales(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS ix_obligacion_periodos_declaracion
ON obligacion_periodos(declaracion_id)
WHERE declaracion_id IS NOT NULL;
INSERT INTO tenant_migrations (scope, version, name)
VALUES ('vertical-contable', 30, '030_obligacion_periodos_declaracion_id')
ON CONFLICT (scope, version) DO NOTHING;

View File

@@ -0,0 +1,27 @@
-- Fix: las declaraciones provisionales no distinguían contribuyente. En un
-- despacho con N RFCs, la declaración IVA de Alexa aparecía también cuando
-- se seleccionaba a Carlos. Agregamos FK nullable para linkear, y las
-- existentes quedan con NULL (interpretadas como "tenant-wide / legacy").
-- `ON DELETE SET NULL` para que borrar un contribuyente no tire declaraciones.
ALTER TABLE declaraciones_provisionales
ADD COLUMN IF NOT EXISTS contribuyente_id uuid
REFERENCES contribuyentes(entidad_id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS ix_declaraciones_contribuyente
ON declaraciones_provisionales(contribuyente_id, año DESC, mes DESC)
WHERE contribuyente_id IS NOT NULL;
-- Reemplaza el UNIQUE (año, mes) WHERE tipo='normal' por uno que incluye
-- contribuyente: cada RFC debe poder tener su propia declaración normal
-- para el mismo mes. Postgres trata NULL != NULL en índices, así que
-- declaraciones legacy sin contribuyente siguen pudiendo coexistir entre
-- sí — no se auto-de-duplican, pero tampoco bloquean las nuevas.
DROP INDEX IF EXISTS uniq_declaracion_normal_mes;
CREATE UNIQUE INDEX IF NOT EXISTS uniq_declaracion_normal_mes_contrib
ON declaraciones_provisionales(año, mes, contribuyente_id)
WHERE tipo = 'normal';
INSERT INTO tenant_migrations (scope, version, name)
VALUES ('vertical-contable', 31, '031_declaraciones_contribuyente_id')
ON CONFLICT (scope, version) DO NOTHING;

View File

@@ -0,0 +1,22 @@
-- Agrega soporte para CfdiRelacionados del propio comprobante (CFDI 4.0).
-- El campo existente `uuid_relacionado` se sigue usando para DoctoRelacionado
-- del complemento de Pagos (tipo P). Estas dos columnas nuevas son para los
-- CfdiRelacionados a nivel raíz del comprobante (típico en tipo E — notas
-- de crédito relacionadas a facturas I, P, o a anticipos aplicados).
--
-- `cfdi_tipo_relacion` — clave SAT de 2 chars (01 NC, 02 Sustitución,
-- 03 Devolución, 04 Sustitución CFDIs previos, 05 Traslados mercancía,
-- 06 Factura por traslado previo, 07 Aplicación de anticipo).
-- `cfdis_relacionados` — UUIDs pipe-separated del/los CfdiRelacionado.
ALTER TABLE cfdis
ADD COLUMN IF NOT EXISTS cfdi_tipo_relacion VARCHAR(2),
ADD COLUMN IF NOT EXISTS cfdis_relacionados TEXT;
CREATE INDEX IF NOT EXISTS ix_cfdis_tipo_relacion
ON cfdis(cfdi_tipo_relacion)
WHERE cfdi_tipo_relacion IS NOT NULL;
INSERT INTO tenant_migrations (scope, version, name)
VALUES ('vertical-contable', 32, '032_cfdis_relaciones')
ON CONFLICT (scope, version) DO NOTHING;

View File

@@ -0,0 +1,11 @@
-- Marca el timestamp del último rechazo SAT que sugiere que el CSD
-- aún no está propagado en la Lista de Contribuyentes Obligados (LCO).
-- La propagación tarda 24-72h; el frontend muestra un banner mientras
-- esta marca esté dentro de las últimas 24h.
ALTER TABLE facturapi_orgs
ADD COLUMN IF NOT EXISTS last_lco_rejection_at timestamptz;
INSERT INTO tenant_migrations (scope, version, name)
VALUES ('vertical-contable', 33, '033_facturapi_orgs_lco_rejection')
ON CONFLICT (scope, version) DO NOTHING;

View File

@@ -0,0 +1,12 @@
-- Preferencias de notificación por correo, por contribuyente.
-- Default: objeto vacío → el código interpreta "todo activado" como
-- comportamiento previo. Cuando el user desactiva un tipo, se guarda
-- `{ "<tipo>": false }`. Tipos soportados (informativos):
-- weekly_update, fiel_notification, documento_subido, subscription_expiring
ALTER TABLE contribuyentes
ADD COLUMN IF NOT EXISTS email_preferences jsonb DEFAULT '{}'::jsonb;
INSERT INTO tenant_migrations (scope, version, name)
VALUES ('vertical-contable', 34, '034_contribuyentes_email_preferences')
ON CONFLICT (scope, version) DO NOTHING;

View File

@@ -0,0 +1,46 @@
-- Tareas operativas del despacho por contribuyente. Recurrentes (semanal a
-- anual). Materialización lazy en tarea_periodos cuando el frontend lee.
-- Solo del presente en adelante.
CREATE TABLE IF NOT EXISTS tareas_catalogo (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
contribuyente_id uuid NOT NULL REFERENCES contribuyentes(entidad_id) ON DELETE CASCADE,
nombre text NOT NULL,
descripcion text,
recurrencia varchar(15) NOT NULL CHECK (recurrencia IN
('semanal','quincenal','mensual','bimestral','trimestral','semestral','anual')),
-- Para semanal/quincenal: día de la semana (1=lunes, 7=domingo)
dia_semana int CHECK (dia_semana BETWEEN 1 AND 7),
-- Para mensual y mayores: día del mes (1-31). Si > último día del mes,
-- se materializa al último día (ej. 31 en febrero → 28/29).
dia_mes int CHECK (dia_mes BETWEEN 1 AND 31),
solo_supervisor_completa boolean DEFAULT false,
es_default boolean DEFAULT false,
active boolean DEFAULT true,
orden int DEFAULT 0,
created_at timestamptz DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_tareas_catalogo_contrib ON tareas_catalogo(contribuyente_id, active);
CREATE TABLE IF NOT EXISTS tarea_periodos (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tarea_id uuid NOT NULL REFERENCES tareas_catalogo(id) ON DELETE CASCADE,
-- '2025-W12' semanal/quincenal, '2025-01' mensual, '2025-B1' bimestral,
-- '2025-Q1' trimestral, '2025-S1' semestral, '2025' anual.
periodo varchar(10) NOT NULL,
fecha_limite date NOT NULL,
completada boolean DEFAULT false,
completada_at timestamptz,
completada_por uuid,
notas text,
created_at timestamptz DEFAULT now(),
UNIQUE (tarea_id, periodo)
);
CREATE INDEX IF NOT EXISTS ix_tarea_periodos_fecha ON tarea_periodos(fecha_limite);
CREATE INDEX IF NOT EXISTS ix_tarea_periodos_completada ON tarea_periodos(completada);
INSERT INTO tenant_migrations (scope, version, name)
VALUES ('vertical-contable', 35, '035_tareas')
ON CONFLICT (scope, version) DO NOTHING;

View File

@@ -0,0 +1,38 @@
-- Papelería de trabajo: archivos del despacho por contribuyente, organizados
-- por mes/año y con flujo opcional de aprobación. Formatos permitidos
-- (validados en backend): pdf, word (doc/docx), excel (xls/xlsx). Máx 5 MB
-- por archivo (validado en backend). NO accesible para usuarios rol cliente.
CREATE TABLE IF NOT EXISTS papeleria_trabajo (
id serial PRIMARY KEY,
contribuyente_id uuid NOT NULL REFERENCES contribuyentes(entidad_id) ON DELETE CASCADE,
nombre varchar(255) NOT NULL, -- "Reporte de cuentas Q1"
descripcion text,
archivo bytea NOT NULL,
archivo_filename varchar(255) NOT NULL, -- "reporte.pdf"
archivo_mime varchar(100) NOT NULL,
archivo_size int NOT NULL,
-- periodo (mes + año)
anio int NOT NULL CHECK (anio BETWEEN 2000 AND 2100),
mes int NOT NULL CHECK (mes BETWEEN 1 AND 12),
-- flujo de aprobación
requiere_aprobacion boolean NOT NULL DEFAULT false,
estado varchar(20) CHECK (estado IS NULL OR estado IN ('pendiente','aprobado','rechazado')),
aprobado_por uuid,
aprobado_at timestamptz,
comentario_rechazo text,
subido_por uuid NOT NULL,
created_at timestamptz DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_papeleria_contrib
ON papeleria_trabajo(contribuyente_id, created_at DESC);
CREATE INDEX IF NOT EXISTS ix_papeleria_periodo
ON papeleria_trabajo(contribuyente_id, anio, mes);
CREATE INDEX IF NOT EXISTS ix_papeleria_estado
ON papeleria_trabajo(estado)
WHERE estado IS NOT NULL;
INSERT INTO tenant_migrations (scope, version, name)
VALUES ('vertical-contable', 36, '036_papeleria_trabajo')
ON CONFLICT (scope, version) DO NOTHING;

View File

@@ -0,0 +1,22 @@
-- Tracking de activos fijos dados de baja (vendidos / desechados / otro).
-- Solo aplica a CFDIs tipo I con uso_cfdi I01-I08 cuyo receptor es el
-- contribuyente. Una fila por CFDI dado de baja; revertir = DELETE.
-- La pestaña "Activos Fijos" en /impuestos consulta esta tabla para
-- detener el cómputo de deducción mensual a partir de `fecha_baja`.
CREATE TABLE IF NOT EXISTS activos_fijos_baja (
id serial PRIMARY KEY,
cfdi_id int NOT NULL REFERENCES cfdis(id) ON DELETE CASCADE,
fecha_baja date NOT NULL,
motivo varchar(20) NOT NULL CHECK (motivo IN ('venta','desecho','otro')),
comentario text,
dado_de_baja_por uuid NOT NULL,
created_at timestamptz DEFAULT now(),
UNIQUE (cfdi_id)
);
CREATE INDEX IF NOT EXISTS ix_activos_fijos_baja_cfdi ON activos_fijos_baja(cfdi_id);
INSERT INTO tenant_migrations (scope, version, name)
VALUES ('vertical-contable', 37, '037_activos_fijos_baja')
ON CONFLICT (scope, version) DO NOTHING;

View File

@@ -0,0 +1,11 @@
-- Permite al contador descartar conceptos de uso CFDI (ej. I06, I07) que
-- en su contribuyente no representan adquisiciones de activos fijos sino
-- gastos regulares (ej. servicio telefónico mensual). Default: lista vacía
-- (todos los usos I01-I08 se consideran activos fijos como hasta ahora).
ALTER TABLE contribuyentes
ADD COLUMN IF NOT EXISTS activos_fijos_usos_excluidos jsonb DEFAULT '[]'::jsonb;
INSERT INTO tenant_migrations (scope, version, name)
VALUES ('vertical-contable', 38, '038_activos_fijos_usos_excluidos')
ON CONFLICT (scope, version) DO NOTHING;

View File

@@ -0,0 +1,22 @@
-- Tracking de alertas automáticas que ya fueron notificadas por email.
-- Mecanismo idempotente: una sola email por (alerta_id, contribuyente_id).
-- Si la alerta deja de devolverse por `generarAlertasAutomaticas`, se marca
-- `resuelta_at`. Si vuelve a aparecer (NULL en resuelta_at), no se re-notifica
-- — política conservadora de Option B (una sola notificación por evento).
CREATE TABLE IF NOT EXISTS alertas_notificadas (
id BIGSERIAL PRIMARY KEY,
alerta_id TEXT NOT NULL,
contribuyente_id UUID,
notified_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
resuelta_at TIMESTAMPTZ
);
-- UNIQUE compuesto con COALESCE para que NULL en contribuyente_id no rompa
-- la dedup (alertas tenant-level vs contribuyente-específicas comparten tabla).
CREATE UNIQUE INDEX IF NOT EXISTS uniq_alertas_notif
ON alertas_notificadas (alerta_id, COALESCE(contribuyente_id::text, ''));
-- Índice para queries del cron que filtra por contribuyente.
CREATE INDEX IF NOT EXISTS idx_alertas_notif_contribuyente
ON alertas_notificadas (contribuyente_id) WHERE contribuyente_id IS NOT NULL;

View File

@@ -0,0 +1,12 @@
-- Tracking de notificaciones email enviadas por recordatorio en cada
-- ventana de proximidad (3 días, 1 día, mismo día). Cada columna se llena
-- una sola vez cuando el cron envía el email correspondiente.
--
-- Si el usuario edita `fecha_limite` después de haber enviado un email,
-- las columnas previas siguen marcadas — el cron no re-notificará para
-- ventanas ya enviadas. Decisión MVP: simple y predecible.
ALTER TABLE recordatorios
ADD COLUMN IF NOT EXISTS email_3d_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS email_1d_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS email_0d_at TIMESTAMPTZ;

View File

@@ -0,0 +1,13 @@
-- Live Secret Key por organización Facturapi (modo Live multi-RFC).
-- Cada organización Facturapi (1:1 con contribuyente del despacho) tiene su
-- propia sk_live_xxx generada vía PUT /v2/organizations/{id}/apikeys/live.
-- La key se cifra con AES-256-GCM (misma derivación que FIEL_ENCRYPTION_KEY)
-- y se guarda con IV + auth tag por componente, igual que las credenciales FIEL.
ALTER TABLE facturapi_orgs
ADD COLUMN IF NOT EXISTS api_key_enc bytea,
ADD COLUMN IF NOT EXISTS api_key_iv bytea,
ADD COLUMN IF NOT EXISTS api_key_tag bytea;
INSERT INTO tenant_migrations (scope, version, name)
VALUES ('vertical-contable', 41, '041_facturapi_orgs_api_key_enc')
ON CONFLICT (scope, version) DO NOTHING;

View File

@@ -0,0 +1,15 @@
-- NCs Emitidas y NCs Recibidas — surface metrics nuevas en /impuestos > ISR.
-- Persistidas en metricas_mensuales para acceso vía cache (mismo patrón que
-- ingresos_cobrados/egresos_pagados) y disponibles para queries directas /
-- reportes BI sin tener que recomputar desde raw CFDIs.
--
-- Defecto 0 — los registros existentes se exponen con valor 0 hasta que el
-- cron de invalidaciones los recompute. El cache total se invalida al deploy
-- vía scripts/refresh-metricas-cache.ts.
ALTER TABLE metricas_mensuales
ADD COLUMN IF NOT EXISTS ncs_emitidas numeric(18,2) DEFAULT 0,
ADD COLUMN IF NOT EXISTS ncs_recibidas numeric(18,2) DEFAULT 0;
INSERT INTO tenant_migrations (scope, version, name)
VALUES ('vertical-contable', 42, '042_metricas_ncs')
ON CONFLICT (scope, version) DO NOTHING;

View File

@@ -0,0 +1,13 @@
-- Gastos no deducibles por Art. 27 fracción III LISR — facturas recibidas
-- pagadas en efectivo (forma_pago='01') con monto > $2,000. Persistido en
-- metricas_mensuales para acceso vía cache + queries directas / reportes BI.
--
-- Defecto 0 — los registros existentes se exponen con 0 hasta que el cron
-- de invalidaciones los recompute. El cache total se invalida al deploy
-- vía scripts/refresh-metricas-cache.ts.
ALTER TABLE metricas_mensuales
ADD COLUMN IF NOT EXISTS gastos_no_deducibles_efectivo numeric(18,2) DEFAULT 0;
INSERT INTO tenant_migrations (scope, version, name)
VALUES ('vertical-contable', 43, '043_metricas_no_deducibles')
ON CONFLICT (scope, version) DO NOTHING;