diff --git a/internal/contract/analytics_contract.go b/internal/contract/analytics_contract.go index 1da3659..f02570b 100644 --- a/internal/contract/analytics_contract.go +++ b/internal/contract/analytics_contract.go @@ -272,10 +272,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"` diff --git a/internal/entities/analytics.go b/internal/entities/analytics.go index bfcf8b7..f911024 100644 --- a/internal/entities/analytics.go +++ b/internal/entities/analytics.go @@ -126,16 +126,30 @@ type DashboardOverview struct { } type ProfitLossAnalytics struct { - Summary ProfitLossSummary - Data []ProfitLossData - ProductData []ProductProfitData - TodayRevenue float64 - TodayCost float64 - MtdRevenue float64 - MtdCost float64 - TodayExpenseByCategory []ExpenseCategoryTotal - MtdExpenseByCategory []ExpenseCategoryTotal - OperationalExpenseItems []OperationalExpenseItem + Summary ProfitLossSummary + Data []ProfitLossData + ProductData []ProductProfitData + TodayRevenue float64 + 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 { diff --git a/internal/models/analytics.go b/internal/models/analytics.go index 446ec92..9e32fd7 100644 --- a/internal/models/analytics.go +++ b/internal/models/analytics.go @@ -282,10 +282,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"` diff --git a/internal/processor/analytics_processor.go b/internal/processor/analytics_processor.go index 20e758c..765ba5a 100644 --- a/internal/processor/analytics_processor.go +++ b/internal/processor/analytics_processor.go @@ -626,6 +626,16 @@ 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, @@ -646,9 +656,18 @@ func (p *AnalyticsProcessorImpl) GetProfitLossAnalytics(ctx context.Context, req AverageProfit: result.Summary.AverageProfit, ProfitabilityRatio: result.Summary.ProfitabilityRatio, }, - Data: data, - ProductData: productData, - MainSummary: mainSummary, + 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 diff --git a/internal/processor/analytics_processor_test.go b/internal/processor/analytics_processor_test.go index 52b5c9e..23d4967 100644 --- a/internal/processor/analytics_processor_test.go +++ b/internal/processor/analytics_processor_test.go @@ -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 } diff --git a/internal/repository/analytics_repository.go b/internal/repository/analytics_repository.go index 66deaa2..cfa15a0 100644 --- a/internal/repository/analytics_repository.go +++ b/internal/repository/analytics_repository.go @@ -747,17 +747,38 @@ 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, - ProductData: productData, - TodayRevenue: todayRC.Revenue, - TodayCost: todayRC.Cost, - MtdRevenue: mtdRC.Revenue, - MtdCost: mtdRC.Cost, - TodayExpenseByCategory: todayExpenseByCategory, - MtdExpenseByCategory: mtdExpenseByCategory, - OperationalExpenseItems: opsItems, + Summary: summary, + Data: data, + ProductData: productData, + TodayRevenue: todayRC.Revenue, + 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, }, nil } @@ -784,6 +805,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 { diff --git a/internal/transformer/analytics_transformer.go b/internal/transformer/analytics_transformer.go index f4b033e..69594a8 100644 --- a/internal/transformer/analytics_transformer.go +++ b/internal/transformer/analytics_transformer.go @@ -524,6 +524,16 @@ 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, @@ -544,9 +554,18 @@ func ProfitLossAnalyticsModelToContract(resp *models.ProfitLossAnalyticsResponse AverageProfit: resp.Summary.AverageProfit, ProfitabilityRatio: resp.Summary.ProfitabilityRatio, }, - Data: data, - ProductData: productData, - MainSummary: mainSummary, + 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, }