reinstate profit loss overview
This commit is contained in:
parent
dc13bb5f93
commit
47fa21d739
@ -241,6 +241,7 @@ type ProfitLossAnalyticsRequest 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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProfitLossAnalyticsResponse struct {
|
type ProfitLossAnalyticsResponse struct {
|
||||||
@ -248,11 +249,57 @@ type ProfitLossAnalyticsResponse struct {
|
|||||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
OutletID *uuid.UUID `json:"outlet_id,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"`
|
||||||
|
Summary ProfitLossSummary `json:"summary"`
|
||||||
|
Data []ProfitLossData `json:"data"`
|
||||||
|
ProductData []ProductProfitData `json:"product_data"`
|
||||||
MainSummary []ProfitLossSummaryRow `json:"main_summary"`
|
MainSummary []ProfitLossSummaryRow `json:"main_summary"`
|
||||||
OperationalExpenses []OperationalExpenseItem `json:"operational_expenses"`
|
OperationalExpenses []OperationalExpenseItem `json:"operational_expenses"`
|
||||||
OperationalExpensesTotal float64 `json:"operational_expenses_total"`
|
OperationalExpensesTotal float64 `json:"operational_expenses_total"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProfitLossSummary struct {
|
||||||
|
TotalRevenue float64 `json:"total_revenue"`
|
||||||
|
TotalCost float64 `json:"total_cost"`
|
||||||
|
GrossProfit float64 `json:"gross_profit"`
|
||||||
|
GrossProfitMargin float64 `json:"gross_profit_margin"`
|
||||||
|
TotalTax float64 `json:"total_tax"`
|
||||||
|
TotalDiscount float64 `json:"total_discount"`
|
||||||
|
NetProfit float64 `json:"net_profit"`
|
||||||
|
NetProfitMargin float64 `json:"net_profit_margin"`
|
||||||
|
TotalOrders int64 `json:"total_orders"`
|
||||||
|
AverageProfit float64 `json:"average_profit"`
|
||||||
|
ProfitabilityRatio float64 `json:"profitability_ratio"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProfitLossData struct {
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
Revenue float64 `json:"revenue"`
|
||||||
|
Cost float64 `json:"cost"`
|
||||||
|
GrossProfit float64 `json:"gross_profit"`
|
||||||
|
GrossProfitMargin float64 `json:"gross_profit_margin"`
|
||||||
|
Tax float64 `json:"tax"`
|
||||||
|
Discount float64 `json:"discount"`
|
||||||
|
NetProfit float64 `json:"net_profit"`
|
||||||
|
NetProfitMargin float64 `json:"net_profit_margin"`
|
||||||
|
Orders int64 `json:"orders"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductProfitData struct {
|
||||||
|
ProductID uuid.UUID `json:"product_id"`
|
||||||
|
ProductName string `json:"product_name"`
|
||||||
|
CategoryID uuid.UUID `json:"category_id"`
|
||||||
|
CategoryName string `json:"category_name"`
|
||||||
|
QuantitySold int64 `json:"quantity_sold"`
|
||||||
|
Revenue float64 `json:"revenue"`
|
||||||
|
Cost float64 `json:"cost"`
|
||||||
|
GrossProfit float64 `json:"gross_profit"`
|
||||||
|
GrossProfitMargin float64 `json:"gross_profit_margin"`
|
||||||
|
AveragePrice float64 `json:"average_price"`
|
||||||
|
AverageCost float64 `json:"average_cost"`
|
||||||
|
ProfitPerUnit float64 `json:"profit_per_unit"`
|
||||||
|
}
|
||||||
|
|
||||||
type ProfitLossSummaryRow struct {
|
type ProfitLossSummaryRow struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Label string `json:"label"`
|
Label string `json:"label"`
|
||||||
|
|||||||
@ -114,6 +114,9 @@ type DashboardOverview struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ProfitLossAnalytics struct {
|
type ProfitLossAnalytics struct {
|
||||||
|
Summary ProfitLossSummary
|
||||||
|
Data []ProfitLossData
|
||||||
|
ProductData []ProductProfitData
|
||||||
TodayRevenue float64
|
TodayRevenue float64
|
||||||
TodayCost float64
|
TodayCost float64
|
||||||
MtdRevenue float64
|
MtdRevenue float64
|
||||||
@ -123,6 +126,48 @@ type ProfitLossAnalytics struct {
|
|||||||
OperationalExpenseItems []OperationalExpenseItem
|
OperationalExpenseItems []OperationalExpenseItem
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProfitLossSummary struct {
|
||||||
|
TotalRevenue float64
|
||||||
|
TotalCost float64
|
||||||
|
GrossProfit float64
|
||||||
|
GrossProfitMargin float64
|
||||||
|
TotalTax float64
|
||||||
|
TotalDiscount float64
|
||||||
|
NetProfit float64
|
||||||
|
NetProfitMargin float64
|
||||||
|
TotalOrders int64
|
||||||
|
AverageProfit float64
|
||||||
|
ProfitabilityRatio float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProfitLossData struct {
|
||||||
|
Date time.Time
|
||||||
|
Revenue float64
|
||||||
|
Cost float64
|
||||||
|
GrossProfit float64
|
||||||
|
GrossProfitMargin float64
|
||||||
|
Tax float64
|
||||||
|
Discount float64
|
||||||
|
NetProfit float64
|
||||||
|
NetProfitMargin float64
|
||||||
|
Orders int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductProfitData struct {
|
||||||
|
ProductID uuid.UUID
|
||||||
|
ProductName string
|
||||||
|
CategoryID uuid.UUID
|
||||||
|
CategoryName string
|
||||||
|
QuantitySold int64
|
||||||
|
Revenue float64
|
||||||
|
Cost float64
|
||||||
|
GrossProfit float64
|
||||||
|
GrossProfitMargin float64
|
||||||
|
AveragePrice float64
|
||||||
|
AverageCost float64
|
||||||
|
ProfitPerUnit float64
|
||||||
|
}
|
||||||
|
|
||||||
type ExpenseCategoryTotal struct {
|
type ExpenseCategoryTotal struct {
|
||||||
CategoryName string
|
CategoryName string
|
||||||
Amount float64
|
Amount float64
|
||||||
|
|||||||
@ -251,6 +251,7 @@ type ProfitLossAnalyticsRequest 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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProfitLossAnalyticsResponse struct {
|
type ProfitLossAnalyticsResponse struct {
|
||||||
@ -258,11 +259,57 @@ type ProfitLossAnalyticsResponse struct {
|
|||||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
OutletID *uuid.UUID `json:"outlet_id,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"`
|
||||||
|
Summary ProfitLossSummary `json:"summary"`
|
||||||
|
Data []ProfitLossData `json:"data"`
|
||||||
|
ProductData []ProductProfitData `json:"product_data"`
|
||||||
MainSummary []ProfitLossSummaryRow `json:"main_summary"`
|
MainSummary []ProfitLossSummaryRow `json:"main_summary"`
|
||||||
OperationalExpenses []OperationalExpenseItem `json:"operational_expenses"`
|
OperationalExpenses []OperationalExpenseItem `json:"operational_expenses"`
|
||||||
OperationalExpensesTotal float64 `json:"operational_expenses_total"`
|
OperationalExpensesTotal float64 `json:"operational_expenses_total"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProfitLossSummary struct {
|
||||||
|
TotalRevenue float64 `json:"total_revenue"`
|
||||||
|
TotalCost float64 `json:"total_cost"`
|
||||||
|
GrossProfit float64 `json:"gross_profit"`
|
||||||
|
GrossProfitMargin float64 `json:"gross_profit_margin"`
|
||||||
|
TotalTax float64 `json:"total_tax"`
|
||||||
|
TotalDiscount float64 `json:"total_discount"`
|
||||||
|
NetProfit float64 `json:"net_profit"`
|
||||||
|
NetProfitMargin float64 `json:"net_profit_margin"`
|
||||||
|
TotalOrders int64 `json:"total_orders"`
|
||||||
|
AverageProfit float64 `json:"average_profit"`
|
||||||
|
ProfitabilityRatio float64 `json:"profitability_ratio"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProfitLossData struct {
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
Revenue float64 `json:"revenue"`
|
||||||
|
Cost float64 `json:"cost"`
|
||||||
|
GrossProfit float64 `json:"gross_profit"`
|
||||||
|
GrossProfitMargin float64 `json:"gross_profit_margin"`
|
||||||
|
Tax float64 `json:"tax"`
|
||||||
|
Discount float64 `json:"discount"`
|
||||||
|
NetProfit float64 `json:"net_profit"`
|
||||||
|
NetProfitMargin float64 `json:"net_profit_margin"`
|
||||||
|
Orders int64 `json:"orders"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductProfitData struct {
|
||||||
|
ProductID uuid.UUID `json:"product_id"`
|
||||||
|
ProductName string `json:"product_name"`
|
||||||
|
CategoryID uuid.UUID `json:"category_id"`
|
||||||
|
CategoryName string `json:"category_name"`
|
||||||
|
QuantitySold int64 `json:"quantity_sold"`
|
||||||
|
Revenue float64 `json:"revenue"`
|
||||||
|
Cost float64 `json:"cost"`
|
||||||
|
GrossProfit float64 `json:"gross_profit"`
|
||||||
|
GrossProfitMargin float64 `json:"gross_profit_margin"`
|
||||||
|
AveragePrice float64 `json:"average_price"`
|
||||||
|
AverageCost float64 `json:"average_cost"`
|
||||||
|
ProfitPerUnit float64 `json:"profit_per_unit"`
|
||||||
|
}
|
||||||
|
|
||||||
type ProfitLossSummaryRow struct {
|
type ProfitLossSummaryRow struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Label string `json:"label"`
|
Label string `json:"label"`
|
||||||
|
|||||||
@ -410,11 +410,49 @@ func (p *AnalyticsProcessorImpl) GetProfitLossAnalytics(ctx context.Context, req
|
|||||||
return nil, fmt.Errorf("date_from cannot be after date_to")
|
return nil, fmt.Errorf("date_from cannot be after date_to")
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := p.analyticsRepo.GetProfitLossAnalytics(ctx, req.OrganizationID, req.OutletID, req.DateFrom, req.DateTo)
|
if req.GroupBy == "" {
|
||||||
|
req.GroupBy = "day"
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := p.analyticsRepo.GetProfitLossAnalytics(ctx, req.OrganizationID, req.OutletID, req.DateFrom, req.DateTo, req.GroupBy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get profit/loss analytics: %w", err)
|
return nil, fmt.Errorf("failed to get profit/loss analytics: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data := make([]models.ProfitLossData, len(result.Data))
|
||||||
|
for i, item := range result.Data {
|
||||||
|
data[i] = models.ProfitLossData{
|
||||||
|
Date: item.Date,
|
||||||
|
Revenue: item.Revenue,
|
||||||
|
Cost: item.Cost,
|
||||||
|
GrossProfit: item.GrossProfit,
|
||||||
|
GrossProfitMargin: item.GrossProfitMargin,
|
||||||
|
Tax: item.Tax,
|
||||||
|
Discount: item.Discount,
|
||||||
|
NetProfit: item.NetProfit,
|
||||||
|
NetProfitMargin: item.NetProfitMargin,
|
||||||
|
Orders: item.Orders,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
productData := make([]models.ProductProfitData, len(result.ProductData))
|
||||||
|
for i, item := range result.ProductData {
|
||||||
|
productData[i] = models.ProductProfitData{
|
||||||
|
ProductID: item.ProductID,
|
||||||
|
ProductName: item.ProductName,
|
||||||
|
CategoryID: item.CategoryID,
|
||||||
|
CategoryName: item.CategoryName,
|
||||||
|
QuantitySold: item.QuantitySold,
|
||||||
|
Revenue: item.Revenue,
|
||||||
|
Cost: item.Cost,
|
||||||
|
GrossProfit: item.GrossProfit,
|
||||||
|
GrossProfitMargin: item.GrossProfitMargin,
|
||||||
|
AveragePrice: item.AveragePrice,
|
||||||
|
AverageCost: item.AverageCost,
|
||||||
|
ProfitPerUnit: item.ProfitPerUnit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
todayPromosi := getExpenseAmountByCategory(result.TodayExpenseByCategory, "promosi")
|
todayPromosi := getExpenseAmountByCategory(result.TodayExpenseByCategory, "promosi")
|
||||||
todayLainLain := getExpenseAmountByCategory(result.TodayExpenseByCategory, "lain")
|
todayLainLain := getExpenseAmountByCategory(result.TodayExpenseByCategory, "lain")
|
||||||
todayTotalOps := todayPromosi + todayLainLain
|
todayTotalOps := todayPromosi + todayLainLain
|
||||||
@ -517,6 +555,22 @@ func (p *AnalyticsProcessorImpl) GetProfitLossAnalytics(ctx context.Context, req
|
|||||||
OutletID: req.OutletID,
|
OutletID: req.OutletID,
|
||||||
DateFrom: req.DateFrom,
|
DateFrom: req.DateFrom,
|
||||||
DateTo: req.DateTo,
|
DateTo: req.DateTo,
|
||||||
|
GroupBy: req.GroupBy,
|
||||||
|
Summary: models.ProfitLossSummary{
|
||||||
|
TotalRevenue: result.Summary.TotalRevenue,
|
||||||
|
TotalCost: result.Summary.TotalCost,
|
||||||
|
GrossProfit: result.Summary.GrossProfit,
|
||||||
|
GrossProfitMargin: result.Summary.GrossProfitMargin,
|
||||||
|
TotalTax: result.Summary.TotalTax,
|
||||||
|
TotalDiscount: result.Summary.TotalDiscount,
|
||||||
|
NetProfit: result.Summary.NetProfit,
|
||||||
|
NetProfitMargin: result.Summary.NetProfitMargin,
|
||||||
|
TotalOrders: result.Summary.TotalOrders,
|
||||||
|
AverageProfit: result.Summary.AverageProfit,
|
||||||
|
ProfitabilityRatio: result.Summary.ProfitabilityRatio,
|
||||||
|
},
|
||||||
|
Data: data,
|
||||||
|
ProductData: productData,
|
||||||
MainSummary: mainSummary,
|
MainSummary: mainSummary,
|
||||||
OperationalExpenses: opsItems,
|
OperationalExpenses: opsItems,
|
||||||
OperationalExpensesTotal: opsTotal,
|
OperationalExpensesTotal: opsTotal,
|
||||||
|
|||||||
@ -14,6 +14,8 @@ import (
|
|||||||
|
|
||||||
type analyticsRepositoryStub struct {
|
type analyticsRepositoryStub struct {
|
||||||
purchasingResult *entities.PurchasingAnalytics
|
purchasingResult *entities.PurchasingAnalytics
|
||||||
|
profitLossResult *entities.ProfitLossAnalytics
|
||||||
|
profitLossGroup string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (analyticsRepositoryStub) GetPaymentMethodAnalytics(context.Context, uuid.UUID, *uuid.UUID, time.Time, time.Time) ([]*entities.PaymentMethodAnalytics, error) {
|
func (analyticsRepositoryStub) GetPaymentMethodAnalytics(context.Context, uuid.UUID, *uuid.UUID, time.Time, time.Time) ([]*entities.PaymentMethodAnalytics, error) {
|
||||||
@ -40,8 +42,9 @@ func (analyticsRepositoryStub) GetDashboardOverview(context.Context, uuid.UUID,
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (analyticsRepositoryStub) GetProfitLossAnalytics(context.Context, uuid.UUID, *uuid.UUID, time.Time, time.Time) (*entities.ProfitLossAnalytics, error) {
|
func (s analyticsRepositoryStub) GetProfitLossAnalytics(_ context.Context, _ uuid.UUID, _ *uuid.UUID, _, _ time.Time, groupBy string) (*entities.ProfitLossAnalytics, error) {
|
||||||
return nil, nil
|
s.profitLossGroup = groupBy
|
||||||
|
return s.profitLossResult, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type expenseRepositoryStub struct{}
|
type expenseRepositoryStub struct{}
|
||||||
@ -88,3 +91,77 @@ func TestAnalyticsProcessorGetPurchasingAnalyticsPassesOutletName(t *testing.T)
|
|||||||
require.Equal(t, outletName, *result.OutletName)
|
require.Equal(t, outletName, *result.OutletName)
|
||||||
require.Equal(t, float64(125), result.Summary.TotalPurchases)
|
require.Equal(t, float64(125), result.Summary.TotalPurchases)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAnalyticsProcessorGetProfitLossAnalyticsMapsOverviewAndReportFields(t *testing.T) {
|
||||||
|
productID := uuid.New()
|
||||||
|
categoryID := uuid.New()
|
||||||
|
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
processor := NewAnalyticsProcessorImpl(analyticsRepositoryStub{
|
||||||
|
profitLossResult: &entities.ProfitLossAnalytics{
|
||||||
|
Summary: entities.ProfitLossSummary{
|
||||||
|
TotalRevenue: 1000,
|
||||||
|
TotalCost: 400,
|
||||||
|
GrossProfit: 600,
|
||||||
|
GrossProfitMargin: 60,
|
||||||
|
TotalTax: 50,
|
||||||
|
TotalDiscount: 25,
|
||||||
|
NetProfit: 575,
|
||||||
|
NetProfitMargin: 57.5,
|
||||||
|
TotalOrders: 10,
|
||||||
|
AverageProfit: 57.5,
|
||||||
|
ProfitabilityRatio: 150,
|
||||||
|
},
|
||||||
|
Data: []entities.ProfitLossData{
|
||||||
|
{
|
||||||
|
Date: now,
|
||||||
|
Revenue: 1000,
|
||||||
|
Cost: 400,
|
||||||
|
GrossProfit: 600,
|
||||||
|
GrossProfitMargin: 60,
|
||||||
|
Tax: 50,
|
||||||
|
Discount: 25,
|
||||||
|
NetProfit: 575,
|
||||||
|
NetProfitMargin: 57.5,
|
||||||
|
Orders: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ProductData: []entities.ProductProfitData{
|
||||||
|
{
|
||||||
|
ProductID: productID,
|
||||||
|
ProductName: "Nasi",
|
||||||
|
CategoryID: categoryID,
|
||||||
|
CategoryName: "Food",
|
||||||
|
QuantitySold: 5,
|
||||||
|
Revenue: 500,
|
||||||
|
Cost: 200,
|
||||||
|
GrossProfit: 300,
|
||||||
|
GrossProfitMargin: 60,
|
||||||
|
AveragePrice: 100,
|
||||||
|
AverageCost: 40,
|
||||||
|
ProfitPerUnit: 60,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TodayRevenue: 1000,
|
||||||
|
TodayCost: 400,
|
||||||
|
MtdRevenue: 2000,
|
||||||
|
MtdCost: 800,
|
||||||
|
},
|
||||||
|
}, expenseRepositoryStub{})
|
||||||
|
|
||||||
|
result, err := processor.GetProfitLossAnalytics(context.Background(), &models.ProfitLossAnalyticsRequest{
|
||||||
|
OrganizationID: uuid.New(),
|
||||||
|
DateFrom: now,
|
||||||
|
DateTo: now,
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, result)
|
||||||
|
require.Equal(t, "day", result.GroupBy)
|
||||||
|
require.Equal(t, float64(1000), result.Summary.TotalRevenue)
|
||||||
|
require.Len(t, result.Data, 1)
|
||||||
|
require.Equal(t, float64(575), result.Data[0].NetProfit)
|
||||||
|
require.Len(t, result.ProductData, 1)
|
||||||
|
require.Equal(t, productID, result.ProductData[0].ProductID)
|
||||||
|
require.NotEmpty(t, result.MainSummary)
|
||||||
|
require.Equal(t, "total_omset", result.MainSummary[0].ID)
|
||||||
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ type AnalyticsRepository interface {
|
|||||||
GetProductAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, limit int) ([]*entities.ProductAnalytics, error)
|
GetProductAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, limit int) ([]*entities.ProductAnalytics, error)
|
||||||
GetProductAnalyticsPerCategory(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) ([]*entities.ProductAnalyticsPerCategory, error)
|
GetProductAnalyticsPerCategory(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) ([]*entities.ProductAnalyticsPerCategory, error)
|
||||||
GetDashboardOverview(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) (*entities.DashboardOverview, error)
|
GetDashboardOverview(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) (*entities.DashboardOverview, error)
|
||||||
GetProfitLossAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) (*entities.ProfitLossAnalytics, error)
|
GetProfitLossAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, groupBy string) (*entities.ProfitLossAnalytics, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type AnalyticsRepositoryImpl struct {
|
type AnalyticsRepositoryImpl struct {
|
||||||
@ -432,11 +432,138 @@ func (r *AnalyticsRepositoryImpl) GetDashboardOverview(ctx context.Context, orga
|
|||||||
return &result, nil
|
return &result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *AnalyticsRepositoryImpl) GetProfitLossAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) (*entities.ProfitLossAnalytics, error) {
|
func (r *AnalyticsRepositoryImpl) GetProfitLossAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, groupBy string) (*entities.ProfitLossAnalytics, error) {
|
||||||
mtdStart := time.Date(dateTo.Year(), dateTo.Month(), 1, 0, 0, 0, 0, dateTo.Location())
|
mtdStart := time.Date(dateTo.Year(), dateTo.Month(), 1, 0, 0, 0, 0, dateTo.Location())
|
||||||
todayStart := time.Date(dateTo.Year(), dateTo.Month(), dateTo.Day(), 0, 0, 0, 0, dateTo.Location())
|
todayStart := time.Date(dateTo.Year(), dateTo.Month(), dateTo.Day(), 0, 0, 0, 0, dateTo.Location())
|
||||||
todayEnd := todayStart.Add(24 * time.Hour).Add(-time.Nanosecond)
|
todayEnd := todayStart.Add(24 * time.Hour).Add(-time.Nanosecond)
|
||||||
|
|
||||||
|
var summary entities.ProfitLossSummary
|
||||||
|
summaryQuery := r.db.WithContext(ctx).
|
||||||
|
Table("orders o").
|
||||||
|
Select(`
|
||||||
|
COALESCE(SUM(o.total_amount), 0) as total_revenue,
|
||||||
|
COALESCE(SUM(o.total_cost), 0) as total_cost,
|
||||||
|
COALESCE(SUM(o.total_amount - o.total_cost), 0) as gross_profit,
|
||||||
|
CASE
|
||||||
|
WHEN SUM(o.total_amount) > 0
|
||||||
|
THEN (SUM(o.total_amount - o.total_cost) / SUM(o.total_amount)) * 100
|
||||||
|
ELSE 0
|
||||||
|
END as gross_profit_margin,
|
||||||
|
COALESCE(SUM(o.tax_amount), 0) as total_tax,
|
||||||
|
COALESCE(SUM(o.discount_amount), 0) as total_discount,
|
||||||
|
COALESCE(SUM(o.total_amount - o.total_cost - o.discount_amount), 0) as net_profit,
|
||||||
|
CASE
|
||||||
|
WHEN SUM(o.total_amount) > 0
|
||||||
|
THEN (SUM(o.total_amount - o.total_cost - o.discount_amount) / SUM(o.total_amount)) * 100
|
||||||
|
ELSE 0
|
||||||
|
END as net_profit_margin,
|
||||||
|
COUNT(o.id) as total_orders,
|
||||||
|
CASE
|
||||||
|
WHEN COUNT(o.id) > 0
|
||||||
|
THEN SUM(o.total_amount - o.total_cost - o.discount_amount) / COUNT(o.id)
|
||||||
|
ELSE 0
|
||||||
|
END as average_profit,
|
||||||
|
CASE
|
||||||
|
WHEN SUM(o.total_cost) > 0
|
||||||
|
THEN (SUM(o.total_amount - o.total_cost) / SUM(o.total_cost)) * 100
|
||||||
|
ELSE 0
|
||||||
|
END as profitability_ratio
|
||||||
|
`).
|
||||||
|
Where("o.organization_id = ?", organizationID).
|
||||||
|
Where("o.status = ?", entities.OrderStatusCompleted).
|
||||||
|
Where("o.payment_status = ?", entities.PaymentStatusCompleted).
|
||||||
|
Where("o.is_void = false AND o.is_refund = false").
|
||||||
|
Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo)
|
||||||
|
summaryQuery = r.resolveOutletID(summaryQuery, outletID, "o.outlet_id")
|
||||||
|
if err := summaryQuery.Scan(&summary).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var timeFormat string
|
||||||
|
switch groupBy {
|
||||||
|
case "hour":
|
||||||
|
timeFormat = "DATE_TRUNC('hour', o.created_at)"
|
||||||
|
case "week":
|
||||||
|
timeFormat = "DATE_TRUNC('week', o.created_at)"
|
||||||
|
case "month":
|
||||||
|
timeFormat = "DATE_TRUNC('month', o.created_at)"
|
||||||
|
default:
|
||||||
|
timeFormat = "DATE_TRUNC('day', o.created_at)"
|
||||||
|
}
|
||||||
|
|
||||||
|
var data []entities.ProfitLossData
|
||||||
|
dataQuery := r.db.WithContext(ctx).
|
||||||
|
Table("orders o").
|
||||||
|
Select(`
|
||||||
|
`+timeFormat+` as date,
|
||||||
|
COALESCE(SUM(o.total_amount), 0) as revenue,
|
||||||
|
COALESCE(SUM(o.total_cost), 0) as cost,
|
||||||
|
COALESCE(SUM(o.total_amount - o.total_cost), 0) as gross_profit,
|
||||||
|
CASE
|
||||||
|
WHEN SUM(o.total_amount) > 0
|
||||||
|
THEN (SUM(o.total_amount - o.total_cost) / SUM(o.total_amount)) * 100
|
||||||
|
ELSE 0
|
||||||
|
END as gross_profit_margin,
|
||||||
|
COALESCE(SUM(o.tax_amount), 0) as tax,
|
||||||
|
COALESCE(SUM(o.discount_amount), 0) as discount,
|
||||||
|
COALESCE(SUM(o.total_amount - o.total_cost - o.discount_amount), 0) as net_profit,
|
||||||
|
CASE
|
||||||
|
WHEN SUM(o.total_amount) > 0
|
||||||
|
THEN (SUM(o.total_amount - o.total_cost - o.discount_amount) / SUM(o.total_amount)) * 100
|
||||||
|
ELSE 0
|
||||||
|
END as net_profit_margin,
|
||||||
|
COUNT(o.id) as orders
|
||||||
|
`).
|
||||||
|
Where("o.organization_id = ?", organizationID).
|
||||||
|
Where("o.status = ?", entities.OrderStatusCompleted).
|
||||||
|
Where("o.payment_status = ?", entities.PaymentStatusCompleted).
|
||||||
|
Where("o.is_void = false AND o.is_refund = false").
|
||||||
|
Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo).
|
||||||
|
Group(timeFormat).
|
||||||
|
Order(timeFormat)
|
||||||
|
dataQuery = r.resolveOutletID(dataQuery, outletID, "o.outlet_id")
|
||||||
|
if err := dataQuery.Scan(&data).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var productData []entities.ProductProfitData
|
||||||
|
productQuery := r.db.WithContext(ctx).
|
||||||
|
Table("order_items oi").
|
||||||
|
Select(`
|
||||||
|
p.id as product_id,
|
||||||
|
p.name as product_name,
|
||||||
|
c.id as category_id,
|
||||||
|
c.name as category_name,
|
||||||
|
SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.quantity - COALESCE(oi.refund_quantity, 0) ELSE 0 END) as quantity_sold,
|
||||||
|
SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_price - COALESCE(oi.refund_amount, 0) ELSE 0 END) as revenue,
|
||||||
|
SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_cost * ((oi.quantity - COALESCE(oi.refund_quantity, 0))::float / NULLIF(oi.quantity, 0)) ELSE 0 END) as cost,
|
||||||
|
SUM(CASE WHEN oi.is_fully_refunded = false THEN (oi.total_price - COALESCE(oi.refund_amount, 0)) - (oi.total_cost * ((oi.quantity - COALESCE(oi.refund_quantity, 0))::float / NULLIF(oi.quantity, 0))) ELSE 0 END) as gross_profit,
|
||||||
|
CASE
|
||||||
|
WHEN SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_price - COALESCE(oi.refund_amount, 0) ELSE 0 END) > 0
|
||||||
|
THEN (SUM(CASE WHEN oi.is_fully_refunded = false THEN (oi.total_price - COALESCE(oi.refund_amount, 0)) - (oi.total_cost * ((oi.quantity - COALESCE(oi.refund_quantity, 0))::float / NULLIF(oi.quantity, 0))) ELSE 0 END) / SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_price - COALESCE(oi.refund_amount, 0) ELSE 0 END)) * 100
|
||||||
|
ELSE 0
|
||||||
|
END as gross_profit_margin,
|
||||||
|
AVG(CASE WHEN oi.is_fully_refunded = false THEN oi.unit_price ELSE NULL END) as average_price,
|
||||||
|
AVG(CASE WHEN oi.is_fully_refunded = false THEN oi.unit_cost ELSE NULL END) as average_cost,
|
||||||
|
AVG(CASE WHEN oi.is_fully_refunded = false THEN oi.unit_price - oi.unit_cost ELSE NULL END) as profit_per_unit
|
||||||
|
`).
|
||||||
|
Joins("JOIN orders o ON oi.order_id = o.id").
|
||||||
|
Joins("JOIN products p ON oi.product_id = p.id").
|
||||||
|
Joins("JOIN categories c ON p.category_id = c.id").
|
||||||
|
Where("o.organization_id = ?", organizationID).
|
||||||
|
Where("o.status = ?", entities.OrderStatusCompleted).
|
||||||
|
Where("o.payment_status = ?", entities.PaymentStatusCompleted).
|
||||||
|
Where("o.is_void = false AND o.is_refund = false").
|
||||||
|
Where("oi.status != ?", entities.OrderItemStatusCancelled).
|
||||||
|
Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo).
|
||||||
|
Group("p.id, p.name, c.id, c.name").
|
||||||
|
Order("p.name ASC").
|
||||||
|
Limit(1000)
|
||||||
|
productQuery = r.resolveOutletID(productQuery, outletID, "o.outlet_id")
|
||||||
|
if err := productQuery.Scan(&productData).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
type revenueCostResult struct {
|
type revenueCostResult struct {
|
||||||
Revenue float64
|
Revenue float64
|
||||||
Cost float64
|
Cost float64
|
||||||
@ -492,6 +619,9 @@ func (r *AnalyticsRepositoryImpl) GetProfitLossAnalytics(ctx context.Context, or
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &entities.ProfitLossAnalytics{
|
return &entities.ProfitLossAnalytics{
|
||||||
|
Summary: summary,
|
||||||
|
Data: data,
|
||||||
|
ProductData: productData,
|
||||||
TodayRevenue: todayRC.Revenue,
|
TodayRevenue: todayRC.Revenue,
|
||||||
TodayCost: todayRC.Cost,
|
TodayCost: todayRC.Cost,
|
||||||
MtdRevenue: mtdRC.Revenue,
|
MtdRevenue: mtdRC.Revenue,
|
||||||
|
|||||||
@ -134,18 +134,6 @@ func (s *AnalyticsServiceImpl) validatePaymentMethodAnalyticsRequest(req *models
|
|||||||
return fmt.Errorf("date_from cannot be after date_to")
|
return fmt.Errorf("date_from cannot be after date_to")
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.GroupBy != "" {
|
|
||||||
validGroupBy := map[string]bool{
|
|
||||||
"day": true,
|
|
||||||
"hour": true,
|
|
||||||
"week": true,
|
|
||||||
"month": true,
|
|
||||||
}
|
|
||||||
if !validGroupBy[req.GroupBy] {
|
|
||||||
return fmt.Errorf("invalid group_by value: %s", req.GroupBy)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -318,5 +306,17 @@ func (s *AnalyticsServiceImpl) validateProfitLossAnalyticsRequest(req *models.Pr
|
|||||||
return fmt.Errorf("date_from cannot be after date_to")
|
return fmt.Errorf("date_from cannot be after date_to")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.GroupBy != "" {
|
||||||
|
validGroupBy := map[string]bool{
|
||||||
|
"day": true,
|
||||||
|
"hour": true,
|
||||||
|
"week": true,
|
||||||
|
"month": true,
|
||||||
|
}
|
||||||
|
if !validGroupBy[req.GroupBy] {
|
||||||
|
return fmt.Errorf("invalid group_by value: %s", req.GroupBy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -154,6 +154,16 @@ func TestAnalyticsServiceGetProfitLossAnalyticsValidation(t *testing.T) {
|
|||||||
},
|
},
|
||||||
wantErr: "date_from cannot be after date_to",
|
wantErr: "date_from cannot be after date_to",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "invalid group_by",
|
||||||
|
req: &models.ProfitLossAnalyticsRequest{
|
||||||
|
OrganizationID: uuid.New(),
|
||||||
|
DateFrom: now,
|
||||||
|
DateTo: now,
|
||||||
|
GroupBy: "quarter",
|
||||||
|
},
|
||||||
|
wantErr: "invalid group_by value: quarter",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@ -166,3 +176,17 @@ func TestAnalyticsServiceGetProfitLossAnalyticsValidation(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAnalyticsServiceGetProfitLossAnalyticsAllowsEmptyGroupBy(t *testing.T) {
|
||||||
|
service := NewAnalyticsServiceImpl(analyticsProcessorStub{})
|
||||||
|
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
resp, err := service.GetProfitLossAnalytics(context.Background(), &models.ProfitLossAnalyticsRequest{
|
||||||
|
OrganizationID: uuid.New(),
|
||||||
|
DateFrom: now,
|
||||||
|
DateTo: now,
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Nil(t, resp)
|
||||||
|
}
|
||||||
|
|||||||
@ -450,6 +450,7 @@ func ProfitLossAnalyticsContractToModel(req *contract.ProfitLossAnalyticsRequest
|
|||||||
OutletID: parseOutletID(req.OutletID),
|
OutletID: parseOutletID(req.OutletID),
|
||||||
DateFrom: *dateFrom,
|
DateFrom: *dateFrom,
|
||||||
DateTo: *dateTo,
|
DateTo: *dateTo,
|
||||||
|
GroupBy: req.GroupBy,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -463,6 +464,40 @@ func ProfitLossAnalyticsModelToContract(resp *models.ProfitLossAnalyticsResponse
|
|||||||
mainSummary[i] = profitLossSummaryRowModelToContract(row)
|
mainSummary[i] = profitLossSummaryRowModelToContract(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data := make([]contract.ProfitLossData, len(resp.Data))
|
||||||
|
for i, item := range resp.Data {
|
||||||
|
data[i] = contract.ProfitLossData{
|
||||||
|
Date: item.Date,
|
||||||
|
Revenue: item.Revenue,
|
||||||
|
Cost: item.Cost,
|
||||||
|
GrossProfit: item.GrossProfit,
|
||||||
|
GrossProfitMargin: item.GrossProfitMargin,
|
||||||
|
Tax: item.Tax,
|
||||||
|
Discount: item.Discount,
|
||||||
|
NetProfit: item.NetProfit,
|
||||||
|
NetProfitMargin: item.NetProfitMargin,
|
||||||
|
Orders: item.Orders,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
productData := make([]contract.ProductProfitData, len(resp.ProductData))
|
||||||
|
for i, item := range resp.ProductData {
|
||||||
|
productData[i] = contract.ProductProfitData{
|
||||||
|
ProductID: item.ProductID,
|
||||||
|
ProductName: item.ProductName,
|
||||||
|
CategoryID: item.CategoryID,
|
||||||
|
CategoryName: item.CategoryName,
|
||||||
|
QuantitySold: item.QuantitySold,
|
||||||
|
Revenue: item.Revenue,
|
||||||
|
Cost: item.Cost,
|
||||||
|
GrossProfit: item.GrossProfit,
|
||||||
|
GrossProfitMargin: item.GrossProfitMargin,
|
||||||
|
AveragePrice: item.AveragePrice,
|
||||||
|
AverageCost: item.AverageCost,
|
||||||
|
ProfitPerUnit: item.ProfitPerUnit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
opsItems := make([]contract.OperationalExpenseItem, len(resp.OperationalExpenses))
|
opsItems := make([]contract.OperationalExpenseItem, len(resp.OperationalExpenses))
|
||||||
for i, item := range resp.OperationalExpenses {
|
for i, item := range resp.OperationalExpenses {
|
||||||
opsItems[i] = contract.OperationalExpenseItem{
|
opsItems[i] = contract.OperationalExpenseItem{
|
||||||
@ -476,6 +511,22 @@ func ProfitLossAnalyticsModelToContract(resp *models.ProfitLossAnalyticsResponse
|
|||||||
OutletID: resp.OutletID,
|
OutletID: resp.OutletID,
|
||||||
DateFrom: resp.DateFrom,
|
DateFrom: resp.DateFrom,
|
||||||
DateTo: resp.DateTo,
|
DateTo: resp.DateTo,
|
||||||
|
GroupBy: resp.GroupBy,
|
||||||
|
Summary: contract.ProfitLossSummary{
|
||||||
|
TotalRevenue: resp.Summary.TotalRevenue,
|
||||||
|
TotalCost: resp.Summary.TotalCost,
|
||||||
|
GrossProfit: resp.Summary.GrossProfit,
|
||||||
|
GrossProfitMargin: resp.Summary.GrossProfitMargin,
|
||||||
|
TotalTax: resp.Summary.TotalTax,
|
||||||
|
TotalDiscount: resp.Summary.TotalDiscount,
|
||||||
|
NetProfit: resp.Summary.NetProfit,
|
||||||
|
NetProfitMargin: resp.Summary.NetProfitMargin,
|
||||||
|
TotalOrders: resp.Summary.TotalOrders,
|
||||||
|
AverageProfit: resp.Summary.AverageProfit,
|
||||||
|
ProfitabilityRatio: resp.Summary.ProfitabilityRatio,
|
||||||
|
},
|
||||||
|
Data: data,
|
||||||
|
ProductData: productData,
|
||||||
MainSummary: mainSummary,
|
MainSummary: mainSummary,
|
||||||
OperationalExpenses: opsItems,
|
OperationalExpenses: opsItems,
|
||||||
OperationalExpensesTotal: resp.OperationalExpensesTotal,
|
OperationalExpensesTotal: resp.OperationalExpensesTotal,
|
||||||
|
|||||||
@ -84,12 +84,14 @@ func TestProfitLossAnalyticsContractToModelParsesDateRange(t *testing.T) {
|
|||||||
OutletID: &outletID,
|
OutletID: &outletID,
|
||||||
DateFrom: "01-05-2026",
|
DateFrom: "01-05-2026",
|
||||||
DateTo: "29-05-2026",
|
DateTo: "29-05-2026",
|
||||||
|
GroupBy: "week",
|
||||||
})
|
})
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, orgID, result.OrganizationID)
|
require.Equal(t, orgID, result.OrganizationID)
|
||||||
require.NotNil(t, result.OutletID)
|
require.NotNil(t, result.OutletID)
|
||||||
require.Equal(t, outletID, result.OutletID.String())
|
require.Equal(t, outletID, result.OutletID.String())
|
||||||
|
require.Equal(t, "week", result.GroupBy)
|
||||||
|
|
||||||
location, err := time.LoadLocation("Asia/Jakarta")
|
location, err := time.LoadLocation("Asia/Jakarta")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -100,14 +102,53 @@ func TestProfitLossAnalyticsContractToModelParsesDateRange(t *testing.T) {
|
|||||||
func TestProfitLossAnalyticsModelToContractCopiesDateRange(t *testing.T) {
|
func TestProfitLossAnalyticsModelToContractCopiesDateRange(t *testing.T) {
|
||||||
dateFrom := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
dateFrom := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
||||||
dateTo := time.Date(2026, 5, 29, 23, 59, 59, int(time.Second-time.Nanosecond), time.UTC)
|
dateTo := time.Date(2026, 5, 29, 23, 59, 59, int(time.Second-time.Nanosecond), time.UTC)
|
||||||
|
productID := uuid.New()
|
||||||
|
categoryID := uuid.New()
|
||||||
|
|
||||||
result := ProfitLossAnalyticsModelToContract(&models.ProfitLossAnalyticsResponse{
|
result := ProfitLossAnalyticsModelToContract(&models.ProfitLossAnalyticsResponse{
|
||||||
OrganizationID: uuid.New(),
|
OrganizationID: uuid.New(),
|
||||||
DateFrom: dateFrom,
|
DateFrom: dateFrom,
|
||||||
DateTo: dateTo,
|
DateTo: dateTo,
|
||||||
|
GroupBy: "month",
|
||||||
|
Summary: models.ProfitLossSummary{
|
||||||
|
TotalRevenue: 1000,
|
||||||
|
NetProfit: 500,
|
||||||
|
},
|
||||||
|
Data: []models.ProfitLossData{
|
||||||
|
{
|
||||||
|
Date: dateFrom,
|
||||||
|
Revenue: 1000,
|
||||||
|
NetProfit: 500,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ProductData: []models.ProductProfitData{
|
||||||
|
{
|
||||||
|
ProductID: productID,
|
||||||
|
ProductName: "Nasi",
|
||||||
|
CategoryID: categoryID,
|
||||||
|
CategoryName: "Food",
|
||||||
|
Revenue: 1000,
|
||||||
|
GrossProfit: 500,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MainSummary: []models.ProfitLossSummaryRow{
|
||||||
|
{
|
||||||
|
ID: "total_omset",
|
||||||
|
Label: "TOTAL OMSET",
|
||||||
|
TodayNominal: 1000,
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
require.NotNil(t, result)
|
require.NotNil(t, result)
|
||||||
require.Equal(t, dateFrom, result.DateFrom)
|
require.Equal(t, dateFrom, result.DateFrom)
|
||||||
require.Equal(t, dateTo, result.DateTo)
|
require.Equal(t, dateTo, result.DateTo)
|
||||||
|
require.Equal(t, "month", result.GroupBy)
|
||||||
|
require.Equal(t, float64(1000), result.Summary.TotalRevenue)
|
||||||
|
require.Len(t, result.Data, 1)
|
||||||
|
require.Equal(t, float64(500), result.Data[0].NetProfit)
|
||||||
|
require.Len(t, result.ProductData, 1)
|
||||||
|
require.Equal(t, productID, result.ProductData[0].ProductID)
|
||||||
|
require.Len(t, result.MainSummary, 1)
|
||||||
|
require.Equal(t, "total_omset", result.MainSummary[0].ID)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user