const { pool } = require('../db'); const { normalizeStockPayload } = require('../mappers/stockMapper'); const { enqueueStockCampaignItem } = require('./campaignService'); const POSITIVE_STOCK_DELTA_THRESHOLD = 1; const listStock = async () => { const result = await pool.query(` SELECT stock.*, graphs.balance::float8 AS graphs_balance, graphs.reason AS graphs_balance_reason, graphs.effective_at AS graphs_balance_effective_at, graphs.updated_by AS graphs_balance_updated_by, graphs.updated_at AS graphs_balance_updated_at, COALESCE(graphs.balance, stock.saldo)::float8 AS planning_balance FROM stock LEFT JOIN stock_graphs_balances graphs ON graphs.produto_id = stock.produto_id ORDER BY stock.nome, stock.produto_id; `); return result.rows; }; const getStockBalance = async (produtoId, client = pool) => { const result = await client.query(` SELECT stock.*, graphs.balance::float8 AS graphs_balance, graphs.reason AS graphs_balance_reason, graphs.effective_at AS graphs_balance_effective_at, graphs.updated_by AS graphs_balance_updated_by, graphs.updated_at AS graphs_balance_updated_at, COALESCE(graphs.balance, stock.saldo)::float8 AS planning_balance FROM stock LEFT JOIN stock_graphs_balances graphs ON graphs.produto_id = stock.produto_id WHERE stock.produto_id = $1; `, [produtoId]); return result.rows[0] || null; }; const listGraphsBalanceHistory = async (produtoId) => { const result = await pool.query(` SELECT id, produto_id, tiny_balance::float8 AS tiny_balance, graphs_balance::float8 AS graphs_balance, reason, effective_at, action, changed_by, created_at FROM stock_graphs_balance_history WHERE produto_id = $1 ORDER BY created_at DESC, id DESC LIMIT 20; `, [produtoId]); return result.rows; }; const normalizeGraphsBalancePayload = (payload) => { const balance = Number(payload?.balance); const reason = String(payload?.reason || '').trim(); const effectiveAt = String(payload?.effectiveAt || '').trim() || null; if (!Number.isFinite(balance)) { const error = new Error('Informe um saldo físico válido para o Graphs.'); error.statusCode = 400; throw error; } if (!reason) { const error = new Error('Informe o motivo da conferência de saldo.'); error.statusCode = 400; throw error; } if (effectiveAt && !/^\d{4}-\d{2}-\d{2}$/.test(effectiveAt)) { const error = new Error('Informe uma data de referência válida.'); error.statusCode = 400; throw error; } return { balance, reason, effectiveAt }; }; const saveGraphsBalance = async (produtoId, payload, updatedBy) => { const normalizedProductId = String(produtoId || '').trim(); if (!normalizedProductId) { const error = new Error('Material inválido para ajuste de saldo.'); error.statusCode = 400; throw error; } const { balance, reason, effectiveAt } = normalizeGraphsBalancePayload(payload); const client = await pool.connect(); try { await client.query('BEGIN'); const stock = await client.query('SELECT saldo FROM stock WHERE produto_id = $1 FOR UPDATE', [normalizedProductId]); if (!stock.rows[0]) { const error = new Error('Material não encontrado no estoque importado.'); error.statusCode = 404; throw error; } await client.query(` INSERT INTO stock_graphs_balances (produto_id, balance, reason, effective_at, updated_by) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (produto_id) DO UPDATE SET balance = EXCLUDED.balance, reason = EXCLUDED.reason, effective_at = EXCLUDED.effective_at, updated_by = EXCLUDED.updated_by, updated_at = CURRENT_TIMESTAMP; `, [normalizedProductId, balance, reason, effectiveAt, updatedBy || null]); await client.query(` INSERT INTO stock_graphs_balance_history ( produto_id, tiny_balance, graphs_balance, reason, effective_at, action, changed_by ) VALUES ($1, $2, $3, $4, $5, 'set', $6); `, [normalizedProductId, stock.rows[0].saldo, balance, reason, effectiveAt, updatedBy || null]); const saved = await getStockBalance(normalizedProductId, client); await client.query('COMMIT'); return saved; } catch (error) { await client.query('ROLLBACK'); throw error; } finally { client.release(); } }; const removeGraphsBalance = async (produtoId, updatedBy) => { const normalizedProductId = String(produtoId || '').trim(); const client = await pool.connect(); try { await client.query('BEGIN'); const current = await client.query(` SELECT stock.saldo, graphs.balance, graphs.reason, graphs.effective_at FROM stock LEFT JOIN stock_graphs_balances graphs ON graphs.produto_id = stock.produto_id WHERE stock.produto_id = $1 FOR UPDATE OF stock; `, [normalizedProductId]); if (!current.rows[0]) { const error = new Error('Material não encontrado no estoque importado.'); error.statusCode = 404; throw error; } if (current.rows[0].balance === null) { const error = new Error('Não há saldo Graphs para remover.'); error.statusCode = 404; throw error; } await client.query('DELETE FROM stock_graphs_balances WHERE produto_id = $1', [normalizedProductId]); await client.query(` INSERT INTO stock_graphs_balance_history ( produto_id, tiny_balance, graphs_balance, reason, effective_at, action, changed_by ) VALUES ($1, $2, $3, $4, $5, 'removed', $6); `, [ normalizedProductId, current.rows[0].saldo, current.rows[0].balance, current.rows[0].reason, current.rows[0].effective_at, updatedBy || null ]); const saved = await getStockBalance(normalizedProductId, client); await client.query('COMMIT'); return saved; } catch (error) { await client.query('ROLLBACK'); throw error; } finally { client.release(); } }; const upsertStockItems = async (payload) => { const client = await pool.connect(); try { await client.query('BEGIN'); const insertQuery = ` INSERT INTO stock (produto_id, nome, saldo, delta_estoque) VALUES ($1, $2, $3, $4) ON CONFLICT (produto_id) DO UPDATE SET nome = EXCLUDED.nome, saldo = EXCLUDED.saldo, delta_estoque = EXCLUDED.delta_estoque, updated_at = CURRENT_TIMESTAMP `; for (const rawItem of payload) { const item = normalizeStockPayload(rawItem); if (!item.produtoId) continue; await client.query(insertQuery, [ item.produtoId, item.nome, item.saldo, item.deltaEstoque ]); if (item.deltaEstoque >= POSITIVE_STOCK_DELTA_THRESHOLD) { await enqueueStockCampaignItem(client, item); } } await client.query('COMMIT'); } catch (error) { await client.query('ROLLBACK'); throw error; } finally { client.release(); } }; module.exports = { listStock, listGraphsBalanceHistory, saveGraphsBalance, removeGraphsBalance, upsertStockItems };