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