feat: add limit owner

This commit is contained in:
efrilm
2026-10-02 23:30:03 +07:00
parent b372821b23
commit fbe7e97dc7
18 changed files with 533 additions and 172 deletions
+58 -7
View File
@@ -600,6 +600,7 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalyticsPerParentCategory(ctx conte
Select(`
pc.id as parent_category_id,
pc.name as parent_category_name,
COALESCE(pc.owner_fee_percent, ?) as owner_fee_percent,
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_price - COALESCE(oi.refund_amount, 0) ELSE 0 END), 0) as total_revenue,
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 c.id) as category_count,
@@ -608,7 +609,7 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalyticsPerParentCategory(ctx conte
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 COALESCE(mahpp.hpp_per_unit, p.cost, 0) * `+billableQtyNet+` ELSE 0 END), 0) as total_moving_average_hpp
`).
`, constants.BudgetLimitOwnerPercent).
Joins("JOIN products p ON oi.product_id = p.id").
Joins("JOIN categories c ON p.category_id = c.id").
// Categories without a parent roll up to themselves, so top-level categories still appear
@@ -640,11 +641,56 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalyticsPerParentCategory(ctx conte
query = r.resolveOutletID(query, outletID, "o.outlet_id")
err := query.
Group("pc.id, pc.name").
Group("pc.id, pc.name, pc.owner_fee_percent").
Order("pc.name ASC").
Scan(&results).Error
if err != nil || len(results) == 0 {
return results, err
}
return results, err
// Best-selling product of each parent category. DISTINCT ON keeps the first row per
// parent category, which the ordering makes the one with the highest revenue.
var topProducts []*entities.ParentCategoryTopProduct
topQuery := r.db.WithContext(ctx).
Table("order_items oi").
Select(`
DISTINCT ON (pc.id)
pc.id as parent_category_id,
p.id as product_id,
p.name as product_name,
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.quantity - COALESCE(oi.refund_quantity, 0) ELSE 0 END), 0) as quantity_sold,
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_price - COALESCE(oi.refund_amount, 0) ELSE 0 END), 0) as revenue
`).
Joins("JOIN products p ON oi.product_id = p.id").
Joins("JOIN categories c ON p.category_id = c.id").
Joins("JOIN categories pc ON pc.id = COALESCE(c.parent_id, c.id)").
Joins("JOIN orders o ON oi.order_id = o.id").
Where("o.organization_id = ?", organizationID).
Where("o.is_void = ?", false).
Where("o.is_refund = ?", false).
Where("o.payment_status = ?", entities.PaymentStatusCompleted).
Where("oi.status != ?", entities.OrderItemStatusCancelled).
Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo)
topQuery = r.resolveOutletID(topQuery, outletID, "o.outlet_id")
err = topQuery.
Group("pc.id, p.id, p.name").
Order("pc.id, COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_price - COALESCE(oi.refund_amount, 0) ELSE 0 END), 0) DESC, p.name ASC").
Scan(&topProducts).Error
if err != nil {
return nil, err
}
topByParent := make(map[uuid.UUID]*entities.ParentCategoryTopProduct, len(topProducts))
for _, product := range topProducts {
topByParent[product.ParentCategoryID] = product
}
for _, result := range results {
result.TopProduct = topByParent[result.ParentCategoryID]
}
return results, nil
}
// movingAverageHppSubquery builds the per-product moving-average HPP lookup shared by
@@ -691,12 +737,13 @@ func (r *AnalyticsRepositoryImpl) GetParentCategoryAnalyticsDetail(ctx context.C
// Resolve the category first so the endpoint still identifies the category when it
// has no sales in the requested range, and rejects ids from another organization.
var parent struct {
ID uuid.UUID
Name string
ID uuid.UUID
Name string
OwnerFeePercent float64
}
if err := r.db.WithContext(ctx).
Table("categories").
Select("id, name").
Select("id, name, COALESCE(owner_fee_percent, ?) as owner_fee_percent", constants.BudgetLimitOwnerPercent).
Where("id = ? AND organization_id = ?", parentCategoryID, organizationID).
Scan(&parent).Error; err != nil {
return nil, err
@@ -732,6 +779,7 @@ func (r *AnalyticsRepositoryImpl) GetParentCategoryAnalyticsDetail(ctx context.C
}
summary.ParentCategoryID = parent.ID
summary.ParentCategoryName = parent.Name
summary.OwnerFeePercent = parent.OwnerFeePercent
detail.Summary = summary
// Sub-category rows.
@@ -810,12 +858,15 @@ func (r *AnalyticsRepositoryImpl) GetBudgetCutOffWeekly(ctx context.Context, org
Select(`
DATE_TRUNC('week', o.created_at) as week_start,
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) * COALESCE(pc.owner_fee_percent, ?) / 100.0), 0) as sdl,
COUNT(DISTINCT oi.order_id) as order_count
`).
`, constants.BudgetLimitOwnerPercent).
// products and categories are joined to keep the scope identical to the report
// the block is attached to, even when no parent category filter is applied
Joins("JOIN products p ON oi.product_id = p.id").
Joins("JOIN categories c ON p.category_id = c.id").
// The owner fee lives on the parent category; a category without a parent is its own group
Joins("JOIN categories pc ON pc.id = COALESCE(c.parent_id, c.id)").
Joins("JOIN orders o ON oi.order_id = o.id").
Where("o.organization_id = ?", organizationID).
Where("o.is_void = ?", false).