-- ============================================================= -- 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 -- =====================