-- ============================================================= -- ARCHIVO: 28-cfdi-operations.sql -- DESCRIPCION: Modulo CFDI - Operaciones: cancelaciones, logs, -- complementos de pago -- VERSION: 1.0.0 -- PROYECTO: ERP-Core V2 -- FECHA: 2026-02-03 -- DEPENDE DE: 27-cfdi-core.sql -- ============================================================= -- ===================== -- TIPOS ENUMERADOS OPERACIONES -- ===================== -- Motivo de cancelacion CFDI DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'cfdi_cancellation_reason') THEN CREATE TYPE fiscal.cfdi_cancellation_reason AS ENUM ( '01', -- Comprobante emitido con errores con relacion '02', -- Comprobante emitido con errores sin relacion '03', -- No se llevo a cabo la operacion '04' -- Operacion nominativa relacionada en factura global ); END IF; END $$; -- Estado de solicitud de cancelacion DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'cancellation_request_status') THEN CREATE TYPE fiscal.cancellation_request_status AS ENUM ( 'pending', -- Pendiente de enviar 'submitted', -- Enviada al SAT 'accepted', -- Aceptada por el receptor 'rejected', -- Rechazada por el receptor 'in_process', -- En proceso (plazo de aceptacion) 'expired', -- Plazo expirado (cancelada automaticamente) 'cancelled', -- Cancelacion exitosa 'error' -- Error en el proceso ); END IF; END $$; -- Tipo de operacion en log DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'cfdi_operation_type') THEN CREATE TYPE fiscal.cfdi_operation_type AS ENUM ( 'create', -- Creacion de CFDI 'stamp', -- Timbrado 'stamp_retry', -- Reintento de timbrado 'cancel_request', -- Solicitud de cancelacion 'cancel_accept', -- Aceptacion de cancelacion 'cancel_reject', -- Rechazo de cancelacion 'cancel_complete', -- Cancelacion completada 'validate', -- Validacion en SAT 'download', -- Descarga de XML/PDF 'email', -- Envio por email 'error' -- Error en operacion ); END IF; END $$; -- Estado del complemento de pago DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'payment_complement_status') THEN CREATE TYPE fiscal.payment_complement_status AS ENUM ( 'draft', -- Borrador 'pending', -- Pendiente de timbrar 'stamped', -- Timbrado exitosamente 'cancelled', -- Cancelado 'error' -- Error ); END IF; END $$; -- ===================== -- TABLA: cfdi_cancellation_requests -- Solicitudes de cancelacion de CFDI -- ===================== CREATE TABLE IF NOT EXISTS fiscal.cfdi_cancellation_requests ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, -- CFDI a cancelar cfdi_invoice_id UUID NOT NULL REFERENCES fiscal.cfdi_invoices(id) ON DELETE CASCADE, cfdi_uuid VARCHAR(36) NOT NULL, -- UUID del CFDI a cancelar -- Motivo de cancelacion cancellation_reason fiscal.cfdi_cancellation_reason NOT NULL, -- CFDI sustituto (para motivo 01) substitute_uuid VARCHAR(36), -- UUID del CFDI que sustituye substitute_cfdi_id UUID REFERENCES fiscal.cfdi_invoices(id), -- Estado de la solicitud status fiscal.cancellation_request_status NOT NULL DEFAULT 'pending', -- Fechas del proceso requested_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, submitted_at TIMESTAMPTZ, -- Fecha de envio al SAT response_at TIMESTAMPTZ, -- Fecha de respuesta expires_at TIMESTAMPTZ, -- Fecha limite para aceptacion (72h) completed_at TIMESTAMPTZ, -- Fecha de cancelacion efectiva -- Respuesta del SAT sat_response_code VARCHAR(10), sat_response_message TEXT, sat_ack_xml TEXT, -- Acuse de cancelacion -- Respuesta del receptor (si aplica) receiver_response VARCHAR(20), -- accepted, rejected receiver_response_at TIMESTAMPTZ, receiver_response_reason TEXT, -- Notas reason_notes TEXT, -- Notas adicionales del motivo internal_notes TEXT, -- Errores last_error TEXT, error_details JSONB, retry_count 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), UNIQUE(cfdi_invoice_id) ); COMMENT ON TABLE fiscal.cfdi_cancellation_requests IS 'Solicitudes de cancelacion de CFDI segun proceso SAT 2022+'; COMMENT ON COLUMN fiscal.cfdi_cancellation_requests.cancellation_reason IS 'Motivo: 01=Con relacion, 02=Sin relacion, 03=No se realizo operacion, 04=Nominativa global'; COMMENT ON COLUMN fiscal.cfdi_cancellation_requests.expires_at IS 'Fecha limite para que el receptor acepte/rechace (72 horas habiles)'; -- ===================== -- TABLA: cfdi_operation_logs -- Audit trail de todas las operaciones CFDI -- ===================== CREATE TABLE IF NOT EXISTS fiscal.cfdi_operation_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, -- Referencia al CFDI cfdi_invoice_id UUID REFERENCES fiscal.cfdi_invoices(id) ON DELETE SET NULL, cfdi_uuid VARCHAR(36), -- Guardamos UUID por si se elimina el registro -- Tipo de operacion operation_type fiscal.cfdi_operation_type NOT NULL, -- Estado antes y despues status_before VARCHAR(50), status_after VARCHAR(50), -- Resultado success BOOLEAN NOT NULL DEFAULT FALSE, -- Detalles de la operacion request_payload JSONB, -- Datos enviados response_payload JSONB, -- Respuesta recibida -- Error (si aplica) error_code VARCHAR(50), error_message TEXT, error_details JSONB, -- PAC utilizado pac_code VARCHAR(20), pac_transaction_id VARCHAR(100), -- Tiempo de respuesta duration_ms INTEGER, -- Duracion en milisegundos -- IP y user agent (para auditorias) ip_address INET, user_agent VARCHAR(500), -- Auditoria created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id) ); COMMENT ON TABLE fiscal.cfdi_operation_logs IS 'Log de todas las operaciones realizadas sobre CFDIs'; COMMENT ON COLUMN fiscal.cfdi_operation_logs.operation_type IS 'Tipo de operacion: stamp, cancel_request, validate, etc.'; COMMENT ON COLUMN fiscal.cfdi_operation_logs.duration_ms IS 'Tiempo de respuesta del PAC/SAT en milisegundos'; -- ===================== -- TABLA: cfdi_payment_complements -- Complementos de pago (CFDI tipo P - REP) -- ===================== CREATE TABLE IF NOT EXISTS fiscal.cfdi_payment_complements ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, -- Pago asociado payment_id UUID REFERENCES billing.payments(id) ON DELETE SET NULL, -- Identificadores CFDI uuid VARCHAR(36), -- UUID del complemento de pago serie VARCHAR(25), folio VARCHAR(40), -- Estado status fiscal.payment_complement_status NOT NULL DEFAULT 'draft', -- Certificado usado certificate_id UUID REFERENCES fiscal.cfdi_certificates(id), certificate_number VARCHAR(20), -- Datos del emisor issuer_rfc VARCHAR(13) NOT NULL, issuer_name VARCHAR(300) NOT NULL, issuer_fiscal_regime VARCHAR(10) NOT NULL, -- Datos del receptor receiver_rfc VARCHAR(13) NOT NULL, receiver_name VARCHAR(300) NOT NULL, receiver_fiscal_regime VARCHAR(10), receiver_zip_code VARCHAR(5), -- Lugar de expedicion expedition_place VARCHAR(5) NOT NULL, -- Datos del pago payment_date DATE NOT NULL, payment_form VARCHAR(10) NOT NULL, -- Forma de pago SAT currency VARCHAR(3) NOT NULL DEFAULT 'MXN', exchange_rate DECIMAL(10, 6) DEFAULT 1, total_amount DECIMAL(18, 6) NOT NULL, -- Datos bancarios (si aplica) payer_bank_rfc VARCHAR(13), payer_bank_name VARCHAR(300), payer_bank_account VARCHAR(50), payee_bank_rfc VARCHAR(13), payee_bank_account VARCHAR(50), -- Cadena de pago (para transferencias) payment_chain TEXT, payment_certificate TEXT, payment_stamp TEXT, -- Operacion (efectivo, cheque, etc.) operation_number VARCHAR(100), -- XML y timbrado xml_original TEXT, xml_stamped TEXT, stamp_date TIMESTAMPTZ, stamp_sat_seal TEXT, stamp_cfdi_seal TEXT, stamp_original_chain TEXT, sat_certificate_number VARCHAR(20), -- PDF pdf_url VARCHAR(500), pdf_generated_at TIMESTAMPTZ, -- Cancelacion cancellation_request_id UUID REFERENCES fiscal.cfdi_cancellation_requests(id), cancellation_date TIMESTAMPTZ, -- 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) ); COMMENT ON TABLE fiscal.cfdi_payment_complements IS 'Complementos de pago (CFDI tipo P - Recibo Electronico de Pago)'; COMMENT ON COLUMN fiscal.cfdi_payment_complements.uuid IS 'UUID del timbre fiscal del complemento de pago'; COMMENT ON COLUMN fiscal.cfdi_payment_complements.payment_form IS 'Forma de pago segun catalogo SAT c_FormaPago'; -- ===================== -- TABLA: cfdi_payment_complement_documents -- Documentos relacionados en el complemento de pago -- ===================== CREATE TABLE IF NOT EXISTS fiscal.cfdi_payment_complement_documents ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), payment_complement_id UUID NOT NULL REFERENCES fiscal.cfdi_payment_complements(id) ON DELETE CASCADE, -- CFDI del documento relacionado related_uuid VARCHAR(36) NOT NULL, -- IdDocumento - UUID de la factura pagada related_cfdi_id UUID REFERENCES fiscal.cfdi_invoices(id), -- Serie y folio del documento serie VARCHAR(25), folio VARCHAR(40), -- Moneda del documento document_currency VARCHAR(3) NOT NULL DEFAULT 'MXN', equivalence DECIMAL(18, 6) DEFAULT 1, -- Equivalencia DR (tipo cambio) -- Numero de parcialidad installment_number INTEGER NOT NULL DEFAULT 1, -- Saldos previous_balance DECIMAL(18, 6) NOT NULL, -- ImpSaldoAnt amount_paid DECIMAL(18, 6) NOT NULL, -- ImpPagado remaining_balance DECIMAL(18, 6) NOT NULL, -- ImpSaldoInsoluto -- Objeto de impuesto tax_object VARCHAR(2) DEFAULT '02', -- ObjetoImpDR -- Auditoria created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); COMMENT ON TABLE fiscal.cfdi_payment_complement_documents IS 'Documentos (facturas) incluidos en el complemento de pago'; COMMENT ON COLUMN fiscal.cfdi_payment_complement_documents.related_uuid IS 'UUID de la factura que se esta pagando'; COMMENT ON COLUMN fiscal.cfdi_payment_complement_documents.installment_number IS 'Numero de parcialidad (1, 2, 3...)'; COMMENT ON COLUMN fiscal.cfdi_payment_complement_documents.previous_balance IS 'Saldo anterior antes de este pago'; COMMENT ON COLUMN fiscal.cfdi_payment_complement_documents.remaining_balance IS 'Saldo pendiente despues de este pago'; -- ===================== -- TABLA: cfdi_payment_complement_document_taxes -- Impuestos por documento en complemento de pago -- ===================== CREATE TABLE IF NOT EXISTS fiscal.cfdi_payment_complement_document_taxes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), payment_complement_document_id UUID NOT NULL REFERENCES fiscal.cfdi_payment_complement_documents(id) ON DELETE CASCADE, -- Tipo de impuesto tax_type VARCHAR(20) NOT NULL, -- transferred o withheld -- Impuesto tax_code VARCHAR(10) NOT NULL, -- 001=ISR, 002=IVA, 003=IEPS -- Factor factor_type VARCHAR(10) NOT NULL, -- Tasa, Cuota, Exento rate DECIMAL(10, 6), -- Montos base_amount DECIMAL(18, 6) NOT NULL, tax_amount DECIMAL(18, 6) NOT NULL, -- Auditoria created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); COMMENT ON TABLE fiscal.cfdi_payment_complement_document_taxes IS 'Impuestos por documento relacionado en el complemento de pago'; -- ===================== -- TABLA: cfdi_stamp_queue -- Cola de timbrado para procesamiento asincrono -- ===================== CREATE TABLE IF NOT EXISTS fiscal.cfdi_stamp_queue ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, -- Tipo de documento a timbrar document_type VARCHAR(20) NOT NULL, -- invoice, payment_complement document_id UUID NOT NULL, -- ID del documento -- Prioridad priority INTEGER DEFAULT 5, -- 1=urgente, 5=normal, 10=baja -- Estado de la cola queue_status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending, processing, completed, failed -- Intentos attempts INTEGER DEFAULT 0, max_attempts INTEGER DEFAULT 3, next_retry_at TIMESTAMPTZ, -- Resultado completed_at TIMESTAMPTZ, result_cfdi_uuid VARCHAR(36), result_error TEXT, -- Auditoria created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); COMMENT ON TABLE fiscal.cfdi_stamp_queue IS 'Cola de documentos pendientes de timbrar para procesamiento asincrono'; -- ===================== -- INDICES OPERACIONES -- ===================== -- cfdi_cancellation_requests CREATE INDEX IF NOT EXISTS idx_cfdi_cancel_tenant ON fiscal.cfdi_cancellation_requests(tenant_id); CREATE INDEX IF NOT EXISTS idx_cfdi_cancel_cfdi ON fiscal.cfdi_cancellation_requests(cfdi_invoice_id); CREATE INDEX IF NOT EXISTS idx_cfdi_cancel_uuid ON fiscal.cfdi_cancellation_requests(cfdi_uuid); CREATE INDEX IF NOT EXISTS idx_cfdi_cancel_status ON fiscal.cfdi_cancellation_requests(status); CREATE INDEX IF NOT EXISTS idx_cfdi_cancel_expires ON fiscal.cfdi_cancellation_requests(expires_at) WHERE status IN ('submitted', 'in_process'); CREATE INDEX IF NOT EXISTS idx_cfdi_cancel_pending ON fiscal.cfdi_cancellation_requests(tenant_id, status) WHERE status IN ('pending', 'submitted', 'in_process'); -- cfdi_operation_logs CREATE INDEX IF NOT EXISTS idx_cfdi_logs_tenant ON fiscal.cfdi_operation_logs(tenant_id); CREATE INDEX IF NOT EXISTS idx_cfdi_logs_cfdi ON fiscal.cfdi_operation_logs(cfdi_invoice_id); CREATE INDEX IF NOT EXISTS idx_cfdi_logs_uuid ON fiscal.cfdi_operation_logs(cfdi_uuid); CREATE INDEX IF NOT EXISTS idx_cfdi_logs_type ON fiscal.cfdi_operation_logs(operation_type); CREATE INDEX IF NOT EXISTS idx_cfdi_logs_date ON fiscal.cfdi_operation_logs(created_at); CREATE INDEX IF NOT EXISTS idx_cfdi_logs_success ON fiscal.cfdi_operation_logs(success); CREATE INDEX IF NOT EXISTS idx_cfdi_logs_errors ON fiscal.cfdi_operation_logs(tenant_id, operation_type, created_at) WHERE success = FALSE; -- cfdi_payment_complements CREATE INDEX IF NOT EXISTS idx_cfdi_pcomp_tenant ON fiscal.cfdi_payment_complements(tenant_id); CREATE INDEX IF NOT EXISTS idx_cfdi_pcomp_payment ON fiscal.cfdi_payment_complements(payment_id); CREATE INDEX IF NOT EXISTS idx_cfdi_pcomp_uuid ON fiscal.cfdi_payment_complements(uuid); CREATE INDEX IF NOT EXISTS idx_cfdi_pcomp_status ON fiscal.cfdi_payment_complements(status); CREATE INDEX IF NOT EXISTS idx_cfdi_pcomp_issuer ON fiscal.cfdi_payment_complements(issuer_rfc); CREATE INDEX IF NOT EXISTS idx_cfdi_pcomp_receiver ON fiscal.cfdi_payment_complements(receiver_rfc); CREATE INDEX IF NOT EXISTS idx_cfdi_pcomp_date ON fiscal.cfdi_payment_complements(payment_date); -- cfdi_payment_complement_documents CREATE INDEX IF NOT EXISTS idx_cfdi_pcomp_docs_comp ON fiscal.cfdi_payment_complement_documents(payment_complement_id); CREATE INDEX IF NOT EXISTS idx_cfdi_pcomp_docs_uuid ON fiscal.cfdi_payment_complement_documents(related_uuid); CREATE INDEX IF NOT EXISTS idx_cfdi_pcomp_docs_cfdi ON fiscal.cfdi_payment_complement_documents(related_cfdi_id); -- cfdi_payment_complement_document_taxes CREATE INDEX IF NOT EXISTS idx_cfdi_pcomp_dtax_doc ON fiscal.cfdi_payment_complement_document_taxes(payment_complement_document_id); -- cfdi_stamp_queue CREATE INDEX IF NOT EXISTS idx_cfdi_queue_tenant ON fiscal.cfdi_stamp_queue(tenant_id); CREATE INDEX IF NOT EXISTS idx_cfdi_queue_status ON fiscal.cfdi_stamp_queue(queue_status); CREATE INDEX IF NOT EXISTS idx_cfdi_queue_priority ON fiscal.cfdi_stamp_queue(priority, created_at) WHERE queue_status = 'pending'; CREATE INDEX IF NOT EXISTS idx_cfdi_queue_retry ON fiscal.cfdi_stamp_queue(next_retry_at) WHERE queue_status = 'failed' AND attempts < max_attempts; CREATE INDEX IF NOT EXISTS idx_cfdi_queue_doc ON fiscal.cfdi_stamp_queue(document_type, document_id); -- ===================== -- FIN DEL ARCHIVO -- =====================