Add grouping by outlet_id
This commit is contained in:
parent
9b0fc9a63b
commit
fbc65c5606
@ -90,7 +90,7 @@ type PurchasingAnalyticsRequest struct {
|
||||
OutletID *string `form:"outlet_id,omitempty"`
|
||||
DateFrom string `form:"date_from" validate:"required"`
|
||||
DateTo string `form:"date_to" validate:"required"`
|
||||
GroupBy string `form:"group_by,default=day" validate:"omitempty,oneof=day hour week month"`
|
||||
GroupBy string `form:"group_by,default=day" validate:"omitempty,oneof=day hour week month outlet_id"`
|
||||
}
|
||||
|
||||
type PurchasingAnalyticsResponse struct {
|
||||
@ -102,6 +102,7 @@ type PurchasingAnalyticsResponse struct {
|
||||
GroupBy string `json:"group_by"`
|
||||
Summary PurchasingSummary `json:"summary"`
|
||||
Data []PurchasingAnalyticsData `json:"data"`
|
||||
OutletData []PurchasingOutletData `json:"outlet_data,omitempty"`
|
||||
IngredientData []PurchasingIngredientData `json:"ingredient_data"`
|
||||
VendorData []PurchasingVendorData `json:"vendor_data"`
|
||||
}
|
||||
@ -132,6 +133,20 @@ type PurchasingAnalyticsData struct {
|
||||
Vendors int64 `json:"vendors"`
|
||||
}
|
||||
|
||||
type PurchasingOutletData struct {
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
OutletName string `json:"outlet_name"`
|
||||
Purchases float64 `json:"purchases"`
|
||||
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||
ExpensePurchases float64 `json:"expense_purchases"`
|
||||
PurchaseOrders int64 `json:"purchase_orders"`
|
||||
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
||||
ExpenseCount int64 `json:"expense_count"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
Ingredients int64 `json:"ingredients"`
|
||||
Vendors int64 `json:"vendors"`
|
||||
}
|
||||
|
||||
type PurchasingIngredientData struct {
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
IngredientName string `json:"ingredient_name"`
|
||||
|
||||
@ -32,6 +32,7 @@ type PurchasingAnalytics struct {
|
||||
OutletName *string `json:"outlet_name,omitempty"`
|
||||
Summary PurchasingSummary `json:"summary"`
|
||||
Data []PurchasingAnalyticsData `json:"data"`
|
||||
OutletData []PurchasingOutletData `json:"outlet_data,omitempty"`
|
||||
IngredientData []PurchasingIngredientData `json:"ingredient_data"`
|
||||
VendorData []PurchasingVendorData `json:"vendor_data"`
|
||||
}
|
||||
@ -62,6 +63,20 @@ type PurchasingAnalyticsData struct {
|
||||
Vendors int64 `json:"vendors"`
|
||||
}
|
||||
|
||||
type PurchasingOutletData struct {
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
OutletName string `json:"outlet_name"`
|
||||
Purchases float64 `json:"purchases"`
|
||||
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||
ExpensePurchases float64 `json:"expense_purchases"`
|
||||
PurchaseOrders int64 `json:"purchase_orders"`
|
||||
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
||||
ExpenseCount int64 `json:"expense_count"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
Ingredients int64 `json:"ingredients"`
|
||||
Vendors int64 `json:"vendors"`
|
||||
}
|
||||
|
||||
type PurchasingIngredientData struct {
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
IngredientName string `json:"ingredient_name"`
|
||||
|
||||
@ -95,7 +95,7 @@ type PurchasingAnalyticsRequest struct {
|
||||
OutletID *uuid.UUID `validate:"omitempty"`
|
||||
DateFrom time.Time `validate:"required"`
|
||||
DateTo time.Time `validate:"required"`
|
||||
GroupBy string `validate:"omitempty,oneof=day hour week month"`
|
||||
GroupBy string `validate:"omitempty,oneof=day hour week month outlet_id"`
|
||||
}
|
||||
|
||||
// PurchasingAnalyticsResponse represents the response for purchasing analytics
|
||||
@ -108,6 +108,7 @@ type PurchasingAnalyticsResponse struct {
|
||||
GroupBy string `json:"group_by"`
|
||||
Summary PurchasingSummary `json:"summary"`
|
||||
Data []PurchasingAnalyticsData `json:"data"`
|
||||
OutletData []PurchasingOutletData `json:"outlet_data,omitempty"`
|
||||
IngredientData []PurchasingIngredientData `json:"ingredient_data"`
|
||||
VendorData []PurchasingVendorData `json:"vendor_data"`
|
||||
}
|
||||
@ -140,6 +141,20 @@ type PurchasingAnalyticsData struct {
|
||||
Vendors int64 `json:"vendors"`
|
||||
}
|
||||
|
||||
type PurchasingOutletData struct {
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
OutletName string `json:"outlet_name"`
|
||||
Purchases float64 `json:"purchases"`
|
||||
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||
ExpensePurchases float64 `json:"expense_purchases"`
|
||||
PurchaseOrders int64 `json:"purchase_orders"`
|
||||
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
||||
ExpenseCount int64 `json:"expense_count"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
Ingredients int64 `json:"ingredients"`
|
||||
Vendors int64 `json:"vendors"`
|
||||
}
|
||||
|
||||
// PurchasingIngredientData represents purchasing analytics for an ingredient
|
||||
type PurchasingIngredientData struct {
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
|
||||
@ -218,6 +218,23 @@ func (p *AnalyticsProcessorImpl) GetPurchasingAnalytics(ctx context.Context, req
|
||||
}
|
||||
}
|
||||
|
||||
outletData := make([]models.PurchasingOutletData, len(result.OutletData))
|
||||
for i, item := range result.OutletData {
|
||||
outletData[i] = models.PurchasingOutletData{
|
||||
OutletID: item.OutletID,
|
||||
OutletName: item.OutletName,
|
||||
Purchases: item.Purchases,
|
||||
RawMaterialPurchases: item.RawMaterialPurchases,
|
||||
ExpensePurchases: item.ExpensePurchases,
|
||||
PurchaseOrders: item.PurchaseOrders,
|
||||
RawMaterialPurchaseOrders: item.RawMaterialPurchaseOrders,
|
||||
ExpenseCount: item.ExpenseCount,
|
||||
Quantity: item.Quantity,
|
||||
Ingredients: item.Ingredients,
|
||||
Vendors: item.Vendors,
|
||||
}
|
||||
}
|
||||
|
||||
ingredientData := make([]models.PurchasingIngredientData, len(result.IngredientData))
|
||||
for i, item := range result.IngredientData {
|
||||
ingredientData[i] = models.PurchasingIngredientData{
|
||||
@ -262,6 +279,7 @@ func (p *AnalyticsProcessorImpl) GetPurchasingAnalytics(ctx context.Context, req
|
||||
TotalVendors: result.Summary.TotalVendors,
|
||||
},
|
||||
Data: data,
|
||||
OutletData: outletData,
|
||||
IngredientData: ingredientData,
|
||||
VendorData: vendorData,
|
||||
}, nil
|
||||
|
||||
@ -145,6 +145,52 @@ func TestAnalyticsProcessorGetPurchasingAnalyticsPassesOutletName(t *testing.T)
|
||||
require.Equal(t, float64(175), result.Data[0].ExpensePurchases)
|
||||
}
|
||||
|
||||
func TestAnalyticsProcessorGetPurchasingAnalyticsMapsOutletData(t *testing.T) {
|
||||
outletID := uuid.New()
|
||||
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
||||
processor := NewAnalyticsProcessorImpl(&analyticsRepositoryStub{
|
||||
purchasingResult: &entities.PurchasingAnalytics{
|
||||
Summary: entities.PurchasingSummary{
|
||||
TotalPurchases: 500,
|
||||
},
|
||||
OutletData: []entities.PurchasingOutletData{
|
||||
{
|
||||
OutletID: &outletID,
|
||||
OutletName: "Outlet A",
|
||||
Purchases: 500,
|
||||
RawMaterialPurchases: 350,
|
||||
ExpensePurchases: 150,
|
||||
PurchaseOrders: 4,
|
||||
RawMaterialPurchaseOrders: 3,
|
||||
ExpenseCount: 2,
|
||||
Quantity: 10,
|
||||
Ingredients: 5,
|
||||
Vendors: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, expenseRepositoryStub{})
|
||||
|
||||
result, err := processor.GetPurchasingAnalytics(context.Background(), &models.PurchasingAnalyticsRequest{
|
||||
OrganizationID: uuid.New(),
|
||||
DateFrom: now,
|
||||
DateTo: now,
|
||||
GroupBy: "outlet_id",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
require.Equal(t, "outlet_id", result.GroupBy)
|
||||
require.Empty(t, result.Data)
|
||||
require.Len(t, result.OutletData, 1)
|
||||
require.Equal(t, &outletID, result.OutletData[0].OutletID)
|
||||
require.Equal(t, "Outlet A", result.OutletData[0].OutletName)
|
||||
require.Equal(t, float64(500), result.OutletData[0].Purchases)
|
||||
require.Equal(t, float64(350), result.OutletData[0].RawMaterialPurchases)
|
||||
require.Equal(t, float64(150), result.OutletData[0].ExpensePurchases)
|
||||
require.Equal(t, int64(4), result.OutletData[0].PurchaseOrders)
|
||||
}
|
||||
|
||||
func TestAnalyticsProcessorGetProfitLossAnalyticsMapsOverviewAndReportFields(t *testing.T) {
|
||||
productID := uuid.New()
|
||||
categoryID := uuid.New()
|
||||
|
||||
@ -224,6 +224,39 @@ func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx contex
|
||||
}
|
||||
|
||||
var data []entities.PurchasingAnalyticsData
|
||||
var outletData []entities.PurchasingOutletData
|
||||
if groupBy == "outlet_id" {
|
||||
outletQuery := r.db.WithContext(ctx).
|
||||
Table("purchase_orders po").
|
||||
Select(`
|
||||
po.outlet_id as outlet_id,
|
||||
COALESCE(o.name, 'No Outlet') as outlet_name,
|
||||
COALESCE(SUM(`+purchaseOrderItemTotalAmountSQL()+`), 0) as purchases,
|
||||
COALESCE(SUM(`+purchaseOrderRawMaterialAmountSQL()+`), 0) as raw_material_purchases,
|
||||
COALESCE(SUM(`+purchaseOrderExpenseAmountSQL()+`), 0) as expense_purchases,
|
||||
COUNT(DISTINCT po.id) as purchase_orders,
|
||||
COUNT(DISTINCT CASE WHEN pc.type = '`+string(entities.PurchaseCategoryTypeRawMaterial)+`' THEN po.id END) as raw_material_purchase_orders,
|
||||
COUNT(CASE WHEN pc.type = '`+string(entities.PurchaseCategoryTypeExpense)+`' THEN poi.id END) as expense_count,
|
||||
COALESCE(SUM(poi.quantity), 0) as quantity,
|
||||
COUNT(DISTINCT i.id) as ingredients,
|
||||
COUNT(DISTINCT COALESCE(po.vendor_id::text, 'no-vendor')) as vendors
|
||||
`).
|
||||
Joins("LEFT JOIN outlets o ON po.outlet_id = o.id").
|
||||
Joins("LEFT JOIN purchase_order_items poi ON poi.purchase_order_id = po.id").
|
||||
Joins("LEFT JOIN purchase_categories pc ON poi.purchase_category_id = pc.id").
|
||||
Joins("LEFT JOIN ingredients i ON poi.ingredient_id = i.id").
|
||||
Joins("LEFT JOIN units u ON poi.unit_id = u.id").
|
||||
Where("po.organization_id = ?", organizationID).
|
||||
Where("po.status != ?", "cancelled").
|
||||
Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo).
|
||||
Group("po.outlet_id, COALESCE(o.name, 'No Outlet')").
|
||||
Order("outlet_name ASC")
|
||||
outletQuery = r.applyPurchaseOrderItemOutletFilter(outletQuery, outletID)
|
||||
|
||||
if err := outletQuery.Scan(&outletData).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
dataQuery := r.db.WithContext(ctx).
|
||||
Table("purchase_orders po").
|
||||
Select(`
|
||||
@ -253,6 +286,7 @@ func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx contex
|
||||
if err := dataQuery.Scan(&data).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var ingredientData []entities.PurchasingIngredientData
|
||||
ingredientQuery := r.db.WithContext(ctx).
|
||||
@ -315,6 +349,7 @@ func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx contex
|
||||
OutletName: outletName,
|
||||
Summary: summary,
|
||||
Data: data,
|
||||
OutletData: outletData,
|
||||
IngredientData: ingredientData,
|
||||
VendorData: vendorData,
|
||||
}, nil
|
||||
|
||||
@ -200,6 +200,7 @@ func (s *AnalyticsServiceImpl) validatePurchasingAnalyticsRequest(req *models.Pu
|
||||
"hour": true,
|
||||
"week": true,
|
||||
"month": true,
|
||||
"outlet_id": true,
|
||||
}
|
||||
if !validGroupBy[req.GroupBy] {
|
||||
return fmt.Errorf("invalid group_by value: %s", req.GroupBy)
|
||||
|
||||
@ -132,6 +132,21 @@ func TestAnalyticsServiceGetPurchasingAnalyticsAllowsEmptyGroupBy(t *testing.T)
|
||||
require.NotNil(t, resp)
|
||||
}
|
||||
|
||||
func TestAnalyticsServiceGetPurchasingAnalyticsAllowsOutletGroupBy(t *testing.T) {
|
||||
service := NewAnalyticsServiceImpl(analyticsProcessorStub{})
|
||||
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
resp, err := service.GetPurchasingAnalytics(context.Background(), &models.PurchasingAnalyticsRequest{
|
||||
OrganizationID: uuid.New(),
|
||||
DateFrom: now,
|
||||
DateTo: now,
|
||||
GroupBy: "outlet_id",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
}
|
||||
|
||||
func TestAnalyticsServiceGetProfitLossAnalyticsValidation(t *testing.T) {
|
||||
service := NewAnalyticsServiceImpl(analyticsProcessorStub{})
|
||||
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
@ -184,6 +184,23 @@ func PurchasingAnalyticsModelToContract(resp *models.PurchasingAnalyticsResponse
|
||||
}
|
||||
}
|
||||
|
||||
outletData := make([]contract.PurchasingOutletData, len(resp.OutletData))
|
||||
for i, item := range resp.OutletData {
|
||||
outletData[i] = contract.PurchasingOutletData{
|
||||
OutletID: item.OutletID,
|
||||
OutletName: item.OutletName,
|
||||
Purchases: item.Purchases,
|
||||
RawMaterialPurchases: item.RawMaterialPurchases,
|
||||
ExpensePurchases: item.ExpensePurchases,
|
||||
PurchaseOrders: item.PurchaseOrders,
|
||||
RawMaterialPurchaseOrders: item.RawMaterialPurchaseOrders,
|
||||
ExpenseCount: item.ExpenseCount,
|
||||
Quantity: item.Quantity,
|
||||
Ingredients: item.Ingredients,
|
||||
Vendors: item.Vendors,
|
||||
}
|
||||
}
|
||||
|
||||
ingredientData := make([]contract.PurchasingIngredientData, len(resp.IngredientData))
|
||||
for i, item := range resp.IngredientData {
|
||||
ingredientData[i] = contract.PurchasingIngredientData{
|
||||
@ -228,6 +245,7 @@ func PurchasingAnalyticsModelToContract(resp *models.PurchasingAnalyticsResponse
|
||||
TotalVendors: resp.Summary.TotalVendors,
|
||||
},
|
||||
Data: data,
|
||||
OutletData: outletData,
|
||||
IngredientData: ingredientData,
|
||||
VendorData: vendorData,
|
||||
}
|
||||
|
||||
@ -105,6 +105,40 @@ func TestPurchasingAnalyticsModelToContractOmitsNilOutletName(t *testing.T) {
|
||||
require.NotContains(t, string(payload), "outlet_name")
|
||||
}
|
||||
|
||||
func TestPurchasingAnalyticsModelToContractCopiesOutletData(t *testing.T) {
|
||||
outletID := uuid.New()
|
||||
|
||||
result := PurchasingAnalyticsModelToContract(&models.PurchasingAnalyticsResponse{
|
||||
OrganizationID: uuid.New(),
|
||||
GroupBy: "outlet_id",
|
||||
OutletData: []models.PurchasingOutletData{
|
||||
{
|
||||
OutletID: &outletID,
|
||||
OutletName: "Outlet A",
|
||||
Purchases: 500,
|
||||
RawMaterialPurchases: 350,
|
||||
ExpensePurchases: 150,
|
||||
PurchaseOrders: 4,
|
||||
RawMaterialPurchaseOrders: 3,
|
||||
ExpenseCount: 2,
|
||||
Quantity: 10,
|
||||
Ingredients: 5,
|
||||
Vendors: 2,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
require.NotNil(t, result)
|
||||
require.Equal(t, "outlet_id", result.GroupBy)
|
||||
require.Len(t, result.OutletData, 1)
|
||||
require.Equal(t, &outletID, result.OutletData[0].OutletID)
|
||||
require.Equal(t, "Outlet A", result.OutletData[0].OutletName)
|
||||
require.Equal(t, float64(500), result.OutletData[0].Purchases)
|
||||
require.Equal(t, float64(350), result.OutletData[0].RawMaterialPurchases)
|
||||
require.Equal(t, float64(150), result.OutletData[0].ExpensePurchases)
|
||||
require.Equal(t, int64(4), result.OutletData[0].PurchaseOrders)
|
||||
}
|
||||
|
||||
func TestProfitLossAnalyticsContractToModelParsesDateRange(t *testing.T) {
|
||||
orgID := uuid.New()
|
||||
outletID := uuid.New().String()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user