feat(brands): Add margin estimation to stores/performance endpoint
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
- Add ?margin_pct query param (default 50% industry standard) - Returns margin_pct and margin_est per store - Includes margin_pct_assumed in response metadata 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -710,6 +710,8 @@ export function createBrandsRouter(pool: Pool): Router {
|
|||||||
const stateCode = req.query.state as string | undefined;
|
const stateCode = req.query.state as string | undefined;
|
||||||
const limit = parseLimit(req.query.limit as string, 100);
|
const limit = parseLimit(req.query.limit as string, 100);
|
||||||
const offset = parseOffset(req.query.offset as string);
|
const offset = parseOffset(req.query.offset as string);
|
||||||
|
// Margin assumption - default 50% (industry standard)
|
||||||
|
const marginPct = Math.min(Math.max(parseFloat(req.query.margin_pct as string) || 50, 0), 100);
|
||||||
|
|
||||||
const startDate = new Date();
|
const startDate = new Date();
|
||||||
startDate.setDate(startDate.getDate() - days);
|
startDate.setDate(startDate.getDate() - days);
|
||||||
@@ -879,11 +881,15 @@ export function createBrandsRouter(pool: Pool): Router {
|
|||||||
brand: brandName,
|
brand: brandName,
|
||||||
period_days: days,
|
period_days: days,
|
||||||
state: stateCode || 'all',
|
state: stateCode || 'all',
|
||||||
|
margin_pct_assumed: marginPct,
|
||||||
summary: {
|
summary: {
|
||||||
total_stores: parseInt(totals.total_stores) || 0,
|
total_stores: parseInt(totals.total_stores) || 0,
|
||||||
total_oos: parseInt(totals.total_oos) || 0,
|
total_oos: parseInt(totals.total_oos) || 0,
|
||||||
},
|
},
|
||||||
stores: result.rows.map(row => ({
|
stores: result.rows.map(row => {
|
||||||
|
const totalSalesEst = row.total_sales_est ? parseFloat(row.total_sales_est) : null;
|
||||||
|
const marginEst = totalSalesEst ? totalSalesEst * (marginPct / 100) : null;
|
||||||
|
return {
|
||||||
store_id: row.store_id,
|
store_id: row.store_id,
|
||||||
store_name: row.store_name,
|
store_name: row.store_name,
|
||||||
state_code: row.state_code,
|
state_code: row.state_code,
|
||||||
@@ -896,7 +902,10 @@ export function createBrandsRouter(pool: Pool): Router {
|
|||||||
oos_pct: parseInt(row.oos_pct) || 0,
|
oos_pct: parseInt(row.oos_pct) || 0,
|
||||||
// Velocity & Sales
|
// Velocity & Sales
|
||||||
avg_daily_units: row.avg_daily_units ? parseFloat(row.avg_daily_units) : null,
|
avg_daily_units: row.avg_daily_units ? parseFloat(row.avg_daily_units) : null,
|
||||||
total_sales_est: row.total_sales_est ? parseFloat(row.total_sales_est) : null,
|
total_sales_est: totalSalesEst,
|
||||||
|
// Margin (estimated)
|
||||||
|
margin_pct: marginPct,
|
||||||
|
margin_est: marginEst ? parseFloat(marginEst.toFixed(2)) : null,
|
||||||
// Inventory
|
// Inventory
|
||||||
avg_days_on_hand: row.avg_days_on_hand ? parseFloat(row.avg_days_on_hand) : null,
|
avg_days_on_hand: row.avg_days_on_hand ? parseFloat(row.avg_days_on_hand) : null,
|
||||||
total_stock: row.total_stock ? parseInt(row.total_stock) : null,
|
total_stock: row.total_stock ? parseInt(row.total_stock) : null,
|
||||||
@@ -906,7 +915,7 @@ export function createBrandsRouter(pool: Pool): Router {
|
|||||||
lost_opportunity: row.lost_opportunity ? parseFloat(row.lost_opportunity) : null,
|
lost_opportunity: row.lost_opportunity ? parseFloat(row.lost_opportunity) : null,
|
||||||
// Categories breakdown
|
// Categories breakdown
|
||||||
categories: row.categories || {},
|
categories: row.categories || {},
|
||||||
})),
|
};}),
|
||||||
pagination: {
|
pagination: {
|
||||||
limit,
|
limit,
|
||||||
offset,
|
offset,
|
||||||
|
|||||||
Reference in New Issue
Block a user