Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbc65c5606 | ||
|
|
9b0fc9a63b | ||
|
|
793919cf10 | ||
|
|
25024c210a | ||
|
|
3977370079 |
@@ -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/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/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.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
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.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
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.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
|
||||
@@ -18,6 +18,7 @@ type PaymentMethodAnalyticsRequest struct {
|
||||
type PaymentMethodAnalyticsResponse struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
OutletName *string `json:"outlet_name,omitempty"`
|
||||
DateFrom time.Time `json:"date_from"`
|
||||
DateTo time.Time `json:"date_to"`
|
||||
GroupBy string `json:"group_by"`
|
||||
@@ -54,6 +55,7 @@ type SalesAnalyticsRequest struct {
|
||||
type SalesAnalyticsResponse struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
OutletName *string `json:"outlet_name,omitempty"`
|
||||
DateFrom time.Time `json:"date_from"`
|
||||
DateTo time.Time `json:"date_to"`
|
||||
GroupBy string `json:"group_by"`
|
||||
@@ -88,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 {
|
||||
@@ -100,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"`
|
||||
}
|
||||
@@ -130,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"`
|
||||
@@ -161,6 +178,7 @@ type ProductAnalyticsRequest struct {
|
||||
type ProductAnalyticsResponse struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
OutletName *string `json:"outlet_name,omitempty"`
|
||||
DateFrom time.Time `json:"date_from"`
|
||||
DateTo time.Time `json:"date_to"`
|
||||
Data []ProductAnalyticsData `json:"data"`
|
||||
@@ -198,6 +216,7 @@ type ProductAnalyticsPerCategoryRequest struct {
|
||||
type ProductAnalyticsPerCategoryResponse struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
OutletName *string `json:"outlet_name,omitempty"`
|
||||
DateFrom time.Time `json:"date_from"`
|
||||
DateTo time.Time `json:"date_to"`
|
||||
Data []ProductAnalyticsPerCategoryData `json:"data"`
|
||||
@@ -227,6 +246,7 @@ type DashboardAnalyticsRequest struct {
|
||||
type DashboardAnalyticsResponse struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
OutletName *string `json:"outlet_name,omitempty"`
|
||||
DateFrom time.Time `json:"date_from"`
|
||||
DateTo time.Time `json:"date_to"`
|
||||
Overview DashboardOverview `json:"overview"`
|
||||
@@ -243,6 +263,9 @@ type DashboardOverview struct {
|
||||
TotalCustomers int64 `json:"total_customers"`
|
||||
VoidedOrders int64 `json:"voided_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 {
|
||||
@@ -256,6 +279,7 @@ type ProfitLossAnalyticsRequest struct {
|
||||
type ProfitLossAnalyticsResponse struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
OutletName *string `json:"outlet_name,omitempty"`
|
||||
DateFrom time.Time `json:"date_from"`
|
||||
DateTo time.Time `json:"date_to"`
|
||||
GroupBy string `json:"group_by"`
|
||||
@@ -263,10 +287,28 @@ type ProfitLossAnalyticsResponse struct {
|
||||
Data []ProfitLossData `json:"data"`
|
||||
ProductData []ProductProfitData `json:"product_data"`
|
||||
MainSummary []ProfitLossSummaryRow `json:"main_summary"`
|
||||
Purchasing ProfitLossPurchasing `json:"purchasing"`
|
||||
OperationalExpenses []OperationalExpenseItem `json:"operational_expenses"`
|
||||
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 {
|
||||
TotalRevenue float64 `json:"total_revenue"`
|
||||
TotalCost float64 `json:"total_cost"`
|
||||
@@ -349,6 +391,7 @@ type ExclusiveSummaryMTDRequest struct {
|
||||
type ExclusiveSummaryPeriodResponse struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
OutletName *string `json:"outlet_name,omitempty"`
|
||||
Period ExclusiveSummaryPeriodRange `json:"period"`
|
||||
Summary ExclusiveSummaryPeriodSummary `json:"summary"`
|
||||
Reimburse ExclusiveSummaryReimburse `json:"reimburse"`
|
||||
@@ -408,6 +451,7 @@ type ExclusiveSummaryDailyTransaction struct {
|
||||
type ExclusiveSummaryMonthlyResponse struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
OutletName *string `json:"outlet_name,omitempty"`
|
||||
Month string `json:"month"`
|
||||
Summary ExclusiveSummaryMonthlySummary `json:"summary"`
|
||||
Periods []ExclusiveSummaryMonthlyPeriod `json:"periods"`
|
||||
|
||||
@@ -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"`
|
||||
@@ -120,6 +135,9 @@ type DashboardOverview struct {
|
||||
TotalCustomers int64 `json:"total_customers"`
|
||||
VoidedOrders int64 `json:"voided_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 {
|
||||
@@ -130,11 +148,25 @@ type ProfitLossAnalytics struct {
|
||||
TodayCost float64
|
||||
MtdRevenue float64
|
||||
MtdCost float64
|
||||
TodayPurchasing float64
|
||||
MtdPurchasing float64
|
||||
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 {
|
||||
TotalRevenue float64
|
||||
TotalCost float64
|
||||
|
||||
@@ -19,6 +19,7 @@ type PaymentMethodAnalyticsRequest struct {
|
||||
type PaymentMethodAnalyticsResponse struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
OutletName *string `json:"outlet_name,omitempty"`
|
||||
DateFrom time.Time `json:"date_from"`
|
||||
DateTo time.Time `json:"date_to"`
|
||||
GroupBy string `json:"group_by"`
|
||||
@@ -58,6 +59,7 @@ type SalesAnalyticsRequest struct {
|
||||
type SalesAnalyticsResponse struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
OutletName *string `json:"outlet_name,omitempty"`
|
||||
DateFrom time.Time `json:"date_from"`
|
||||
DateTo time.Time `json:"date_to"`
|
||||
GroupBy string `json:"group_by"`
|
||||
@@ -93,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
|
||||
@@ -106,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"`
|
||||
}
|
||||
@@ -138,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"`
|
||||
@@ -171,6 +188,7 @@ type ProductAnalyticsRequest struct {
|
||||
type ProductAnalyticsResponse struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
OutletName *string `json:"outlet_name,omitempty"`
|
||||
DateFrom time.Time `json:"date_from"`
|
||||
DateTo time.Time `json:"date_to"`
|
||||
Data []ProductAnalyticsData `json:"data"`
|
||||
@@ -208,6 +226,7 @@ type ProductAnalyticsPerCategoryRequest struct {
|
||||
type ProductAnalyticsPerCategoryResponse struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
OutletName *string `json:"outlet_name,omitempty"`
|
||||
DateFrom time.Time `json:"date_from"`
|
||||
DateTo time.Time `json:"date_to"`
|
||||
Data []ProductAnalyticsPerCategoryData `json:"data"`
|
||||
@@ -237,6 +256,7 @@ type DashboardAnalyticsRequest struct {
|
||||
type DashboardAnalyticsResponse struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
OutletName *string `json:"outlet_name,omitempty"`
|
||||
DateFrom time.Time `json:"date_from"`
|
||||
DateTo time.Time `json:"date_to"`
|
||||
Overview DashboardOverview `json:"overview"`
|
||||
@@ -253,6 +273,9 @@ type DashboardOverview struct {
|
||||
TotalCustomers int64 `json:"total_customers"`
|
||||
VoidedOrders int64 `json:"voided_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 {
|
||||
@@ -266,6 +289,7 @@ type ProfitLossAnalyticsRequest struct {
|
||||
type ProfitLossAnalyticsResponse struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
OutletName *string `json:"outlet_name,omitempty"`
|
||||
DateFrom time.Time `json:"date_from"`
|
||||
DateTo time.Time `json:"date_to"`
|
||||
GroupBy string `json:"group_by"`
|
||||
@@ -273,10 +297,28 @@ type ProfitLossAnalyticsResponse struct {
|
||||
Data []ProfitLossData `json:"data"`
|
||||
ProductData []ProductProfitData `json:"product_data"`
|
||||
MainSummary []ProfitLossSummaryRow `json:"main_summary"`
|
||||
Purchasing ProfitLossPurchasing `json:"purchasing"`
|
||||
OperationalExpenses []OperationalExpenseItem `json:"operational_expenses"`
|
||||
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 {
|
||||
TotalRevenue float64 `json:"total_revenue"`
|
||||
TotalCost float64 `json:"total_cost"`
|
||||
@@ -359,6 +401,7 @@ type ExclusiveSummaryMTDRequest struct {
|
||||
type ExclusiveSummaryPeriodResponse struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
OutletName *string `json:"outlet_name,omitempty"`
|
||||
Period ExclusiveSummaryPeriodRange `json:"period"`
|
||||
Summary ExclusiveSummaryPeriodSummary `json:"summary"`
|
||||
Reimburse ExclusiveSummaryReimburse `json:"reimburse"`
|
||||
@@ -418,6 +461,7 @@ type ExclusiveSummaryDailyTransaction struct {
|
||||
type ExclusiveSummaryMonthlyResponse struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
OutletName *string `json:"outlet_name,omitempty"`
|
||||
Month string `json:"month"`
|
||||
Summary ExclusiveSummaryMonthlySummary `json:"summary"`
|
||||
Periods []ExclusiveSummaryMonthlyPeriod `json:"periods"`
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/models"
|
||||
"apskel-pos-be/internal/repository"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
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) {
|
||||
if req.DateFrom.After(req.DateTo) {
|
||||
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{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
OutletName: p.resolveOutletName(ctx, req.OrganizationID, req.OutletID),
|
||||
DateFrom: req.DateFrom,
|
||||
DateTo: req.DateTo,
|
||||
GroupBy: req.GroupBy,
|
||||
@@ -164,6 +179,7 @@ func (p *AnalyticsProcessorImpl) GetSalesAnalytics(ctx context.Context, req *mod
|
||||
return &models.SalesAnalyticsResponse{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
OutletName: p.resolveOutletName(ctx, req.OrganizationID, req.OutletID),
|
||||
DateFrom: req.DateFrom,
|
||||
DateTo: req.DateTo,
|
||||
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))
|
||||
for i, item := range result.IngredientData {
|
||||
ingredientData[i] = models.PurchasingIngredientData{
|
||||
@@ -246,6 +279,7 @@ func (p *AnalyticsProcessorImpl) GetPurchasingAnalytics(ctx context.Context, req
|
||||
TotalVendors: result.Summary.TotalVendors,
|
||||
},
|
||||
Data: data,
|
||||
OutletData: outletData,
|
||||
IngredientData: ingredientData,
|
||||
VendorData: vendorData,
|
||||
}, nil
|
||||
@@ -295,6 +329,7 @@ func (p *AnalyticsProcessorImpl) GetProductAnalytics(ctx context.Context, req *m
|
||||
return &models.ProductAnalyticsResponse{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
OutletName: p.resolveOutletName(ctx, req.OrganizationID, req.OutletID),
|
||||
DateFrom: req.DateFrom,
|
||||
DateTo: req.DateTo,
|
||||
Data: resultData,
|
||||
@@ -332,6 +367,7 @@ func (p *AnalyticsProcessorImpl) GetProductAnalyticsPerCategory(ctx context.Cont
|
||||
return &models.ProductAnalyticsPerCategoryResponse{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
OutletName: p.resolveOutletName(ctx, req.OrganizationID, req.OutletID),
|
||||
DateFrom: req.DateFrom,
|
||||
DateTo: req.DateTo,
|
||||
Data: resultData,
|
||||
@@ -393,6 +429,7 @@ func (p *AnalyticsProcessorImpl) GetDashboardAnalytics(ctx context.Context, req
|
||||
return &models.DashboardAnalyticsResponse{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
OutletName: p.resolveOutletName(ctx, req.OrganizationID, req.OutletID),
|
||||
DateFrom: req.DateFrom,
|
||||
DateTo: req.DateTo,
|
||||
Overview: models.DashboardOverview{
|
||||
@@ -402,6 +439,9 @@ func (p *AnalyticsProcessorImpl) GetDashboardAnalytics(ctx context.Context, req
|
||||
TotalCustomers: overview.TotalCustomers,
|
||||
VoidedOrders: overview.VoidedOrders,
|
||||
RefundedOrders: overview.RefundedOrders,
|
||||
TotalItemSold: overview.TotalItemSold,
|
||||
TotalLowStock: overview.TotalLowStock,
|
||||
TotalProductActive: overview.TotalProductActive,
|
||||
},
|
||||
TopProducts: topProducts.Data,
|
||||
PaymentMethods: paymentMethods.Data,
|
||||
@@ -604,9 +644,20 @@ func (p *AnalyticsProcessorImpl) GetProfitLossAnalytics(ctx context.Context, req
|
||||
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{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
OutletName: p.resolveOutletName(ctx, req.OrganizationID, req.OutletID),
|
||||
DateFrom: req.DateFrom,
|
||||
DateTo: req.DateTo,
|
||||
GroupBy: req.GroupBy,
|
||||
@@ -626,6 +677,15 @@ func (p *AnalyticsProcessorImpl) GetProfitLossAnalytics(ctx context.Context, req
|
||||
Data: data,
|
||||
ProductData: productData,
|
||||
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,
|
||||
OperationalExpensesTotal: opsTotal,
|
||||
}, nil
|
||||
@@ -721,6 +781,7 @@ func (p *AnalyticsProcessorImpl) GetExclusiveSummaryMonthly(ctx context.Context,
|
||||
return &models.ExclusiveSummaryMonthlyResponse{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
OutletName: p.resolveOutletName(ctx, req.OrganizationID, req.OutletID),
|
||||
Month: monthStart.Format("2006-01"),
|
||||
Summary: models.ExclusiveSummaryMonthlySummary{
|
||||
TotalSales: fullPeriod.Summary.Sales,
|
||||
@@ -795,6 +856,7 @@ func (p *AnalyticsProcessorImpl) buildExclusiveSummaryPeriod(ctx context.Context
|
||||
return &models.ExclusiveSummaryPeriodResponse{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
OutletName: p.resolveOutletName(ctx, req.OrganizationID, req.OutletID),
|
||||
Period: models.ExclusiveSummaryPeriodRange{
|
||||
DateFrom: req.DateFrom,
|
||||
DateTo: req.DateTo,
|
||||
|
||||
@@ -68,6 +68,10 @@ func (s *analyticsRepositoryStub) GetExclusiveSummaryBankBalances(context.Contex
|
||||
return s.bankBalances, nil
|
||||
}
|
||||
|
||||
func (analyticsRepositoryStub) GetOutletName(context.Context, uuid.UUID, uuid.UUID) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
type expenseRepositoryStub struct{}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
@@ -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)
|
||||
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)
|
||||
GetOutletName(ctx context.Context, organizationID uuid.UUID, outletID uuid.UUID) (string, error)
|
||||
}
|
||||
|
||||
type AnalyticsRepositoryImpl struct {
|
||||
@@ -40,6 +41,22 @@ func (r *AnalyticsRepositoryImpl) resolveOutletID(query *gorm.DB, outletID *uuid
|
||||
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 {
|
||||
return "CASE WHEN pc.type = '" + string(entities.PurchaseCategoryTypeRawMaterial) + "' THEN COALESCE(poi.quantity, 0) * poi.amount ELSE poi.amount END"
|
||||
}
|
||||
@@ -207,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(`
|
||||
@@ -236,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).
|
||||
@@ -298,6 +349,7 @@ func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx contex
|
||||
OutletName: outletName,
|
||||
Summary: summary,
|
||||
Data: data,
|
||||
OutletData: outletData,
|
||||
IngredientData: ingredientData,
|
||||
VendorData: vendorData,
|
||||
}, nil
|
||||
@@ -318,6 +370,13 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organ
|
||||
Select(`
|
||||
p.id as product_id,
|
||||
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.name as category_name,
|
||||
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 categories c ON p.category_id = c.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",
|
||||
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").
|
||||
@@ -376,7 +436,7 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organ
|
||||
query = r.resolveOutletID(query, outletID, "o.outlet_id")
|
||||
|
||||
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").
|
||||
Limit(limit).
|
||||
Scan(&results).Error
|
||||
@@ -463,6 +523,41 @@ func (r *AnalyticsRepositoryImpl) GetDashboardOverview(ctx context.Context, orga
|
||||
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
|
||||
}
|
||||
|
||||
@@ -687,6 +782,20 @@ func (r *AnalyticsRepositoryImpl) GetProfitLossAnalytics(ctx context.Context, or
|
||||
}
|
||||
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{
|
||||
Summary: summary,
|
||||
Data: data,
|
||||
@@ -695,6 +804,13 @@ func (r *AnalyticsRepositoryImpl) GetProfitLossAnalytics(ctx context.Context, or
|
||||
TodayCost: todayRC.Cost,
|
||||
MtdRevenue: mtdRC.Revenue,
|
||||
MtdCost: mtdRC.Cost,
|
||||
TodayPurchasing: todayPurchasing.Total,
|
||||
MtdPurchasing: mtdPurchasing.Total,
|
||||
TodayPurchasingRawMaterial: todayPurchasing.RawMaterial,
|
||||
MtdPurchasingRawMaterial: mtdPurchasing.RawMaterial,
|
||||
TodayPurchasingExpense: todayPurchasing.Expense,
|
||||
MtdPurchasingExpense: mtdPurchasing.Expense,
|
||||
PurchasingItems: purchasingItems,
|
||||
TodayExpenseByCategory: todayExpenseByCategory,
|
||||
MtdExpenseByCategory: mtdExpenseByCategory,
|
||||
OperationalExpenseItems: opsItems,
|
||||
@@ -724,6 +840,68 @@ func (r *AnalyticsRepositoryImpl) getPurchaseOrderRawMaterialTotal(ctx context.C
|
||||
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) {
|
||||
var dateFormat string
|
||||
switch groupBy {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -66,6 +66,7 @@ func PaymentMethodAnalyticsModelToContract(resp *models.PaymentMethodAnalyticsRe
|
||||
return &contract.PaymentMethodAnalyticsResponse{
|
||||
OrganizationID: resp.OrganizationID,
|
||||
OutletID: resp.OutletID,
|
||||
OutletName: resp.OutletName,
|
||||
DateFrom: resp.DateFrom,
|
||||
DateTo: resp.DateTo,
|
||||
GroupBy: resp.GroupBy,
|
||||
@@ -122,6 +123,7 @@ func SalesAnalyticsModelToContract(resp *models.SalesAnalyticsResponse) *contrac
|
||||
return &contract.SalesAnalyticsResponse{
|
||||
OrganizationID: resp.OrganizationID,
|
||||
OutletID: resp.OutletID,
|
||||
OutletName: resp.OutletName,
|
||||
DateFrom: resp.DateFrom,
|
||||
DateTo: resp.DateTo,
|
||||
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))
|
||||
for i, item := range resp.IngredientData {
|
||||
ingredientData[i] = contract.PurchasingIngredientData{
|
||||
@@ -226,6 +245,7 @@ func PurchasingAnalyticsModelToContract(resp *models.PurchasingAnalyticsResponse
|
||||
TotalVendors: resp.Summary.TotalVendors,
|
||||
},
|
||||
Data: data,
|
||||
OutletData: outletData,
|
||||
IngredientData: ingredientData,
|
||||
VendorData: vendorData,
|
||||
}
|
||||
@@ -285,6 +305,7 @@ func ProductAnalyticsModelToContract(resp *models.ProductAnalyticsResponse) *con
|
||||
return &contract.ProductAnalyticsResponse{
|
||||
OrganizationID: resp.OrganizationID,
|
||||
OutletID: resp.OutletID,
|
||||
OutletName: resp.OutletName,
|
||||
DateFrom: resp.DateFrom,
|
||||
DateTo: resp.DateTo,
|
||||
Data: data,
|
||||
@@ -337,6 +358,7 @@ func ProductAnalyticsPerCategoryModelToContract(resp *models.ProductAnalyticsPer
|
||||
return &contract.ProductAnalyticsPerCategoryResponse{
|
||||
OrganizationID: resp.OrganizationID,
|
||||
OutletID: resp.OutletID,
|
||||
OutletName: resp.OutletName,
|
||||
DateFrom: resp.DateFrom,
|
||||
DateTo: resp.DateTo,
|
||||
Data: data,
|
||||
@@ -421,6 +443,7 @@ func DashboardAnalyticsModelToContract(resp *models.DashboardAnalyticsResponse)
|
||||
return &contract.DashboardAnalyticsResponse{
|
||||
OrganizationID: resp.OrganizationID,
|
||||
OutletID: resp.OutletID,
|
||||
OutletName: resp.OutletName,
|
||||
DateFrom: resp.DateFrom,
|
||||
DateTo: resp.DateTo,
|
||||
Overview: contract.DashboardOverview{
|
||||
@@ -430,6 +453,9 @@ func DashboardAnalyticsModelToContract(resp *models.DashboardAnalyticsResponse)
|
||||
TotalCustomers: resp.Overview.TotalCustomers,
|
||||
VoidedOrders: resp.Overview.VoidedOrders,
|
||||
RefundedOrders: resp.Overview.RefundedOrders,
|
||||
TotalItemSold: resp.Overview.TotalItemSold,
|
||||
TotalLowStock: resp.Overview.TotalLowStock,
|
||||
TotalProductActive: resp.Overview.TotalProductActive,
|
||||
},
|
||||
TopProducts: topProducts,
|
||||
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{
|
||||
OrganizationID: resp.OrganizationID,
|
||||
OutletID: resp.OutletID,
|
||||
OutletName: resp.OutletName,
|
||||
DateFrom: resp.DateFrom,
|
||||
DateTo: resp.DateTo,
|
||||
GroupBy: resp.GroupBy,
|
||||
@@ -538,6 +575,15 @@ func ProfitLossAnalyticsModelToContract(resp *models.ProfitLossAnalyticsResponse
|
||||
Data: data,
|
||||
ProductData: productData,
|
||||
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,
|
||||
OperationalExpensesTotal: resp.OperationalExpensesTotal,
|
||||
}
|
||||
@@ -664,6 +710,7 @@ func ExclusiveSummaryPeriodModelToContract(resp *models.ExclusiveSummaryPeriodRe
|
||||
return &contract.ExclusiveSummaryPeriodResponse{
|
||||
OrganizationID: resp.OrganizationID,
|
||||
OutletID: resp.OutletID,
|
||||
OutletName: resp.OutletName,
|
||||
Period: contract.ExclusiveSummaryPeriodRange{
|
||||
DateFrom: resp.Period.DateFrom,
|
||||
DateTo: resp.Period.DateTo,
|
||||
@@ -726,6 +773,7 @@ func ExclusiveSummaryMonthlyModelToContract(resp *models.ExclusiveSummaryMonthly
|
||||
return &contract.ExclusiveSummaryMonthlyResponse{
|
||||
OrganizationID: resp.OrganizationID,
|
||||
OutletID: resp.OutletID,
|
||||
OutletName: resp.OutletName,
|
||||
Month: resp.Month,
|
||||
Summary: contract.ExclusiveSummaryMonthlySummary{
|
||||
TotalSales: resp.Summary.TotalSales,
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user