Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
664847098e |
+2
-2
@@ -372,7 +372,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
|||||||
ingredientProcessor: processor.NewIngredientProcessor(repos.ingredientRepo, repos.unitRepo, repos.ingredientCompositionRepo),
|
ingredientProcessor: processor.NewIngredientProcessor(repos.ingredientRepo, repos.unitRepo, repos.ingredientCompositionRepo),
|
||||||
productRecipeProcessor: processor.NewProductRecipeProcessor(repos.productRecipeRepo, repos.productRepo, repos.ingredientRepo),
|
productRecipeProcessor: processor.NewProductRecipeProcessor(repos.productRecipeRepo, repos.productRepo, repos.ingredientRepo),
|
||||||
vendorProcessor: processor.NewVendorProcessorImpl(repos.vendorRepo),
|
vendorProcessor: processor.NewVendorProcessorImpl(repos.vendorRepo),
|
||||||
purchaseOrderProcessor: processor.NewPurchaseOrderProcessorImpl(repos.purchaseOrderRepo, repos.vendorRepo, repos.ingredientRepo, repos.purchaseCategoryRepo, repos.unitRepo, repos.fileRepo, inventoryMovementService, repos.unitConverterRepo),
|
purchaseOrderProcessor: processor.NewPurchaseOrderProcessorImpl(repos.purchaseOrderRepo, repos.vendorRepo, repos.ingredientRepo, repos.unitRepo, repos.fileRepo, inventoryMovementService, repos.unitConverterRepo),
|
||||||
purchaseCategoryProcessor: processor.NewPurchaseCategoryProcessorImpl(repos.purchaseCategoryRepo),
|
purchaseCategoryProcessor: processor.NewPurchaseCategoryProcessorImpl(repos.purchaseCategoryRepo),
|
||||||
unitConverterProcessor: processor.NewIngredientUnitConverterProcessorImpl(repos.unitConverterRepo, repos.ingredientRepo, repos.unitRepo),
|
unitConverterProcessor: processor.NewIngredientUnitConverterProcessorImpl(repos.unitConverterRepo, repos.ingredientRepo, repos.unitRepo),
|
||||||
chartOfAccountTypeProcessor: processor.NewChartOfAccountTypeProcessorImpl(repos.chartOfAccountTypeRepo),
|
chartOfAccountTypeProcessor: processor.NewChartOfAccountTypeProcessorImpl(repos.chartOfAccountTypeRepo),
|
||||||
@@ -396,7 +396,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
|||||||
userDeviceProcessor: processor.NewUserDeviceProcessorImpl(repos.userDeviceRepo),
|
userDeviceProcessor: processor.NewUserDeviceProcessorImpl(repos.userDeviceRepo),
|
||||||
notificationProcessor: buildNotificationProcessor(cfg, repos),
|
notificationProcessor: buildNotificationProcessor(cfg, repos),
|
||||||
productOutletPriceProcessor: processor.NewProductOutletPriceProcessorImpl(repos.productOutletPriceRepo, repos.productRepo, repos.outletRepo),
|
productOutletPriceProcessor: processor.NewProductOutletPriceProcessorImpl(repos.productOutletPriceRepo, repos.productRepo, repos.outletRepo),
|
||||||
expenseProcessor: processor.NewExpenseProcessorImpl(repos.expenseRepo, repos.purchaseCategoryRepo),
|
expenseProcessor: processor.NewExpenseProcessorImpl(repos.expenseRepo),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -106,11 +106,7 @@ type PurchasingAnalyticsResponse struct {
|
|||||||
|
|
||||||
type PurchasingSummary struct {
|
type PurchasingSummary struct {
|
||||||
TotalPurchases float64 `json:"total_purchases"`
|
TotalPurchases float64 `json:"total_purchases"`
|
||||||
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
|
||||||
ExpensePurchases float64 `json:"expense_purchases"`
|
|
||||||
TotalPurchaseOrders int64 `json:"total_purchase_orders"`
|
TotalPurchaseOrders int64 `json:"total_purchase_orders"`
|
||||||
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
|
||||||
ExpenseCount int64 `json:"expense_count"`
|
|
||||||
TotalQuantity float64 `json:"total_quantity"`
|
TotalQuantity float64 `json:"total_quantity"`
|
||||||
AveragePurchaseOrderValue float64 `json:"average_purchase_order_value"`
|
AveragePurchaseOrderValue float64 `json:"average_purchase_order_value"`
|
||||||
TotalIngredients int64 `json:"total_ingredients"`
|
TotalIngredients int64 `json:"total_ingredients"`
|
||||||
@@ -118,16 +114,12 @@ type PurchasingSummary struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PurchasingAnalyticsData struct {
|
type PurchasingAnalyticsData struct {
|
||||||
Date time.Time `json:"date"`
|
Date time.Time `json:"date"`
|
||||||
Purchases float64 `json:"purchases"`
|
Purchases float64 `json:"purchases"`
|
||||||
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
PurchaseOrders int64 `json:"purchase_orders"`
|
||||||
ExpensePurchases float64 `json:"expense_purchases"`
|
Quantity float64 `json:"quantity"`
|
||||||
PurchaseOrders int64 `json:"purchase_orders"`
|
Ingredients int64 `json:"ingredients"`
|
||||||
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
Vendors int64 `json:"vendors"`
|
||||||
ExpenseCount int64 `json:"expense_count"`
|
|
||||||
Quantity float64 `json:"quantity"`
|
|
||||||
Ingredients int64 `json:"ingredients"`
|
|
||||||
Vendors int64 `json:"vendors"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PurchasingIngredientData struct {
|
type PurchasingIngredientData struct {
|
||||||
@@ -324,114 +316,3 @@ type OperationalExpenseItem struct {
|
|||||||
Item string `json:"item"`
|
Item string `json:"item"`
|
||||||
Nominal float64 `json:"nominal"`
|
Nominal float64 `json:"nominal"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExclusiveSummaryPeriodRequest struct {
|
|
||||||
OrganizationID uuid.UUID
|
|
||||||
OutletID *string `form:"outlet_id,omitempty"`
|
|
||||||
DateFrom string `form:"date_from" validate:"required"`
|
|
||||||
DateTo string `form:"date_to" validate:"required"`
|
|
||||||
ExcludeGajiStaffFromReimburse bool `form:"exclude_gaji_staff_from_reimburse"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExclusiveSummaryMonthlyRequest struct {
|
|
||||||
OrganizationID uuid.UUID
|
|
||||||
OutletID *string `form:"outlet_id,omitempty"`
|
|
||||||
Month string `form:"month" validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExclusiveSummaryPeriodResponse struct {
|
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
|
||||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
|
||||||
Period ExclusiveSummaryPeriodRange `json:"period"`
|
|
||||||
Summary ExclusiveSummaryPeriodSummary `json:"summary"`
|
|
||||||
Reimburse ExclusiveSummaryReimburse `json:"reimburse"`
|
|
||||||
HPPBreakdown []ExclusiveSummaryCategoryBreakdown `json:"hpp_breakdown"`
|
|
||||||
OperationalExpenseBreakdown []ExclusiveSummaryCategoryBreakdown `json:"operational_expense_breakdown"`
|
|
||||||
DailySummary []ExclusiveSummaryDailySummary `json:"daily_summary"`
|
|
||||||
DailyTransactions []ExclusiveSummaryDailyTransaction `json:"daily_transactions"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExclusiveSummaryPeriodRange struct {
|
|
||||||
DateFrom time.Time `json:"date_from"`
|
|
||||||
DateTo time.Time `json:"date_to"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExclusiveSummaryPeriodSummary struct {
|
|
||||||
Sales float64 `json:"sales"`
|
|
||||||
HPP float64 `json:"hpp"`
|
|
||||||
GrossProfit float64 `json:"gross_profit"`
|
|
||||||
SalaryTotal float64 `json:"salary_total"`
|
|
||||||
SalaryDW float64 `json:"salary_dw"`
|
|
||||||
SalaryStaff float64 `json:"salary_staff"`
|
|
||||||
SalaryOther float64 `json:"salary_other"`
|
|
||||||
OtherOperationalExpenses float64 `json:"other_operational_expenses"`
|
|
||||||
OperationalExpensesTotal float64 `json:"operational_expenses_total"`
|
|
||||||
TotalCost float64 `json:"total_cost"`
|
|
||||||
NetProfit float64 `json:"net_profit"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExclusiveSummaryReimburse struct {
|
|
||||||
TotalCost float64 `json:"total_cost"`
|
|
||||||
ExcludedSalaryStaff float64 `json:"excluded_salary_staff"`
|
|
||||||
TotalReimburse float64 `json:"total_reimburse"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExclusiveSummaryCategoryBreakdown struct {
|
|
||||||
CategoryCode string `json:"category_code"`
|
|
||||||
CategoryName string `json:"category_name"`
|
|
||||||
Amount float64 `json:"amount"`
|
|
||||||
Percentage float64 `json:"percentage"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExclusiveSummaryDailySummary struct {
|
|
||||||
Date time.Time `json:"date"`
|
|
||||||
TransactionCount int64 `json:"transaction_count"`
|
|
||||||
TotalCost float64 `json:"total_cost"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExclusiveSummaryDailyTransaction struct {
|
|
||||||
Date time.Time `json:"date"`
|
|
||||||
CategoryCode string `json:"category_code"`
|
|
||||||
CategoryName string `json:"category_name"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Amount float64 `json:"amount"`
|
|
||||||
Source string `json:"source"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExclusiveSummaryMonthlyResponse struct {
|
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
|
||||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
|
||||||
Month string `json:"month"`
|
|
||||||
Summary ExclusiveSummaryMonthlySummary `json:"summary"`
|
|
||||||
Periods []ExclusiveSummaryMonthlyPeriod `json:"periods"`
|
|
||||||
BankBalance []ExclusiveSummaryBankBalance `json:"bank_balance"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExclusiveSummaryMonthlySummary struct {
|
|
||||||
TotalSales float64 `json:"total_sales"`
|
|
||||||
HPP float64 `json:"hpp"`
|
|
||||||
GrossProfit float64 `json:"gross_profit"`
|
|
||||||
OperationalExpensesTotal float64 `json:"operational_expenses_total"`
|
|
||||||
TotalCost float64 `json:"total_cost"`
|
|
||||||
NetProfit float64 `json:"net_profit"`
|
|
||||||
NetProfitMargin float64 `json:"net_profit_margin"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExclusiveSummaryMonthlyPeriod struct {
|
|
||||||
Label string `json:"label"`
|
|
||||||
DateFrom time.Time `json:"date_from"`
|
|
||||||
DateTo time.Time `json:"date_to"`
|
|
||||||
Sales float64 `json:"sales"`
|
|
||||||
HPP float64 `json:"hpp"`
|
|
||||||
GrossProfit float64 `json:"gross_profit"`
|
|
||||||
GrossMargin float64 `json:"gross_margin"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExclusiveSummaryBankBalance struct {
|
|
||||||
Bank string `json:"bank"`
|
|
||||||
OpeningBalance *float64 `json:"opening_balance"`
|
|
||||||
IncomingMutation *float64 `json:"incoming_mutation"`
|
|
||||||
OutgoingMutation *float64 `json:"outgoing_mutation"`
|
|
||||||
ClosingBalance *float64 `json:"closing_balance"`
|
|
||||||
Notes *string `json:"notes"`
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -19,11 +19,10 @@ type CreateExpenseRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreateExpenseItemRequest struct {
|
type CreateExpenseItemRequest struct {
|
||||||
ChartOfAccountID string `json:"chart_of_account_id" validate:"required"`
|
ChartOfAccountID string `json:"chart_of_account_id" validate:"required"`
|
||||||
PurchaseCategoryID string `json:"purchase_category_id" validate:"required"`
|
Item string `json:"item" validate:"required"`
|
||||||
Item string `json:"item" validate:"required"`
|
Description *string `json:"description,omitempty"`
|
||||||
Description *string `json:"description,omitempty"`
|
Amount float64 `json:"amount" validate:"required"`
|
||||||
Amount float64 `json:"amount" validate:"required"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateExpenseRequest struct {
|
type UpdateExpenseRequest struct {
|
||||||
@@ -40,11 +39,10 @@ type UpdateExpenseRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UpdateExpenseItemRequest struct {
|
type UpdateExpenseItemRequest struct {
|
||||||
ChartOfAccountID *string `json:"chart_of_account_id,omitempty"`
|
ChartOfAccountID *string `json:"chart_of_account_id,omitempty"`
|
||||||
PurchaseCategoryID *string `json:"purchase_category_id,omitempty"`
|
Item *string `json:"item,omitempty"`
|
||||||
Item *string `json:"item,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
Description *string `json:"description,omitempty"`
|
Amount *float64 `json:"amount,omitempty"`
|
||||||
Amount *float64 `json:"amount,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseResponse struct {
|
type ExpenseResponse struct {
|
||||||
@@ -65,19 +63,15 @@ type ExpenseResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseItemResponse struct {
|
type ExpenseItemResponse struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
ExpenseID uuid.UUID `json:"expense_id"`
|
ExpenseID uuid.UUID `json:"expense_id"`
|
||||||
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
||||||
ChartOfAccountName string `json:"chart_of_account_name,omitempty"`
|
ChartOfAccountName string `json:"chart_of_account_name,omitempty"`
|
||||||
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
Item string `json:"item"`
|
||||||
PurchaseCategoryName string `json:"purchase_category_name,omitempty"`
|
Description *string `json:"description"`
|
||||||
PurchaseCategoryType string `json:"purchase_category_type,omitempty"`
|
Amount float64 `json:"amount"`
|
||||||
PurchaseCategory *PurchaseCategoryResponse `json:"purchase_category,omitempty"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
Item string `json:"item"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
Description *string `json:"description"`
|
|
||||||
Amount float64 `json:"amount"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListExpenseRequest struct {
|
type ListExpenseRequest struct {
|
||||||
@@ -106,16 +100,15 @@ type ExpenseAnalyticsRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseAnalyticsResponse struct {
|
type ExpenseAnalyticsResponse 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"`
|
||||||
DateFrom time.Time `json:"date_from"`
|
DateFrom time.Time `json:"date_from"`
|
||||||
DateTo time.Time `json:"date_to"`
|
DateTo time.Time `json:"date_to"`
|
||||||
GroupBy string `json:"group_by"`
|
GroupBy string `json:"group_by"`
|
||||||
Summary ExpenseAnalyticsSummary `json:"summary"`
|
Summary ExpenseAnalyticsSummary `json:"summary"`
|
||||||
Data []ExpenseAnalyticsData `json:"data"`
|
Data []ExpenseAnalyticsData `json:"data"`
|
||||||
CategoryData []ExpenseAnalyticsCategoryData `json:"category_data"`
|
CategoryData []ExpenseAnalyticsCategoryData `json:"category_data"`
|
||||||
ChartOfAccountData []ExpenseAnalyticsChartOfAccountData `json:"chart_of_account_data"`
|
ItemData []ExpenseAnalyticsItemData `json:"item_data"`
|
||||||
ItemData []ExpenseAnalyticsItemData `json:"item_data"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseAnalyticsSummary struct {
|
type ExpenseAnalyticsSummary struct {
|
||||||
@@ -137,15 +130,6 @@ type ExpenseAnalyticsData struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseAnalyticsCategoryData struct {
|
type ExpenseAnalyticsCategoryData struct {
|
||||||
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
|
||||||
PurchaseCategoryName string `json:"purchase_category_name"`
|
|
||||||
PurchaseCategoryType string `json:"purchase_category_type"`
|
|
||||||
TotalAmount float64 `json:"total_amount"`
|
|
||||||
ExpenseCount int64 `json:"expense_count"`
|
|
||||||
ItemCount int64 `json:"item_count"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExpenseAnalyticsChartOfAccountData struct {
|
|
||||||
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
||||||
ChartOfAccountName string `json:"chart_of_account_name"`
|
ChartOfAccountName string `json:"chart_of_account_name"`
|
||||||
TotalAmount float64 `json:"total_amount"`
|
TotalAmount float64 `json:"total_amount"`
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ type CreatePurchaseCategoryRequest struct {
|
|||||||
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||||
Code *string `json:"code,omitempty"`
|
Code *string `json:"code,omitempty"`
|
||||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||||
Type string `json:"type" validate:"required,oneof=raw_material expense"`
|
Type string `json:"type" validate:"required,oneof=raw_material non_inventory"`
|
||||||
SortOrder *int `json:"sort_order,omitempty"`
|
SortOrder *int `json:"sort_order,omitempty"`
|
||||||
IsActive *bool `json:"is_active,omitempty"`
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -19,14 +19,14 @@ type UpdatePurchaseCategoryRequest struct {
|
|||||||
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||||
Code *string `json:"code,omitempty"`
|
Code *string `json:"code,omitempty"`
|
||||||
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
|
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
|
||||||
Type *string `json:"type,omitempty" validate:"omitempty,oneof=raw_material expense"`
|
Type *string `json:"type,omitempty" validate:"omitempty,oneof=raw_material non_inventory"`
|
||||||
SortOrder *int `json:"sort_order,omitempty"`
|
SortOrder *int `json:"sort_order,omitempty"`
|
||||||
IsActive *bool `json:"is_active,omitempty"`
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListPurchaseCategoriesRequest struct {
|
type ListPurchaseCategoriesRequest struct {
|
||||||
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||||
Type string `json:"type,omitempty" validate:"omitempty,oneof=raw_material expense"`
|
Type string `json:"type,omitempty" validate:"omitempty,oneof=raw_material non_inventory"`
|
||||||
Search string `json:"search,omitempty"`
|
Search string `json:"search,omitempty"`
|
||||||
IsActive *bool `json:"is_active,omitempty"`
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
Page int `json:"page" validate:"required,min=1"`
|
Page int `json:"page" validate:"required,min=1"`
|
||||||
|
|||||||
@@ -19,12 +19,11 @@ type CreatePurchaseOrderRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreatePurchaseOrderItemRequest struct {
|
type CreatePurchaseOrderItemRequest struct {
|
||||||
IngredientID *uuid.UUID `json:"ingredient_id,omitempty" validate:"omitempty"`
|
IngredientID uuid.UUID `json:"ingredient_id" validate:"required"`
|
||||||
PurchaseCategoryID uuid.UUID `json:"purchase_category_id" validate:"required"`
|
Description *string `json:"description,omitempty" validate:"omitempty"`
|
||||||
Description *string `json:"description,omitempty" validate:"omitempty"`
|
Quantity float64 `json:"quantity" validate:"required,gt=0"`
|
||||||
Quantity *float64 `json:"quantity,omitempty" validate:"omitempty,gt=0"`
|
UnitID uuid.UUID `json:"unit_id" validate:"required"`
|
||||||
UnitID *uuid.UUID `json:"unit_id,omitempty" validate:"omitempty"`
|
Amount float64 `json:"amount" validate:"required,gte=0"`
|
||||||
Amount float64 `json:"amount" validate:"required,gte=0"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdatePurchaseOrderRequest struct {
|
type UpdatePurchaseOrderRequest struct {
|
||||||
@@ -40,13 +39,12 @@ type UpdatePurchaseOrderRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UpdatePurchaseOrderItemRequest struct {
|
type UpdatePurchaseOrderItemRequest struct {
|
||||||
ID *uuid.UUID `json:"id,omitempty"` // For existing items
|
ID *uuid.UUID `json:"id,omitempty"` // For existing items
|
||||||
IngredientID *uuid.UUID `json:"ingredient_id,omitempty" validate:"omitempty"`
|
IngredientID *uuid.UUID `json:"ingredient_id,omitempty" validate:"omitempty"`
|
||||||
PurchaseCategoryID *uuid.UUID `json:"purchase_category_id,omitempty" validate:"omitempty"`
|
Description *string `json:"description,omitempty" validate:"omitempty"`
|
||||||
Description *string `json:"description,omitempty" validate:"omitempty"`
|
Quantity *float64 `json:"quantity,omitempty" validate:"omitempty,gt=0"`
|
||||||
Quantity *float64 `json:"quantity,omitempty" validate:"omitempty,gt=0"`
|
UnitID *uuid.UUID `json:"unit_id,omitempty" validate:"omitempty"`
|
||||||
UnitID *uuid.UUID `json:"unit_id,omitempty" validate:"omitempty"`
|
Amount *float64 `json:"amount,omitempty" validate:"omitempty,gte=0"`
|
||||||
Amount *float64 `json:"amount,omitempty" validate:"omitempty,gte=0"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PurchaseOrderResponse struct {
|
type PurchaseOrderResponse struct {
|
||||||
@@ -68,19 +66,17 @@ type PurchaseOrderResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PurchaseOrderItemResponse struct {
|
type PurchaseOrderItemResponse struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
PurchaseOrderID uuid.UUID `json:"purchase_order_id"`
|
PurchaseOrderID uuid.UUID `json:"purchase_order_id"`
|
||||||
IngredientID *uuid.UUID `json:"ingredient_id"`
|
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||||
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
Description *string `json:"description"`
|
||||||
Description *string `json:"description"`
|
Quantity float64 `json:"quantity"`
|
||||||
Quantity *float64 `json:"quantity"`
|
UnitID uuid.UUID `json:"unit_id"`
|
||||||
UnitID *uuid.UUID `json:"unit_id"`
|
Amount float64 `json:"amount"`
|
||||||
Amount float64 `json:"amount"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
Ingredient *IngredientResponse `json:"ingredient,omitempty"`
|
||||||
Ingredient *IngredientResponse `json:"ingredient,omitempty"`
|
Unit *UnitResponse `json:"unit,omitempty"`
|
||||||
PurchaseCategory *PurchaseCategoryResponse `json:"purchase_category,omitempty"`
|
|
||||||
Unit *UnitResponse `json:"unit,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PurchaseOrderAttachmentResponse struct {
|
type PurchaseOrderAttachmentResponse struct {
|
||||||
|
|||||||
@@ -38,11 +38,7 @@ type PurchasingAnalytics struct {
|
|||||||
|
|
||||||
type PurchasingSummary struct {
|
type PurchasingSummary struct {
|
||||||
TotalPurchases float64 `json:"total_purchases"`
|
TotalPurchases float64 `json:"total_purchases"`
|
||||||
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
|
||||||
ExpensePurchases float64 `json:"expense_purchases"`
|
|
||||||
TotalPurchaseOrders int64 `json:"total_purchase_orders"`
|
TotalPurchaseOrders int64 `json:"total_purchase_orders"`
|
||||||
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
|
||||||
ExpenseCount int64 `json:"expense_count"`
|
|
||||||
TotalQuantity float64 `json:"total_quantity"`
|
TotalQuantity float64 `json:"total_quantity"`
|
||||||
AveragePurchaseOrderValue float64 `json:"average_purchase_order_value"`
|
AveragePurchaseOrderValue float64 `json:"average_purchase_order_value"`
|
||||||
TotalIngredients int64 `json:"total_ingredients"`
|
TotalIngredients int64 `json:"total_ingredients"`
|
||||||
@@ -50,16 +46,12 @@ type PurchasingSummary struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PurchasingAnalyticsData struct {
|
type PurchasingAnalyticsData struct {
|
||||||
Date time.Time `json:"date"`
|
Date time.Time `json:"date"`
|
||||||
Purchases float64 `json:"purchases"`
|
Purchases float64 `json:"purchases"`
|
||||||
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
PurchaseOrders int64 `json:"purchase_orders"`
|
||||||
ExpensePurchases float64 `json:"expense_purchases"`
|
Quantity float64 `json:"quantity"`
|
||||||
PurchaseOrders int64 `json:"purchase_orders"`
|
Ingredients int64 `json:"ingredients"`
|
||||||
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
Vendors int64 `json:"vendors"`
|
||||||
ExpenseCount int64 `json:"expense_count"`
|
|
||||||
Quantity float64 `json:"quantity"`
|
|
||||||
Ingredients int64 `json:"ingredients"`
|
|
||||||
Vendors int64 `json:"vendors"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PurchasingIngredientData struct {
|
type PurchasingIngredientData struct {
|
||||||
@@ -186,42 +178,3 @@ type OperationalExpenseItem struct {
|
|||||||
Item string
|
Item string
|
||||||
Amount float64
|
Amount float64
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExclusiveSummaryAnalytics struct {
|
|
||||||
SalesTotal float64
|
|
||||||
SalesCount int64
|
|
||||||
HPPBreakdown []ExclusiveSummaryCategoryTotal
|
|
||||||
OperationalExpenseBreakdown []ExclusiveSummaryCategoryTotal
|
|
||||||
DailySummary []ExclusiveSummaryDailySummary
|
|
||||||
DailyTransactions []ExclusiveSummaryDailyTransaction
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExclusiveSummaryCategoryTotal struct {
|
|
||||||
CategoryCode string
|
|
||||||
CategoryName string
|
|
||||||
Amount float64
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExclusiveSummaryDailySummary struct {
|
|
||||||
Date time.Time
|
|
||||||
TransactionCount int64
|
|
||||||
TotalCost float64
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExclusiveSummaryDailyTransaction struct {
|
|
||||||
Date time.Time
|
|
||||||
CategoryCode string
|
|
||||||
CategoryName string
|
|
||||||
Description string
|
|
||||||
Amount float64
|
|
||||||
Source string
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExclusiveSummaryBankBalance struct {
|
|
||||||
Bank string
|
|
||||||
OpeningBalance *float64
|
|
||||||
IncomingMutation *float64
|
|
||||||
OutgoingMutation *float64
|
|
||||||
ClosingBalance *float64
|
|
||||||
Notes *string
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -29,11 +29,10 @@ type Expense struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseAnalytics struct {
|
type ExpenseAnalytics struct {
|
||||||
Summary ExpenseAnalyticsSummary
|
Summary ExpenseAnalyticsSummary
|
||||||
Data []ExpenseAnalyticsData
|
Data []ExpenseAnalyticsData
|
||||||
CategoryData []ExpenseAnalyticsCategoryData
|
CategoryData []ExpenseAnalyticsCategoryData
|
||||||
ChartOfAccountData []ExpenseAnalyticsChartOfAccountData
|
ItemData []ExpenseAnalyticsItemData
|
||||||
ItemData []ExpenseAnalyticsItemData
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseAnalyticsSummary struct {
|
type ExpenseAnalyticsSummary struct {
|
||||||
@@ -55,15 +54,6 @@ type ExpenseAnalyticsData struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseAnalyticsCategoryData struct {
|
type ExpenseAnalyticsCategoryData struct {
|
||||||
PurchaseCategoryID uuid.UUID
|
|
||||||
PurchaseCategoryName string
|
|
||||||
PurchaseCategoryType string
|
|
||||||
TotalAmount float64
|
|
||||||
ExpenseCount int64
|
|
||||||
ItemCount int64
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExpenseAnalyticsChartOfAccountData struct {
|
|
||||||
ChartOfAccountID uuid.UUID
|
ChartOfAccountID uuid.UUID
|
||||||
ChartOfAccountName string
|
ChartOfAccountName string
|
||||||
TotalAmount float64
|
TotalAmount float64
|
||||||
|
|||||||
@@ -9,19 +9,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ExpenseItem struct {
|
type ExpenseItem 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"`
|
||||||
ExpenseID uuid.UUID `gorm:"type:uuid;not null;index" json:"expense_id"`
|
ExpenseID uuid.UUID `gorm:"type:uuid;not null;index" json:"expense_id"`
|
||||||
ChartOfAccountID uuid.UUID `gorm:"type:uuid;not null;index" json:"chart_of_account_id"`
|
ChartOfAccountID uuid.UUID `gorm:"type:uuid;not null;index" json:"chart_of_account_id"`
|
||||||
PurchaseCategoryID uuid.UUID `gorm:"type:uuid;not null;index" json:"purchase_category_id"`
|
Item string `gorm:"not null;size:255" json:"item"`
|
||||||
Item string `gorm:"not null;size:255" json:"item"`
|
Description *string `gorm:"type:text" json:"description"`
|
||||||
Description *string `gorm:"type:text" json:"description"`
|
Amount float64 `gorm:"type:decimal(15,2);not null;default:0" json:"amount"`
|
||||||
Amount float64 `gorm:"type:decimal(15,2);not null;default:0" json:"amount"`
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
|
||||||
|
|
||||||
Expense *Expense `gorm:"foreignKey:ExpenseID" json:"expense,omitempty"`
|
Expense *Expense `gorm:"foreignKey:ExpenseID" json:"expense,omitempty"`
|
||||||
ChartOfAccount *ChartOfAccount `gorm:"foreignKey:ChartOfAccountID" json:"chart_of_account,omitempty"`
|
ChartOfAccount *ChartOfAccount `gorm:"foreignKey:ChartOfAccountID" json:"chart_of_account,omitempty"`
|
||||||
PurchaseCategory *PurchaseCategory `gorm:"foreignKey:PurchaseCategoryID" json:"purchase_category,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ExpenseItem) BeforeCreate(tx *gorm.DB) error {
|
func (e *ExpenseItem) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
|||||||
@@ -36,36 +36,34 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type InventoryMovement struct {
|
type InventoryMovement 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" validate:"required"`
|
OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id" validate:"required"`
|
||||||
OutletID uuid.UUID `gorm:"type:uuid;not null;index" json:"outlet_id" validate:"required"`
|
OutletID uuid.UUID `gorm:"type:uuid;not null;index" json:"outlet_id" validate:"required"`
|
||||||
ItemID uuid.UUID `gorm:"type:uuid;not null;index" json:"item_id" validate:"required"`
|
ItemID uuid.UUID `gorm:"type:uuid;not null;index" json:"item_id" validate:"required"`
|
||||||
ItemType string `gorm:"not null;size:20" json:"item_type" validate:"required"` // "PRODUCT" or "INGREDIENT"
|
ItemType string `gorm:"not null;size:20" json:"item_type" validate:"required"` // "PRODUCT" or "INGREDIENT"
|
||||||
MovementType InventoryMovementType `gorm:"not null;size:50" json:"movement_type" validate:"required"`
|
MovementType InventoryMovementType `gorm:"not null;size:50" json:"movement_type" validate:"required"`
|
||||||
Quantity float64 `gorm:"type:decimal(12,3);not null" json:"quantity" validate:"required"`
|
Quantity float64 `gorm:"type:decimal(12,3);not null" json:"quantity" validate:"required"`
|
||||||
PreviousQuantity float64 `gorm:"type:decimal(12,3)" json:"previous_quantity"`
|
PreviousQuantity float64 `gorm:"type:decimal(12,3)" json:"previous_quantity"`
|
||||||
NewQuantity float64 `gorm:"type:decimal(12,3)" json:"new_quantity"`
|
NewQuantity float64 `gorm:"type:decimal(12,3)" json:"new_quantity"`
|
||||||
UnitCost float64 `gorm:"type:decimal(12,2);default:0.00" json:"unit_cost"`
|
UnitCost float64 `gorm:"type:decimal(12,2);default:0.00" json:"unit_cost"`
|
||||||
TotalCost float64 `gorm:"type:decimal(12,2);default:0.00" json:"total_cost"`
|
TotalCost float64 `gorm:"type:decimal(12,2);default:0.00" json:"total_cost"`
|
||||||
ReferenceType *InventoryMovementReferenceType `gorm:"size:50" json:"reference_type"`
|
ReferenceType *InventoryMovementReferenceType `gorm:"size:50" json:"reference_type"`
|
||||||
ReferenceID *uuid.UUID `gorm:"type:uuid;index" json:"reference_id"`
|
ReferenceID *uuid.UUID `gorm:"type:uuid;index" json:"reference_id"`
|
||||||
PurchaseOrderItemID *uuid.UUID `gorm:"type:uuid;index" json:"purchase_order_item_id"`
|
OrderID *uuid.UUID `gorm:"type:uuid;index" json:"order_id"`
|
||||||
OrderID *uuid.UUID `gorm:"type:uuid;index" json:"order_id"`
|
PaymentID *uuid.UUID `gorm:"type:uuid;index" json:"payment_id"`
|
||||||
PaymentID *uuid.UUID `gorm:"type:uuid;index" json:"payment_id"`
|
UserID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_id" validate:"required"`
|
||||||
UserID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_id" validate:"required"`
|
Reason *string `gorm:"size:255" json:"reason"`
|
||||||
Reason *string `gorm:"size:255" json:"reason"`
|
Notes *string `gorm:"type:text" json:"notes"`
|
||||||
Notes *string `gorm:"type:text" json:"notes"`
|
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
|
||||||
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
|
||||||
|
|
||||||
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
||||||
Outlet Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
|
Outlet Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
|
||||||
Product *Product `gorm:"foreignKey:ItemID" json:"product,omitempty"`
|
Product *Product `gorm:"foreignKey:ItemID" json:"product,omitempty"`
|
||||||
Ingredient *Ingredient `gorm:"foreignKey:ItemID" json:"ingredient,omitempty"`
|
Ingredient *Ingredient `gorm:"foreignKey:ItemID" json:"ingredient,omitempty"`
|
||||||
PurchaseOrderItem *PurchaseOrderItem `gorm:"foreignKey:PurchaseOrderItemID" json:"purchase_order_item,omitempty"`
|
Order *Order `gorm:"foreignKey:OrderID" json:"order,omitempty"`
|
||||||
Order *Order `gorm:"foreignKey:OrderID" json:"order,omitempty"`
|
Payment *Payment `gorm:"foreignKey:PaymentID" json:"payment,omitempty"`
|
||||||
Payment *Payment `gorm:"foreignKey:PaymentID" json:"payment,omitempty"`
|
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||||
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (im *InventoryMovement) BeforeCreate(tx *gorm.DB) error {
|
func (im *InventoryMovement) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import (
|
|||||||
type PurchaseCategoryType string
|
type PurchaseCategoryType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
PurchaseCategoryTypeRawMaterial PurchaseCategoryType = "raw_material"
|
PurchaseCategoryTypeRawMaterial PurchaseCategoryType = "raw_material"
|
||||||
PurchaseCategoryTypeExpense PurchaseCategoryType = "expense"
|
PurchaseCategoryTypeNonInventory PurchaseCategoryType = "non_inventory"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PurchaseCategoryPreset struct {
|
type PurchaseCategoryPreset struct {
|
||||||
|
|||||||
@@ -41,21 +41,19 @@ func (PurchaseOrder) TableName() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PurchaseOrderItem struct {
|
type PurchaseOrderItem 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"`
|
||||||
PurchaseOrderID uuid.UUID `gorm:"type:uuid;not null" json:"purchase_order_id" validate:"required"`
|
PurchaseOrderID uuid.UUID `gorm:"type:uuid;not null" json:"purchase_order_id" validate:"required"`
|
||||||
IngredientID *uuid.UUID `gorm:"type:uuid" json:"ingredient_id" validate:"omitempty"`
|
IngredientID uuid.UUID `gorm:"type:uuid;not null" json:"ingredient_id" validate:"required"`
|
||||||
PurchaseCategoryID uuid.UUID `gorm:"type:uuid;not null;index" json:"purchase_category_id" validate:"required"`
|
Description *string `gorm:"type:text" json:"description" validate:"omitempty"`
|
||||||
Description *string `gorm:"type:text" json:"description" validate:"omitempty"`
|
Quantity float64 `gorm:"type:decimal(10,3);not null" json:"quantity" validate:"required,gt=0"`
|
||||||
Quantity *float64 `gorm:"type:decimal(10,3)" json:"quantity" validate:"omitempty,gt=0"`
|
UnitID uuid.UUID `gorm:"type:uuid;not null" json:"unit_id" validate:"required"`
|
||||||
UnitID *uuid.UUID `gorm:"type:uuid" json:"unit_id" validate:"omitempty"`
|
Amount float64 `gorm:"type:decimal(15,2);not null" json:"amount" validate:"required,gte=0"`
|
||||||
Amount float64 `gorm:"type:decimal(15,2);not null" json:"amount" validate:"required,gte=0"`
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
|
||||||
|
|
||||||
PurchaseOrder *PurchaseOrder `gorm:"foreignKey:PurchaseOrderID" json:"purchase_order,omitempty"`
|
PurchaseOrder *PurchaseOrder `gorm:"foreignKey:PurchaseOrderID" json:"purchase_order,omitempty"`
|
||||||
Ingredient *Ingredient `gorm:"foreignKey:IngredientID" json:"ingredient,omitempty"`
|
Ingredient *Ingredient `gorm:"foreignKey:IngredientID" json:"ingredient,omitempty"`
|
||||||
PurchaseCategory *PurchaseCategory `gorm:"foreignKey:PurchaseCategoryID" json:"purchase_category,omitempty"`
|
Unit *Unit `gorm:"foreignKey:UnitID" json:"unit,omitempty"`
|
||||||
Unit *Unit `gorm:"foreignKey:UnitID" json:"unit,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (poi *PurchaseOrderItem) BeforeCreate(tx *gorm.DB) error {
|
func (poi *PurchaseOrderItem) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
|||||||
@@ -210,59 +210,3 @@ func (h *AnalyticsHandler) GetProfitLossAnalytics(c *gin.Context) {
|
|||||||
contractResp := transformer.ProfitLossAnalyticsModelToContract(response)
|
contractResp := transformer.ProfitLossAnalyticsModelToContract(response)
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "AnalyticsHandler::GetProfitLossAnalytics")
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "AnalyticsHandler::GetProfitLossAnalytics")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *AnalyticsHandler) GetExclusiveSummaryPeriod(c *gin.Context) {
|
|
||||||
ctx := c.Request.Context()
|
|
||||||
contextInfo := appcontext.FromGinContext(ctx)
|
|
||||||
|
|
||||||
var req contract.ExclusiveSummaryPeriodRequest
|
|
||||||
if err := c.ShouldBindQuery(&req); err != nil {
|
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "AnalyticsHandler::GetExclusiveSummaryPeriod", err.Error())}), "AnalyticsHandler::GetExclusiveSummaryPeriod")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
req.OrganizationID = contextInfo.OrganizationID
|
|
||||||
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
|
||||||
modelReq, err := transformer.ExclusiveSummaryPeriodContractToModel(&req)
|
|
||||||
if err != nil {
|
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "AnalyticsHandler::GetExclusiveSummaryPeriod", err.Error())}), "AnalyticsHandler::GetExclusiveSummaryPeriod")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response, err := h.analyticsService.GetExclusiveSummaryPeriod(ctx, modelReq)
|
|
||||||
if err != nil {
|
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "AnalyticsHandler::GetExclusiveSummaryPeriod", err.Error())}), "AnalyticsHandler::GetExclusiveSummaryPeriod")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
contractResp := transformer.ExclusiveSummaryPeriodModelToContract(response)
|
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "AnalyticsHandler::GetExclusiveSummaryPeriod")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *AnalyticsHandler) GetExclusiveSummaryMonthly(c *gin.Context) {
|
|
||||||
ctx := c.Request.Context()
|
|
||||||
contextInfo := appcontext.FromGinContext(ctx)
|
|
||||||
|
|
||||||
var req contract.ExclusiveSummaryMonthlyRequest
|
|
||||||
if err := c.ShouldBindQuery(&req); err != nil {
|
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "AnalyticsHandler::GetExclusiveSummaryMonthly", err.Error())}), "AnalyticsHandler::GetExclusiveSummaryMonthly")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
req.OrganizationID = contextInfo.OrganizationID
|
|
||||||
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
|
||||||
modelReq, err := transformer.ExclusiveSummaryMonthlyContractToModel(&req)
|
|
||||||
if err != nil {
|
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "AnalyticsHandler::GetExclusiveSummaryMonthly", err.Error())}), "AnalyticsHandler::GetExclusiveSummaryMonthly")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response, err := h.analyticsService.GetExclusiveSummaryMonthly(ctx, modelReq)
|
|
||||||
if err != nil {
|
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "AnalyticsHandler::GetExclusiveSummaryMonthly", err.Error())}), "AnalyticsHandler::GetExclusiveSummaryMonthly")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
contractResp := transformer.ExclusiveSummaryMonthlyModelToContract(response)
|
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "AnalyticsHandler::GetExclusiveSummaryMonthly")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -95,27 +95,20 @@ func ExpenseItemEntityToResponse(entity *entities.ExpenseItem) *models.ExpenseIt
|
|||||||
}
|
}
|
||||||
|
|
||||||
response := &models.ExpenseItemResponse{
|
response := &models.ExpenseItemResponse{
|
||||||
ID: entity.ID,
|
ID: entity.ID,
|
||||||
ExpenseID: entity.ExpenseID,
|
ExpenseID: entity.ExpenseID,
|
||||||
ChartOfAccountID: entity.ChartOfAccountID,
|
ChartOfAccountID: entity.ChartOfAccountID,
|
||||||
PurchaseCategoryID: entity.PurchaseCategoryID,
|
Item: entity.Item,
|
||||||
Item: entity.Item,
|
Description: entity.Description,
|
||||||
Description: entity.Description,
|
Amount: entity.Amount,
|
||||||
Amount: entity.Amount,
|
CreatedAt: entity.CreatedAt,
|
||||||
CreatedAt: entity.CreatedAt,
|
UpdatedAt: entity.UpdatedAt,
|
||||||
UpdatedAt: entity.UpdatedAt,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if entity.ChartOfAccount != nil {
|
if entity.ChartOfAccount != nil {
|
||||||
response.ChartOfAccountName = entity.ChartOfAccount.Name
|
response.ChartOfAccountName = entity.ChartOfAccount.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
if entity.PurchaseCategory != nil {
|
|
||||||
response.PurchaseCategoryName = entity.PurchaseCategory.Name
|
|
||||||
response.PurchaseCategoryType = string(entity.PurchaseCategory.Type)
|
|
||||||
response.PurchaseCategory = PurchaseCategoryEntityToResponse(entity.PurchaseCategory)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,16 +91,15 @@ func PurchaseOrderItemEntityToModel(entity *entities.PurchaseOrderItem) *models.
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &models.PurchaseOrderItem{
|
return &models.PurchaseOrderItem{
|
||||||
ID: entity.ID,
|
ID: entity.ID,
|
||||||
PurchaseOrderID: entity.PurchaseOrderID,
|
PurchaseOrderID: entity.PurchaseOrderID,
|
||||||
IngredientID: entity.IngredientID,
|
IngredientID: entity.IngredientID,
|
||||||
PurchaseCategoryID: entity.PurchaseCategoryID,
|
Description: entity.Description,
|
||||||
Description: entity.Description,
|
Quantity: entity.Quantity,
|
||||||
Quantity: entity.Quantity,
|
UnitID: entity.UnitID,
|
||||||
UnitID: entity.UnitID,
|
Amount: entity.Amount,
|
||||||
Amount: entity.Amount,
|
CreatedAt: entity.CreatedAt,
|
||||||
CreatedAt: entity.CreatedAt,
|
UpdatedAt: entity.UpdatedAt,
|
||||||
UpdatedAt: entity.UpdatedAt,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,16 +109,15 @@ func PurchaseOrderItemModelToEntity(model *models.PurchaseOrderItem) *entities.P
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &entities.PurchaseOrderItem{
|
return &entities.PurchaseOrderItem{
|
||||||
ID: model.ID,
|
ID: model.ID,
|
||||||
PurchaseOrderID: model.PurchaseOrderID,
|
PurchaseOrderID: model.PurchaseOrderID,
|
||||||
IngredientID: model.IngredientID,
|
IngredientID: model.IngredientID,
|
||||||
PurchaseCategoryID: model.PurchaseCategoryID,
|
Description: model.Description,
|
||||||
Description: model.Description,
|
Quantity: model.Quantity,
|
||||||
Quantity: model.Quantity,
|
UnitID: model.UnitID,
|
||||||
UnitID: model.UnitID,
|
Amount: model.Amount,
|
||||||
Amount: model.Amount,
|
CreatedAt: model.CreatedAt,
|
||||||
CreatedAt: model.CreatedAt,
|
UpdatedAt: model.UpdatedAt,
|
||||||
UpdatedAt: model.UpdatedAt,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,16 +127,15 @@ func PurchaseOrderItemEntityToResponse(entity *entities.PurchaseOrderItem) *mode
|
|||||||
}
|
}
|
||||||
|
|
||||||
response := &models.PurchaseOrderItemResponse{
|
response := &models.PurchaseOrderItemResponse{
|
||||||
ID: entity.ID,
|
ID: entity.ID,
|
||||||
PurchaseOrderID: entity.PurchaseOrderID,
|
PurchaseOrderID: entity.PurchaseOrderID,
|
||||||
IngredientID: entity.IngredientID,
|
IngredientID: entity.IngredientID,
|
||||||
PurchaseCategoryID: entity.PurchaseCategoryID,
|
Description: entity.Description,
|
||||||
Description: entity.Description,
|
Quantity: entity.Quantity,
|
||||||
Quantity: entity.Quantity,
|
UnitID: entity.UnitID,
|
||||||
UnitID: entity.UnitID,
|
Amount: entity.Amount,
|
||||||
Amount: entity.Amount,
|
CreatedAt: entity.CreatedAt,
|
||||||
CreatedAt: entity.CreatedAt,
|
UpdatedAt: entity.UpdatedAt,
|
||||||
UpdatedAt: entity.UpdatedAt,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map ingredient if present
|
// Map ingredient if present
|
||||||
@@ -149,10 +146,6 @@ func PurchaseOrderItemEntityToResponse(entity *entities.PurchaseOrderItem) *mode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if entity.PurchaseCategory != nil {
|
|
||||||
response.PurchaseCategory = PurchaseCategoryEntityToResponse(entity.PurchaseCategory)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map unit if present
|
// Map unit if present
|
||||||
if entity.Unit != nil {
|
if entity.Unit != nil {
|
||||||
response.Unit = &models.UnitResponse{
|
response.Unit = &models.UnitResponse{
|
||||||
|
|||||||
@@ -113,11 +113,7 @@ type PurchasingAnalyticsResponse struct {
|
|||||||
// PurchasingSummary represents the summary of purchasing analytics
|
// PurchasingSummary represents the summary of purchasing analytics
|
||||||
type PurchasingSummary struct {
|
type PurchasingSummary struct {
|
||||||
TotalPurchases float64 `json:"total_purchases"`
|
TotalPurchases float64 `json:"total_purchases"`
|
||||||
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
|
||||||
ExpensePurchases float64 `json:"expense_purchases"`
|
|
||||||
TotalPurchaseOrders int64 `json:"total_purchase_orders"`
|
TotalPurchaseOrders int64 `json:"total_purchase_orders"`
|
||||||
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
|
||||||
ExpenseCount int64 `json:"expense_count"`
|
|
||||||
TotalQuantity float64 `json:"total_quantity"`
|
TotalQuantity float64 `json:"total_quantity"`
|
||||||
AveragePurchaseOrderValue float64 `json:"average_purchase_order_value"`
|
AveragePurchaseOrderValue float64 `json:"average_purchase_order_value"`
|
||||||
TotalIngredients int64 `json:"total_ingredients"`
|
TotalIngredients int64 `json:"total_ingredients"`
|
||||||
@@ -126,16 +122,12 @@ type PurchasingSummary struct {
|
|||||||
|
|
||||||
// PurchasingAnalyticsData represents purchasing analytics by time period
|
// PurchasingAnalyticsData represents purchasing analytics by time period
|
||||||
type PurchasingAnalyticsData struct {
|
type PurchasingAnalyticsData struct {
|
||||||
Date time.Time `json:"date"`
|
Date time.Time `json:"date"`
|
||||||
Purchases float64 `json:"purchases"`
|
Purchases float64 `json:"purchases"`
|
||||||
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
PurchaseOrders int64 `json:"purchase_orders"`
|
||||||
ExpensePurchases float64 `json:"expense_purchases"`
|
Quantity float64 `json:"quantity"`
|
||||||
PurchaseOrders int64 `json:"purchase_orders"`
|
Ingredients int64 `json:"ingredients"`
|
||||||
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
Vendors int64 `json:"vendors"`
|
||||||
ExpenseCount int64 `json:"expense_count"`
|
|
||||||
Quantity float64 `json:"quantity"`
|
|
||||||
Ingredients int64 `json:"ingredients"`
|
|
||||||
Vendors int64 `json:"vendors"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PurchasingIngredientData represents purchasing analytics for an ingredient
|
// PurchasingIngredientData represents purchasing analytics for an ingredient
|
||||||
@@ -334,114 +326,3 @@ type OperationalExpenseItem struct {
|
|||||||
Item string `json:"item"`
|
Item string `json:"item"`
|
||||||
Nominal float64 `json:"nominal"`
|
Nominal float64 `json:"nominal"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExclusiveSummaryPeriodRequest struct {
|
|
||||||
OrganizationID uuid.UUID `validate:"required"`
|
|
||||||
OutletID *uuid.UUID `validate:"omitempty"`
|
|
||||||
DateFrom time.Time `validate:"required"`
|
|
||||||
DateTo time.Time `validate:"required"`
|
|
||||||
ExcludeGajiStaffFromReimburse bool `validate:"omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExclusiveSummaryMonthlyRequest struct {
|
|
||||||
OrganizationID uuid.UUID `validate:"required"`
|
|
||||||
OutletID *uuid.UUID `validate:"omitempty"`
|
|
||||||
Month time.Time `validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExclusiveSummaryPeriodResponse struct {
|
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
|
||||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
|
||||||
Period ExclusiveSummaryPeriodRange `json:"period"`
|
|
||||||
Summary ExclusiveSummaryPeriodSummary `json:"summary"`
|
|
||||||
Reimburse ExclusiveSummaryReimburse `json:"reimburse"`
|
|
||||||
HPPBreakdown []ExclusiveSummaryCategoryBreakdown `json:"hpp_breakdown"`
|
|
||||||
OperationalExpenseBreakdown []ExclusiveSummaryCategoryBreakdown `json:"operational_expense_breakdown"`
|
|
||||||
DailySummary []ExclusiveSummaryDailySummary `json:"daily_summary"`
|
|
||||||
DailyTransactions []ExclusiveSummaryDailyTransaction `json:"daily_transactions"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExclusiveSummaryPeriodRange struct {
|
|
||||||
DateFrom time.Time `json:"date_from"`
|
|
||||||
DateTo time.Time `json:"date_to"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExclusiveSummaryPeriodSummary struct {
|
|
||||||
Sales float64 `json:"sales"`
|
|
||||||
HPP float64 `json:"hpp"`
|
|
||||||
GrossProfit float64 `json:"gross_profit"`
|
|
||||||
SalaryTotal float64 `json:"salary_total"`
|
|
||||||
SalaryDW float64 `json:"salary_dw"`
|
|
||||||
SalaryStaff float64 `json:"salary_staff"`
|
|
||||||
SalaryOther float64 `json:"salary_other"`
|
|
||||||
OtherOperationalExpenses float64 `json:"other_operational_expenses"`
|
|
||||||
OperationalExpensesTotal float64 `json:"operational_expenses_total"`
|
|
||||||
TotalCost float64 `json:"total_cost"`
|
|
||||||
NetProfit float64 `json:"net_profit"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExclusiveSummaryReimburse struct {
|
|
||||||
TotalCost float64 `json:"total_cost"`
|
|
||||||
ExcludedSalaryStaff float64 `json:"excluded_salary_staff"`
|
|
||||||
TotalReimburse float64 `json:"total_reimburse"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExclusiveSummaryCategoryBreakdown struct {
|
|
||||||
CategoryCode string `json:"category_code"`
|
|
||||||
CategoryName string `json:"category_name"`
|
|
||||||
Amount float64 `json:"amount"`
|
|
||||||
Percentage float64 `json:"percentage"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExclusiveSummaryDailySummary struct {
|
|
||||||
Date time.Time `json:"date"`
|
|
||||||
TransactionCount int64 `json:"transaction_count"`
|
|
||||||
TotalCost float64 `json:"total_cost"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExclusiveSummaryDailyTransaction struct {
|
|
||||||
Date time.Time `json:"date"`
|
|
||||||
CategoryCode string `json:"category_code"`
|
|
||||||
CategoryName string `json:"category_name"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Amount float64 `json:"amount"`
|
|
||||||
Source string `json:"source"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExclusiveSummaryMonthlyResponse struct {
|
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
|
||||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
|
||||||
Month string `json:"month"`
|
|
||||||
Summary ExclusiveSummaryMonthlySummary `json:"summary"`
|
|
||||||
Periods []ExclusiveSummaryMonthlyPeriod `json:"periods"`
|
|
||||||
BankBalance []ExclusiveSummaryBankBalance `json:"bank_balance"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExclusiveSummaryMonthlySummary struct {
|
|
||||||
TotalSales float64 `json:"total_sales"`
|
|
||||||
HPP float64 `json:"hpp"`
|
|
||||||
GrossProfit float64 `json:"gross_profit"`
|
|
||||||
OperationalExpensesTotal float64 `json:"operational_expenses_total"`
|
|
||||||
TotalCost float64 `json:"total_cost"`
|
|
||||||
NetProfit float64 `json:"net_profit"`
|
|
||||||
NetProfitMargin float64 `json:"net_profit_margin"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExclusiveSummaryMonthlyPeriod struct {
|
|
||||||
Label string `json:"label"`
|
|
||||||
DateFrom time.Time `json:"date_from"`
|
|
||||||
DateTo time.Time `json:"date_to"`
|
|
||||||
Sales float64 `json:"sales"`
|
|
||||||
HPP float64 `json:"hpp"`
|
|
||||||
GrossProfit float64 `json:"gross_profit"`
|
|
||||||
GrossMargin float64 `json:"gross_margin"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExclusiveSummaryBankBalance struct {
|
|
||||||
Bank string `json:"bank"`
|
|
||||||
OpeningBalance *float64 `json:"opening_balance"`
|
|
||||||
IncomingMutation *float64 `json:"incoming_mutation"`
|
|
||||||
OutgoingMutation *float64 `json:"outgoing_mutation"`
|
|
||||||
ClosingBalance *float64 `json:"closing_balance"`
|
|
||||||
Notes *string `json:"notes"`
|
|
||||||
}
|
|
||||||
|
|||||||
+34
-51
@@ -23,15 +23,14 @@ type Expense struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseItem struct {
|
type ExpenseItem struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
ExpenseID uuid.UUID `json:"expense_id"`
|
ExpenseID uuid.UUID `json:"expense_id"`
|
||||||
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
||||||
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
Item string `json:"item"`
|
||||||
Item string `json:"item"`
|
Description *string `json:"description"`
|
||||||
Description *string `json:"description"`
|
Amount float64 `json:"amount"`
|
||||||
Amount float64 `json:"amount"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseResponse struct {
|
type ExpenseResponse struct {
|
||||||
@@ -52,19 +51,15 @@ type ExpenseResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseItemResponse struct {
|
type ExpenseItemResponse struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
ExpenseID uuid.UUID `json:"expense_id"`
|
ExpenseID uuid.UUID `json:"expense_id"`
|
||||||
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
||||||
ChartOfAccountName string `json:"chart_of_account_name,omitempty"`
|
ChartOfAccountName string `json:"chart_of_account_name,omitempty"`
|
||||||
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
Item string `json:"item"`
|
||||||
PurchaseCategoryName string `json:"purchase_category_name,omitempty"`
|
Description *string `json:"description"`
|
||||||
PurchaseCategoryType string `json:"purchase_category_type,omitempty"`
|
Amount float64 `json:"amount"`
|
||||||
PurchaseCategory *PurchaseCategoryResponse `json:"purchase_category,omitempty"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
Item string `json:"item"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
Description *string `json:"description"`
|
|
||||||
Amount float64 `json:"amount"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateExpenseRequest struct {
|
type CreateExpenseRequest struct {
|
||||||
@@ -80,11 +75,10 @@ type CreateExpenseRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreateExpenseItemRequest struct {
|
type CreateExpenseItemRequest struct {
|
||||||
ChartOfAccountID string `json:"chart_of_account_id"`
|
ChartOfAccountID string `json:"chart_of_account_id"`
|
||||||
PurchaseCategoryID string `json:"purchase_category_id"`
|
Item string `json:"item"`
|
||||||
Item string `json:"item"`
|
Description *string `json:"description,omitempty"`
|
||||||
Description *string `json:"description,omitempty"`
|
Amount float64 `json:"amount"`
|
||||||
Amount float64 `json:"amount"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateExpenseRequest struct {
|
type UpdateExpenseRequest struct {
|
||||||
@@ -101,11 +95,10 @@ type UpdateExpenseRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UpdateExpenseItemRequest struct {
|
type UpdateExpenseItemRequest struct {
|
||||||
ChartOfAccountID *string `json:"chart_of_account_id,omitempty"`
|
ChartOfAccountID *string `json:"chart_of_account_id,omitempty"`
|
||||||
PurchaseCategoryID *string `json:"purchase_category_id,omitempty"`
|
Item *string `json:"item,omitempty"`
|
||||||
Item *string `json:"item,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
Description *string `json:"description,omitempty"`
|
Amount *float64 `json:"amount,omitempty"`
|
||||||
Amount *float64 `json:"amount,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListExpenseRequest struct {
|
type ListExpenseRequest struct {
|
||||||
@@ -135,16 +128,15 @@ type ExpenseAnalyticsRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseAnalyticsResponse struct {
|
type ExpenseAnalyticsResponse 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"`
|
||||||
DateFrom time.Time `json:"date_from"`
|
DateFrom time.Time `json:"date_from"`
|
||||||
DateTo time.Time `json:"date_to"`
|
DateTo time.Time `json:"date_to"`
|
||||||
GroupBy string `json:"group_by"`
|
GroupBy string `json:"group_by"`
|
||||||
Summary ExpenseAnalyticsSummary `json:"summary"`
|
Summary ExpenseAnalyticsSummary `json:"summary"`
|
||||||
Data []ExpenseAnalyticsData `json:"data"`
|
Data []ExpenseAnalyticsData `json:"data"`
|
||||||
CategoryData []ExpenseAnalyticsCategoryData `json:"category_data"`
|
CategoryData []ExpenseAnalyticsCategoryData `json:"category_data"`
|
||||||
ChartOfAccountData []ExpenseAnalyticsChartOfAccountData `json:"chart_of_account_data"`
|
ItemData []ExpenseAnalyticsItemData `json:"item_data"`
|
||||||
ItemData []ExpenseAnalyticsItemData `json:"item_data"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseAnalyticsSummary struct {
|
type ExpenseAnalyticsSummary struct {
|
||||||
@@ -166,15 +158,6 @@ type ExpenseAnalyticsData struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseAnalyticsCategoryData struct {
|
type ExpenseAnalyticsCategoryData struct {
|
||||||
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
|
||||||
PurchaseCategoryName string `json:"purchase_category_name"`
|
|
||||||
PurchaseCategoryType string `json:"purchase_category_type"`
|
|
||||||
TotalAmount float64 `json:"total_amount"`
|
|
||||||
ExpenseCount int64 `json:"expense_count"`
|
|
||||||
ItemCount int64 `json:"item_count"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExpenseAnalyticsChartOfAccountData struct {
|
|
||||||
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
||||||
ChartOfAccountName string `json:"chart_of_account_name"`
|
ChartOfAccountName string `json:"chart_of_account_name"`
|
||||||
TotalAmount float64 `json:"total_amount"`
|
TotalAmount float64 `json:"total_amount"`
|
||||||
|
|||||||
@@ -22,16 +22,15 @@ type PurchaseOrder struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PurchaseOrderItem struct {
|
type PurchaseOrderItem struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
PurchaseOrderID uuid.UUID `json:"purchase_order_id"`
|
PurchaseOrderID uuid.UUID `json:"purchase_order_id"`
|
||||||
IngredientID *uuid.UUID `json:"ingredient_id"`
|
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||||
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
Description *string `json:"description"`
|
||||||
Description *string `json:"description"`
|
Quantity float64 `json:"quantity"`
|
||||||
Quantity *float64 `json:"quantity"`
|
UnitID uuid.UUID `json:"unit_id"`
|
||||||
UnitID *uuid.UUID `json:"unit_id"`
|
Amount float64 `json:"amount"`
|
||||||
Amount float64 `json:"amount"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PurchaseOrderAttachment struct {
|
type PurchaseOrderAttachment struct {
|
||||||
@@ -60,19 +59,17 @@ type PurchaseOrderResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PurchaseOrderItemResponse struct {
|
type PurchaseOrderItemResponse struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
PurchaseOrderID uuid.UUID `json:"purchase_order_id"`
|
PurchaseOrderID uuid.UUID `json:"purchase_order_id"`
|
||||||
IngredientID *uuid.UUID `json:"ingredient_id"`
|
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||||
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
Description *string `json:"description"`
|
||||||
Description *string `json:"description"`
|
Quantity float64 `json:"quantity"`
|
||||||
Quantity *float64 `json:"quantity"`
|
UnitID uuid.UUID `json:"unit_id"`
|
||||||
UnitID *uuid.UUID `json:"unit_id"`
|
Amount float64 `json:"amount"`
|
||||||
Amount float64 `json:"amount"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
Ingredient *IngredientResponse `json:"ingredient,omitempty"`
|
||||||
Ingredient *IngredientResponse `json:"ingredient,omitempty"`
|
Unit *UnitResponse `json:"unit,omitempty"`
|
||||||
PurchaseCategory *PurchaseCategoryResponse `json:"purchase_category,omitempty"`
|
|
||||||
Unit *UnitResponse `json:"unit,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PurchaseOrderAttachmentResponse struct {
|
type PurchaseOrderAttachmentResponse struct {
|
||||||
@@ -96,12 +93,11 @@ type CreatePurchaseOrderRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreatePurchaseOrderItemRequest struct {
|
type CreatePurchaseOrderItemRequest struct {
|
||||||
IngredientID *uuid.UUID `json:"ingredient_id,omitempty"`
|
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||||
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
Description *string `json:"description,omitempty"`
|
||||||
Description *string `json:"description,omitempty"`
|
Quantity float64 `json:"quantity"`
|
||||||
Quantity *float64 `json:"quantity,omitempty"`
|
UnitID uuid.UUID `json:"unit_id"`
|
||||||
UnitID *uuid.UUID `json:"unit_id,omitempty"`
|
Amount float64 `json:"amount"`
|
||||||
Amount float64 `json:"amount"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdatePurchaseOrderRequest struct {
|
type UpdatePurchaseOrderRequest struct {
|
||||||
@@ -117,13 +113,12 @@ type UpdatePurchaseOrderRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UpdatePurchaseOrderItemRequest struct {
|
type UpdatePurchaseOrderItemRequest struct {
|
||||||
ID *uuid.UUID `json:"id,omitempty"` // For existing items
|
ID *uuid.UUID `json:"id,omitempty"` // For existing items
|
||||||
IngredientID *uuid.UUID `json:"ingredient_id,omitempty"`
|
IngredientID *uuid.UUID `json:"ingredient_id,omitempty"`
|
||||||
PurchaseCategoryID *uuid.UUID `json:"purchase_category_id,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
Description *string `json:"description,omitempty"`
|
Quantity *float64 `json:"quantity,omitempty"`
|
||||||
Quantity *float64 `json:"quantity,omitempty"`
|
UnitID *uuid.UUID `json:"unit_id,omitempty"`
|
||||||
UnitID *uuid.UUID `json:"unit_id,omitempty"`
|
Amount *float64 `json:"amount,omitempty"`
|
||||||
Amount *float64 `json:"amount,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListPurchaseOrdersRequest struct {
|
type ListPurchaseOrdersRequest struct {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"strings"
|
"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"
|
||||||
)
|
)
|
||||||
@@ -19,8 +18,6 @@ type AnalyticsProcessor interface {
|
|||||||
GetProductAnalyticsPerCategory(ctx context.Context, req *models.ProductAnalyticsPerCategoryRequest) (*models.ProductAnalyticsPerCategoryResponse, error)
|
GetProductAnalyticsPerCategory(ctx context.Context, req *models.ProductAnalyticsPerCategoryRequest) (*models.ProductAnalyticsPerCategoryResponse, error)
|
||||||
GetDashboardAnalytics(ctx context.Context, req *models.DashboardAnalyticsRequest) (*models.DashboardAnalyticsResponse, error)
|
GetDashboardAnalytics(ctx context.Context, req *models.DashboardAnalyticsRequest) (*models.DashboardAnalyticsResponse, error)
|
||||||
GetProfitLossAnalytics(ctx context.Context, req *models.ProfitLossAnalyticsRequest) (*models.ProfitLossAnalyticsResponse, error)
|
GetProfitLossAnalytics(ctx context.Context, req *models.ProfitLossAnalyticsRequest) (*models.ProfitLossAnalyticsResponse, error)
|
||||||
GetExclusiveSummaryPeriod(ctx context.Context, req *models.ExclusiveSummaryPeriodRequest) (*models.ExclusiveSummaryPeriodResponse, error)
|
|
||||||
GetExclusiveSummaryMonthly(ctx context.Context, req *models.ExclusiveSummaryMonthlyRequest) (*models.ExclusiveSummaryMonthlyResponse, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AnalyticsProcessorImpl struct {
|
type AnalyticsProcessorImpl struct {
|
||||||
@@ -188,16 +185,12 @@ func (p *AnalyticsProcessorImpl) GetPurchasingAnalytics(ctx context.Context, req
|
|||||||
data := make([]models.PurchasingAnalyticsData, len(result.Data))
|
data := make([]models.PurchasingAnalyticsData, len(result.Data))
|
||||||
for i, item := range result.Data {
|
for i, item := range result.Data {
|
||||||
data[i] = models.PurchasingAnalyticsData{
|
data[i] = models.PurchasingAnalyticsData{
|
||||||
Date: item.Date,
|
Date: item.Date,
|
||||||
Purchases: item.Purchases,
|
Purchases: item.Purchases,
|
||||||
RawMaterialPurchases: item.RawMaterialPurchases,
|
PurchaseOrders: item.PurchaseOrders,
|
||||||
ExpensePurchases: item.ExpensePurchases,
|
Quantity: item.Quantity,
|
||||||
PurchaseOrders: item.PurchaseOrders,
|
Ingredients: item.Ingredients,
|
||||||
RawMaterialPurchaseOrders: item.RawMaterialPurchaseOrders,
|
Vendors: item.Vendors,
|
||||||
ExpenseCount: item.ExpenseCount,
|
|
||||||
Quantity: item.Quantity,
|
|
||||||
Ingredients: item.Ingredients,
|
|
||||||
Vendors: item.Vendors,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,11 +227,7 @@ func (p *AnalyticsProcessorImpl) GetPurchasingAnalytics(ctx context.Context, req
|
|||||||
GroupBy: req.GroupBy,
|
GroupBy: req.GroupBy,
|
||||||
Summary: models.PurchasingSummary{
|
Summary: models.PurchasingSummary{
|
||||||
TotalPurchases: result.Summary.TotalPurchases,
|
TotalPurchases: result.Summary.TotalPurchases,
|
||||||
RawMaterialPurchases: result.Summary.RawMaterialPurchases,
|
|
||||||
ExpensePurchases: result.Summary.ExpensePurchases,
|
|
||||||
TotalPurchaseOrders: result.Summary.TotalPurchaseOrders,
|
TotalPurchaseOrders: result.Summary.TotalPurchaseOrders,
|
||||||
RawMaterialPurchaseOrders: result.Summary.RawMaterialPurchaseOrders,
|
|
||||||
ExpenseCount: result.Summary.ExpenseCount,
|
|
||||||
TotalQuantity: result.Summary.TotalQuantity,
|
TotalQuantity: result.Summary.TotalQuantity,
|
||||||
AveragePurchaseOrderValue: result.Summary.AveragePurchaseOrderValue,
|
AveragePurchaseOrderValue: result.Summary.AveragePurchaseOrderValue,
|
||||||
TotalIngredients: result.Summary.TotalIngredients,
|
TotalIngredients: result.Summary.TotalIngredients,
|
||||||
@@ -654,266 +643,3 @@ func slugify(s string) string {
|
|||||||
}
|
}
|
||||||
return string(result)
|
return string(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *AnalyticsProcessorImpl) GetExclusiveSummaryPeriod(ctx context.Context, req *models.ExclusiveSummaryPeriodRequest) (*models.ExclusiveSummaryPeriodResponse, error) {
|
|
||||||
if req.DateFrom.After(req.DateTo) {
|
|
||||||
return nil, fmt.Errorf("date_from cannot be after date_to")
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.buildExclusiveSummaryPeriod(ctx, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *AnalyticsProcessorImpl) GetExclusiveSummaryMonthly(ctx context.Context, req *models.ExclusiveSummaryMonthlyRequest) (*models.ExclusiveSummaryMonthlyResponse, error) {
|
|
||||||
monthStart := time.Date(req.Month.Year(), req.Month.Month(), 1, 0, 0, 0, 0, req.Month.Location())
|
|
||||||
monthEnd := monthStart.AddDate(0, 1, 0).Add(-time.Nanosecond)
|
|
||||||
|
|
||||||
fullPeriod, err := p.buildExclusiveSummaryPeriod(ctx, &models.ExclusiveSummaryPeriodRequest{
|
|
||||||
OrganizationID: req.OrganizationID,
|
|
||||||
OutletID: req.OutletID,
|
|
||||||
DateFrom: monthStart,
|
|
||||||
DateTo: monthEnd,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
periods := make([]models.ExclusiveSummaryMonthlyPeriod, 0)
|
|
||||||
for _, bucket := range buildExclusiveSummaryMonthlyBuckets(monthStart) {
|
|
||||||
period, err := p.buildExclusiveSummaryPeriod(ctx, &models.ExclusiveSummaryPeriodRequest{
|
|
||||||
OrganizationID: req.OrganizationID,
|
|
||||||
OutletID: req.OutletID,
|
|
||||||
DateFrom: bucket.DateFrom,
|
|
||||||
DateTo: bucket.DateTo,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
periods = append(periods, models.ExclusiveSummaryMonthlyPeriod{
|
|
||||||
Label: bucket.Label,
|
|
||||||
DateFrom: bucket.DateFrom,
|
|
||||||
DateTo: bucket.DateTo,
|
|
||||||
Sales: period.Summary.Sales,
|
|
||||||
HPP: period.Summary.HPP,
|
|
||||||
GrossProfit: period.Summary.GrossProfit,
|
|
||||||
GrossMargin: percentage(period.Summary.GrossProfit, period.Summary.Sales),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
bankBalances, err := p.analyticsRepo.GetExclusiveSummaryBankBalances(ctx, req.OrganizationID, req.OutletID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get exclusive summary bank balances: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
bankBalance := make([]models.ExclusiveSummaryBankBalance, len(bankBalances))
|
|
||||||
for i, item := range bankBalances {
|
|
||||||
bankBalance[i] = models.ExclusiveSummaryBankBalance{
|
|
||||||
Bank: item.Bank,
|
|
||||||
OpeningBalance: item.OpeningBalance,
|
|
||||||
IncomingMutation: item.IncomingMutation,
|
|
||||||
OutgoingMutation: item.OutgoingMutation,
|
|
||||||
ClosingBalance: item.ClosingBalance,
|
|
||||||
Notes: item.Notes,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &models.ExclusiveSummaryMonthlyResponse{
|
|
||||||
OrganizationID: req.OrganizationID,
|
|
||||||
OutletID: req.OutletID,
|
|
||||||
Month: monthStart.Format("2006-01"),
|
|
||||||
Summary: models.ExclusiveSummaryMonthlySummary{
|
|
||||||
TotalSales: fullPeriod.Summary.Sales,
|
|
||||||
HPP: fullPeriod.Summary.HPP,
|
|
||||||
GrossProfit: fullPeriod.Summary.GrossProfit,
|
|
||||||
OperationalExpensesTotal: fullPeriod.Summary.OperationalExpensesTotal,
|
|
||||||
TotalCost: fullPeriod.Summary.TotalCost,
|
|
||||||
NetProfit: fullPeriod.Summary.NetProfit,
|
|
||||||
NetProfitMargin: percentage(fullPeriod.Summary.NetProfit, fullPeriod.Summary.Sales),
|
|
||||||
},
|
|
||||||
Periods: periods,
|
|
||||||
BankBalance: bankBalance,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *AnalyticsProcessorImpl) buildExclusiveSummaryPeriod(ctx context.Context, req *models.ExclusiveSummaryPeriodRequest) (*models.ExclusiveSummaryPeriodResponse, error) {
|
|
||||||
result, err := p.analyticsRepo.GetExclusiveSummaryAnalytics(ctx, req.OrganizationID, req.OutletID, req.DateFrom, req.DateTo)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get exclusive summary analytics: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
hppBreakdown, hppTotal := exclusiveSummaryCategoryBreakdown(result.HPPBreakdown)
|
|
||||||
operationalBreakdown, operationalTotal := exclusiveSummaryCategoryBreakdown(result.OperationalExpenseBreakdown)
|
|
||||||
salaryDW, salaryStaff, salaryOther := exclusiveSummarySalaryBreakdown(result.DailyTransactions)
|
|
||||||
salaryTotal := salaryDW + salaryStaff + salaryOther
|
|
||||||
otherOperationalExpenses := operationalTotal - salaryTotal
|
|
||||||
if otherOperationalExpenses < 0 {
|
|
||||||
otherOperationalExpenses = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
grossProfit := result.SalesTotal - hppTotal
|
|
||||||
totalCost := hppTotal + operationalTotal
|
|
||||||
netProfit := result.SalesTotal - totalCost
|
|
||||||
excludedSalaryStaff := 0.0
|
|
||||||
if req.ExcludeGajiStaffFromReimburse {
|
|
||||||
excludedSalaryStaff = salaryStaff
|
|
||||||
}
|
|
||||||
|
|
||||||
dailySummary := make([]models.ExclusiveSummaryDailySummary, len(result.DailySummary))
|
|
||||||
for i, item := range result.DailySummary {
|
|
||||||
dailySummary[i] = models.ExclusiveSummaryDailySummary{
|
|
||||||
Date: item.Date,
|
|
||||||
TransactionCount: item.TransactionCount,
|
|
||||||
TotalCost: item.TotalCost,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dailyTransactions := make([]models.ExclusiveSummaryDailyTransaction, len(result.DailyTransactions))
|
|
||||||
for i, item := range result.DailyTransactions {
|
|
||||||
dailyTransactions[i] = models.ExclusiveSummaryDailyTransaction{
|
|
||||||
Date: item.Date,
|
|
||||||
CategoryCode: item.CategoryCode,
|
|
||||||
CategoryName: item.CategoryName,
|
|
||||||
Description: item.Description,
|
|
||||||
Amount: item.Amount,
|
|
||||||
Source: item.Source,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &models.ExclusiveSummaryPeriodResponse{
|
|
||||||
OrganizationID: req.OrganizationID,
|
|
||||||
OutletID: req.OutletID,
|
|
||||||
Period: models.ExclusiveSummaryPeriodRange{
|
|
||||||
DateFrom: req.DateFrom,
|
|
||||||
DateTo: req.DateTo,
|
|
||||||
},
|
|
||||||
Summary: models.ExclusiveSummaryPeriodSummary{
|
|
||||||
Sales: result.SalesTotal,
|
|
||||||
HPP: hppTotal,
|
|
||||||
GrossProfit: grossProfit,
|
|
||||||
SalaryTotal: salaryTotal,
|
|
||||||
SalaryDW: salaryDW,
|
|
||||||
SalaryStaff: salaryStaff,
|
|
||||||
SalaryOther: salaryOther,
|
|
||||||
OtherOperationalExpenses: otherOperationalExpenses,
|
|
||||||
OperationalExpensesTotal: operationalTotal,
|
|
||||||
TotalCost: totalCost,
|
|
||||||
NetProfit: netProfit,
|
|
||||||
},
|
|
||||||
Reimburse: models.ExclusiveSummaryReimburse{
|
|
||||||
TotalCost: totalCost,
|
|
||||||
ExcludedSalaryStaff: excludedSalaryStaff,
|
|
||||||
TotalReimburse: totalCost - excludedSalaryStaff,
|
|
||||||
},
|
|
||||||
HPPBreakdown: hppBreakdown,
|
|
||||||
OperationalExpenseBreakdown: operationalBreakdown,
|
|
||||||
DailySummary: dailySummary,
|
|
||||||
DailyTransactions: dailyTransactions,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func exclusiveSummaryCategoryBreakdown(items []entities.ExclusiveSummaryCategoryTotal) ([]models.ExclusiveSummaryCategoryBreakdown, float64) {
|
|
||||||
var total float64
|
|
||||||
for _, item := range items {
|
|
||||||
total += item.Amount
|
|
||||||
}
|
|
||||||
|
|
||||||
breakdown := make([]models.ExclusiveSummaryCategoryBreakdown, len(items))
|
|
||||||
for i, item := range items {
|
|
||||||
breakdown[i] = models.ExclusiveSummaryCategoryBreakdown{
|
|
||||||
CategoryCode: item.CategoryCode,
|
|
||||||
CategoryName: item.CategoryName,
|
|
||||||
Amount: item.Amount,
|
|
||||||
Percentage: percentage(item.Amount, total),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return breakdown, total
|
|
||||||
}
|
|
||||||
|
|
||||||
func exclusiveSummarySalaryBreakdown(transactions []entities.ExclusiveSummaryDailyTransaction) (float64, float64, float64) {
|
|
||||||
var salaryDW float64
|
|
||||||
var salaryStaff float64
|
|
||||||
var salaryOther float64
|
|
||||||
|
|
||||||
for _, transaction := range transactions {
|
|
||||||
if !isExclusiveSummarySalary(transaction.CategoryCode, transaction.CategoryName, transaction.Description) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
classification := strings.ToLower(transaction.CategoryCode + " " + transaction.CategoryName + " " + transaction.Description)
|
|
||||||
switch {
|
|
||||||
case strings.Contains(classification, "staff") || strings.Contains(classification, "kary") || strings.Contains(classification, "karyawan"):
|
|
||||||
salaryStaff += transaction.Amount
|
|
||||||
case strings.Contains(classification, "dw"):
|
|
||||||
salaryDW += transaction.Amount
|
|
||||||
default:
|
|
||||||
salaryOther += transaction.Amount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return salaryDW, salaryStaff, salaryOther
|
|
||||||
}
|
|
||||||
|
|
||||||
func isExclusiveSummarySalary(parts ...string) bool {
|
|
||||||
text := strings.ToLower(strings.Join(parts, " "))
|
|
||||||
return strings.Contains(text, "gaji") || strings.Contains(text, "salary")
|
|
||||||
}
|
|
||||||
|
|
||||||
func percentage(numerator, denominator float64) float64 {
|
|
||||||
if denominator == 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return (numerator / denominator) * 100
|
|
||||||
}
|
|
||||||
|
|
||||||
type exclusiveSummaryMonthlyBucket struct {
|
|
||||||
Label string
|
|
||||||
DateFrom time.Time
|
|
||||||
DateTo time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildExclusiveSummaryMonthlyBuckets(monthStart time.Time) []exclusiveSummaryMonthlyBucket {
|
|
||||||
monthEnd := monthStart.AddDate(0, 1, 0).Add(-time.Nanosecond)
|
|
||||||
buckets := make([]exclusiveSummaryMonthlyBucket, 0, 6)
|
|
||||||
currentStart := monthStart
|
|
||||||
|
|
||||||
for !currentStart.After(monthEnd) {
|
|
||||||
currentEnd := currentStart
|
|
||||||
for currentEnd.Weekday() != time.Sunday && currentEnd.Day() < monthEnd.Day() {
|
|
||||||
currentEnd = currentEnd.AddDate(0, 0, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
bucketEnd := time.Date(currentEnd.Year(), currentEnd.Month(), currentEnd.Day(), 23, 59, 59, int(time.Second-time.Nanosecond), currentEnd.Location())
|
|
||||||
if bucketEnd.After(monthEnd) {
|
|
||||||
bucketEnd = monthEnd
|
|
||||||
}
|
|
||||||
|
|
||||||
buckets = append(buckets, exclusiveSummaryMonthlyBucket{
|
|
||||||
Label: fmt.Sprintf("%d - %d %s", currentStart.Day(), bucketEnd.Day(), indonesianMonthName(currentStart.Month())),
|
|
||||||
DateFrom: currentStart,
|
|
||||||
DateTo: bucketEnd,
|
|
||||||
})
|
|
||||||
|
|
||||||
currentStart = time.Date(bucketEnd.Year(), bucketEnd.Month(), bucketEnd.Day(), 0, 0, 0, 0, bucketEnd.Location()).AddDate(0, 0, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return buckets
|
|
||||||
}
|
|
||||||
|
|
||||||
func indonesianMonthName(month time.Month) string {
|
|
||||||
names := map[time.Month]string{
|
|
||||||
time.January: "Januari",
|
|
||||||
time.February: "Februari",
|
|
||||||
time.March: "Maret",
|
|
||||||
time.April: "April",
|
|
||||||
time.May: "Mei",
|
|
||||||
time.June: "Juni",
|
|
||||||
time.July: "Juli",
|
|
||||||
time.August: "Agustus",
|
|
||||||
time.September: "September",
|
|
||||||
time.October: "Oktober",
|
|
||||||
time.November: "November",
|
|
||||||
time.December: "Desember",
|
|
||||||
}
|
|
||||||
return names[month]
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -13,12 +13,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type analyticsRepositoryStub struct {
|
type analyticsRepositoryStub struct {
|
||||||
purchasingResult *entities.PurchasingAnalytics
|
purchasingResult *entities.PurchasingAnalytics
|
||||||
profitLossResult *entities.ProfitLossAnalytics
|
profitLossResult *entities.ProfitLossAnalytics
|
||||||
exclusiveSummaryResults []*entities.ExclusiveSummaryAnalytics
|
profitLossGroup string
|
||||||
bankBalances []entities.ExclusiveSummaryBankBalance
|
|
||||||
profitLossGroup string
|
|
||||||
exclusiveSummaryCalls int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
@@ -50,20 +47,6 @@ func (s analyticsRepositoryStub) GetProfitLossAnalytics(_ context.Context, _ uui
|
|||||||
return s.profitLossResult, nil
|
return s.profitLossResult, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *analyticsRepositoryStub) GetExclusiveSummaryAnalytics(context.Context, uuid.UUID, *uuid.UUID, time.Time, time.Time) (*entities.ExclusiveSummaryAnalytics, error) {
|
|
||||||
if s.exclusiveSummaryCalls < len(s.exclusiveSummaryResults) {
|
|
||||||
result := s.exclusiveSummaryResults[s.exclusiveSummaryCalls]
|
|
||||||
s.exclusiveSummaryCalls++
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
s.exclusiveSummaryCalls++
|
|
||||||
return &entities.ExclusiveSummaryAnalytics{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *analyticsRepositoryStub) GetExclusiveSummaryBankBalances(context.Context, uuid.UUID, *uuid.UUID) ([]entities.ExclusiveSummaryBankBalance, error) {
|
|
||||||
return s.bankBalances, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type expenseRepositoryStub struct{}
|
type expenseRepositoryStub struct{}
|
||||||
|
|
||||||
func (expenseRepositoryStub) Create(context.Context, *entities.Expense) error { return nil }
|
func (expenseRepositoryStub) Create(context.Context, *entities.Expense) error { return nil }
|
||||||
@@ -88,27 +71,11 @@ func TestAnalyticsProcessorGetPurchasingAnalyticsPassesOutletName(t *testing.T)
|
|||||||
outletID := uuid.New()
|
outletID := uuid.New()
|
||||||
outletName := "Main Outlet"
|
outletName := "Main Outlet"
|
||||||
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
||||||
processor := NewAnalyticsProcessorImpl(&analyticsRepositoryStub{
|
processor := NewAnalyticsProcessorImpl(analyticsRepositoryStub{
|
||||||
purchasingResult: &entities.PurchasingAnalytics{
|
purchasingResult: &entities.PurchasingAnalytics{
|
||||||
OutletName: &outletName,
|
OutletName: &outletName,
|
||||||
Summary: entities.PurchasingSummary{
|
Summary: entities.PurchasingSummary{
|
||||||
TotalPurchases: 300,
|
TotalPurchases: 125,
|
||||||
RawMaterialPurchases: 125,
|
|
||||||
ExpensePurchases: 175,
|
|
||||||
TotalPurchaseOrders: 3,
|
|
||||||
RawMaterialPurchaseOrders: 1,
|
|
||||||
ExpenseCount: 2,
|
|
||||||
},
|
|
||||||
Data: []entities.PurchasingAnalyticsData{
|
|
||||||
{
|
|
||||||
Date: now,
|
|
||||||
Purchases: 300,
|
|
||||||
RawMaterialPurchases: 125,
|
|
||||||
ExpensePurchases: 175,
|
|
||||||
PurchaseOrders: 3,
|
|
||||||
RawMaterialPurchaseOrders: 1,
|
|
||||||
ExpenseCount: 2,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, expenseRepositoryStub{})
|
}, expenseRepositoryStub{})
|
||||||
@@ -125,23 +92,14 @@ func TestAnalyticsProcessorGetPurchasingAnalyticsPassesOutletName(t *testing.T)
|
|||||||
require.Equal(t, &outletID, result.OutletID)
|
require.Equal(t, &outletID, result.OutletID)
|
||||||
require.NotNil(t, result.OutletName)
|
require.NotNil(t, result.OutletName)
|
||||||
require.Equal(t, outletName, *result.OutletName)
|
require.Equal(t, outletName, *result.OutletName)
|
||||||
require.Equal(t, float64(300), result.Summary.TotalPurchases)
|
require.Equal(t, float64(125), result.Summary.TotalPurchases)
|
||||||
require.Equal(t, float64(125), result.Summary.RawMaterialPurchases)
|
|
||||||
require.Equal(t, float64(175), result.Summary.ExpensePurchases)
|
|
||||||
require.Equal(t, int64(3), result.Summary.TotalPurchaseOrders)
|
|
||||||
require.Equal(t, int64(1), result.Summary.RawMaterialPurchaseOrders)
|
|
||||||
require.Equal(t, int64(2), result.Summary.ExpenseCount)
|
|
||||||
require.Len(t, result.Data, 1)
|
|
||||||
require.Equal(t, float64(300), result.Data[0].Purchases)
|
|
||||||
require.Equal(t, float64(125), result.Data[0].RawMaterialPurchases)
|
|
||||||
require.Equal(t, float64(175), result.Data[0].ExpensePurchases)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAnalyticsProcessorGetProfitLossAnalyticsMapsOverviewAndReportFields(t *testing.T) {
|
func TestAnalyticsProcessorGetProfitLossAnalyticsMapsOverviewAndReportFields(t *testing.T) {
|
||||||
productID := uuid.New()
|
productID := uuid.New()
|
||||||
categoryID := uuid.New()
|
categoryID := uuid.New()
|
||||||
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
||||||
processor := NewAnalyticsProcessorImpl(&analyticsRepositoryStub{
|
processor := NewAnalyticsProcessorImpl(analyticsRepositoryStub{
|
||||||
profitLossResult: &entities.ProfitLossAnalytics{
|
profitLossResult: &entities.ProfitLossAnalytics{
|
||||||
Summary: entities.ProfitLossSummary{
|
Summary: entities.ProfitLossSummary{
|
||||||
TotalRevenue: 1000,
|
TotalRevenue: 1000,
|
||||||
@@ -213,7 +171,7 @@ func TestAnalyticsProcessorGetProfitLossAnalyticsMapsOverviewAndReportFields(t *
|
|||||||
|
|
||||||
func TestAnalyticsProcessorGetProfitLossAnalyticsDynamicExpenseCategories(t *testing.T) {
|
func TestAnalyticsProcessorGetProfitLossAnalyticsDynamicExpenseCategories(t *testing.T) {
|
||||||
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
||||||
processor := NewAnalyticsProcessorImpl(&analyticsRepositoryStub{
|
processor := NewAnalyticsProcessorImpl(analyticsRepositoryStub{
|
||||||
profitLossResult: &entities.ProfitLossAnalytics{
|
profitLossResult: &entities.ProfitLossAnalytics{
|
||||||
Summary: entities.ProfitLossSummary{
|
Summary: entities.ProfitLossSummary{
|
||||||
TotalRevenue: 10000,
|
TotalRevenue: 10000,
|
||||||
@@ -290,106 +248,3 @@ func TestAnalyticsProcessorGetProfitLossAnalyticsDynamicExpenseCategories(t *tes
|
|||||||
require.Equal(t, float64(7400), result.MainSummary[6].MtdNominal)
|
require.Equal(t, float64(7400), result.MainSummary[6].MtdNominal)
|
||||||
require.True(t, result.MainSummary[6].IsBold)
|
require.True(t, result.MainSummary[6].IsBold)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAnalyticsProcessorGetExclusiveSummaryPeriodCalculatesTotalsAndReimburse(t *testing.T) {
|
|
||||||
now := time.Date(2026, 5, 26, 0, 0, 0, 0, time.UTC)
|
|
||||||
processor := NewAnalyticsProcessorImpl(&analyticsRepositoryStub{
|
|
||||||
exclusiveSummaryResults: []*entities.ExclusiveSummaryAnalytics{
|
|
||||||
{
|
|
||||||
SalesTotal: 1000,
|
|
||||||
HPPBreakdown: []entities.ExclusiveSummaryCategoryTotal{
|
|
||||||
{CategoryCode: "RAW", CategoryName: "Raw", Amount: 400},
|
|
||||||
},
|
|
||||||
OperationalExpenseBreakdown: []entities.ExclusiveSummaryCategoryTotal{
|
|
||||||
{CategoryCode: "GAJI", CategoryName: "Gaji", Amount: 250},
|
|
||||||
{CategoryCode: "OPS", CategoryName: "Operasional", Amount: 100},
|
|
||||||
},
|
|
||||||
DailySummary: []entities.ExclusiveSummaryDailySummary{
|
|
||||||
{Date: now, TransactionCount: 3, TotalCost: 750},
|
|
||||||
},
|
|
||||||
DailyTransactions: []entities.ExclusiveSummaryDailyTransaction{
|
|
||||||
{Date: now, CategoryCode: "RAW", CategoryName: "Raw", Description: "beras", Amount: 400, Source: "purchase_order"},
|
|
||||||
{Date: now, CategoryCode: "GAJI", CategoryName: "Gaji", Description: "gaji karyawan", Amount: 200, Source: "purchase_order"},
|
|
||||||
{Date: now, CategoryCode: "GAJI", CategoryName: "Gaji", Description: "DW", Amount: 50, Source: "purchase_order"},
|
|
||||||
{Date: now, CategoryCode: "OPS", CategoryName: "Operasional", Description: "atk", Amount: 100, Source: "purchase_order"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, expenseRepositoryStub{})
|
|
||||||
|
|
||||||
result, err := processor.GetExclusiveSummaryPeriod(context.Background(), &models.ExclusiveSummaryPeriodRequest{
|
|
||||||
OrganizationID: uuid.New(),
|
|
||||||
DateFrom: now,
|
|
||||||
DateTo: now,
|
|
||||||
ExcludeGajiStaffFromReimburse: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, result)
|
|
||||||
require.Equal(t, float64(1000), result.Summary.Sales)
|
|
||||||
require.Equal(t, float64(400), result.Summary.HPP)
|
|
||||||
require.Equal(t, float64(600), result.Summary.GrossProfit)
|
|
||||||
require.Equal(t, float64(350), result.Summary.OperationalExpensesTotal)
|
|
||||||
require.Equal(t, float64(750), result.Summary.TotalCost)
|
|
||||||
require.Equal(t, float64(250), result.Summary.NetProfit)
|
|
||||||
require.Equal(t, float64(250), result.Summary.SalaryTotal)
|
|
||||||
require.Equal(t, float64(50), result.Summary.SalaryDW)
|
|
||||||
require.Equal(t, float64(200), result.Summary.SalaryStaff)
|
|
||||||
require.Equal(t, float64(100), result.Summary.OtherOperationalExpenses)
|
|
||||||
require.Equal(t, float64(200), result.Reimburse.ExcludedSalaryStaff)
|
|
||||||
require.Equal(t, float64(550), result.Reimburse.TotalReimburse)
|
|
||||||
require.Len(t, result.HPPBreakdown, 1)
|
|
||||||
require.Equal(t, float64(100), result.HPPBreakdown[0].Percentage)
|
|
||||||
require.Len(t, result.DailySummary, 1)
|
|
||||||
require.Len(t, result.DailyTransactions, 4)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAnalyticsProcessorGetExclusiveSummaryMonthlyBuildsSummaryAndBuckets(t *testing.T) {
|
|
||||||
location, err := time.LoadLocation("Asia/Jakarta")
|
|
||||||
require.NoError(t, err)
|
|
||||||
month := time.Date(2026, 5, 1, 0, 0, 0, 0, location)
|
|
||||||
openingBalance := 5000000.0
|
|
||||||
closingBalance := 5000000.0
|
|
||||||
notes := "Main cash account for daily transactions"
|
|
||||||
stub := &analyticsRepositoryStub{
|
|
||||||
exclusiveSummaryResults: []*entities.ExclusiveSummaryAnalytics{
|
|
||||||
{SalesTotal: 1000, HPPBreakdown: []entities.ExclusiveSummaryCategoryTotal{{Amount: 400}}, OperationalExpenseBreakdown: []entities.ExclusiveSummaryCategoryTotal{{Amount: 100}}},
|
|
||||||
{SalesTotal: 100, HPPBreakdown: []entities.ExclusiveSummaryCategoryTotal{{Amount: 40}}},
|
|
||||||
{SalesTotal: 200, HPPBreakdown: []entities.ExclusiveSummaryCategoryTotal{{Amount: 80}}},
|
|
||||||
{SalesTotal: 300, HPPBreakdown: []entities.ExclusiveSummaryCategoryTotal{{Amount: 120}}},
|
|
||||||
{SalesTotal: 400, HPPBreakdown: []entities.ExclusiveSummaryCategoryTotal{{Amount: 160}}},
|
|
||||||
{SalesTotal: 500, HPPBreakdown: []entities.ExclusiveSummaryCategoryTotal{{Amount: 200}}},
|
|
||||||
},
|
|
||||||
bankBalances: []entities.ExclusiveSummaryBankBalance{
|
|
||||||
{Bank: "Cash and Bank", OpeningBalance: &openingBalance, ClosingBalance: &closingBalance, Notes: ¬es},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
processor := NewAnalyticsProcessorImpl(stub, expenseRepositoryStub{})
|
|
||||||
|
|
||||||
result, err := processor.GetExclusiveSummaryMonthly(context.Background(), &models.ExclusiveSummaryMonthlyRequest{
|
|
||||||
OrganizationID: uuid.New(),
|
|
||||||
Month: month,
|
|
||||||
})
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, result)
|
|
||||||
require.Equal(t, "2026-05", result.Month)
|
|
||||||
require.Equal(t, float64(1000), result.Summary.TotalSales)
|
|
||||||
require.Equal(t, float64(400), result.Summary.HPP)
|
|
||||||
require.Equal(t, float64(500), result.Summary.NetProfit)
|
|
||||||
require.InDelta(t, float64(50), result.Summary.NetProfitMargin, 0.0001)
|
|
||||||
require.Len(t, result.Periods, 5)
|
|
||||||
require.Equal(t, "1 - 3 Mei", result.Periods[0].Label)
|
|
||||||
require.Equal(t, "25 - 31 Mei", result.Periods[4].Label)
|
|
||||||
require.Len(t, result.BankBalance, 1)
|
|
||||||
require.Equal(t, "Cash and Bank", result.BankBalance[0].Bank)
|
|
||||||
require.NotNil(t, result.BankBalance[0].OpeningBalance)
|
|
||||||
require.Equal(t, openingBalance, *result.BankBalance[0].OpeningBalance)
|
|
||||||
require.NotNil(t, result.BankBalance[0].ClosingBalance)
|
|
||||||
require.Equal(t, closingBalance, *result.BankBalance[0].ClosingBalance)
|
|
||||||
require.Nil(t, result.BankBalance[0].IncomingMutation)
|
|
||||||
require.Nil(t, result.BankBalance[0].OutgoingMutation)
|
|
||||||
require.NotNil(t, result.BankBalance[0].Notes)
|
|
||||||
require.Equal(t, notes, *result.BankBalance[0].Notes)
|
|
||||||
require.Equal(t, 6, stub.exclusiveSummaryCalls)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -23,14 +23,12 @@ type ExpenseProcessor interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseProcessorImpl struct {
|
type ExpenseProcessorImpl struct {
|
||||||
expenseRepo ExpenseRepository
|
expenseRepo ExpenseRepository
|
||||||
purchaseCategoryRepo PurchaseCategoryRepository
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewExpenseProcessorImpl(expenseRepo ExpenseRepository, purchaseCategoryRepo PurchaseCategoryRepository) *ExpenseProcessorImpl {
|
func NewExpenseProcessorImpl(expenseRepo ExpenseRepository) *ExpenseProcessorImpl {
|
||||||
return &ExpenseProcessorImpl{
|
return &ExpenseProcessorImpl{
|
||||||
expenseRepo: expenseRepo,
|
expenseRepo: expenseRepo,
|
||||||
purchaseCategoryRepo: purchaseCategoryRepo,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,30 +48,6 @@ func (p *ExpenseProcessorImpl) CreateExpense(ctx context.Context, organizationID
|
|||||||
status = *req.Status
|
status = *req.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
items := make([]entities.ExpenseItem, len(req.Items))
|
|
||||||
for i, itemReq := range req.Items {
|
|
||||||
chartOfAccountID, err := uuid.Parse(itemReq.ChartOfAccountID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid chart_of_account_id for item: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
purchaseCategoryID, err := uuid.Parse(itemReq.PurchaseCategoryID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid purchase_category_id for item: %w", err)
|
|
||||||
}
|
|
||||||
if err := p.validateExpensePurchaseCategory(ctx, purchaseCategoryID, organizationID); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
items[i] = entities.ExpenseItem{
|
|
||||||
ChartOfAccountID: chartOfAccountID,
|
|
||||||
PurchaseCategoryID: purchaseCategoryID,
|
|
||||||
Item: itemReq.Item,
|
|
||||||
Description: itemReq.Description,
|
|
||||||
Amount: itemReq.Amount,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
expenseEntity := &entities.Expense{
|
expenseEntity := &entities.Expense{
|
||||||
OrganizationID: organizationID,
|
OrganizationID: organizationID,
|
||||||
OutletID: outletID,
|
OutletID: outletID,
|
||||||
@@ -91,10 +65,21 @@ func (p *ExpenseProcessorImpl) CreateExpense(ctx context.Context, organizationID
|
|||||||
return nil, fmt.Errorf("failed to create expense: %w", err)
|
return nil, fmt.Errorf("failed to create expense: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range items {
|
for _, itemReq := range req.Items {
|
||||||
items[i].ExpenseID = expenseEntity.ID
|
chartOfAccountID, err := uuid.Parse(itemReq.ChartOfAccountID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid chart_of_account_id for item: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
err = p.expenseRepo.CreateItem(ctx, &items[i])
|
itemEntity := &entities.ExpenseItem{
|
||||||
|
ExpenseID: expenseEntity.ID,
|
||||||
|
ChartOfAccountID: chartOfAccountID,
|
||||||
|
Item: itemReq.Item,
|
||||||
|
Description: itemReq.Description,
|
||||||
|
Amount: itemReq.Amount,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = p.expenseRepo.CreateItem(ctx, itemEntity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create expense item: %w", err)
|
return nil, fmt.Errorf("failed to create expense item: %w", err)
|
||||||
}
|
}
|
||||||
@@ -150,10 +135,13 @@ func (p *ExpenseProcessorImpl) UpdateExpense(ctx context.Context, id, organizati
|
|||||||
expenseEntity.Reserved1 = req.Reserved1
|
expenseEntity.Reserved1 = req.Reserved1
|
||||||
}
|
}
|
||||||
|
|
||||||
var items []entities.ExpenseItem
|
|
||||||
if req.Items != nil {
|
if req.Items != nil {
|
||||||
items = make([]entities.ExpenseItem, len(req.Items))
|
err = p.expenseRepo.DeleteItemsByExpenseID(ctx, expenseEntity.ID)
|
||||||
for i, itemReq := range req.Items {
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to delete existing items: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, itemReq := range req.Items {
|
||||||
chartOfAccountID := uuid.Nil
|
chartOfAccountID := uuid.Nil
|
||||||
if itemReq.ChartOfAccountID != nil {
|
if itemReq.ChartOfAccountID != nil {
|
||||||
chartOfAccountID, err = uuid.Parse(*itemReq.ChartOfAccountID)
|
chartOfAccountID, err = uuid.Parse(*itemReq.ChartOfAccountID)
|
||||||
@@ -162,17 +150,6 @@ func (p *ExpenseProcessorImpl) UpdateExpense(ctx context.Context, id, organizati
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if itemReq.PurchaseCategoryID == nil {
|
|
||||||
return nil, fmt.Errorf("purchase_category_id is required for item")
|
|
||||||
}
|
|
||||||
purchaseCategoryID, err := uuid.Parse(*itemReq.PurchaseCategoryID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid purchase_category_id for item: %w", err)
|
|
||||||
}
|
|
||||||
if err := p.validateExpensePurchaseCategory(ctx, purchaseCategoryID, organizationID); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
amount := 0.0
|
amount := 0.0
|
||||||
if itemReq.Amount != nil {
|
if itemReq.Amount != nil {
|
||||||
amount = *itemReq.Amount
|
amount = *itemReq.Amount
|
||||||
@@ -182,23 +159,15 @@ func (p *ExpenseProcessorImpl) UpdateExpense(ctx context.Context, id, organizati
|
|||||||
item = *itemReq.Item
|
item = *itemReq.Item
|
||||||
}
|
}
|
||||||
|
|
||||||
items[i] = entities.ExpenseItem{
|
itemEntity := &entities.ExpenseItem{
|
||||||
ExpenseID: expenseEntity.ID,
|
ExpenseID: expenseEntity.ID,
|
||||||
ChartOfAccountID: chartOfAccountID,
|
ChartOfAccountID: chartOfAccountID,
|
||||||
PurchaseCategoryID: purchaseCategoryID,
|
Item: item,
|
||||||
Item: item,
|
Description: itemReq.Description,
|
||||||
Description: itemReq.Description,
|
Amount: amount,
|
||||||
Amount: amount,
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
err = p.expenseRepo.DeleteItemsByExpenseID(ctx, expenseEntity.ID)
|
err = p.expenseRepo.CreateItem(ctx, itemEntity)
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to delete existing items: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range items {
|
|
||||||
err = p.expenseRepo.CreateItem(ctx, &items[i])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create expense item: %w", err)
|
return nil, fmt.Errorf("failed to create expense item: %w", err)
|
||||||
}
|
}
|
||||||
@@ -283,18 +252,6 @@ func (p *ExpenseProcessorImpl) GetExpenseAnalytics(ctx context.Context, req *mod
|
|||||||
categoryData := make([]models.ExpenseAnalyticsCategoryData, len(result.CategoryData))
|
categoryData := make([]models.ExpenseAnalyticsCategoryData, len(result.CategoryData))
|
||||||
for i, item := range result.CategoryData {
|
for i, item := range result.CategoryData {
|
||||||
categoryData[i] = models.ExpenseAnalyticsCategoryData{
|
categoryData[i] = models.ExpenseAnalyticsCategoryData{
|
||||||
PurchaseCategoryID: item.PurchaseCategoryID,
|
|
||||||
PurchaseCategoryName: item.PurchaseCategoryName,
|
|
||||||
PurchaseCategoryType: item.PurchaseCategoryType,
|
|
||||||
TotalAmount: item.TotalAmount,
|
|
||||||
ExpenseCount: item.ExpenseCount,
|
|
||||||
ItemCount: item.ItemCount,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
chartOfAccountData := make([]models.ExpenseAnalyticsChartOfAccountData, len(result.ChartOfAccountData))
|
|
||||||
for i, item := range result.ChartOfAccountData {
|
|
||||||
chartOfAccountData[i] = models.ExpenseAnalyticsChartOfAccountData{
|
|
||||||
ChartOfAccountID: item.ChartOfAccountID,
|
ChartOfAccountID: item.ChartOfAccountID,
|
||||||
ChartOfAccountName: item.ChartOfAccountName,
|
ChartOfAccountName: item.ChartOfAccountName,
|
||||||
TotalAmount: item.TotalAmount,
|
TotalAmount: item.TotalAmount,
|
||||||
@@ -327,26 +284,8 @@ func (p *ExpenseProcessorImpl) GetExpenseAnalytics(ctx context.Context, req *mod
|
|||||||
TotalCategories: result.Summary.TotalCategories,
|
TotalCategories: result.Summary.TotalCategories,
|
||||||
TotalItems: result.Summary.TotalItems,
|
TotalItems: result.Summary.TotalItems,
|
||||||
},
|
},
|
||||||
Data: data,
|
Data: data,
|
||||||
CategoryData: categoryData,
|
CategoryData: categoryData,
|
||||||
ChartOfAccountData: chartOfAccountData,
|
ItemData: itemData,
|
||||||
ItemData: itemData,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ExpenseProcessorImpl) validateExpensePurchaseCategory(ctx context.Context, categoryID, organizationID uuid.UUID) error {
|
|
||||||
category, err := p.purchaseCategoryRepo.GetByIDAndOrganizationID(ctx, categoryID, organizationID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("purchase category not found: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !category.IsActive {
|
|
||||||
return fmt.Errorf("purchase category is inactive")
|
|
||||||
}
|
|
||||||
|
|
||||||
if category.Type != entities.PurchaseCategoryTypeExpense {
|
|
||||||
return fmt.Errorf("purchase category must be expense")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -18,45 +18,6 @@ type expenseRepositoryCaptureStub struct {
|
|||||||
analytics *entities.ExpenseAnalytics
|
analytics *entities.ExpenseAnalytics
|
||||||
}
|
}
|
||||||
|
|
||||||
type expensePurchaseCategoryRepositoryStub struct {
|
|
||||||
category *entities.PurchaseCategory
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*expensePurchaseCategoryRepositoryStub) Create(context.Context, *entities.PurchaseCategory) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *expensePurchaseCategoryRepositoryStub) GetByIDAndOrganizationID(context.Context, uuid.UUID, uuid.UUID) (*entities.PurchaseCategory, error) {
|
|
||||||
return s.category, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*expensePurchaseCategoryRepositoryStub) Update(context.Context, *entities.PurchaseCategory) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*expensePurchaseCategoryRepositoryStub) SoftDelete(context.Context, uuid.UUID, uuid.UUID) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*expensePurchaseCategoryRepositoryStub) List(context.Context, uuid.UUID, map[string]interface{}, int, int) ([]*entities.PurchaseCategory, int64, error) {
|
|
||||||
return nil, 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*expensePurchaseCategoryRepositoryStub) ExistsByCode(context.Context, uuid.UUID, string, *uuid.UUID) (bool, error) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newExpensePurchaseCategoryRepo(categoryID uuid.UUID, categoryType entities.PurchaseCategoryType) *expensePurchaseCategoryRepositoryStub {
|
|
||||||
return &expensePurchaseCategoryRepositoryStub{
|
|
||||||
category: &entities.PurchaseCategory{
|
|
||||||
ID: categoryID,
|
|
||||||
Name: "Operational",
|
|
||||||
Type: categoryType,
|
|
||||||
IsActive: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *expenseRepositoryCaptureStub) Create(_ context.Context, expense *entities.Expense) error {
|
func (s *expenseRepositoryCaptureStub) Create(_ context.Context, expense *entities.Expense) error {
|
||||||
if expense.ID == uuid.Nil {
|
if expense.ID == uuid.Nil {
|
||||||
expense.ID = uuid.New()
|
expense.ID = uuid.New()
|
||||||
@@ -101,8 +62,7 @@ func (*expenseRepositoryCaptureStub) DeleteItemsByExpenseID(context.Context, uui
|
|||||||
|
|
||||||
func TestExpenseProcessorCreatePersistsItemName(t *testing.T) {
|
func TestExpenseProcessorCreatePersistsItemName(t *testing.T) {
|
||||||
repo := &expenseRepositoryCaptureStub{}
|
repo := &expenseRepositoryCaptureStub{}
|
||||||
purchaseCategoryID := uuid.New()
|
p := NewExpenseProcessorImpl(repo)
|
||||||
p := NewExpenseProcessorImpl(repo, newExpensePurchaseCategoryRepo(purchaseCategoryID, entities.PurchaseCategoryTypeExpense))
|
|
||||||
chartOfAccountID := uuid.New()
|
chartOfAccountID := uuid.New()
|
||||||
|
|
||||||
resp, err := p.CreateExpense(context.Background(), uuid.New(), &models.CreateExpenseRequest{
|
resp, err := p.CreateExpense(context.Background(), uuid.New(), &models.CreateExpenseRequest{
|
||||||
@@ -113,10 +73,9 @@ func TestExpenseProcessorCreatePersistsItemName(t *testing.T) {
|
|||||||
Total: 10000,
|
Total: 10000,
|
||||||
Items: []models.CreateExpenseItemRequest{
|
Items: []models.CreateExpenseItemRequest{
|
||||||
{
|
{
|
||||||
ChartOfAccountID: chartOfAccountID.String(),
|
ChartOfAccountID: chartOfAccountID.String(),
|
||||||
PurchaseCategoryID: purchaseCategoryID.String(),
|
Item: "Cleaning supplies",
|
||||||
Item: "Cleaning supplies",
|
Amount: 10000,
|
||||||
Amount: 10000,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -125,15 +84,13 @@ func TestExpenseProcessorCreatePersistsItemName(t *testing.T) {
|
|||||||
require.NotNil(t, resp)
|
require.NotNil(t, resp)
|
||||||
require.Len(t, repo.createdItems, 1)
|
require.Len(t, repo.createdItems, 1)
|
||||||
require.Equal(t, "Cleaning supplies", repo.createdItems[0].Item)
|
require.Equal(t, "Cleaning supplies", repo.createdItems[0].Item)
|
||||||
require.Equal(t, purchaseCategoryID, repo.createdItems[0].PurchaseCategoryID)
|
|
||||||
require.Len(t, resp.Items, 1)
|
require.Len(t, resp.Items, 1)
|
||||||
require.Equal(t, "Cleaning supplies", resp.Items[0].Item)
|
require.Equal(t, "Cleaning supplies", resp.Items[0].Item)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExpenseProcessorCreateDefaultsStatusToDraft(t *testing.T) {
|
func TestExpenseProcessorCreateDefaultsStatusToDraft(t *testing.T) {
|
||||||
repo := &expenseRepositoryCaptureStub{}
|
repo := &expenseRepositoryCaptureStub{}
|
||||||
purchaseCategoryID := uuid.New()
|
p := NewExpenseProcessorImpl(repo)
|
||||||
p := NewExpenseProcessorImpl(repo, newExpensePurchaseCategoryRepo(purchaseCategoryID, entities.PurchaseCategoryTypeExpense))
|
|
||||||
|
|
||||||
resp, err := p.CreateExpense(context.Background(), uuid.New(), &models.CreateExpenseRequest{
|
resp, err := p.CreateExpense(context.Background(), uuid.New(), &models.CreateExpenseRequest{
|
||||||
Receiver: "Cashier",
|
Receiver: "Cashier",
|
||||||
@@ -143,10 +100,9 @@ func TestExpenseProcessorCreateDefaultsStatusToDraft(t *testing.T) {
|
|||||||
Total: 10000,
|
Total: 10000,
|
||||||
Items: []models.CreateExpenseItemRequest{
|
Items: []models.CreateExpenseItemRequest{
|
||||||
{
|
{
|
||||||
ChartOfAccountID: uuid.NewString(),
|
ChartOfAccountID: uuid.NewString(),
|
||||||
PurchaseCategoryID: purchaseCategoryID.String(),
|
Item: "Cleaning supplies",
|
||||||
Item: "Cleaning supplies",
|
Amount: 10000,
|
||||||
Amount: 10000,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -159,8 +115,7 @@ func TestExpenseProcessorCreateDefaultsStatusToDraft(t *testing.T) {
|
|||||||
|
|
||||||
func TestExpenseProcessorCreatePersistsProvidedStatus(t *testing.T) {
|
func TestExpenseProcessorCreatePersistsProvidedStatus(t *testing.T) {
|
||||||
repo := &expenseRepositoryCaptureStub{}
|
repo := &expenseRepositoryCaptureStub{}
|
||||||
purchaseCategoryID := uuid.New()
|
p := NewExpenseProcessorImpl(repo)
|
||||||
p := NewExpenseProcessorImpl(repo, newExpensePurchaseCategoryRepo(purchaseCategoryID, entities.PurchaseCategoryTypeExpense))
|
|
||||||
status := "approved"
|
status := "approved"
|
||||||
|
|
||||||
resp, err := p.CreateExpense(context.Background(), uuid.New(), &models.CreateExpenseRequest{
|
resp, err := p.CreateExpense(context.Background(), uuid.New(), &models.CreateExpenseRequest{
|
||||||
@@ -172,10 +127,9 @@ func TestExpenseProcessorCreatePersistsProvidedStatus(t *testing.T) {
|
|||||||
Total: 10000,
|
Total: 10000,
|
||||||
Items: []models.CreateExpenseItemRequest{
|
Items: []models.CreateExpenseItemRequest{
|
||||||
{
|
{
|
||||||
ChartOfAccountID: uuid.NewString(),
|
ChartOfAccountID: uuid.NewString(),
|
||||||
PurchaseCategoryID: purchaseCategoryID.String(),
|
Item: "Cleaning supplies",
|
||||||
Item: "Cleaning supplies",
|
Amount: 10000,
|
||||||
Amount: 10000,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -186,35 +140,8 @@ func TestExpenseProcessorCreatePersistsProvidedStatus(t *testing.T) {
|
|||||||
require.Equal(t, "approved", resp.Status)
|
require.Equal(t, "approved", resp.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExpenseProcessorCreateRejectsRawMaterialPurchaseCategory(t *testing.T) {
|
|
||||||
repo := &expenseRepositoryCaptureStub{}
|
|
||||||
purchaseCategoryID := uuid.New()
|
|
||||||
p := NewExpenseProcessorImpl(repo, newExpensePurchaseCategoryRepo(purchaseCategoryID, entities.PurchaseCategoryTypeRawMaterial))
|
|
||||||
|
|
||||||
resp, err := p.CreateExpense(context.Background(), uuid.New(), &models.CreateExpenseRequest{
|
|
||||||
Receiver: "Cashier",
|
|
||||||
TransactionDate: "2026-05-29",
|
|
||||||
CodeNumber: "EXP-001",
|
|
||||||
OutletID: uuid.NewString(),
|
|
||||||
Total: 10000,
|
|
||||||
Items: []models.CreateExpenseItemRequest{
|
|
||||||
{
|
|
||||||
ChartOfAccountID: uuid.NewString(),
|
|
||||||
PurchaseCategoryID: purchaseCategoryID.String(),
|
|
||||||
Item: "Cleaning supplies",
|
|
||||||
Amount: 10000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
require.Error(t, err)
|
|
||||||
require.Nil(t, resp)
|
|
||||||
require.Contains(t, err.Error(), "expense")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExpenseProcessorGetExpenseAnalyticsDefaultsGroupByAndMapsResponse(t *testing.T) {
|
func TestExpenseProcessorGetExpenseAnalyticsDefaultsGroupByAndMapsResponse(t *testing.T) {
|
||||||
coaID := uuid.New()
|
coaID := uuid.New()
|
||||||
purchaseCategoryID := uuid.New()
|
|
||||||
outletID := uuid.New()
|
outletID := uuid.New()
|
||||||
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
||||||
repo := &expenseRepositoryCaptureStub{
|
repo := &expenseRepositoryCaptureStub{
|
||||||
@@ -238,16 +165,6 @@ func TestExpenseProcessorGetExpenseAnalyticsDefaultsGroupByAndMapsResponse(t *te
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
CategoryData: []entities.ExpenseAnalyticsCategoryData{
|
CategoryData: []entities.ExpenseAnalyticsCategoryData{
|
||||||
{
|
|
||||||
PurchaseCategoryID: purchaseCategoryID,
|
|
||||||
PurchaseCategoryName: "Operational Supplies",
|
|
||||||
PurchaseCategoryType: "expense",
|
|
||||||
TotalAmount: 100000,
|
|
||||||
ExpenseCount: 2,
|
|
||||||
ItemCount: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ChartOfAccountData: []entities.ExpenseAnalyticsChartOfAccountData{
|
|
||||||
{
|
{
|
||||||
ChartOfAccountID: coaID,
|
ChartOfAccountID: coaID,
|
||||||
ChartOfAccountName: "Operational",
|
ChartOfAccountName: "Operational",
|
||||||
@@ -266,7 +183,7 @@ func TestExpenseProcessorGetExpenseAnalyticsDefaultsGroupByAndMapsResponse(t *te
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
p := NewExpenseProcessorImpl(repo, newExpensePurchaseCategoryRepo(purchaseCategoryID, entities.PurchaseCategoryTypeExpense))
|
p := NewExpenseProcessorImpl(repo)
|
||||||
|
|
||||||
resp, err := p.GetExpenseAnalytics(context.Background(), &models.ExpenseAnalyticsRequest{
|
resp, err := p.GetExpenseAnalytics(context.Background(), &models.ExpenseAnalyticsRequest{
|
||||||
OrganizationID: uuid.New(),
|
OrganizationID: uuid.New(),
|
||||||
@@ -283,9 +200,7 @@ func TestExpenseProcessorGetExpenseAnalyticsDefaultsGroupByAndMapsResponse(t *te
|
|||||||
require.Len(t, resp.Data, 1)
|
require.Len(t, resp.Data, 1)
|
||||||
require.Equal(t, int64(2), resp.Data[0].ExpenseCount)
|
require.Equal(t, int64(2), resp.Data[0].ExpenseCount)
|
||||||
require.Len(t, resp.CategoryData, 1)
|
require.Len(t, resp.CategoryData, 1)
|
||||||
require.Equal(t, purchaseCategoryID, resp.CategoryData[0].PurchaseCategoryID)
|
require.Equal(t, coaID, resp.CategoryData[0].ChartOfAccountID)
|
||||||
require.Len(t, resp.ChartOfAccountData, 1)
|
|
||||||
require.Equal(t, coaID, resp.ChartOfAccountData[0].ChartOfAccountID)
|
|
||||||
require.Len(t, resp.ItemData, 1)
|
require.Len(t, resp.ItemData, 1)
|
||||||
require.Equal(t, "Cleaning supplies", resp.ItemData[0].Item)
|
require.Equal(t, "Cleaning supplies", resp.ItemData[0].Item)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ type CustomerRepository interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type InventoryMovementService interface {
|
type InventoryMovementService interface {
|
||||||
CreateIngredientMovement(ctx context.Context, ingredientID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID, purchaseOrderItemID *uuid.UUID) error
|
CreateIngredientMovement(ctx context.Context, ingredientID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error
|
||||||
CreateProductMovement(ctx context.Context, productID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error
|
CreateProductMovement(ctx context.Context, productID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ type PurchaseOrderProcessorImpl struct {
|
|||||||
purchaseOrderRepo PurchaseOrderRepository
|
purchaseOrderRepo PurchaseOrderRepository
|
||||||
vendorRepo VendorRepository
|
vendorRepo VendorRepository
|
||||||
ingredientRepo IngredientRepository
|
ingredientRepo IngredientRepository
|
||||||
purchaseCategoryRepo PurchaseCategoryRepository
|
|
||||||
unitRepo UnitRepository
|
unitRepo UnitRepository
|
||||||
fileRepo FileRepository
|
fileRepo FileRepository
|
||||||
inventoryMovementService InventoryMovementService
|
inventoryMovementService InventoryMovementService
|
||||||
@@ -36,7 +35,6 @@ func NewPurchaseOrderProcessorImpl(
|
|||||||
purchaseOrderRepo PurchaseOrderRepository,
|
purchaseOrderRepo PurchaseOrderRepository,
|
||||||
vendorRepo VendorRepository,
|
vendorRepo VendorRepository,
|
||||||
ingredientRepo IngredientRepository,
|
ingredientRepo IngredientRepository,
|
||||||
purchaseCategoryRepo PurchaseCategoryRepository,
|
|
||||||
unitRepo UnitRepository,
|
unitRepo UnitRepository,
|
||||||
fileRepo FileRepository,
|
fileRepo FileRepository,
|
||||||
inventoryMovementService InventoryMovementService,
|
inventoryMovementService InventoryMovementService,
|
||||||
@@ -46,7 +44,6 @@ func NewPurchaseOrderProcessorImpl(
|
|||||||
purchaseOrderRepo: purchaseOrderRepo,
|
purchaseOrderRepo: purchaseOrderRepo,
|
||||||
vendorRepo: vendorRepo,
|
vendorRepo: vendorRepo,
|
||||||
ingredientRepo: ingredientRepo,
|
ingredientRepo: ingredientRepo,
|
||||||
purchaseCategoryRepo: purchaseCategoryRepo,
|
|
||||||
unitRepo: unitRepo,
|
unitRepo: unitRepo,
|
||||||
fileRepo: fileRepo,
|
fileRepo: fileRepo,
|
||||||
inventoryMovementService: inventoryMovementService,
|
inventoryMovementService: inventoryMovementService,
|
||||||
@@ -67,40 +64,16 @@ func (p *PurchaseOrderProcessorImpl) CreatePurchaseOrder(ctx context.Context, or
|
|||||||
return nil, fmt.Errorf("purchase order with PO number %s already exists in this organization", req.PONumber)
|
return nil, fmt.Errorf("purchase order with PO number %s already exists in this organization", req.PONumber)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate categories and inventory fields per item type.
|
// Validate ingredients and units exist
|
||||||
for i, item := range req.Items {
|
for i, item := range req.Items {
|
||||||
category, err := p.validatePurchaseCategory(ctx, item.PurchaseCategoryID, organizationID, i)
|
_, err := p.ingredientRepo.GetByID(ctx, item.IngredientID, organizationID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("ingredient not found for item %d: %w", i, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch category.Type {
|
_, err = p.unitRepo.GetByID(ctx, item.UnitID, organizationID)
|
||||||
case entities.PurchaseCategoryTypeRawMaterial:
|
if err != nil {
|
||||||
if item.IngredientID == nil {
|
return nil, fmt.Errorf("unit not found for item %d: %w", i, err)
|
||||||
return nil, fmt.Errorf("ingredient_id is required for raw_material item %d", i)
|
|
||||||
}
|
|
||||||
if item.Quantity == nil {
|
|
||||||
return nil, fmt.Errorf("quantity is required for raw_material item %d", i)
|
|
||||||
}
|
|
||||||
if item.UnitID == nil {
|
|
||||||
return nil, fmt.Errorf("unit_id is required for raw_material item %d", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := p.ingredientRepo.GetByID(ctx, *item.IngredientID, organizationID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("ingredient not found for item %d: %w", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = p.unitRepo.GetByID(ctx, *item.UnitID, organizationID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unit not found for item %d: %w", i, err)
|
|
||||||
}
|
|
||||||
case entities.PurchaseCategoryTypeExpense:
|
|
||||||
if item.IngredientID != nil || item.Quantity != nil || item.UnitID != nil {
|
|
||||||
return nil, fmt.Errorf("ingredient_id, quantity, and unit_id must be empty for expense item %d", i)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("purchase category for item %d has unsupported type %s", i, category.Type)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,13 +109,12 @@ func (p *PurchaseOrderProcessorImpl) CreatePurchaseOrder(ctx context.Context, or
|
|||||||
// Create purchase order items
|
// Create purchase order items
|
||||||
for _, itemReq := range req.Items {
|
for _, itemReq := range req.Items {
|
||||||
itemEntity := &entities.PurchaseOrderItem{
|
itemEntity := &entities.PurchaseOrderItem{
|
||||||
PurchaseOrderID: poEntity.ID,
|
PurchaseOrderID: poEntity.ID,
|
||||||
IngredientID: itemReq.IngredientID,
|
IngredientID: itemReq.IngredientID,
|
||||||
PurchaseCategoryID: itemReq.PurchaseCategoryID,
|
Description: itemReq.Description,
|
||||||
Description: itemReq.Description,
|
Quantity: itemReq.Quantity,
|
||||||
Quantity: itemReq.Quantity,
|
UnitID: itemReq.UnitID,
|
||||||
UnitID: itemReq.UnitID,
|
Amount: itemReq.Amount,
|
||||||
Amount: itemReq.Amount,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = p.purchaseOrderRepo.CreateItem(ctx, itemEntity)
|
err = p.purchaseOrderRepo.CreateItem(ctx, itemEntity)
|
||||||
@@ -217,80 +189,68 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrder(ctx context.Context, id
|
|||||||
|
|
||||||
// Update items if provided
|
// Update items if provided
|
||||||
if req.Items != nil {
|
if req.Items != nil {
|
||||||
totalAmount := 0.0
|
// Delete existing items
|
||||||
items := make([]*entities.PurchaseOrderItem, len(req.Items))
|
|
||||||
for i, itemReq := range req.Items {
|
|
||||||
if itemReq.PurchaseCategoryID == nil {
|
|
||||||
return nil, fmt.Errorf("purchase_category_id is required for item %d", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
ingredientID := itemReq.IngredientID
|
|
||||||
purchaseCategoryID := *itemReq.PurchaseCategoryID
|
|
||||||
unitID := itemReq.UnitID
|
|
||||||
quantity := itemReq.Quantity
|
|
||||||
amount := 0.0
|
|
||||||
if itemReq.Amount != nil {
|
|
||||||
amount = *itemReq.Amount
|
|
||||||
}
|
|
||||||
description := itemReq.Description
|
|
||||||
|
|
||||||
category, err := p.validatePurchaseCategory(ctx, purchaseCategoryID, organizationID, i)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch category.Type {
|
|
||||||
case entities.PurchaseCategoryTypeRawMaterial:
|
|
||||||
if ingredientID == nil {
|
|
||||||
return nil, fmt.Errorf("ingredient_id is required for raw_material item %d", i)
|
|
||||||
}
|
|
||||||
if quantity == nil {
|
|
||||||
return nil, fmt.Errorf("quantity is required for raw_material item %d", i)
|
|
||||||
}
|
|
||||||
if unitID == nil {
|
|
||||||
return nil, fmt.Errorf("unit_id is required for raw_material item %d", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := p.ingredientRepo.GetByID(ctx, *ingredientID, organizationID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("ingredient not found: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = p.unitRepo.GetByID(ctx, *unitID, organizationID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unit not found: %w", err)
|
|
||||||
}
|
|
||||||
case entities.PurchaseCategoryTypeExpense:
|
|
||||||
if ingredientID != nil || quantity != nil || unitID != nil {
|
|
||||||
return nil, fmt.Errorf("ingredient_id, quantity, and unit_id must be empty for expense item %d", i)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("purchase category for item %d has unsupported type %s", i, category.Type)
|
|
||||||
}
|
|
||||||
|
|
||||||
items[i] = &entities.PurchaseOrderItem{
|
|
||||||
PurchaseOrderID: poEntity.ID,
|
|
||||||
IngredientID: ingredientID,
|
|
||||||
PurchaseCategoryID: purchaseCategoryID,
|
|
||||||
Description: description,
|
|
||||||
Quantity: quantity,
|
|
||||||
UnitID: unitID,
|
|
||||||
Amount: amount,
|
|
||||||
}
|
|
||||||
totalAmount += amount
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete and recreate only after all replacement items are valid.
|
|
||||||
err = p.purchaseOrderRepo.DeleteItemsByPurchaseOrderID(ctx, poEntity.ID)
|
err = p.purchaseOrderRepo.DeleteItemsByPurchaseOrderID(ctx, poEntity.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to delete existing items: %w", err)
|
return nil, fmt.Errorf("failed to delete existing items: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, itemEntity := range items {
|
// Create new items
|
||||||
|
totalAmount := 0.0
|
||||||
|
for _, itemReq := range req.Items {
|
||||||
|
// Validate ingredients and units exist
|
||||||
|
if itemReq.IngredientID != nil {
|
||||||
|
_, err := p.ingredientRepo.GetByID(ctx, *itemReq.IngredientID, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ingredient not found: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if itemReq.UnitID != nil {
|
||||||
|
_, err := p.unitRepo.GetByID(ctx, *itemReq.UnitID, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unit not found: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use existing values if not provided
|
||||||
|
ingredientID := poEntity.Items[0].IngredientID // This is a simplified approach
|
||||||
|
unitID := poEntity.Items[0].UnitID
|
||||||
|
quantity := poEntity.Items[0].Quantity
|
||||||
|
amount := poEntity.Items[0].Amount
|
||||||
|
description := poEntity.Items[0].Description
|
||||||
|
|
||||||
|
if itemReq.IngredientID != nil {
|
||||||
|
ingredientID = *itemReq.IngredientID
|
||||||
|
}
|
||||||
|
if itemReq.UnitID != nil {
|
||||||
|
unitID = *itemReq.UnitID
|
||||||
|
}
|
||||||
|
if itemReq.Quantity != nil {
|
||||||
|
quantity = *itemReq.Quantity
|
||||||
|
}
|
||||||
|
if itemReq.Amount != nil {
|
||||||
|
amount = *itemReq.Amount
|
||||||
|
}
|
||||||
|
if itemReq.Description != nil {
|
||||||
|
description = itemReq.Description
|
||||||
|
}
|
||||||
|
|
||||||
|
itemEntity := &entities.PurchaseOrderItem{
|
||||||
|
PurchaseOrderID: poEntity.ID,
|
||||||
|
IngredientID: ingredientID,
|
||||||
|
Description: description,
|
||||||
|
Quantity: quantity,
|
||||||
|
UnitID: unitID,
|
||||||
|
Amount: amount,
|
||||||
|
}
|
||||||
|
|
||||||
err = p.purchaseOrderRepo.CreateItem(ctx, itemEntity)
|
err = p.purchaseOrderRepo.CreateItem(ctx, itemEntity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create purchase order item: %w", err)
|
return nil, fmt.Errorf("failed to create purchase order item: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
totalAmount += amount
|
||||||
}
|
}
|
||||||
|
|
||||||
poEntity.TotalAmount = totalAmount
|
poEntity.TotalAmount = totalAmount
|
||||||
@@ -419,27 +379,19 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrderStatus(ctx context.Conte
|
|||||||
|
|
||||||
// Update inventory for each item
|
// Update inventory for each item
|
||||||
for _, item := range poWithItems.Items {
|
for _, item := range poWithItems.Items {
|
||||||
if item.PurchaseCategory != nil && item.PurchaseCategory.Type == entities.PurchaseCategoryTypeExpense {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if item.IngredientID == nil || item.UnitID == nil || item.Quantity == nil {
|
|
||||||
return nil, fmt.Errorf("purchase order item %s is missing raw material inventory fields", item.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get ingredient to find its base unit
|
// Get ingredient to find its base unit
|
||||||
ingredient, err := p.ingredientRepo.GetByID(ctx, *item.IngredientID, organizationID)
|
ingredient, err := p.ingredientRepo.GetByID(ctx, item.IngredientID, organizationID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get ingredient %s: %w", *item.IngredientID, err)
|
return nil, fmt.Errorf("failed to get ingredient %s: %w", item.IngredientID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert quantity to ingredient's base unit if needed
|
// Convert quantity to ingredient's base unit if needed
|
||||||
quantityToAdd := *item.Quantity
|
quantityToAdd := item.Quantity
|
||||||
if *item.UnitID != ingredient.UnitID {
|
if item.UnitID != ingredient.UnitID {
|
||||||
// Convert from purchase unit to ingredient's base unit
|
// Convert from purchase unit to ingredient's base unit
|
||||||
convertedQuantity, err := p.unitConverterRepo.ConvertQuantity(ctx, *item.IngredientID, *item.UnitID, ingredient.UnitID, organizationID, *item.Quantity)
|
convertedQuantity, err := p.unitConverterRepo.ConvertQuantity(ctx, item.IngredientID, item.UnitID, ingredient.UnitID, organizationID, item.Quantity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to convert quantity for ingredient %s from unit %s to %s: %w", *item.IngredientID, *item.UnitID, ingredient.UnitID, err)
|
return nil, fmt.Errorf("failed to convert quantity for ingredient %s from unit %s to %s: %w", item.IngredientID, item.UnitID, ingredient.UnitID, err)
|
||||||
}
|
}
|
||||||
quantityToAdd = convertedQuantity
|
quantityToAdd = convertedQuantity
|
||||||
}
|
}
|
||||||
@@ -457,7 +409,7 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrderStatus(ctx context.Conte
|
|||||||
|
|
||||||
err = p.inventoryMovementService.CreateIngredientMovement(
|
err = p.inventoryMovementService.CreateIngredientMovement(
|
||||||
ctx,
|
ctx,
|
||||||
*item.IngredientID,
|
item.IngredientID,
|
||||||
organizationID,
|
organizationID,
|
||||||
outletID,
|
outletID,
|
||||||
userID,
|
userID,
|
||||||
@@ -467,10 +419,9 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrderStatus(ctx context.Conte
|
|||||||
reason,
|
reason,
|
||||||
&referenceType,
|
&referenceType,
|
||||||
referenceID,
|
referenceID,
|
||||||
&item.ID,
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create inventory movement for ingredient %s: %w", *item.IngredientID, err)
|
return nil, fmt.Errorf("failed to create inventory movement for ingredient %s: %w", item.IngredientID, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -489,20 +440,3 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrderStatus(ctx context.Conte
|
|||||||
|
|
||||||
return mappers.PurchaseOrderEntityToResponse(updatedPO), nil
|
return mappers.PurchaseOrderEntityToResponse(updatedPO), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PurchaseOrderProcessorImpl) validatePurchaseCategory(ctx context.Context, categoryID, organizationID uuid.UUID, itemIndex int) (*entities.PurchaseCategory, error) {
|
|
||||||
category, err := p.purchaseCategoryRepo.GetByIDAndOrganizationID(ctx, categoryID, organizationID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("purchase category not found for item %d: %w", itemIndex, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !category.IsActive {
|
|
||||||
return nil, fmt.Errorf("purchase category for item %d is inactive", itemIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
if category.Type != entities.PurchaseCategoryTypeRawMaterial && category.Type != entities.PurchaseCategoryTypeExpense {
|
|
||||||
return nil, fmt.Errorf("purchase category for item %d must be raw_material or expense", itemIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
return category, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ type AnalyticsRepository interface {
|
|||||||
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, groupBy string) (*entities.ProfitLossAnalytics, error)
|
GetProfitLossAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, groupBy string) (*entities.ProfitLossAnalytics, error)
|
||||||
GetExclusiveSummaryAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) (*entities.ExclusiveSummaryAnalytics, error)
|
|
||||||
GetExclusiveSummaryBankBalances(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) ([]entities.ExclusiveSummaryBankBalance, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AnalyticsRepositoryImpl struct {
|
type AnalyticsRepositoryImpl struct {
|
||||||
@@ -126,6 +124,7 @@ func (r *AnalyticsRepositoryImpl) GetSalesAnalytics(ctx context.Context, organiz
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *AnalyticsRepositoryImpl) GetPurchasingAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, groupBy string) (*entities.PurchasingAnalytics, error) {
|
func (r *AnalyticsRepositoryImpl) GetPurchasingAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, groupBy string) (*entities.PurchasingAnalytics, error) {
|
||||||
|
var summary entities.PurchasingSummary
|
||||||
var outletName *string
|
var outletName *string
|
||||||
|
|
||||||
if outletID != nil {
|
if outletID != nil {
|
||||||
@@ -145,32 +144,29 @@ func (r *AnalyticsRepositoryImpl) GetPurchasingAnalytics(ctx context.Context, or
|
|||||||
outletName = &outlet.Name
|
outletName = &outlet.Name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return r.getPurchaseOrderPurchasingAnalytics(ctx, organizationID, outletID, outletName, dateFrom, dateTo, groupBy)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, outletName *string, dateFrom, dateTo time.Time, groupBy string) (*entities.PurchasingAnalytics, error) {
|
|
||||||
var summary entities.PurchasingSummary
|
|
||||||
summaryQuery := r.db.WithContext(ctx).
|
summaryQuery := r.db.WithContext(ctx).
|
||||||
Table("purchase_orders po").
|
Table("inventory_movements im").
|
||||||
Select(`
|
Select(`
|
||||||
COALESCE(SUM(poi.amount), 0) as total_purchases,
|
COALESCE(SUM(im.total_cost), 0) as total_purchases,
|
||||||
COUNT(DISTINCT po.id) as total_purchase_orders,
|
COUNT(DISTINCT im.reference_id) as total_purchase_orders,
|
||||||
COALESCE(SUM(poi.quantity), 0) as total_quantity,
|
COALESCE(SUM(im.quantity), 0) as total_quantity,
|
||||||
CASE
|
CASE
|
||||||
WHEN COUNT(DISTINCT po.id) > 0
|
WHEN COUNT(DISTINCT im.reference_id) > 0
|
||||||
THEN COALESCE(SUM(poi.amount), 0) / COUNT(DISTINCT po.id)
|
THEN COALESCE(SUM(im.total_cost), 0) / COUNT(DISTINCT im.reference_id)
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END as average_purchase_order_value,
|
END as average_purchase_order_value,
|
||||||
COUNT(DISTINCT i.id) as total_ingredients,
|
COUNT(DISTINCT im.item_id) as total_ingredients,
|
||||||
COUNT(DISTINCT po.vendor_id) as total_vendors
|
COUNT(DISTINCT po.vendor_id) as total_vendors
|
||||||
`).
|
`).
|
||||||
Joins("LEFT JOIN purchase_order_items poi ON poi.purchase_order_id = po.id").
|
Joins("LEFT JOIN purchase_orders po ON im.reference_id = po.id").
|
||||||
Joins("LEFT JOIN ingredients i ON poi.ingredient_id = i.id").
|
Where("im.organization_id = ?", organizationID).
|
||||||
Joins("LEFT JOIN units u ON poi.unit_id = u.id").
|
Where("im.movement_type = ?", entities.InventoryMovementTypePurchase).
|
||||||
Where("po.organization_id = ?", organizationID).
|
Where("im.item_type = ?", "INGREDIENT").
|
||||||
Where("po.status != ?", "cancelled").
|
Where("im.reference_type = ?", entities.InventoryMovementReferenceTypePurchaseOrder).
|
||||||
Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo)
|
Where("im.created_at >= ? AND im.created_at <= ?", dateFrom, dateTo)
|
||||||
summaryQuery = r.applyPurchaseOrderItemOutletFilter(summaryQuery, outletID)
|
|
||||||
|
summaryQuery = r.resolveOutletID(summaryQuery, outletID, "im.outlet_id")
|
||||||
|
|
||||||
if err := summaryQuery.Scan(&summary).Error; err != nil {
|
if err := summaryQuery.Scan(&summary).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -179,35 +175,36 @@ func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx contex
|
|||||||
var dateFormat string
|
var dateFormat string
|
||||||
switch groupBy {
|
switch groupBy {
|
||||||
case "hour":
|
case "hour":
|
||||||
dateFormat = "DATE_TRUNC('hour', po.created_at)"
|
dateFormat = "DATE_TRUNC('hour', im.created_at)"
|
||||||
case "week":
|
case "week":
|
||||||
dateFormat = "DATE_TRUNC('week', po.transaction_date::timestamp)"
|
dateFormat = "DATE_TRUNC('week', im.created_at)"
|
||||||
case "month":
|
case "month":
|
||||||
dateFormat = "DATE_TRUNC('month', po.transaction_date::timestamp)"
|
dateFormat = "DATE_TRUNC('month', im.created_at)"
|
||||||
default:
|
default:
|
||||||
dateFormat = "DATE_TRUNC('day', po.transaction_date::timestamp)"
|
dateFormat = "DATE_TRUNC('day', im.created_at)"
|
||||||
}
|
}
|
||||||
|
|
||||||
var data []entities.PurchasingAnalyticsData
|
var data []entities.PurchasingAnalyticsData
|
||||||
dataQuery := r.db.WithContext(ctx).
|
dataQuery := r.db.WithContext(ctx).
|
||||||
Table("purchase_orders po").
|
Table("inventory_movements im").
|
||||||
Select(`
|
Select(`
|
||||||
`+dateFormat+` as date,
|
`+dateFormat+` as date,
|
||||||
COALESCE(SUM(poi.amount), 0) as purchases,
|
COALESCE(SUM(im.total_cost), 0) as purchases,
|
||||||
COUNT(DISTINCT po.id) as purchase_orders,
|
COUNT(DISTINCT im.reference_id) as purchase_orders,
|
||||||
COALESCE(SUM(poi.quantity), 0) as quantity,
|
COALESCE(SUM(im.quantity), 0) as quantity,
|
||||||
COUNT(DISTINCT i.id) as ingredients,
|
COUNT(DISTINCT im.item_id) as ingredients,
|
||||||
COUNT(DISTINCT po.vendor_id) as vendors
|
COUNT(DISTINCT po.vendor_id) as vendors
|
||||||
`).
|
`).
|
||||||
Joins("LEFT JOIN purchase_order_items poi ON poi.purchase_order_id = po.id").
|
Joins("LEFT JOIN purchase_orders po ON im.reference_id = po.id").
|
||||||
Joins("LEFT JOIN ingredients i ON poi.ingredient_id = i.id").
|
Where("im.organization_id = ?", organizationID).
|
||||||
Joins("LEFT JOIN units u ON poi.unit_id = u.id").
|
Where("im.movement_type = ?", entities.InventoryMovementTypePurchase).
|
||||||
Where("po.organization_id = ?", organizationID).
|
Where("im.item_type = ?", "INGREDIENT").
|
||||||
Where("po.status != ?", "cancelled").
|
Where("im.reference_type = ?", entities.InventoryMovementReferenceTypePurchaseOrder).
|
||||||
Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo).
|
Where("im.created_at >= ? AND im.created_at <= ?", dateFrom, dateTo).
|
||||||
Group(dateFormat).
|
Group(dateFormat).
|
||||||
Order(dateFormat)
|
Order(dateFormat)
|
||||||
dataQuery = r.applyPurchaseOrderItemOutletFilter(dataQuery, outletID)
|
|
||||||
|
dataQuery = r.resolveOutletID(dataQuery, outletID, "im.outlet_id")
|
||||||
|
|
||||||
if err := dataQuery.Scan(&data).Error; err != nil {
|
if err := dataQuery.Scan(&data).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -215,28 +212,29 @@ func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx contex
|
|||||||
|
|
||||||
var ingredientData []entities.PurchasingIngredientData
|
var ingredientData []entities.PurchasingIngredientData
|
||||||
ingredientQuery := r.db.WithContext(ctx).
|
ingredientQuery := r.db.WithContext(ctx).
|
||||||
Table("purchase_order_items poi").
|
Table("inventory_movements im").
|
||||||
Select(`
|
Select(`
|
||||||
i.id as ingredient_id,
|
i.id as ingredient_id,
|
||||||
i.name as ingredient_name,
|
i.name as ingredient_name,
|
||||||
COALESCE(SUM(poi.quantity), 0) as quantity,
|
COALESCE(SUM(im.quantity), 0) as quantity,
|
||||||
COALESCE(SUM(poi.amount), 0) as total_cost,
|
COALESCE(SUM(im.total_cost), 0) as total_cost,
|
||||||
CASE
|
CASE
|
||||||
WHEN SUM(poi.quantity) > 0
|
WHEN SUM(im.quantity) > 0
|
||||||
THEN COALESCE(SUM(poi.amount), 0) / SUM(poi.quantity)
|
THEN COALESCE(SUM(im.total_cost), 0) / SUM(im.quantity)
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END as average_unit_cost,
|
END as average_unit_cost,
|
||||||
COUNT(DISTINCT po.id) as purchase_order_count
|
COUNT(DISTINCT im.reference_id) as purchase_order_count
|
||||||
`).
|
`).
|
||||||
Joins("JOIN purchase_orders po ON poi.purchase_order_id = po.id").
|
Joins("JOIN ingredients i ON im.item_id = i.id").
|
||||||
Joins("JOIN ingredients i ON poi.ingredient_id = i.id").
|
Where("im.organization_id = ?", organizationID).
|
||||||
Joins("LEFT JOIN units u ON poi.unit_id = u.id").
|
Where("im.movement_type = ?", entities.InventoryMovementTypePurchase).
|
||||||
Where("po.organization_id = ?", organizationID).
|
Where("im.item_type = ?", "INGREDIENT").
|
||||||
Where("po.status != ?", "cancelled").
|
Where("im.reference_type = ?", entities.InventoryMovementReferenceTypePurchaseOrder).
|
||||||
Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo).
|
Where("im.created_at >= ? AND im.created_at <= ?", dateFrom, dateTo).
|
||||||
Group("i.id, i.name").
|
Group("i.id, i.name").
|
||||||
Order("total_cost DESC")
|
Order("total_cost DESC")
|
||||||
ingredientQuery = r.applyPurchaseOrderItemOutletFilter(ingredientQuery, outletID)
|
|
||||||
|
ingredientQuery = r.resolveOutletID(ingredientQuery, outletID, "im.outlet_id")
|
||||||
|
|
||||||
if err := ingredientQuery.Scan(&ingredientData).Error; err != nil {
|
if err := ingredientQuery.Scan(&ingredientData).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -244,25 +242,26 @@ func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx contex
|
|||||||
|
|
||||||
var vendorData []entities.PurchasingVendorData
|
var vendorData []entities.PurchasingVendorData
|
||||||
vendorQuery := r.db.WithContext(ctx).
|
vendorQuery := r.db.WithContext(ctx).
|
||||||
Table("purchase_orders po").
|
Table("inventory_movements im").
|
||||||
Select(`
|
Select(`
|
||||||
v.id as vendor_id,
|
v.id as vendor_id,
|
||||||
v.name as vendor_name,
|
v.name as vendor_name,
|
||||||
COALESCE(SUM(poi.amount), 0) as total_cost,
|
COALESCE(SUM(im.total_cost), 0) as total_cost,
|
||||||
COUNT(DISTINCT po.id) as purchase_order_count,
|
COUNT(DISTINCT im.reference_id) as purchase_order_count,
|
||||||
COUNT(DISTINCT i.id) as ingredient_count,
|
COUNT(DISTINCT im.item_id) as ingredient_count,
|
||||||
COALESCE(SUM(poi.quantity), 0) as quantity
|
COALESCE(SUM(im.quantity), 0) as quantity
|
||||||
`).
|
`).
|
||||||
|
Joins("JOIN purchase_orders po ON im.reference_id = po.id").
|
||||||
Joins("JOIN vendors v ON po.vendor_id = v.id").
|
Joins("JOIN vendors v ON po.vendor_id = v.id").
|
||||||
Joins("LEFT JOIN purchase_order_items poi ON poi.purchase_order_id = po.id").
|
Where("im.organization_id = ?", organizationID).
|
||||||
Joins("LEFT JOIN ingredients i ON poi.ingredient_id = i.id").
|
Where("im.movement_type = ?", entities.InventoryMovementTypePurchase).
|
||||||
Joins("LEFT JOIN units u ON poi.unit_id = u.id").
|
Where("im.item_type = ?", "INGREDIENT").
|
||||||
Where("po.organization_id = ?", organizationID).
|
Where("im.reference_type = ?", entities.InventoryMovementReferenceTypePurchaseOrder).
|
||||||
Where("po.status != ?", "cancelled").
|
Where("im.created_at >= ? AND im.created_at <= ?", dateFrom, dateTo).
|
||||||
Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo).
|
|
||||||
Group("v.id, v.name").
|
Group("v.id, v.name").
|
||||||
Order("total_cost DESC")
|
Order("total_cost DESC")
|
||||||
vendorQuery = r.applyPurchaseOrderItemOutletFilter(vendorQuery, outletID)
|
|
||||||
|
vendorQuery = r.resolveOutletID(vendorQuery, outletID, "im.outlet_id")
|
||||||
|
|
||||||
if err := vendorQuery.Scan(&vendorData).Error; err != nil {
|
if err := vendorQuery.Scan(&vendorData).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -277,13 +276,6 @@ func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx contex
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *AnalyticsRepositoryImpl) applyPurchaseOrderItemOutletFilter(query *gorm.DB, outletID *uuid.UUID) *gorm.DB {
|
|
||||||
if outletID == nil {
|
|
||||||
return query
|
|
||||||
}
|
|
||||||
return query.Where("(i.outlet_id = ? OR u.outlet_id = ?)", *outletID, *outletID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, limit int) ([]*entities.ProductAnalytics, error) {
|
func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, limit int) ([]*entities.ProductAnalytics, error) {
|
||||||
var results []*entities.ProductAnalytics
|
var results []*entities.ProductAnalytics
|
||||||
|
|
||||||
@@ -292,6 +284,7 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organ
|
|||||||
Select(`
|
Select(`
|
||||||
p.id as product_id,
|
p.id as product_id,
|
||||||
p.name as product_name,
|
p.name as product_name,
|
||||||
|
p.price as product_price,
|
||||||
c.id as category_id,
|
c.id as category_id,
|
||||||
c.name as category_name,
|
c.name as category_name,
|
||||||
c.order as category_order,
|
c.order as category_order,
|
||||||
@@ -350,7 +343,7 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organ
|
|||||||
query = r.resolveOutletID(query, outletID, "o.outlet_id")
|
query = r.resolveOutletID(query, outletID, "o.outlet_id")
|
||||||
|
|
||||||
err := query.
|
err := query.
|
||||||
Group("p.id, p.name, p.cost, c.id, c.name, c.order, mahpp.hpp_per_unit").
|
Group("p.id, p.name, p.price, p.cost, c.id, c.name, c.order, mahpp.hpp_per_unit").
|
||||||
Order("revenue DESC").
|
Order("revenue DESC").
|
||||||
Limit(limit).
|
Limit(limit).
|
||||||
Scan(&results).Error
|
Scan(&results).Error
|
||||||
@@ -688,214 +681,3 @@ func (r *AnalyticsRepositoryImpl) getOperationalExpenseItems(ctx context.Context
|
|||||||
|
|
||||||
return results, err
|
return results, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *AnalyticsRepositoryImpl) GetExclusiveSummaryAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) (*entities.ExclusiveSummaryAnalytics, error) {
|
|
||||||
type salesResult struct {
|
|
||||||
SalesTotal float64
|
|
||||||
SalesCount int64
|
|
||||||
}
|
|
||||||
|
|
||||||
var sales salesResult
|
|
||||||
salesQuery := r.db.WithContext(ctx).
|
|
||||||
Table("orders o").
|
|
||||||
Select(`
|
|
||||||
COALESCE(SUM(o.total_amount), 0) as sales_total,
|
|
||||||
COUNT(o.id) as sales_count
|
|
||||||
`).
|
|
||||||
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)
|
|
||||||
salesQuery = r.resolveOutletID(salesQuery, outletID, "o.outlet_id")
|
|
||||||
if err := salesQuery.Scan(&sales).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
hppBreakdown, err := r.getExclusiveSummaryHPPBreakdown(ctx, organizationID, outletID, dateFrom, dateTo)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
operationalExpenseBreakdown, err := r.getExclusiveSummaryOperationalExpenseBreakdown(ctx, organizationID, dateFrom, dateTo)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
dailySummary, err := r.getExclusiveSummaryDailySummary(ctx, organizationID, outletID, dateFrom, dateTo)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
dailyTransactions, err := r.getExclusiveSummaryDailyTransactions(ctx, organizationID, outletID, dateFrom, dateTo)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &entities.ExclusiveSummaryAnalytics{
|
|
||||||
SalesTotal: sales.SalesTotal,
|
|
||||||
SalesCount: sales.SalesCount,
|
|
||||||
HPPBreakdown: hppBreakdown,
|
|
||||||
OperationalExpenseBreakdown: operationalExpenseBreakdown,
|
|
||||||
DailySummary: dailySummary,
|
|
||||||
DailyTransactions: dailyTransactions,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *AnalyticsRepositoryImpl) getExclusiveSummaryHPPBreakdown(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) ([]entities.ExclusiveSummaryCategoryTotal, error) {
|
|
||||||
var results []entities.ExclusiveSummaryCategoryTotal
|
|
||||||
|
|
||||||
query := r.db.WithContext(ctx).
|
|
||||||
Table("purchase_order_items poi").
|
|
||||||
Select(`
|
|
||||||
pc.code as category_code,
|
|
||||||
pc.name as category_name,
|
|
||||||
COALESCE(SUM(poi.amount), 0) as amount
|
|
||||||
`).
|
|
||||||
Joins("JOIN purchase_orders po ON poi.purchase_order_id = po.id").
|
|
||||||
Joins("JOIN purchase_categories pc ON poi.purchase_category_id = pc.id").
|
|
||||||
Joins("LEFT JOIN ingredients i ON poi.ingredient_id = i.id").
|
|
||||||
Joins("LEFT JOIN units u ON poi.unit_id = u.id").
|
|
||||||
Where("po.organization_id = ?", organizationID).
|
|
||||||
Where("pc.type = ?", entities.PurchaseCategoryTypeRawMaterial).
|
|
||||||
Where("po.status = ?", "received").
|
|
||||||
Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo)
|
|
||||||
query = r.applyPurchaseOrderItemOutletFilter(query, outletID)
|
|
||||||
|
|
||||||
err := query.
|
|
||||||
Group("pc.id, pc.code, pc.name, pc.sort_order").
|
|
||||||
Order("pc.sort_order ASC, pc.name ASC").
|
|
||||||
Scan(&results).Error
|
|
||||||
|
|
||||||
return results, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *AnalyticsRepositoryImpl) getExclusiveSummaryOperationalExpenseBreakdown(ctx context.Context, organizationID uuid.UUID, dateFrom, dateTo time.Time) ([]entities.ExclusiveSummaryCategoryTotal, error) {
|
|
||||||
var results []entities.ExclusiveSummaryCategoryTotal
|
|
||||||
|
|
||||||
err := r.db.WithContext(ctx).
|
|
||||||
Table("purchase_order_items poi").
|
|
||||||
Select(`
|
|
||||||
pc.code as category_code,
|
|
||||||
pc.name as category_name,
|
|
||||||
COALESCE(SUM(poi.amount), 0) as amount
|
|
||||||
`).
|
|
||||||
Joins("JOIN purchase_orders po ON poi.purchase_order_id = po.id").
|
|
||||||
Joins("JOIN purchase_categories pc ON poi.purchase_category_id = pc.id").
|
|
||||||
Where("po.organization_id = ?", organizationID).
|
|
||||||
Where("pc.type = ?", entities.PurchaseCategoryTypeExpense).
|
|
||||||
Where("po.status = ?", "received").
|
|
||||||
Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo).
|
|
||||||
Group("pc.id, pc.code, pc.name, pc.sort_order").
|
|
||||||
Order("pc.sort_order ASC, pc.name ASC").
|
|
||||||
Scan(&results).Error
|
|
||||||
|
|
||||||
return results, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *AnalyticsRepositoryImpl) getExclusiveSummaryDailySummary(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) ([]entities.ExclusiveSummaryDailySummary, error) {
|
|
||||||
var results []entities.ExclusiveSummaryDailySummary
|
|
||||||
rawQuery, args := r.exclusiveSummaryPurchaseOrderItemQuery(organizationID, outletID, dateFrom, dateTo)
|
|
||||||
|
|
||||||
err := r.db.WithContext(ctx).Raw(`
|
|
||||||
SELECT date, COUNT(*) as transaction_count, COALESCE(SUM(amount), 0) as total_cost
|
|
||||||
FROM (`+rawQuery+`) transactions
|
|
||||||
GROUP BY date
|
|
||||||
ORDER BY date ASC
|
|
||||||
`, args...).Scan(&results).Error
|
|
||||||
|
|
||||||
return results, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *AnalyticsRepositoryImpl) getExclusiveSummaryDailyTransactions(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) ([]entities.ExclusiveSummaryDailyTransaction, error) {
|
|
||||||
var results []entities.ExclusiveSummaryDailyTransaction
|
|
||||||
rawQuery, args := r.exclusiveSummaryPurchaseOrderItemQuery(organizationID, outletID, dateFrom, dateTo)
|
|
||||||
|
|
||||||
err := r.db.WithContext(ctx).Raw(`
|
|
||||||
SELECT date, category_code, category_name, description, amount, source
|
|
||||||
FROM (`+rawQuery+`) transactions
|
|
||||||
ORDER BY date ASC, category_name ASC, description ASC
|
|
||||||
`, args...).Scan(&results).Error
|
|
||||||
|
|
||||||
return results, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *AnalyticsRepositoryImpl) exclusiveSummaryPurchaseOrderItemQuery(organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) (string, []interface{}) {
|
|
||||||
outletFilter := ""
|
|
||||||
args := []interface{}{
|
|
||||||
organizationID,
|
|
||||||
"received",
|
|
||||||
dateFrom,
|
|
||||||
dateTo,
|
|
||||||
}
|
|
||||||
|
|
||||||
if outletID != nil {
|
|
||||||
outletFilter = "AND (pc.type = ? OR i.outlet_id = ? OR u.outlet_id = ?)"
|
|
||||||
args = append(args, entities.PurchaseCategoryTypeExpense, *outletID, *outletID)
|
|
||||||
}
|
|
||||||
|
|
||||||
query := `
|
|
||||||
SELECT
|
|
||||||
DATE(po.transaction_date) as date,
|
|
||||||
pc.code as category_code,
|
|
||||||
pc.name as category_name,
|
|
||||||
COALESCE(NULLIF(poi.description, ''), i.name, pc.name) as description,
|
|
||||||
poi.amount as amount,
|
|
||||||
'purchase_order' as source
|
|
||||||
FROM purchase_order_items poi
|
|
||||||
JOIN purchase_orders po ON poi.purchase_order_id = po.id
|
|
||||||
JOIN purchase_categories pc ON poi.purchase_category_id = pc.id
|
|
||||||
LEFT JOIN ingredients i ON poi.ingredient_id = i.id
|
|
||||||
LEFT JOIN units u ON poi.unit_id = u.id
|
|
||||||
WHERE po.organization_id = ?
|
|
||||||
AND po.status = ?
|
|
||||||
AND po.transaction_date >= ? AND po.transaction_date <= ?
|
|
||||||
` + outletFilter + `
|
|
||||||
`
|
|
||||||
|
|
||||||
return query, args
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *AnalyticsRepositoryImpl) GetExclusiveSummaryBankBalances(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) ([]entities.ExclusiveSummaryBankBalance, error) {
|
|
||||||
type accountBalance struct {
|
|
||||||
Name string
|
|
||||||
OpeningBalance float64
|
|
||||||
CurrentBalance float64
|
|
||||||
Description *string
|
|
||||||
}
|
|
||||||
|
|
||||||
var accounts []accountBalance
|
|
||||||
query := r.db.WithContext(ctx).
|
|
||||||
Table("accounts").
|
|
||||||
Select("name, opening_balance, current_balance, description").
|
|
||||||
Where("organization_id = ?", organizationID).
|
|
||||||
Where("account_type IN ?", []entities.AccountType{entities.AccountTypeCash, entities.AccountTypeWallet, entities.AccountTypeBank}).
|
|
||||||
Where("is_active = ?", true)
|
|
||||||
|
|
||||||
if outletID != nil {
|
|
||||||
query = query.Where("outlet_id = ? OR outlet_id IS NULL", *outletID)
|
|
||||||
} else {
|
|
||||||
query = query.Where("outlet_id IS NULL")
|
|
||||||
}
|
|
||||||
|
|
||||||
err := query.
|
|
||||||
Order("number ASC, name ASC").
|
|
||||||
Scan(&accounts).Error
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
balances := make([]entities.ExclusiveSummaryBankBalance, len(accounts))
|
|
||||||
for i, account := range accounts {
|
|
||||||
openingBalance := account.OpeningBalance
|
|
||||||
closingBalance := account.CurrentBalance
|
|
||||||
balances[i] = entities.ExclusiveSummaryBankBalance{
|
|
||||||
Bank: account.Name,
|
|
||||||
OpeningBalance: &openingBalance,
|
|
||||||
ClosingBalance: &closingBalance,
|
|
||||||
Notes: account.Description,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return balances, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ func (r *ExpenseRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*ent
|
|||||||
var expense entities.Expense
|
var expense entities.Expense
|
||||||
err := r.db.WithContext(ctx).
|
err := r.db.WithContext(ctx).
|
||||||
Preload("Items.ChartOfAccount").
|
Preload("Items.ChartOfAccount").
|
||||||
Preload("Items.PurchaseCategory").
|
|
||||||
First(&expense, "id = ?", id).Error
|
First(&expense, "id = ?", id).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -42,7 +41,6 @@ func (r *ExpenseRepositoryImpl) GetByIDAndOrganizationID(ctx context.Context, id
|
|||||||
var expense entities.Expense
|
var expense entities.Expense
|
||||||
err := r.db.WithContext(ctx).
|
err := r.db.WithContext(ctx).
|
||||||
Preload("Items.ChartOfAccount").
|
Preload("Items.ChartOfAccount").
|
||||||
Preload("Items.PurchaseCategory").
|
|
||||||
Where("id = ? AND organization_id = ?", id, organizationID).
|
Where("id = ? AND organization_id = ?", id, organizationID).
|
||||||
First(&expense).Error
|
First(&expense).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -109,7 +107,6 @@ func (r *ExpenseRepositoryImpl) List(ctx context.Context, organizationID uuid.UU
|
|||||||
|
|
||||||
err := query.
|
err := query.
|
||||||
Preload("Items.ChartOfAccount").
|
Preload("Items.ChartOfAccount").
|
||||||
Preload("Items.PurchaseCategory").
|
|
||||||
Order("created_at DESC").
|
Order("created_at DESC").
|
||||||
Limit(limit).
|
Limit(limit).
|
||||||
Offset(offset).
|
Offset(offset).
|
||||||
@@ -142,7 +139,7 @@ func (r *ExpenseRepositoryImpl) GetAnalytics(ctx context.Context, organizationID
|
|||||||
Table("expense_items ei").
|
Table("expense_items ei").
|
||||||
Select(`
|
Select(`
|
||||||
COUNT(ei.id) as total_items,
|
COUNT(ei.id) as total_items,
|
||||||
COUNT(DISTINCT ei.purchase_category_id) as total_categories
|
COUNT(DISTINCT ei.chart_of_account_id) as total_categories
|
||||||
`).
|
`).
|
||||||
Joins("JOIN expenses e ON ei.expense_id = e.id").
|
Joins("JOIN expenses e ON ei.expense_id = e.id").
|
||||||
Where("e.organization_id = ?", organizationID).
|
Where("e.organization_id = ?", organizationID).
|
||||||
@@ -177,7 +174,7 @@ func (r *ExpenseRepositoryImpl) GetAnalytics(ctx context.Context, organizationID
|
|||||||
COALESCE(SUM(item_counts.categories), 0) as categories
|
COALESCE(SUM(item_counts.categories), 0) as categories
|
||||||
`).
|
`).
|
||||||
Joins(`LEFT JOIN (
|
Joins(`LEFT JOIN (
|
||||||
SELECT expense_id, COUNT(id) as items, COUNT(DISTINCT purchase_category_id) as categories
|
SELECT expense_id, COUNT(id) as items, COUNT(DISTINCT chart_of_account_id) as categories
|
||||||
FROM expense_items
|
FROM expense_items
|
||||||
GROUP BY expense_id
|
GROUP BY expense_id
|
||||||
) item_counts ON item_counts.expense_id = e.id`).
|
) item_counts ON item_counts.expense_id = e.id`).
|
||||||
@@ -195,32 +192,6 @@ func (r *ExpenseRepositoryImpl) GetAnalytics(ctx context.Context, organizationID
|
|||||||
|
|
||||||
var categoryData []entities.ExpenseAnalyticsCategoryData
|
var categoryData []entities.ExpenseAnalyticsCategoryData
|
||||||
categoryQuery := r.db.WithContext(ctx).
|
categoryQuery := r.db.WithContext(ctx).
|
||||||
Table("expense_items ei").
|
|
||||||
Select(`
|
|
||||||
pc.id as purchase_category_id,
|
|
||||||
pc.name as purchase_category_name,
|
|
||||||
pc.type as purchase_category_type,
|
|
||||||
COALESCE(SUM(ei.amount), 0) as total_amount,
|
|
||||||
COUNT(DISTINCT e.id) as expense_count,
|
|
||||||
COUNT(ei.id) as item_count
|
|
||||||
`).
|
|
||||||
Joins("JOIN expenses e ON ei.expense_id = e.id").
|
|
||||||
Joins("JOIN purchase_categories pc ON ei.purchase_category_id = pc.id").
|
|
||||||
Where("e.organization_id = ?", organizationID).
|
|
||||||
Where("pc.type = ?", entities.PurchaseCategoryTypeExpense).
|
|
||||||
Where("e.status = ?", "approved").
|
|
||||||
Where("e.transaction_date >= ? AND e.transaction_date <= ?", dateFrom, dateTo).
|
|
||||||
Group("pc.id, pc.name, pc.type").
|
|
||||||
Order("total_amount DESC")
|
|
||||||
if outletID != nil {
|
|
||||||
categoryQuery = categoryQuery.Where("e.outlet_id = ?", *outletID)
|
|
||||||
}
|
|
||||||
if err := categoryQuery.Scan(&categoryData).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var chartOfAccountData []entities.ExpenseAnalyticsChartOfAccountData
|
|
||||||
chartOfAccountQuery := r.db.WithContext(ctx).
|
|
||||||
Table("expense_items ei").
|
Table("expense_items ei").
|
||||||
Select(`
|
Select(`
|
||||||
COALESCE(parent_coa.id, coa.id) as chart_of_account_id,
|
COALESCE(parent_coa.id, coa.id) as chart_of_account_id,
|
||||||
@@ -238,9 +209,9 @@ func (r *ExpenseRepositoryImpl) GetAnalytics(ctx context.Context, organizationID
|
|||||||
Group("COALESCE(parent_coa.id, coa.id), COALESCE(parent_coa.name, coa.name, 'Lain-lain')").
|
Group("COALESCE(parent_coa.id, coa.id), COALESCE(parent_coa.name, coa.name, 'Lain-lain')").
|
||||||
Order("total_amount DESC")
|
Order("total_amount DESC")
|
||||||
if outletID != nil {
|
if outletID != nil {
|
||||||
chartOfAccountQuery = chartOfAccountQuery.Where("e.outlet_id = ?", *outletID)
|
categoryQuery = categoryQuery.Where("e.outlet_id = ?", *outletID)
|
||||||
}
|
}
|
||||||
if err := chartOfAccountQuery.Scan(&chartOfAccountData).Error; err != nil {
|
if err := categoryQuery.Scan(&categoryData).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,11 +239,10 @@ func (r *ExpenseRepositoryImpl) GetAnalytics(ctx context.Context, organizationID
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &entities.ExpenseAnalytics{
|
return &entities.ExpenseAnalytics{
|
||||||
Summary: summary,
|
Summary: summary,
|
||||||
Data: data,
|
Data: data,
|
||||||
CategoryData: categoryData,
|
CategoryData: categoryData,
|
||||||
ChartOfAccountData: chartOfAccountData,
|
ItemData: itemData,
|
||||||
ItemData: itemData,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ func (r *PurchaseOrderRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID)
|
|||||||
err := r.db.WithContext(ctx).
|
err := r.db.WithContext(ctx).
|
||||||
Preload("Vendor").
|
Preload("Vendor").
|
||||||
Preload("Items.Ingredient").
|
Preload("Items.Ingredient").
|
||||||
Preload("Items.PurchaseCategory").
|
|
||||||
Preload("Items.Unit").
|
Preload("Items.Unit").
|
||||||
Preload("Attachments.File").
|
Preload("Attachments.File").
|
||||||
First(&po, "id = ?", id).Error
|
First(&po, "id = ?", id).Error
|
||||||
@@ -46,7 +45,6 @@ func (r *PurchaseOrderRepositoryImpl) GetByIDAndOrganizationID(ctx context.Conte
|
|||||||
err := r.db.WithContext(ctx).
|
err := r.db.WithContext(ctx).
|
||||||
Preload("Vendor").
|
Preload("Vendor").
|
||||||
Preload("Items.Ingredient").
|
Preload("Items.Ingredient").
|
||||||
Preload("Items.PurchaseCategory").
|
|
||||||
Preload("Items.Unit").
|
Preload("Items.Unit").
|
||||||
Preload("Attachments.File").
|
Preload("Attachments.File").
|
||||||
Where("id = ? AND organization_id = ?", id, organizationID).
|
Where("id = ? AND organization_id = ?", id, organizationID).
|
||||||
@@ -107,7 +105,6 @@ func (r *PurchaseOrderRepositoryImpl) List(ctx context.Context, organizationID u
|
|||||||
err := query.
|
err := query.
|
||||||
Preload("Vendor").
|
Preload("Vendor").
|
||||||
Preload("Items.Ingredient").
|
Preload("Items.Ingredient").
|
||||||
Preload("Items.PurchaseCategory").
|
|
||||||
Preload("Items.Unit").
|
Preload("Items.Unit").
|
||||||
Preload("Attachments.File").
|
Preload("Attachments.File").
|
||||||
Order("created_at DESC").
|
Order("created_at DESC").
|
||||||
@@ -171,7 +168,6 @@ func (r *PurchaseOrderRepositoryImpl) GetByStatus(ctx context.Context, organizat
|
|||||||
Where("organization_id = ? AND status = ?", organizationID, status).
|
Where("organization_id = ? AND status = ?", organizationID, status).
|
||||||
Preload("Vendor").
|
Preload("Vendor").
|
||||||
Preload("Items.Ingredient").
|
Preload("Items.Ingredient").
|
||||||
Preload("Items.PurchaseCategory").
|
|
||||||
Preload("Items.Unit").
|
Preload("Items.Unit").
|
||||||
Find(&pos).Error
|
Find(&pos).Error
|
||||||
return pos, err
|
return pos, err
|
||||||
@@ -183,7 +179,6 @@ func (r *PurchaseOrderRepositoryImpl) GetOverdue(ctx context.Context, organizati
|
|||||||
Where("organization_id = ? AND due_date < ? AND status IN (?)", organizationID, time.Now(), []string{"draft", "sent", "approved"}).
|
Where("organization_id = ? AND due_date < ? AND status IN (?)", organizationID, time.Now(), []string{"draft", "sent", "approved"}).
|
||||||
Preload("Vendor").
|
Preload("Vendor").
|
||||||
Preload("Items.Ingredient").
|
Preload("Items.Ingredient").
|
||||||
Preload("Items.PurchaseCategory").
|
|
||||||
Preload("Items.Unit").
|
Preload("Items.Unit").
|
||||||
Find(&pos).Error
|
Find(&pos).Error
|
||||||
return pos, err
|
return pos, err
|
||||||
@@ -224,7 +219,6 @@ func (r *PurchaseOrderRepositoryImpl) GetItemsByPurchaseOrderID(ctx context.Cont
|
|||||||
var items []*entities.PurchaseOrderItem
|
var items []*entities.PurchaseOrderItem
|
||||||
err := r.db.WithContext(ctx).
|
err := r.db.WithContext(ctx).
|
||||||
Preload("Ingredient").
|
Preload("Ingredient").
|
||||||
Preload("PurchaseCategory").
|
|
||||||
Preload("Unit").
|
Preload("Unit").
|
||||||
Where("purchase_order_id = ?", purchaseOrderID).
|
Where("purchase_order_id = ?", purchaseOrderID).
|
||||||
Find(&items).Error
|
Find(&items).Error
|
||||||
|
|||||||
@@ -337,8 +337,6 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
|||||||
analytics.GET("/categories", r.analyticsHandler.GetProductAnalyticsPerCategory)
|
analytics.GET("/categories", r.analyticsHandler.GetProductAnalyticsPerCategory)
|
||||||
analytics.GET("/dashboard", r.analyticsHandler.GetDashboardAnalytics)
|
analytics.GET("/dashboard", r.analyticsHandler.GetDashboardAnalytics)
|
||||||
analytics.GET("/profit-loss", r.analyticsHandler.GetProfitLossAnalytics)
|
analytics.GET("/profit-loss", r.analyticsHandler.GetProfitLossAnalytics)
|
||||||
analytics.GET("/exclusive-summary/period", r.analyticsHandler.GetExclusiveSummaryPeriod)
|
|
||||||
analytics.GET("/exclusive-summary/monthly", r.analyticsHandler.GetExclusiveSummaryMonthly)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tables := protected.Group("/tables")
|
tables := protected.Group("/tables")
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ type AnalyticsService interface {
|
|||||||
GetProductAnalyticsPerCategory(ctx context.Context, req *models.ProductAnalyticsPerCategoryRequest) (*models.ProductAnalyticsPerCategoryResponse, error)
|
GetProductAnalyticsPerCategory(ctx context.Context, req *models.ProductAnalyticsPerCategoryRequest) (*models.ProductAnalyticsPerCategoryResponse, error)
|
||||||
GetDashboardAnalytics(ctx context.Context, req *models.DashboardAnalyticsRequest) (*models.DashboardAnalyticsResponse, error)
|
GetDashboardAnalytics(ctx context.Context, req *models.DashboardAnalyticsRequest) (*models.DashboardAnalyticsResponse, error)
|
||||||
GetProfitLossAnalytics(ctx context.Context, req *models.ProfitLossAnalyticsRequest) (*models.ProfitLossAnalyticsResponse, error)
|
GetProfitLossAnalytics(ctx context.Context, req *models.ProfitLossAnalyticsRequest) (*models.ProfitLossAnalyticsResponse, error)
|
||||||
GetExclusiveSummaryPeriod(ctx context.Context, req *models.ExclusiveSummaryPeriodRequest) (*models.ExclusiveSummaryPeriodResponse, error)
|
|
||||||
GetExclusiveSummaryMonthly(ctx context.Context, req *models.ExclusiveSummaryMonthlyRequest) (*models.ExclusiveSummaryMonthlyResponse, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AnalyticsServiceImpl struct {
|
type AnalyticsServiceImpl struct {
|
||||||
@@ -322,69 +320,3 @@ func (s *AnalyticsServiceImpl) validateProfitLossAnalyticsRequest(req *models.Pr
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AnalyticsServiceImpl) GetExclusiveSummaryPeriod(ctx context.Context, req *models.ExclusiveSummaryPeriodRequest) (*models.ExclusiveSummaryPeriodResponse, error) {
|
|
||||||
if err := s.validateExclusiveSummaryPeriodRequest(req); err != nil {
|
|
||||||
return nil, fmt.Errorf("validation error: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
response, err := s.analyticsProcessor.GetExclusiveSummaryPeriod(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get exclusive summary period: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AnalyticsServiceImpl) GetExclusiveSummaryMonthly(ctx context.Context, req *models.ExclusiveSummaryMonthlyRequest) (*models.ExclusiveSummaryMonthlyResponse, error) {
|
|
||||||
if err := s.validateExclusiveSummaryMonthlyRequest(req); err != nil {
|
|
||||||
return nil, fmt.Errorf("validation error: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
response, err := s.analyticsProcessor.GetExclusiveSummaryMonthly(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get exclusive summary monthly: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AnalyticsServiceImpl) validateExclusiveSummaryPeriodRequest(req *models.ExclusiveSummaryPeriodRequest) error {
|
|
||||||
if req == nil {
|
|
||||||
return fmt.Errorf("request cannot be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.OrganizationID == uuid.Nil {
|
|
||||||
return fmt.Errorf("organization_id is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.DateFrom.IsZero() {
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AnalyticsServiceImpl) validateExclusiveSummaryMonthlyRequest(req *models.ExclusiveSummaryMonthlyRequest) error {
|
|
||||||
if req == nil {
|
|
||||||
return fmt.Errorf("request cannot be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.OrganizationID == uuid.Nil {
|
|
||||||
return fmt.Errorf("organization_id is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.Month.IsZero() {
|
|
||||||
return fmt.Errorf("month is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -41,14 +41,6 @@ func (analyticsProcessorStub) GetProfitLossAnalytics(context.Context, *models.Pr
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (analyticsProcessorStub) GetExclusiveSummaryPeriod(context.Context, *models.ExclusiveSummaryPeriodRequest) (*models.ExclusiveSummaryPeriodResponse, error) {
|
|
||||||
return &models.ExclusiveSummaryPeriodResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (analyticsProcessorStub) GetExclusiveSummaryMonthly(context.Context, *models.ExclusiveSummaryMonthlyRequest) (*models.ExclusiveSummaryMonthlyResponse, error) {
|
|
||||||
return &models.ExclusiveSummaryMonthlyResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAnalyticsServiceGetPurchasingAnalyticsValidation(t *testing.T) {
|
func TestAnalyticsServiceGetPurchasingAnalyticsValidation(t *testing.T) {
|
||||||
service := NewAnalyticsServiceImpl(analyticsProcessorStub{})
|
service := NewAnalyticsServiceImpl(analyticsProcessorStub{})
|
||||||
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
||||||
@@ -198,59 +190,3 @@ func TestAnalyticsServiceGetProfitLossAnalyticsAllowsEmptyGroupBy(t *testing.T)
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Nil(t, resp)
|
require.Nil(t, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAnalyticsServiceGetExclusiveSummaryPeriodValidation(t *testing.T) {
|
|
||||||
service := NewAnalyticsServiceImpl(analyticsProcessorStub{})
|
|
||||||
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
req *models.ExclusiveSummaryPeriodRequest
|
|
||||||
wantErr string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "nil request",
|
|
||||||
req: nil,
|
|
||||||
wantErr: "request cannot be nil",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing organization",
|
|
||||||
req: &models.ExclusiveSummaryPeriodRequest{
|
|
||||||
DateFrom: now,
|
|
||||||
DateTo: now,
|
|
||||||
},
|
|
||||||
wantErr: "organization_id is required",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "reversed dates",
|
|
||||||
req: &models.ExclusiveSummaryPeriodRequest{
|
|
||||||
OrganizationID: uuid.New(),
|
|
||||||
DateFrom: now.AddDate(0, 0, 1),
|
|
||||||
DateTo: now,
|
|
||||||
},
|
|
||||||
wantErr: "date_from cannot be after date_to",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
resp, err := service.GetExclusiveSummaryPeriod(context.Background(), tt.req)
|
|
||||||
|
|
||||||
require.Nil(t, resp)
|
|
||||||
require.Error(t, err)
|
|
||||||
require.Contains(t, err.Error(), tt.wantErr)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAnalyticsServiceGetExclusiveSummaryMonthlyValidation(t *testing.T) {
|
|
||||||
service := NewAnalyticsServiceImpl(analyticsProcessorStub{})
|
|
||||||
|
|
||||||
resp, err := service.GetExclusiveSummaryMonthly(context.Background(), &models.ExclusiveSummaryMonthlyRequest{
|
|
||||||
OrganizationID: uuid.New(),
|
|
||||||
})
|
|
||||||
|
|
||||||
require.Nil(t, resp)
|
|
||||||
require.Error(t, err)
|
|
||||||
require.Contains(t, err.Error(), "month is required")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type InventoryMovementService interface {
|
type InventoryMovementService interface {
|
||||||
CreateIngredientMovement(ctx context.Context, ingredientID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID, purchaseOrderItemID *uuid.UUID) error
|
CreateIngredientMovement(ctx context.Context, ingredientID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error
|
||||||
CreateProductMovement(ctx context.Context, productID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error
|
CreateProductMovement(ctx context.Context, productID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ func NewInventoryMovementService(inventoryMovementRepo repository.InventoryMovem
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InventoryMovementServiceImpl) CreateIngredientMovement(ctx context.Context, ingredientID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID, purchaseOrderItemID *uuid.UUID) error {
|
func (s *InventoryMovementServiceImpl) CreateIngredientMovement(ctx context.Context, ingredientID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error {
|
||||||
ingredient, err := s.ingredientRepo.GetByID(ctx, ingredientID, organizationID)
|
ingredient, err := s.ingredientRepo.GetByID(ctx, ingredientID, organizationID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -36,23 +36,22 @@ func (s *InventoryMovementServiceImpl) CreateIngredientMovement(ctx context.Cont
|
|||||||
newQuantity := previousQuantity + quantity
|
newQuantity := previousQuantity + quantity
|
||||||
|
|
||||||
movement := &entities.InventoryMovement{
|
movement := &entities.InventoryMovement{
|
||||||
ID: uuid.New(),
|
ID: uuid.New(),
|
||||||
OrganizationID: organizationID,
|
OrganizationID: organizationID,
|
||||||
OutletID: outletID,
|
OutletID: outletID,
|
||||||
ItemID: ingredientID,
|
ItemID: ingredientID,
|
||||||
ItemType: "INGREDIENT",
|
ItemType: "INGREDIENT",
|
||||||
MovementType: movementType,
|
MovementType: movementType,
|
||||||
Quantity: quantity,
|
Quantity: quantity,
|
||||||
PreviousQuantity: previousQuantity,
|
PreviousQuantity: previousQuantity,
|
||||||
NewQuantity: newQuantity,
|
NewQuantity: newQuantity,
|
||||||
UnitCost: unitCost,
|
UnitCost: unitCost,
|
||||||
TotalCost: unitCost * quantity,
|
TotalCost: unitCost * quantity,
|
||||||
ReferenceType: referenceType,
|
ReferenceType: referenceType,
|
||||||
ReferenceID: referenceID,
|
ReferenceID: referenceID,
|
||||||
PurchaseOrderItemID: purchaseOrderItemID,
|
UserID: userID,
|
||||||
UserID: userID,
|
Reason: &reason,
|
||||||
Reason: &reason,
|
CreatedAt: time.Now(),
|
||||||
CreatedAt: time.Now(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.inventoryMovementRepo.Create(ctx, movement)
|
err = s.inventoryMovementRepo.Create(ctx, movement)
|
||||||
|
|||||||
@@ -169,16 +169,12 @@ func PurchasingAnalyticsModelToContract(resp *models.PurchasingAnalyticsResponse
|
|||||||
data := make([]contract.PurchasingAnalyticsData, len(resp.Data))
|
data := make([]contract.PurchasingAnalyticsData, len(resp.Data))
|
||||||
for i, item := range resp.Data {
|
for i, item := range resp.Data {
|
||||||
data[i] = contract.PurchasingAnalyticsData{
|
data[i] = contract.PurchasingAnalyticsData{
|
||||||
Date: item.Date,
|
Date: item.Date,
|
||||||
Purchases: item.Purchases,
|
Purchases: item.Purchases,
|
||||||
RawMaterialPurchases: item.RawMaterialPurchases,
|
PurchaseOrders: item.PurchaseOrders,
|
||||||
ExpensePurchases: item.ExpensePurchases,
|
Quantity: item.Quantity,
|
||||||
PurchaseOrders: item.PurchaseOrders,
|
Ingredients: item.Ingredients,
|
||||||
RawMaterialPurchaseOrders: item.RawMaterialPurchaseOrders,
|
Vendors: item.Vendors,
|
||||||
ExpenseCount: item.ExpenseCount,
|
|
||||||
Quantity: item.Quantity,
|
|
||||||
Ingredients: item.Ingredients,
|
|
||||||
Vendors: item.Vendors,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,11 +211,7 @@ func PurchasingAnalyticsModelToContract(resp *models.PurchasingAnalyticsResponse
|
|||||||
GroupBy: resp.GroupBy,
|
GroupBy: resp.GroupBy,
|
||||||
Summary: contract.PurchasingSummary{
|
Summary: contract.PurchasingSummary{
|
||||||
TotalPurchases: resp.Summary.TotalPurchases,
|
TotalPurchases: resp.Summary.TotalPurchases,
|
||||||
RawMaterialPurchases: resp.Summary.RawMaterialPurchases,
|
|
||||||
ExpensePurchases: resp.Summary.ExpensePurchases,
|
|
||||||
TotalPurchaseOrders: resp.Summary.TotalPurchaseOrders,
|
TotalPurchaseOrders: resp.Summary.TotalPurchaseOrders,
|
||||||
RawMaterialPurchaseOrders: resp.Summary.RawMaterialPurchaseOrders,
|
|
||||||
ExpenseCount: resp.Summary.ExpenseCount,
|
|
||||||
TotalQuantity: resp.Summary.TotalQuantity,
|
TotalQuantity: resp.Summary.TotalQuantity,
|
||||||
AveragePurchaseOrderValue: resp.Summary.AveragePurchaseOrderValue,
|
AveragePurchaseOrderValue: resp.Summary.AveragePurchaseOrderValue,
|
||||||
TotalIngredients: resp.Summary.TotalIngredients,
|
TotalIngredients: resp.Summary.TotalIngredients,
|
||||||
@@ -559,229 +551,3 @@ func profitLossSummaryRowModelToContract(row models.ProfitLossSummaryRow) contra
|
|||||||
SubItems: subItems,
|
SubItems: subItems,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExclusiveSummaryPeriodContractToModel(req *contract.ExclusiveSummaryPeriodRequest) (*models.ExclusiveSummaryPeriodRequest, error) {
|
|
||||||
if req == nil {
|
|
||||||
return nil, fmt.Errorf("request cannot be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
dateFrom, dateTo, err := parseFlexibleDateRangeToJakartaTime(req.DateFrom, req.DateTo)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid date range: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if dateFrom == nil {
|
|
||||||
return nil, fmt.Errorf("date_from is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
if dateTo == nil {
|
|
||||||
return nil, fmt.Errorf("date_to is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &models.ExclusiveSummaryPeriodRequest{
|
|
||||||
OrganizationID: req.OrganizationID,
|
|
||||||
OutletID: parseOutletID(req.OutletID),
|
|
||||||
DateFrom: *dateFrom,
|
|
||||||
DateTo: *dateTo,
|
|
||||||
ExcludeGajiStaffFromReimburse: req.ExcludeGajiStaffFromReimburse,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExclusiveSummaryMonthlyContractToModel(req *contract.ExclusiveSummaryMonthlyRequest) (*models.ExclusiveSummaryMonthlyRequest, error) {
|
|
||||||
if req == nil {
|
|
||||||
return nil, fmt.Errorf("request cannot be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
month, err := parseMonthToJakartaTime(req.Month)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid month: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &models.ExclusiveSummaryMonthlyRequest{
|
|
||||||
OrganizationID: req.OrganizationID,
|
|
||||||
OutletID: parseOutletID(req.OutletID),
|
|
||||||
Month: month,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExclusiveSummaryPeriodModelToContract(resp *models.ExclusiveSummaryPeriodResponse) *contract.ExclusiveSummaryPeriodResponse {
|
|
||||||
if resp == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
hppBreakdown := make([]contract.ExclusiveSummaryCategoryBreakdown, len(resp.HPPBreakdown))
|
|
||||||
for i, item := range resp.HPPBreakdown {
|
|
||||||
hppBreakdown[i] = exclusiveSummaryCategoryBreakdownModelToContract(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
operationalBreakdown := make([]contract.ExclusiveSummaryCategoryBreakdown, len(resp.OperationalExpenseBreakdown))
|
|
||||||
for i, item := range resp.OperationalExpenseBreakdown {
|
|
||||||
operationalBreakdown[i] = exclusiveSummaryCategoryBreakdownModelToContract(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
dailySummary := make([]contract.ExclusiveSummaryDailySummary, len(resp.DailySummary))
|
|
||||||
for i, item := range resp.DailySummary {
|
|
||||||
dailySummary[i] = contract.ExclusiveSummaryDailySummary{
|
|
||||||
Date: item.Date,
|
|
||||||
TransactionCount: item.TransactionCount,
|
|
||||||
TotalCost: item.TotalCost,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dailyTransactions := make([]contract.ExclusiveSummaryDailyTransaction, len(resp.DailyTransactions))
|
|
||||||
for i, item := range resp.DailyTransactions {
|
|
||||||
dailyTransactions[i] = contract.ExclusiveSummaryDailyTransaction{
|
|
||||||
Date: item.Date,
|
|
||||||
CategoryCode: item.CategoryCode,
|
|
||||||
CategoryName: item.CategoryName,
|
|
||||||
Description: item.Description,
|
|
||||||
Amount: item.Amount,
|
|
||||||
Source: item.Source,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &contract.ExclusiveSummaryPeriodResponse{
|
|
||||||
OrganizationID: resp.OrganizationID,
|
|
||||||
OutletID: resp.OutletID,
|
|
||||||
Period: contract.ExclusiveSummaryPeriodRange{
|
|
||||||
DateFrom: resp.Period.DateFrom,
|
|
||||||
DateTo: resp.Period.DateTo,
|
|
||||||
},
|
|
||||||
Summary: contract.ExclusiveSummaryPeriodSummary{
|
|
||||||
Sales: resp.Summary.Sales,
|
|
||||||
HPP: resp.Summary.HPP,
|
|
||||||
GrossProfit: resp.Summary.GrossProfit,
|
|
||||||
SalaryTotal: resp.Summary.SalaryTotal,
|
|
||||||
SalaryDW: resp.Summary.SalaryDW,
|
|
||||||
SalaryStaff: resp.Summary.SalaryStaff,
|
|
||||||
SalaryOther: resp.Summary.SalaryOther,
|
|
||||||
OtherOperationalExpenses: resp.Summary.OtherOperationalExpenses,
|
|
||||||
OperationalExpensesTotal: resp.Summary.OperationalExpensesTotal,
|
|
||||||
TotalCost: resp.Summary.TotalCost,
|
|
||||||
NetProfit: resp.Summary.NetProfit,
|
|
||||||
},
|
|
||||||
Reimburse: contract.ExclusiveSummaryReimburse{
|
|
||||||
TotalCost: resp.Reimburse.TotalCost,
|
|
||||||
ExcludedSalaryStaff: resp.Reimburse.ExcludedSalaryStaff,
|
|
||||||
TotalReimburse: resp.Reimburse.TotalReimburse,
|
|
||||||
},
|
|
||||||
HPPBreakdown: hppBreakdown,
|
|
||||||
OperationalExpenseBreakdown: operationalBreakdown,
|
|
||||||
DailySummary: dailySummary,
|
|
||||||
DailyTransactions: dailyTransactions,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExclusiveSummaryMonthlyModelToContract(resp *models.ExclusiveSummaryMonthlyResponse) *contract.ExclusiveSummaryMonthlyResponse {
|
|
||||||
if resp == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
periods := make([]contract.ExclusiveSummaryMonthlyPeriod, len(resp.Periods))
|
|
||||||
for i, item := range resp.Periods {
|
|
||||||
periods[i] = contract.ExclusiveSummaryMonthlyPeriod{
|
|
||||||
Label: item.Label,
|
|
||||||
DateFrom: item.DateFrom,
|
|
||||||
DateTo: item.DateTo,
|
|
||||||
Sales: item.Sales,
|
|
||||||
HPP: item.HPP,
|
|
||||||
GrossProfit: item.GrossProfit,
|
|
||||||
GrossMargin: item.GrossMargin,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bankBalance := make([]contract.ExclusiveSummaryBankBalance, len(resp.BankBalance))
|
|
||||||
for i, item := range resp.BankBalance {
|
|
||||||
bankBalance[i] = contract.ExclusiveSummaryBankBalance{
|
|
||||||
Bank: item.Bank,
|
|
||||||
OpeningBalance: item.OpeningBalance,
|
|
||||||
IncomingMutation: item.IncomingMutation,
|
|
||||||
OutgoingMutation: item.OutgoingMutation,
|
|
||||||
ClosingBalance: item.ClosingBalance,
|
|
||||||
Notes: item.Notes,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &contract.ExclusiveSummaryMonthlyResponse{
|
|
||||||
OrganizationID: resp.OrganizationID,
|
|
||||||
OutletID: resp.OutletID,
|
|
||||||
Month: resp.Month,
|
|
||||||
Summary: contract.ExclusiveSummaryMonthlySummary{
|
|
||||||
TotalSales: resp.Summary.TotalSales,
|
|
||||||
HPP: resp.Summary.HPP,
|
|
||||||
GrossProfit: resp.Summary.GrossProfit,
|
|
||||||
OperationalExpensesTotal: resp.Summary.OperationalExpensesTotal,
|
|
||||||
TotalCost: resp.Summary.TotalCost,
|
|
||||||
NetProfit: resp.Summary.NetProfit,
|
|
||||||
NetProfitMargin: resp.Summary.NetProfitMargin,
|
|
||||||
},
|
|
||||||
Periods: periods,
|
|
||||||
BankBalance: bankBalance,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func exclusiveSummaryCategoryBreakdownModelToContract(item models.ExclusiveSummaryCategoryBreakdown) contract.ExclusiveSummaryCategoryBreakdown {
|
|
||||||
return contract.ExclusiveSummaryCategoryBreakdown{
|
|
||||||
CategoryCode: item.CategoryCode,
|
|
||||||
CategoryName: item.CategoryName,
|
|
||||||
Amount: item.Amount,
|
|
||||||
Percentage: item.Percentage,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseFlexibleDateRangeToJakartaTime(dateFrom, dateTo string) (*time.Time, *time.Time, error) {
|
|
||||||
fromTime, toTime, err := util.ParseDateRangeToJakartaTime(dateFrom, dateTo)
|
|
||||||
if err == nil {
|
|
||||||
return fromTime, toTime, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
fromTime, err = parseISODateToJakartaTime(dateFrom, false)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
toTime, err = parseISODateToJakartaTime(dateTo, true)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return fromTime, toTime, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseISODateToJakartaTime(dateStr string, endOfDay bool) (*time.Time, error) {
|
|
||||||
if dateStr == "" {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
date, err := time.Parse("2006-01-02", dateStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
location, err := time.LoadLocation("Asia/Jakarta")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if endOfDay {
|
|
||||||
result := time.Date(date.Year(), date.Month(), date.Day(), 23, 59, 59, 999999999, location)
|
|
||||||
return &result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
result := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, location)
|
|
||||||
return &result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseMonthToJakartaTime(month string) (time.Time, error) {
|
|
||||||
location, err := time.LoadLocation("Asia/Jakarta")
|
|
||||||
if err != nil {
|
|
||||||
return time.Time{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
parsed, err := time.ParseInLocation("2006-01", month, location)
|
|
||||||
if err != nil {
|
|
||||||
return time.Time{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return time.Date(parsed.Year(), parsed.Month(), 1, 0, 0, 0, 0, location), nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -52,47 +52,17 @@ func TestPurchasingAnalyticsContractToModelIgnoresInvalidOutlet(t *testing.T) {
|
|||||||
func TestPurchasingAnalyticsModelToContractCopiesOutletName(t *testing.T) {
|
func TestPurchasingAnalyticsModelToContractCopiesOutletName(t *testing.T) {
|
||||||
outletID := uuid.New()
|
outletID := uuid.New()
|
||||||
outletName := "Main Outlet"
|
outletName := "Main Outlet"
|
||||||
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
|
||||||
|
|
||||||
result := PurchasingAnalyticsModelToContract(&models.PurchasingAnalyticsResponse{
|
result := PurchasingAnalyticsModelToContract(&models.PurchasingAnalyticsResponse{
|
||||||
OrganizationID: uuid.New(),
|
OrganizationID: uuid.New(),
|
||||||
OutletID: &outletID,
|
OutletID: &outletID,
|
||||||
OutletName: &outletName,
|
OutletName: &outletName,
|
||||||
Summary: models.PurchasingSummary{
|
|
||||||
TotalPurchases: 300,
|
|
||||||
RawMaterialPurchases: 125,
|
|
||||||
ExpensePurchases: 175,
|
|
||||||
TotalPurchaseOrders: 3,
|
|
||||||
RawMaterialPurchaseOrders: 1,
|
|
||||||
ExpenseCount: 2,
|
|
||||||
},
|
|
||||||
Data: []models.PurchasingAnalyticsData{
|
|
||||||
{
|
|
||||||
Date: now,
|
|
||||||
Purchases: 300,
|
|
||||||
RawMaterialPurchases: 125,
|
|
||||||
ExpensePurchases: 175,
|
|
||||||
PurchaseOrders: 3,
|
|
||||||
RawMaterialPurchaseOrders: 1,
|
|
||||||
ExpenseCount: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
require.NotNil(t, result)
|
require.NotNil(t, result)
|
||||||
require.Equal(t, &outletID, result.OutletID)
|
require.Equal(t, &outletID, result.OutletID)
|
||||||
require.NotNil(t, result.OutletName)
|
require.NotNil(t, result.OutletName)
|
||||||
require.Equal(t, outletName, *result.OutletName)
|
require.Equal(t, outletName, *result.OutletName)
|
||||||
require.Equal(t, float64(300), result.Summary.TotalPurchases)
|
|
||||||
require.Equal(t, float64(125), result.Summary.RawMaterialPurchases)
|
|
||||||
require.Equal(t, float64(175), result.Summary.ExpensePurchases)
|
|
||||||
require.Equal(t, int64(3), result.Summary.TotalPurchaseOrders)
|
|
||||||
require.Equal(t, int64(1), result.Summary.RawMaterialPurchaseOrders)
|
|
||||||
require.Equal(t, int64(2), result.Summary.ExpenseCount)
|
|
||||||
require.Len(t, result.Data, 1)
|
|
||||||
require.Equal(t, float64(300), result.Data[0].Purchases)
|
|
||||||
require.Equal(t, float64(125), result.Data[0].RawMaterialPurchases)
|
|
||||||
require.Equal(t, float64(175), result.Data[0].ExpensePurchases)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPurchasingAnalyticsModelToContractOmitsNilOutletName(t *testing.T) {
|
func TestPurchasingAnalyticsModelToContractOmitsNilOutletName(t *testing.T) {
|
||||||
@@ -182,43 +152,3 @@ func TestProfitLossAnalyticsModelToContractCopiesDateRange(t *testing.T) {
|
|||||||
require.Len(t, result.MainSummary, 1)
|
require.Len(t, result.MainSummary, 1)
|
||||||
require.Equal(t, "total_omset", result.MainSummary[0].ID)
|
require.Equal(t, "total_omset", result.MainSummary[0].ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExclusiveSummaryPeriodContractToModelParsesFlexibleDates(t *testing.T) {
|
|
||||||
orgID := uuid.New()
|
|
||||||
outletID := uuid.New().String()
|
|
||||||
|
|
||||||
result, err := ExclusiveSummaryPeriodContractToModel(&contract.ExclusiveSummaryPeriodRequest{
|
|
||||||
OrganizationID: orgID,
|
|
||||||
OutletID: &outletID,
|
|
||||||
DateFrom: "2026-05-26",
|
|
||||||
DateTo: "2026-05-31",
|
|
||||||
ExcludeGajiStaffFromReimburse: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, orgID, result.OrganizationID)
|
|
||||||
require.NotNil(t, result.OutletID)
|
|
||||||
require.Equal(t, outletID, result.OutletID.String())
|
|
||||||
require.True(t, result.ExcludeGajiStaffFromReimburse)
|
|
||||||
|
|
||||||
location, err := time.LoadLocation("Asia/Jakarta")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, time.Date(2026, 5, 26, 0, 0, 0, 0, location), result.DateFrom)
|
|
||||||
require.Equal(t, time.Date(2026, 5, 31, 23, 59, 59, int(time.Second-time.Nanosecond), location), result.DateTo)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExclusiveSummaryMonthlyContractToModelParsesMonth(t *testing.T) {
|
|
||||||
orgID := uuid.New()
|
|
||||||
|
|
||||||
result, err := ExclusiveSummaryMonthlyContractToModel(&contract.ExclusiveSummaryMonthlyRequest{
|
|
||||||
OrganizationID: orgID,
|
|
||||||
Month: "2026-05",
|
|
||||||
})
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, orgID, result.OrganizationID)
|
|
||||||
|
|
||||||
location, err := time.LoadLocation("Asia/Jakarta")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, time.Date(2026, 5, 1, 0, 0, 0, 0, location), result.Month)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -27,11 +27,10 @@ func CreateExpenseRequestToModel(req *contract.CreateExpenseRequest) *models.Cre
|
|||||||
|
|
||||||
func CreateExpenseItemRequestToModel(req *contract.CreateExpenseItemRequest) models.CreateExpenseItemRequest {
|
func CreateExpenseItemRequestToModel(req *contract.CreateExpenseItemRequest) models.CreateExpenseItemRequest {
|
||||||
return models.CreateExpenseItemRequest{
|
return models.CreateExpenseItemRequest{
|
||||||
ChartOfAccountID: req.ChartOfAccountID,
|
ChartOfAccountID: req.ChartOfAccountID,
|
||||||
PurchaseCategoryID: req.PurchaseCategoryID,
|
Item: req.Item,
|
||||||
Item: req.Item,
|
Description: req.Description,
|
||||||
Description: req.Description,
|
Amount: req.Amount,
|
||||||
Amount: req.Amount,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,11 +60,10 @@ func UpdateExpenseRequestToModel(req *contract.UpdateExpenseRequest) *models.Upd
|
|||||||
|
|
||||||
func UpdateExpenseItemRequestToModel(req *contract.UpdateExpenseItemRequest) models.UpdateExpenseItemRequest {
|
func UpdateExpenseItemRequestToModel(req *contract.UpdateExpenseItemRequest) models.UpdateExpenseItemRequest {
|
||||||
return models.UpdateExpenseItemRequest{
|
return models.UpdateExpenseItemRequest{
|
||||||
ChartOfAccountID: req.ChartOfAccountID,
|
ChartOfAccountID: req.ChartOfAccountID,
|
||||||
PurchaseCategoryID: req.PurchaseCategoryID,
|
Item: req.Item,
|
||||||
Item: req.Item,
|
Description: req.Description,
|
||||||
Description: req.Description,
|
Amount: req.Amount,
|
||||||
Amount: req.Amount,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,19 +109,15 @@ func ExpenseModelResponseToResponse(expense *models.ExpenseResponse) *contract.E
|
|||||||
|
|
||||||
func ExpenseItemModelResponseToResponse(item *models.ExpenseItemResponse) contract.ExpenseItemResponse {
|
func ExpenseItemModelResponseToResponse(item *models.ExpenseItemResponse) contract.ExpenseItemResponse {
|
||||||
return contract.ExpenseItemResponse{
|
return contract.ExpenseItemResponse{
|
||||||
ID: item.ID,
|
ID: item.ID,
|
||||||
ExpenseID: item.ExpenseID,
|
ExpenseID: item.ExpenseID,
|
||||||
ChartOfAccountID: item.ChartOfAccountID,
|
ChartOfAccountID: item.ChartOfAccountID,
|
||||||
ChartOfAccountName: item.ChartOfAccountName,
|
ChartOfAccountName: item.ChartOfAccountName,
|
||||||
PurchaseCategoryID: item.PurchaseCategoryID,
|
Item: item.Item,
|
||||||
PurchaseCategoryName: item.PurchaseCategoryName,
|
Description: item.Description,
|
||||||
PurchaseCategoryType: item.PurchaseCategoryType,
|
Amount: item.Amount,
|
||||||
PurchaseCategory: PurchaseCategoryModelResponseToResponse(item.PurchaseCategory),
|
CreatedAt: item.CreatedAt,
|
||||||
Item: item.Item,
|
UpdatedAt: item.UpdatedAt,
|
||||||
Description: item.Description,
|
|
||||||
Amount: item.Amount,
|
|
||||||
CreatedAt: item.CreatedAt,
|
|
||||||
UpdatedAt: item.UpdatedAt,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,18 +176,6 @@ func ExpenseAnalyticsModelToContract(resp *models.ExpenseAnalyticsResponse) *con
|
|||||||
categoryData := make([]contract.ExpenseAnalyticsCategoryData, len(resp.CategoryData))
|
categoryData := make([]contract.ExpenseAnalyticsCategoryData, len(resp.CategoryData))
|
||||||
for i, item := range resp.CategoryData {
|
for i, item := range resp.CategoryData {
|
||||||
categoryData[i] = contract.ExpenseAnalyticsCategoryData{
|
categoryData[i] = contract.ExpenseAnalyticsCategoryData{
|
||||||
PurchaseCategoryID: item.PurchaseCategoryID,
|
|
||||||
PurchaseCategoryName: item.PurchaseCategoryName,
|
|
||||||
PurchaseCategoryType: item.PurchaseCategoryType,
|
|
||||||
TotalAmount: item.TotalAmount,
|
|
||||||
ExpenseCount: item.ExpenseCount,
|
|
||||||
ItemCount: item.ItemCount,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
chartOfAccountData := make([]contract.ExpenseAnalyticsChartOfAccountData, len(resp.ChartOfAccountData))
|
|
||||||
for i, item := range resp.ChartOfAccountData {
|
|
||||||
chartOfAccountData[i] = contract.ExpenseAnalyticsChartOfAccountData{
|
|
||||||
ChartOfAccountID: item.ChartOfAccountID,
|
ChartOfAccountID: item.ChartOfAccountID,
|
||||||
ChartOfAccountName: item.ChartOfAccountName,
|
ChartOfAccountName: item.ChartOfAccountName,
|
||||||
TotalAmount: item.TotalAmount,
|
TotalAmount: item.TotalAmount,
|
||||||
@@ -226,9 +208,8 @@ func ExpenseAnalyticsModelToContract(resp *models.ExpenseAnalyticsResponse) *con
|
|||||||
TotalCategories: resp.Summary.TotalCategories,
|
TotalCategories: resp.Summary.TotalCategories,
|
||||||
TotalItems: resp.Summary.TotalItems,
|
TotalItems: resp.Summary.TotalItems,
|
||||||
},
|
},
|
||||||
Data: data,
|
Data: data,
|
||||||
CategoryData: categoryData,
|
CategoryData: categoryData,
|
||||||
ChartOfAccountData: chartOfAccountData,
|
ItemData: itemData,
|
||||||
ItemData: itemData,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,12 +11,11 @@ func CreatePurchaseOrderRequestToModel(req *contract.CreatePurchaseOrderRequest)
|
|||||||
items := make([]models.CreatePurchaseOrderItemRequest, len(req.Items))
|
items := make([]models.CreatePurchaseOrderItemRequest, len(req.Items))
|
||||||
for i, item := range req.Items {
|
for i, item := range req.Items {
|
||||||
items[i] = models.CreatePurchaseOrderItemRequest{
|
items[i] = models.CreatePurchaseOrderItemRequest{
|
||||||
IngredientID: item.IngredientID,
|
IngredientID: item.IngredientID,
|
||||||
PurchaseCategoryID: item.PurchaseCategoryID,
|
Description: item.Description,
|
||||||
Description: item.Description,
|
Quantity: item.Quantity,
|
||||||
Quantity: item.Quantity,
|
UnitID: item.UnitID,
|
||||||
UnitID: item.UnitID,
|
Amount: item.Amount,
|
||||||
Amount: item.Amount,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,13 +54,12 @@ func UpdatePurchaseOrderRequestToModel(req *contract.UpdatePurchaseOrderRequest)
|
|||||||
items = make([]models.UpdatePurchaseOrderItemRequest, len(req.Items))
|
items = make([]models.UpdatePurchaseOrderItemRequest, len(req.Items))
|
||||||
for i, item := range req.Items {
|
for i, item := range req.Items {
|
||||||
items[i] = models.UpdatePurchaseOrderItemRequest{
|
items[i] = models.UpdatePurchaseOrderItemRequest{
|
||||||
ID: item.ID,
|
ID: item.ID,
|
||||||
IngredientID: item.IngredientID,
|
IngredientID: item.IngredientID,
|
||||||
PurchaseCategoryID: item.PurchaseCategoryID,
|
Description: item.Description,
|
||||||
Description: item.Description,
|
Quantity: item.Quantity,
|
||||||
Quantity: item.Quantity,
|
UnitID: item.UnitID,
|
||||||
UnitID: item.UnitID,
|
Amount: item.Amount,
|
||||||
Amount: item.Amount,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,16 +154,15 @@ func PurchaseOrderModelResponseToResponse(po *models.PurchaseOrderResponse) *con
|
|||||||
response.Items = make([]contract.PurchaseOrderItemResponse, len(po.Items))
|
response.Items = make([]contract.PurchaseOrderItemResponse, len(po.Items))
|
||||||
for i, item := range po.Items {
|
for i, item := range po.Items {
|
||||||
response.Items[i] = contract.PurchaseOrderItemResponse{
|
response.Items[i] = contract.PurchaseOrderItemResponse{
|
||||||
ID: item.ID,
|
ID: item.ID,
|
||||||
PurchaseOrderID: item.PurchaseOrderID,
|
PurchaseOrderID: item.PurchaseOrderID,
|
||||||
IngredientID: item.IngredientID,
|
IngredientID: item.IngredientID,
|
||||||
PurchaseCategoryID: item.PurchaseCategoryID,
|
Description: item.Description,
|
||||||
Description: item.Description,
|
Quantity: item.Quantity,
|
||||||
Quantity: item.Quantity,
|
UnitID: item.UnitID,
|
||||||
UnitID: item.UnitID,
|
Amount: item.Amount,
|
||||||
Amount: item.Amount,
|
CreatedAt: item.CreatedAt,
|
||||||
CreatedAt: item.CreatedAt,
|
UpdatedAt: item.UpdatedAt,
|
||||||
UpdatedAt: item.UpdatedAt,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map ingredient if present
|
// Map ingredient if present
|
||||||
@@ -176,10 +173,6 @@ func PurchaseOrderModelResponseToResponse(po *models.PurchaseOrderResponse) *con
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if item.PurchaseCategory != nil {
|
|
||||||
response.Items[i].PurchaseCategory = PurchaseCategoryModelResponseToResponse(item.PurchaseCategory)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map unit if present
|
// Map unit if present
|
||||||
if item.Unit != nil {
|
if item.Unit != nil {
|
||||||
response.Items[i].Unit = &contract.UnitResponse{
|
response.Items[i].Unit = &contract.UnitResponse{
|
||||||
|
|||||||
@@ -12,19 +12,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestCreatePurchaseOrderRequestToModelAllowsMissingDueDate(t *testing.T) {
|
func TestCreatePurchaseOrderRequestToModelAllowsMissingDueDate(t *testing.T) {
|
||||||
ingredientID := uuid.New()
|
|
||||||
quantity := 1.0
|
|
||||||
unitID := uuid.New()
|
|
||||||
|
|
||||||
result, err := CreatePurchaseOrderRequestToModel(&contract.CreatePurchaseOrderRequest{
|
result, err := CreatePurchaseOrderRequestToModel(&contract.CreatePurchaseOrderRequest{
|
||||||
VendorID: uuid.New(),
|
VendorID: uuid.New(),
|
||||||
PONumber: "PO-001",
|
PONumber: "PO-001",
|
||||||
TransactionDate: "2026-05-29",
|
TransactionDate: "2026-05-29",
|
||||||
Items: []contract.CreatePurchaseOrderItemRequest{
|
Items: []contract.CreatePurchaseOrderItemRequest{
|
||||||
{
|
{
|
||||||
IngredientID: &ingredientID,
|
IngredientID: uuid.New(),
|
||||||
Quantity: &quantity,
|
Quantity: 1,
|
||||||
UnitID: &unitID,
|
UnitID: uuid.New(),
|
||||||
Amount: 1000,
|
Amount: 1000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -68,18 +68,12 @@ func (v *ExpenseValidatorImpl) ValidateCreateExpenseRequest(req *contract.Create
|
|||||||
if strings.TrimSpace(item.ChartOfAccountID) == "" {
|
if strings.TrimSpace(item.ChartOfAccountID) == "" {
|
||||||
return fmt.Errorf("item %d: chart_of_account_id is required", i), constants.MissingFieldErrorCode
|
return fmt.Errorf("item %d: chart_of_account_id is required", i), constants.MissingFieldErrorCode
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(item.PurchaseCategoryID) == "" {
|
|
||||||
return fmt.Errorf("item %d: purchase_category_id is required", i), constants.MissingFieldErrorCode
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(item.Item) == "" {
|
if strings.TrimSpace(item.Item) == "" {
|
||||||
return fmt.Errorf("item %d: item is required", i), constants.MissingFieldErrorCode
|
return fmt.Errorf("item %d: item is required", i), constants.MissingFieldErrorCode
|
||||||
}
|
}
|
||||||
if _, err := uuid.Parse(item.ChartOfAccountID); err != nil {
|
if _, err := uuid.Parse(item.ChartOfAccountID); err != nil {
|
||||||
return fmt.Errorf("item %d: chart_of_account_id must be a valid UUID", i), constants.MalformedFieldErrorCode
|
return fmt.Errorf("item %d: chart_of_account_id must be a valid UUID", i), constants.MalformedFieldErrorCode
|
||||||
}
|
}
|
||||||
if _, err := uuid.Parse(item.PurchaseCategoryID); err != nil {
|
|
||||||
return fmt.Errorf("item %d: purchase_category_id must be a valid UUID", i), constants.MalformedFieldErrorCode
|
|
||||||
}
|
|
||||||
if item.Amount <= 0 {
|
if item.Amount <= 0 {
|
||||||
return fmt.Errorf("item %d: amount must be greater than 0", i), constants.MalformedFieldErrorCode
|
return fmt.Errorf("item %d: amount must be greater than 0", i), constants.MalformedFieldErrorCode
|
||||||
}
|
}
|
||||||
@@ -132,15 +126,6 @@ func (v *ExpenseValidatorImpl) ValidateUpdateExpenseRequest(req *contract.Update
|
|||||||
return fmt.Errorf("item %d: chart_of_account_id must be a valid UUID", i), constants.MalformedFieldErrorCode
|
return fmt.Errorf("item %d: chart_of_account_id must be a valid UUID", i), constants.MalformedFieldErrorCode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if item.PurchaseCategoryID == nil {
|
|
||||||
return fmt.Errorf("item %d: purchase_category_id is required", i), constants.MissingFieldErrorCode
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(*item.PurchaseCategoryID) == "" {
|
|
||||||
return fmt.Errorf("item %d: purchase_category_id cannot be empty", i), constants.MalformedFieldErrorCode
|
|
||||||
}
|
|
||||||
if _, err := uuid.Parse(*item.PurchaseCategoryID); err != nil {
|
|
||||||
return fmt.Errorf("item %d: purchase_category_id must be a valid UUID", i), constants.MalformedFieldErrorCode
|
|
||||||
}
|
|
||||||
if item.Item != nil && strings.TrimSpace(*item.Item) == "" {
|
if item.Item != nil && strings.TrimSpace(*item.Item) == "" {
|
||||||
return fmt.Errorf("item %d: item cannot be empty", i), constants.MalformedFieldErrorCode
|
return fmt.Errorf("item %d: item cannot be empty", i), constants.MalformedFieldErrorCode
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,8 @@ func TestExpenseValidatorCreateRequiresItemName(t *testing.T) {
|
|||||||
Total: 10000,
|
Total: 10000,
|
||||||
Items: []contract.CreateExpenseItemRequest{
|
Items: []contract.CreateExpenseItemRequest{
|
||||||
{
|
{
|
||||||
ChartOfAccountID: uuid.NewString(),
|
ChartOfAccountID: uuid.NewString(),
|
||||||
PurchaseCategoryID: uuid.NewString(),
|
Amount: 10000,
|
||||||
Amount: 10000,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -46,10 +45,9 @@ func TestExpenseValidatorCreateDoesNotRequireHeaderExpenseName(t *testing.T) {
|
|||||||
Total: 10000,
|
Total: 10000,
|
||||||
Items: []contract.CreateExpenseItemRequest{
|
Items: []contract.CreateExpenseItemRequest{
|
||||||
{
|
{
|
||||||
ChartOfAccountID: uuid.NewString(),
|
ChartOfAccountID: uuid.NewString(),
|
||||||
PurchaseCategoryID: uuid.NewString(),
|
Item: "Cleaning supplies",
|
||||||
Item: "Cleaning supplies",
|
Amount: 10000,
|
||||||
Amount: 10000,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -73,10 +71,9 @@ func TestExpenseValidatorCreateAllowsValidOptionalStatus(t *testing.T) {
|
|||||||
Total: 10000,
|
Total: 10000,
|
||||||
Items: []contract.CreateExpenseItemRequest{
|
Items: []contract.CreateExpenseItemRequest{
|
||||||
{
|
{
|
||||||
ChartOfAccountID: uuid.NewString(),
|
ChartOfAccountID: uuid.NewString(),
|
||||||
PurchaseCategoryID: uuid.NewString(),
|
Item: "Cleaning supplies",
|
||||||
Item: "Cleaning supplies",
|
Amount: 10000,
|
||||||
Amount: 10000,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -100,10 +97,9 @@ func TestExpenseValidatorCreateRejectsInvalidStatus(t *testing.T) {
|
|||||||
Total: 10000,
|
Total: 10000,
|
||||||
Items: []contract.CreateExpenseItemRequest{
|
Items: []contract.CreateExpenseItemRequest{
|
||||||
{
|
{
|
||||||
ChartOfAccountID: uuid.NewString(),
|
ChartOfAccountID: uuid.NewString(),
|
||||||
PurchaseCategoryID: uuid.NewString(),
|
Item: "Cleaning supplies",
|
||||||
Item: "Cleaning supplies",
|
Amount: 10000,
|
||||||
Amount: 10000,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -118,11 +114,10 @@ func TestExpenseValidatorCreateRejectsInvalidStatus(t *testing.T) {
|
|||||||
func TestExpenseValidatorUpdateRejectsEmptyItemNameWhenProvided(t *testing.T) {
|
func TestExpenseValidatorUpdateRejectsEmptyItemNameWhenProvided(t *testing.T) {
|
||||||
v := NewExpenseValidator()
|
v := NewExpenseValidator()
|
||||||
empty := " "
|
empty := " "
|
||||||
purchaseCategoryID := uuid.NewString()
|
|
||||||
|
|
||||||
req := &contract.UpdateExpenseRequest{
|
req := &contract.UpdateExpenseRequest{
|
||||||
Items: []contract.UpdateExpenseItemRequest{
|
Items: []contract.UpdateExpenseItemRequest{
|
||||||
{PurchaseCategoryID: &purchaseCategoryID, Item: &empty},
|
{Item: &empty},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func (v *PurchaseCategoryValidatorImpl) ValidateCreatePurchaseCategoryRequest(re
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !isValidPurchaseCategoryType(req.Type) {
|
if !isValidPurchaseCategoryType(req.Type) {
|
||||||
return errors.New("type must be raw_material or expense"), constants.MalformedFieldErrorCode
|
return errors.New("type must be raw_material or non_inventory"), constants.MalformedFieldErrorCode
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Code != nil && len(strings.TrimSpace(*req.Code)) > 100 {
|
if req.Code != nil && len(strings.TrimSpace(*req.Code)) > 100 {
|
||||||
@@ -63,7 +63,7 @@ func (v *PurchaseCategoryValidatorImpl) ValidateUpdatePurchaseCategoryRequest(re
|
|||||||
}
|
}
|
||||||
|
|
||||||
if req.Type != nil && !isValidPurchaseCategoryType(*req.Type) {
|
if req.Type != nil && !isValidPurchaseCategoryType(*req.Type) {
|
||||||
return errors.New("type must be raw_material or expense"), constants.MalformedFieldErrorCode
|
return errors.New("type must be raw_material or non_inventory"), constants.MalformedFieldErrorCode
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Code != nil && len(strings.TrimSpace(*req.Code)) > 100 {
|
if req.Code != nil && len(strings.TrimSpace(*req.Code)) > 100 {
|
||||||
@@ -87,7 +87,7 @@ func (v *PurchaseCategoryValidatorImpl) ValidateListPurchaseCategoriesRequest(re
|
|||||||
}
|
}
|
||||||
|
|
||||||
if req.Type != "" && !isValidPurchaseCategoryType(req.Type) {
|
if req.Type != "" && !isValidPurchaseCategoryType(req.Type) {
|
||||||
return errors.New("type must be raw_material or expense"), constants.MalformedFieldErrorCode
|
return errors.New("type must be raw_material or non_inventory"), constants.MalformedFieldErrorCode
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, ""
|
return nil, ""
|
||||||
@@ -95,7 +95,7 @@ func (v *PurchaseCategoryValidatorImpl) ValidateListPurchaseCategoriesRequest(re
|
|||||||
|
|
||||||
func isValidPurchaseCategoryType(categoryType string) bool {
|
func isValidPurchaseCategoryType(categoryType string) bool {
|
||||||
switch categoryType {
|
switch categoryType {
|
||||||
case "raw_material", "expense":
|
case "raw_material", "non_inventory":
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -2,14 +2,11 @@ package validator
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"apskel-pos-be/internal/constants"
|
"apskel-pos-be/internal/constants"
|
||||||
"apskel-pos-be/internal/contract"
|
"apskel-pos-be/internal/contract"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type PurchaseOrderValidator interface {
|
type PurchaseOrderValidator interface {
|
||||||
@@ -29,7 +26,7 @@ func (v *PurchaseOrderValidatorImpl) ValidateCreatePurchaseOrderRequest(req *con
|
|||||||
return errors.New("request body is required"), constants.MissingFieldErrorCode
|
return errors.New("request body is required"), constants.MissingFieldErrorCode
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.VendorID == uuid.Nil {
|
if req.VendorID.String() == "" {
|
||||||
return errors.New("vendor_id is required"), constants.MissingFieldErrorCode
|
return errors.New("vendor_id is required"), constants.MissingFieldErrorCode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,48 +178,32 @@ func (v *PurchaseOrderValidatorImpl) ValidateListPurchaseOrdersRequest(req *cont
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (v *PurchaseOrderValidatorImpl) validatePurchaseOrderItem(item *contract.CreatePurchaseOrderItemRequest, index int) (error, string) {
|
func (v *PurchaseOrderValidatorImpl) validatePurchaseOrderItem(item *contract.CreatePurchaseOrderItemRequest, index int) (error, string) {
|
||||||
if item.PurchaseCategoryID == uuid.Nil {
|
if item.IngredientID.String() == "" {
|
||||||
return errors.New("items[" + strconv.Itoa(index) + "].purchase_category_id is required"), constants.MissingFieldErrorCode
|
return errors.New("items[" + string(rune(index)) + "].ingredient_id is required"), constants.MissingFieldErrorCode
|
||||||
}
|
}
|
||||||
|
|
||||||
if item.IngredientID != nil && *item.IngredientID == uuid.Nil {
|
if item.Quantity <= 0 {
|
||||||
return errors.New("items[" + strconv.Itoa(index) + "].ingredient_id cannot be empty"), constants.MalformedFieldErrorCode
|
return errors.New("items[" + string(rune(index)) + "].quantity must be greater than 0"), constants.MalformedFieldErrorCode
|
||||||
}
|
}
|
||||||
|
|
||||||
if item.Quantity != nil && *item.Quantity <= 0 {
|
if item.UnitID.String() == "" {
|
||||||
return errors.New("items[" + strconv.Itoa(index) + "].quantity must be greater than 0"), constants.MalformedFieldErrorCode
|
return errors.New("items[" + string(rune(index)) + "].unit_id is required"), constants.MissingFieldErrorCode
|
||||||
}
|
|
||||||
|
|
||||||
if item.UnitID != nil && *item.UnitID == uuid.Nil {
|
|
||||||
return errors.New("items[" + strconv.Itoa(index) + "].unit_id cannot be empty"), constants.MalformedFieldErrorCode
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if item.Amount < 0 {
|
if item.Amount < 0 {
|
||||||
return errors.New("items[" + strconv.Itoa(index) + "].amount must be greater than or equal to 0"), constants.MalformedFieldErrorCode
|
return errors.New("items[" + string(rune(index)) + "].amount must be greater than or equal to 0"), constants.MalformedFieldErrorCode
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, ""
|
return nil, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *PurchaseOrderValidatorImpl) validateUpdatePurchaseOrderItem(item *contract.UpdatePurchaseOrderItemRequest, index int) (error, string) {
|
func (v *PurchaseOrderValidatorImpl) validateUpdatePurchaseOrderItem(item *contract.UpdatePurchaseOrderItemRequest, index int) (error, string) {
|
||||||
if item.PurchaseCategoryID == nil || *item.PurchaseCategoryID == uuid.Nil {
|
|
||||||
return errors.New("items[" + strconv.Itoa(index) + "].purchase_category_id is required"), constants.MissingFieldErrorCode
|
|
||||||
}
|
|
||||||
|
|
||||||
if item.IngredientID != nil && *item.IngredientID == uuid.Nil {
|
|
||||||
return errors.New("items[" + strconv.Itoa(index) + "].ingredient_id cannot be empty"), constants.MalformedFieldErrorCode
|
|
||||||
}
|
|
||||||
|
|
||||||
if item.UnitID != nil && *item.UnitID == uuid.Nil {
|
|
||||||
return errors.New("items[" + strconv.Itoa(index) + "].unit_id cannot be empty"), constants.MalformedFieldErrorCode
|
|
||||||
}
|
|
||||||
|
|
||||||
if item.Quantity != nil && *item.Quantity <= 0 {
|
if item.Quantity != nil && *item.Quantity <= 0 {
|
||||||
return errors.New("items[" + strconv.Itoa(index) + "].quantity must be greater than 0"), constants.MalformedFieldErrorCode
|
return errors.New("items[" + string(rune(index)) + "].quantity must be greater than 0"), constants.MalformedFieldErrorCode
|
||||||
}
|
}
|
||||||
|
|
||||||
if item.Amount != nil && *item.Amount < 0 {
|
if item.Amount != nil && *item.Amount < 0 {
|
||||||
return errors.New("items[" + strconv.Itoa(index) + "].amount must be greater than or equal to 0"), constants.MalformedFieldErrorCode
|
return errors.New("items[" + string(rune(index)) + "].amount must be greater than or equal to 0"), constants.MalformedFieldErrorCode
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, ""
|
return nil, ""
|
||||||
|
|||||||
@@ -11,21 +11,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func validCreatePurchaseOrderRequest() *contract.CreatePurchaseOrderRequest {
|
func validCreatePurchaseOrderRequest() *contract.CreatePurchaseOrderRequest {
|
||||||
ingredientID := uuid.New()
|
|
||||||
quantity := 1.0
|
|
||||||
unitID := uuid.New()
|
|
||||||
|
|
||||||
return &contract.CreatePurchaseOrderRequest{
|
return &contract.CreatePurchaseOrderRequest{
|
||||||
VendorID: uuid.New(),
|
VendorID: uuid.New(),
|
||||||
PONumber: "PO-001",
|
PONumber: "PO-001",
|
||||||
TransactionDate: "2026-05-29",
|
TransactionDate: "2026-05-29",
|
||||||
Items: []contract.CreatePurchaseOrderItemRequest{
|
Items: []contract.CreatePurchaseOrderItemRequest{
|
||||||
{
|
{
|
||||||
IngredientID: &ingredientID,
|
IngredientID: uuid.New(),
|
||||||
PurchaseCategoryID: uuid.New(),
|
Quantity: 1,
|
||||||
Quantity: &quantity,
|
UnitID: uuid.New(),
|
||||||
UnitID: &unitID,
|
Amount: 1000,
|
||||||
Amount: 1000,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ CREATE TABLE purchase_category_presets (
|
|||||||
parent_id UUID REFERENCES purchase_category_presets(id) ON DELETE SET NULL,
|
parent_id UUID REFERENCES purchase_category_presets(id) ON DELETE SET NULL,
|
||||||
code VARCHAR(100) NOT NULL UNIQUE,
|
code VARCHAR(100) NOT NULL UNIQUE,
|
||||||
name VARCHAR(255) NOT NULL,
|
name VARCHAR(255) NOT NULL,
|
||||||
type VARCHAR(20) NOT NULL CHECK (type IN ('raw_material', 'expense')),
|
type VARCHAR(20) NOT NULL CHECK (type IN ('raw_material', 'non_inventory')),
|
||||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
@@ -17,7 +17,7 @@ CREATE TABLE purchase_categories (
|
|||||||
parent_id UUID REFERENCES purchase_categories(id) ON DELETE SET NULL,
|
parent_id UUID REFERENCES purchase_categories(id) ON DELETE SET NULL,
|
||||||
code VARCHAR(100) NOT NULL,
|
code VARCHAR(100) NOT NULL,
|
||||||
name VARCHAR(255) NOT NULL,
|
name VARCHAR(255) NOT NULL,
|
||||||
type VARCHAR(20) NOT NULL CHECK (type IN ('raw_material', 'expense')),
|
type VARCHAR(20) NOT NULL CHECK (type IN ('raw_material', 'non_inventory')),
|
||||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||||
is_system BOOLEAN NOT NULL DEFAULT false,
|
is_system BOOLEAN NOT NULL DEFAULT false,
|
||||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||||
@@ -38,8 +38,8 @@ CREATE INDEX idx_purchase_categories_is_active ON purchase_categories(is_active)
|
|||||||
|
|
||||||
INSERT INTO purchase_category_presets (code, name, type, sort_order)
|
INSERT INTO purchase_category_presets (code, name, type, sort_order)
|
||||||
VALUES
|
VALUES
|
||||||
('bahan_baku', 'Bahan Baku', 'raw_material', 1),
|
('hpp', 'HPP', 'raw_material', 1),
|
||||||
('biaya_lain_lain', 'Biaya Lain-lain', 'expense', 2)
|
('biaya_lain_lain', 'Biaya Lain-lain', 'non_inventory', 2)
|
||||||
ON CONFLICT (code) DO NOTHING;
|
ON CONFLICT (code) DO NOTHING;
|
||||||
|
|
||||||
INSERT INTO purchase_category_presets (parent_id, code, name, type, sort_order)
|
INSERT INTO purchase_category_presets (parent_id, code, name, type, sort_order)
|
||||||
@@ -47,14 +47,18 @@ SELECT parent.id, child.code, child.name, child.type, child.sort_order
|
|||||||
FROM purchase_category_presets parent
|
FROM purchase_category_presets parent
|
||||||
JOIN (
|
JOIN (
|
||||||
VALUES
|
VALUES
|
||||||
('biaya_lain_lain', 'biaya_atk_perlengkapan', 'ATK & Perlengkapan', 'expense', 1),
|
('hpp', 'hpp_bakso_mie_ayam', 'Bakso & Mie Ayam', 'raw_material', 1),
|
||||||
('biaya_lain_lain', 'biaya_makan_karyawan', 'Makan Karyawan', 'expense', 2),
|
('hpp', 'hpp_nusantara', 'Nusantara', 'raw_material', 2),
|
||||||
('biaya_lain_lain', 'biaya_bensin_parkir', 'Bensin & Parkir', 'expense', 3),
|
('hpp', 'hpp_ramen', 'Ramen', 'raw_material', 3),
|
||||||
('biaya_lain_lain', 'biaya_kebersihan_keamanan', 'Kebersihan & Keamanan', 'expense', 4),
|
('hpp', 'hpp_minuman_kopi', 'Minuman/Kopi', 'raw_material', 4),
|
||||||
('biaya_lain_lain', 'biaya_gaji_dw', 'Gaji DW', 'expense', 5),
|
('biaya_lain_lain', 'biaya_atk_perlengkapan', 'ATK & Perlengkapan', 'non_inventory', 1),
|
||||||
('biaya_lain_lain', 'biaya_gaji_staff', 'Gaji Staff', 'expense', 6),
|
('biaya_lain_lain', 'biaya_makan_karyawan', 'Makan Karyawan', 'non_inventory', 2),
|
||||||
('biaya_lain_lain', 'biaya_internet_server', 'Internet & Server', 'expense', 7),
|
('biaya_lain_lain', 'biaya_bensin_parkir', 'Bensin & Parkir', 'non_inventory', 3),
|
||||||
('biaya_lain_lain', 'biaya_air_listrik', 'Air & Listrik', 'expense', 8),
|
('biaya_lain_lain', 'biaya_kebersihan_keamanan', 'Kebersihan & Keamanan', 'non_inventory', 4),
|
||||||
('biaya_lain_lain', 'biaya_promosi', 'Promosi', 'expense', 9)
|
('biaya_lain_lain', 'biaya_gaji_dw', 'Gaji DW', 'non_inventory', 5),
|
||||||
|
('biaya_lain_lain', 'biaya_gaji_staff', 'Gaji Staff', 'non_inventory', 6),
|
||||||
|
('biaya_lain_lain', 'biaya_internet_server', 'Internet & Server', 'non_inventory', 7),
|
||||||
|
('biaya_lain_lain', 'biaya_air_listrik', 'Air & Listrik', 'non_inventory', 8),
|
||||||
|
('biaya_lain_lain', 'biaya_promosi', 'Promosi', 'non_inventory', 9)
|
||||||
) AS child(parent_code, code, name, type, sort_order) ON parent.code = child.parent_code
|
) AS child(parent_code, code, name, type, sort_order) ON parent.code = child.parent_code
|
||||||
ON CONFLICT (code) DO NOTHING;
|
ON CONFLICT (code) DO NOTHING;
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
DROP INDEX IF EXISTS idx_inventory_movements_purchase_order_item_id;
|
|
||||||
ALTER TABLE inventory_movements DROP COLUMN IF EXISTS purchase_order_item_id;
|
|
||||||
|
|
||||||
DROP INDEX IF EXISTS idx_purchase_order_items_purchase_category_id;
|
|
||||||
ALTER TABLE purchase_order_items DROP COLUMN IF EXISTS purchase_category_id;
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
ALTER TABLE purchase_order_items
|
|
||||||
ADD COLUMN IF NOT EXISTS purchase_category_id UUID REFERENCES purchase_categories(id) ON DELETE RESTRICT;
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_purchase_order_items_purchase_category_id
|
|
||||||
ON purchase_order_items(purchase_category_id);
|
|
||||||
|
|
||||||
ALTER TABLE inventory_movements
|
|
||||||
ADD COLUMN IF NOT EXISTS purchase_order_item_id UUID REFERENCES purchase_order_items(id) ON DELETE SET NULL;
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_inventory_movements_purchase_order_item_id
|
|
||||||
ON inventory_movements(purchase_order_item_id);
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
DROP INDEX IF EXISTS idx_expense_items_purchase_category_id;
|
|
||||||
ALTER TABLE expense_items DROP COLUMN IF EXISTS purchase_category_id;
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
ALTER TABLE expense_items
|
|
||||||
ADD COLUMN IF NOT EXISTS purchase_category_id UUID REFERENCES purchase_categories(id) ON DELETE RESTRICT;
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_expense_items_purchase_category_id
|
|
||||||
ON expense_items(purchase_category_id);
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
ALTER TABLE purchase_category_presets DROP CONSTRAINT IF EXISTS purchase_category_presets_type_check;
|
|
||||||
ALTER TABLE purchase_categories DROP CONSTRAINT IF EXISTS purchase_categories_type_check;
|
|
||||||
|
|
||||||
UPDATE purchase_category_presets
|
|
||||||
SET type = 'non_inventory'
|
|
||||||
WHERE type = 'expense';
|
|
||||||
|
|
||||||
UPDATE purchase_categories
|
|
||||||
SET type = 'non_inventory'
|
|
||||||
WHERE type = 'expense';
|
|
||||||
|
|
||||||
UPDATE purchase_category_presets
|
|
||||||
SET code = 'hpp', name = 'HPP'
|
|
||||||
WHERE code = 'bahan_baku' AND type = 'raw_material';
|
|
||||||
|
|
||||||
UPDATE purchase_categories
|
|
||||||
SET code = 'hpp', name = 'HPP'
|
|
||||||
WHERE code = 'bahan_baku' AND type = 'raw_material';
|
|
||||||
|
|
||||||
UPDATE purchase_category_presets
|
|
||||||
SET is_active = true
|
|
||||||
WHERE code LIKE 'hpp\_%' ESCAPE '\' AND type = 'raw_material';
|
|
||||||
|
|
||||||
UPDATE purchase_categories
|
|
||||||
SET is_active = true
|
|
||||||
WHERE code LIKE 'hpp\_%' ESCAPE '\' AND type = 'raw_material';
|
|
||||||
|
|
||||||
ALTER TABLE purchase_category_presets
|
|
||||||
ADD CONSTRAINT purchase_category_presets_type_check CHECK (type IN ('raw_material', 'non_inventory'));
|
|
||||||
|
|
||||||
ALTER TABLE purchase_categories
|
|
||||||
ADD CONSTRAINT purchase_categories_type_check CHECK (type IN ('raw_material', 'non_inventory'));
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
ALTER TABLE purchase_category_presets DROP CONSTRAINT IF EXISTS purchase_category_presets_type_check;
|
|
||||||
ALTER TABLE purchase_categories DROP CONSTRAINT IF EXISTS purchase_categories_type_check;
|
|
||||||
|
|
||||||
UPDATE purchase_category_presets
|
|
||||||
SET type = 'expense'
|
|
||||||
WHERE type = 'non_inventory';
|
|
||||||
|
|
||||||
UPDATE purchase_categories
|
|
||||||
SET type = 'expense'
|
|
||||||
WHERE type = 'non_inventory';
|
|
||||||
|
|
||||||
UPDATE purchase_category_presets
|
|
||||||
SET code = 'bahan_baku', name = 'Bahan Baku'
|
|
||||||
WHERE code = 'hpp' AND type = 'raw_material';
|
|
||||||
|
|
||||||
UPDATE purchase_categories
|
|
||||||
SET code = 'bahan_baku', name = 'Bahan Baku'
|
|
||||||
WHERE code = 'hpp' AND type = 'raw_material';
|
|
||||||
|
|
||||||
UPDATE purchase_category_presets
|
|
||||||
SET is_active = false
|
|
||||||
WHERE code LIKE 'hpp\_%' ESCAPE '\' AND type = 'raw_material';
|
|
||||||
|
|
||||||
UPDATE purchase_categories
|
|
||||||
SET is_active = false
|
|
||||||
WHERE code LIKE 'hpp\_%' ESCAPE '\' AND type = 'raw_material';
|
|
||||||
|
|
||||||
ALTER TABLE purchase_category_presets
|
|
||||||
ADD CONSTRAINT purchase_category_presets_type_check CHECK (type IN ('raw_material', 'expense'));
|
|
||||||
|
|
||||||
ALTER TABLE purchase_categories
|
|
||||||
ADD CONSTRAINT purchase_categories_type_check CHECK (type IN ('raw_material', 'expense'));
|
|
||||||
|
|
||||||
ALTER TABLE purchase_order_items ALTER COLUMN ingredient_id DROP NOT NULL;
|
|
||||||
ALTER TABLE purchase_order_items ALTER COLUMN quantity DROP NOT NULL;
|
|
||||||
ALTER TABLE purchase_order_items ALTER COLUMN unit_id DROP NOT NULL;
|
|
||||||
Reference in New Issue
Block a user