erp-core-database-v2/ddl/28-cfdi-operations.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

443 lines
17 KiB
SQL

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