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
+56 -8
View File
@@ -413,9 +413,22 @@ func (p *AnalyticsProcessorImpl) GetProductAnalyticsPerParentCategory(ctx contex
// Transform data
var resultData []models.ProductAnalyticsPerParentCategoryData
for _, data := range analyticsData {
var topProduct *models.ParentCategoryTopProduct
if data.TopProduct != nil {
topProduct = &models.ParentCategoryTopProduct{
ProductID: data.TopProduct.ProductID,
ProductName: data.TopProduct.ProductName,
QuantitySold: data.TopProduct.QuantitySold,
Revenue: data.TopProduct.Revenue,
}
}
resultData = append(resultData, models.ProductAnalyticsPerParentCategoryData{
TopProduct: topProduct,
ParentCategoryID: data.ParentCategoryID,
ParentCategoryName: data.ParentCategoryName,
OwnerFeePercent: data.OwnerFeePercent,
SDL: data.TotalRevenue * data.OwnerFeePercent / 100,
TotalRevenue: data.TotalRevenue,
TotalQuantity: data.TotalQuantity,
CategoryCount: data.CategoryCount,
@@ -500,6 +513,9 @@ func (p *AnalyticsProcessorImpl) GetParentCategoryAnalyticsDetail(ctx context.Co
summary := models.ParentCategoryAnalyticsDetailSummary{}
if detail.Summary != nil {
summary = models.ParentCategoryAnalyticsDetailSummary{
TopProduct: topProductByRevenue(detail.Products),
OwnerFeePercent: detail.Summary.OwnerFeePercent,
SDL: detail.Summary.TotalRevenue * detail.Summary.OwnerFeePercent / 100,
TotalRevenue: detail.Summary.TotalRevenue,
TotalQuantity: detail.Summary.TotalQuantity,
CategoryCount: detail.Summary.CategoryCount,
@@ -515,6 +531,10 @@ func (p *AnalyticsProcessorImpl) GetParentCategoryAnalyticsDetail(ctx context.Co
if err != nil {
return nil, err
}
// The block covers a single parent category, so it reports that category's own fee
if detail.Summary != nil {
budget.Percentages.Owner = detail.Summary.OwnerFeePercent
}
return &models.ParentCategoryAnalyticsDetailResponse{
OrganizationID: req.OrganizationID,
@@ -530,6 +550,30 @@ func (p *AnalyticsProcessorImpl) GetParentCategoryAnalyticsDetail(ctx context.Co
}, nil
}
// topProductByRevenue picks the best-selling product of a parent category. A product
// can span several rows (one per outlet price), so the rows are added up per product
// first. Ties keep the product that was seen first.
func topProductByRevenue(products []*entities.ProductAnalytics) *models.ParentCategoryTopProduct {
totals := make(map[uuid.UUID]*models.ParentCategoryTopProduct)
for _, product := range products {
total, ok := totals[product.ProductID]
if !ok {
total = &models.ParentCategoryTopProduct{ProductID: product.ProductID, ProductName: product.ProductName}
totals[product.ProductID] = total
}
total.QuantitySold += product.QuantitySold
total.Revenue += product.Revenue
}
var top *models.ParentCategoryTopProduct
for _, product := range products {
if total := totals[product.ProductID]; top == nil || total.Revenue > top.Revenue {
top = total
}
}
return top
}
// startOfWeek returns the Monday 00:00 of the week containing t, in t's own location.
func startOfWeek(t time.Time) time.Time {
daysSinceMonday := (int(t.Weekday()) + 6) % 7
@@ -542,15 +586,16 @@ func endOfWeek(t time.Time) time.Time {
return startOfWeek(t).AddDate(0, 0, 7).Add(-time.Nanosecond)
}
// newBudgetPeriod splits a period's revenue into the spending limits.
func newBudgetPeriod(start, end time.Time, revenue float64, orderCount int64) models.BudgetPeriod {
// newBudgetPeriod splits a period's revenue into the spending limits. The owner limit
// is passed in because a parent category can carry its own owner fee percent.
func newBudgetPeriod(start, end time.Time, revenue, sdl float64, orderCount int64) models.BudgetPeriod {
return models.BudgetPeriod{
PeriodStart: start,
PeriodEnd: end,
Revenue: revenue,
OrderCount: orderCount,
LimitPurchase: revenue * constants.BudgetLimitPurchasePercent / 100,
LimitOwner: revenue * constants.BudgetLimitOwnerPercent / 100,
SDL: sdl,
LimitTeam: revenue * constants.BudgetLimitTeamPercent / 100,
}
}
@@ -588,22 +633,24 @@ func (p *AnalyticsProcessorImpl) buildBudgetCutOff(ctx context.Context, organiza
var (
totalRevenue float64
totalSDL float64
totalOrders int64
monthOrder []string
monthAccumulator = map[string]*models.BudgetMonthPeriod{}
)
for week := cutOffFrom; !week.After(cutOffTo); week = week.AddDate(0, 0, 7) {
var revenue float64
var revenue, sdl float64
var orderCount int64
if row, ok := rowsByWeek[week.Format("2006-01-02")]; ok {
revenue, orderCount = row.Revenue, row.OrderCount
revenue, sdl, orderCount = row.Revenue, row.SDL, row.OrderCount
}
period := newBudgetPeriod(week, endOfWeek(week), revenue, orderCount)
period := newBudgetPeriod(week, endOfWeek(week), revenue, sdl, orderCount)
budget.Weekly = append(budget.Weekly, period)
totalRevenue += revenue
totalSDL += sdl
totalOrders += orderCount
// A week belongs to the month of its Monday, so every week is counted once
@@ -618,6 +665,7 @@ func (p *AnalyticsProcessorImpl) buildBudgetCutOff(ctx context.Context, organiza
month.WeekCount++
month.PeriodEnd = period.PeriodEnd
month.Revenue += revenue
month.SDL += sdl
month.OrderCount += orderCount
}
@@ -626,11 +674,11 @@ func (p *AnalyticsProcessorImpl) buildBudgetCutOff(ctx context.Context, organiza
budget.Monthly = append(budget.Monthly, models.BudgetMonthPeriod{
Month: month.Month,
WeekCount: month.WeekCount,
BudgetPeriod: newBudgetPeriod(month.PeriodStart, month.PeriodEnd, month.Revenue, month.OrderCount),
BudgetPeriod: newBudgetPeriod(month.PeriodStart, month.PeriodEnd, month.Revenue, month.SDL, month.OrderCount),
})
}
budget.Total = newBudgetPeriod(cutOffFrom, cutOffTo, totalRevenue, totalOrders)
budget.Total = newBudgetPeriod(cutOffFrom, cutOffTo, totalRevenue, totalSDL, totalOrders)
return budget, nil
}