All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m33s
230 lines
7.8 KiB
JavaScript
230 lines
7.8 KiB
JavaScript
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
|
|
};
|