Add product composition export
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m16s

This commit is contained in:
Cauê Faleiros
2026-07-31 10:30:49 -03:00
parent 5559e9951a
commit b431802d50
5 changed files with 134 additions and 42 deletions

View File

@@ -10,7 +10,7 @@ const {
getProductDetailsAnalytics,
getRfmAnalytics
} = require('../services/analyticsService');
const { getProductComposition } = require('../services/productionOrderService');
const { getProductComposition, listProductCompositions } = require('../services/productionOrderService');
const router = express.Router();
@@ -69,6 +69,15 @@ router.get('/analytics/products/:productId/composition', verifyToken, async (req
}
});
router.get('/analytics/product-compositions', verifyToken, async (req, res) => {
try {
res.json({ compositions: await listProductCompositions() });
} catch (error) {
console.error('Error exporting product compositions:', error);
res.status(500).json({ error: 'Internal Server Error' });
}
});
router.get('/analytics/clients', verifyToken, async (req, res) => {
try {
res.json(await getClientAnalytics(getClientAnalyticsFilters(req.query)));

View File

@@ -699,9 +699,8 @@ const mapProductCompositionRow = (row) => ({
})) : []
});
const getProductComposition = async (productId, client = pool) => {
const findProductCompositions = async (productId, client = pool) => {
const identity = normalizeText(productId);
if (!identity) return null;
const result = await client.query(`
SELECT
@@ -751,15 +750,22 @@ const getProductComposition = async (productId, client = pool) => {
FROM product_compositions composition
WHERE composition.source = $1
AND (
composition.finished_tiny_product_id = $2
$2 = ''
OR composition.finished_tiny_product_id = $2
OR composition.finished_product_sku = UPPER($2)
)
LIMIT 1;
ORDER BY composition.finished_product_sku, composition.finished_product_description;
`, [TINY_OLIST_V3_COMPOSITION_SOURCE, identity]);
return result.rows[0] ? mapProductCompositionRow(result.rows[0]) : null;
return result.rows.map(mapProductCompositionRow);
};
const getProductComposition = async (productId, client = pool) => (
(await findProductCompositions(productId, client))[0] || null
);
const listProductCompositions = async (client = pool) => findProductCompositions('', client);
const upsertTinyProductComposition = async (order, components) => {
const finishedTinyProductId = getTinyProductIdFromCompositionReference(order.tinyId);
const finishedProductSku = normalizeSku(order.productSku);
@@ -1051,6 +1057,7 @@ const updateProductionOrderStatus = async (id, status) => {
module.exports = {
createProductionOrders,
getProductComposition,
listProductCompositions,
listProductionOrders,
normalizeStatus,
upsertTinyProductionOrderDetail,

View File

@@ -13,6 +13,7 @@ const getRouteHandler = (router, method, path) => {
const tinySyncHandler = getRouteHandler(productionOrderRouter, 'post', '/production-orders/tiny-sync');
const productCompositionHandler = getRouteHandler(analyticsRouter, 'get', '/analytics/products/:productId/composition');
const productCompositionsHandler = getRouteHandler(analyticsRouter, 'get', '/analytics/product-compositions');
const invokeHandler = async (handler, req) => {
let statusCode = 200;
@@ -132,18 +133,17 @@ const withFakeDatabase = async (run) => {
}
if (normalizedSql.includes('FROM product_compositions composition')) {
const composition = state.compositions.find(item => (
const compositions = state.compositions.filter(item => (
item.source === params[0]
&& (item.finished_tiny_product_id === params[1] || item.finished_product_sku === String(params[1]).toUpperCase())
&& (params[1] === '' || item.finished_tiny_product_id === params[1] || item.finished_product_sku === String(params[1]).toUpperCase())
));
if (!composition) return { rows: [] };
return {
rows: [{
rows: compositions.map(composition => ({
...composition,
components: state.compositionComponents
.filter(item => item.product_composition_id === composition.id)
.map(item => ({ ...item, component_product_id: null }))
}]
}))
};
}
@@ -243,6 +243,17 @@ test('product composition detail lookup returns stored components', async () =>
});
});
test('product composition export endpoint returns every stored composition', async () => {
await withFakeDatabase(async () => {
await invokeHandler(tinySyncHandler, { body: compositionPayload() });
const response = await invokeHandler(productCompositionsHandler, { params: {} });
assert.equal(response.statusCode, 200);
assert.equal(response.body.compositions.length, 1);
assert.equal(response.body.compositions[0].components.length, 2);
});
});
test('ordinary Tiny production-order payloads continue to create production orders', async () => {
await withFakeDatabase(async (state) => {
const response = await invokeHandler(tinySyncHandler, { body: {