Add grouping by outlet_id #22

Open
ryan wants to merge 1 commits from feature/outlet-grouping into main
10 changed files with 263 additions and 51 deletions

View File

@ -90,7 +90,7 @@ type PurchasingAnalyticsRequest struct {
OutletID *string `form:"outlet_id,omitempty"` OutletID *string `form:"outlet_id,omitempty"`
DateFrom string `form:"date_from" validate:"required"` DateFrom string `form:"date_from" validate:"required"`
DateTo string `form:"date_to" 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 { type PurchasingAnalyticsResponse struct {
@ -102,6 +102,7 @@ type PurchasingAnalyticsResponse struct {
GroupBy string `json:"group_by"` GroupBy string `json:"group_by"`
Summary PurchasingSummary `json:"summary"` Summary PurchasingSummary `json:"summary"`
Data []PurchasingAnalyticsData `json:"data"` Data []PurchasingAnalyticsData `json:"data"`
OutletData []PurchasingOutletData `json:"outlet_data,omitempty"`
IngredientData []PurchasingIngredientData `json:"ingredient_data"` IngredientData []PurchasingIngredientData `json:"ingredient_data"`
VendorData []PurchasingVendorData `json:"vendor_data"` VendorData []PurchasingVendorData `json:"vendor_data"`
} }
@ -132,6 +133,20 @@ type PurchasingAnalyticsData struct {
Vendors int64 `json:"vendors"` 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 { type PurchasingIngredientData struct {
IngredientID uuid.UUID `json:"ingredient_id"` IngredientID uuid.UUID `json:"ingredient_id"`
IngredientName string `json:"ingredient_name"` IngredientName string `json:"ingredient_name"`

View File

@ -32,6 +32,7 @@ type PurchasingAnalytics struct {
OutletName *string `json:"outlet_name,omitempty"` OutletName *string `json:"outlet_name,omitempty"`
Summary PurchasingSummary `json:"summary"` Summary PurchasingSummary `json:"summary"`
Data []PurchasingAnalyticsData `json:"data"` Data []PurchasingAnalyticsData `json:"data"`
OutletData []PurchasingOutletData `json:"outlet_data,omitempty"`
IngredientData []PurchasingIngredientData `json:"ingredient_data"` IngredientData []PurchasingIngredientData `json:"ingredient_data"`
VendorData []PurchasingVendorData `json:"vendor_data"` VendorData []PurchasingVendorData `json:"vendor_data"`
} }
@ -62,6 +63,20 @@ type PurchasingAnalyticsData struct {
Vendors int64 `json:"vendors"` 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 { type PurchasingIngredientData struct {
IngredientID uuid.UUID `json:"ingredient_id"` IngredientID uuid.UUID `json:"ingredient_id"`
IngredientName string `json:"ingredient_name"` IngredientName string `json:"ingredient_name"`
@ -114,15 +129,15 @@ type ProductAnalyticsPerCategory struct {
// DashboardOverview represents dashboard overview data // DashboardOverview represents dashboard overview data
type DashboardOverview struct { type DashboardOverview struct {
TotalSales float64 `json:"total_sales"` TotalSales float64 `json:"total_sales"`
TotalOrders int64 `json:"total_orders"` TotalOrders int64 `json:"total_orders"`
AverageOrderValue float64 `json:"average_order_value"` AverageOrderValue float64 `json:"average_order_value"`
TotalCustomers int64 `json:"total_customers"` TotalCustomers int64 `json:"total_customers"`
VoidedOrders int64 `json:"voided_orders"` VoidedOrders int64 `json:"voided_orders"`
RefundedOrders int64 `json:"refunded_orders"` RefundedOrders int64 `json:"refunded_orders"`
TotalItemSold int64 `json:"total_item_sold"` TotalItemSold int64 `json:"total_item_sold"`
TotalLowStock int64 `json:"total_low_stock"` TotalLowStock int64 `json:"total_low_stock"`
TotalProductActive int64 `json:"total_product_active"` TotalProductActive int64 `json:"total_product_active"`
} }
type ProfitLossAnalytics struct { type ProfitLossAnalytics struct {

View File

@ -95,7 +95,7 @@ type PurchasingAnalyticsRequest struct {
OutletID *uuid.UUID `validate:"omitempty"` OutletID *uuid.UUID `validate:"omitempty"`
DateFrom time.Time `validate:"required"` DateFrom time.Time `validate:"required"`
DateTo 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 // PurchasingAnalyticsResponse represents the response for purchasing analytics
@ -108,6 +108,7 @@ type PurchasingAnalyticsResponse struct {
GroupBy string `json:"group_by"` GroupBy string `json:"group_by"`
Summary PurchasingSummary `json:"summary"` Summary PurchasingSummary `json:"summary"`
Data []PurchasingAnalyticsData `json:"data"` Data []PurchasingAnalyticsData `json:"data"`
OutletData []PurchasingOutletData `json:"outlet_data,omitempty"`
IngredientData []PurchasingIngredientData `json:"ingredient_data"` IngredientData []PurchasingIngredientData `json:"ingredient_data"`
VendorData []PurchasingVendorData `json:"vendor_data"` VendorData []PurchasingVendorData `json:"vendor_data"`
} }
@ -140,6 +141,20 @@ type PurchasingAnalyticsData struct {
Vendors int64 `json:"vendors"` 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 // PurchasingIngredientData represents purchasing analytics for an ingredient
type PurchasingIngredientData struct { type PurchasingIngredientData struct {
IngredientID uuid.UUID `json:"ingredient_id"` IngredientID uuid.UUID `json:"ingredient_id"`
@ -288,12 +303,12 @@ type ProfitLossAnalyticsResponse struct {
} }
type ProfitLossPurchasing struct { type ProfitLossPurchasing struct {
TodayTotal float64 `json:"today_total"` TodayTotal float64 `json:"today_total"`
MtdTotal float64 `json:"mtd_total"` MtdTotal float64 `json:"mtd_total"`
TodayRawMaterial float64 `json:"today_raw_material"` TodayRawMaterial float64 `json:"today_raw_material"`
MtdRawMaterial float64 `json:"mtd_raw_material"` MtdRawMaterial float64 `json:"mtd_raw_material"`
TodayExpense float64 `json:"today_expense"` TodayExpense float64 `json:"today_expense"`
MtdExpense float64 `json:"mtd_expense"` MtdExpense float64 `json:"mtd_expense"`
Items []ProfitLossPurchasingItem `json:"items"` Items []ProfitLossPurchasingItem `json:"items"`
} }

View File

@ -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)) ingredientData := make([]models.PurchasingIngredientData, len(result.IngredientData))
for i, item := range result.IngredientData { for i, item := range result.IngredientData {
ingredientData[i] = models.PurchasingIngredientData{ ingredientData[i] = models.PurchasingIngredientData{
@ -262,6 +279,7 @@ func (p *AnalyticsProcessorImpl) GetPurchasingAnalytics(ctx context.Context, req
TotalVendors: result.Summary.TotalVendors, TotalVendors: result.Summary.TotalVendors,
}, },
Data: data, Data: data,
OutletData: outletData,
IngredientData: ingredientData, IngredientData: ingredientData,
VendorData: vendorData, VendorData: vendorData,
}, nil }, nil

View File

@ -145,6 +145,52 @@ func TestAnalyticsProcessorGetPurchasingAnalyticsPassesOutletName(t *testing.T)
require.Equal(t, float64(175), result.Data[0].ExpensePurchases) 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) { func TestAnalyticsProcessorGetProfitLossAnalyticsMapsOverviewAndReportFields(t *testing.T) {
productID := uuid.New() productID := uuid.New()
categoryID := uuid.New() categoryID := uuid.New()

View File

@ -224,34 +224,68 @@ func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx contex
} }
var data []entities.PurchasingAnalyticsData var data []entities.PurchasingAnalyticsData
dataQuery := r.db.WithContext(ctx). var outletData []entities.PurchasingOutletData
Table("purchase_orders po"). if groupBy == "outlet_id" {
Select(` outletQuery := r.db.WithContext(ctx).
`+dateFormat+` as date, Table("purchase_orders po").
COALESCE(SUM(`+purchaseOrderItemTotalAmountSQL()+`), 0) as purchases, Select(`
COALESCE(SUM(`+purchaseOrderRawMaterialAmountSQL()+`), 0) as raw_material_purchases, po.outlet_id as outlet_id,
COALESCE(SUM(`+purchaseOrderExpenseAmountSQL()+`), 0) as expense_purchases, COALESCE(o.name, 'No Outlet') as outlet_name,
COUNT(DISTINCT po.id) as purchase_orders, COALESCE(SUM(`+purchaseOrderItemTotalAmountSQL()+`), 0) as purchases,
COUNT(DISTINCT CASE WHEN pc.type = '`+string(entities.PurchaseCategoryTypeRawMaterial)+`' THEN po.id END) as raw_material_purchase_orders, COALESCE(SUM(`+purchaseOrderRawMaterialAmountSQL()+`), 0) as raw_material_purchases,
COUNT(CASE WHEN pc.type = '`+string(entities.PurchaseCategoryTypeExpense)+`' THEN poi.id END) as expense_count, COALESCE(SUM(`+purchaseOrderExpenseAmountSQL()+`), 0) as expense_purchases,
COALESCE(SUM(poi.quantity), 0) as quantity, COUNT(DISTINCT po.id) as purchase_orders,
COUNT(DISTINCT i.id) as ingredients, COUNT(DISTINCT CASE WHEN pc.type = '`+string(entities.PurchaseCategoryTypeRawMaterial)+`' THEN po.id END) as raw_material_purchase_orders,
COUNT(DISTINCT COALESCE(po.vendor_id::text, 'no-vendor')) as vendors COUNT(CASE WHEN pc.type = '`+string(entities.PurchaseCategoryTypeExpense)+`' THEN poi.id END) as expense_count,
`). COALESCE(SUM(poi.quantity), 0) as quantity,
Joins("LEFT JOIN purchase_order_items poi ON poi.purchase_order_id = po.id"). COUNT(DISTINCT i.id) as ingredients,
Joins("LEFT JOIN purchase_categories pc ON poi.purchase_category_id = pc.id"). COUNT(DISTINCT COALESCE(po.vendor_id::text, 'no-vendor')) as vendors
Joins("LEFT JOIN ingredients i ON poi.ingredient_id = i.id"). `).
Joins("LEFT JOIN units u ON poi.unit_id = u.id"). Joins("LEFT JOIN outlets o ON po.outlet_id = o.id").
Where("po.organization_id = ?", organizationID). Joins("LEFT JOIN purchase_order_items poi ON poi.purchase_order_id = po.id").
Where("po.status != ?", "cancelled"). Joins("LEFT JOIN purchase_categories pc ON poi.purchase_category_id = pc.id").
Where("pc.type = ?", entities.PurchaseCategoryTypeRawMaterial). Joins("LEFT JOIN ingredients i ON poi.ingredient_id = i.id").
Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo). Joins("LEFT JOIN units u ON poi.unit_id = u.id").
Group(dateFormat). Where("po.organization_id = ?", organizationID).
Order(dateFormat) Where("po.status != ?", "cancelled").
dataQuery = r.applyPurchaseOrderItemOutletFilter(dataQuery, outletID) 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 := dataQuery.Scan(&data).Error; err != nil { if err := outletQuery.Scan(&outletData).Error; err != nil {
return nil, err return nil, err
}
} else {
dataQuery := r.db.WithContext(ctx).
Table("purchase_orders po").
Select(`
`+dateFormat+` as date,
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 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("pc.type = ?", entities.PurchaseCategoryTypeRawMaterial).
Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo).
Group(dateFormat).
Order(dateFormat)
dataQuery = r.applyPurchaseOrderItemOutletFilter(dataQuery, outletID)
if err := dataQuery.Scan(&data).Error; err != nil {
return nil, err
}
} }
var ingredientData []entities.PurchasingIngredientData var ingredientData []entities.PurchasingIngredientData
@ -315,6 +349,7 @@ func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx contex
OutletName: outletName, OutletName: outletName,
Summary: summary, Summary: summary,
Data: data, Data: data,
OutletData: outletData,
IngredientData: ingredientData, IngredientData: ingredientData,
VendorData: vendorData, VendorData: vendorData,
}, nil }, nil
@ -806,9 +841,9 @@ func (r *AnalyticsRepositoryImpl) getPurchaseOrderRawMaterialTotal(ctx context.C
} }
type purchasingTotals struct { type purchasingTotals struct {
Total float64 Total float64
RawMaterial float64 RawMaterial float64
Expense float64 Expense float64
} }
func (r *AnalyticsRepositoryImpl) getPurchaseOrderTotals(ctx context.Context, organizationID uuid.UUID, dateFrom, dateTo time.Time) (purchasingTotals, error) { func (r *AnalyticsRepositoryImpl) getPurchaseOrderTotals(ctx context.Context, organizationID uuid.UUID, dateFrom, dateTo time.Time) (purchasingTotals, error) {

View File

@ -196,10 +196,11 @@ func (s *AnalyticsServiceImpl) validatePurchasingAnalyticsRequest(req *models.Pu
if req.GroupBy != "" { if req.GroupBy != "" {
validGroupBy := map[string]bool{ validGroupBy := map[string]bool{
"day": true, "day": true,
"hour": true, "hour": true,
"week": true, "week": true,
"month": true, "month": true,
"outlet_id": true,
} }
if !validGroupBy[req.GroupBy] { if !validGroupBy[req.GroupBy] {
return fmt.Errorf("invalid group_by value: %s", req.GroupBy) return fmt.Errorf("invalid group_by value: %s", req.GroupBy)

View File

@ -132,6 +132,21 @@ func TestAnalyticsServiceGetPurchasingAnalyticsAllowsEmptyGroupBy(t *testing.T)
require.NotNil(t, resp) 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) { func TestAnalyticsServiceGetProfitLossAnalyticsValidation(t *testing.T) {
service := NewAnalyticsServiceImpl(analyticsProcessorStub{}) service := NewAnalyticsServiceImpl(analyticsProcessorStub{})
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC) now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)

View File

@ -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)) ingredientData := make([]contract.PurchasingIngredientData, len(resp.IngredientData))
for i, item := range resp.IngredientData { for i, item := range resp.IngredientData {
ingredientData[i] = contract.PurchasingIngredientData{ ingredientData[i] = contract.PurchasingIngredientData{
@ -228,6 +245,7 @@ func PurchasingAnalyticsModelToContract(resp *models.PurchasingAnalyticsResponse
TotalVendors: resp.Summary.TotalVendors, TotalVendors: resp.Summary.TotalVendors,
}, },
Data: data, Data: data,
OutletData: outletData,
IngredientData: ingredientData, IngredientData: ingredientData,
VendorData: vendorData, VendorData: vendorData,
} }

View File

@ -105,6 +105,40 @@ func TestPurchasingAnalyticsModelToContractOmitsNilOutletName(t *testing.T) {
require.NotContains(t, string(payload), "outlet_name") 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) { func TestProfitLossAnalyticsContractToModelParsesDateRange(t *testing.T) {
orgID := uuid.New() orgID := uuid.New()
outletID := uuid.New().String() outletID := uuid.New().String()