Compare commits
No commits in common. "a55a3f4ee2be0d0c8a37e7816f061daa1dfffe8a" and "b8be29e1103de1bdf5284a9edb8865e85f700468" have entirely different histories.
a55a3f4ee2
...
b8be29e110
@ -359,7 +359,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
|||||||
paymentMethodProcessor: processor.NewPaymentMethodProcessorImpl(repos.paymentMethodRepo),
|
paymentMethodProcessor: processor.NewPaymentMethodProcessorImpl(repos.paymentMethodRepo),
|
||||||
fileProcessor: processor.NewFileProcessorImpl(repos.fileRepo, fileClient),
|
fileProcessor: processor.NewFileProcessorImpl(repos.fileRepo, fileClient),
|
||||||
customerProcessor: processor.NewCustomerProcessor(repos.customerRepo),
|
customerProcessor: processor.NewCustomerProcessor(repos.customerRepo),
|
||||||
analyticsProcessor: processor.NewAnalyticsProcessorImpl(repos.analyticsRepo, repos.expenseRepo),
|
analyticsProcessor: processor.NewAnalyticsProcessorImpl(repos.analyticsRepo),
|
||||||
tableProcessor: processor.NewTableProcessor(repos.tableRepo, repos.orderRepo),
|
tableProcessor: processor.NewTableProcessor(repos.tableRepo, repos.orderRepo),
|
||||||
unitProcessor: processor.NewUnitProcessor(repos.unitRepo),
|
unitProcessor: processor.NewUnitProcessor(repos.unitRepo),
|
||||||
ingredientProcessor: processor.NewIngredientProcessor(repos.ingredientRepo, repos.unitRepo, repos.ingredientCompositionRepo),
|
ingredientProcessor: processor.NewIngredientProcessor(repos.ingredientRepo, repos.unitRepo, repos.ingredientCompositionRepo),
|
||||||
|
|||||||
@ -236,33 +236,68 @@ type DashboardOverview struct {
|
|||||||
RefundedOrders int64 `json:"refunded_orders"`
|
RefundedOrders int64 `json:"refunded_orders"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProfitLossAnalyticsRequest represents the request for profit and loss analytics
|
||||||
type ProfitLossAnalyticsRequest struct {
|
type ProfitLossAnalyticsRequest struct {
|
||||||
OrganizationID uuid.UUID
|
OrganizationID uuid.UUID
|
||||||
OutletID *string `form:"outlet_id,omitempty"`
|
OutletID *string `form:"outlet_id,omitempty"`
|
||||||
Date string `form:"date" validate:"required"`
|
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProfitLossAnalyticsResponse represents the response for profit and loss analytics
|
||||||
type ProfitLossAnalyticsResponse struct {
|
type ProfitLossAnalyticsResponse struct {
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
Date time.Time `json:"date"`
|
DateFrom time.Time `json:"date_from"`
|
||||||
MainSummary []ProfitLossSummaryRow `json:"main_summary"`
|
DateTo time.Time `json:"date_to"`
|
||||||
OperationalExpenses []OperationalExpenseItem `json:"operational_expenses"`
|
GroupBy string `json:"group_by"`
|
||||||
OperationalExpensesTotal float64 `json:"operational_expenses_total"`
|
Summary ProfitLossSummary `json:"summary"`
|
||||||
|
Data []ProfitLossData `json:"data"`
|
||||||
|
ProductData []ProductProfitData `json:"product_data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProfitLossSummaryRow struct {
|
// ProfitLossSummary represents the summary of profit and loss analytics
|
||||||
ID string `json:"id"`
|
type ProfitLossSummary struct {
|
||||||
Label string `json:"label"`
|
TotalRevenue float64 `json:"total_revenue"`
|
||||||
IsBold bool `json:"is_bold"`
|
TotalCost float64 `json:"total_cost"`
|
||||||
TodayNominal float64 `json:"today_nominal"`
|
GrossProfit float64 `json:"gross_profit"`
|
||||||
TodayPct float64 `json:"today_pct"`
|
GrossProfitMargin float64 `json:"gross_profit_margin"`
|
||||||
MtdNominal float64 `json:"mtd_nominal"`
|
TotalTax float64 `json:"total_tax"`
|
||||||
MtdPct float64 `json:"mtd_pct"`
|
TotalDiscount float64 `json:"total_discount"`
|
||||||
SubItems []ProfitLossSummaryRow `json:"sub_items,omitempty"`
|
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 OperationalExpenseItem struct {
|
// ProfitLossData represents individual profit and loss data point by time period
|
||||||
Item string `json:"item"`
|
type ProfitLossData struct {
|
||||||
Nominal float64 `json:"nominal"`
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProductProfitData represents profit data for individual products
|
||||||
|
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"`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type CreateExpenseRequest struct {
|
type CreateExpenseRequest struct {
|
||||||
ExpenseName string `json:"expense_name" validate:"required"`
|
|
||||||
Receiver string `json:"receiver" validate:"required"`
|
Receiver string `json:"receiver" validate:"required"`
|
||||||
TransactionDate string `json:"transaction_date" validate:"required"`
|
TransactionDate string `json:"transaction_date" validate:"required"`
|
||||||
CodeNumber string `json:"code_number" validate:"required"`
|
CodeNumber string `json:"code_number" validate:"required"`
|
||||||
@ -25,7 +24,6 @@ type CreateExpenseItemRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UpdateExpenseRequest struct {
|
type UpdateExpenseRequest struct {
|
||||||
ExpenseName *string `json:"expense_name,omitempty"`
|
|
||||||
Receiver *string `json:"receiver,omitempty"`
|
Receiver *string `json:"receiver,omitempty"`
|
||||||
TransactionDate *string `json:"transaction_date,omitempty"`
|
TransactionDate *string `json:"transaction_date,omitempty"`
|
||||||
CodeNumber *string `json:"code_number,omitempty"`
|
CodeNumber *string `json:"code_number,omitempty"`
|
||||||
@ -47,7 +45,6 @@ type ExpenseResponse struct {
|
|||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
OutletID uuid.UUID `json:"outlet_id"`
|
OutletID uuid.UUID `json:"outlet_id"`
|
||||||
ExpenseName string `json:"expense_name"`
|
|
||||||
Receiver string `json:"receiver"`
|
Receiver string `json:"receiver"`
|
||||||
TransactionDate time.Time `json:"transaction_date"`
|
TransactionDate time.Time `json:"transaction_date"`
|
||||||
CodeNumber string `json:"code_number"`
|
CodeNumber string `json:"code_number"`
|
||||||
|
|||||||
@ -113,22 +113,54 @@ type DashboardOverview struct {
|
|||||||
RefundedOrders int64 `json:"refunded_orders"`
|
RefundedOrders int64 `json:"refunded_orders"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProfitLossAnalytics represents profit and loss analytics data
|
||||||
type ProfitLossAnalytics struct {
|
type ProfitLossAnalytics struct {
|
||||||
TodayRevenue float64
|
Summary ProfitLossSummary `json:"summary"`
|
||||||
TodayCost float64
|
Data []ProfitLossData `json:"data"`
|
||||||
MtdRevenue float64
|
ProductData []ProductProfitData `json:"product_data"`
|
||||||
MtdCost float64
|
|
||||||
TodayExpenseByCategory []ExpenseCategoryTotal
|
|
||||||
MtdExpenseByCategory []ExpenseCategoryTotal
|
|
||||||
OperationalExpenseItems []OperationalExpenseItem
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseCategoryTotal struct {
|
// ProfitLossSummary represents profit and loss summary data
|
||||||
CategoryName string
|
type ProfitLossSummary struct {
|
||||||
Amount float64
|
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 OperationalExpenseItem struct {
|
// ProfitLossData represents profit and loss data by time period
|
||||||
Description string
|
type ProfitLossData struct {
|
||||||
Amount float64
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProductProfitData represents profit data for individual products
|
||||||
|
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"`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,6 @@ type Expense struct {
|
|||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id"`
|
OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id"`
|
||||||
OutletID uuid.UUID `gorm:"type:uuid;not null;index" json:"outlet_id"`
|
OutletID uuid.UUID `gorm:"type:uuid;not null;index" json:"outlet_id"`
|
||||||
ExpenseName string `gorm:"not null;size:255" json:"expense_name"`
|
|
||||||
Receiver string `gorm:"not null;size:255" json:"receiver"`
|
Receiver string `gorm:"not null;size:255" json:"receiver"`
|
||||||
TransactionDate time.Time `gorm:"type:date;not null" json:"transaction_date"`
|
TransactionDate time.Time `gorm:"type:date;not null" json:"transaction_date"`
|
||||||
CodeNumber string `gorm:"not null;size:50" json:"code_number"`
|
CodeNumber string `gorm:"not null;size:50" json:"code_number"`
|
||||||
|
|||||||
@ -14,7 +14,6 @@ func ExpenseEntityToModel(entity *entities.Expense) *models.Expense {
|
|||||||
ID: entity.ID,
|
ID: entity.ID,
|
||||||
OrganizationID: entity.OrganizationID,
|
OrganizationID: entity.OrganizationID,
|
||||||
OutletID: entity.OutletID,
|
OutletID: entity.OutletID,
|
||||||
ExpenseName: entity.ExpenseName,
|
|
||||||
Receiver: entity.Receiver,
|
Receiver: entity.Receiver,
|
||||||
TransactionDate: entity.TransactionDate,
|
TransactionDate: entity.TransactionDate,
|
||||||
CodeNumber: entity.CodeNumber,
|
CodeNumber: entity.CodeNumber,
|
||||||
@ -36,7 +35,6 @@ func ExpenseModelToEntity(model *models.Expense) *entities.Expense {
|
|||||||
ID: model.ID,
|
ID: model.ID,
|
||||||
OrganizationID: model.OrganizationID,
|
OrganizationID: model.OrganizationID,
|
||||||
OutletID: model.OutletID,
|
OutletID: model.OutletID,
|
||||||
ExpenseName: model.ExpenseName,
|
|
||||||
Receiver: model.Receiver,
|
Receiver: model.Receiver,
|
||||||
TransactionDate: model.TransactionDate,
|
TransactionDate: model.TransactionDate,
|
||||||
CodeNumber: model.CodeNumber,
|
CodeNumber: model.CodeNumber,
|
||||||
@ -58,7 +56,6 @@ func ExpenseEntityToResponse(entity *entities.Expense) *models.ExpenseResponse {
|
|||||||
ID: entity.ID,
|
ID: entity.ID,
|
||||||
OrganizationID: entity.OrganizationID,
|
OrganizationID: entity.OrganizationID,
|
||||||
OutletID: entity.OutletID,
|
OutletID: entity.OutletID,
|
||||||
ExpenseName: entity.ExpenseName,
|
|
||||||
Receiver: entity.Receiver,
|
Receiver: entity.Receiver,
|
||||||
TransactionDate: entity.TransactionDate,
|
TransactionDate: entity.TransactionDate,
|
||||||
CodeNumber: entity.CodeNumber,
|
CodeNumber: entity.CodeNumber,
|
||||||
|
|||||||
@ -246,33 +246,68 @@ type DashboardOverview struct {
|
|||||||
RefundedOrders int64 `json:"refunded_orders"`
|
RefundedOrders int64 `json:"refunded_orders"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProfitLossAnalyticsRequest represents the request for profit and loss analytics
|
||||||
type ProfitLossAnalyticsRequest struct {
|
type ProfitLossAnalyticsRequest struct {
|
||||||
OrganizationID uuid.UUID `validate:"required"`
|
OrganizationID uuid.UUID `validate:"required"`
|
||||||
OutletID *uuid.UUID `validate:"omitempty"`
|
OutletID *uuid.UUID `validate:"omitempty"`
|
||||||
Date time.Time `validate:"required"`
|
DateFrom time.Time `validate:"required"`
|
||||||
|
DateTo time.Time `validate:"required"`
|
||||||
|
GroupBy string `validate:"omitempty,oneof=day hour week month"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProfitLossAnalyticsResponse represents the response for profit and loss analytics
|
||||||
type ProfitLossAnalyticsResponse struct {
|
type ProfitLossAnalyticsResponse struct {
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
Date time.Time `json:"date"`
|
DateFrom time.Time `json:"date_from"`
|
||||||
MainSummary []ProfitLossSummaryRow `json:"main_summary"`
|
DateTo time.Time `json:"date_to"`
|
||||||
OperationalExpenses []OperationalExpenseItem `json:"operational_expenses"`
|
GroupBy string `json:"group_by"`
|
||||||
OperationalExpensesTotal float64 `json:"operational_expenses_total"`
|
Summary ProfitLossSummary `json:"summary"`
|
||||||
|
Data []ProfitLossData `json:"data"`
|
||||||
|
ProductData []ProductProfitData `json:"product_data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProfitLossSummaryRow struct {
|
// ProfitLossSummary represents the summary of profit and loss analytics
|
||||||
ID string `json:"id"`
|
type ProfitLossSummary struct {
|
||||||
Label string `json:"label"`
|
TotalRevenue float64 `json:"total_revenue"`
|
||||||
IsBold bool `json:"is_bold"`
|
TotalCost float64 `json:"total_cost"`
|
||||||
TodayNominal float64 `json:"today_nominal"`
|
GrossProfit float64 `json:"gross_profit"`
|
||||||
TodayPct float64 `json:"today_pct"`
|
GrossProfitMargin float64 `json:"gross_profit_margin"`
|
||||||
MtdNominal float64 `json:"mtd_nominal"`
|
TotalTax float64 `json:"total_tax"`
|
||||||
MtdPct float64 `json:"mtd_pct"`
|
TotalDiscount float64 `json:"total_discount"`
|
||||||
SubItems []ProfitLossSummaryRow `json:"sub_items,omitempty"`
|
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 OperationalExpenseItem struct {
|
// ProfitLossData represents individual profit and loss data point by time period
|
||||||
Item string `json:"item"`
|
type ProfitLossData struct {
|
||||||
Nominal float64 `json:"nominal"`
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProductProfitData represents profit data for individual products
|
||||||
|
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"`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,6 @@ type Expense struct {
|
|||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
OutletID uuid.UUID `json:"outlet_id"`
|
OutletID uuid.UUID `json:"outlet_id"`
|
||||||
ExpenseName string `json:"expense_name"`
|
|
||||||
Receiver string `json:"receiver"`
|
Receiver string `json:"receiver"`
|
||||||
TransactionDate time.Time `json:"transaction_date"`
|
TransactionDate time.Time `json:"transaction_date"`
|
||||||
CodeNumber string `json:"code_number"`
|
CodeNumber string `json:"code_number"`
|
||||||
@ -36,7 +35,6 @@ type ExpenseResponse struct {
|
|||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
OutletID uuid.UUID `json:"outlet_id"`
|
OutletID uuid.UUID `json:"outlet_id"`
|
||||||
ExpenseName string `json:"expense_name"`
|
|
||||||
Receiver string `json:"receiver"`
|
Receiver string `json:"receiver"`
|
||||||
TransactionDate time.Time `json:"transaction_date"`
|
TransactionDate time.Time `json:"transaction_date"`
|
||||||
CodeNumber string `json:"code_number"`
|
CodeNumber string `json:"code_number"`
|
||||||
@ -61,7 +59,6 @@ type ExpenseItemResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreateExpenseRequest struct {
|
type CreateExpenseRequest struct {
|
||||||
ExpenseName string `json:"expense_name"`
|
|
||||||
Receiver string `json:"receiver"`
|
Receiver string `json:"receiver"`
|
||||||
TransactionDate string `json:"transaction_date"`
|
TransactionDate string `json:"transaction_date"`
|
||||||
CodeNumber string `json:"code_number"`
|
CodeNumber string `json:"code_number"`
|
||||||
@ -79,7 +76,6 @@ type CreateExpenseItemRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UpdateExpenseRequest struct {
|
type UpdateExpenseRequest struct {
|
||||||
ExpenseName *string `json:"expense_name,omitempty"`
|
|
||||||
Receiver *string `json:"receiver,omitempty"`
|
Receiver *string `json:"receiver,omitempty"`
|
||||||
TransactionDate *string `json:"transaction_date,omitempty"`
|
TransactionDate *string `json:"transaction_date,omitempty"`
|
||||||
CodeNumber *string `json:"code_number,omitempty"`
|
CodeNumber *string `json:"code_number,omitempty"`
|
||||||
|
|||||||
@ -3,10 +3,8 @@ package processor
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"apskel-pos-be/internal/entities"
|
|
||||||
"apskel-pos-be/internal/models"
|
"apskel-pos-be/internal/models"
|
||||||
"apskel-pos-be/internal/repository"
|
"apskel-pos-be/internal/repository"
|
||||||
)
|
)
|
||||||
@ -23,13 +21,11 @@ type AnalyticsProcessor interface {
|
|||||||
|
|
||||||
type AnalyticsProcessorImpl struct {
|
type AnalyticsProcessorImpl struct {
|
||||||
analyticsRepo repository.AnalyticsRepository
|
analyticsRepo repository.AnalyticsRepository
|
||||||
expenseRepo ExpenseRepository
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAnalyticsProcessorImpl(analyticsRepo repository.AnalyticsRepository, expenseRepo ExpenseRepository) *AnalyticsProcessorImpl {
|
func NewAnalyticsProcessorImpl(analyticsRepo repository.AnalyticsRepository) *AnalyticsProcessorImpl {
|
||||||
return &AnalyticsProcessorImpl{
|
return &AnalyticsProcessorImpl{
|
||||||
analyticsRepo: analyticsRepo,
|
analyticsRepo: analyticsRepo,
|
||||||
expenseRepo: expenseRepo,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -398,127 +394,71 @@ func (p *AnalyticsProcessorImpl) GetDashboardAnalytics(ctx context.Context, req
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *AnalyticsProcessorImpl) GetProfitLossAnalytics(ctx context.Context, req *models.ProfitLossAnalyticsRequest) (*models.ProfitLossAnalyticsResponse, error) {
|
func (p *AnalyticsProcessorImpl) GetProfitLossAnalytics(ctx context.Context, req *models.ProfitLossAnalyticsRequest) (*models.ProfitLossAnalyticsResponse, error) {
|
||||||
if req.Date.IsZero() {
|
if req.DateFrom.After(req.DateTo) {
|
||||||
return nil, fmt.Errorf("date is required")
|
return nil, fmt.Errorf("date_from cannot be after date_to")
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := p.analyticsRepo.GetProfitLossAnalytics(ctx, req.OrganizationID, req.OutletID, req.Date)
|
// Get analytics data from repository
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
todayPromosi := getExpenseAmountByCategory(result.TodayExpenseByCategory, "promosi")
|
// Transform entities to models
|
||||||
todayLainLain := getExpenseAmountByCategory(result.TodayExpenseByCategory, "lain")
|
data := make([]models.ProfitLossData, len(result.Data))
|
||||||
todayTotalOps := todayPromosi + todayLainLain
|
for i, item := range result.Data {
|
||||||
todayGaji := getExpenseAmountByCategory(result.TodayExpenseByCategory, "gaji")
|
data[i] = models.ProfitLossData{
|
||||||
|
Date: item.Date,
|
||||||
mtdPromosi := getExpenseAmountByCategory(result.MtdExpenseByCategory, "promosi")
|
Revenue: item.Revenue,
|
||||||
mtdLainLain := getExpenseAmountByCategory(result.MtdExpenseByCategory, "lain")
|
Cost: item.Cost,
|
||||||
mtdTotalOps := mtdPromosi + mtdLainLain
|
GrossProfit: item.GrossProfit,
|
||||||
mtdGaji := getExpenseAmountByCategory(result.MtdExpenseByCategory, "gaji")
|
GrossProfitMargin: item.GrossProfitMargin,
|
||||||
|
Tax: item.Tax,
|
||||||
todayGrossProfit := result.TodayRevenue - result.TodayCost
|
Discount: item.Discount,
|
||||||
mtdGrossProfit := result.MtdRevenue - result.MtdCost
|
NetProfit: item.NetProfit,
|
||||||
|
NetProfitMargin: item.NetProfitMargin,
|
||||||
todayProfitBeforeGaji := todayGrossProfit - todayTotalOps
|
Orders: item.Orders,
|
||||||
mtdProfitBeforeGaji := mtdGrossProfit - mtdTotalOps
|
|
||||||
|
|
||||||
todayNetProfit := todayProfitBeforeGaji - todayGaji
|
|
||||||
mtdNetProfit := mtdProfitBeforeGaji - mtdGaji
|
|
||||||
|
|
||||||
todayPct := func(nominal float64) float64 {
|
|
||||||
if result.TodayRevenue == 0 {
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
return (nominal / result.TodayRevenue) * 100
|
|
||||||
}
|
|
||||||
mtdPct := func(nominal float64) float64 {
|
|
||||||
if result.MtdRevenue == 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return (nominal / result.MtdRevenue) * 100
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mainSummary := []models.ProfitLossSummaryRow{
|
productData := make([]models.ProductProfitData, len(result.ProductData))
|
||||||
{
|
for i, item := range result.ProductData {
|
||||||
ID: "total_omset", Label: "TOTAL OMSET",
|
productData[i] = models.ProductProfitData{
|
||||||
TodayNominal: result.TodayRevenue, TodayPct: todayPct(result.TodayRevenue),
|
ProductID: item.ProductID,
|
||||||
MtdNominal: result.MtdRevenue, MtdPct: mtdPct(result.MtdRevenue),
|
ProductName: item.ProductName,
|
||||||
},
|
CategoryID: item.CategoryID,
|
||||||
{
|
CategoryName: item.CategoryName,
|
||||||
ID: "hpp", Label: "HPP",
|
QuantitySold: item.QuantitySold,
|
||||||
TodayNominal: result.TodayCost, TodayPct: todayPct(result.TodayCost),
|
Revenue: item.Revenue,
|
||||||
MtdNominal: result.MtdCost, MtdPct: mtdPct(result.MtdCost),
|
Cost: item.Cost,
|
||||||
},
|
GrossProfit: item.GrossProfit,
|
||||||
{
|
GrossProfitMargin: item.GrossProfitMargin,
|
||||||
ID: "laba_kotor", Label: "Laba Kotor (1-2)",
|
AveragePrice: item.AveragePrice,
|
||||||
TodayNominal: todayGrossProfit, TodayPct: todayPct(todayGrossProfit),
|
AverageCost: item.AverageCost,
|
||||||
MtdNominal: mtdGrossProfit, MtdPct: mtdPct(mtdGrossProfit),
|
ProfitPerUnit: item.ProfitPerUnit,
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: "biaya_ops", Label: "BIAYA OPS",
|
|
||||||
TodayNominal: todayTotalOps, TodayPct: todayPct(todayTotalOps),
|
|
||||||
MtdNominal: mtdTotalOps, MtdPct: mtdPct(mtdTotalOps),
|
|
||||||
SubItems: []models.ProfitLossSummaryRow{
|
|
||||||
{
|
|
||||||
ID: "by_promosi", Label: "1. By Promosi",
|
|
||||||
TodayNominal: todayPromosi, TodayPct: todayPct(todayPromosi),
|
|
||||||
MtdNominal: mtdPromosi, MtdPct: mtdPct(mtdPromosi),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: "by_lain_lain", Label: "2. By Lain lain",
|
|
||||||
TodayNominal: todayLainLain, TodayPct: todayPct(todayLainLain),
|
|
||||||
MtdNominal: mtdLainLain, MtdPct: mtdPct(mtdLainLain),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: "total_biaya_ops", Label: "Total Biaya OPS (4.1+4.2)", IsBold: true,
|
|
||||||
TodayNominal: todayTotalOps, TodayPct: todayPct(todayTotalOps),
|
|
||||||
MtdNominal: mtdTotalOps, MtdPct: mtdPct(mtdTotalOps),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: "laba_rugi_sblm_gaji", Label: "Laba/Rugi sblm Gaji (3-4)",
|
|
||||||
TodayNominal: todayProfitBeforeGaji, TodayPct: todayPct(todayProfitBeforeGaji),
|
|
||||||
MtdNominal: mtdProfitBeforeGaji, MtdPct: mtdPct(mtdProfitBeforeGaji),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: "biaya_gaji", Label: "BIAYA GAJI",
|
|
||||||
TodayNominal: todayGaji, TodayPct: todayPct(todayGaji),
|
|
||||||
MtdNominal: mtdGaji, MtdPct: mtdPct(mtdGaji),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: "laba_rugi", Label: "Laba/Rugi (5-6)", IsBold: true,
|
|
||||||
TodayNominal: todayNetProfit, TodayPct: todayPct(todayNetProfit),
|
|
||||||
MtdNominal: mtdNetProfit, MtdPct: mtdPct(mtdNetProfit),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
opsItems := make([]models.OperationalExpenseItem, len(result.OperationalExpenseItems))
|
|
||||||
var opsTotal float64
|
|
||||||
for i, item := range result.OperationalExpenseItems {
|
|
||||||
opsItems[i] = models.OperationalExpenseItem{
|
|
||||||
Item: item.Description,
|
|
||||||
Nominal: item.Amount,
|
|
||||||
}
|
}
|
||||||
opsTotal += item.Amount
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &models.ProfitLossAnalyticsResponse{
|
return &models.ProfitLossAnalyticsResponse{
|
||||||
OrganizationID: req.OrganizationID,
|
OrganizationID: req.OrganizationID,
|
||||||
OutletID: req.OutletID,
|
OutletID: req.OutletID,
|
||||||
Date: req.Date,
|
DateFrom: req.DateFrom,
|
||||||
MainSummary: mainSummary,
|
DateTo: req.DateTo,
|
||||||
OperationalExpenses: opsItems,
|
GroupBy: req.GroupBy,
|
||||||
OperationalExpensesTotal: opsTotal,
|
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,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getExpenseAmountByCategory(categories []entities.ExpenseCategoryTotal, keyword string) float64 {
|
|
||||||
for _, cat := range categories {
|
|
||||||
if strings.Contains(strings.ToLower(cat.CategoryName), keyword) {
|
|
||||||
return cat.Amount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|||||||
@ -40,27 +40,10 @@ func (analyticsRepositoryStub) GetDashboardOverview(context.Context, uuid.UUID,
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (analyticsRepositoryStub) GetProfitLossAnalytics(context.Context, uuid.UUID, *uuid.UUID, time.Time) (*entities.ProfitLossAnalytics, error) {
|
func (analyticsRepositoryStub) GetProfitLossAnalytics(context.Context, uuid.UUID, *uuid.UUID, time.Time, time.Time, string) (*entities.ProfitLossAnalytics, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type expenseRepositoryStub struct{}
|
|
||||||
|
|
||||||
func (expenseRepositoryStub) Create(context.Context, *entities.Expense) error { return nil }
|
|
||||||
func (expenseRepositoryStub) GetByID(context.Context, uuid.UUID) (*entities.Expense, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
func (expenseRepositoryStub) GetByIDAndOrganizationID(context.Context, uuid.UUID, uuid.UUID) (*entities.Expense, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
func (expenseRepositoryStub) Update(context.Context, *entities.Expense) error { return nil }
|
|
||||||
func (expenseRepositoryStub) Delete(context.Context, uuid.UUID) error { return nil }
|
|
||||||
func (expenseRepositoryStub) List(context.Context, uuid.UUID, map[string]interface{}, int, int) ([]*entities.Expense, int64, error) {
|
|
||||||
return nil, 0, nil
|
|
||||||
}
|
|
||||||
func (expenseRepositoryStub) CreateItem(context.Context, *entities.ExpenseItem) error { return nil }
|
|
||||||
func (expenseRepositoryStub) DeleteItemsByExpenseID(context.Context, uuid.UUID) error { return nil }
|
|
||||||
|
|
||||||
func TestAnalyticsProcessorGetPurchasingAnalyticsPassesOutletName(t *testing.T) {
|
func TestAnalyticsProcessorGetPurchasingAnalyticsPassesOutletName(t *testing.T) {
|
||||||
outletID := uuid.New()
|
outletID := uuid.New()
|
||||||
outletName := "Main Outlet"
|
outletName := "Main Outlet"
|
||||||
@ -72,7 +55,7 @@ func TestAnalyticsProcessorGetPurchasingAnalyticsPassesOutletName(t *testing.T)
|
|||||||
TotalPurchases: 125,
|
TotalPurchases: 125,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, expenseRepositoryStub{})
|
})
|
||||||
|
|
||||||
result, err := processor.GetPurchasingAnalytics(context.Background(), &models.PurchasingAnalyticsRequest{
|
result, err := processor.GetPurchasingAnalytics(context.Background(), &models.PurchasingAnalyticsRequest{
|
||||||
OrganizationID: uuid.New(),
|
OrganizationID: uuid.New(),
|
||||||
|
|||||||
@ -44,7 +44,6 @@ func (p *ExpenseProcessorImpl) CreateExpense(ctx context.Context, organizationID
|
|||||||
expenseEntity := &entities.Expense{
|
expenseEntity := &entities.Expense{
|
||||||
OrganizationID: organizationID,
|
OrganizationID: organizationID,
|
||||||
OutletID: outletID,
|
OutletID: outletID,
|
||||||
ExpenseName: req.ExpenseName,
|
|
||||||
Receiver: req.Receiver,
|
Receiver: req.Receiver,
|
||||||
TransactionDate: transactionDate,
|
TransactionDate: transactionDate,
|
||||||
CodeNumber: req.CodeNumber,
|
CodeNumber: req.CodeNumber,
|
||||||
@ -91,9 +90,6 @@ func (p *ExpenseProcessorImpl) UpdateExpense(ctx context.Context, id, organizati
|
|||||||
return nil, fmt.Errorf("expense not found: %w", err)
|
return nil, fmt.Errorf("expense not found: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.ExpenseName != nil {
|
|
||||||
expenseEntity.ExpenseName = *req.ExpenseName
|
|
||||||
}
|
|
||||||
if req.Receiver != nil {
|
if req.Receiver != nil {
|
||||||
expenseEntity.Receiver = *req.Receiver
|
expenseEntity.Receiver = *req.Receiver
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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, date 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,119 +432,152 @@ 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, date 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(date.Year(), date.Month(), 1, 0, 0, 0, 0, date.Location())
|
// Summary query
|
||||||
todayStart := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
|
var summary entities.ProfitLossSummary
|
||||||
todayEnd := todayStart.Add(24 * time.Hour).Add(-time.Nanosecond)
|
|
||||||
|
|
||||||
type revenueCostResult struct {
|
summaryQuery := r.db.WithContext(ctx).
|
||||||
Revenue float64
|
|
||||||
Cost float64
|
|
||||||
}
|
|
||||||
|
|
||||||
var todayRC revenueCostResult
|
|
||||||
todayQuery := r.db.WithContext(ctx).
|
|
||||||
Table("orders o").
|
Table("orders o").
|
||||||
Select(`
|
Select(`
|
||||||
COALESCE(SUM(o.total_amount), 0) as revenue,
|
COALESCE(SUM(o.total_amount), 0) as total_revenue,
|
||||||
COALESCE(SUM(o.total_cost), 0) as cost
|
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.organization_id = ?", organizationID).
|
||||||
Where("o.status = ?", entities.OrderStatusCompleted).
|
Where("o.status = ?", entities.OrderStatusCompleted).
|
||||||
Where("o.payment_status = ?", entities.PaymentStatusCompleted).
|
Where("o.payment_status = ?", entities.PaymentStatusCompleted).
|
||||||
Where("o.is_void = false AND o.is_refund = false").
|
Where("o.is_void = false AND o.is_refund = false").
|
||||||
Where("o.created_at >= ? AND o.created_at <= ?", todayStart, todayEnd)
|
Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo)
|
||||||
todayQuery = r.resolveOutletID(todayQuery, outletID, "o.outlet_id")
|
|
||||||
if err := todayQuery.Scan(&todayRC).Error; err != nil {
|
summaryQuery = r.resolveOutletID(summaryQuery, outletID, "o.outlet_id")
|
||||||
|
|
||||||
|
err := summaryQuery.Scan(&summary).Error
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var mtdRC revenueCostResult
|
// Time series data query
|
||||||
mtdQuery := r.db.WithContext(ctx).
|
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: // day
|
||||||
|
timeFormat = "DATE_TRUNC('day', o.created_at)"
|
||||||
|
}
|
||||||
|
|
||||||
|
var data []entities.ProfitLossData
|
||||||
|
|
||||||
|
dataQuery := r.db.WithContext(ctx).
|
||||||
Table("orders o").
|
Table("orders o").
|
||||||
Select(`
|
Select(`
|
||||||
|
`+timeFormat+` as date,
|
||||||
COALESCE(SUM(o.total_amount), 0) as revenue,
|
COALESCE(SUM(o.total_amount), 0) as revenue,
|
||||||
COALESCE(SUM(o.total_cost), 0) as cost
|
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.organization_id = ?", organizationID).
|
||||||
Where("o.status = ?", entities.OrderStatusCompleted).
|
Where("o.status = ?", entities.OrderStatusCompleted).
|
||||||
Where("o.payment_status = ?", entities.PaymentStatusCompleted).
|
Where("o.payment_status = ?", entities.PaymentStatusCompleted).
|
||||||
Where("o.is_void = false AND o.is_refund = false").
|
Where("o.is_void = false AND o.is_refund = false").
|
||||||
Where("o.created_at >= ? AND o.created_at <= ?", mtdStart, todayEnd)
|
Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo).
|
||||||
mtdQuery = r.resolveOutletID(mtdQuery, outletID, "o.outlet_id")
|
Group(timeFormat).
|
||||||
if err := mtdQuery.Scan(&mtdRC).Error; err != nil {
|
Order(timeFormat)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
todayExpenseByCategory, err := r.getExpenseByCategory(ctx, organizationID, outletID, todayStart, todayEnd)
|
dataQuery = r.resolveOutletID(dataQuery, outletID, "o.outlet_id")
|
||||||
|
|
||||||
|
err = dataQuery.Scan(&data).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
mtdExpenseByCategory, err := r.getExpenseByCategory(ctx, organizationID, outletID, mtdStart, todayEnd)
|
// Product profit data query
|
||||||
if err != nil {
|
var productData []entities.ProductProfitData
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
opsItems, err := r.getOperationalExpenseItems(ctx, organizationID, outletID, mtdStart, todayEnd)
|
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")
|
||||||
|
|
||||||
|
err = productQuery.Scan(&productData).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &entities.ProfitLossAnalytics{
|
return &entities.ProfitLossAnalytics{
|
||||||
TodayRevenue: todayRC.Revenue,
|
Summary: summary,
|
||||||
TodayCost: todayRC.Cost,
|
Data: data,
|
||||||
MtdRevenue: mtdRC.Revenue,
|
ProductData: productData,
|
||||||
MtdCost: mtdRC.Cost,
|
|
||||||
TodayExpenseByCategory: todayExpenseByCategory,
|
|
||||||
MtdExpenseByCategory: mtdExpenseByCategory,
|
|
||||||
OperationalExpenseItems: opsItems,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *AnalyticsRepositoryImpl) getExpenseByCategory(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) ([]entities.ExpenseCategoryTotal, error) {
|
|
||||||
var results []entities.ExpenseCategoryTotal
|
|
||||||
|
|
||||||
query := r.db.WithContext(ctx).
|
|
||||||
Table("expense_items ei").
|
|
||||||
Select(`COALESCE(parent_coa.name, 'Lain-lain') as category_name, COALESCE(SUM(ei.amount), 0) as amount`).
|
|
||||||
Joins("JOIN expenses e ON ei.expense_id = e.id").
|
|
||||||
Joins("JOIN chart_of_accounts coa ON ei.chart_of_account_id = coa.id").
|
|
||||||
Joins("LEFT JOIN chart_of_accounts parent_coa ON coa.parent_id = parent_coa.id").
|
|
||||||
Where("e.organization_id = ?", organizationID).
|
|
||||||
Where("e.transaction_date >= ? AND e.transaction_date <= ?", dateFrom, dateTo)
|
|
||||||
|
|
||||||
if outletID != nil {
|
|
||||||
query = query.Where("e.outlet_id = ?", *outletID)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := query.
|
|
||||||
Group("parent_coa.name").
|
|
||||||
Order("parent_coa.name").
|
|
||||||
Scan(&results).Error
|
|
||||||
|
|
||||||
return results, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *AnalyticsRepositoryImpl) getOperationalExpenseItems(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) ([]entities.OperationalExpenseItem, error) {
|
|
||||||
var results []entities.OperationalExpenseItem
|
|
||||||
|
|
||||||
query := r.db.WithContext(ctx).
|
|
||||||
Table("expense_items ei").
|
|
||||||
Select(`COALESCE(ei.description, coa.name) as description, COALESCE(SUM(ei.amount), 0) as amount`).
|
|
||||||
Joins("JOIN expenses e ON ei.expense_id = e.id").
|
|
||||||
Joins("JOIN chart_of_accounts coa ON ei.chart_of_account_id = coa.id").
|
|
||||||
Where("e.organization_id = ?", organizationID).
|
|
||||||
Where("e.transaction_date >= ? AND e.transaction_date <= ?", dateFrom, dateTo)
|
|
||||||
|
|
||||||
if outletID != nil {
|
|
||||||
query = query.Where("e.outlet_id = ?", *outletID)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := query.
|
|
||||||
Group("COALESCE(ei.description, coa.name)").
|
|
||||||
Order("amount DESC").
|
|
||||||
Scan(&results).Error
|
|
||||||
|
|
||||||
return results, err
|
|
||||||
}
|
|
||||||
|
|||||||
@ -67,8 +67,8 @@ func (r *ExpenseRepositoryImpl) List(ctx context.Context, organizationID uuid.UU
|
|||||||
case "search":
|
case "search":
|
||||||
if searchStr, ok := value.(string); ok && searchStr != "" {
|
if searchStr, ok := value.(string); ok && searchStr != "" {
|
||||||
searchPattern := "%" + strings.ToLower(searchStr) + "%"
|
searchPattern := "%" + strings.ToLower(searchStr) + "%"
|
||||||
query = query.Where("LOWER(expense_name) LIKE ? OR LOWER(receiver) LIKE ? OR LOWER(code_number) LIKE ? OR LOWER(description) LIKE ?",
|
query = query.Where("LOWER(receiver) LIKE ? OR LOWER(code_number) LIKE ? OR LOWER(description) LIKE ?",
|
||||||
searchPattern, searchPattern, searchPattern, searchPattern)
|
searchPattern, searchPattern, searchPattern)
|
||||||
}
|
}
|
||||||
case "outlet_id":
|
case "outlet_id":
|
||||||
if outletID, ok := value.(uuid.UUID); ok {
|
if outletID, ok := value.(uuid.UUID); ok {
|
||||||
|
|||||||
@ -306,8 +306,20 @@ func (s *AnalyticsServiceImpl) validateProfitLossAnalyticsRequest(req *models.Pr
|
|||||||
return fmt.Errorf("organization_id is required")
|
return fmt.Errorf("organization_id is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Date.IsZero() {
|
if req.DateFrom.IsZero() {
|
||||||
return fmt.Errorf("date is required")
|
return fmt.Errorf("date_from is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.DateTo.IsZero() {
|
||||||
|
return fmt.Errorf("date_to is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.DateFrom.After(req.DateTo) {
|
||||||
|
return fmt.Errorf("date_from cannot be after date_to")
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.GroupBy != "" && req.GroupBy != "hour" && req.GroupBy != "day" && req.GroupBy != "week" && req.GroupBy != "month" {
|
||||||
|
return fmt.Errorf("invalid group_by value, must be one of: hour, day, week, month")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@ -113,8 +113,7 @@ func (s *ReportServiceImpl) GenerateDailyTransactionPDF(ctx context.Context, org
|
|||||||
end := day.Add(24*time.Hour - time.Nanosecond)
|
end := day.Add(24*time.Hour - time.Nanosecond)
|
||||||
|
|
||||||
salesReq := &models.SalesAnalyticsRequest{OrganizationID: orgID, OutletID: &outID, DateFrom: start, DateTo: end, GroupBy: "day"}
|
salesReq := &models.SalesAnalyticsRequest{OrganizationID: orgID, OutletID: &outID, DateFrom: start, DateTo: end, GroupBy: "day"}
|
||||||
plReq := &models.ProfitLossAnalyticsRequest{OrganizationID: orgID, OutletID: &outID, Date: day}
|
plReq := &models.ProfitLossAnalyticsRequest{OrganizationID: orgID, OutletID: &outID, DateFrom: start, DateTo: end, GroupBy: "day"}
|
||||||
productReq := &models.ProductAnalyticsRequest{OrganizationID: orgID, OutletID: &outID, DateFrom: start, DateTo: end, Limit: 1000}
|
|
||||||
|
|
||||||
sales, err := s.analyticsService.GetSalesAnalytics(ctx, salesReq)
|
sales, err := s.analyticsService.GetSalesAnalytics(ctx, salesReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -124,15 +123,6 @@ func (s *ReportServiceImpl) GenerateDailyTransactionPDF(ctx context.Context, org
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", fmt.Errorf("get profit/loss analytics: %w", err)
|
return "", "", fmt.Errorf("get profit/loss analytics: %w", err)
|
||||||
}
|
}
|
||||||
products, err := s.analyticsService.GetProductAnalytics(ctx, productReq)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", fmt.Errorf("get product analytics: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
totalOmset := getPLNominalByID(pl.MainSummary, "total_omset")
|
|
||||||
hpp := getPLNominalByID(pl.MainSummary, "hpp")
|
|
||||||
labaKotor := getPLNominalByID(pl.MainSummary, "laba_kotor")
|
|
||||||
labaKotorPct := getPLPctByID(pl.MainSummary, "laba_kotor")
|
|
||||||
|
|
||||||
data := reportTemplateData{
|
data := reportTemplateData{
|
||||||
OrganizationName: org.Name,
|
OrganizationName: org.Name,
|
||||||
@ -143,28 +133,28 @@ func (s *ReportServiceImpl) GenerateDailyTransactionPDF(ctx context.Context, org
|
|||||||
GeneratedBy: generatedBy,
|
GeneratedBy: generatedBy,
|
||||||
PrintTime: time.Now().Format("02/01/2006 15:04:05"),
|
PrintTime: time.Now().Format("02/01/2006 15:04:05"),
|
||||||
Summary: reportSummary{
|
Summary: reportSummary{
|
||||||
TotalTransactions: sales.Summary.TotalOrders,
|
TotalTransactions: pl.Summary.TotalOrders,
|
||||||
TotalItems: sales.Summary.TotalItems,
|
TotalItems: sales.Summary.TotalItems,
|
||||||
GrossSales: formatCurrency(totalOmset),
|
GrossSales: formatCurrency(pl.Summary.TotalRevenue),
|
||||||
Discount: formatCurrency(sales.Summary.TotalDiscount),
|
Discount: formatCurrency(pl.Summary.TotalDiscount),
|
||||||
Tax: formatCurrency(sales.Summary.TotalTax),
|
Tax: formatCurrency(pl.Summary.TotalTax),
|
||||||
NetSales: formatCurrency(sales.Summary.NetSales),
|
NetSales: formatCurrency(sales.Summary.NetSales),
|
||||||
COGS: formatCurrency(hpp),
|
COGS: formatCurrency(pl.Summary.TotalCost),
|
||||||
GrossProfit: formatCurrency(labaKotor),
|
GrossProfit: formatCurrency(pl.Summary.GrossProfit),
|
||||||
GrossMarginPercent: fmt.Sprintf("%.2f", labaKotorPct),
|
GrossMarginPercent: fmt.Sprintf("%.2f", pl.Summary.GrossProfitMargin),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
items := make([]reportItem, 0, len(products.Data))
|
items := make([]reportItem, 0, len(pl.ProductData))
|
||||||
for _, p := range products.Data {
|
for _, p := range pl.ProductData {
|
||||||
items = append(items, reportItem{
|
items = append(items, reportItem{
|
||||||
Name: p.ProductName,
|
Name: p.ProductName,
|
||||||
Quantity: p.QuantitySold,
|
Quantity: p.QuantitySold,
|
||||||
GrossSales: formatCurrency(p.Revenue),
|
GrossSales: formatCurrency(p.Revenue),
|
||||||
Discount: formatCurrency(0),
|
Discount: formatCurrency(0),
|
||||||
NetSales: formatCurrency(p.Revenue),
|
NetSales: formatCurrency(p.Revenue),
|
||||||
COGS: formatCurrency(p.StandardHppTotal),
|
COGS: formatCurrency(p.Cost),
|
||||||
GrossProfit: formatCurrency(p.Revenue - p.StandardHppTotal),
|
GrossProfit: formatCurrency(p.GrossProfit),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
data.Items = items
|
data.Items = items
|
||||||
@ -200,21 +190,3 @@ func (s *ReportServiceImpl) GenerateDailyTransactionPDF(ctx context.Context, org
|
|||||||
|
|
||||||
return publicURL, fileName, nil
|
return publicURL, fileName, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPLNominalByID(rows []models.ProfitLossSummaryRow, id string) float64 {
|
|
||||||
for _, row := range rows {
|
|
||||||
if row.ID == id {
|
|
||||||
return row.TodayNominal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPLPctByID(rows []models.ProfitLossSummaryRow, id string) float64 {
|
|
||||||
for _, row := range rows {
|
|
||||||
if row.ID == id {
|
|
||||||
return row.TodayPct
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|||||||
@ -427,68 +427,93 @@ func DashboardAnalyticsModelToContract(resp *models.DashboardAnalyticsResponse)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProfitLossAnalyticsContractToModel transforms contract request to model
|
||||||
func ProfitLossAnalyticsContractToModel(req *contract.ProfitLossAnalyticsRequest) (*models.ProfitLossAnalyticsRequest, error) {
|
func ProfitLossAnalyticsContractToModel(req *contract.ProfitLossAnalyticsRequest) (*models.ProfitLossAnalyticsRequest, error) {
|
||||||
if req == nil {
|
if req == nil {
|
||||||
return nil, fmt.Errorf("request cannot be nil")
|
return nil, fmt.Errorf("request cannot be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
dateTime, err := util.ParseDateToJakartaTime(req.Date)
|
// Parse date range using utility function
|
||||||
|
dateFrom, dateTo, err := util.ParseDateRangeToJakartaTime(req.DateFrom, req.DateTo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid date format: %w", err)
|
return nil, fmt.Errorf("invalid date format: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if dateTime == nil {
|
if dateFrom == nil || dateTo == nil {
|
||||||
return nil, fmt.Errorf("date is required")
|
return nil, fmt.Errorf("both date_from and date_to are required")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &models.ProfitLossAnalyticsRequest{
|
return &models.ProfitLossAnalyticsRequest{
|
||||||
OrganizationID: req.OrganizationID,
|
OrganizationID: req.OrganizationID,
|
||||||
OutletID: parseOutletID(req.OutletID),
|
OutletID: parseOutletID(req.OutletID),
|
||||||
Date: *dateTime,
|
DateFrom: *dateFrom,
|
||||||
|
DateTo: *dateTo,
|
||||||
|
GroupBy: req.GroupBy,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProfitLossAnalyticsModelToContract transforms model response to contract
|
||||||
func ProfitLossAnalyticsModelToContract(resp *models.ProfitLossAnalyticsResponse) *contract.ProfitLossAnalyticsResponse {
|
func ProfitLossAnalyticsModelToContract(resp *models.ProfitLossAnalyticsResponse) *contract.ProfitLossAnalyticsResponse {
|
||||||
if resp == nil {
|
if resp == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
mainSummary := make([]contract.ProfitLossSummaryRow, len(resp.MainSummary))
|
// Transform profit/loss data
|
||||||
for i, row := range resp.MainSummary {
|
data := make([]contract.ProfitLossData, len(resp.Data))
|
||||||
mainSummary[i] = profitLossSummaryRowModelToContract(row)
|
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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
opsItems := make([]contract.OperationalExpenseItem, len(resp.OperationalExpenses))
|
// Transform product profit data
|
||||||
for i, item := range resp.OperationalExpenses {
|
productData := make([]contract.ProductProfitData, len(resp.ProductData))
|
||||||
opsItems[i] = contract.OperationalExpenseItem{
|
for i, item := range resp.ProductData {
|
||||||
Item: item.Item,
|
productData[i] = contract.ProductProfitData{
|
||||||
Nominal: item.Nominal,
|
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &contract.ProfitLossAnalyticsResponse{
|
return &contract.ProfitLossAnalyticsResponse{
|
||||||
OrganizationID: resp.OrganizationID,
|
OrganizationID: resp.OrganizationID,
|
||||||
OutletID: resp.OutletID,
|
OutletID: resp.OutletID,
|
||||||
Date: resp.Date,
|
DateFrom: resp.DateFrom,
|
||||||
MainSummary: mainSummary,
|
DateTo: resp.DateTo,
|
||||||
OperationalExpenses: opsItems,
|
GroupBy: resp.GroupBy,
|
||||||
OperationalExpensesTotal: resp.OperationalExpensesTotal,
|
Summary: contract.ProfitLossSummary{
|
||||||
}
|
TotalRevenue: resp.Summary.TotalRevenue,
|
||||||
}
|
TotalCost: resp.Summary.TotalCost,
|
||||||
|
GrossProfit: resp.Summary.GrossProfit,
|
||||||
func profitLossSummaryRowModelToContract(row models.ProfitLossSummaryRow) contract.ProfitLossSummaryRow {
|
GrossProfitMargin: resp.Summary.GrossProfitMargin,
|
||||||
subItems := make([]contract.ProfitLossSummaryRow, len(row.SubItems))
|
TotalTax: resp.Summary.TotalTax,
|
||||||
for i, sub := range row.SubItems {
|
TotalDiscount: resp.Summary.TotalDiscount,
|
||||||
subItems[i] = profitLossSummaryRowModelToContract(sub)
|
NetProfit: resp.Summary.NetProfit,
|
||||||
}
|
NetProfitMargin: resp.Summary.NetProfitMargin,
|
||||||
return contract.ProfitLossSummaryRow{
|
TotalOrders: resp.Summary.TotalOrders,
|
||||||
ID: row.ID,
|
AverageProfit: resp.Summary.AverageProfit,
|
||||||
Label: row.Label,
|
ProfitabilityRatio: resp.Summary.ProfitabilityRatio,
|
||||||
IsBold: row.IsBold,
|
},
|
||||||
TodayNominal: row.TodayNominal,
|
Data: data,
|
||||||
TodayPct: row.TodayPct,
|
ProductData: productData,
|
||||||
MtdNominal: row.MtdNominal,
|
|
||||||
MtdPct: row.MtdPct,
|
|
||||||
SubItems: subItems,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,6 @@ func CreateExpenseRequestToModel(req *contract.CreateExpenseRequest) *models.Cre
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &models.CreateExpenseRequest{
|
return &models.CreateExpenseRequest{
|
||||||
ExpenseName: req.ExpenseName,
|
|
||||||
Receiver: req.Receiver,
|
Receiver: req.Receiver,
|
||||||
TransactionDate: req.TransactionDate,
|
TransactionDate: req.TransactionDate,
|
||||||
CodeNumber: req.CodeNumber,
|
CodeNumber: req.CodeNumber,
|
||||||
@ -34,7 +33,6 @@ func CreateExpenseItemRequestToModel(req *contract.CreateExpenseItemRequest) mod
|
|||||||
|
|
||||||
func UpdateExpenseRequestToModel(req *contract.UpdateExpenseRequest) *models.UpdateExpenseRequest {
|
func UpdateExpenseRequestToModel(req *contract.UpdateExpenseRequest) *models.UpdateExpenseRequest {
|
||||||
modelReq := &models.UpdateExpenseRequest{
|
modelReq := &models.UpdateExpenseRequest{
|
||||||
ExpenseName: req.ExpenseName,
|
|
||||||
Receiver: req.Receiver,
|
Receiver: req.Receiver,
|
||||||
TransactionDate: req.TransactionDate,
|
TransactionDate: req.TransactionDate,
|
||||||
CodeNumber: req.CodeNumber,
|
CodeNumber: req.CodeNumber,
|
||||||
@ -86,7 +84,6 @@ func ExpenseModelResponseToResponse(expense *models.ExpenseResponse) *contract.E
|
|||||||
ID: expense.ID,
|
ID: expense.ID,
|
||||||
OrganizationID: expense.OrganizationID,
|
OrganizationID: expense.OrganizationID,
|
||||||
OutletID: expense.OutletID,
|
OutletID: expense.OutletID,
|
||||||
ExpenseName: expense.ExpenseName,
|
|
||||||
Receiver: expense.Receiver,
|
Receiver: expense.Receiver,
|
||||||
TransactionDate: expense.TransactionDate,
|
TransactionDate: expense.TransactionDate,
|
||||||
CodeNumber: expense.CodeNumber,
|
CodeNumber: expense.CodeNumber,
|
||||||
|
|||||||
@ -28,10 +28,6 @@ func (v *ExpenseValidatorImpl) ValidateCreateExpenseRequest(req *contract.Create
|
|||||||
return errors.New("request body is required"), constants.MissingFieldErrorCode
|
return errors.New("request body is required"), constants.MissingFieldErrorCode
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.TrimSpace(req.ExpenseName) == "" {
|
|
||||||
return errors.New("expense_name is required"), constants.MissingFieldErrorCode
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.TrimSpace(req.Receiver) == "" {
|
if strings.TrimSpace(req.Receiver) == "" {
|
||||||
return errors.New("receiver is required"), constants.MissingFieldErrorCode
|
return errors.New("receiver is required"), constants.MissingFieldErrorCode
|
||||||
}
|
}
|
||||||
@ -84,10 +80,6 @@ func (v *ExpenseValidatorImpl) ValidateUpdateExpenseRequest(req *contract.Update
|
|||||||
return errors.New("request body is required"), constants.MissingFieldErrorCode
|
return errors.New("request body is required"), constants.MissingFieldErrorCode
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.ExpenseName != nil && strings.TrimSpace(*req.ExpenseName) == "" {
|
|
||||||
return errors.New("expense_name cannot be empty"), constants.MalformedFieldErrorCode
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.Receiver != nil && strings.TrimSpace(*req.Receiver) == "" {
|
if req.Receiver != nil && strings.TrimSpace(*req.Receiver) == "" {
|
||||||
return errors.New("receiver cannot be empty"), constants.MalformedFieldErrorCode
|
return errors.New("receiver cannot be empty"), constants.MalformedFieldErrorCode
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,2 +0,0 @@
|
|||||||
DROP INDEX IF EXISTS idx_expenses_expense_name;
|
|
||||||
ALTER TABLE expenses DROP COLUMN expense_name;
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE expenses ADD COLUMN expense_name VARCHAR(255) NOT NULL DEFAULT '';
|
|
||||||
CREATE INDEX idx_expenses_expense_name ON expenses(expense_name);
|
|
||||||
Loading…
x
Reference in New Issue
Block a user