Compare commits

..
11 changed files with 600 additions and 91 deletions
+2 -1
View File
@@ -351,6 +351,8 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@@ -380,7 +382,6 @@ go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+51 -7
View File
@@ -18,6 +18,7 @@ type PaymentMethodAnalyticsRequest struct {
type PaymentMethodAnalyticsResponse struct { type PaymentMethodAnalyticsResponse struct {
OrganizationID uuid.UUID `json:"organization_id"` OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id,omitempty"` OutletID *uuid.UUID `json:"outlet_id,omitempty"`
OutletName *string `json:"outlet_name,omitempty"`
DateFrom time.Time `json:"date_from"` DateFrom time.Time `json:"date_from"`
DateTo time.Time `json:"date_to"` DateTo time.Time `json:"date_to"`
GroupBy string `json:"group_by"` GroupBy string `json:"group_by"`
@@ -54,6 +55,7 @@ type SalesAnalyticsRequest struct {
type SalesAnalyticsResponse struct { type SalesAnalyticsResponse struct {
OrganizationID uuid.UUID `json:"organization_id"` OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id,omitempty"` OutletID *uuid.UUID `json:"outlet_id,omitempty"`
OutletName *string `json:"outlet_name,omitempty"`
DateFrom time.Time `json:"date_from"` DateFrom time.Time `json:"date_from"`
DateTo time.Time `json:"date_to"` DateTo time.Time `json:"date_to"`
GroupBy string `json:"group_by"` GroupBy string `json:"group_by"`
@@ -88,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 {
@@ -100,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"`
} }
@@ -130,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"`
@@ -161,6 +178,7 @@ type ProductAnalyticsRequest struct {
type ProductAnalyticsResponse struct { type ProductAnalyticsResponse struct {
OrganizationID uuid.UUID `json:"organization_id"` OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id,omitempty"` OutletID *uuid.UUID `json:"outlet_id,omitempty"`
OutletName *string `json:"outlet_name,omitempty"`
DateFrom time.Time `json:"date_from"` DateFrom time.Time `json:"date_from"`
DateTo time.Time `json:"date_to"` DateTo time.Time `json:"date_to"`
Data []ProductAnalyticsData `json:"data"` Data []ProductAnalyticsData `json:"data"`
@@ -198,6 +216,7 @@ type ProductAnalyticsPerCategoryRequest struct {
type ProductAnalyticsPerCategoryResponse struct { type ProductAnalyticsPerCategoryResponse struct {
OrganizationID uuid.UUID `json:"organization_id"` OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id,omitempty"` OutletID *uuid.UUID `json:"outlet_id,omitempty"`
OutletName *string `json:"outlet_name,omitempty"`
DateFrom time.Time `json:"date_from"` DateFrom time.Time `json:"date_from"`
DateTo time.Time `json:"date_to"` DateTo time.Time `json:"date_to"`
Data []ProductAnalyticsPerCategoryData `json:"data"` Data []ProductAnalyticsPerCategoryData `json:"data"`
@@ -227,6 +246,7 @@ type DashboardAnalyticsRequest struct {
type DashboardAnalyticsResponse struct { type DashboardAnalyticsResponse struct {
OrganizationID uuid.UUID `json:"organization_id"` OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id,omitempty"` OutletID *uuid.UUID `json:"outlet_id,omitempty"`
OutletName *string `json:"outlet_name,omitempty"`
DateFrom time.Time `json:"date_from"` DateFrom time.Time `json:"date_from"`
DateTo time.Time `json:"date_to"` DateTo time.Time `json:"date_to"`
Overview DashboardOverview `json:"overview"` Overview DashboardOverview `json:"overview"`
@@ -237,12 +257,15 @@ type DashboardAnalyticsResponse struct {
// DashboardOverview represents the overview data for dashboard // DashboardOverview represents the overview data for dashboard
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"`
TotalLowStock int64 `json:"total_low_stock"`
TotalProductActive int64 `json:"total_product_active"`
} }
type ProfitLossAnalyticsRequest struct { type ProfitLossAnalyticsRequest struct {
@@ -256,6 +279,7 @@ type ProfitLossAnalyticsRequest struct {
type ProfitLossAnalyticsResponse struct { type ProfitLossAnalyticsResponse struct {
OrganizationID uuid.UUID `json:"organization_id"` OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id,omitempty"` OutletID *uuid.UUID `json:"outlet_id,omitempty"`
OutletName *string `json:"outlet_name,omitempty"`
DateFrom time.Time `json:"date_from"` DateFrom time.Time `json:"date_from"`
DateTo time.Time `json:"date_to"` DateTo time.Time `json:"date_to"`
GroupBy string `json:"group_by"` GroupBy string `json:"group_by"`
@@ -263,10 +287,28 @@ type ProfitLossAnalyticsResponse struct {
Data []ProfitLossData `json:"data"` Data []ProfitLossData `json:"data"`
ProductData []ProductProfitData `json:"product_data"` ProductData []ProductProfitData `json:"product_data"`
MainSummary []ProfitLossSummaryRow `json:"main_summary"` MainSummary []ProfitLossSummaryRow `json:"main_summary"`
Purchasing ProfitLossPurchasing `json:"purchasing"`
OperationalExpenses []OperationalExpenseItem `json:"operational_expenses"` OperationalExpenses []OperationalExpenseItem `json:"operational_expenses"`
OperationalExpensesTotal float64 `json:"operational_expenses_total"` OperationalExpensesTotal float64 `json:"operational_expenses_total"`
} }
type ProfitLossPurchasing struct {
TodayTotal float64 `json:"today_total"`
MtdTotal float64 `json:"mtd_total"`
TodayRawMaterial float64 `json:"today_raw_material"`
MtdRawMaterial float64 `json:"mtd_raw_material"`
TodayExpense float64 `json:"today_expense"`
MtdExpense float64 `json:"mtd_expense"`
Items []ProfitLossPurchasingItem `json:"items"`
}
type ProfitLossPurchasingItem struct {
Date time.Time `json:"date"`
Item string `json:"item"`
Quantity float64 `json:"quantity"`
Nominal float64 `json:"nominal"`
}
type ProfitLossSummary struct { type ProfitLossSummary struct {
TotalRevenue float64 `json:"total_revenue"` TotalRevenue float64 `json:"total_revenue"`
TotalCost float64 `json:"total_cost"` TotalCost float64 `json:"total_cost"`
@@ -349,6 +391,7 @@ type ExclusiveSummaryMTDRequest struct {
type ExclusiveSummaryPeriodResponse struct { type ExclusiveSummaryPeriodResponse struct {
OrganizationID uuid.UUID `json:"organization_id"` OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id,omitempty"` OutletID *uuid.UUID `json:"outlet_id,omitempty"`
OutletName *string `json:"outlet_name,omitempty"`
Period ExclusiveSummaryPeriodRange `json:"period"` Period ExclusiveSummaryPeriodRange `json:"period"`
Summary ExclusiveSummaryPeriodSummary `json:"summary"` Summary ExclusiveSummaryPeriodSummary `json:"summary"`
Reimburse ExclusiveSummaryReimburse `json:"reimburse"` Reimburse ExclusiveSummaryReimburse `json:"reimburse"`
@@ -408,6 +451,7 @@ type ExclusiveSummaryDailyTransaction struct {
type ExclusiveSummaryMonthlyResponse struct { type ExclusiveSummaryMonthlyResponse struct {
OrganizationID uuid.UUID `json:"organization_id"` OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id,omitempty"` OutletID *uuid.UUID `json:"outlet_id,omitempty"`
OutletName *string `json:"outlet_name,omitempty"`
Month string `json:"month"` Month string `json:"month"`
Summary ExclusiveSummaryMonthlySummary `json:"summary"` Summary ExclusiveSummaryMonthlySummary `json:"summary"`
Periods []ExclusiveSummaryMonthlyPeriod `json:"periods"` Periods []ExclusiveSummaryMonthlyPeriod `json:"periods"`
+48 -16
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,25 +129,42 @@ 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"`
TotalLowStock int64 `json:"total_low_stock"`
TotalProductActive int64 `json:"total_product_active"`
} }
type ProfitLossAnalytics struct { type ProfitLossAnalytics struct {
Summary ProfitLossSummary Summary ProfitLossSummary
Data []ProfitLossData Data []ProfitLossData
ProductData []ProductProfitData ProductData []ProductProfitData
TodayRevenue float64 TodayRevenue float64
TodayCost float64 TodayCost float64
MtdRevenue float64 MtdRevenue float64
MtdCost float64 MtdCost float64
TodayExpenseByCategory []ExpenseCategoryTotal TodayPurchasing float64
MtdExpenseByCategory []ExpenseCategoryTotal MtdPurchasing float64
OperationalExpenseItems []OperationalExpenseItem TodayPurchasingRawMaterial float64
MtdPurchasingRawMaterial float64
TodayPurchasingExpense float64
MtdPurchasingExpense float64
PurchasingItems []PurchasingItemDetail
TodayExpenseByCategory []ExpenseCategoryTotal
MtdExpenseByCategory []ExpenseCategoryTotal
OperationalExpenseItems []OperationalExpenseItem
}
type PurchasingItemDetail struct {
Date time.Time
Item string
Quantity float64
Amount float64
} }
type ProfitLossSummary struct { type ProfitLossSummary struct {
+51 -7
View File
@@ -19,6 +19,7 @@ type PaymentMethodAnalyticsRequest struct {
type PaymentMethodAnalyticsResponse struct { type PaymentMethodAnalyticsResponse struct {
OrganizationID uuid.UUID `json:"organization_id"` OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id,omitempty"` OutletID *uuid.UUID `json:"outlet_id,omitempty"`
OutletName *string `json:"outlet_name,omitempty"`
DateFrom time.Time `json:"date_from"` DateFrom time.Time `json:"date_from"`
DateTo time.Time `json:"date_to"` DateTo time.Time `json:"date_to"`
GroupBy string `json:"group_by"` GroupBy string `json:"group_by"`
@@ -58,6 +59,7 @@ type SalesAnalyticsRequest struct {
type SalesAnalyticsResponse struct { type SalesAnalyticsResponse struct {
OrganizationID uuid.UUID `json:"organization_id"` OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id,omitempty"` OutletID *uuid.UUID `json:"outlet_id,omitempty"`
OutletName *string `json:"outlet_name,omitempty"`
DateFrom time.Time `json:"date_from"` DateFrom time.Time `json:"date_from"`
DateTo time.Time `json:"date_to"` DateTo time.Time `json:"date_to"`
GroupBy string `json:"group_by"` GroupBy string `json:"group_by"`
@@ -93,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
@@ -106,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"`
} }
@@ -138,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"`
@@ -171,6 +188,7 @@ type ProductAnalyticsRequest struct {
type ProductAnalyticsResponse struct { type ProductAnalyticsResponse struct {
OrganizationID uuid.UUID `json:"organization_id"` OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id,omitempty"` OutletID *uuid.UUID `json:"outlet_id,omitempty"`
OutletName *string `json:"outlet_name,omitempty"`
DateFrom time.Time `json:"date_from"` DateFrom time.Time `json:"date_from"`
DateTo time.Time `json:"date_to"` DateTo time.Time `json:"date_to"`
Data []ProductAnalyticsData `json:"data"` Data []ProductAnalyticsData `json:"data"`
@@ -208,6 +226,7 @@ type ProductAnalyticsPerCategoryRequest struct {
type ProductAnalyticsPerCategoryResponse struct { type ProductAnalyticsPerCategoryResponse struct {
OrganizationID uuid.UUID `json:"organization_id"` OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id,omitempty"` OutletID *uuid.UUID `json:"outlet_id,omitempty"`
OutletName *string `json:"outlet_name,omitempty"`
DateFrom time.Time `json:"date_from"` DateFrom time.Time `json:"date_from"`
DateTo time.Time `json:"date_to"` DateTo time.Time `json:"date_to"`
Data []ProductAnalyticsPerCategoryData `json:"data"` Data []ProductAnalyticsPerCategoryData `json:"data"`
@@ -237,6 +256,7 @@ type DashboardAnalyticsRequest struct {
type DashboardAnalyticsResponse struct { type DashboardAnalyticsResponse struct {
OrganizationID uuid.UUID `json:"organization_id"` OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id,omitempty"` OutletID *uuid.UUID `json:"outlet_id,omitempty"`
OutletName *string `json:"outlet_name,omitempty"`
DateFrom time.Time `json:"date_from"` DateFrom time.Time `json:"date_from"`
DateTo time.Time `json:"date_to"` DateTo time.Time `json:"date_to"`
Overview DashboardOverview `json:"overview"` Overview DashboardOverview `json:"overview"`
@@ -247,12 +267,15 @@ type DashboardAnalyticsResponse struct {
// DashboardOverview represents the overview data for dashboard // DashboardOverview represents the overview data for dashboard
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"`
TotalLowStock int64 `json:"total_low_stock"`
TotalProductActive int64 `json:"total_product_active"`
} }
type ProfitLossAnalyticsRequest struct { type ProfitLossAnalyticsRequest struct {
@@ -266,6 +289,7 @@ type ProfitLossAnalyticsRequest struct {
type ProfitLossAnalyticsResponse struct { type ProfitLossAnalyticsResponse struct {
OrganizationID uuid.UUID `json:"organization_id"` OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id,omitempty"` OutletID *uuid.UUID `json:"outlet_id,omitempty"`
OutletName *string `json:"outlet_name,omitempty"`
DateFrom time.Time `json:"date_from"` DateFrom time.Time `json:"date_from"`
DateTo time.Time `json:"date_to"` DateTo time.Time `json:"date_to"`
GroupBy string `json:"group_by"` GroupBy string `json:"group_by"`
@@ -273,10 +297,28 @@ type ProfitLossAnalyticsResponse struct {
Data []ProfitLossData `json:"data"` Data []ProfitLossData `json:"data"`
ProductData []ProductProfitData `json:"product_data"` ProductData []ProductProfitData `json:"product_data"`
MainSummary []ProfitLossSummaryRow `json:"main_summary"` MainSummary []ProfitLossSummaryRow `json:"main_summary"`
Purchasing ProfitLossPurchasing `json:"purchasing"`
OperationalExpenses []OperationalExpenseItem `json:"operational_expenses"` OperationalExpenses []OperationalExpenseItem `json:"operational_expenses"`
OperationalExpensesTotal float64 `json:"operational_expenses_total"` OperationalExpensesTotal float64 `json:"operational_expenses_total"`
} }
type ProfitLossPurchasing struct {
TodayTotal float64 `json:"today_total"`
MtdTotal float64 `json:"mtd_total"`
TodayRawMaterial float64 `json:"today_raw_material"`
MtdRawMaterial float64 `json:"mtd_raw_material"`
TodayExpense float64 `json:"today_expense"`
MtdExpense float64 `json:"mtd_expense"`
Items []ProfitLossPurchasingItem `json:"items"`
}
type ProfitLossPurchasingItem struct {
Date time.Time `json:"date"`
Item string `json:"item"`
Quantity float64 `json:"quantity"`
Nominal float64 `json:"nominal"`
}
type ProfitLossSummary struct { type ProfitLossSummary struct {
TotalRevenue float64 `json:"total_revenue"` TotalRevenue float64 `json:"total_revenue"`
TotalCost float64 `json:"total_cost"` TotalCost float64 `json:"total_cost"`
@@ -359,6 +401,7 @@ type ExclusiveSummaryMTDRequest struct {
type ExclusiveSummaryPeriodResponse struct { type ExclusiveSummaryPeriodResponse struct {
OrganizationID uuid.UUID `json:"organization_id"` OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id,omitempty"` OutletID *uuid.UUID `json:"outlet_id,omitempty"`
OutletName *string `json:"outlet_name,omitempty"`
Period ExclusiveSummaryPeriodRange `json:"period"` Period ExclusiveSummaryPeriodRange `json:"period"`
Summary ExclusiveSummaryPeriodSummary `json:"summary"` Summary ExclusiveSummaryPeriodSummary `json:"summary"`
Reimburse ExclusiveSummaryReimburse `json:"reimburse"` Reimburse ExclusiveSummaryReimburse `json:"reimburse"`
@@ -418,6 +461,7 @@ type ExclusiveSummaryDailyTransaction struct {
type ExclusiveSummaryMonthlyResponse struct { type ExclusiveSummaryMonthlyResponse struct {
OrganizationID uuid.UUID `json:"organization_id"` OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id,omitempty"` OutletID *uuid.UUID `json:"outlet_id,omitempty"`
OutletName *string `json:"outlet_name,omitempty"`
Month string `json:"month"` Month string `json:"month"`
Summary ExclusiveSummaryMonthlySummary `json:"summary"` Summary ExclusiveSummaryMonthlySummary `json:"summary"`
Periods []ExclusiveSummaryMonthlyPeriod `json:"periods"` Periods []ExclusiveSummaryMonthlyPeriod `json:"periods"`
+71 -9
View File
@@ -9,6 +9,8 @@ import (
"apskel-pos-be/internal/entities" "apskel-pos-be/internal/entities"
"apskel-pos-be/internal/models" "apskel-pos-be/internal/models"
"apskel-pos-be/internal/repository" "apskel-pos-be/internal/repository"
"github.com/google/uuid"
) )
type AnalyticsProcessor interface { type AnalyticsProcessor interface {
@@ -36,6 +38,18 @@ func NewAnalyticsProcessorImpl(analyticsRepo repository.AnalyticsRepository, exp
} }
} }
// resolveOutletName fetches the outlet name from the database if outletID is provided
func (p *AnalyticsProcessorImpl) resolveOutletName(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) *string {
if outletID == nil {
return nil
}
name, err := p.analyticsRepo.GetOutletName(ctx, organizationID, *outletID)
if err != nil || name == "" {
return nil
}
return &name
}
func (p *AnalyticsProcessorImpl) GetPaymentMethodAnalytics(ctx context.Context, req *models.PaymentMethodAnalyticsRequest) (*models.PaymentMethodAnalyticsResponse, error) { func (p *AnalyticsProcessorImpl) GetPaymentMethodAnalytics(ctx context.Context, req *models.PaymentMethodAnalyticsRequest) (*models.PaymentMethodAnalyticsResponse, error) {
if req.DateFrom.After(req.DateTo) { if req.DateFrom.After(req.DateTo) {
return nil, fmt.Errorf("date_from cannot be after date_to") return nil, fmt.Errorf("date_from cannot be after date_to")
@@ -90,6 +104,7 @@ func (p *AnalyticsProcessorImpl) GetPaymentMethodAnalytics(ctx context.Context,
return &models.PaymentMethodAnalyticsResponse{ return &models.PaymentMethodAnalyticsResponse{
OrganizationID: req.OrganizationID, OrganizationID: req.OrganizationID,
OutletID: req.OutletID, OutletID: req.OutletID,
OutletName: p.resolveOutletName(ctx, req.OrganizationID, req.OutletID),
DateFrom: req.DateFrom, DateFrom: req.DateFrom,
DateTo: req.DateTo, DateTo: req.DateTo,
GroupBy: req.GroupBy, GroupBy: req.GroupBy,
@@ -164,6 +179,7 @@ func (p *AnalyticsProcessorImpl) GetSalesAnalytics(ctx context.Context, req *mod
return &models.SalesAnalyticsResponse{ return &models.SalesAnalyticsResponse{
OrganizationID: req.OrganizationID, OrganizationID: req.OrganizationID,
OutletID: req.OutletID, OutletID: req.OutletID,
OutletName: p.resolveOutletName(ctx, req.OrganizationID, req.OutletID),
DateFrom: req.DateFrom, DateFrom: req.DateFrom,
DateTo: req.DateTo, DateTo: req.DateTo,
GroupBy: req.GroupBy, GroupBy: req.GroupBy,
@@ -202,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{
@@ -246,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
@@ -295,6 +329,7 @@ func (p *AnalyticsProcessorImpl) GetProductAnalytics(ctx context.Context, req *m
return &models.ProductAnalyticsResponse{ return &models.ProductAnalyticsResponse{
OrganizationID: req.OrganizationID, OrganizationID: req.OrganizationID,
OutletID: req.OutletID, OutletID: req.OutletID,
OutletName: p.resolveOutletName(ctx, req.OrganizationID, req.OutletID),
DateFrom: req.DateFrom, DateFrom: req.DateFrom,
DateTo: req.DateTo, DateTo: req.DateTo,
Data: resultData, Data: resultData,
@@ -332,6 +367,7 @@ func (p *AnalyticsProcessorImpl) GetProductAnalyticsPerCategory(ctx context.Cont
return &models.ProductAnalyticsPerCategoryResponse{ return &models.ProductAnalyticsPerCategoryResponse{
OrganizationID: req.OrganizationID, OrganizationID: req.OrganizationID,
OutletID: req.OutletID, OutletID: req.OutletID,
OutletName: p.resolveOutletName(ctx, req.OrganizationID, req.OutletID),
DateFrom: req.DateFrom, DateFrom: req.DateFrom,
DateTo: req.DateTo, DateTo: req.DateTo,
Data: resultData, Data: resultData,
@@ -393,15 +429,19 @@ func (p *AnalyticsProcessorImpl) GetDashboardAnalytics(ctx context.Context, req
return &models.DashboardAnalyticsResponse{ return &models.DashboardAnalyticsResponse{
OrganizationID: req.OrganizationID, OrganizationID: req.OrganizationID,
OutletID: req.OutletID, OutletID: req.OutletID,
OutletName: p.resolveOutletName(ctx, req.OrganizationID, req.OutletID),
DateFrom: req.DateFrom, DateFrom: req.DateFrom,
DateTo: req.DateTo, DateTo: req.DateTo,
Overview: models.DashboardOverview{ Overview: models.DashboardOverview{
TotalSales: overview.TotalSales, TotalSales: overview.TotalSales,
TotalOrders: overview.TotalOrders, TotalOrders: overview.TotalOrders,
AverageOrderValue: overview.AverageOrderValue, AverageOrderValue: overview.AverageOrderValue,
TotalCustomers: overview.TotalCustomers, TotalCustomers: overview.TotalCustomers,
VoidedOrders: overview.VoidedOrders, VoidedOrders: overview.VoidedOrders,
RefundedOrders: overview.RefundedOrders, RefundedOrders: overview.RefundedOrders,
TotalItemSold: overview.TotalItemSold,
TotalLowStock: overview.TotalLowStock,
TotalProductActive: overview.TotalProductActive,
}, },
TopProducts: topProducts.Data, TopProducts: topProducts.Data,
PaymentMethods: paymentMethods.Data, PaymentMethods: paymentMethods.Data,
@@ -604,9 +644,20 @@ func (p *AnalyticsProcessorImpl) GetProfitLossAnalytics(ctx context.Context, req
opsTotal += item.Amount opsTotal += item.Amount
} }
purchasingItems := make([]models.ProfitLossPurchasingItem, len(result.PurchasingItems))
for i, item := range result.PurchasingItems {
purchasingItems[i] = models.ProfitLossPurchasingItem{
Date: item.Date,
Item: item.Item,
Quantity: item.Quantity,
Nominal: item.Amount,
}
}
return &models.ProfitLossAnalyticsResponse{ return &models.ProfitLossAnalyticsResponse{
OrganizationID: req.OrganizationID, OrganizationID: req.OrganizationID,
OutletID: req.OutletID, OutletID: req.OutletID,
OutletName: p.resolveOutletName(ctx, req.OrganizationID, req.OutletID),
DateFrom: req.DateFrom, DateFrom: req.DateFrom,
DateTo: req.DateTo, DateTo: req.DateTo,
GroupBy: req.GroupBy, GroupBy: req.GroupBy,
@@ -623,9 +674,18 @@ func (p *AnalyticsProcessorImpl) GetProfitLossAnalytics(ctx context.Context, req
AverageProfit: result.Summary.AverageProfit, AverageProfit: result.Summary.AverageProfit,
ProfitabilityRatio: result.Summary.ProfitabilityRatio, ProfitabilityRatio: result.Summary.ProfitabilityRatio,
}, },
Data: data, Data: data,
ProductData: productData, ProductData: productData,
MainSummary: mainSummary, MainSummary: mainSummary,
Purchasing: models.ProfitLossPurchasing{
TodayTotal: result.TodayPurchasing,
MtdTotal: result.MtdPurchasing,
TodayRawMaterial: result.TodayPurchasingRawMaterial,
MtdRawMaterial: result.MtdPurchasingRawMaterial,
TodayExpense: result.TodayPurchasingExpense,
MtdExpense: result.MtdPurchasingExpense,
Items: purchasingItems,
},
OperationalExpenses: opsItems, OperationalExpenses: opsItems,
OperationalExpensesTotal: opsTotal, OperationalExpensesTotal: opsTotal,
}, nil }, nil
@@ -721,6 +781,7 @@ func (p *AnalyticsProcessorImpl) GetExclusiveSummaryMonthly(ctx context.Context,
return &models.ExclusiveSummaryMonthlyResponse{ return &models.ExclusiveSummaryMonthlyResponse{
OrganizationID: req.OrganizationID, OrganizationID: req.OrganizationID,
OutletID: req.OutletID, OutletID: req.OutletID,
OutletName: p.resolveOutletName(ctx, req.OrganizationID, req.OutletID),
Month: monthStart.Format("2006-01"), Month: monthStart.Format("2006-01"),
Summary: models.ExclusiveSummaryMonthlySummary{ Summary: models.ExclusiveSummaryMonthlySummary{
TotalSales: fullPeriod.Summary.Sales, TotalSales: fullPeriod.Summary.Sales,
@@ -795,6 +856,7 @@ func (p *AnalyticsProcessorImpl) buildExclusiveSummaryPeriod(ctx context.Context
return &models.ExclusiveSummaryPeriodResponse{ return &models.ExclusiveSummaryPeriodResponse{
OrganizationID: req.OrganizationID, OrganizationID: req.OrganizationID,
OutletID: req.OutletID, OutletID: req.OutletID,
OutletName: p.resolveOutletName(ctx, req.OrganizationID, req.OutletID),
Period: models.ExclusiveSummaryPeriodRange{ Period: models.ExclusiveSummaryPeriodRange{
DateFrom: req.DateFrom, DateFrom: req.DateFrom,
DateTo: req.DateTo, DateTo: req.DateTo,
@@ -68,6 +68,10 @@ func (s *analyticsRepositoryStub) GetExclusiveSummaryBankBalances(context.Contex
return s.bankBalances, nil return s.bankBalances, nil
} }
func (analyticsRepositoryStub) GetOutletName(context.Context, uuid.UUID, uuid.UUID) (string, error) {
return "", nil
}
type expenseRepositoryStub struct{} type expenseRepositoryStub struct{}
func (expenseRepositoryStub) Create(context.Context, *entities.Expense) error { return nil } func (expenseRepositoryStub) Create(context.Context, *entities.Expense) error { return nil }
@@ -141,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()
+216 -38
View File
@@ -21,6 +21,7 @@ type AnalyticsRepository interface {
GetProfitLossAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, groupBy string) (*entities.ProfitLossAnalytics, error) GetProfitLossAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, groupBy string) (*entities.ProfitLossAnalytics, error)
GetExclusiveSummaryAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) (*entities.ExclusiveSummaryAnalytics, error) GetExclusiveSummaryAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) (*entities.ExclusiveSummaryAnalytics, error)
GetExclusiveSummaryBankBalances(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) ([]entities.ExclusiveSummaryBankBalance, error) GetExclusiveSummaryBankBalances(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) ([]entities.ExclusiveSummaryBankBalance, error)
GetOutletName(ctx context.Context, organizationID uuid.UUID, outletID uuid.UUID) (string, error)
} }
type AnalyticsRepositoryImpl struct { type AnalyticsRepositoryImpl struct {
@@ -40,6 +41,22 @@ func (r *AnalyticsRepositoryImpl) resolveOutletID(query *gorm.DB, outletID *uuid
return query return query
} }
func (r *AnalyticsRepositoryImpl) GetOutletName(ctx context.Context, organizationID uuid.UUID, outletID uuid.UUID) (string, error) {
var outlet struct {
Name string
}
result := r.db.WithContext(ctx).
Table("outlets").
Select("name").
Where("id = ? AND organization_id = ?", outletID, organizationID).
Limit(1).
Scan(&outlet)
if result.Error != nil {
return "", result.Error
}
return outlet.Name, nil
}
func purchaseOrderItemTotalAmountSQL() string { func purchaseOrderItemTotalAmountSQL() string {
return "CASE WHEN pc.type = '" + string(entities.PurchaseCategoryTypeRawMaterial) + "' THEN COALESCE(poi.quantity, 0) * poi.amount ELSE poi.amount END" return "CASE WHEN pc.type = '" + string(entities.PurchaseCategoryTypeRawMaterial) + "' THEN COALESCE(poi.quantity, 0) * poi.amount ELSE poi.amount END"
} }
@@ -207,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
@@ -298,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
@@ -318,6 +370,13 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organ
Select(` Select(`
p.id as product_id, p.id as product_id,
p.name as product_name, p.name as product_name,
p.sku as product_sku,
COALESCE(
NULLIF(pop.price, 0),
(SELECT price FROM product_outlet_prices WHERE product_id = p.id ORDER BY updated_at DESC LIMIT 1),
NULLIF(p.price, 0),
0
) as product_price,
c.id as category_id, c.id as category_id,
c.name as category_name, c.name as category_name,
c.order as category_order, c.order as category_order,
@@ -351,6 +410,7 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organ
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").
Joins("JOIN orders o ON oi.order_id = o.id"). Joins("JOIN orders o ON oi.order_id = o.id").
Joins("LEFT JOIN product_outlet_prices pop ON pop.product_id = p.id AND pop.outlet_id = o.outlet_id").
Joins("LEFT JOIN (?) mahpp ON mahpp.product_id = p.id", Joins("LEFT JOIN (?) mahpp ON mahpp.product_id = p.id",
r.db.Table("product_recipes pr2"). r.db.Table("product_recipes pr2").
Select("pr2.product_id, SUM(pr2.quantity * (1 + COALESCE(pr2.waste_percentage, 0)/100.0) * COALESCE(ma.moving_avg_cost, ing.cost)) as hpp_per_unit"). Select("pr2.product_id, SUM(pr2.quantity * (1 + COALESCE(pr2.waste_percentage, 0)/100.0) * COALESCE(ma.moving_avg_cost, ing.cost)) as hpp_per_unit").
@@ -376,7 +436,7 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organ
query = r.resolveOutletID(query, outletID, "o.outlet_id") query = r.resolveOutletID(query, outletID, "o.outlet_id")
err := query. err := query.
Group("p.id, p.name, p.cost, c.id, c.name, c.order, mahpp.hpp_per_unit"). Group("p.id, p.name, p.sku, p.price, p.cost, pop.price, c.id, c.name, c.order, mahpp.hpp_per_unit").
Order("revenue DESC"). Order("revenue DESC").
Limit(limit). Limit(limit).
Scan(&results).Error Scan(&results).Error
@@ -463,6 +523,41 @@ func (r *AnalyticsRepositoryImpl) GetDashboardOverview(ctx context.Context, orga
return nil, err return nil, err
} }
// Total item sold (sum of order_items quantity for completed orders in date range)
var totalItemSold int64
itemQuery := r.db.WithContext(ctx).
Table("order_items oi").
Select("COALESCE(SUM(oi.quantity), 0)").
Joins("JOIN orders o ON o.id = oi.order_id").
Where("o.organization_id = ?", organizationID).
Where("o.is_void = false AND o.is_refund = false AND o.payment_status = 'completed'").
Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo)
itemQuery = r.resolveOutletID(itemQuery, outletID, "o.outlet_id")
itemQuery.Scan(&totalItemSold)
result.TotalItemSold = totalItemSold
// Total low stock (inventory where quantity <= reorder_level)
var totalLowStock int64
lowStockQuery := r.db.WithContext(ctx).
Table("inventory i").
Select("COUNT(i.id)").
Joins("JOIN products p ON p.id = i.product_id").
Where("p.organization_id = ?", organizationID).
Where("i.quantity <= i.reorder_level")
lowStockQuery = r.resolveOutletID(lowStockQuery, outletID, "i.outlet_id")
lowStockQuery.Scan(&totalLowStock)
result.TotalLowStock = totalLowStock
// Total active products
var totalProductActive int64
productQuery := r.db.WithContext(ctx).
Table("products p").
Select("COUNT(p.id)").
Where("p.organization_id = ?", organizationID).
Where("p.is_active = true")
productQuery.Scan(&totalProductActive)
result.TotalProductActive = totalProductActive
return &result, nil return &result, nil
} }
@@ -687,17 +782,38 @@ func (r *AnalyticsRepositoryImpl) GetProfitLossAnalytics(ctx context.Context, or
} }
opsItems = mergeOperationalExpenseItems(opsItems, poOpsItems) opsItems = mergeOperationalExpenseItems(opsItems, poOpsItems)
todayPurchasing, err := r.getPurchaseOrderTotals(ctx, organizationID, todayStart, todayEnd)
if err != nil {
return nil, err
}
mtdPurchasing, err := r.getPurchaseOrderTotals(ctx, organizationID, mtdStart, todayEnd)
if err != nil {
return nil, err
}
purchasingItems, err := r.getPurchasingItemDetails(ctx, organizationID, dateFrom, dateTo)
if err != nil {
return nil, err
}
return &entities.ProfitLossAnalytics{ return &entities.ProfitLossAnalytics{
Summary: summary, Summary: summary,
Data: data, Data: data,
ProductData: productData, ProductData: productData,
TodayRevenue: todayRC.Revenue, TodayRevenue: todayRC.Revenue,
TodayCost: todayRC.Cost, TodayCost: todayRC.Cost,
MtdRevenue: mtdRC.Revenue, MtdRevenue: mtdRC.Revenue,
MtdCost: mtdRC.Cost, MtdCost: mtdRC.Cost,
TodayExpenseByCategory: todayExpenseByCategory, TodayPurchasing: todayPurchasing.Total,
MtdExpenseByCategory: mtdExpenseByCategory, MtdPurchasing: mtdPurchasing.Total,
OperationalExpenseItems: opsItems, TodayPurchasingRawMaterial: todayPurchasing.RawMaterial,
MtdPurchasingRawMaterial: mtdPurchasing.RawMaterial,
TodayPurchasingExpense: todayPurchasing.Expense,
MtdPurchasingExpense: mtdPurchasing.Expense,
PurchasingItems: purchasingItems,
TodayExpenseByCategory: todayExpenseByCategory,
MtdExpenseByCategory: mtdExpenseByCategory,
OperationalExpenseItems: opsItems,
}, nil }, nil
} }
@@ -724,6 +840,68 @@ func (r *AnalyticsRepositoryImpl) getPurchaseOrderRawMaterialTotal(ctx context.C
return result.Total, nil return result.Total, nil
} }
type purchasingTotals struct {
Total float64
RawMaterial float64
Expense float64
}
func (r *AnalyticsRepositoryImpl) getPurchaseOrderTotals(ctx context.Context, organizationID uuid.UUID, dateFrom, dateTo time.Time) (purchasingTotals, error) {
type result struct {
Total float64
RawMaterial float64
Expense float64
}
var res result
query := r.db.WithContext(ctx).
Table("purchase_order_items poi").
Select(`
COALESCE(SUM(`+purchaseOrderItemTotalAmountSQL()+`), 0) as total,
COALESCE(SUM(`+purchaseOrderRawMaterialAmountSQL()+`), 0) as raw_material,
COALESCE(SUM(`+purchaseOrderExpenseAmountSQL()+`), 0) as expense
`).
Joins("JOIN purchase_orders po ON poi.purchase_order_id = po.id").
Joins("JOIN purchase_categories pc ON poi.purchase_category_id = pc.id").
Where("po.organization_id = ?", organizationID).
Where("po.status = ?", "received").
Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo)
if err := query.Scan(&res).Error; err != nil {
return purchasingTotals{}, err
}
return purchasingTotals{
Total: res.Total,
RawMaterial: res.RawMaterial,
Expense: res.Expense,
}, nil
}
func (r *AnalyticsRepositoryImpl) getPurchasingItemDetails(ctx context.Context, organizationID uuid.UUID, dateFrom, dateTo time.Time) ([]entities.PurchasingItemDetail, error) {
var results []entities.PurchasingItemDetail
query := r.db.WithContext(ctx).
Table("purchase_order_items poi").
Select(`
po.transaction_date as date,
COALESCE(NULLIF(poi.description, ''), i.name, pc.name) as item,
COALESCE(poi.quantity, 0) as quantity,
CASE WHEN pc.type = '`+string(entities.PurchaseCategoryTypeRawMaterial)+`' THEN COALESCE(poi.quantity, 0) * poi.amount ELSE poi.amount END as amount
`).
Joins("JOIN purchase_orders po 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").
Where("po.organization_id = ?", organizationID).
Where("po.status = ?", "received").
Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo).
Order("po.transaction_date DESC, poi.created_at DESC")
if err := query.Scan(&results).Error; err != nil {
return nil, err
}
return results, nil
}
func (r *AnalyticsRepositoryImpl) getPurchaseOrderRawMaterialCostByPeriod(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, groupBy string) ([]entities.ProfitLossData, error) { func (r *AnalyticsRepositoryImpl) getPurchaseOrderRawMaterialCostByPeriod(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, groupBy string) ([]entities.ProfitLossData, error) {
var dateFormat string var dateFormat string
switch groupBy { switch groupBy {
+5 -4
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)
@@ -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)
+57 -9
View File
@@ -66,6 +66,7 @@ func PaymentMethodAnalyticsModelToContract(resp *models.PaymentMethodAnalyticsRe
return &contract.PaymentMethodAnalyticsResponse{ return &contract.PaymentMethodAnalyticsResponse{
OrganizationID: resp.OrganizationID, OrganizationID: resp.OrganizationID,
OutletID: resp.OutletID, OutletID: resp.OutletID,
OutletName: resp.OutletName,
DateFrom: resp.DateFrom, DateFrom: resp.DateFrom,
DateTo: resp.DateTo, DateTo: resp.DateTo,
GroupBy: resp.GroupBy, GroupBy: resp.GroupBy,
@@ -122,6 +123,7 @@ func SalesAnalyticsModelToContract(resp *models.SalesAnalyticsResponse) *contrac
return &contract.SalesAnalyticsResponse{ return &contract.SalesAnalyticsResponse{
OrganizationID: resp.OrganizationID, OrganizationID: resp.OrganizationID,
OutletID: resp.OutletID, OutletID: resp.OutletID,
OutletName: resp.OutletName,
DateFrom: resp.DateFrom, DateFrom: resp.DateFrom,
DateTo: resp.DateTo, DateTo: resp.DateTo,
GroupBy: resp.GroupBy, GroupBy: resp.GroupBy,
@@ -182,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{
@@ -226,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,
} }
@@ -285,6 +305,7 @@ func ProductAnalyticsModelToContract(resp *models.ProductAnalyticsResponse) *con
return &contract.ProductAnalyticsResponse{ return &contract.ProductAnalyticsResponse{
OrganizationID: resp.OrganizationID, OrganizationID: resp.OrganizationID,
OutletID: resp.OutletID, OutletID: resp.OutletID,
OutletName: resp.OutletName,
DateFrom: resp.DateFrom, DateFrom: resp.DateFrom,
DateTo: resp.DateTo, DateTo: resp.DateTo,
Data: data, Data: data,
@@ -337,6 +358,7 @@ func ProductAnalyticsPerCategoryModelToContract(resp *models.ProductAnalyticsPer
return &contract.ProductAnalyticsPerCategoryResponse{ return &contract.ProductAnalyticsPerCategoryResponse{
OrganizationID: resp.OrganizationID, OrganizationID: resp.OrganizationID,
OutletID: resp.OutletID, OutletID: resp.OutletID,
OutletName: resp.OutletName,
DateFrom: resp.DateFrom, DateFrom: resp.DateFrom,
DateTo: resp.DateTo, DateTo: resp.DateTo,
Data: data, Data: data,
@@ -421,15 +443,19 @@ func DashboardAnalyticsModelToContract(resp *models.DashboardAnalyticsResponse)
return &contract.DashboardAnalyticsResponse{ return &contract.DashboardAnalyticsResponse{
OrganizationID: resp.OrganizationID, OrganizationID: resp.OrganizationID,
OutletID: resp.OutletID, OutletID: resp.OutletID,
OutletName: resp.OutletName,
DateFrom: resp.DateFrom, DateFrom: resp.DateFrom,
DateTo: resp.DateTo, DateTo: resp.DateTo,
Overview: contract.DashboardOverview{ Overview: contract.DashboardOverview{
TotalSales: resp.Overview.TotalSales, TotalSales: resp.Overview.TotalSales,
TotalOrders: resp.Overview.TotalOrders, TotalOrders: resp.Overview.TotalOrders,
AverageOrderValue: resp.Overview.AverageOrderValue, AverageOrderValue: resp.Overview.AverageOrderValue,
TotalCustomers: resp.Overview.TotalCustomers, TotalCustomers: resp.Overview.TotalCustomers,
VoidedOrders: resp.Overview.VoidedOrders, VoidedOrders: resp.Overview.VoidedOrders,
RefundedOrders: resp.Overview.RefundedOrders, RefundedOrders: resp.Overview.RefundedOrders,
TotalItemSold: resp.Overview.TotalItemSold,
TotalLowStock: resp.Overview.TotalLowStock,
TotalProductActive: resp.Overview.TotalProductActive,
}, },
TopProducts: topProducts, TopProducts: topProducts,
PaymentMethods: paymentMethods, PaymentMethods: paymentMethods,
@@ -516,9 +542,20 @@ func ProfitLossAnalyticsModelToContract(resp *models.ProfitLossAnalyticsResponse
} }
} }
purchasingItems := make([]contract.ProfitLossPurchasingItem, len(resp.Purchasing.Items))
for i, item := range resp.Purchasing.Items {
purchasingItems[i] = contract.ProfitLossPurchasingItem{
Date: item.Date,
Item: item.Item,
Quantity: item.Quantity,
Nominal: item.Nominal,
}
}
return &contract.ProfitLossAnalyticsResponse{ return &contract.ProfitLossAnalyticsResponse{
OrganizationID: resp.OrganizationID, OrganizationID: resp.OrganizationID,
OutletID: resp.OutletID, OutletID: resp.OutletID,
OutletName: resp.OutletName,
DateFrom: resp.DateFrom, DateFrom: resp.DateFrom,
DateTo: resp.DateTo, DateTo: resp.DateTo,
GroupBy: resp.GroupBy, GroupBy: resp.GroupBy,
@@ -535,9 +572,18 @@ func ProfitLossAnalyticsModelToContract(resp *models.ProfitLossAnalyticsResponse
AverageProfit: resp.Summary.AverageProfit, AverageProfit: resp.Summary.AverageProfit,
ProfitabilityRatio: resp.Summary.ProfitabilityRatio, ProfitabilityRatio: resp.Summary.ProfitabilityRatio,
}, },
Data: data, Data: data,
ProductData: productData, ProductData: productData,
MainSummary: mainSummary, MainSummary: mainSummary,
Purchasing: contract.ProfitLossPurchasing{
TodayTotal: resp.Purchasing.TodayTotal,
MtdTotal: resp.Purchasing.MtdTotal,
TodayRawMaterial: resp.Purchasing.TodayRawMaterial,
MtdRawMaterial: resp.Purchasing.MtdRawMaterial,
TodayExpense: resp.Purchasing.TodayExpense,
MtdExpense: resp.Purchasing.MtdExpense,
Items: purchasingItems,
},
OperationalExpenses: opsItems, OperationalExpenses: opsItems,
OperationalExpensesTotal: resp.OperationalExpensesTotal, OperationalExpensesTotal: resp.OperationalExpensesTotal,
} }
@@ -664,6 +710,7 @@ func ExclusiveSummaryPeriodModelToContract(resp *models.ExclusiveSummaryPeriodRe
return &contract.ExclusiveSummaryPeriodResponse{ return &contract.ExclusiveSummaryPeriodResponse{
OrganizationID: resp.OrganizationID, OrganizationID: resp.OrganizationID,
OutletID: resp.OutletID, OutletID: resp.OutletID,
OutletName: resp.OutletName,
Period: contract.ExclusiveSummaryPeriodRange{ Period: contract.ExclusiveSummaryPeriodRange{
DateFrom: resp.Period.DateFrom, DateFrom: resp.Period.DateFrom,
DateTo: resp.Period.DateTo, DateTo: resp.Period.DateTo,
@@ -726,6 +773,7 @@ func ExclusiveSummaryMonthlyModelToContract(resp *models.ExclusiveSummaryMonthly
return &contract.ExclusiveSummaryMonthlyResponse{ return &contract.ExclusiveSummaryMonthlyResponse{
OrganizationID: resp.OrganizationID, OrganizationID: resp.OrganizationID,
OutletID: resp.OutletID, OutletID: resp.OutletID,
OutletName: resp.OutletName,
Month: resp.Month, Month: resp.Month,
Summary: contract.ExclusiveSummaryMonthlySummary{ Summary: contract.ExclusiveSummaryMonthlySummary{
TotalSales: resp.Summary.TotalSales, TotalSales: resp.Summary.TotalSales,
@@ -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()