fix(analytics): cost weighed lines by weight, not row count
A weight-based line is one weighing, so order_items.quantity is pinned to 1 while unit_price and unit_cost are per unit of weight. Analytics SQL was multiplying and dividing per-unit rates by the raw quantity, costing a 4.2 ons fish as a single ons: standard_hpp_total and moving_average_hpp_total came out far too low across all four product reports, overstating gross profit, and average_price and fifo_hpp_per_unit read per weighing while standard_hpp_per_unit read per unit, so the three HPP figures in one row could not be compared. Adds billableQty and billableQtyNet as the single place that decides the multiplier, mirroring entities.OrderItem.BillableQuantity. quantity_sold and total_items stay as weighing counts; weight_sold already carries the amount. revenue and fifo_hpp_total were already correct via total_price/total_cost. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,20 @@ type AnalyticsRepository interface {
|
|||||||
GetOutletName(ctx context.Context, organizationID uuid.UUID, outletID uuid.UUID) (string, error)
|
GetOutletName(ctx context.Context, organizationID uuid.UUID, outletID uuid.UUID) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A weight-based line is a single weighing: order_items.quantity is pinned to 1 and
|
||||||
|
// the weighed amount lives in order_items.weight, while unit_price and unit_cost are
|
||||||
|
// per unit of weight. Anything that multiplies or divides by a per-unit rate must use
|
||||||
|
// the weighed amount, not the row count, or a 4.2 ons fish is costed as a single ons.
|
||||||
|
// These mirror entities.OrderItem.BillableQuantity for aggregate SQL.
|
||||||
|
const (
|
||||||
|
// billableQty is the multiplier for a per-unit price or cost, before refunds.
|
||||||
|
billableQty = `COALESCE(oi.weight, oi.quantity)`
|
||||||
|
|
||||||
|
// billableQtyNet is billableQty after refunds. A weighed line is always refunded
|
||||||
|
// whole, so with quantity pinned to 1 the factor is the full weight or zero.
|
||||||
|
billableQtyNet = `COALESCE(oi.weight, 1) * (oi.quantity - COALESCE(oi.refund_quantity, 0))`
|
||||||
|
)
|
||||||
|
|
||||||
type AnalyticsRepositoryImpl struct {
|
type AnalyticsRepositoryImpl struct {
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
}
|
}
|
||||||
@@ -464,7 +478,7 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organ
|
|||||||
COALESCE(SUM(oi.weight), 0) as weight_sold,
|
COALESCE(SUM(oi.weight), 0) as weight_sold,
|
||||||
COALESCE(SUM(oi.total_price), 0) as revenue,
|
COALESCE(SUM(oi.total_price), 0) as revenue,
|
||||||
CASE
|
CASE
|
||||||
WHEN SUM(oi.quantity) > 0 THEN COALESCE(SUM(oi.total_price), 0) / SUM(oi.quantity)
|
WHEN SUM(` + billableQty + `) > 0 THEN COALESCE(SUM(oi.total_price), 0) / SUM(` + billableQty + `)
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END as average_price,
|
END as average_price,
|
||||||
COUNT(DISTINCT oi.order_id) as order_count,
|
COUNT(DISTINCT oi.order_id) as order_count,
|
||||||
@@ -479,14 +493,14 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organ
|
|||||||
FROM product_recipes pr
|
FROM product_recipes pr
|
||||||
JOIN ingredients i ON pr.ingredient_id = i.id
|
JOIN ingredients i ON pr.ingredient_id = i.id
|
||||||
WHERE pr.product_id = p.id
|
WHERE pr.product_id = p.id
|
||||||
), p.cost, 0) * COALESCE(SUM(oi.quantity), 0) as standard_hpp_total,
|
), p.cost, 0) * COALESCE(SUM(` + billableQty + `), 0) as standard_hpp_total,
|
||||||
CASE
|
CASE
|
||||||
WHEN SUM(oi.quantity) > 0 THEN COALESCE(SUM(oi.total_cost), 0) / SUM(oi.quantity)
|
WHEN SUM(` + billableQty + `) > 0 THEN COALESCE(SUM(oi.total_cost), 0) / SUM(` + billableQty + `)
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END as fifo_hpp_per_unit,
|
END as fifo_hpp_per_unit,
|
||||||
COALESCE(SUM(oi.total_cost), 0) as fifo_hpp_total,
|
COALESCE(SUM(oi.total_cost), 0) as fifo_hpp_total,
|
||||||
COALESCE(mahpp.hpp_per_unit, p.cost, 0) as moving_average_hpp_per_unit,
|
COALESCE(mahpp.hpp_per_unit, p.cost, 0) as moving_average_hpp_per_unit,
|
||||||
COALESCE(mahpp.hpp_per_unit, p.cost, 0) * COALESCE(SUM(oi.quantity), 0) as moving_average_hpp_total
|
COALESCE(mahpp.hpp_per_unit, p.cost, 0) * COALESCE(SUM(` + billableQty + `), 0) as moving_average_hpp_total
|
||||||
`).
|
`).
|
||||||
Joins("JOIN products p ON oi.product_id = p.id").
|
Joins("JOIN products p ON oi.product_id = p.id").
|
||||||
Joins("JOIN categories c ON p.category_id = c.id").
|
Joins("JOIN categories c ON p.category_id = c.id").
|
||||||
@@ -537,9 +551,9 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalyticsPerCategory(ctx context.Con
|
|||||||
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.quantity - COALESCE(oi.refund_quantity, 0) ELSE 0 END), 0) as total_quantity,
|
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.quantity - COALESCE(oi.refund_quantity, 0) ELSE 0 END), 0) as total_quantity,
|
||||||
COUNT(DISTINCT p.id) as product_count,
|
COUNT(DISTINCT p.id) as product_count,
|
||||||
COUNT(DISTINCT oi.order_id) as order_count,
|
COUNT(DISTINCT oi.order_id) as order_count,
|
||||||
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN COALESCE(shpp.hpp_per_unit, p.cost, 0) * (oi.quantity - COALESCE(oi.refund_quantity, 0)) ELSE 0 END), 0) as total_standard_hpp,
|
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN COALESCE(shpp.hpp_per_unit, p.cost, 0) * ` + billableQtyNet + ` ELSE 0 END), 0) as total_standard_hpp,
|
||||||
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_cost * ((oi.quantity - COALESCE(oi.refund_quantity, 0))::float / NULLIF(oi.quantity, 0)) ELSE 0 END), 0) as total_fifo_hpp,
|
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_cost * ((oi.quantity - COALESCE(oi.refund_quantity, 0))::float / NULLIF(oi.quantity, 0)) ELSE 0 END), 0) as total_fifo_hpp,
|
||||||
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN COALESCE(mahpp.hpp_per_unit, p.cost, 0) * (oi.quantity - COALESCE(oi.refund_quantity, 0)) ELSE 0 END), 0) as total_moving_average_hpp
|
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN COALESCE(mahpp.hpp_per_unit, p.cost, 0) * ` + billableQtyNet + ` ELSE 0 END), 0) as total_moving_average_hpp
|
||||||
`).
|
`).
|
||||||
Joins("JOIN products p ON oi.product_id = p.id").
|
Joins("JOIN products p ON oi.product_id = p.id").
|
||||||
Joins("JOIN categories c ON p.category_id = c.id").
|
Joins("JOIN categories c ON p.category_id = c.id").
|
||||||
@@ -590,9 +604,9 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalyticsPerParentCategory(ctx conte
|
|||||||
COUNT(DISTINCT c.id) as category_count,
|
COUNT(DISTINCT c.id) as category_count,
|
||||||
COUNT(DISTINCT p.id) as product_count,
|
COUNT(DISTINCT p.id) as product_count,
|
||||||
COUNT(DISTINCT oi.order_id) as order_count,
|
COUNT(DISTINCT oi.order_id) as order_count,
|
||||||
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN COALESCE(shpp.hpp_per_unit, p.cost, 0) * (oi.quantity - COALESCE(oi.refund_quantity, 0)) ELSE 0 END), 0) as total_standard_hpp,
|
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN COALESCE(shpp.hpp_per_unit, p.cost, 0) * ` + billableQtyNet + ` ELSE 0 END), 0) as total_standard_hpp,
|
||||||
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_cost * ((oi.quantity - COALESCE(oi.refund_quantity, 0))::float / NULLIF(oi.quantity, 0)) ELSE 0 END), 0) as total_fifo_hpp,
|
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_cost * ((oi.quantity - COALESCE(oi.refund_quantity, 0))::float / NULLIF(oi.quantity, 0)) ELSE 0 END), 0) as total_fifo_hpp,
|
||||||
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN COALESCE(mahpp.hpp_per_unit, p.cost, 0) * (oi.quantity - COALESCE(oi.refund_quantity, 0)) ELSE 0 END), 0) as total_moving_average_hpp
|
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN COALESCE(mahpp.hpp_per_unit, p.cost, 0) * ` + billableQtyNet + ` ELSE 0 END), 0) as total_moving_average_hpp
|
||||||
`).
|
`).
|
||||||
Joins("JOIN products p ON oi.product_id = p.id").
|
Joins("JOIN products p ON oi.product_id = p.id").
|
||||||
Joins("JOIN categories c ON p.category_id = c.id").
|
Joins("JOIN categories c ON p.category_id = c.id").
|
||||||
@@ -707,9 +721,9 @@ func (r *AnalyticsRepositoryImpl) GetParentCategoryAnalyticsDetail(ctx context.C
|
|||||||
COUNT(DISTINCT c.id) as category_count,
|
COUNT(DISTINCT c.id) as category_count,
|
||||||
COUNT(DISTINCT p.id) as product_count,
|
COUNT(DISTINCT p.id) as product_count,
|
||||||
COUNT(DISTINCT oi.order_id) as order_count,
|
COUNT(DISTINCT oi.order_id) as order_count,
|
||||||
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN COALESCE(shpp.hpp_per_unit, p.cost, 0) * (oi.quantity - COALESCE(oi.refund_quantity, 0)) ELSE 0 END), 0) as total_standard_hpp,
|
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN COALESCE(shpp.hpp_per_unit, p.cost, 0) * ` + billableQtyNet + ` ELSE 0 END), 0) as total_standard_hpp,
|
||||||
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_cost * ((oi.quantity - COALESCE(oi.refund_quantity, 0))::float / NULLIF(oi.quantity, 0)) ELSE 0 END), 0) as total_fifo_hpp,
|
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_cost * ((oi.quantity - COALESCE(oi.refund_quantity, 0))::float / NULLIF(oi.quantity, 0)) ELSE 0 END), 0) as total_fifo_hpp,
|
||||||
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN COALESCE(mahpp.hpp_per_unit, p.cost, 0) * (oi.quantity - COALESCE(oi.refund_quantity, 0)) ELSE 0 END), 0) as total_moving_average_hpp
|
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN COALESCE(mahpp.hpp_per_unit, p.cost, 0) * ` + billableQtyNet + ` ELSE 0 END), 0) as total_moving_average_hpp
|
||||||
`).
|
`).
|
||||||
Scan(summary).Error
|
Scan(summary).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -728,9 +742,9 @@ func (r *AnalyticsRepositoryImpl) GetParentCategoryAnalyticsDetail(ctx context.C
|
|||||||
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.quantity - COALESCE(oi.refund_quantity, 0) ELSE 0 END), 0) as total_quantity,
|
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.quantity - COALESCE(oi.refund_quantity, 0) ELSE 0 END), 0) as total_quantity,
|
||||||
COUNT(DISTINCT p.id) as product_count,
|
COUNT(DISTINCT p.id) as product_count,
|
||||||
COUNT(DISTINCT oi.order_id) as order_count,
|
COUNT(DISTINCT oi.order_id) as order_count,
|
||||||
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN COALESCE(shpp.hpp_per_unit, p.cost, 0) * (oi.quantity - COALESCE(oi.refund_quantity, 0)) ELSE 0 END), 0) as total_standard_hpp,
|
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN COALESCE(shpp.hpp_per_unit, p.cost, 0) * ` + billableQtyNet + ` ELSE 0 END), 0) as total_standard_hpp,
|
||||||
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_cost * ((oi.quantity - COALESCE(oi.refund_quantity, 0))::float / NULLIF(oi.quantity, 0)) ELSE 0 END), 0) as total_fifo_hpp,
|
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_cost * ((oi.quantity - COALESCE(oi.refund_quantity, 0))::float / NULLIF(oi.quantity, 0)) ELSE 0 END), 0) as total_fifo_hpp,
|
||||||
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN COALESCE(mahpp.hpp_per_unit, p.cost, 0) * (oi.quantity - COALESCE(oi.refund_quantity, 0)) ELSE 0 END), 0) as total_moving_average_hpp
|
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN COALESCE(mahpp.hpp_per_unit, p.cost, 0) * ` + billableQtyNet + ` ELSE 0 END), 0) as total_moving_average_hpp
|
||||||
`).
|
`).
|
||||||
Group("c.id, c.name, c.order").
|
Group("c.id, c.name, c.order").
|
||||||
Order("c.order ASC, c.name ASC").
|
Order("c.order ASC, c.name ASC").
|
||||||
@@ -760,18 +774,18 @@ func (r *AnalyticsRepositoryImpl) GetParentCategoryAnalyticsDetail(ctx context.C
|
|||||||
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_price - COALESCE(oi.refund_amount, 0) ELSE 0 END), 0) as revenue,
|
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_price - COALESCE(oi.refund_amount, 0) ELSE 0 END), 0) as revenue,
|
||||||
COALESCE(
|
COALESCE(
|
||||||
SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_price - COALESCE(oi.refund_amount, 0) ELSE 0 END)
|
SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_price - COALESCE(oi.refund_amount, 0) ELSE 0 END)
|
||||||
/ NULLIF(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.quantity - COALESCE(oi.refund_quantity, 0) ELSE 0 END), 0),
|
/ NULLIF(SUM(CASE WHEN oi.is_fully_refunded = false THEN ` + billableQtyNet + ` ELSE 0 END), 0),
|
||||||
0) as average_price,
|
0) as average_price,
|
||||||
COUNT(DISTINCT oi.order_id) as order_count,
|
COUNT(DISTINCT oi.order_id) as order_count,
|
||||||
COALESCE(shpp.hpp_per_unit, p.cost, 0) as standard_hpp_per_unit,
|
COALESCE(shpp.hpp_per_unit, p.cost, 0) as standard_hpp_per_unit,
|
||||||
COALESCE(shpp.hpp_per_unit, p.cost, 0) * COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.quantity - COALESCE(oi.refund_quantity, 0) ELSE 0 END), 0) as standard_hpp_total,
|
COALESCE(shpp.hpp_per_unit, p.cost, 0) * COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN ` + billableQtyNet + ` ELSE 0 END), 0) as standard_hpp_total,
|
||||||
COALESCE(
|
COALESCE(
|
||||||
SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_cost * ((oi.quantity - COALESCE(oi.refund_quantity, 0))::float / NULLIF(oi.quantity, 0)) ELSE 0 END)
|
SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_cost * ((oi.quantity - COALESCE(oi.refund_quantity, 0))::float / NULLIF(oi.quantity, 0)) ELSE 0 END)
|
||||||
/ NULLIF(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.quantity - COALESCE(oi.refund_quantity, 0) ELSE 0 END), 0),
|
/ NULLIF(SUM(CASE WHEN oi.is_fully_refunded = false THEN ` + billableQtyNet + ` ELSE 0 END), 0),
|
||||||
0) as fifo_hpp_per_unit,
|
0) as fifo_hpp_per_unit,
|
||||||
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_cost * ((oi.quantity - COALESCE(oi.refund_quantity, 0))::float / NULLIF(oi.quantity, 0)) ELSE 0 END), 0) as fifo_hpp_total,
|
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_cost * ((oi.quantity - COALESCE(oi.refund_quantity, 0))::float / NULLIF(oi.quantity, 0)) ELSE 0 END), 0) as fifo_hpp_total,
|
||||||
COALESCE(mahpp.hpp_per_unit, p.cost, 0) as moving_average_hpp_per_unit,
|
COALESCE(mahpp.hpp_per_unit, p.cost, 0) as moving_average_hpp_per_unit,
|
||||||
COALESCE(mahpp.hpp_per_unit, p.cost, 0) * COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.quantity - COALESCE(oi.refund_quantity, 0) ELSE 0 END), 0) as moving_average_hpp_total
|
COALESCE(mahpp.hpp_per_unit, p.cost, 0) * COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN ` + billableQtyNet + ` ELSE 0 END), 0) as moving_average_hpp_total
|
||||||
`).
|
`).
|
||||||
Group("p.id, p.name, p.sku, p.price, p.cost, pop.price, c.id, c.name, c.order, shpp.hpp_per_unit, mahpp.hpp_per_unit").
|
Group("p.id, p.name, p.sku, p.price, p.cost, pop.price, c.id, c.name, c.order, shpp.hpp_per_unit, mahpp.hpp_per_unit").
|
||||||
Order("revenue DESC").
|
Order("revenue DESC").
|
||||||
|
|||||||
Reference in New Issue
Block a user