- 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>
531 lines
17 KiB
PL/PgSQL
531 lines
17 KiB
PL/PgSQL
-- =============================================================
|
|
-- ARCHIVO: 29-cfdi-rls-functions.sql
|
|
-- DESCRIPCION: Modulo CFDI - RLS policies, funciones auxiliares,
|
|
-- triggers y vistas
|
|
-- VERSION: 1.0.0
|
|
-- PROYECTO: ERP-Core V2
|
|
-- FECHA: 2026-02-03
|
|
-- DEPENDE DE: 27-cfdi-core.sql, 28-cfdi-operations.sql
|
|
-- =============================================================
|
|
|
|
-- =====================
|
|
-- ROW LEVEL SECURITY (RLS)
|
|
-- =====================
|
|
|
|
-- cfdi_certificates
|
|
ALTER TABLE fiscal.cfdi_certificates ENABLE ROW LEVEL SECURITY;
|
|
DROP POLICY IF EXISTS tenant_isolation_cfdi_certificates ON fiscal.cfdi_certificates;
|
|
CREATE POLICY tenant_isolation_cfdi_certificates ON fiscal.cfdi_certificates
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
-- cfdi_pac_configurations
|
|
ALTER TABLE fiscal.cfdi_pac_configurations ENABLE ROW LEVEL SECURITY;
|
|
DROP POLICY IF EXISTS tenant_isolation_cfdi_pac ON fiscal.cfdi_pac_configurations;
|
|
CREATE POLICY tenant_isolation_cfdi_pac ON fiscal.cfdi_pac_configurations
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
-- cfdi_invoices
|
|
ALTER TABLE fiscal.cfdi_invoices ENABLE ROW LEVEL SECURITY;
|
|
DROP POLICY IF EXISTS tenant_isolation_cfdi_invoices ON fiscal.cfdi_invoices;
|
|
CREATE POLICY tenant_isolation_cfdi_invoices ON fiscal.cfdi_invoices
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
-- cfdi_cancellation_requests
|
|
ALTER TABLE fiscal.cfdi_cancellation_requests ENABLE ROW LEVEL SECURITY;
|
|
DROP POLICY IF EXISTS tenant_isolation_cfdi_cancellations ON fiscal.cfdi_cancellation_requests;
|
|
CREATE POLICY tenant_isolation_cfdi_cancellations ON fiscal.cfdi_cancellation_requests
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
-- cfdi_operation_logs
|
|
ALTER TABLE fiscal.cfdi_operation_logs ENABLE ROW LEVEL SECURITY;
|
|
DROP POLICY IF EXISTS tenant_isolation_cfdi_logs ON fiscal.cfdi_operation_logs;
|
|
CREATE POLICY tenant_isolation_cfdi_logs ON fiscal.cfdi_operation_logs
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
-- cfdi_payment_complements
|
|
ALTER TABLE fiscal.cfdi_payment_complements ENABLE ROW LEVEL SECURITY;
|
|
DROP POLICY IF EXISTS tenant_isolation_cfdi_payment_comp ON fiscal.cfdi_payment_complements;
|
|
CREATE POLICY tenant_isolation_cfdi_payment_comp ON fiscal.cfdi_payment_complements
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
-- cfdi_stamp_queue
|
|
ALTER TABLE fiscal.cfdi_stamp_queue ENABLE ROW LEVEL SECURITY;
|
|
DROP POLICY IF EXISTS tenant_isolation_cfdi_queue ON fiscal.cfdi_stamp_queue;
|
|
CREATE POLICY tenant_isolation_cfdi_queue ON fiscal.cfdi_stamp_queue
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
-- =====================
|
|
-- COMENTARIOS RLS
|
|
-- =====================
|
|
COMMENT ON POLICY tenant_isolation_cfdi_certificates ON fiscal.cfdi_certificates IS 'Aislamiento multi-tenant para certificados CSD';
|
|
COMMENT ON POLICY tenant_isolation_cfdi_invoices ON fiscal.cfdi_invoices IS 'Aislamiento multi-tenant para CFDIs';
|
|
COMMENT ON POLICY tenant_isolation_cfdi_cancellations ON fiscal.cfdi_cancellation_requests IS 'Aislamiento multi-tenant para cancelaciones';
|
|
COMMENT ON POLICY tenant_isolation_cfdi_logs ON fiscal.cfdi_operation_logs IS 'Aislamiento multi-tenant para logs de operaciones';
|
|
COMMENT ON POLICY tenant_isolation_cfdi_payment_comp ON fiscal.cfdi_payment_complements IS 'Aislamiento multi-tenant para complementos de pago';
|
|
|
|
-- =====================
|
|
-- FUNCIONES AUXILIARES
|
|
-- =====================
|
|
|
|
-- Funcion: Obtener certificado activo por defecto del tenant
|
|
CREATE OR REPLACE FUNCTION fiscal.get_default_certificate(p_tenant_id UUID)
|
|
RETURNS UUID AS $$
|
|
DECLARE
|
|
v_certificate_id UUID;
|
|
BEGIN
|
|
SELECT id INTO v_certificate_id
|
|
FROM fiscal.cfdi_certificates
|
|
WHERE tenant_id = p_tenant_id
|
|
AND is_default = TRUE
|
|
AND status = 'active'
|
|
AND deleted_at IS NULL
|
|
AND expires_at > CURRENT_TIMESTAMP
|
|
LIMIT 1;
|
|
|
|
RETURN v_certificate_id;
|
|
END;
|
|
$$ LANGUAGE plpgsql STABLE;
|
|
|
|
COMMENT ON FUNCTION fiscal.get_default_certificate IS 'Obtiene el ID del certificado CSD activo y vigente por defecto del tenant';
|
|
|
|
-- Funcion: Obtener configuracion PAC activa del tenant
|
|
CREATE OR REPLACE FUNCTION fiscal.get_active_pac(p_tenant_id UUID)
|
|
RETURNS TABLE (
|
|
pac_id UUID,
|
|
pac_code VARCHAR,
|
|
environment VARCHAR,
|
|
api_url VARCHAR
|
|
) AS $$
|
|
BEGIN
|
|
RETURN QUERY
|
|
SELECT
|
|
pc.id,
|
|
pc.pac_code,
|
|
pc.environment,
|
|
CASE
|
|
WHEN pc.environment = 'production' THEN pc.production_url
|
|
ELSE pc.sandbox_url
|
|
END as api_url
|
|
FROM fiscal.cfdi_pac_configurations pc
|
|
WHERE pc.tenant_id = p_tenant_id
|
|
AND pc.is_active = TRUE
|
|
AND pc.is_default = TRUE
|
|
AND pc.deleted_at IS NULL
|
|
LIMIT 1;
|
|
END;
|
|
$$ LANGUAGE plpgsql STABLE;
|
|
|
|
COMMENT ON FUNCTION fiscal.get_active_pac IS 'Obtiene la configuracion del PAC activo del tenant';
|
|
|
|
-- Funcion: Validar RFC mexicano
|
|
CREATE OR REPLACE FUNCTION fiscal.validate_rfc(p_rfc VARCHAR)
|
|
RETURNS BOOLEAN AS $$
|
|
DECLARE
|
|
v_pattern_fisica VARCHAR := '^[A-Z&N]{4}[0-9]{6}[A-Z0-9]{3}$';
|
|
v_pattern_moral VARCHAR := '^[A-Z&N]{3}[0-9]{6}[A-Z0-9]{3}$';
|
|
v_rfc VARCHAR;
|
|
BEGIN
|
|
-- Normalizar RFC (mayusculas, sin espacios)
|
|
v_rfc := UPPER(TRIM(p_rfc));
|
|
|
|
-- RFC generico publico en general
|
|
IF v_rfc = 'XAXX010101000' THEN
|
|
RETURN TRUE;
|
|
END IF;
|
|
|
|
-- RFC generico extranjero
|
|
IF v_rfc = 'XEXX010101000' THEN
|
|
RETURN TRUE;
|
|
END IF;
|
|
|
|
-- Validar patron persona fisica (13 caracteres)
|
|
IF LENGTH(v_rfc) = 13 AND v_rfc ~ v_pattern_fisica THEN
|
|
RETURN TRUE;
|
|
END IF;
|
|
|
|
-- Validar patron persona moral (12 caracteres)
|
|
IF LENGTH(v_rfc) = 12 AND v_rfc ~ v_pattern_moral THEN
|
|
RETURN TRUE;
|
|
END IF;
|
|
|
|
RETURN FALSE;
|
|
END;
|
|
$$ LANGUAGE plpgsql IMMUTABLE;
|
|
|
|
COMMENT ON FUNCTION fiscal.validate_rfc IS 'Valida el formato de un RFC mexicano (persona fisica o moral)';
|
|
|
|
-- Funcion: Generar folio CFDI
|
|
CREATE OR REPLACE FUNCTION fiscal.generate_cfdi_folio(
|
|
p_tenant_id UUID,
|
|
p_serie VARCHAR DEFAULT NULL
|
|
)
|
|
RETURNS VARCHAR AS $$
|
|
DECLARE
|
|
v_next_folio INTEGER;
|
|
v_serie VARCHAR;
|
|
BEGIN
|
|
-- Serie por defecto si no se proporciona
|
|
v_serie := COALESCE(p_serie, 'A');
|
|
|
|
-- Obtener siguiente folio
|
|
SELECT COALESCE(MAX(folio::INTEGER), 0) + 1
|
|
INTO v_next_folio
|
|
FROM fiscal.cfdi_invoices
|
|
WHERE tenant_id = p_tenant_id
|
|
AND serie = v_serie
|
|
AND folio ~ '^\d+$'; -- Solo folios numericos
|
|
|
|
RETURN v_next_folio::VARCHAR;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION fiscal.generate_cfdi_folio IS 'Genera el siguiente folio para una serie de CFDI';
|
|
|
|
-- Funcion: Verificar si CFDI puede ser cancelado
|
|
CREATE OR REPLACE FUNCTION fiscal.can_cancel_cfdi(p_cfdi_id UUID)
|
|
RETURNS TABLE (
|
|
can_cancel BOOLEAN,
|
|
reason VARCHAR,
|
|
requires_acceptance BOOLEAN
|
|
) AS $$
|
|
DECLARE
|
|
v_cfdi RECORD;
|
|
v_has_payments BOOLEAN;
|
|
v_is_month_old BOOLEAN;
|
|
BEGIN
|
|
-- Obtener datos del CFDI
|
|
SELECT ci.*, bi.total, bi.amount_paid
|
|
INTO v_cfdi
|
|
FROM fiscal.cfdi_invoices ci
|
|
JOIN billing.invoices bi ON ci.invoice_id = bi.id
|
|
WHERE ci.id = p_cfdi_id;
|
|
|
|
IF NOT FOUND THEN
|
|
RETURN QUERY SELECT FALSE, 'CFDI no encontrado'::VARCHAR, FALSE;
|
|
RETURN;
|
|
END IF;
|
|
|
|
-- Verificar estado
|
|
IF v_cfdi.status != 'stamped' THEN
|
|
RETURN QUERY SELECT FALSE, 'CFDI no esta timbrado'::VARCHAR, FALSE;
|
|
RETURN;
|
|
END IF;
|
|
|
|
-- Verificar si ya tiene solicitud de cancelacion
|
|
IF EXISTS (
|
|
SELECT 1 FROM fiscal.cfdi_cancellation_requests
|
|
WHERE cfdi_invoice_id = p_cfdi_id
|
|
AND status NOT IN ('rejected', 'error')
|
|
) THEN
|
|
RETURN QUERY SELECT FALSE, 'Ya existe solicitud de cancelacion'::VARCHAR, FALSE;
|
|
RETURN;
|
|
END IF;
|
|
|
|
-- Verificar si tiene pagos aplicados
|
|
v_has_payments := COALESCE(v_cfdi.amount_paid, 0) > 0;
|
|
|
|
-- Verificar si tiene mas de un mes
|
|
v_is_month_old := v_cfdi.stamp_date < (CURRENT_DATE - INTERVAL '1 month');
|
|
|
|
-- Determinar si requiere aceptacion del receptor
|
|
-- Segun reglas SAT 2022+
|
|
IF v_cfdi.total > 5000 OR v_is_month_old OR v_has_payments THEN
|
|
RETURN QUERY SELECT TRUE, 'Puede cancelar con aceptacion del receptor'::VARCHAR, TRUE;
|
|
ELSE
|
|
RETURN QUERY SELECT TRUE, 'Puede cancelar sin aceptacion'::VARCHAR, FALSE;
|
|
END IF;
|
|
END;
|
|
$$ LANGUAGE plpgsql STABLE;
|
|
|
|
COMMENT ON FUNCTION fiscal.can_cancel_cfdi IS 'Verifica si un CFDI puede ser cancelado y si requiere aceptacion del receptor';
|
|
|
|
-- Funcion: Calcular saldo pendiente de una factura con pagos parciales
|
|
CREATE OR REPLACE FUNCTION fiscal.get_invoice_payment_balance(p_cfdi_uuid VARCHAR)
|
|
RETURNS TABLE (
|
|
total_amount DECIMAL,
|
|
total_paid DECIMAL,
|
|
remaining_balance DECIMAL,
|
|
installment_count INTEGER
|
|
) AS $$
|
|
BEGIN
|
|
RETURN QUERY
|
|
SELECT
|
|
ci.total,
|
|
COALESCE(SUM(pcd.amount_paid), 0) as total_paid,
|
|
ci.total - COALESCE(SUM(pcd.amount_paid), 0) as remaining_balance,
|
|
COUNT(pcd.id)::INTEGER as installment_count
|
|
FROM fiscal.cfdi_invoices ci
|
|
LEFT JOIN fiscal.cfdi_payment_complement_documents pcd
|
|
ON pcd.related_uuid = ci.uuid
|
|
LEFT JOIN fiscal.cfdi_payment_complements pc
|
|
ON pc.id = pcd.payment_complement_id
|
|
AND pc.status = 'stamped'
|
|
WHERE ci.uuid = p_cfdi_uuid
|
|
GROUP BY ci.id, ci.total;
|
|
END;
|
|
$$ LANGUAGE plpgsql STABLE;
|
|
|
|
COMMENT ON FUNCTION fiscal.get_invoice_payment_balance IS 'Obtiene el saldo pendiente de una factura considerando complementos de pago';
|
|
|
|
-- Funcion: Obtener resumen de CFDIs por periodo
|
|
CREATE OR REPLACE FUNCTION fiscal.get_cfdi_summary(
|
|
p_tenant_id UUID,
|
|
p_start_date DATE,
|
|
p_end_date DATE
|
|
)
|
|
RETURNS TABLE (
|
|
voucher_type fiscal.cfdi_voucher_type,
|
|
status fiscal.cfdi_status,
|
|
count BIGINT,
|
|
total_amount DECIMAL
|
|
) AS $$
|
|
BEGIN
|
|
RETURN QUERY
|
|
SELECT
|
|
ci.voucher_type,
|
|
ci.status,
|
|
COUNT(*) as count,
|
|
SUM(ci.total) as total_amount
|
|
FROM fiscal.cfdi_invoices ci
|
|
WHERE ci.tenant_id = p_tenant_id
|
|
AND ci.created_at::DATE BETWEEN p_start_date AND p_end_date
|
|
AND ci.deleted_at IS NULL
|
|
GROUP BY ci.voucher_type, ci.status
|
|
ORDER BY ci.voucher_type, ci.status;
|
|
END;
|
|
$$ LANGUAGE plpgsql STABLE;
|
|
|
|
COMMENT ON FUNCTION fiscal.get_cfdi_summary IS 'Obtiene resumen de CFDIs agrupados por tipo y estado en un periodo';
|
|
|
|
-- =====================
|
|
-- TRIGGERS
|
|
-- =====================
|
|
|
|
-- Trigger: Actualizar updated_at en cfdi_invoices
|
|
CREATE OR REPLACE FUNCTION fiscal.update_cfdi_timestamp()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
NEW.updated_at = CURRENT_TIMESTAMP;
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
DROP TRIGGER IF EXISTS trg_cfdi_invoices_timestamp ON fiscal.cfdi_invoices;
|
|
CREATE TRIGGER trg_cfdi_invoices_timestamp
|
|
BEFORE UPDATE ON fiscal.cfdi_invoices
|
|
FOR EACH ROW EXECUTE FUNCTION fiscal.update_cfdi_timestamp();
|
|
|
|
DROP TRIGGER IF EXISTS trg_cfdi_certificates_timestamp ON fiscal.cfdi_certificates;
|
|
CREATE TRIGGER trg_cfdi_certificates_timestamp
|
|
BEFORE UPDATE ON fiscal.cfdi_certificates
|
|
FOR EACH ROW EXECUTE FUNCTION fiscal.update_cfdi_timestamp();
|
|
|
|
DROP TRIGGER IF EXISTS trg_cfdi_pac_timestamp ON fiscal.cfdi_pac_configurations;
|
|
CREATE TRIGGER trg_cfdi_pac_timestamp
|
|
BEFORE UPDATE ON fiscal.cfdi_pac_configurations
|
|
FOR EACH ROW EXECUTE FUNCTION fiscal.update_cfdi_timestamp();
|
|
|
|
DROP TRIGGER IF EXISTS trg_cfdi_pcomp_timestamp ON fiscal.cfdi_payment_complements;
|
|
CREATE TRIGGER trg_cfdi_pcomp_timestamp
|
|
BEFORE UPDATE ON fiscal.cfdi_payment_complements
|
|
FOR EACH ROW EXECUTE FUNCTION fiscal.update_cfdi_timestamp();
|
|
|
|
DROP TRIGGER IF EXISTS trg_cfdi_cancel_timestamp ON fiscal.cfdi_cancellation_requests;
|
|
CREATE TRIGGER trg_cfdi_cancel_timestamp
|
|
BEFORE UPDATE ON fiscal.cfdi_cancellation_requests
|
|
FOR EACH ROW EXECUTE FUNCTION fiscal.update_cfdi_timestamp();
|
|
|
|
-- Trigger: Log automatico de cambios de estado
|
|
CREATE OR REPLACE FUNCTION fiscal.log_cfdi_status_change()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
-- Solo si cambio el status
|
|
IF OLD.status IS DISTINCT FROM NEW.status THEN
|
|
INSERT INTO fiscal.cfdi_operation_logs (
|
|
tenant_id,
|
|
cfdi_invoice_id,
|
|
cfdi_uuid,
|
|
operation_type,
|
|
status_before,
|
|
status_after,
|
|
success,
|
|
created_by
|
|
) VALUES (
|
|
NEW.tenant_id,
|
|
NEW.id,
|
|
NEW.uuid,
|
|
CASE
|
|
WHEN NEW.status = 'stamped' THEN 'stamp'::fiscal.cfdi_operation_type
|
|
WHEN NEW.status = 'cancelled' THEN 'cancel_complete'::fiscal.cfdi_operation_type
|
|
WHEN NEW.status = 'error' THEN 'error'::fiscal.cfdi_operation_type
|
|
ELSE 'create'::fiscal.cfdi_operation_type
|
|
END,
|
|
OLD.status::VARCHAR,
|
|
NEW.status::VARCHAR,
|
|
NEW.status NOT IN ('error'),
|
|
NEW.updated_by
|
|
);
|
|
END IF;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
DROP TRIGGER IF EXISTS trg_cfdi_status_log ON fiscal.cfdi_invoices;
|
|
CREATE TRIGGER trg_cfdi_status_log
|
|
AFTER UPDATE ON fiscal.cfdi_invoices
|
|
FOR EACH ROW EXECUTE FUNCTION fiscal.log_cfdi_status_change();
|
|
|
|
-- Trigger: Validar RFC antes de insertar CFDI
|
|
CREATE OR REPLACE FUNCTION fiscal.validate_cfdi_rfc()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
-- Validar RFC emisor
|
|
IF NOT fiscal.validate_rfc(NEW.issuer_rfc) THEN
|
|
RAISE EXCEPTION 'RFC del emisor invalido: %', NEW.issuer_rfc;
|
|
END IF;
|
|
|
|
-- Validar RFC receptor
|
|
IF NOT fiscal.validate_rfc(NEW.receiver_rfc) THEN
|
|
RAISE EXCEPTION 'RFC del receptor invalido: %', NEW.receiver_rfc;
|
|
END IF;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
DROP TRIGGER IF EXISTS trg_cfdi_validate_rfc ON fiscal.cfdi_invoices;
|
|
CREATE TRIGGER trg_cfdi_validate_rfc
|
|
BEFORE INSERT OR UPDATE ON fiscal.cfdi_invoices
|
|
FOR EACH ROW EXECUTE FUNCTION fiscal.validate_cfdi_rfc();
|
|
|
|
-- =====================
|
|
-- VISTAS
|
|
-- =====================
|
|
|
|
-- Vista: CFDIs pendientes de timbrar
|
|
CREATE OR REPLACE VIEW fiscal.v_cfdi_pending_stamp AS
|
|
SELECT
|
|
ci.id,
|
|
ci.tenant_id,
|
|
ci.invoice_id,
|
|
ci.serie,
|
|
ci.folio,
|
|
ci.voucher_type,
|
|
ci.status,
|
|
ci.issuer_rfc,
|
|
ci.receiver_rfc,
|
|
ci.receiver_name,
|
|
ci.total,
|
|
ci.created_at,
|
|
bi.invoice_number,
|
|
bi.invoice_date
|
|
FROM fiscal.cfdi_invoices ci
|
|
JOIN billing.invoices bi ON ci.invoice_id = bi.id
|
|
WHERE ci.status IN ('draft', 'pending')
|
|
AND ci.deleted_at IS NULL
|
|
ORDER BY ci.created_at;
|
|
|
|
COMMENT ON VIEW fiscal.v_cfdi_pending_stamp IS 'CFDIs pendientes de timbrar';
|
|
|
|
-- Vista: Cancelaciones pendientes de respuesta
|
|
CREATE OR REPLACE VIEW fiscal.v_cfdi_pending_cancellations AS
|
|
SELECT
|
|
cr.id,
|
|
cr.tenant_id,
|
|
cr.cfdi_uuid,
|
|
cr.cancellation_reason,
|
|
cr.status,
|
|
cr.requested_at,
|
|
cr.submitted_at,
|
|
cr.expires_at,
|
|
ci.serie,
|
|
ci.folio,
|
|
ci.receiver_rfc,
|
|
ci.receiver_name,
|
|
ci.total
|
|
FROM fiscal.cfdi_cancellation_requests cr
|
|
JOIN fiscal.cfdi_invoices ci ON cr.cfdi_invoice_id = ci.id
|
|
WHERE cr.status IN ('submitted', 'in_process')
|
|
ORDER BY cr.expires_at;
|
|
|
|
COMMENT ON VIEW fiscal.v_cfdi_pending_cancellations IS 'Solicitudes de cancelacion pendientes de respuesta';
|
|
|
|
-- Vista: Certificados por vencer
|
|
CREATE OR REPLACE VIEW fiscal.v_certificates_expiring AS
|
|
SELECT
|
|
cc.id,
|
|
cc.tenant_id,
|
|
cc.certificate_number,
|
|
cc.rfc,
|
|
cc.status,
|
|
cc.expires_at,
|
|
cc.is_default,
|
|
t.name as tenant_name,
|
|
(cc.expires_at - CURRENT_TIMESTAMP) as time_to_expire
|
|
FROM fiscal.cfdi_certificates cc
|
|
JOIN auth.tenants t ON cc.tenant_id = t.id
|
|
WHERE cc.deleted_at IS NULL
|
|
AND cc.status = 'active'
|
|
AND cc.expires_at < (CURRENT_TIMESTAMP + INTERVAL '30 days')
|
|
ORDER BY cc.expires_at;
|
|
|
|
COMMENT ON VIEW fiscal.v_certificates_expiring IS 'Certificados CSD que vencen en los proximos 30 dias';
|
|
|
|
-- Vista: Resumen de uso de timbres por PAC
|
|
CREATE OR REPLACE VIEW fiscal.v_pac_stamp_usage AS
|
|
SELECT
|
|
pc.tenant_id,
|
|
pc.pac_code,
|
|
pc.pac_name,
|
|
pc.environment,
|
|
pc.monthly_stamp_limit,
|
|
pc.stamps_used_this_month,
|
|
CASE
|
|
WHEN pc.monthly_stamp_limit IS NULL THEN NULL
|
|
ELSE (pc.stamps_used_this_month::DECIMAL / pc.monthly_stamp_limit * 100)
|
|
END as usage_percentage,
|
|
pc.contract_expires_at,
|
|
t.name as tenant_name
|
|
FROM fiscal.cfdi_pac_configurations pc
|
|
JOIN auth.tenants t ON pc.tenant_id = t.id
|
|
WHERE pc.is_active = TRUE
|
|
AND pc.deleted_at IS NULL
|
|
ORDER BY pc.tenant_id, pc.pac_code;
|
|
|
|
COMMENT ON VIEW fiscal.v_pac_stamp_usage IS 'Uso de timbres por PAC y tenant';
|
|
|
|
-- Vista: Facturas con saldo pendiente (PPD)
|
|
CREATE OR REPLACE VIEW fiscal.v_invoices_pending_payment AS
|
|
SELECT
|
|
ci.id,
|
|
ci.tenant_id,
|
|
ci.uuid,
|
|
ci.serie,
|
|
ci.folio,
|
|
ci.receiver_rfc,
|
|
ci.receiver_name,
|
|
ci.total,
|
|
ci.stamp_date,
|
|
COALESCE(SUM(pcd.amount_paid), 0) as total_paid,
|
|
ci.total - COALESCE(SUM(pcd.amount_paid), 0) as remaining_balance,
|
|
COUNT(pcd.id) as installment_count
|
|
FROM fiscal.cfdi_invoices ci
|
|
LEFT JOIN fiscal.cfdi_payment_complement_documents pcd
|
|
ON pcd.related_uuid = ci.uuid
|
|
LEFT JOIN fiscal.cfdi_payment_complements pc
|
|
ON pc.id = pcd.payment_complement_id
|
|
AND pc.status = 'stamped'
|
|
WHERE ci.payment_method = 'PPD'
|
|
AND ci.status = 'stamped'
|
|
AND ci.deleted_at IS NULL
|
|
GROUP BY ci.id
|
|
HAVING ci.total - COALESCE(SUM(pcd.amount_paid), 0) > 0
|
|
ORDER BY ci.stamp_date;
|
|
|
|
COMMENT ON VIEW fiscal.v_invoices_pending_payment IS 'Facturas PPD con saldo pendiente de complemento de pago';
|
|
|
|
-- =====================
|
|
-- FIN DEL ARCHIVO
|
|
-- =====================
|