-- ============================================================
-- FIX COMPLETO: Flujo de inventario sin duplicacion
-- Fecha: 2026-06-09
-- Autor: Analisis automatico del sistema
-- ============================================================
-- PROBLEMA IDENTIFICADO:
-- El stock se duplicaba al pagar un expense porque:
-- 1. newexpensev2 y updateexpensev4 creaban movimientos de
--    stock al crear/editar un expense (movimientos fantasma).
-- 2. purchaseentry tambien creaba movimientos y actualizaba
--    el stock al recibir fisicamente.
-- 3. setpaymentstatusv2 sumaba de nuevo al stock al pagar.
--
-- ADEMAS:
-- La unica forma de cancelar un expense es rechazando el pago
-- (status = 3), pero setpaymentstatusv2 no revertia el stock
-- al cancelar.
--
-- SOLUCION APLICADA:
-- 1. newexpensev2 y updateexpensev4 ya NO tocan stock ni
--    stock_movements. Solo manejan expenses y purchase_detail.
-- 2. purchaseentry es la UNICA funcion que ingresa stock al
--    recibir mercancia fisicamente.
-- 3. setpaymentstatusv2(status = 1) solo paga, no toca stock.
-- 4. setpaymentstatusv2(status = 3) revierte el stock si ya
--    se habia recibido fisicamente (purchaseentry), creando
--    un movimiento de tipo "Cancellation".
-- 5. Se crea el tipo de movimiento "Cancellation" en
--    movement_type si no existe.
--
-- NOTA: Este cambio es reversible. Si algo falla, ejecutar
--       rollback_inventory_flow_complete.sql
-- ============================================================

-- ============================================================
-- 1. CREAR TIPO DE MOVIMIENTO "Cancellation" SI NO EXISTE
-- ============================================================
DO $$
BEGIN
    IF NOT EXISTS (
        SELECT 1 FROM movement_type WHERE name_mov_type = 'Cancellation'
    ) THEN
        INSERT INTO movement_type(name_mov_type) VALUES ('Cancellation');
    END IF;
END $$;

-- ============================================================
-- 2. FIX: setpaymentstatusv2
-- ============================================================
-- Pagar (status = 1): solo actualiza payments.
-- Cancelar/Rechazar pago (status = 3): revierte stock si hubo
-- recepcion fisica previa (purchaseentry).
-- ============================================================
CREATE OR REPLACE FUNCTION public.setpaymentstatusv2(
    expense integer,
    status integer)
    RETURNS integer
    LANGUAGE 'plpgsql'
    COST 100
    VOLATILE PARALLEL UNSAFE
AS $BODY$
DECLARE
    rows_updated INT := 0;
    details RECORD;
    approval INT := 0;
    stock_anterior BIGINT;
    stock_actual BIGINT;
    v_id_stock_mov INT;
    cancellation_type_id INT;
BEGIN
    SELECT id_approval_stat INTO approval FROM expenses WHERE id_expense = expense;

    -- Solo procesar si el gasto esta aprobado
    IF approval = 1 THEN

        -- ============================================================
        -- PAGAR: status = 1
        -- ============================================================
        IF status = 1 THEN
            UPDATE payments
            SET id_payment_status = status,
                payment_date = CURRENT_DATE
            WHERE id_expense = expense;

            GET DIAGNOSTICS rows_updated = ROW_COUNT;

            IF rows_updated = 0 THEN
                RETURN -1;
            END IF;

            -- NO se toca stock. purchaseentry ya lo hizo correctamente.
            RETURN status;

        -- ============================================================
        -- CANCELAR/RECHAZAR PAGO: status = 3
        -- ============================================================
        ELSIF status = 3 THEN
            UPDATE payments
            SET id_payment_status = status,
                payment_date = CURRENT_DATE
            WHERE id_expense = expense;

            GET DIAGNOSTICS rows_updated = ROW_COUNT;

            IF rows_updated = 0 THEN
                RETURN -1;
            END IF;

            -- Obtener id del tipo de movimiento Cancellation
            SELECT id_mov_type INTO cancellation_type_id
            FROM movement_type
            WHERE name_mov_type = 'Cancellation';

            -- Revertir stock si ya se habia recibido fisicamente
            IF EXISTS (
                SELECT 1
                FROM purchase_detail pd
                WHERE pd.id_expense = expense
                  AND pd.delivered > 0
            ) THEN
                FOR details IN
                    SELECT pd.id_product, pd.delivered
                    FROM purchase_detail pd
                    WHERE pd.id_expense = expense
                      AND pd.delivered > 0
                LOOP
                    SELECT stock INTO stock_anterior
                    FROM stock
                    WHERE id_product = details.id_product
                    FOR UPDATE;

                    stock_actual := stock_anterior - details.delivered;

                    UPDATE stock
                    SET stock = stock_actual
                    WHERE id_product = details.id_product;

                    INSERT INTO stock_movements(
                        id_product,
                        id_mov_type,
                        date_st_mov,
                        id_expense,
                        quantity,
                        stock_before,
                        stock_current
                    )
                    VALUES (
                        details.id_product,
                        cancellation_type_id,
                        CURRENT_DATE,
                        expense,
                        details.delivered * (-1),
                        stock_anterior,
                        stock_actual
                    )
                    RETURNING id_sto_mov INTO v_id_stock_mov;
                END LOOP;
            END IF;

            RETURN status;

        ELSE
            -- Status invalido
            RETURN -2;
        END IF;

    ELSE
        -- No esta aprobado, no se puede actualizar el pago
        RETURN -3;
    END IF;
END;
$BODY$;

-- ============================================================
-- 3. FIX: newexpensev2
-- ============================================================
-- Ya NO crea stock_movements. Solo crea expenses y
-- purchase_detail.
-- ============================================================
CREATE OR REPLACE FUNCTION public.newexpensev2(
    new_description character varying,
    suppliers_id integer,
    new_request_date date,
    new_payment_deadline date,
    request_by integer,
    area integer,
    expense_cat integer,
    currency_id integer,
    needtoapprove boolean,
    products jsonb DEFAULT NULL::jsonb,
    new_subtotal numeric DEFAULT NULL::numeric,
    new_iva numeric DEFAULT NULL::numeric,
    new_ieps numeric DEFAULT NULL::numeric,
    new_total numeric DEFAULT NULL::numeric)
    RETURNS integer
    LANGUAGE 'plpgsql'
    COST 100
    VOLATILE PARALLEL UNSAFE
AS $BODY$
DECLARE
    new_expense INT;
    product JSONB;

    approval_status INT := 3;
    approval_datevar DATE := NULL;

    monedacambio NUMERIC := 1;
    fecha_busqueda DATE;

    productsubtotal NUMERIC := 0;
    producttotal NUMERIC := 0;
    productsiva NUMERIC := 0;

    subtotalcambiomoneda NUMERIC := 0;
    totalcambiomoneda NUMERIC := 0;
    ivacambiomoneda NUMERIC := 0;
    iepscambiomoneda NUMERIC := 0;
BEGIN

    IF needtoapprove THEN
        approval_status := 3;
        approval_datevar := NULL;
    ELSE
        approval_status := 1;
        approval_datevar := CURRENT_DATE;
    END IF;

    -- Si la moneda es dolares (2), buscar tipo de cambio
    IF (currency_id = 2) THEN
        IF (new_payment_deadline < CURRENT_DATE) THEN
            fecha_busqueda := new_payment_deadline;
        ELSE
            fecha_busqueda := CURRENT_DATE;
        END IF;

        LOOP
            SELECT onegetchange(fecha_busqueda) INTO monedacambio;
            EXIT WHEN monedacambio IS NOT NULL;
            fecha_busqueda := fecha_busqueda - INTERVAL '1 day';

            IF fecha_busqueda < CURRENT_DATE - INTERVAL '30 day' THEN
                RAISE NOTICE 'No se encontro tipo de cambio valido en los ultimos 30 dias desde %', fecha_busqueda;
                monedacambio := 1;
                EXIT;
            END IF;
        END LOOP;
    END IF;

    -- Calcular totales de productos
    IF jsonb_array_length(products) > 0 THEN
        SELECT SUM((prod->>'quantity')::NUMERIC * (prod->>'unit_cost')::NUMERIC)
        INTO productsubtotal
        FROM jsonb_array_elements(products) AS prod;

        SELECT SUM((prod->>'quantity')::NUMERIC * (prod->>'unit_cost')::NUMERIC * t.number)
        INTO productsiva
        FROM jsonb_array_elements(products) AS prod
        LEFT JOIN taxes t ON t.id_tax = (prod->>'id_tax')::INT;

        SELECT SUM((prod->>'quantity')::NUMERIC * (prod->>'unit_cost')::NUMERIC * (1 + t.number))
        INTO producttotal
        FROM jsonb_array_elements(products) AS prod
        LEFT JOIN taxes t ON t.id_tax = (prod->>'id_tax')::INT;

        producttotal := producttotal + COALESCE(new_ieps, 0);
    ELSE
        productsubtotal := new_subtotal;
        producttotal := new_total;
        productsiva := new_iva;
    END IF;

    -- Aplicar cambio de moneda
    IF (currency_id = 2) THEN
        subtotalcambiomoneda := productsubtotal * monedacambio;
        ivacambiomoneda := productsiva * monedacambio;
        iepscambiomoneda := new_ieps * monedacambio;
        totalcambiomoneda := producttotal * monedacambio;
    ELSE
        subtotalcambiomoneda := productsubtotal;
        totalcambiomoneda := producttotal;
        ivacambiomoneda := productsiva;
        iepscambiomoneda := new_ieps;
    END IF;

    INSERT INTO expenses(
        id_properties, id_expense_type, id_recurrence,
        description, id_suppliers, request_date,
        payment_deadline, approval_date, id_request_by,
        id_approval_by, id_area, id_expense_cat,
        id_approval_stat, id_currency,
        subtotal, iva, ieps, total
    )
    VALUES (
        1, 2, 3,
        new_description, suppliers_id,
        new_request_date, new_payment_deadline,
        approval_datevar, request_by,
        null, area, expense_cat,
        approval_status, currency_id,
        COALESCE(subtotalcambiomoneda, 0),
        COALESCE(ivacambiomoneda, 0),
        COALESCE(iepscambiomoneda, 0),
        COALESCE(totalcambiomoneda, 0)
    )
    RETURNING id_expense INTO new_expense;

    INSERT INTO payments(id_expense, id_payment_status, payment_date)
    VALUES (new_expense, 2, NULL);

    -- Crear purchase_detail. NO se crea stock_movements aqui.
    IF products IS NOT NULL AND jsonb_array_length(products) > 0 THEN
        FOR product IN SELECT jsonb_array_elements(products)
        LOOP
            INSERT INTO purchase_detail (
                id_expense, id_product, quantity, id_tax,
                unit_cost, total, id_stock_mov
            )
            VALUES (
                new_expense,
                (product->>'id_product')::INT,
                (product->>'quantity')::INT,
                (product->>'id_tax')::INT,
                ((product->>'unit_cost')::NUMERIC * monedacambio),
                COALESCE(
                    (SELECT
                        ((product->>'quantity')::NUMERIC *
                         (product->>'unit_cost')::NUMERIC *
                         (1 + COALESCE(t.number, 0))) * monedacambio
                     FROM taxes t
                     WHERE t.id_tax = (product->>'id_tax')::INT),
                    0
                ),
                NULL  -- id_stock_mov se asigna en purchaseentry
            );
        END LOOP;
    END IF;

    RETURN new_expense;
END;
$BODY$;

-- ============================================================
-- 4. FIX: updateexpensev4
-- ============================================================
-- Ya NO crea ni elimina stock_movements. Solo actualiza
-- expenses y purchase_detail.
-- ============================================================
CREATE OR REPLACE FUNCTION public.updateexpensev4(
    expense_id integer,
    up_description character varying,
    suppliers_id integer,
    up_req_date date,
    up_deadline date,
    up_currency integer,
    up_request_by integer,
    up_area integer,
    up_category integer,
    needtoapprove boolean,
    up_subtotal numeric DEFAULT NULL::numeric,
    up_iva numeric DEFAULT NULL::numeric,
    up_ieps numeric DEFAULT NULL::numeric,
    up_total numeric DEFAULT NULL::numeric,
    products jsonb DEFAULT NULL::jsonb)
    RETURNS integer
    LANGUAGE 'plpgsql'
    COST 100
    VOLATILE PARALLEL UNSAFE
AS $BODY$
DECLARE
    product JSONB;
    approval_status INT := 3;
    approval_datevar date := NULL;

    monedacambio NUMERIC := 1;
    fecha_busqueda DATE;

    productsubtotal NUMERIC := 0;
    producttotal NUMERIC := 0;
    productsiva NUMERIC := 0;

    subtotalcambiomoneda NUMERIC := 0;
    totalcambiomoneda NUMERIC := 0;
    ivacambiomoneda NUMERIC := 0;
    iepscambiomoneda NUMERIC := 0;
BEGIN

    IF needtoapprove THEN
        approval_status := 3;
        approval_datevar := NULL;
    ELSE
        approval_status := 1;
        approval_datevar := CURRENT_DATE;
    END IF;

    -- Si la moneda es dolares (2), buscar tipo de cambio
    IF (up_currency = 2) THEN
        IF (up_deadline < CURRENT_DATE) THEN
            fecha_busqueda := up_deadline;
        ELSE
            fecha_busqueda := CURRENT_DATE;
        END IF;

        LOOP
            SELECT onegetchange(fecha_busqueda) INTO monedacambio;
            EXIT WHEN monedacambio IS NOT NULL;
            fecha_busqueda := fecha_busqueda - INTERVAL '1 day';

            IF fecha_busqueda < CURRENT_DATE - INTERVAL '30 day' THEN
                RAISE NOTICE 'No se encontro tipo de cambio valido en los ultimos 30 dias desde %', fecha_busqueda;
                monedacambio := 1;
                EXIT;
            END IF;
        END LOOP;
    END IF;

    -- Calcular totales de productos
    IF jsonb_array_length(products) > 0 THEN
        SELECT SUM((prod->>'quantity')::NUMERIC * (prod->>'unit_cost')::NUMERIC)
        INTO productsubtotal
        FROM jsonb_array_elements(products) AS prod;

        SELECT SUM((prod->>'quantity')::NUMERIC * (prod->>'unit_cost')::NUMERIC * t.number)
        INTO productsiva
        FROM jsonb_array_elements(products) AS prod
        LEFT JOIN taxes t ON t.id_tax = (prod->>'id_tax')::INT;

        SELECT SUM((prod->>'quantity')::NUMERIC * (prod->>'unit_cost')::NUMERIC * (1 + t.number))
        INTO producttotal
        FROM jsonb_array_elements(products) AS prod
        LEFT JOIN taxes t ON t.id_tax = (prod->>'id_tax')::INT;

        producttotal := producttotal + COALESCE(up_ieps, 0);
    ELSE
        productsubtotal := up_subtotal;
        producttotal := up_total;
        productsiva := up_iva;
    END IF;

    -- Aplicar cambio de moneda
    IF (up_currency = 2) THEN
        subtotalcambiomoneda := productsubtotal * monedacambio;
        ivacambiomoneda := productsiva * monedacambio;
        iepscambiomoneda := up_ieps * monedacambio;
        totalcambiomoneda := producttotal * monedacambio;
    ELSE
        subtotalcambiomoneda := productsubtotal;
        totalcambiomoneda := producttotal;
        ivacambiomoneda := productsiva;
        iepscambiomoneda := up_ieps;
    END IF;

    -- Actualizar la cabecera del gasto
    UPDATE expenses
    SET
        description = up_description,
        id_suppliers = suppliers_id,
        request_date = up_req_date,
        payment_deadline = up_deadline,
        id_currency = up_currency,
        id_request_by = up_request_by,
        id_area = up_area,
        id_expense_cat = up_category,
        approval_date = approval_datevar,
        id_approval_stat = approval_status,
        subtotal = COALESCE(subtotalcambiomoneda, 0),
        iva = COALESCE(ivacambiomoneda, 0),
        ieps = COALESCE(iepscambiomoneda, 0),
        total = COALESCE(totalcambiomoneda, 0)
    WHERE id_expense = expense_id;

    -- Manejar detalle de productos
    IF products IS NOT NULL THEN
        -- Eliminar purchase_detail que ya no vienen en el JSON
        DELETE FROM purchase_detail pd
        WHERE pd.id_expense = expense_id
          AND pd.id_product NOT IN (
            SELECT (p->>'id_product')::INT
            FROM jsonb_array_elements(products) AS p
          );

        -- Actualizar o insertar cada producto del JSON
        FOR product IN SELECT jsonb_array_elements(products)
        LOOP
            UPDATE purchase_detail
            SET
                quantity = (product->>'quantity')::INT,
                id_tax = (product->>'id_tax')::INT,
                unit_cost = ((product->>'unit_cost')::NUMERIC * monedacambio),
                total = COALESCE(
                    (SELECT ((product->>'quantity')::NUMERIC *
                             (product->>'unit_cost')::NUMERIC *
                             (1 + COALESCE(t.number, 0))) * monedacambio
                     FROM taxes t
                     WHERE t.id_tax = (product->>'id_tax')::INT),
                    0
                )
            WHERE id_expense = expense_id
              AND id_product = (product->>'id_product')::INT;

            -- Si no actualizo nada (no existia), insertar
            IF NOT FOUND THEN
                INSERT INTO purchase_detail (
                    id_expense, id_product, quantity, id_tax,
                    unit_cost, total, id_stock_mov
                )
                VALUES (
                    expense_id,
                    (product->>'id_product')::INT,
                    (product->>'quantity')::INT,
                    (product->>'id_tax')::INT,
                    ((product->>'unit_cost')::NUMERIC * monedacambio),
                    COALESCE(
                        (SELECT ((product->>'quantity')::NUMERIC *
                                 (product->>'unit_cost')::NUMERIC *
                                 (1 + COALESCE(t.number, 0))) * monedacambio
                         FROM taxes t
                         WHERE t.id_tax = (product->>'id_tax')::INT),
                        0
                    ),
                    NULL
                );
            END IF;
        END LOOP;
    END IF;

    RETURN 1;
END;
$BODY$;

-- ============================================================
-- 5. FIX: purchaseentry
-- ============================================================
-- Actualiza purchase_detail.id_stock_mov con el movimiento
-- recien creado, para mantener integridad referencial.
-- ============================================================
CREATE OR REPLACE FUNCTION public.purchaseentry(
    purchase_id integer,
    checking integer)
    RETURNS integer
    LANGUAGE 'plpgsql'
    COST 100
    VOLATILE PARALLEL UNSAFE
AS $BODY$
DECLARE
    v_id_product INT;
    v_id_expense INT;
    v_quantity INT;
    v_unit_cost NUMERIC;
    v_delivered INT;
    stock_anterior BIGINT;
    stock_actual BIGINT;
    v_id_stock_mov INT;
    v_quantity_received INT;
BEGIN
    -- Recuperamos la informacion de purchase_detail
    SELECT id_product, id_expense, quantity, unit_cost, delivered
    INTO v_id_product, v_id_expense, v_quantity, v_unit_cost, v_delivered
    FROM purchase_detail
    WHERE id_purchase_dt = purchase_id;

    -- Cantidad efectivamente recibida en este movimiento
    v_quantity_received := LEAST(checking, v_quantity - COALESCE(v_delivered, 0));

    IF v_quantity_received <= 0 THEN
        RETURN 0; -- Ya se recibio todo o cantidad invalida
    END IF;

    UPDATE purchase_detail
    SET delivered = LEAST(COALESCE(delivered, 0) + checking, quantity)
    WHERE id_purchase_dt = purchase_id;

    SELECT stock INTO stock_anterior
    FROM stock WHERE id_product = v_id_product FOR UPDATE;

    stock_actual := stock_anterior + v_quantity_received;

    INSERT INTO stock_movements(
        id_product, id_mov_type, date_st_mov, id_purchase_dt,
        id_expense, quantity, stock_before, stock_current
    )
    VALUES (
        v_id_product, 1, CURRENT_DATE, purchase_id,
        v_id_expense, v_quantity_received, stock_anterior, stock_actual
    )
    RETURNING id_sto_mov INTO v_id_stock_mov;

    UPDATE stock
    SET
        average_cost = CASE
            WHEN average_cost IS NULL THEN v_unit_cost
            ELSE (
                ((average_cost * stock) + (v_quantity_received * v_unit_cost))
                / (stock + v_quantity_received)
            )
        END,
        stock = stock_actual
    WHERE id_product = v_id_product;

    -- Asignar el id del movimiento al purchase_detail
    UPDATE purchase_detail
    SET id_stock_mov = v_id_stock_mov
    WHERE id_purchase_dt = purchase_id;

    RETURN 1;
END;
$BODY$;

-- Confirmacion
SELECT 'Fix completo de flujo de inventario aplicado correctamente' AS resultado;
