erp-core-database-v2/ddl/27-cfdi-core.sql
Adrian Flores Cortes 02ab2caf26 [TASK-2026-02-05-EJECUCION-REMEDIATION-ERP-CORE] feat: DDL fixes and new schemas
- 00-auth-base.sql: Extracted auth.tenants+users from recreate-database.sh
- 03b-core-companies.sql: DDL for auth.companies entity
- 21b-inventory-extended.sql: 7 new tables for inventory entities without DDL
- 24-invoices.sql: billing→operations schema to resolve duplication
- 27/28/29-cfdi: Track existing CFDI DDL files
- recreate-database.sh: Updated ddl_files array (17→43 entries)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 21:52:22 -06:00

461 lines
19 KiB
SQL

-- =============================================================
-- ARCHIVO: 27-cfdi-core.sql
-- DESCRIPCION: Modulo CFDI - Facturacion electronica SAT Mexico
-- Certificados CSD, tipos, extension de facturas
-- VERSION: 1.0.0
-- PROYECTO: ERP-Core V2
-- FECHA: 2026-02-03
-- DEPENDE DE: 01-auth-profiles.sql, 24-invoices.sql, 26-fiscal-catalogs.sql
-- =============================================================
-- =====================
-- NOTA: El schema fiscal ya existe en 26-fiscal-catalogs.sql
-- Aqui solo agregamos las tablas especificas de CFDI
-- =====================
-- =====================
-- TIPOS ENUMERADOS CFDI
-- =====================
-- Tipo de comprobante CFDI
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'cfdi_voucher_type') THEN
CREATE TYPE fiscal.cfdi_voucher_type AS ENUM (
'I', -- Ingreso
'E', -- Egreso
'T', -- Traslado
'N', -- Nomina
'P' -- Pago
);
END IF;
END $$;
-- Estado del certificado CSD
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'csd_certificate_status') THEN
CREATE TYPE fiscal.csd_certificate_status AS ENUM (
'active', -- Activo y vigente
'expired', -- Expirado
'revoked', -- Revocado por el SAT
'pending', -- Pendiente de activacion
'inactive' -- Desactivado manualmente
);
END IF;
END $$;
-- Estado del CFDI
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'cfdi_status') THEN
CREATE TYPE fiscal.cfdi_status AS ENUM (
'draft', -- Borrador, no timbrado
'pending', -- Pendiente de timbrado
'stamping', -- En proceso de timbrado
'stamped', -- Timbrado exitosamente
'delivered', -- Entregado al receptor
'cancel_requested', -- Solicitud de cancelacion
'cancelled', -- Cancelado
'error' -- Error en timbrado
);
END IF;
END $$;
-- Tipo de relacionCFDI
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'cfdi_relation_type') THEN
CREATE TYPE fiscal.cfdi_relation_type AS ENUM (
'01', -- Nota de credito de documentos relacionados
'02', -- Nota de debito de documentos relacionados
'03', -- Devolucion de mercancia
'04', -- Sustitucion de CFDI previos
'05', -- Traslados de mercancias facturadas
'06', -- Factura generada por traslados previos
'07', -- CFDI por aplicacion de anticipo
'08', -- Factura generada por pagos en parcialidades
'09' -- Factura generada por pagos diferidos
);
END IF;
END $$;
-- =====================
-- TABLA: cfdi_certificates
-- Certificados de Sello Digital (CSD) del SAT
-- =====================
CREATE TABLE IF NOT EXISTS fiscal.cfdi_certificates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
-- Identificacion del certificado
certificate_number VARCHAR(20) NOT NULL, -- Numero de certificado SAT (20 digitos)
serial_number VARCHAR(50), -- Numero de serie del certificado
-- RFC asociado
rfc VARCHAR(13) NOT NULL, -- RFC del contribuyente
-- Archivos del certificado (almacenados encriptados)
certificate_pem TEXT NOT NULL, -- Certificado .cer en formato PEM
private_key_pem_encrypted TEXT NOT NULL, -- Llave privada .key encriptada
-- Metadatos del certificado
issued_at TIMESTAMPTZ NOT NULL, -- Fecha de emision
expires_at TIMESTAMPTZ NOT NULL, -- Fecha de expiracion
-- Estado y configuracion
status fiscal.csd_certificate_status NOT NULL DEFAULT 'pending',
is_default BOOLEAN DEFAULT FALSE, -- Certificado por defecto para este tenant
-- Informacion adicional
issuer_name VARCHAR(500), -- Nombre del emisor (SAT)
subject_name VARCHAR(500), -- Nombre del sujeto (contribuyente)
-- Notas
description VARCHAR(255),
notes TEXT,
-- Auditoria
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
created_by UUID REFERENCES auth.users(id),
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_by UUID REFERENCES auth.users(id),
deleted_at TIMESTAMPTZ,
-- Solo un certificado activo por defecto por tenant
UNIQUE(tenant_id, certificate_number),
CONSTRAINT chk_only_one_default EXCLUDE USING btree (tenant_id WITH =)
WHERE (is_default = TRUE AND deleted_at IS NULL AND status = 'active')
);
COMMENT ON TABLE fiscal.cfdi_certificates IS 'Certificados de Sello Digital (CSD) para timbrado CFDI';
COMMENT ON COLUMN fiscal.cfdi_certificates.certificate_number IS 'Numero de certificado emitido por el SAT (20 digitos)';
COMMENT ON COLUMN fiscal.cfdi_certificates.certificate_pem IS 'Certificado .cer convertido a formato PEM';
COMMENT ON COLUMN fiscal.cfdi_certificates.private_key_pem_encrypted IS 'Llave privada .key encriptada con AES-256';
COMMENT ON COLUMN fiscal.cfdi_certificates.is_default IS 'Indica si es el certificado predeterminado para facturacion';
-- =====================
-- TABLA: cfdi_pac_configurations
-- Configuracion de Proveedores Autorizados de Certificacion (PAC)
-- =====================
CREATE TABLE IF NOT EXISTS fiscal.cfdi_pac_configurations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
-- Identificacion del PAC
pac_code VARCHAR(20) NOT NULL, -- Codigo del PAC (finkok, sat, etc.)
pac_name VARCHAR(100) NOT NULL, -- Nombre comercial del PAC
-- Credenciales (encriptadas)
username VARCHAR(255), -- Usuario del PAC
password_encrypted TEXT, -- Contrasena encriptada
api_key_encrypted TEXT, -- API Key encriptada (si aplica)
-- Endpoints
production_url VARCHAR(500), -- URL produccion
sandbox_url VARCHAR(500), -- URL sandbox/pruebas
-- Configuracion
environment VARCHAR(20) DEFAULT 'sandbox', -- sandbox, production
is_active BOOLEAN DEFAULT TRUE,
is_default BOOLEAN DEFAULT FALSE,
-- Contrato
contract_number VARCHAR(50),
contract_expires_at DATE,
-- Limites
monthly_stamp_limit INTEGER, -- Limite mensual de timbres
stamps_used_this_month INTEGER DEFAULT 0,
-- Auditoria
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
created_by UUID REFERENCES auth.users(id),
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_by UUID REFERENCES auth.users(id),
deleted_at TIMESTAMPTZ,
UNIQUE(tenant_id, pac_code)
);
COMMENT ON TABLE fiscal.cfdi_pac_configurations IS 'Configuracion de conexion con Proveedores Autorizados de Certificacion (PAC)';
COMMENT ON COLUMN fiscal.cfdi_pac_configurations.pac_code IS 'Codigo identificador del PAC: finkok, sat, facturapi, etc.';
COMMENT ON COLUMN fiscal.cfdi_pac_configurations.environment IS 'Ambiente: sandbox para pruebas, production para produccion';
-- =====================
-- TABLA: cfdi_invoices
-- Extension de facturas con datos CFDI completos
-- =====================
CREATE TABLE IF NOT EXISTS fiscal.cfdi_invoices (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
-- Relacion con factura operacional
invoice_id UUID NOT NULL REFERENCES billing.invoices(id) ON DELETE CASCADE,
-- Identificadores CFDI
uuid VARCHAR(36), -- UUID del timbre fiscal (folio fiscal)
serie VARCHAR(25), -- Serie del comprobante
folio VARCHAR(40), -- Folio del comprobante
-- Tipo de comprobante
voucher_type fiscal.cfdi_voucher_type NOT NULL DEFAULT 'I',
-- Certificado usado
certificate_id UUID REFERENCES fiscal.cfdi_certificates(id),
certificate_number VARCHAR(20), -- Numero de certificado usado
-- Estado del CFDI
status fiscal.cfdi_status NOT NULL DEFAULT 'draft',
-- Datos del emisor
issuer_rfc VARCHAR(13) NOT NULL,
issuer_name VARCHAR(300) NOT NULL,
issuer_fiscal_regime VARCHAR(10) NOT NULL, -- Regimen fiscal del emisor
-- Datos del receptor
receiver_rfc VARCHAR(13) NOT NULL,
receiver_name VARCHAR(300) NOT NULL,
receiver_fiscal_regime VARCHAR(10), -- Regimen fiscal del receptor (CFDI 4.0)
receiver_tax_residence VARCHAR(3), -- Residencia fiscal extranjero
receiver_tax_id VARCHAR(40), -- NumRegIdTrib para extranjeros
cfdi_use VARCHAR(10) NOT NULL, -- Uso del CFDI (G01, G02, etc.)
receiver_zip_code VARCHAR(5), -- Codigo postal receptor (CFDI 4.0)
-- Lugar de expedicion
expedition_place VARCHAR(5) NOT NULL, -- Codigo postal lugar expedicion
-- Metodo y forma de pago
payment_method VARCHAR(10), -- PUE, PPD
payment_form VARCHAR(10), -- 01, 02, 03, etc.
payment_conditions VARCHAR(1000),
-- Moneda y tipo de cambio
currency VARCHAR(3) NOT NULL DEFAULT 'MXN',
exchange_rate DECIMAL(10, 6) DEFAULT 1,
-- Exportacion
exportation VARCHAR(2) DEFAULT '01', -- 01=No aplica, 02=Definitiva, etc.
-- Totales
subtotal DECIMAL(18, 6) NOT NULL DEFAULT 0,
discount DECIMAL(18, 6) DEFAULT 0,
total DECIMAL(18, 6) NOT NULL DEFAULT 0,
-- Impuestos trasladados (resumen)
total_transferred_taxes DECIMAL(18, 6) DEFAULT 0,
-- Impuestos retenidos (resumen)
total_withheld_taxes DECIMAL(18, 6) DEFAULT 0,
-- CFDI relacionados
related_cfdi_type fiscal.cfdi_relation_type,
-- Confirmacion SAT (para montos grandes)
confirmation_code VARCHAR(17),
-- Informacion global (para publico en general)
global_info_periodicity VARCHAR(10), -- 01=Diario, 02=Semanal, etc.
global_info_months VARCHAR(10), -- 01-12
global_info_year VARCHAR(4),
-- XML y timbrado
xml_original TEXT, -- XML antes de timbrar
xml_stamped TEXT, -- XML timbrado
stamp_date TIMESTAMPTZ, -- Fecha de timbrado
stamp_sat_seal TEXT, -- Sello del SAT
stamp_cfdi_seal TEXT, -- Sello del CFDI
stamp_original_chain TEXT, -- Cadena original del timbre
sat_certificate_number VARCHAR(20), -- Numero certificado SAT
-- PDF
pdf_url VARCHAR(500),
pdf_generated_at TIMESTAMPTZ,
-- Cancelacion
cancellation_status VARCHAR(50),
cancellation_date TIMESTAMPTZ,
cancellation_ack_xml TEXT,
-- Validacion SAT
last_sat_validation TIMESTAMPTZ,
sat_validation_status VARCHAR(50),
sat_validation_response JSONB,
-- Errores
last_error TEXT,
error_details JSONB,
-- Auditoria
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
created_by UUID REFERENCES auth.users(id),
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_by UUID REFERENCES auth.users(id),
deleted_at TIMESTAMPTZ,
UNIQUE(tenant_id, uuid),
UNIQUE(invoice_id)
);
COMMENT ON TABLE fiscal.cfdi_invoices IS 'Extension de facturas con datos completos de CFDI 4.0';
COMMENT ON COLUMN fiscal.cfdi_invoices.uuid IS 'UUID del timbre fiscal digital (folio fiscal unico del SAT)';
COMMENT ON COLUMN fiscal.cfdi_invoices.voucher_type IS 'Tipo de comprobante: I=Ingreso, E=Egreso, T=Traslado, N=Nomina, P=Pago';
COMMENT ON COLUMN fiscal.cfdi_invoices.cfdi_use IS 'Uso del CFDI segun catalogo SAT c_UsoCFDI';
COMMENT ON COLUMN fiscal.cfdi_invoices.payment_method IS 'Metodo de pago: PUE=Pago en Una Exhibicion, PPD=Pago en Parcialidades o Diferido';
COMMENT ON COLUMN fiscal.cfdi_invoices.exportation IS 'Tipo de exportacion: 01=No aplica, 02=Definitiva, 03=Temporal, 04=Definitiva con clave';
-- =====================
-- TABLA: cfdi_invoice_items
-- Conceptos/lineas del CFDI con datos fiscales completos
-- =====================
CREATE TABLE IF NOT EXISTS fiscal.cfdi_invoice_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
cfdi_invoice_id UUID NOT NULL REFERENCES fiscal.cfdi_invoices(id) ON DELETE CASCADE,
-- Referencia a linea de factura original
invoice_item_id UUID, -- FK a billing.invoice_items si existe
-- Datos del concepto
line_number INTEGER NOT NULL DEFAULT 1,
-- Claves SAT
product_service_key VARCHAR(10) NOT NULL, -- Clave producto/servicio SAT
product_service_name VARCHAR(1000), -- Descripcion del catalogo SAT
unit_key VARCHAR(10) NOT NULL, -- Clave unidad SAT
unit_name VARCHAR(50), -- Nombre unidad
-- Identificacion del producto
sku VARCHAR(100), -- NoIdentificacion
description VARCHAR(1000) NOT NULL, -- Descripcion
-- Cantidades y montos
quantity DECIMAL(18, 6) NOT NULL,
unit_price DECIMAL(18, 6) NOT NULL,
subtotal DECIMAL(18, 6) NOT NULL, -- Importe = quantity * unit_price
discount DECIMAL(18, 6) DEFAULT 0,
-- Objeto de impuesto
tax_object VARCHAR(2) NOT NULL DEFAULT '02', -- 01=No objeto, 02=Si objeto, 03=Si objeto no desglosado
-- Cuenta predial (para inmuebles)
property_tax_account VARCHAR(150),
-- Informacion aduanera
customs_info JSONB, -- Array de {NumeroPedimento, FechaPedimento, Aduana}
-- Parte (para componentes)
parts JSONB, -- Array de partes si aplica
-- Auditoria
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE fiscal.cfdi_invoice_items IS 'Conceptos del CFDI con claves SAT y desglose fiscal';
COMMENT ON COLUMN fiscal.cfdi_invoice_items.product_service_key IS 'Clave de producto/servicio del catalogo SAT c_ClaveProdServ';
COMMENT ON COLUMN fiscal.cfdi_invoice_items.unit_key IS 'Clave de unidad del catalogo SAT c_ClaveUnidad';
COMMENT ON COLUMN fiscal.cfdi_invoice_items.tax_object IS 'ObjetoImp: 01=No objeto de impuesto, 02=Si objeto, 03=Si objeto no desglosado';
-- =====================
-- TABLA: cfdi_invoice_item_taxes
-- Impuestos por concepto del CFDI
-- =====================
CREATE TABLE IF NOT EXISTS fiscal.cfdi_invoice_item_taxes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
cfdi_invoice_item_id UUID NOT NULL REFERENCES fiscal.cfdi_invoice_items(id) ON DELETE CASCADE,
-- Tipo de impuesto
tax_type VARCHAR(20) NOT NULL, -- transferred (traslado) o withheld (retencion)
-- Impuesto
tax_code VARCHAR(10) NOT NULL, -- 001=ISR, 002=IVA, 003=IEPS
tax_name VARCHAR(50),
-- Factor
factor_type VARCHAR(10) NOT NULL, -- Tasa, Cuota, Exento
rate DECIMAL(10, 6), -- Tasa o cuota
-- Montos
base_amount DECIMAL(18, 6) NOT NULL, -- Base del impuesto
tax_amount DECIMAL(18, 6) NOT NULL, -- Importe del impuesto
-- Auditoria
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE fiscal.cfdi_invoice_item_taxes IS 'Impuestos trasladados y retenidos por concepto del CFDI';
COMMENT ON COLUMN fiscal.cfdi_invoice_item_taxes.tax_type IS 'transferred=Impuesto trasladado, withheld=Impuesto retenido';
COMMENT ON COLUMN fiscal.cfdi_invoice_item_taxes.tax_code IS 'Codigo SAT: 001=ISR, 002=IVA, 003=IEPS';
COMMENT ON COLUMN fiscal.cfdi_invoice_item_taxes.factor_type IS 'Tipo de factor: Tasa, Cuota, o Exento';
-- =====================
-- TABLA: cfdi_related_documents
-- CFDI relacionados
-- =====================
CREATE TABLE IF NOT EXISTS fiscal.cfdi_related_documents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
cfdi_invoice_id UUID NOT NULL REFERENCES fiscal.cfdi_invoices(id) ON DELETE CASCADE,
-- UUID del CFDI relacionado
related_uuid VARCHAR(36) NOT NULL,
-- Tipo de relacion
relation_type fiscal.cfdi_relation_type NOT NULL,
-- Referencia interna (si existe en nuestro sistema)
related_cfdi_invoice_id UUID REFERENCES fiscal.cfdi_invoices(id),
-- Auditoria
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE fiscal.cfdi_related_documents IS 'Relaciones entre CFDIs (notas de credito, sustituciones, etc.)';
COMMENT ON COLUMN fiscal.cfdi_related_documents.relation_type IS 'Tipo de relacion segun catalogo SAT c_TipoRelacion';
-- =====================
-- INDICES CFDI CORE
-- =====================
-- cfdi_certificates
CREATE INDEX IF NOT EXISTS idx_cfdi_certificates_tenant ON fiscal.cfdi_certificates(tenant_id);
CREATE INDEX IF NOT EXISTS idx_cfdi_certificates_rfc ON fiscal.cfdi_certificates(rfc);
CREATE INDEX IF NOT EXISTS idx_cfdi_certificates_status ON fiscal.cfdi_certificates(status);
CREATE INDEX IF NOT EXISTS idx_cfdi_certificates_expires ON fiscal.cfdi_certificates(expires_at);
CREATE INDEX IF NOT EXISTS idx_cfdi_certificates_default ON fiscal.cfdi_certificates(tenant_id, is_default)
WHERE is_default = TRUE AND deleted_at IS NULL;
-- cfdi_pac_configurations
CREATE INDEX IF NOT EXISTS idx_cfdi_pac_tenant ON fiscal.cfdi_pac_configurations(tenant_id);
CREATE INDEX IF NOT EXISTS idx_cfdi_pac_code ON fiscal.cfdi_pac_configurations(pac_code);
CREATE INDEX IF NOT EXISTS idx_cfdi_pac_active ON fiscal.cfdi_pac_configurations(tenant_id, is_active)
WHERE is_active = TRUE;
-- cfdi_invoices
CREATE INDEX IF NOT EXISTS idx_cfdi_invoices_tenant ON fiscal.cfdi_invoices(tenant_id);
CREATE INDEX IF NOT EXISTS idx_cfdi_invoices_invoice ON fiscal.cfdi_invoices(invoice_id);
CREATE INDEX IF NOT EXISTS idx_cfdi_invoices_uuid ON fiscal.cfdi_invoices(uuid);
CREATE INDEX IF NOT EXISTS idx_cfdi_invoices_serie_folio ON fiscal.cfdi_invoices(serie, folio);
CREATE INDEX IF NOT EXISTS idx_cfdi_invoices_status ON fiscal.cfdi_invoices(status);
CREATE INDEX IF NOT EXISTS idx_cfdi_invoices_issuer ON fiscal.cfdi_invoices(issuer_rfc);
CREATE INDEX IF NOT EXISTS idx_cfdi_invoices_receiver ON fiscal.cfdi_invoices(receiver_rfc);
CREATE INDEX IF NOT EXISTS idx_cfdi_invoices_stamp_date ON fiscal.cfdi_invoices(stamp_date);
CREATE INDEX IF NOT EXISTS idx_cfdi_invoices_voucher_type ON fiscal.cfdi_invoices(voucher_type);
CREATE INDEX IF NOT EXISTS idx_cfdi_invoices_pending ON fiscal.cfdi_invoices(tenant_id, status)
WHERE status IN ('draft', 'pending', 'stamping');
-- cfdi_invoice_items
CREATE INDEX IF NOT EXISTS idx_cfdi_items_cfdi ON fiscal.cfdi_invoice_items(cfdi_invoice_id);
CREATE INDEX IF NOT EXISTS idx_cfdi_items_product_key ON fiscal.cfdi_invoice_items(product_service_key);
CREATE INDEX IF NOT EXISTS idx_cfdi_items_line ON fiscal.cfdi_invoice_items(cfdi_invoice_id, line_number);
-- cfdi_invoice_item_taxes
CREATE INDEX IF NOT EXISTS idx_cfdi_item_taxes_item ON fiscal.cfdi_invoice_item_taxes(cfdi_invoice_item_id);
CREATE INDEX IF NOT EXISTS idx_cfdi_item_taxes_code ON fiscal.cfdi_invoice_item_taxes(tax_code);
-- cfdi_related_documents
CREATE INDEX IF NOT EXISTS idx_cfdi_related_cfdi ON fiscal.cfdi_related_documents(cfdi_invoice_id);
CREATE INDEX IF NOT EXISTS idx_cfdi_related_uuid ON fiscal.cfdi_related_documents(related_uuid);
-- =====================
-- FIN DEL ARCHIVO
-- =====================