Compare commits
10 Commits
main
...
feature/ex
| Author | SHA1 | Date | |
|---|---|---|---|
| 1718c5adab | |||
| d0c090a657 | |||
| c3db919531 | |||
| e09feff36d | |||
| e7dd9660da | |||
| 29aeb58fc0 | |||
| 69d8c8ce5e | |||
| 094e8b2a47 | |||
| b90a3cde4a | |||
| 7c8c7fb7db |
@ -108,6 +108,8 @@ func (a *App) Initialize(cfg *config.Config) error {
|
|||||||
validators.vendorValidator,
|
validators.vendorValidator,
|
||||||
services.purchaseOrderService,
|
services.purchaseOrderService,
|
||||||
validators.purchaseOrderValidator,
|
validators.purchaseOrderValidator,
|
||||||
|
services.purchaseCategoryService,
|
||||||
|
validators.purchaseCategoryValidator,
|
||||||
services.unitConverterService,
|
services.unitConverterService,
|
||||||
validators.unitConverterValidator,
|
validators.unitConverterValidator,
|
||||||
services.chartOfAccountTypeService,
|
services.chartOfAccountTypeService,
|
||||||
@ -216,6 +218,7 @@ type repositories struct {
|
|||||||
productRecipeRepo *repository.ProductRecipeRepository
|
productRecipeRepo *repository.ProductRecipeRepository
|
||||||
vendorRepo *repository.VendorRepositoryImpl
|
vendorRepo *repository.VendorRepositoryImpl
|
||||||
purchaseOrderRepo *repository.PurchaseOrderRepositoryImpl
|
purchaseOrderRepo *repository.PurchaseOrderRepositoryImpl
|
||||||
|
purchaseCategoryRepo *repository.PurchaseCategoryRepositoryImpl
|
||||||
unitConverterRepo *repository.IngredientUnitConverterRepositoryImpl
|
unitConverterRepo *repository.IngredientUnitConverterRepositoryImpl
|
||||||
chartOfAccountTypeRepo *repository.ChartOfAccountTypeRepositoryImpl
|
chartOfAccountTypeRepo *repository.ChartOfAccountTypeRepositoryImpl
|
||||||
chartOfAccountRepo *repository.ChartOfAccountRepositoryImpl
|
chartOfAccountRepo *repository.ChartOfAccountRepositoryImpl
|
||||||
@ -269,6 +272,7 @@ func (a *App) initRepositories() *repositories {
|
|||||||
productRecipeRepo: repository.NewProductRecipeRepository(a.db),
|
productRecipeRepo: repository.NewProductRecipeRepository(a.db),
|
||||||
vendorRepo: repository.NewVendorRepositoryImpl(a.db),
|
vendorRepo: repository.NewVendorRepositoryImpl(a.db),
|
||||||
purchaseOrderRepo: repository.NewPurchaseOrderRepositoryImpl(a.db),
|
purchaseOrderRepo: repository.NewPurchaseOrderRepositoryImpl(a.db),
|
||||||
|
purchaseCategoryRepo: repository.NewPurchaseCategoryRepositoryImpl(a.db),
|
||||||
unitConverterRepo: repository.NewIngredientUnitConverterRepositoryImpl(a.db).(*repository.IngredientUnitConverterRepositoryImpl),
|
unitConverterRepo: repository.NewIngredientUnitConverterRepositoryImpl(a.db).(*repository.IngredientUnitConverterRepositoryImpl),
|
||||||
chartOfAccountTypeRepo: repository.NewChartOfAccountTypeRepositoryImpl(a.db),
|
chartOfAccountTypeRepo: repository.NewChartOfAccountTypeRepositoryImpl(a.db),
|
||||||
chartOfAccountRepo: repository.NewChartOfAccountRepositoryImpl(a.db),
|
chartOfAccountRepo: repository.NewChartOfAccountRepositoryImpl(a.db),
|
||||||
@ -317,6 +321,7 @@ type processors struct {
|
|||||||
productRecipeProcessor *processor.ProductRecipeProcessorImpl
|
productRecipeProcessor *processor.ProductRecipeProcessorImpl
|
||||||
vendorProcessor *processor.VendorProcessorImpl
|
vendorProcessor *processor.VendorProcessorImpl
|
||||||
purchaseOrderProcessor *processor.PurchaseOrderProcessorImpl
|
purchaseOrderProcessor *processor.PurchaseOrderProcessorImpl
|
||||||
|
purchaseCategoryProcessor *processor.PurchaseCategoryProcessorImpl
|
||||||
unitConverterProcessor *processor.IngredientUnitConverterProcessorImpl
|
unitConverterProcessor *processor.IngredientUnitConverterProcessorImpl
|
||||||
chartOfAccountTypeProcessor *processor.ChartOfAccountTypeProcessorImpl
|
chartOfAccountTypeProcessor *processor.ChartOfAccountTypeProcessorImpl
|
||||||
chartOfAccountProcessor *processor.ChartOfAccountProcessorImpl
|
chartOfAccountProcessor *processor.ChartOfAccountProcessorImpl
|
||||||
@ -367,7 +372,8 @@ 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.unitRepo, repos.fileRepo, inventoryMovementService, repos.unitConverterRepo),
|
purchaseOrderProcessor: processor.NewPurchaseOrderProcessorImpl(repos.purchaseOrderRepo, repos.vendorRepo, repos.ingredientRepo, repos.purchaseCategoryRepo, repos.unitRepo, repos.fileRepo, inventoryMovementService, repos.unitConverterRepo),
|
||||||
|
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),
|
||||||
chartOfAccountProcessor: processor.NewChartOfAccountProcessorImpl(repos.chartOfAccountRepo, repos.chartOfAccountTypeRepo),
|
chartOfAccountProcessor: processor.NewChartOfAccountProcessorImpl(repos.chartOfAccountRepo, repos.chartOfAccountTypeRepo),
|
||||||
@ -390,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),
|
expenseProcessor: processor.NewExpenseProcessorImpl(repos.expenseRepo, repos.purchaseCategoryRepo),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -416,6 +422,7 @@ type services struct {
|
|||||||
productRecipeService *service.ProductRecipeServiceImpl
|
productRecipeService *service.ProductRecipeServiceImpl
|
||||||
vendorService *service.VendorServiceImpl
|
vendorService *service.VendorServiceImpl
|
||||||
purchaseOrderService *service.PurchaseOrderServiceImpl
|
purchaseOrderService *service.PurchaseOrderServiceImpl
|
||||||
|
purchaseCategoryService service.PurchaseCategoryService
|
||||||
unitConverterService *service.IngredientUnitConverterServiceImpl
|
unitConverterService *service.IngredientUnitConverterServiceImpl
|
||||||
chartOfAccountTypeService service.ChartOfAccountTypeService
|
chartOfAccountTypeService service.ChartOfAccountTypeService
|
||||||
chartOfAccountService service.ChartOfAccountService
|
chartOfAccountService service.ChartOfAccountService
|
||||||
@ -455,6 +462,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
|||||||
productRecipeService := service.NewProductRecipeService(processors.productRecipeProcessor)
|
productRecipeService := service.NewProductRecipeService(processors.productRecipeProcessor)
|
||||||
vendorService := service.NewVendorService(processors.vendorProcessor)
|
vendorService := service.NewVendorService(processors.vendorProcessor)
|
||||||
purchaseOrderService := service.NewPurchaseOrderService(processors.purchaseOrderProcessor)
|
purchaseOrderService := service.NewPurchaseOrderService(processors.purchaseOrderProcessor)
|
||||||
|
purchaseCategoryService := service.NewPurchaseCategoryService(processors.purchaseCategoryProcessor)
|
||||||
unitConverterService := service.NewIngredientUnitConverterService(processors.unitConverterProcessor)
|
unitConverterService := service.NewIngredientUnitConverterService(processors.unitConverterProcessor)
|
||||||
chartOfAccountTypeService := service.NewChartOfAccountTypeService(processors.chartOfAccountTypeProcessor)
|
chartOfAccountTypeService := service.NewChartOfAccountTypeService(processors.chartOfAccountTypeProcessor)
|
||||||
chartOfAccountService := service.NewChartOfAccountService(processors.chartOfAccountProcessor)
|
chartOfAccountService := service.NewChartOfAccountService(processors.chartOfAccountProcessor)
|
||||||
@ -494,6 +502,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
|||||||
productRecipeService: productRecipeService,
|
productRecipeService: productRecipeService,
|
||||||
vendorService: vendorService,
|
vendorService: vendorService,
|
||||||
purchaseOrderService: purchaseOrderService,
|
purchaseOrderService: purchaseOrderService,
|
||||||
|
purchaseCategoryService: purchaseCategoryService,
|
||||||
unitConverterService: unitConverterService,
|
unitConverterService: unitConverterService,
|
||||||
chartOfAccountTypeService: chartOfAccountTypeService,
|
chartOfAccountTypeService: chartOfAccountTypeService,
|
||||||
chartOfAccountService: chartOfAccountService,
|
chartOfAccountService: chartOfAccountService,
|
||||||
@ -539,6 +548,7 @@ type validators struct {
|
|||||||
tableValidator *validator.TableValidator
|
tableValidator *validator.TableValidator
|
||||||
vendorValidator *validator.VendorValidatorImpl
|
vendorValidator *validator.VendorValidatorImpl
|
||||||
purchaseOrderValidator *validator.PurchaseOrderValidatorImpl
|
purchaseOrderValidator *validator.PurchaseOrderValidatorImpl
|
||||||
|
purchaseCategoryValidator *validator.PurchaseCategoryValidatorImpl
|
||||||
unitConverterValidator *validator.IngredientUnitConverterValidatorImpl
|
unitConverterValidator *validator.IngredientUnitConverterValidatorImpl
|
||||||
chartOfAccountTypeValidator *validator.ChartOfAccountTypeValidatorImpl
|
chartOfAccountTypeValidator *validator.ChartOfAccountTypeValidatorImpl
|
||||||
chartOfAccountValidator *validator.ChartOfAccountValidatorImpl
|
chartOfAccountValidator *validator.ChartOfAccountValidatorImpl
|
||||||
@ -570,6 +580,7 @@ func (a *App) initValidators() *validators {
|
|||||||
tableValidator: validator.NewTableValidator(),
|
tableValidator: validator.NewTableValidator(),
|
||||||
vendorValidator: validator.NewVendorValidator(),
|
vendorValidator: validator.NewVendorValidator(),
|
||||||
purchaseOrderValidator: validator.NewPurchaseOrderValidator(),
|
purchaseOrderValidator: validator.NewPurchaseOrderValidator(),
|
||||||
|
purchaseCategoryValidator: validator.NewPurchaseCategoryValidator(),
|
||||||
unitConverterValidator: validator.NewIngredientUnitConverterValidator().(*validator.IngredientUnitConverterValidatorImpl),
|
unitConverterValidator: validator.NewIngredientUnitConverterValidator().(*validator.IngredientUnitConverterValidatorImpl),
|
||||||
chartOfAccountTypeValidator: validator.NewChartOfAccountTypeValidator().(*validator.ChartOfAccountTypeValidatorImpl),
|
chartOfAccountTypeValidator: validator.NewChartOfAccountTypeValidator().(*validator.ChartOfAccountTypeValidatorImpl),
|
||||||
chartOfAccountValidator: validator.NewChartOfAccountValidator().(*validator.ChartOfAccountValidatorImpl),
|
chartOfAccountValidator: validator.NewChartOfAccountValidator().(*validator.ChartOfAccountValidatorImpl),
|
||||||
|
|||||||
@ -40,6 +40,7 @@ const (
|
|||||||
OutletServiceEntity = "outlet_service"
|
OutletServiceEntity = "outlet_service"
|
||||||
VendorServiceEntity = "vendor_service"
|
VendorServiceEntity = "vendor_service"
|
||||||
PurchaseOrderServiceEntity = "purchase_order_service"
|
PurchaseOrderServiceEntity = "purchase_order_service"
|
||||||
|
PurchaseCategoryServiceEntity = "purchase_category_service"
|
||||||
IngredientUnitConverterServiceEntity = "ingredient_unit_converter_service"
|
IngredientUnitConverterServiceEntity = "ingredient_unit_converter_service"
|
||||||
IngredientCompositionServiceEntity = "ingredient_composition_service"
|
IngredientCompositionServiceEntity = "ingredient_composition_service"
|
||||||
TableEntity = "table"
|
TableEntity = "table"
|
||||||
|
|||||||
@ -5,12 +5,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type CreateAccountRequest struct {
|
type CreateAccountRequest struct {
|
||||||
ChartOfAccountID uuid.UUID `json:"chart_of_account_id" validate:"required"`
|
ChartOfAccountID uuid.UUID `json:"chart_of_account_id" validate:"required"`
|
||||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||||
Number string `json:"number" validate:"required,min=1,max=50"`
|
Number string `json:"number" validate:"required,min=1,max=50"`
|
||||||
AccountType string `json:"account_type" validate:"required,oneof=cash wallet bank credit debit asset liability equity revenue expense"`
|
AccountType string `json:"account_type" validate:"required,oneof=cash wallet bank credit debit asset liability equity revenue expense"`
|
||||||
OpeningBalance float64 `json:"opening_balance"`
|
OpeningBalance float64 `json:"opening_balance"`
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateAccountRequest struct {
|
type UpdateAccountRequest struct {
|
||||||
@ -24,21 +24,21 @@ type UpdateAccountRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AccountResponse struct {
|
type AccountResponse struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id"`
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Number string `json:"number"`
|
Number string `json:"number"`
|
||||||
AccountType string `json:"account_type"`
|
AccountType string `json:"account_type"`
|
||||||
OpeningBalance float64 `json:"opening_balance"`
|
OpeningBalance float64 `json:"opening_balance"`
|
||||||
CurrentBalance float64 `json:"current_balance"`
|
CurrentBalance float64 `json:"current_balance"`
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
IsActive bool `json:"is_active"`
|
IsActive bool `json:"is_active"`
|
||||||
IsSystem bool `json:"is_system"`
|
IsSystem bool `json:"is_system"`
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
UpdatedAt string `json:"updated_at"`
|
UpdatedAt string `json:"updated_at"`
|
||||||
ChartOfAccount *ChartOfAccountResponse `json:"chart_of_account,omitempty"`
|
ChartOfAccount *ChartOfAccountResponse `json:"chart_of_account,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListAccountsRequest struct {
|
type ListAccountsRequest struct {
|
||||||
|
|||||||
@ -106,7 +106,11 @@ 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"`
|
||||||
@ -114,12 +118,16 @@ 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"`
|
||||||
PurchaseOrders int64 `json:"purchase_orders"`
|
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||||
Quantity float64 `json:"quantity"`
|
ExpensePurchases float64 `json:"expense_purchases"`
|
||||||
Ingredients int64 `json:"ingredients"`
|
PurchaseOrders int64 `json:"purchase_orders"`
|
||||||
Vendors int64 `json:"vendors"`
|
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
||||||
|
ExpenseCount int64 `json:"expense_count"`
|
||||||
|
Quantity float64 `json:"quantity"`
|
||||||
|
Ingredients int64 `json:"ingredients"`
|
||||||
|
Vendors int64 `json:"vendors"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PurchasingIngredientData struct {
|
type PurchasingIngredientData struct {
|
||||||
|
|||||||
@ -19,10 +19,11 @@ 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"`
|
||||||
Item string `json:"item" validate:"required"`
|
PurchaseCategoryID string `json:"purchase_category_id" validate:"required"`
|
||||||
Description *string `json:"description,omitempty"`
|
Item string `json:"item" validate:"required"`
|
||||||
Amount float64 `json:"amount" validate:"required"`
|
Description *string `json:"description,omitempty"`
|
||||||
|
Amount float64 `json:"amount" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateExpenseRequest struct {
|
type UpdateExpenseRequest struct {
|
||||||
@ -39,10 +40,11 @@ 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"`
|
||||||
Item *string `json:"item,omitempty"`
|
PurchaseCategoryID *string `json:"purchase_category_id,omitempty"`
|
||||||
Description *string `json:"description,omitempty"`
|
Item *string `json:"item,omitempty"`
|
||||||
Amount *float64 `json:"amount,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
|
Amount *float64 `json:"amount,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseResponse struct {
|
type ExpenseResponse struct {
|
||||||
@ -63,15 +65,19 @@ 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"`
|
||||||
Item string `json:"item"`
|
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||||
Description *string `json:"description"`
|
PurchaseCategoryName string `json:"purchase_category_name,omitempty"`
|
||||||
Amount float64 `json:"amount"`
|
PurchaseCategoryType string `json:"purchase_category_type,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
PurchaseCategory *PurchaseCategoryResponse `json:"purchase_category,omitempty"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
Item string `json:"item"`
|
||||||
|
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 {
|
||||||
@ -91,3 +97,65 @@ type ListExpenseResponse struct {
|
|||||||
Limit int `json:"limit"`
|
Limit int `json:"limit"`
|
||||||
TotalPages int `json:"total_pages"`
|
TotalPages int `json:"total_pages"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalyticsRequest struct {
|
||||||
|
OutletID *string `form:"outlet_id,omitempty"`
|
||||||
|
DateFrom string `form:"date_from" validate:"required"`
|
||||||
|
DateTo string `form:"date_to" validate:"required"`
|
||||||
|
GroupBy string `form:"group_by,default=day" validate:"omitempty,oneof=day hour week month"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalyticsResponse struct {
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
|
DateFrom time.Time `json:"date_from"`
|
||||||
|
DateTo time.Time `json:"date_to"`
|
||||||
|
GroupBy string `json:"group_by"`
|
||||||
|
Summary ExpenseAnalyticsSummary `json:"summary"`
|
||||||
|
Data []ExpenseAnalyticsData `json:"data"`
|
||||||
|
CategoryData []ExpenseAnalyticsCategoryData `json:"category_data"`
|
||||||
|
ChartOfAccountData []ExpenseAnalyticsChartOfAccountData `json:"chart_of_account_data"`
|
||||||
|
ItemData []ExpenseAnalyticsItemData `json:"item_data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalyticsSummary struct {
|
||||||
|
TotalExpenses float64 `json:"total_expenses"`
|
||||||
|
TotalExpenseCount int64 `json:"total_expense_count"`
|
||||||
|
TotalTax float64 `json:"total_tax"`
|
||||||
|
AverageExpenseValue float64 `json:"average_expense_value"`
|
||||||
|
TotalCategories int64 `json:"total_categories"`
|
||||||
|
TotalItems int64 `json:"total_items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalyticsData struct {
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
Expenses float64 `json:"expenses"`
|
||||||
|
ExpenseCount int64 `json:"expense_count"`
|
||||||
|
Tax float64 `json:"tax"`
|
||||||
|
Items int64 `json:"items"`
|
||||||
|
Categories int64 `json:"categories"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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"`
|
||||||
|
ChartOfAccountName string `json:"chart_of_account_name"`
|
||||||
|
TotalAmount float64 `json:"total_amount"`
|
||||||
|
ExpenseCount int64 `json:"expense_count"`
|
||||||
|
ItemCount int64 `json:"item_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalyticsItemData struct {
|
||||||
|
Item string `json:"item"`
|
||||||
|
TotalAmount float64 `json:"total_amount"`
|
||||||
|
ExpenseCount int64 `json:"expense_count"`
|
||||||
|
ItemCount int64 `json:"item_count"`
|
||||||
|
}
|
||||||
|
|||||||
@ -81,4 +81,3 @@ type IngredientUnitsResponse struct {
|
|||||||
BaseUnitName string `json:"base_unit_name"`
|
BaseUnitName string `json:"base_unit_name"`
|
||||||
Units []*UnitResponse `json:"units"`
|
Units []*UnitResponse `json:"units"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -26,9 +26,9 @@ type AdjustInventoryRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RestockInventoryRequest struct {
|
type RestockInventoryRequest struct {
|
||||||
OutletID uuid.UUID `json:"outlet_id" validate:"required"`
|
OutletID uuid.UUID `json:"outlet_id" validate:"required"`
|
||||||
Items []RestockItem `json:"items" validate:"required,min=1,dive"`
|
Items []RestockItem `json:"items" validate:"required,min=1,dive"`
|
||||||
Reason string `json:"reason" validate:"required,min=1,max=255"`
|
Reason string `json:"reason" validate:"required,min=1,max=255"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RestockItem struct {
|
type RestockItem struct {
|
||||||
@ -82,10 +82,10 @@ type InventoryAdjustmentResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RestockInventoryResponse struct {
|
type RestockInventoryResponse struct {
|
||||||
OutletID uuid.UUID `json:"outlet_id"`
|
OutletID uuid.UUID `json:"outlet_id"`
|
||||||
Items []RestockItemResult `json:"items"`
|
Items []RestockItemResult `json:"items"`
|
||||||
Reason string `json:"reason"`
|
Reason string `json:"reason"`
|
||||||
RestockedAt time.Time `json:"restocked_at"`
|
RestockedAt time.Time `json:"restocked_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RestockItemResult struct {
|
type RestockItemResult struct {
|
||||||
|
|||||||
@ -34,34 +34,34 @@ type BulkCreateProductRecipeRequest struct {
|
|||||||
|
|
||||||
// Response structures
|
// Response structures
|
||||||
type ProductRecipeResponse struct {
|
type ProductRecipeResponse struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id"`
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
ProductID uuid.UUID `json:"product_id"`
|
ProductID uuid.UUID `json:"product_id"`
|
||||||
VariantID *uuid.UUID `json:"variant_id"`
|
VariantID *uuid.UUID `json:"variant_id"`
|
||||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||||
Quantity float64 `json:"quantity"`
|
Quantity float64 `json:"quantity"`
|
||||||
WastePercentage float64 `json:"waste_percentage"`
|
WastePercentage float64 `json:"waste_percentage"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
Product *ProductResponse `json:"product,omitempty"`
|
Product *ProductResponse `json:"product,omitempty"`
|
||||||
ProductVariant *ProductVariantResponse `json:"product_variant,omitempty"`
|
ProductVariant *ProductVariantResponse `json:"product_variant,omitempty"`
|
||||||
Ingredient *ProductRecipeIngredientResponse `json:"ingredient,omitempty"`
|
Ingredient *ProductRecipeIngredientResponse `json:"ingredient,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProductRecipeIngredientResponse struct {
|
type ProductRecipeIngredientResponse struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id"`
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
UnitID uuid.UUID `json:"unit_id"`
|
UnitID uuid.UUID `json:"unit_id"`
|
||||||
Cost float64 `json:"cost"`
|
Cost float64 `json:"cost"`
|
||||||
Stock float64 `json:"stock"`
|
Stock float64 `json:"stock"`
|
||||||
IsSemiFinished bool `json:"is_semi_finished"`
|
IsSemiFinished bool `json:"is_semi_finished"`
|
||||||
IsActive bool `json:"is_active"`
|
IsActive bool `json:"is_active"`
|
||||||
Metadata map[string]interface{} `json:"metadata"`
|
Metadata map[string]interface{} `json:"metadata"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
Unit *ProductRecipeUnitResponse `json:"unit,omitempty"`
|
Unit *ProductRecipeUnitResponse `json:"unit,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
57
internal/contract/purchase_category_contract.go
Normal file
57
internal/contract/purchase_category_contract.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package contract
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CreatePurchaseCategoryRequest struct {
|
||||||
|
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||||
|
Code *string `json:"code,omitempty"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||||
|
Type string `json:"type" validate:"required,oneof=raw_material expense"`
|
||||||
|
SortOrder *int `json:"sort_order,omitempty"`
|
||||||
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdatePurchaseCategoryRequest struct {
|
||||||
|
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||||
|
Code *string `json:"code,omitempty"`
|
||||||
|
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
|
||||||
|
Type *string `json:"type,omitempty" validate:"omitempty,oneof=raw_material expense"`
|
||||||
|
SortOrder *int `json:"sort_order,omitempty"`
|
||||||
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListPurchaseCategoriesRequest struct {
|
||||||
|
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||||
|
Type string `json:"type,omitempty" validate:"omitempty,oneof=raw_material expense"`
|
||||||
|
Search string `json:"search,omitempty"`
|
||||||
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
|
Page int `json:"page" validate:"required,min=1"`
|
||||||
|
Limit int `json:"limit" validate:"required,min=1,max=100"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PurchaseCategoryResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
PresetID *uuid.UUID `json:"preset_id"`
|
||||||
|
ParentID *uuid.UUID `json:"parent_id"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
SortOrder int `json:"sort_order"`
|
||||||
|
IsSystem bool `json:"is_system"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListPurchaseCategoriesResponse struct {
|
||||||
|
PurchaseCategories []PurchaseCategoryResponse `json:"purchase_categories"`
|
||||||
|
TotalCount int `json:"total_count"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
TotalPages int `json:"total_pages"`
|
||||||
|
}
|
||||||
@ -19,11 +19,12 @@ type CreatePurchaseOrderRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreatePurchaseOrderItemRequest struct {
|
type CreatePurchaseOrderItemRequest struct {
|
||||||
IngredientID uuid.UUID `json:"ingredient_id" validate:"required"`
|
IngredientID *uuid.UUID `json:"ingredient_id,omitempty" validate:"omitempty"`
|
||||||
Description *string `json:"description,omitempty" validate:"omitempty"`
|
PurchaseCategoryID uuid.UUID `json:"purchase_category_id" validate:"required"`
|
||||||
Quantity float64 `json:"quantity" validate:"required,gt=0"`
|
Description *string `json:"description,omitempty" validate:"omitempty"`
|
||||||
UnitID uuid.UUID `json:"unit_id" validate:"required"`
|
Quantity *float64 `json:"quantity,omitempty" validate:"omitempty,gt=0"`
|
||||||
Amount float64 `json:"amount" validate:"required,gte=0"`
|
UnitID *uuid.UUID `json:"unit_id,omitempty" validate:"omitempty"`
|
||||||
|
Amount float64 `json:"amount" validate:"required,gte=0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdatePurchaseOrderRequest struct {
|
type UpdatePurchaseOrderRequest struct {
|
||||||
@ -39,12 +40,13 @@ 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"`
|
||||||
Description *string `json:"description,omitempty" validate:"omitempty"`
|
PurchaseCategoryID *uuid.UUID `json:"purchase_category_id,omitempty" validate:"omitempty"`
|
||||||
Quantity *float64 `json:"quantity,omitempty" validate:"omitempty,gt=0"`
|
Description *string `json:"description,omitempty" validate:"omitempty"`
|
||||||
UnitID *uuid.UUID `json:"unit_id,omitempty" validate:"omitempty"`
|
Quantity *float64 `json:"quantity,omitempty" validate:"omitempty,gt=0"`
|
||||||
Amount *float64 `json:"amount,omitempty" validate:"omitempty,gte=0"`
|
UnitID *uuid.UUID `json:"unit_id,omitempty" validate:"omitempty"`
|
||||||
|
Amount *float64 `json:"amount,omitempty" validate:"omitempty,gte=0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PurchaseOrderResponse struct {
|
type PurchaseOrderResponse struct {
|
||||||
@ -66,17 +68,19 @@ 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"`
|
||||||
Description *string `json:"description"`
|
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||||
Quantity float64 `json:"quantity"`
|
Description *string `json:"description"`
|
||||||
UnitID uuid.UUID `json:"unit_id"`
|
Quantity *float64 `json:"quantity"`
|
||||||
Amount float64 `json:"amount"`
|
UnitID *uuid.UUID `json:"unit_id"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
Amount float64 `json:"amount"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
Ingredient *IngredientResponse `json:"ingredient,omitempty"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
Unit *UnitResponse `json:"unit,omitempty"`
|
Ingredient *IngredientResponse `json:"ingredient,omitempty"`
|
||||||
|
PurchaseCategory *PurchaseCategoryResponse `json:"purchase_category,omitempty"`
|
||||||
|
Unit *UnitResponse `json:"unit,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PurchaseOrderAttachmentResponse struct {
|
type PurchaseOrderAttachmentResponse struct {
|
||||||
|
|||||||
@ -38,7 +38,11 @@ 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"`
|
||||||
@ -46,12 +50,16 @@ 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"`
|
||||||
PurchaseOrders int64 `json:"purchase_orders"`
|
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||||
Quantity float64 `json:"quantity"`
|
ExpensePurchases float64 `json:"expense_purchases"`
|
||||||
Ingredients int64 `json:"ingredients"`
|
PurchaseOrders int64 `json:"purchase_orders"`
|
||||||
Vendors int64 `json:"vendors"`
|
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
||||||
|
ExpenseCount int64 `json:"expense_count"`
|
||||||
|
Quantity float64 `json:"quantity"`
|
||||||
|
Ingredients int64 `json:"ingredients"`
|
||||||
|
Vendors int64 `json:"vendors"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PurchasingIngredientData struct {
|
type PurchasingIngredientData struct {
|
||||||
|
|||||||
@ -28,6 +28,56 @@ type Expense struct {
|
|||||||
Items []ExpenseItem `gorm:"foreignKey:ExpenseID" json:"items,omitempty"`
|
Items []ExpenseItem `gorm:"foreignKey:ExpenseID" json:"items,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalytics struct {
|
||||||
|
Summary ExpenseAnalyticsSummary
|
||||||
|
Data []ExpenseAnalyticsData
|
||||||
|
CategoryData []ExpenseAnalyticsCategoryData
|
||||||
|
ChartOfAccountData []ExpenseAnalyticsChartOfAccountData
|
||||||
|
ItemData []ExpenseAnalyticsItemData
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalyticsSummary struct {
|
||||||
|
TotalExpenses float64
|
||||||
|
TotalExpenseCount int64
|
||||||
|
TotalTax float64
|
||||||
|
AverageExpenseValue float64
|
||||||
|
TotalCategories int64
|
||||||
|
TotalItems int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalyticsData struct {
|
||||||
|
Date time.Time
|
||||||
|
Expenses float64
|
||||||
|
ExpenseCount int64
|
||||||
|
Tax float64
|
||||||
|
Items int64
|
||||||
|
Categories int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalyticsCategoryData struct {
|
||||||
|
PurchaseCategoryID uuid.UUID
|
||||||
|
PurchaseCategoryName string
|
||||||
|
PurchaseCategoryType string
|
||||||
|
TotalAmount float64
|
||||||
|
ExpenseCount int64
|
||||||
|
ItemCount int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalyticsChartOfAccountData struct {
|
||||||
|
ChartOfAccountID uuid.UUID
|
||||||
|
ChartOfAccountName string
|
||||||
|
TotalAmount float64
|
||||||
|
ExpenseCount int64
|
||||||
|
ItemCount int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalyticsItemData struct {
|
||||||
|
Item string
|
||||||
|
TotalAmount float64
|
||||||
|
ExpenseCount int64
|
||||||
|
ItemCount int64
|
||||||
|
}
|
||||||
|
|
||||||
func (e *Expense) BeforeCreate(tx *gorm.DB) error {
|
func (e *Expense) BeforeCreate(tx *gorm.DB) error {
|
||||||
if e.ID == uuid.Nil {
|
if e.ID == uuid.Nil {
|
||||||
e.ID = uuid.New()
|
e.ID = uuid.New()
|
||||||
|
|||||||
@ -9,17 +9,19 @@ 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"`
|
||||||
Item string `gorm:"not null;size:255" json:"item"`
|
PurchaseCategoryID uuid.UUID `gorm:"type:uuid;not null;index" json:"purchase_category_id"`
|
||||||
Description *string `gorm:"type:text" json:"description"`
|
Item string `gorm:"not null;size:255" json:"item"`
|
||||||
Amount float64 `gorm:"type:decimal(15,2);not null;default:0" json:"amount"`
|
Description *string `gorm:"type:text" json:"description"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
Amount float64 `gorm:"type:decimal(15,2);not null;default:0" json:"amount"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_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 {
|
||||||
|
|||||||
@ -39,4 +39,3 @@ func (iuc *IngredientUnitConverter) BeforeCreate() error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -36,34 +36,36 @@ 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"`
|
||||||
OrderID *uuid.UUID `gorm:"type:uuid;index" json:"order_id"`
|
PurchaseOrderItemID *uuid.UUID `gorm:"type:uuid;index" json:"purchase_order_item_id"`
|
||||||
PaymentID *uuid.UUID `gorm:"type:uuid;index" json:"payment_id"`
|
OrderID *uuid.UUID `gorm:"type:uuid;index" json:"order_id"`
|
||||||
UserID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_id" validate:"required"`
|
PaymentID *uuid.UUID `gorm:"type:uuid;index" json:"payment_id"`
|
||||||
Reason *string `gorm:"size:255" json:"reason"`
|
UserID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_id" validate:"required"`
|
||||||
Notes *string `gorm:"type:text" json:"notes"`
|
Reason *string `gorm:"size:255" json:"reason"`
|
||||||
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
|
Notes *string `gorm:"type:text" json:"notes"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
|
||||||
|
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"`
|
||||||
Order *Order `gorm:"foreignKey:OrderID" json:"order,omitempty"`
|
PurchaseOrderItem *PurchaseOrderItem `gorm:"foreignKey:PurchaseOrderItemID" json:"purchase_order_item,omitempty"`
|
||||||
Payment *Payment `gorm:"foreignKey:PaymentID" json:"payment,omitempty"`
|
Order *Order `gorm:"foreignKey:OrderID" json:"order,omitempty"`
|
||||||
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
Payment *Payment `gorm:"foreignKey:PaymentID" json:"payment,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 {
|
||||||
|
|||||||
@ -26,14 +26,14 @@ type OrderIngredientTransaction struct {
|
|||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
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"`
|
||||||
Order Order `gorm:"foreignKey:OrderID" json:"order,omitempty"`
|
Order Order `gorm:"foreignKey:OrderID" json:"order,omitempty"`
|
||||||
OrderItem *OrderItem `gorm:"foreignKey:OrderItemID" json:"order_item,omitempty"`
|
OrderItem *OrderItem `gorm:"foreignKey:OrderItemID" json:"order_item,omitempty"`
|
||||||
Product Product `gorm:"foreignKey:ProductID" json:"product,omitempty"`
|
Product Product `gorm:"foreignKey:ProductID" json:"product,omitempty"`
|
||||||
ProductVariant *ProductVariant `gorm:"foreignKey:ProductVariantID" json:"product_variant,omitempty"`
|
ProductVariant *ProductVariant `gorm:"foreignKey:ProductVariantID" json:"product_variant,omitempty"`
|
||||||
Ingredient Ingredient `gorm:"foreignKey:IngredientID" json:"ingredient,omitempty"`
|
Ingredient Ingredient `gorm:"foreignKey:IngredientID" json:"ingredient,omitempty"`
|
||||||
CreatedByUser User `gorm:"foreignKey:CreatedBy" json:"created_by_user,omitempty"`
|
CreatedByUser User `gorm:"foreignKey:CreatedBy" json:"created_by_user,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (oit *OrderIngredientTransaction) BeforeCreate(tx *gorm.DB) error {
|
func (oit *OrderIngredientTransaction) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
|||||||
@ -7,15 +7,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ProductIngredient struct {
|
type ProductIngredient struct {
|
||||||
ID uuid.UUID `json:"id" db:"id"`
|
ID uuid.UUID `json:"id" db:"id"`
|
||||||
OrganizationID uuid.UUID `json:"organization_id" db:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id" db:"organization_id"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id" db:"outlet_id"`
|
OutletID *uuid.UUID `json:"outlet_id" db:"outlet_id"`
|
||||||
ProductID uuid.UUID `json:"product_id" db:"product_id"`
|
ProductID uuid.UUID `json:"product_id" db:"product_id"`
|
||||||
IngredientID uuid.UUID `json:"ingredient_id" db:"ingredient_id"`
|
IngredientID uuid.UUID `json:"ingredient_id" db:"ingredient_id"`
|
||||||
Quantity float64 `json:"quantity" db:"quantity"`
|
Quantity float64 `json:"quantity" db:"quantity"`
|
||||||
WastePercentage float64 `json:"waste_percentage" db:"waste_percentage"`
|
WastePercentage float64 `json:"waste_percentage" db:"waste_percentage"`
|
||||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
Product *Product `json:"product,omitempty"`
|
Product *Product `json:"product,omitempty"`
|
||||||
|
|||||||
71
internal/entities/purchase_category.go
Normal file
71
internal/entities/purchase_category.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PurchaseCategoryType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PurchaseCategoryTypeRawMaterial PurchaseCategoryType = "raw_material"
|
||||||
|
PurchaseCategoryTypeExpense PurchaseCategoryType = "expense"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PurchaseCategoryPreset struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
ParentID *uuid.UUID `gorm:"type:uuid;index" json:"parent_id"`
|
||||||
|
Code string `gorm:"not null;unique;size:100" json:"code"`
|
||||||
|
Name string `gorm:"not null;size:255" json:"name"`
|
||||||
|
Type PurchaseCategoryType `gorm:"not null;size:20" json:"type"`
|
||||||
|
SortOrder int `gorm:"not null;default:0" json:"sort_order"`
|
||||||
|
IsActive bool `gorm:"not null;default:true" json:"is_active"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
|
||||||
|
Parent *PurchaseCategoryPreset `gorm:"foreignKey:ParentID" json:"parent,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PurchaseCategoryPreset) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if p.ID == uuid.Nil {
|
||||||
|
p.ID = uuid.New()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (PurchaseCategoryPreset) TableName() string {
|
||||||
|
return "purchase_category_presets"
|
||||||
|
}
|
||||||
|
|
||||||
|
type PurchaseCategory struct {
|
||||||
|
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"`
|
||||||
|
PresetID *uuid.UUID `gorm:"type:uuid;index" json:"preset_id"`
|
||||||
|
ParentID *uuid.UUID `gorm:"type:uuid;index" json:"parent_id"`
|
||||||
|
Code string `gorm:"not null;size:100" json:"code"`
|
||||||
|
Name string `gorm:"not null;size:255" json:"name"`
|
||||||
|
Type PurchaseCategoryType `gorm:"not null;size:20" json:"type"`
|
||||||
|
SortOrder int `gorm:"not null;default:0" json:"sort_order"`
|
||||||
|
IsSystem bool `gorm:"not null;default:false" json:"is_system"`
|
||||||
|
IsActive bool `gorm:"not null;default:true" json:"is_active"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
|
||||||
|
Organization *Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
||||||
|
Preset *PurchaseCategoryPreset `gorm:"foreignKey:PresetID" json:"preset,omitempty"`
|
||||||
|
Parent *PurchaseCategory `gorm:"foreignKey:ParentID" json:"parent,omitempty"`
|
||||||
|
Children []PurchaseCategory `gorm:"foreignKey:ParentID" json:"children,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PurchaseCategory) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if c.ID == uuid.Nil {
|
||||||
|
c.ID = uuid.New()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (PurchaseCategory) TableName() string {
|
||||||
|
return "purchase_categories"
|
||||||
|
}
|
||||||
@ -41,19 +41,21 @@ 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;not null" json:"ingredient_id" validate:"required"`
|
IngredientID *uuid.UUID `gorm:"type:uuid" json:"ingredient_id" validate:"omitempty"`
|
||||||
Description *string `gorm:"type:text" json:"description" validate:"omitempty"`
|
PurchaseCategoryID uuid.UUID `gorm:"type:uuid;not null;index" json:"purchase_category_id" validate:"required"`
|
||||||
Quantity float64 `gorm:"type:decimal(10,3);not null" json:"quantity" validate:"required,gt=0"`
|
Description *string `gorm:"type:text" json:"description" validate:"omitempty"`
|
||||||
UnitID uuid.UUID `gorm:"type:uuid;not null" json:"unit_id" validate:"required"`
|
Quantity *float64 `gorm:"type:decimal(10,3)" json:"quantity" validate:"omitempty,gt=0"`
|
||||||
Amount float64 `gorm:"type:decimal(15,2);not null" json:"amount" validate:"required,gte=0"`
|
UnitID *uuid.UUID `gorm:"type:uuid" json:"unit_id" validate:"omitempty"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
Amount float64 `gorm:"type:decimal(15,2);not null" json:"amount" validate:"required,gte=0"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_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"`
|
||||||
Unit *Unit `gorm:"foreignKey:UnitID" json:"unit,omitempty"`
|
PurchaseCategory *PurchaseCategory `gorm:"foreignKey:PurchaseCategoryID" json:"purchase_category,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 {
|
||||||
|
|||||||
@ -199,3 +199,31 @@ func (h *ExpenseHandler) ListExpenses(c *gin.Context) {
|
|||||||
|
|
||||||
util.HandleResponse(c.Writer, c.Request, expenseResponse, "ExpenseHandler::ListExpenses")
|
util.HandleResponse(c.Writer, c.Request, expenseResponse, "ExpenseHandler::ListExpenses")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *ExpenseHandler) GetExpenseAnalytics(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
var req contract.ExpenseAnalyticsRequest
|
||||||
|
if err := c.ShouldBindQuery(&req); err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("ExpenseHandler::GetExpenseAnalytics -> request binding failed")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, err.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ExpenseHandler::GetExpenseAnalytics")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if contextInfo.OutletID != uuid.Nil {
|
||||||
|
outletID := contextInfo.OutletID.String()
|
||||||
|
req.OutletID = &outletID
|
||||||
|
} else if outletID := c.Query("outlet_id"); outletID != "" {
|
||||||
|
req.OutletID = &outletID
|
||||||
|
}
|
||||||
|
|
||||||
|
expenseResponse := h.expenseService.GetExpenseAnalytics(ctx, contextInfo, &req)
|
||||||
|
if expenseResponse.HasErrors() {
|
||||||
|
errorResp := expenseResponse.GetErrors()[0]
|
||||||
|
logger.FromContext(ctx).WithError(errorResp).Error("ExpenseHandler::GetExpenseAnalytics -> Failed to get expense analytics from service")
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, expenseResponse, "ExpenseHandler::GetExpenseAnalytics")
|
||||||
|
}
|
||||||
|
|||||||
@ -275,4 +275,3 @@ func (h *IngredientUnitConverterHandler) GetUnitsByIngredientID(c *gin.Context)
|
|||||||
|
|
||||||
util.HandleResponse(c.Writer, c.Request, unitsResponse, "IngredientUnitConverterHandler::GetUnitsByIngredientID")
|
util.HandleResponse(c.Writer, c.Request, unitsResponse, "IngredientUnitConverterHandler::GetUnitsByIngredientID")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
160
internal/handler/purchase_category_handler.go
Normal file
160
internal/handler/purchase_category_handler.go
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"apskel-pos-be/internal/appcontext"
|
||||||
|
"apskel-pos-be/internal/constants"
|
||||||
|
"apskel-pos-be/internal/contract"
|
||||||
|
"apskel-pos-be/internal/logger"
|
||||||
|
"apskel-pos-be/internal/service"
|
||||||
|
"apskel-pos-be/internal/util"
|
||||||
|
"apskel-pos-be/internal/validator"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PurchaseCategoryHandler struct {
|
||||||
|
purchaseCategoryService service.PurchaseCategoryService
|
||||||
|
purchaseCategoryValidator validator.PurchaseCategoryValidator
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPurchaseCategoryHandler(purchaseCategoryService service.PurchaseCategoryService, purchaseCategoryValidator validator.PurchaseCategoryValidator) *PurchaseCategoryHandler {
|
||||||
|
return &PurchaseCategoryHandler{
|
||||||
|
purchaseCategoryService: purchaseCategoryService,
|
||||||
|
purchaseCategoryValidator: purchaseCategoryValidator,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PurchaseCategoryHandler) CreatePurchaseCategory(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
var req contract.CreatePurchaseCategoryRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("PurchaseCategoryHandler::CreatePurchaseCategory -> request binding failed")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseCategoryHandler::CreatePurchaseCategory")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if validationError, validationErrorCode := h.purchaseCategoryValidator.ValidateCreatePurchaseCategoryRequest(&req); validationError != nil {
|
||||||
|
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseCategoryHandler::CreatePurchaseCategory")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := h.purchaseCategoryService.CreatePurchaseCategory(ctx, contextInfo, &req)
|
||||||
|
util.HandleResponse(c.Writer, c.Request, response, "PurchaseCategoryHandler::CreatePurchaseCategory")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PurchaseCategoryHandler) UpdatePurchaseCategory(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
categoryID, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid purchase category ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseCategoryHandler::UpdatePurchaseCategory")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req contract.UpdatePurchaseCategoryRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("PurchaseCategoryHandler::UpdatePurchaseCategory -> request binding failed")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseCategoryHandler::UpdatePurchaseCategory")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if validationError, validationErrorCode := h.purchaseCategoryValidator.ValidateUpdatePurchaseCategoryRequest(&req); validationError != nil {
|
||||||
|
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseCategoryHandler::UpdatePurchaseCategory")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := h.purchaseCategoryService.UpdatePurchaseCategory(ctx, contextInfo, categoryID, &req)
|
||||||
|
util.HandleResponse(c.Writer, c.Request, response, "PurchaseCategoryHandler::UpdatePurchaseCategory")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PurchaseCategoryHandler) DeletePurchaseCategory(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
categoryID, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid purchase category ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseCategoryHandler::DeletePurchaseCategory")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := h.purchaseCategoryService.DeletePurchaseCategory(ctx, contextInfo, categoryID)
|
||||||
|
util.HandleResponse(c.Writer, c.Request, response, "PurchaseCategoryHandler::DeletePurchaseCategory")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PurchaseCategoryHandler) GetPurchaseCategory(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
categoryID, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid purchase category ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseCategoryHandler::GetPurchaseCategory")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := h.purchaseCategoryService.GetPurchaseCategoryByID(ctx, contextInfo, categoryID)
|
||||||
|
util.HandleResponse(c.Writer, c.Request, response, "PurchaseCategoryHandler::GetPurchaseCategory")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PurchaseCategoryHandler) ListPurchaseCategories(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
req := &contract.ListPurchaseCategoriesRequest{
|
||||||
|
Page: 1,
|
||||||
|
Limit: 100,
|
||||||
|
}
|
||||||
|
|
||||||
|
if pageStr := c.Query("page"); pageStr != "" {
|
||||||
|
if page, err := strconv.Atoi(pageStr); err == nil {
|
||||||
|
req.Page = page
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if limitStr := c.Query("limit"); limitStr != "" {
|
||||||
|
if limit, err := strconv.Atoi(limitStr); err == nil {
|
||||||
|
req.Limit = limit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if parentIDStr := c.Query("parent_id"); parentIDStr != "" {
|
||||||
|
if parentID, err := uuid.Parse(parentIDStr); err == nil {
|
||||||
|
req.ParentID = &parentID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if categoryType := c.Query("type"); categoryType != "" {
|
||||||
|
req.Type = categoryType
|
||||||
|
}
|
||||||
|
|
||||||
|
if search := c.Query("search"); search != "" {
|
||||||
|
req.Search = search
|
||||||
|
}
|
||||||
|
|
||||||
|
if isActiveStr := c.Query("is_active"); isActiveStr != "" {
|
||||||
|
if isActive, err := strconv.ParseBool(isActiveStr); err == nil {
|
||||||
|
req.IsActive = &isActive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if validationError, validationErrorCode := h.purchaseCategoryValidator.ValidateListPurchaseCategoriesRequest(req); validationError != nil {
|
||||||
|
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PurchaseCategoryHandler::ListPurchaseCategories")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := h.purchaseCategoryService.ListPurchaseCategories(ctx, contextInfo, req)
|
||||||
|
util.HandleResponse(c.Writer, c.Request, response, "PurchaseCategoryHandler::ListPurchaseCategories")
|
||||||
|
}
|
||||||
@ -95,20 +95,27 @@ 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,
|
||||||
Item: entity.Item,
|
PurchaseCategoryID: entity.PurchaseCategoryID,
|
||||||
Description: entity.Description,
|
Item: entity.Item,
|
||||||
Amount: entity.Amount,
|
Description: entity.Description,
|
||||||
CreatedAt: entity.CreatedAt,
|
Amount: entity.Amount,
|
||||||
UpdatedAt: entity.UpdatedAt,
|
CreatedAt: entity.CreatedAt,
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,17 +11,17 @@ func MapProductIngredientEntityToModel(entity *entities.ProductIngredient) *mode
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &models.ProductIngredient{
|
return &models.ProductIngredient{
|
||||||
ID: entity.ID,
|
ID: entity.ID,
|
||||||
OrganizationID: entity.OrganizationID,
|
OrganizationID: entity.OrganizationID,
|
||||||
OutletID: entity.OutletID,
|
OutletID: entity.OutletID,
|
||||||
ProductID: entity.ProductID,
|
ProductID: entity.ProductID,
|
||||||
IngredientID: entity.IngredientID,
|
IngredientID: entity.IngredientID,
|
||||||
Quantity: entity.Quantity,
|
Quantity: entity.Quantity,
|
||||||
WastePercentage: entity.WastePercentage,
|
WastePercentage: entity.WastePercentage,
|
||||||
CreatedAt: entity.CreatedAt,
|
CreatedAt: entity.CreatedAt,
|
||||||
UpdatedAt: entity.UpdatedAt,
|
UpdatedAt: entity.UpdatedAt,
|
||||||
Product: ProductEntityToModel(entity.Product),
|
Product: ProductEntityToModel(entity.Product),
|
||||||
Ingredient: MapIngredientEntityToModel(entity.Ingredient),
|
Ingredient: MapIngredientEntityToModel(entity.Ingredient),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,17 +31,17 @@ func MapProductIngredientModelToEntity(model *models.ProductIngredient) *entitie
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &entities.ProductIngredient{
|
return &entities.ProductIngredient{
|
||||||
ID: model.ID,
|
ID: model.ID,
|
||||||
OrganizationID: model.OrganizationID,
|
OrganizationID: model.OrganizationID,
|
||||||
OutletID: model.OutletID,
|
OutletID: model.OutletID,
|
||||||
ProductID: model.ProductID,
|
ProductID: model.ProductID,
|
||||||
IngredientID: model.IngredientID,
|
IngredientID: model.IngredientID,
|
||||||
Quantity: model.Quantity,
|
Quantity: model.Quantity,
|
||||||
WastePercentage: model.WastePercentage,
|
WastePercentage: model.WastePercentage,
|
||||||
CreatedAt: model.CreatedAt,
|
CreatedAt: model.CreatedAt,
|
||||||
UpdatedAt: model.UpdatedAt,
|
UpdatedAt: model.UpdatedAt,
|
||||||
Product: ProductModelToEntity(model.Product),
|
Product: ProductModelToEntity(model.Product),
|
||||||
Ingredient: MapIngredientModelToEntity(model.Ingredient),
|
Ingredient: MapIngredientModelToEntity(model.Ingredient),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
53
internal/mappers/purchase_category_mapper.go
Normal file
53
internal/mappers/purchase_category_mapper.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package mappers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreatePurchaseCategoryRequestToEntity(req *models.CreatePurchaseCategoryRequest) *entities.PurchaseCategory {
|
||||||
|
if req == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &entities.PurchaseCategory{
|
||||||
|
OrganizationID: req.OrganizationID,
|
||||||
|
ParentID: req.ParentID,
|
||||||
|
Name: req.Name,
|
||||||
|
Type: entities.PurchaseCategoryType(req.Type),
|
||||||
|
SortOrder: req.SortOrder,
|
||||||
|
IsActive: req.IsActive,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PurchaseCategoryEntityToResponse(entity *entities.PurchaseCategory) *models.PurchaseCategoryResponse {
|
||||||
|
if entity == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.PurchaseCategoryResponse{
|
||||||
|
ID: entity.ID,
|
||||||
|
OrganizationID: entity.OrganizationID,
|
||||||
|
PresetID: entity.PresetID,
|
||||||
|
ParentID: entity.ParentID,
|
||||||
|
Code: entity.Code,
|
||||||
|
Name: entity.Name,
|
||||||
|
Type: string(entity.Type),
|
||||||
|
SortOrder: entity.SortOrder,
|
||||||
|
IsSystem: entity.IsSystem,
|
||||||
|
IsActive: entity.IsActive,
|
||||||
|
CreatedAt: entity.CreatedAt,
|
||||||
|
UpdatedAt: entity.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PurchaseCategoryEntitiesToResponses(categoryEntities []*entities.PurchaseCategory) []models.PurchaseCategoryResponse {
|
||||||
|
responses := make([]models.PurchaseCategoryResponse, len(categoryEntities))
|
||||||
|
for i, entity := range categoryEntities {
|
||||||
|
response := PurchaseCategoryEntityToResponse(entity)
|
||||||
|
if response != nil {
|
||||||
|
responses[i] = *response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return responses
|
||||||
|
}
|
||||||
@ -91,15 +91,16 @@ 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,
|
||||||
Description: entity.Description,
|
PurchaseCategoryID: entity.PurchaseCategoryID,
|
||||||
Quantity: entity.Quantity,
|
Description: entity.Description,
|
||||||
UnitID: entity.UnitID,
|
Quantity: entity.Quantity,
|
||||||
Amount: entity.Amount,
|
UnitID: entity.UnitID,
|
||||||
CreatedAt: entity.CreatedAt,
|
Amount: entity.Amount,
|
||||||
UpdatedAt: entity.UpdatedAt,
|
CreatedAt: entity.CreatedAt,
|
||||||
|
UpdatedAt: entity.UpdatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,15 +110,16 @@ 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,
|
||||||
Description: model.Description,
|
PurchaseCategoryID: model.PurchaseCategoryID,
|
||||||
Quantity: model.Quantity,
|
Description: model.Description,
|
||||||
UnitID: model.UnitID,
|
Quantity: model.Quantity,
|
||||||
Amount: model.Amount,
|
UnitID: model.UnitID,
|
||||||
CreatedAt: model.CreatedAt,
|
Amount: model.Amount,
|
||||||
UpdatedAt: model.UpdatedAt,
|
CreatedAt: model.CreatedAt,
|
||||||
|
UpdatedAt: model.UpdatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,15 +129,16 @@ 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,
|
||||||
Description: entity.Description,
|
PurchaseCategoryID: entity.PurchaseCategoryID,
|
||||||
Quantity: entity.Quantity,
|
Description: entity.Description,
|
||||||
UnitID: entity.UnitID,
|
Quantity: entity.Quantity,
|
||||||
Amount: entity.Amount,
|
UnitID: entity.UnitID,
|
||||||
CreatedAt: entity.CreatedAt,
|
Amount: entity.Amount,
|
||||||
UpdatedAt: entity.UpdatedAt,
|
CreatedAt: entity.CreatedAt,
|
||||||
|
UpdatedAt: entity.UpdatedAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map ingredient if present
|
// Map ingredient if present
|
||||||
@ -146,6 +149,10 @@ 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{
|
||||||
|
|||||||
@ -25,12 +25,12 @@ type AccountResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreateAccountRequest struct {
|
type CreateAccountRequest struct {
|
||||||
ChartOfAccountID uuid.UUID `json:"chart_of_account_id" validate:"required"`
|
ChartOfAccountID uuid.UUID `json:"chart_of_account_id" validate:"required"`
|
||||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||||
Number string `json:"number" validate:"required,min=1,max=50"`
|
Number string `json:"number" validate:"required,min=1,max=50"`
|
||||||
AccountType string `json:"account_type" validate:"required,oneof=cash wallet bank credit debit asset liability equity revenue expense"`
|
AccountType string `json:"account_type" validate:"required,oneof=cash wallet bank credit debit asset liability equity revenue expense"`
|
||||||
OpeningBalance float64 `json:"opening_balance"`
|
OpeningBalance float64 `json:"opening_balance"`
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateAccountRequest struct {
|
type UpdateAccountRequest struct {
|
||||||
|
|||||||
@ -113,7 +113,11 @@ 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"`
|
||||||
@ -122,12 +126,16 @@ 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"`
|
||||||
PurchaseOrders int64 `json:"purchase_orders"`
|
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||||
Quantity float64 `json:"quantity"`
|
ExpensePurchases float64 `json:"expense_purchases"`
|
||||||
Ingredients int64 `json:"ingredients"`
|
PurchaseOrders int64 `json:"purchase_orders"`
|
||||||
Vendors int64 `json:"vendors"`
|
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
||||||
|
ExpenseCount int64 `json:"expense_count"`
|
||||||
|
Quantity float64 `json:"quantity"`
|
||||||
|
Ingredients int64 `json:"ingredients"`
|
||||||
|
Vendors int64 `json:"vendors"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PurchasingIngredientData represents purchasing analytics for an ingredient
|
// PurchasingIngredientData represents purchasing analytics for an ingredient
|
||||||
|
|||||||
@ -23,17 +23,17 @@ type UpdateCustomerRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CustomerResponse struct {
|
type CustomerResponse struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Email *string `json:"email,omitempty"`
|
Email *string `json:"email,omitempty"`
|
||||||
Phone *string `json:"phone,omitempty"`
|
Phone *string `json:"phone,omitempty"`
|
||||||
Address *string `json:"address,omitempty"`
|
Address *string `json:"address,omitempty"`
|
||||||
IsDefault bool `json:"is_default"`
|
IsDefault bool `json:"is_default"`
|
||||||
IsActive bool `json:"is_active"`
|
IsActive bool `json:"is_active"`
|
||||||
Metadata entities.Metadata `json:"metadata"`
|
Metadata entities.Metadata `json:"metadata"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListCustomersQuery represents query parameters for listing customers
|
// ListCustomersQuery represents query parameters for listing customers
|
||||||
|
|||||||
@ -23,14 +23,15 @@ 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"`
|
||||||
Item string `json:"item"`
|
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||||
Description *string `json:"description"`
|
Item string `json:"item"`
|
||||||
Amount float64 `json:"amount"`
|
Description *string `json:"description"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
Amount float64 `json:"amount"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseResponse struct {
|
type ExpenseResponse struct {
|
||||||
@ -51,15 +52,19 @@ 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"`
|
||||||
Item string `json:"item"`
|
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||||
Description *string `json:"description"`
|
PurchaseCategoryName string `json:"purchase_category_name,omitempty"`
|
||||||
Amount float64 `json:"amount"`
|
PurchaseCategoryType string `json:"purchase_category_type,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
PurchaseCategory *PurchaseCategoryResponse `json:"purchase_category,omitempty"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
Item string `json:"item"`
|
||||||
|
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 {
|
||||||
@ -75,10 +80,11 @@ type CreateExpenseRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreateExpenseItemRequest struct {
|
type CreateExpenseItemRequest struct {
|
||||||
ChartOfAccountID string `json:"chart_of_account_id"`
|
ChartOfAccountID string `json:"chart_of_account_id"`
|
||||||
Item string `json:"item"`
|
PurchaseCategoryID string `json:"purchase_category_id"`
|
||||||
Description *string `json:"description,omitempty"`
|
Item string `json:"item"`
|
||||||
Amount float64 `json:"amount"`
|
Description *string `json:"description,omitempty"`
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateExpenseRequest struct {
|
type UpdateExpenseRequest struct {
|
||||||
@ -95,10 +101,11 @@ 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"`
|
||||||
Item *string `json:"item,omitempty"`
|
PurchaseCategoryID *string `json:"purchase_category_id,omitempty"`
|
||||||
Description *string `json:"description,omitempty"`
|
Item *string `json:"item,omitempty"`
|
||||||
Amount *float64 `json:"amount,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
|
Amount *float64 `json:"amount,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListExpenseRequest struct {
|
type ListExpenseRequest struct {
|
||||||
@ -118,3 +125,66 @@ type ListExpenseResponse struct {
|
|||||||
Limit int `json:"limit"`
|
Limit int `json:"limit"`
|
||||||
TotalPages int `json:"total_pages"`
|
TotalPages int `json:"total_pages"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalyticsRequest struct {
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
|
DateFrom time.Time `json:"date_from"`
|
||||||
|
DateTo time.Time `json:"date_to"`
|
||||||
|
GroupBy string `json:"group_by"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalyticsResponse struct {
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||||
|
DateFrom time.Time `json:"date_from"`
|
||||||
|
DateTo time.Time `json:"date_to"`
|
||||||
|
GroupBy string `json:"group_by"`
|
||||||
|
Summary ExpenseAnalyticsSummary `json:"summary"`
|
||||||
|
Data []ExpenseAnalyticsData `json:"data"`
|
||||||
|
CategoryData []ExpenseAnalyticsCategoryData `json:"category_data"`
|
||||||
|
ChartOfAccountData []ExpenseAnalyticsChartOfAccountData `json:"chart_of_account_data"`
|
||||||
|
ItemData []ExpenseAnalyticsItemData `json:"item_data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalyticsSummary struct {
|
||||||
|
TotalExpenses float64 `json:"total_expenses"`
|
||||||
|
TotalExpenseCount int64 `json:"total_expense_count"`
|
||||||
|
TotalTax float64 `json:"total_tax"`
|
||||||
|
AverageExpenseValue float64 `json:"average_expense_value"`
|
||||||
|
TotalCategories int64 `json:"total_categories"`
|
||||||
|
TotalItems int64 `json:"total_items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalyticsData struct {
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
Expenses float64 `json:"expenses"`
|
||||||
|
ExpenseCount int64 `json:"expense_count"`
|
||||||
|
Tax float64 `json:"tax"`
|
||||||
|
Items int64 `json:"items"`
|
||||||
|
Categories int64 `json:"categories"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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"`
|
||||||
|
ChartOfAccountName string `json:"chart_of_account_name"`
|
||||||
|
TotalAmount float64 `json:"total_amount"`
|
||||||
|
ExpenseCount int64 `json:"expense_count"`
|
||||||
|
ItemCount int64 `json:"item_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseAnalyticsItemData struct {
|
||||||
|
Item string `json:"item"`
|
||||||
|
TotalAmount float64 `json:"total_amount"`
|
||||||
|
ExpenseCount int64 `json:"expense_count"`
|
||||||
|
ItemCount int64 `json:"item_count"`
|
||||||
|
}
|
||||||
|
|||||||
@ -101,4 +101,3 @@ type IngredientUnitsResponse struct {
|
|||||||
BaseUnitName string `json:"base_unit_name"`
|
BaseUnitName string `json:"base_unit_name"`
|
||||||
Units []*UnitResponse `json:"units"`
|
Units []*UnitResponse `json:"units"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -52,8 +52,8 @@ type UpdateOrderIngredientTransactionRequest struct {
|
|||||||
GrossQty *float64 `json:"gross_qty,omitempty" validate:"omitempty,gt=0"`
|
GrossQty *float64 `json:"gross_qty,omitempty" validate:"omitempty,gt=0"`
|
||||||
NetQty *float64 `json:"net_qty,omitempty" validate:"omitempty,gt=0"`
|
NetQty *float64 `json:"net_qty,omitempty" validate:"omitempty,gt=0"`
|
||||||
WasteQty *float64 `json:"waste_qty,omitempty" validate:"min=0"`
|
WasteQty *float64 `json:"waste_qty,omitempty" validate:"min=0"`
|
||||||
Unit *string `json:"unit,omitempty" validate:"omitempty,max=50"`
|
Unit *string `json:"unit,omitempty" validate:"omitempty,max=50"`
|
||||||
TransactionDate *time.Time `json:"transaction_date,omitempty"`
|
TransactionDate *time.Time `json:"transaction_date,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OrderIngredientTransactionResponse struct {
|
type OrderIngredientTransactionResponse struct {
|
||||||
@ -98,11 +98,11 @@ type ListOrderIngredientTransactionsRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OrderIngredientTransactionSummary struct {
|
type OrderIngredientTransactionSummary struct {
|
||||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||||
IngredientName string `json:"ingredient_name"`
|
IngredientName string `json:"ingredient_name"`
|
||||||
TotalGrossQty float64 `json:"total_gross_qty"`
|
TotalGrossQty float64 `json:"total_gross_qty"`
|
||||||
TotalNetQty float64 `json:"total_net_qty"`
|
TotalNetQty float64 `json:"total_net_qty"`
|
||||||
TotalWasteQty float64 `json:"total_waste_qty"`
|
TotalWasteQty float64 `json:"total_waste_qty"`
|
||||||
WastePercentage float64 `json:"waste_percentage"`
|
WastePercentage float64 `json:"waste_percentage"`
|
||||||
Unit string `json:"unit"`
|
Unit string `json:"unit"`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,15 +7,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ProductIngredient struct {
|
type ProductIngredient struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id"`
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
ProductID uuid.UUID `json:"product_id"`
|
ProductID uuid.UUID `json:"product_id"`
|
||||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||||
Quantity float64 `json:"quantity"`
|
Quantity float64 `json:"quantity"`
|
||||||
WastePercentage float64 `json:"waste_percentage"`
|
WastePercentage float64 `json:"waste_percentage"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
Product *Product `json:"product,omitempty"`
|
Product *Product `json:"product,omitempty"`
|
||||||
@ -37,15 +37,15 @@ type UpdateProductIngredientRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ProductIngredientResponse struct {
|
type ProductIngredientResponse struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id"`
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
ProductID uuid.UUID `json:"product_id"`
|
ProductID uuid.UUID `json:"product_id"`
|
||||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||||
Quantity float64 `json:"quantity"`
|
Quantity float64 `json:"quantity"`
|
||||||
WastePercentage float64 `json:"waste_percentage"`
|
WastePercentage float64 `json:"waste_percentage"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
Product *Product `json:"product,omitempty"`
|
Product *Product `json:"product,omitempty"`
|
||||||
|
|||||||
51
internal/models/purchase_category.go
Normal file
51
internal/models/purchase_category.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PurchaseCategoryResponse struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
OrganizationID uuid.UUID
|
||||||
|
PresetID *uuid.UUID
|
||||||
|
ParentID *uuid.UUID
|
||||||
|
Code string
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
SortOrder int
|
||||||
|
IsSystem bool
|
||||||
|
IsActive bool
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreatePurchaseCategoryRequest struct {
|
||||||
|
OrganizationID uuid.UUID
|
||||||
|
ParentID *uuid.UUID
|
||||||
|
Code *string
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
SortOrder int
|
||||||
|
IsActive bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdatePurchaseCategoryRequest struct {
|
||||||
|
ParentID *uuid.UUID
|
||||||
|
Code *string
|
||||||
|
Name *string
|
||||||
|
Type *string
|
||||||
|
SortOrder *int
|
||||||
|
IsActive *bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListPurchaseCategoriesRequest struct {
|
||||||
|
OrganizationID uuid.UUID
|
||||||
|
ParentID *uuid.UUID
|
||||||
|
Type string
|
||||||
|
Search string
|
||||||
|
IsActive *bool
|
||||||
|
Page int
|
||||||
|
Limit int
|
||||||
|
}
|
||||||
@ -22,15 +22,16 @@ 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"`
|
||||||
Description *string `json:"description"`
|
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||||
Quantity float64 `json:"quantity"`
|
Description *string `json:"description"`
|
||||||
UnitID uuid.UUID `json:"unit_id"`
|
Quantity *float64 `json:"quantity"`
|
||||||
Amount float64 `json:"amount"`
|
UnitID *uuid.UUID `json:"unit_id"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
Amount float64 `json:"amount"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PurchaseOrderAttachment struct {
|
type PurchaseOrderAttachment struct {
|
||||||
@ -59,17 +60,19 @@ 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"`
|
||||||
Description *string `json:"description"`
|
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||||
Quantity float64 `json:"quantity"`
|
Description *string `json:"description"`
|
||||||
UnitID uuid.UUID `json:"unit_id"`
|
Quantity *float64 `json:"quantity"`
|
||||||
Amount float64 `json:"amount"`
|
UnitID *uuid.UUID `json:"unit_id"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
Amount float64 `json:"amount"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
Ingredient *IngredientResponse `json:"ingredient,omitempty"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
Unit *UnitResponse `json:"unit,omitempty"`
|
Ingredient *IngredientResponse `json:"ingredient,omitempty"`
|
||||||
|
PurchaseCategory *PurchaseCategoryResponse `json:"purchase_category,omitempty"`
|
||||||
|
Unit *UnitResponse `json:"unit,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PurchaseOrderAttachmentResponse struct {
|
type PurchaseOrderAttachmentResponse struct {
|
||||||
@ -93,11 +96,12 @@ type CreatePurchaseOrderRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreatePurchaseOrderItemRequest struct {
|
type CreatePurchaseOrderItemRequest struct {
|
||||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
IngredientID *uuid.UUID `json:"ingredient_id,omitempty"`
|
||||||
Description *string `json:"description,omitempty"`
|
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||||
Quantity float64 `json:"quantity"`
|
Description *string `json:"description,omitempty"`
|
||||||
UnitID uuid.UUID `json:"unit_id"`
|
Quantity *float64 `json:"quantity,omitempty"`
|
||||||
Amount float64 `json:"amount"`
|
UnitID *uuid.UUID `json:"unit_id,omitempty"`
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdatePurchaseOrderRequest struct {
|
type UpdatePurchaseOrderRequest struct {
|
||||||
@ -113,12 +117,13 @@ 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"`
|
||||||
Description *string `json:"description,omitempty"`
|
PurchaseCategoryID *uuid.UUID `json:"purchase_category_id,omitempty"`
|
||||||
Quantity *float64 `json:"quantity,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
UnitID *uuid.UUID `json:"unit_id,omitempty"`
|
Quantity *float64 `json:"quantity,omitempty"`
|
||||||
Amount *float64 `json:"amount,omitempty"`
|
UnitID *uuid.UUID `json:"unit_id,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"
|
||||||
)
|
)
|
||||||
@ -186,12 +185,16 @@ 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,
|
||||||
PurchaseOrders: item.PurchaseOrders,
|
RawMaterialPurchases: item.RawMaterialPurchases,
|
||||||
Quantity: item.Quantity,
|
ExpensePurchases: item.ExpensePurchases,
|
||||||
Ingredients: item.Ingredients,
|
PurchaseOrders: item.PurchaseOrders,
|
||||||
Vendors: item.Vendors,
|
RawMaterialPurchaseOrders: item.RawMaterialPurchaseOrders,
|
||||||
|
ExpenseCount: item.ExpenseCount,
|
||||||
|
Quantity: item.Quantity,
|
||||||
|
Ingredients: item.Ingredients,
|
||||||
|
Vendors: item.Vendors,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -228,7 +231,11 @@ 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,
|
||||||
@ -454,15 +461,46 @@ func (p *AnalyticsProcessorImpl) GetProfitLossAnalytics(ctx context.Context, req
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
todayPromosi := getExpenseAmountByCategory(result.TodayExpenseByCategory, "promosi")
|
type categoryAmount struct {
|
||||||
todayLainLain := getExpenseAmountByCategory(result.TodayExpenseByCategory, "lain")
|
Name string
|
||||||
todayTotalOps := todayPromosi + todayLainLain
|
TodayAmt float64
|
||||||
todayGaji := getExpenseAmountByCategory(result.TodayExpenseByCategory, "gaji")
|
MtdAmt float64
|
||||||
|
}
|
||||||
|
|
||||||
mtdPromosi := getExpenseAmountByCategory(result.MtdExpenseByCategory, "promosi")
|
categoryMap := make(map[string]*categoryAmount)
|
||||||
mtdLainLain := getExpenseAmountByCategory(result.MtdExpenseByCategory, "lain")
|
var categoryOrder []string
|
||||||
mtdTotalOps := mtdPromosi + mtdLainLain
|
|
||||||
mtdGaji := getExpenseAmountByCategory(result.MtdExpenseByCategory, "gaji")
|
for _, cat := range result.TodayExpenseByCategory {
|
||||||
|
name := cat.CategoryName
|
||||||
|
if _, exists := categoryMap[name]; !exists {
|
||||||
|
categoryMap[name] = &categoryAmount{Name: name}
|
||||||
|
categoryOrder = append(categoryOrder, name)
|
||||||
|
}
|
||||||
|
categoryMap[name].TodayAmt = cat.Amount
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cat := range result.MtdExpenseByCategory {
|
||||||
|
name := cat.CategoryName
|
||||||
|
if _, exists := categoryMap[name]; !exists {
|
||||||
|
categoryMap[name] = &categoryAmount{Name: name}
|
||||||
|
categoryOrder = append(categoryOrder, name)
|
||||||
|
}
|
||||||
|
categoryMap[name].MtdAmt = cat.Amount
|
||||||
|
}
|
||||||
|
|
||||||
|
var todayTotalOps float64
|
||||||
|
var mtdTotalOps float64
|
||||||
|
var todayGaji float64
|
||||||
|
var mtdGaji float64
|
||||||
|
for _, cat := range categoryMap {
|
||||||
|
if isSalaryExpenseCategory(cat.Name) {
|
||||||
|
todayGaji += cat.TodayAmt
|
||||||
|
mtdGaji += cat.MtdAmt
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
todayTotalOps += cat.TodayAmt
|
||||||
|
mtdTotalOps += cat.MtdAmt
|
||||||
|
}
|
||||||
|
|
||||||
todayGrossProfit := result.TodayRevenue - result.TodayCost
|
todayGrossProfit := result.TodayRevenue - result.TodayCost
|
||||||
mtdGrossProfit := result.MtdRevenue - result.MtdCost
|
mtdGrossProfit := result.MtdRevenue - result.MtdCost
|
||||||
@ -486,6 +524,33 @@ func (p *AnalyticsProcessorImpl) GetProfitLossAnalytics(ctx context.Context, req
|
|||||||
return (nominal / result.MtdRevenue) * 100
|
return (nominal / result.MtdRevenue) * 100
|
||||||
}
|
}
|
||||||
|
|
||||||
|
opsSubItems := make([]models.ProfitLossSummaryRow, 0, len(categoryOrder)+1)
|
||||||
|
opsCategoryCount := 0
|
||||||
|
for _, name := range categoryOrder {
|
||||||
|
cat := categoryMap[name]
|
||||||
|
if isSalaryExpenseCategory(cat.Name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
opsCategoryCount++
|
||||||
|
opsSubItems = append(opsSubItems, models.ProfitLossSummaryRow{
|
||||||
|
ID: fmt.Sprintf("by_%s", slugify(name)),
|
||||||
|
Label: fmt.Sprintf("%d. %s", opsCategoryCount, cat.Name),
|
||||||
|
TodayNominal: cat.TodayAmt,
|
||||||
|
TodayPct: todayPct(cat.TodayAmt),
|
||||||
|
MtdNominal: cat.MtdAmt,
|
||||||
|
MtdPct: mtdPct(cat.MtdAmt),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
opsSubItems = append(opsSubItems, models.ProfitLossSummaryRow{
|
||||||
|
ID: "total_biaya_ops",
|
||||||
|
Label: fmt.Sprintf("Total Biaya OPS (%d kategori)", opsCategoryCount),
|
||||||
|
IsBold: true,
|
||||||
|
TodayNominal: todayTotalOps,
|
||||||
|
TodayPct: todayPct(todayTotalOps),
|
||||||
|
MtdNominal: mtdTotalOps,
|
||||||
|
MtdPct: mtdPct(mtdTotalOps),
|
||||||
|
})
|
||||||
|
|
||||||
mainSummary := []models.ProfitLossSummaryRow{
|
mainSummary := []models.ProfitLossSummaryRow{
|
||||||
{
|
{
|
||||||
ID: "total_omset", Label: "TOTAL OMSET",
|
ID: "total_omset", Label: "TOTAL OMSET",
|
||||||
@ -506,23 +571,7 @@ func (p *AnalyticsProcessorImpl) GetProfitLossAnalytics(ctx context.Context, req
|
|||||||
ID: "biaya_ops", Label: "BIAYA OPS",
|
ID: "biaya_ops", Label: "BIAYA OPS",
|
||||||
TodayNominal: todayTotalOps, TodayPct: todayPct(todayTotalOps),
|
TodayNominal: todayTotalOps, TodayPct: todayPct(todayTotalOps),
|
||||||
MtdNominal: mtdTotalOps, MtdPct: mtdPct(mtdTotalOps),
|
MtdNominal: mtdTotalOps, MtdPct: mtdPct(mtdTotalOps),
|
||||||
SubItems: []models.ProfitLossSummaryRow{
|
SubItems: opsSubItems,
|
||||||
{
|
|
||||||
ID: "by_promosi", Label: "1. By Promosi",
|
|
||||||
TodayNominal: todayPromosi, TodayPct: todayPct(todayPromosi),
|
|
||||||
MtdNominal: mtdPromosi, MtdPct: mtdPct(mtdPromosi),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: "by_lain_lain", Label: "2. By Lain lain",
|
|
||||||
TodayNominal: todayLainLain, TodayPct: todayPct(todayLainLain),
|
|
||||||
MtdNominal: mtdLainLain, MtdPct: mtdPct(mtdLainLain),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: "total_biaya_ops", Label: "Total Biaya OPS (4.1+4.2)", IsBold: true,
|
|
||||||
TodayNominal: todayTotalOps, TodayPct: todayPct(todayTotalOps),
|
|
||||||
MtdNominal: mtdTotalOps, MtdPct: mtdPct(mtdTotalOps),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "laba_rugi_sblm_gaji", Label: "Laba/Rugi sblm Gaji (3-4)",
|
ID: "laba_rugi_sblm_gaji", Label: "Laba/Rugi sblm Gaji (3-4)",
|
||||||
@ -578,11 +627,27 @@ func (p *AnalyticsProcessorImpl) GetProfitLossAnalytics(ctx context.Context, req
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getExpenseAmountByCategory(categories []entities.ExpenseCategoryTotal, keyword string) float64 {
|
func isSalaryExpenseCategory(name string) bool {
|
||||||
for _, cat := range categories {
|
name = strings.ToLower(name)
|
||||||
if strings.Contains(strings.ToLower(cat.CategoryName), keyword) {
|
return strings.Contains(name, "gaji") || strings.Contains(name, "salary")
|
||||||
return cat.Amount
|
}
|
||||||
|
|
||||||
|
func slugify(s string) string {
|
||||||
|
result := make([]byte, 0, len(s))
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
c := s[i]
|
||||||
|
switch {
|
||||||
|
case c >= 'a' && c <= 'z':
|
||||||
|
result = append(result, c)
|
||||||
|
case c >= 'A' && c <= 'Z':
|
||||||
|
result = append(result, c+32)
|
||||||
|
case c >= '0' && c <= '9':
|
||||||
|
result = append(result, c)
|
||||||
|
default:
|
||||||
|
if len(result) == 0 || result[len(result)-1] != '_' {
|
||||||
|
result = append(result, '_')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 0
|
return string(result)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -61,6 +61,9 @@ func (expenseRepositoryStub) Delete(context.Context, uuid.UUID) error {
|
|||||||
func (expenseRepositoryStub) List(context.Context, uuid.UUID, map[string]interface{}, int, int) ([]*entities.Expense, int64, error) {
|
func (expenseRepositoryStub) List(context.Context, uuid.UUID, map[string]interface{}, int, int) ([]*entities.Expense, int64, error) {
|
||||||
return nil, 0, nil
|
return nil, 0, nil
|
||||||
}
|
}
|
||||||
|
func (expenseRepositoryStub) GetAnalytics(context.Context, uuid.UUID, *uuid.UUID, time.Time, time.Time, string) (*entities.ExpenseAnalytics, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
func (expenseRepositoryStub) CreateItem(context.Context, *entities.ExpenseItem) error { return nil }
|
func (expenseRepositoryStub) CreateItem(context.Context, *entities.ExpenseItem) error { return nil }
|
||||||
func (expenseRepositoryStub) DeleteItemsByExpenseID(context.Context, uuid.UUID) error { return nil }
|
func (expenseRepositoryStub) DeleteItemsByExpenseID(context.Context, uuid.UUID) error { return nil }
|
||||||
|
|
||||||
@ -72,7 +75,23 @@ func TestAnalyticsProcessorGetPurchasingAnalyticsPassesOutletName(t *testing.T)
|
|||||||
purchasingResult: &entities.PurchasingAnalytics{
|
purchasingResult: &entities.PurchasingAnalytics{
|
||||||
OutletName: &outletName,
|
OutletName: &outletName,
|
||||||
Summary: entities.PurchasingSummary{
|
Summary: entities.PurchasingSummary{
|
||||||
TotalPurchases: 125,
|
TotalPurchases: 300,
|
||||||
|
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{})
|
||||||
@ -89,7 +108,16 @@ 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(125), result.Summary.TotalPurchases)
|
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 TestAnalyticsProcessorGetProfitLossAnalyticsMapsOverviewAndReportFields(t *testing.T) {
|
func TestAnalyticsProcessorGetProfitLossAnalyticsMapsOverviewAndReportFields(t *testing.T) {
|
||||||
@ -165,3 +193,83 @@ func TestAnalyticsProcessorGetProfitLossAnalyticsMapsOverviewAndReportFields(t *
|
|||||||
require.NotEmpty(t, result.MainSummary)
|
require.NotEmpty(t, result.MainSummary)
|
||||||
require.Equal(t, "total_omset", result.MainSummary[0].ID)
|
require.Equal(t, "total_omset", result.MainSummary[0].ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAnalyticsProcessorGetProfitLossAnalyticsDynamicExpenseCategories(t *testing.T) {
|
||||||
|
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
processor := NewAnalyticsProcessorImpl(analyticsRepositoryStub{
|
||||||
|
profitLossResult: &entities.ProfitLossAnalytics{
|
||||||
|
Summary: entities.ProfitLossSummary{
|
||||||
|
TotalRevenue: 10000,
|
||||||
|
TotalCost: 4000,
|
||||||
|
},
|
||||||
|
TodayRevenue: 10000,
|
||||||
|
TodayCost: 4000,
|
||||||
|
MtdRevenue: 20000,
|
||||||
|
MtdCost: 8000,
|
||||||
|
TodayExpenseByCategory: []entities.ExpenseCategoryTotal{
|
||||||
|
{CategoryName: "Gaji", Amount: 1500},
|
||||||
|
{CategoryName: "Promosi", Amount: 300},
|
||||||
|
{CategoryName: "Sewa", Amount: 500},
|
||||||
|
},
|
||||||
|
MtdExpenseByCategory: []entities.ExpenseCategoryTotal{
|
||||||
|
{CategoryName: "Gaji", Amount: 3000},
|
||||||
|
{CategoryName: "Promosi", Amount: 600},
|
||||||
|
{CategoryName: "Sewa", Amount: 1000},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, expenseRepositoryStub{})
|
||||||
|
|
||||||
|
result, err := processor.GetProfitLossAnalytics(context.Background(), &models.ProfitLossAnalyticsRequest{
|
||||||
|
OrganizationID: uuid.New(),
|
||||||
|
DateFrom: now,
|
||||||
|
DateTo: now,
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, result)
|
||||||
|
|
||||||
|
require.Len(t, result.MainSummary, 7)
|
||||||
|
|
||||||
|
require.Equal(t, "total_omset", result.MainSummary[0].ID)
|
||||||
|
require.Equal(t, float64(10000), result.MainSummary[0].TodayNominal)
|
||||||
|
require.Equal(t, float64(20000), result.MainSummary[0].MtdNominal)
|
||||||
|
|
||||||
|
require.Equal(t, "hpp", result.MainSummary[1].ID)
|
||||||
|
require.Equal(t, float64(4000), result.MainSummary[1].TodayNominal)
|
||||||
|
require.Equal(t, float64(8000), result.MainSummary[1].MtdNominal)
|
||||||
|
|
||||||
|
require.Equal(t, "laba_kotor", result.MainSummary[2].ID)
|
||||||
|
require.Equal(t, float64(6000), result.MainSummary[2].TodayNominal)
|
||||||
|
require.Equal(t, float64(12000), result.MainSummary[2].MtdNominal)
|
||||||
|
|
||||||
|
require.Equal(t, "biaya_ops", result.MainSummary[3].ID)
|
||||||
|
require.Equal(t, float64(800), result.MainSummary[3].TodayNominal)
|
||||||
|
require.Equal(t, float64(1600), result.MainSummary[3].MtdNominal)
|
||||||
|
require.Len(t, result.MainSummary[3].SubItems, 3) // 2 operational categories + 1 total
|
||||||
|
|
||||||
|
require.Equal(t, "by_promosi", result.MainSummary[3].SubItems[0].ID)
|
||||||
|
require.Equal(t, float64(300), result.MainSummary[3].SubItems[0].TodayNominal)
|
||||||
|
require.Equal(t, float64(600), result.MainSummary[3].SubItems[0].MtdNominal)
|
||||||
|
|
||||||
|
require.Equal(t, "by_sewa", result.MainSummary[3].SubItems[1].ID)
|
||||||
|
require.Equal(t, float64(500), result.MainSummary[3].SubItems[1].TodayNominal)
|
||||||
|
require.Equal(t, float64(1000), result.MainSummary[3].SubItems[1].MtdNominal)
|
||||||
|
|
||||||
|
require.Equal(t, "total_biaya_ops", result.MainSummary[3].SubItems[2].ID)
|
||||||
|
require.True(t, result.MainSummary[3].SubItems[2].IsBold)
|
||||||
|
require.Equal(t, float64(800), result.MainSummary[3].SubItems[2].TodayNominal)
|
||||||
|
require.Equal(t, float64(1600), result.MainSummary[3].SubItems[2].MtdNominal)
|
||||||
|
|
||||||
|
require.Equal(t, "laba_rugi_sblm_gaji", result.MainSummary[4].ID)
|
||||||
|
require.Equal(t, float64(5200), result.MainSummary[4].TodayNominal)
|
||||||
|
require.Equal(t, float64(10400), result.MainSummary[4].MtdNominal)
|
||||||
|
|
||||||
|
require.Equal(t, "biaya_gaji", result.MainSummary[5].ID)
|
||||||
|
require.Equal(t, float64(1500), result.MainSummary[5].TodayNominal)
|
||||||
|
require.Equal(t, float64(3000), result.MainSummary[5].MtdNominal)
|
||||||
|
|
||||||
|
require.Equal(t, "laba_rugi", result.MainSummary[6].ID)
|
||||||
|
require.Equal(t, float64(3700), result.MainSummary[6].TodayNominal)
|
||||||
|
require.Equal(t, float64(7400), result.MainSummary[6].MtdNominal)
|
||||||
|
require.True(t, result.MainSummary[6].IsBold)
|
||||||
|
}
|
||||||
|
|||||||
@ -19,15 +19,18 @@ type ExpenseProcessor interface {
|
|||||||
DeleteExpense(ctx context.Context, id, organizationID uuid.UUID) error
|
DeleteExpense(ctx context.Context, id, organizationID uuid.UUID) error
|
||||||
GetExpenseByID(ctx context.Context, id, organizationID uuid.UUID) (*models.ExpenseResponse, error)
|
GetExpenseByID(ctx context.Context, id, organizationID uuid.UUID) (*models.ExpenseResponse, error)
|
||||||
ListExpenses(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, page, limit int) ([]*models.ExpenseResponse, int, error)
|
ListExpenses(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, page, limit int) ([]*models.ExpenseResponse, int, error)
|
||||||
|
GetExpenseAnalytics(ctx context.Context, req *models.ExpenseAnalyticsRequest) (*models.ExpenseAnalyticsResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseProcessorImpl struct {
|
type ExpenseProcessorImpl struct {
|
||||||
expenseRepo ExpenseRepository
|
expenseRepo ExpenseRepository
|
||||||
|
purchaseCategoryRepo PurchaseCategoryRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewExpenseProcessorImpl(expenseRepo ExpenseRepository) *ExpenseProcessorImpl {
|
func NewExpenseProcessorImpl(expenseRepo ExpenseRepository, purchaseCategoryRepo PurchaseCategoryRepository) *ExpenseProcessorImpl {
|
||||||
return &ExpenseProcessorImpl{
|
return &ExpenseProcessorImpl{
|
||||||
expenseRepo: expenseRepo,
|
expenseRepo: expenseRepo,
|
||||||
|
purchaseCategoryRepo: purchaseCategoryRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,6 +50,30 @@ 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,
|
||||||
@ -64,21 +91,10 @@ 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 _, itemReq := range req.Items {
|
for i := range items {
|
||||||
chartOfAccountID, err := uuid.Parse(itemReq.ChartOfAccountID)
|
items[i].ExpenseID = expenseEntity.ID
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid chart_of_account_id for item: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
itemEntity := &entities.ExpenseItem{
|
err = p.expenseRepo.CreateItem(ctx, &items[i])
|
||||||
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)
|
||||||
}
|
}
|
||||||
@ -134,13 +150,10 @@ 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 {
|
||||||
err = p.expenseRepo.DeleteItemsByExpenseID(ctx, expenseEntity.ID)
|
items = make([]entities.ExpenseItem, len(req.Items))
|
||||||
if err != nil {
|
for i, itemReq := range req.Items {
|
||||||
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)
|
||||||
@ -149,6 +162,17 @@ 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
|
||||||
@ -158,15 +182,23 @@ func (p *ExpenseProcessorImpl) UpdateExpense(ctx context.Context, id, organizati
|
|||||||
item = *itemReq.Item
|
item = *itemReq.Item
|
||||||
}
|
}
|
||||||
|
|
||||||
itemEntity := &entities.ExpenseItem{
|
items[i] = entities.ExpenseItem{
|
||||||
ExpenseID: expenseEntity.ID,
|
ExpenseID: expenseEntity.ID,
|
||||||
ChartOfAccountID: chartOfAccountID,
|
ChartOfAccountID: chartOfAccountID,
|
||||||
Item: item,
|
PurchaseCategoryID: purchaseCategoryID,
|
||||||
Description: itemReq.Description,
|
Item: item,
|
||||||
Amount: amount,
|
Description: itemReq.Description,
|
||||||
|
Amount: amount,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err = p.expenseRepo.CreateItem(ctx, itemEntity)
|
err = p.expenseRepo.DeleteItemsByExpenseID(ctx, expenseEntity.ID)
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
@ -221,3 +253,100 @@ func (p *ExpenseProcessorImpl) ListExpenses(ctx context.Context, organizationID
|
|||||||
|
|
||||||
return expenseResponses, totalPages, nil
|
return expenseResponses, totalPages, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *ExpenseProcessorImpl) GetExpenseAnalytics(ctx context.Context, req *models.ExpenseAnalyticsRequest) (*models.ExpenseAnalyticsResponse, error) {
|
||||||
|
if req.DateFrom.After(req.DateTo) {
|
||||||
|
return nil, fmt.Errorf("date_from cannot be after date_to")
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.GroupBy == "" {
|
||||||
|
req.GroupBy = "day"
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := p.expenseRepo.GetAnalytics(ctx, req.OrganizationID, req.OutletID, req.DateFrom, req.DateTo, req.GroupBy)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get expense analytics: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make([]models.ExpenseAnalyticsData, len(result.Data))
|
||||||
|
for i, item := range result.Data {
|
||||||
|
data[i] = models.ExpenseAnalyticsData{
|
||||||
|
Date: item.Date,
|
||||||
|
Expenses: item.Expenses,
|
||||||
|
ExpenseCount: item.ExpenseCount,
|
||||||
|
Tax: item.Tax,
|
||||||
|
Items: item.Items,
|
||||||
|
Categories: item.Categories,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryData := make([]models.ExpenseAnalyticsCategoryData, len(result.CategoryData))
|
||||||
|
for i, item := range result.CategoryData {
|
||||||
|
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,
|
||||||
|
ChartOfAccountName: item.ChartOfAccountName,
|
||||||
|
TotalAmount: item.TotalAmount,
|
||||||
|
ExpenseCount: item.ExpenseCount,
|
||||||
|
ItemCount: item.ItemCount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
itemData := make([]models.ExpenseAnalyticsItemData, len(result.ItemData))
|
||||||
|
for i, item := range result.ItemData {
|
||||||
|
itemData[i] = models.ExpenseAnalyticsItemData{
|
||||||
|
Item: item.Item,
|
||||||
|
TotalAmount: item.TotalAmount,
|
||||||
|
ExpenseCount: item.ExpenseCount,
|
||||||
|
ItemCount: item.ItemCount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.ExpenseAnalyticsResponse{
|
||||||
|
OrganizationID: req.OrganizationID,
|
||||||
|
OutletID: req.OutletID,
|
||||||
|
DateFrom: req.DateFrom,
|
||||||
|
DateTo: req.DateTo,
|
||||||
|
GroupBy: req.GroupBy,
|
||||||
|
Summary: models.ExpenseAnalyticsSummary{
|
||||||
|
TotalExpenses: result.Summary.TotalExpenses,
|
||||||
|
TotalExpenseCount: result.Summary.TotalExpenseCount,
|
||||||
|
TotalTax: result.Summary.TotalTax,
|
||||||
|
AverageExpenseValue: result.Summary.AverageExpenseValue,
|
||||||
|
TotalCategories: result.Summary.TotalCategories,
|
||||||
|
TotalItems: result.Summary.TotalItems,
|
||||||
|
},
|
||||||
|
Data: data,
|
||||||
|
CategoryData: categoryData,
|
||||||
|
ChartOfAccountData: chartOfAccountData,
|
||||||
|
ItemData: itemData,
|
||||||
|
}, 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
|
||||||
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package processor
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"apskel-pos-be/internal/entities"
|
"apskel-pos-be/internal/entities"
|
||||||
"apskel-pos-be/internal/models"
|
"apskel-pos-be/internal/models"
|
||||||
@ -14,6 +15,46 @@ import (
|
|||||||
type expenseRepositoryCaptureStub struct {
|
type expenseRepositoryCaptureStub struct {
|
||||||
createdExpense *entities.Expense
|
createdExpense *entities.Expense
|
||||||
createdItems []*entities.ExpenseItem
|
createdItems []*entities.ExpenseItem
|
||||||
|
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 {
|
||||||
@ -44,6 +85,9 @@ func (*expenseRepositoryCaptureStub) Delete(context.Context, uuid.UUID) error
|
|||||||
func (*expenseRepositoryCaptureStub) List(context.Context, uuid.UUID, map[string]interface{}, int, int) ([]*entities.Expense, int64, error) {
|
func (*expenseRepositoryCaptureStub) List(context.Context, uuid.UUID, map[string]interface{}, int, int) ([]*entities.Expense, int64, error) {
|
||||||
return nil, 0, nil
|
return nil, 0, nil
|
||||||
}
|
}
|
||||||
|
func (s *expenseRepositoryCaptureStub) GetAnalytics(context.Context, uuid.UUID, *uuid.UUID, time.Time, time.Time, string) (*entities.ExpenseAnalytics, error) {
|
||||||
|
return s.analytics, nil
|
||||||
|
}
|
||||||
func (s *expenseRepositoryCaptureStub) CreateItem(_ context.Context, item *entities.ExpenseItem) error {
|
func (s *expenseRepositoryCaptureStub) CreateItem(_ context.Context, item *entities.ExpenseItem) error {
|
||||||
if item.ID == uuid.Nil {
|
if item.ID == uuid.Nil {
|
||||||
item.ID = uuid.New()
|
item.ID = uuid.New()
|
||||||
@ -57,7 +101,8 @@ func (*expenseRepositoryCaptureStub) DeleteItemsByExpenseID(context.Context, uui
|
|||||||
|
|
||||||
func TestExpenseProcessorCreatePersistsItemName(t *testing.T) {
|
func TestExpenseProcessorCreatePersistsItemName(t *testing.T) {
|
||||||
repo := &expenseRepositoryCaptureStub{}
|
repo := &expenseRepositoryCaptureStub{}
|
||||||
p := NewExpenseProcessorImpl(repo)
|
purchaseCategoryID := uuid.New()
|
||||||
|
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{
|
||||||
@ -68,9 +113,10 @@ func TestExpenseProcessorCreatePersistsItemName(t *testing.T) {
|
|||||||
Total: 10000,
|
Total: 10000,
|
||||||
Items: []models.CreateExpenseItemRequest{
|
Items: []models.CreateExpenseItemRequest{
|
||||||
{
|
{
|
||||||
ChartOfAccountID: chartOfAccountID.String(),
|
ChartOfAccountID: chartOfAccountID.String(),
|
||||||
Item: "Cleaning supplies",
|
PurchaseCategoryID: purchaseCategoryID.String(),
|
||||||
Amount: 10000,
|
Item: "Cleaning supplies",
|
||||||
|
Amount: 10000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -79,13 +125,15 @@ 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{}
|
||||||
p := NewExpenseProcessorImpl(repo)
|
purchaseCategoryID := uuid.New()
|
||||||
|
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",
|
||||||
@ -95,9 +143,10 @@ func TestExpenseProcessorCreateDefaultsStatusToDraft(t *testing.T) {
|
|||||||
Total: 10000,
|
Total: 10000,
|
||||||
Items: []models.CreateExpenseItemRequest{
|
Items: []models.CreateExpenseItemRequest{
|
||||||
{
|
{
|
||||||
ChartOfAccountID: uuid.NewString(),
|
ChartOfAccountID: uuid.NewString(),
|
||||||
Item: "Cleaning supplies",
|
PurchaseCategoryID: purchaseCategoryID.String(),
|
||||||
Amount: 10000,
|
Item: "Cleaning supplies",
|
||||||
|
Amount: 10000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -110,7 +159,8 @@ func TestExpenseProcessorCreateDefaultsStatusToDraft(t *testing.T) {
|
|||||||
|
|
||||||
func TestExpenseProcessorCreatePersistsProvidedStatus(t *testing.T) {
|
func TestExpenseProcessorCreatePersistsProvidedStatus(t *testing.T) {
|
||||||
repo := &expenseRepositoryCaptureStub{}
|
repo := &expenseRepositoryCaptureStub{}
|
||||||
p := NewExpenseProcessorImpl(repo)
|
purchaseCategoryID := uuid.New()
|
||||||
|
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{
|
||||||
@ -122,9 +172,10 @@ func TestExpenseProcessorCreatePersistsProvidedStatus(t *testing.T) {
|
|||||||
Total: 10000,
|
Total: 10000,
|
||||||
Items: []models.CreateExpenseItemRequest{
|
Items: []models.CreateExpenseItemRequest{
|
||||||
{
|
{
|
||||||
ChartOfAccountID: uuid.NewString(),
|
ChartOfAccountID: uuid.NewString(),
|
||||||
Item: "Cleaning supplies",
|
PurchaseCategoryID: purchaseCategoryID.String(),
|
||||||
Amount: 10000,
|
Item: "Cleaning supplies",
|
||||||
|
Amount: 10000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -134,3 +185,107 @@ func TestExpenseProcessorCreatePersistsProvidedStatus(t *testing.T) {
|
|||||||
require.Equal(t, "approved", repo.createdExpense.Status)
|
require.Equal(t, "approved", repo.createdExpense.Status)
|
||||||
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) {
|
||||||
|
coaID := uuid.New()
|
||||||
|
purchaseCategoryID := uuid.New()
|
||||||
|
outletID := uuid.New()
|
||||||
|
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
repo := &expenseRepositoryCaptureStub{
|
||||||
|
analytics: &entities.ExpenseAnalytics{
|
||||||
|
Summary: entities.ExpenseAnalyticsSummary{
|
||||||
|
TotalExpenses: 100000,
|
||||||
|
TotalExpenseCount: 2,
|
||||||
|
TotalTax: 10000,
|
||||||
|
AverageExpenseValue: 50000,
|
||||||
|
TotalCategories: 1,
|
||||||
|
TotalItems: 2,
|
||||||
|
},
|
||||||
|
Data: []entities.ExpenseAnalyticsData{
|
||||||
|
{
|
||||||
|
Date: now,
|
||||||
|
Expenses: 100000,
|
||||||
|
ExpenseCount: 2,
|
||||||
|
Tax: 10000,
|
||||||
|
Items: 2,
|
||||||
|
Categories: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CategoryData: []entities.ExpenseAnalyticsCategoryData{
|
||||||
|
{
|
||||||
|
PurchaseCategoryID: purchaseCategoryID,
|
||||||
|
PurchaseCategoryName: "Operational Supplies",
|
||||||
|
PurchaseCategoryType: "expense",
|
||||||
|
TotalAmount: 100000,
|
||||||
|
ExpenseCount: 2,
|
||||||
|
ItemCount: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ChartOfAccountData: []entities.ExpenseAnalyticsChartOfAccountData{
|
||||||
|
{
|
||||||
|
ChartOfAccountID: coaID,
|
||||||
|
ChartOfAccountName: "Operational",
|
||||||
|
TotalAmount: 100000,
|
||||||
|
ExpenseCount: 2,
|
||||||
|
ItemCount: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ItemData: []entities.ExpenseAnalyticsItemData{
|
||||||
|
{
|
||||||
|
Item: "Cleaning supplies",
|
||||||
|
TotalAmount: 100000,
|
||||||
|
ExpenseCount: 2,
|
||||||
|
ItemCount: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
p := NewExpenseProcessorImpl(repo, newExpensePurchaseCategoryRepo(purchaseCategoryID, entities.PurchaseCategoryTypeExpense))
|
||||||
|
|
||||||
|
resp, err := p.GetExpenseAnalytics(context.Background(), &models.ExpenseAnalyticsRequest{
|
||||||
|
OrganizationID: uuid.New(),
|
||||||
|
OutletID: &outletID,
|
||||||
|
DateFrom: now,
|
||||||
|
DateTo: now,
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, resp)
|
||||||
|
require.Equal(t, "day", resp.GroupBy)
|
||||||
|
require.Equal(t, &outletID, resp.OutletID)
|
||||||
|
require.Equal(t, float64(100000), resp.Summary.TotalExpenses)
|
||||||
|
require.Len(t, resp.Data, 1)
|
||||||
|
require.Equal(t, int64(2), resp.Data[0].ExpenseCount)
|
||||||
|
require.Len(t, resp.CategoryData, 1)
|
||||||
|
require.Equal(t, purchaseCategoryID, resp.CategoryData[0].PurchaseCategoryID)
|
||||||
|
require.Len(t, resp.ChartOfAccountData, 1)
|
||||||
|
require.Equal(t, coaID, resp.ChartOfAccountData[0].ChartOfAccountID)
|
||||||
|
require.Len(t, resp.ItemData, 1)
|
||||||
|
require.Equal(t, "Cleaning supplies", resp.ItemData[0].Item)
|
||||||
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package processor
|
|||||||
import (
|
import (
|
||||||
"apskel-pos-be/internal/entities"
|
"apskel-pos-be/internal/entities"
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
@ -14,6 +15,7 @@ type ExpenseRepository interface {
|
|||||||
Update(ctx context.Context, expense *entities.Expense) error
|
Update(ctx context.Context, expense *entities.Expense) error
|
||||||
Delete(ctx context.Context, id uuid.UUID) error
|
Delete(ctx context.Context, id uuid.UUID) error
|
||||||
List(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, limit, offset int) ([]*entities.Expense, int64, error)
|
List(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, limit, offset int) ([]*entities.Expense, int64, error)
|
||||||
|
GetAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, groupBy string) (*entities.ExpenseAnalytics, error)
|
||||||
CreateItem(ctx context.Context, item *entities.ExpenseItem) error
|
CreateItem(ctx context.Context, item *entities.ExpenseItem) error
|
||||||
DeleteItemsByExpenseID(ctx context.Context, expenseID uuid.UUID) error
|
DeleteItemsByExpenseID(ctx context.Context, expenseID uuid.UUID) error
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) 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, purchaseOrderItemID *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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
210
internal/processor/purchase_category_processor.go
Normal file
210
internal/processor/purchase_category_processor.go
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
package processor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
"apskel-pos-be/internal/mappers"
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PurchaseCategoryProcessor interface {
|
||||||
|
CreatePurchaseCategory(ctx context.Context, req *models.CreatePurchaseCategoryRequest) (*models.PurchaseCategoryResponse, error)
|
||||||
|
UpdatePurchaseCategory(ctx context.Context, id, organizationID uuid.UUID, req *models.UpdatePurchaseCategoryRequest) (*models.PurchaseCategoryResponse, error)
|
||||||
|
DeletePurchaseCategory(ctx context.Context, id, organizationID uuid.UUID) error
|
||||||
|
GetPurchaseCategoryByID(ctx context.Context, id, organizationID uuid.UUID) (*models.PurchaseCategoryResponse, error)
|
||||||
|
ListPurchaseCategories(ctx context.Context, req *models.ListPurchaseCategoriesRequest) ([]models.PurchaseCategoryResponse, int, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type PurchaseCategoryRepository interface {
|
||||||
|
Create(ctx context.Context, category *entities.PurchaseCategory) error
|
||||||
|
GetByIDAndOrganizationID(ctx context.Context, id, organizationID uuid.UUID) (*entities.PurchaseCategory, error)
|
||||||
|
Update(ctx context.Context, category *entities.PurchaseCategory) error
|
||||||
|
SoftDelete(ctx context.Context, id, organizationID uuid.UUID) error
|
||||||
|
List(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, limit, offset int) ([]*entities.PurchaseCategory, int64, error)
|
||||||
|
ExistsByCode(ctx context.Context, organizationID uuid.UUID, code string, excludeID *uuid.UUID) (bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type PurchaseCategoryProcessorImpl struct {
|
||||||
|
purchaseCategoryRepo PurchaseCategoryRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPurchaseCategoryProcessorImpl(purchaseCategoryRepo PurchaseCategoryRepository) *PurchaseCategoryProcessorImpl {
|
||||||
|
return &PurchaseCategoryProcessorImpl{purchaseCategoryRepo: purchaseCategoryRepo}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PurchaseCategoryProcessorImpl) CreatePurchaseCategory(ctx context.Context, req *models.CreatePurchaseCategoryRequest) (*models.PurchaseCategoryResponse, error) {
|
||||||
|
code := ""
|
||||||
|
if req.Code != nil && strings.TrimSpace(*req.Code) != "" {
|
||||||
|
code = normalizePurchaseCategoryCode(*req.Code)
|
||||||
|
} else {
|
||||||
|
code = normalizePurchaseCategoryCode(req.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if code == "" {
|
||||||
|
return nil, fmt.Errorf("purchase category code cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, err := p.purchaseCategoryRepo.ExistsByCode(ctx, req.OrganizationID, code, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to check purchase category code uniqueness: %w", err)
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
return nil, fmt.Errorf("purchase category with code '%s' already exists", code)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ParentID != nil {
|
||||||
|
parent, err := p.purchaseCategoryRepo.GetByIDAndOrganizationID(ctx, *req.ParentID, req.OrganizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parent purchase category not found: %w", err)
|
||||||
|
}
|
||||||
|
if string(parent.Type) != req.Type {
|
||||||
|
return nil, fmt.Errorf("parent purchase category type must match child type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
category := mappers.CreatePurchaseCategoryRequestToEntity(req)
|
||||||
|
category.Code = code
|
||||||
|
|
||||||
|
if err := p.purchaseCategoryRepo.Create(ctx, category); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create purchase category: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappers.PurchaseCategoryEntityToResponse(category), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PurchaseCategoryProcessorImpl) UpdatePurchaseCategory(ctx context.Context, id, organizationID uuid.UUID, req *models.UpdatePurchaseCategoryRequest) (*models.PurchaseCategoryResponse, error) {
|
||||||
|
category, err := p.purchaseCategoryRepo.GetByIDAndOrganizationID(ctx, id, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("purchase category not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newType := string(category.Type)
|
||||||
|
if req.Type != nil {
|
||||||
|
newType = *req.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ParentID != nil {
|
||||||
|
if *req.ParentID == id {
|
||||||
|
return nil, fmt.Errorf("purchase category cannot be its own parent")
|
||||||
|
}
|
||||||
|
|
||||||
|
parent, err := p.purchaseCategoryRepo.GetByIDAndOrganizationID(ctx, *req.ParentID, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parent purchase category not found: %w", err)
|
||||||
|
}
|
||||||
|
if string(parent.Type) != newType {
|
||||||
|
return nil, fmt.Errorf("parent purchase category type must match child type")
|
||||||
|
}
|
||||||
|
category.ParentID = req.ParentID
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Code != nil {
|
||||||
|
code := normalizePurchaseCategoryCode(*req.Code)
|
||||||
|
if code == "" {
|
||||||
|
return nil, fmt.Errorf("purchase category code cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if code != category.Code {
|
||||||
|
exists, err := p.purchaseCategoryRepo.ExistsByCode(ctx, organizationID, code, &id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to check purchase category code uniqueness: %w", err)
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
return nil, fmt.Errorf("purchase category with code '%s' already exists", code)
|
||||||
|
}
|
||||||
|
category.Code = code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Name != nil {
|
||||||
|
category.Name = strings.TrimSpace(*req.Name)
|
||||||
|
}
|
||||||
|
if req.Type != nil {
|
||||||
|
category.Type = entities.PurchaseCategoryType(*req.Type)
|
||||||
|
}
|
||||||
|
if req.SortOrder != nil {
|
||||||
|
category.SortOrder = *req.SortOrder
|
||||||
|
}
|
||||||
|
if req.IsActive != nil {
|
||||||
|
category.IsActive = *req.IsActive
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.purchaseCategoryRepo.Update(ctx, category); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to update purchase category: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappers.PurchaseCategoryEntityToResponse(category), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PurchaseCategoryProcessorImpl) DeletePurchaseCategory(ctx context.Context, id, organizationID uuid.UUID) error {
|
||||||
|
_, err := p.purchaseCategoryRepo.GetByIDAndOrganizationID(ctx, id, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("purchase category not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.purchaseCategoryRepo.SoftDelete(ctx, id, organizationID); err != nil {
|
||||||
|
return fmt.Errorf("failed to delete purchase category: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PurchaseCategoryProcessorImpl) GetPurchaseCategoryByID(ctx context.Context, id, organizationID uuid.UUID) (*models.PurchaseCategoryResponse, error) {
|
||||||
|
category, err := p.purchaseCategoryRepo.GetByIDAndOrganizationID(ctx, id, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("purchase category not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappers.PurchaseCategoryEntityToResponse(category), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PurchaseCategoryProcessorImpl) ListPurchaseCategories(ctx context.Context, req *models.ListPurchaseCategoriesRequest) ([]models.PurchaseCategoryResponse, int, error) {
|
||||||
|
filters := make(map[string]interface{})
|
||||||
|
if req.ParentID != nil {
|
||||||
|
filters["parent_id"] = *req.ParentID
|
||||||
|
}
|
||||||
|
if req.Type != "" {
|
||||||
|
filters["type"] = req.Type
|
||||||
|
}
|
||||||
|
if req.Search != "" {
|
||||||
|
filters["search"] = req.Search
|
||||||
|
}
|
||||||
|
if req.IsActive != nil {
|
||||||
|
filters["is_active"] = *req.IsActive
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := (req.Page - 1) * req.Limit
|
||||||
|
categories, total, err := p.purchaseCategoryRepo.List(ctx, req.OrganizationID, filters, req.Limit, offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, fmt.Errorf("failed to list purchase categories: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappers.PurchaseCategoryEntitiesToResponses(categories), int(total), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizePurchaseCategoryCode(value string) string {
|
||||||
|
value = strings.TrimSpace(strings.ToLower(value))
|
||||||
|
var builder strings.Builder
|
||||||
|
lastUnderscore := false
|
||||||
|
|
||||||
|
for _, r := range value {
|
||||||
|
if unicode.IsLetter(r) || unicode.IsDigit(r) {
|
||||||
|
builder.WriteRune(r)
|
||||||
|
lastUnderscore = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !lastUnderscore {
|
||||||
|
builder.WriteRune('_')
|
||||||
|
lastUnderscore = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Trim(builder.String(), "_")
|
||||||
|
}
|
||||||
@ -25,6 +25,7 @@ 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
|
||||||
@ -35,6 +36,7 @@ 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,
|
||||||
@ -44,6 +46,7 @@ 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,
|
||||||
@ -64,16 +67,40 @@ 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 ingredients and units exist
|
// Validate categories and inventory fields per item type.
|
||||||
for i, item := range req.Items {
|
for i, item := range req.Items {
|
||||||
_, err := p.ingredientRepo.GetByID(ctx, item.IngredientID, organizationID)
|
category, err := p.validatePurchaseCategory(ctx, item.PurchaseCategoryID, organizationID, i)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ingredient not found for item %d: %w", i, err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = p.unitRepo.GetByID(ctx, item.UnitID, organizationID)
|
switch category.Type {
|
||||||
if err != nil {
|
case entities.PurchaseCategoryTypeRawMaterial:
|
||||||
return nil, fmt.Errorf("unit not found for item %d: %w", i, err)
|
if item.IngredientID == nil {
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,12 +136,13 @@ 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,
|
||||||
Description: itemReq.Description,
|
PurchaseCategoryID: itemReq.PurchaseCategoryID,
|
||||||
Quantity: itemReq.Quantity,
|
Description: itemReq.Description,
|
||||||
UnitID: itemReq.UnitID,
|
Quantity: itemReq.Quantity,
|
||||||
Amount: itemReq.Amount,
|
UnitID: itemReq.UnitID,
|
||||||
|
Amount: itemReq.Amount,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = p.purchaseOrderRepo.CreateItem(ctx, itemEntity)
|
err = p.purchaseOrderRepo.CreateItem(ctx, itemEntity)
|
||||||
@ -189,68 +217,80 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrder(ctx context.Context, id
|
|||||||
|
|
||||||
// Update items if provided
|
// Update items if provided
|
||||||
if req.Items != nil {
|
if req.Items != nil {
|
||||||
// Delete existing items
|
totalAmount := 0.0
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new items
|
for _, itemEntity := range 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
|
||||||
@ -379,19 +419,27 @@ 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
|
||||||
}
|
}
|
||||||
@ -409,7 +457,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,
|
||||||
@ -419,9 +467,10 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -440,3 +489,20 @@ 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
|
||||||
|
}
|
||||||
|
|||||||
@ -22,8 +22,6 @@ const (
|
|||||||
MetadataKeyLastSplitQuantities = "last_split_quantities"
|
MetadataKeyLastSplitQuantities = "last_split_quantities"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
type SplitBillValidation struct {
|
type SplitBillValidation struct {
|
||||||
OrderItems map[uuid.UUID]*entities.OrderItem
|
OrderItems map[uuid.UUID]*entities.OrderItem
|
||||||
PaidQuantities map[uuid.UUID]int
|
PaidQuantities map[uuid.UUID]int
|
||||||
|
|||||||
@ -145,68 +145,179 @@ func (r *AnalyticsRepositoryImpl) GetPurchasingAnalytics(ctx context.Context, or
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
summaryQuery := r.db.WithContext(ctx).
|
rawMaterialOutletFilter := ""
|
||||||
Table("inventory_movements im").
|
expenseOutletFilter := ""
|
||||||
Select(`
|
rawMaterialSummaryArgs := []interface{}{
|
||||||
COALESCE(SUM(im.total_cost), 0) as total_purchases,
|
organizationID,
|
||||||
COUNT(DISTINCT im.reference_id) as total_purchase_orders,
|
entities.InventoryMovementTypePurchase,
|
||||||
COALESCE(SUM(im.quantity), 0) as total_quantity,
|
"INGREDIENT",
|
||||||
|
entities.InventoryMovementReferenceTypePurchaseOrder,
|
||||||
|
dateFrom,
|
||||||
|
dateTo,
|
||||||
|
}
|
||||||
|
expenseSummaryArgs := []interface{}{
|
||||||
|
organizationID,
|
||||||
|
entities.PurchaseCategoryTypeExpense,
|
||||||
|
"approved",
|
||||||
|
dateFrom,
|
||||||
|
dateTo,
|
||||||
|
}
|
||||||
|
if outletID != nil {
|
||||||
|
rawMaterialOutletFilter = "AND im.outlet_id = ?"
|
||||||
|
expenseOutletFilter = "AND e.outlet_id = ?"
|
||||||
|
rawMaterialSummaryArgs = append(rawMaterialSummaryArgs, *outletID)
|
||||||
|
expenseSummaryArgs = append(expenseSummaryArgs, *outletID)
|
||||||
|
}
|
||||||
|
summaryArgs := append(rawMaterialSummaryArgs, expenseSummaryArgs...)
|
||||||
|
|
||||||
|
summaryQuery := `
|
||||||
|
WITH raw_material AS (
|
||||||
|
SELECT
|
||||||
|
COALESCE(SUM(im.total_cost), 0) as raw_material_purchases,
|
||||||
|
COUNT(DISTINCT im.reference_id) as raw_material_purchase_orders,
|
||||||
|
COALESCE(SUM(im.quantity), 0) as total_quantity,
|
||||||
|
COUNT(DISTINCT im.item_id) as total_ingredients,
|
||||||
|
COUNT(DISTINCT po.vendor_id) as total_vendors
|
||||||
|
FROM inventory_movements im
|
||||||
|
LEFT JOIN purchase_orders po ON im.reference_id = po.id
|
||||||
|
WHERE im.organization_id = ?
|
||||||
|
AND im.movement_type = ?
|
||||||
|
AND im.item_type = ?
|
||||||
|
AND im.reference_type = ?
|
||||||
|
AND im.created_at >= ? AND im.created_at <= ?
|
||||||
|
` + rawMaterialOutletFilter + `
|
||||||
|
),
|
||||||
|
expense AS (
|
||||||
|
SELECT
|
||||||
|
COALESCE(SUM(ei.amount), 0) as expense_purchases,
|
||||||
|
COUNT(DISTINCT e.id) as expense_count
|
||||||
|
FROM expense_items ei
|
||||||
|
JOIN expenses e ON ei.expense_id = e.id
|
||||||
|
JOIN purchase_categories pc ON ei.purchase_category_id = pc.id
|
||||||
|
WHERE e.organization_id = ?
|
||||||
|
AND pc.type = ?
|
||||||
|
AND e.status = ?
|
||||||
|
AND e.transaction_date >= ? AND e.transaction_date <= ?
|
||||||
|
` + expenseOutletFilter + `
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
rm.raw_material_purchases + ex.expense_purchases as total_purchases,
|
||||||
|
rm.raw_material_purchases,
|
||||||
|
ex.expense_purchases,
|
||||||
|
rm.raw_material_purchase_orders + ex.expense_count as total_purchase_orders,
|
||||||
|
rm.raw_material_purchase_orders,
|
||||||
|
ex.expense_count,
|
||||||
|
rm.total_quantity,
|
||||||
CASE
|
CASE
|
||||||
WHEN COUNT(DISTINCT im.reference_id) > 0
|
WHEN rm.raw_material_purchase_orders + ex.expense_count > 0
|
||||||
THEN COALESCE(SUM(im.total_cost), 0) / COUNT(DISTINCT im.reference_id)
|
THEN (rm.raw_material_purchases + ex.expense_purchases) / (rm.raw_material_purchase_orders + ex.expense_count)
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END as average_purchase_order_value,
|
END as average_purchase_order_value,
|
||||||
COUNT(DISTINCT im.item_id) as total_ingredients,
|
rm.total_ingredients,
|
||||||
COUNT(DISTINCT po.vendor_id) as total_vendors
|
rm.total_vendors
|
||||||
`).
|
FROM raw_material rm
|
||||||
Joins("LEFT JOIN purchase_orders po ON im.reference_id = po.id").
|
CROSS JOIN expense ex
|
||||||
Where("im.organization_id = ?", organizationID).
|
`
|
||||||
Where("im.movement_type = ?", entities.InventoryMovementTypePurchase).
|
|
||||||
Where("im.item_type = ?", "INGREDIENT").
|
|
||||||
Where("im.reference_type = ?", entities.InventoryMovementReferenceTypePurchaseOrder).
|
|
||||||
Where("im.created_at >= ? AND im.created_at <= ?", dateFrom, dateTo)
|
|
||||||
|
|
||||||
summaryQuery = r.resolveOutletID(summaryQuery, outletID, "im.outlet_id")
|
if err := r.db.WithContext(ctx).Raw(summaryQuery, summaryArgs...).Scan(&summary).Error; err != nil {
|
||||||
|
|
||||||
if err := summaryQuery.Scan(&summary).Error; err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var dateFormat string
|
var dateFormat string
|
||||||
switch groupBy {
|
switch groupBy {
|
||||||
case "hour":
|
case "hour":
|
||||||
dateFormat = "DATE_TRUNC('hour', im.created_at)"
|
dateFormat = "DATE_TRUNC('hour', im.created_at)::timestamp"
|
||||||
case "week":
|
case "week":
|
||||||
dateFormat = "DATE_TRUNC('week', im.created_at)"
|
dateFormat = "DATE_TRUNC('week', im.created_at)::timestamp"
|
||||||
case "month":
|
case "month":
|
||||||
dateFormat = "DATE_TRUNC('month', im.created_at)"
|
dateFormat = "DATE_TRUNC('month', im.created_at)::timestamp"
|
||||||
default:
|
default:
|
||||||
dateFormat = "DATE_TRUNC('day', im.created_at)"
|
dateFormat = "DATE_TRUNC('day', im.created_at)::timestamp"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expenseDateFormat := "DATE_TRUNC('day', e.transaction_date)::timestamp"
|
||||||
|
switch groupBy {
|
||||||
|
case "hour":
|
||||||
|
expenseDateFormat = "DATE_TRUNC('hour', e.transaction_date)::timestamp"
|
||||||
|
case "week":
|
||||||
|
expenseDateFormat = "DATE_TRUNC('week', e.transaction_date)::timestamp"
|
||||||
|
case "month":
|
||||||
|
expenseDateFormat = "DATE_TRUNC('month', e.transaction_date)::timestamp"
|
||||||
|
}
|
||||||
|
|
||||||
|
rawMaterialDataArgs := []interface{}{
|
||||||
|
organizationID,
|
||||||
|
entities.InventoryMovementTypePurchase,
|
||||||
|
"INGREDIENT",
|
||||||
|
entities.InventoryMovementReferenceTypePurchaseOrder,
|
||||||
|
dateFrom,
|
||||||
|
dateTo,
|
||||||
|
}
|
||||||
|
expenseDataArgs := []interface{}{
|
||||||
|
organizationID,
|
||||||
|
entities.PurchaseCategoryTypeExpense,
|
||||||
|
"approved",
|
||||||
|
dateFrom,
|
||||||
|
dateTo,
|
||||||
|
}
|
||||||
|
if outletID != nil {
|
||||||
|
rawMaterialDataArgs = append(rawMaterialDataArgs, *outletID)
|
||||||
|
expenseDataArgs = append(expenseDataArgs, *outletID)
|
||||||
|
}
|
||||||
|
dataArgs := append(rawMaterialDataArgs, expenseDataArgs...)
|
||||||
|
|
||||||
var data []entities.PurchasingAnalyticsData
|
var data []entities.PurchasingAnalyticsData
|
||||||
dataQuery := r.db.WithContext(ctx).
|
dataQuery := `
|
||||||
Table("inventory_movements im").
|
WITH raw_material AS (
|
||||||
Select(`
|
SELECT
|
||||||
`+dateFormat+` as date,
|
` + dateFormat + ` as date,
|
||||||
COALESCE(SUM(im.total_cost), 0) as purchases,
|
COALESCE(SUM(im.total_cost), 0) as raw_material_purchases,
|
||||||
COUNT(DISTINCT im.reference_id) as purchase_orders,
|
COUNT(DISTINCT im.reference_id) as raw_material_purchase_orders,
|
||||||
COALESCE(SUM(im.quantity), 0) as quantity,
|
COALESCE(SUM(im.quantity), 0) as quantity,
|
||||||
COUNT(DISTINCT im.item_id) as ingredients,
|
COUNT(DISTINCT im.item_id) as ingredients,
|
||||||
COUNT(DISTINCT po.vendor_id) as vendors
|
COUNT(DISTINCT po.vendor_id) as vendors
|
||||||
`).
|
FROM inventory_movements im
|
||||||
Joins("LEFT JOIN purchase_orders po ON im.reference_id = po.id").
|
LEFT JOIN purchase_orders po ON im.reference_id = po.id
|
||||||
Where("im.organization_id = ?", organizationID).
|
WHERE im.organization_id = ?
|
||||||
Where("im.movement_type = ?", entities.InventoryMovementTypePurchase).
|
AND im.movement_type = ?
|
||||||
Where("im.item_type = ?", "INGREDIENT").
|
AND im.item_type = ?
|
||||||
Where("im.reference_type = ?", entities.InventoryMovementReferenceTypePurchaseOrder).
|
AND im.reference_type = ?
|
||||||
Where("im.created_at >= ? AND im.created_at <= ?", dateFrom, dateTo).
|
AND im.created_at >= ? AND im.created_at <= ?
|
||||||
Group(dateFormat).
|
` + rawMaterialOutletFilter + `
|
||||||
Order(dateFormat)
|
GROUP BY 1
|
||||||
|
),
|
||||||
|
expense AS (
|
||||||
|
SELECT
|
||||||
|
` + expenseDateFormat + ` as date,
|
||||||
|
COALESCE(SUM(ei.amount), 0) as expense_purchases,
|
||||||
|
COUNT(DISTINCT e.id) as expense_count
|
||||||
|
FROM expense_items ei
|
||||||
|
JOIN expenses e ON ei.expense_id = e.id
|
||||||
|
JOIN purchase_categories pc ON ei.purchase_category_id = pc.id
|
||||||
|
WHERE e.organization_id = ?
|
||||||
|
AND pc.type = ?
|
||||||
|
AND e.status = ?
|
||||||
|
AND e.transaction_date >= ? AND e.transaction_date <= ?
|
||||||
|
` + expenseOutletFilter + `
|
||||||
|
GROUP BY 1
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
COALESCE(rm.date, ex.date) as date,
|
||||||
|
COALESCE(rm.raw_material_purchases, 0) + COALESCE(ex.expense_purchases, 0) as purchases,
|
||||||
|
COALESCE(rm.raw_material_purchases, 0) as raw_material_purchases,
|
||||||
|
COALESCE(ex.expense_purchases, 0) as expense_purchases,
|
||||||
|
COALESCE(rm.raw_material_purchase_orders, 0) + COALESCE(ex.expense_count, 0) as purchase_orders,
|
||||||
|
COALESCE(rm.raw_material_purchase_orders, 0) as raw_material_purchase_orders,
|
||||||
|
COALESCE(ex.expense_count, 0) as expense_count,
|
||||||
|
COALESCE(rm.quantity, 0) as quantity,
|
||||||
|
COALESCE(rm.ingredients, 0) as ingredients,
|
||||||
|
COALESCE(rm.vendors, 0) as vendors
|
||||||
|
FROM raw_material rm
|
||||||
|
FULL OUTER JOIN expense ex ON rm.date = ex.date
|
||||||
|
ORDER BY date
|
||||||
|
`
|
||||||
|
|
||||||
dataQuery = r.resolveOutletID(dataQuery, outletID, "im.outlet_id")
|
if err := r.db.WithContext(ctx).Raw(dataQuery, dataArgs...).Scan(&data).Error; err != nil {
|
||||||
|
|
||||||
if err := dataQuery.Scan(&data).Error; err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -638,7 +749,7 @@ func (r *AnalyticsRepositoryImpl) getExpenseByCategory(ctx context.Context, orga
|
|||||||
|
|
||||||
query := r.db.WithContext(ctx).
|
query := r.db.WithContext(ctx).
|
||||||
Table("expense_items ei").
|
Table("expense_items ei").
|
||||||
Select(`COALESCE(parent_coa.name, 'Lain-lain') as category_name, COALESCE(SUM(ei.amount), 0) as amount`).
|
Select(`COALESCE(parent_coa.name, coa.name, 'Lain-lain') as category_name, COALESCE(SUM(ei.amount), 0) as amount`).
|
||||||
Joins("JOIN expenses e ON ei.expense_id = e.id").
|
Joins("JOIN expenses e ON ei.expense_id = e.id").
|
||||||
Joins("JOIN chart_of_accounts coa ON ei.chart_of_account_id = coa.id").
|
Joins("JOIN chart_of_accounts coa ON ei.chart_of_account_id = coa.id").
|
||||||
Joins("LEFT JOIN chart_of_accounts parent_coa ON coa.parent_id = parent_coa.id").
|
Joins("LEFT JOIN chart_of_accounts parent_coa ON coa.parent_id = parent_coa.id").
|
||||||
@ -651,8 +762,8 @@ func (r *AnalyticsRepositoryImpl) getExpenseByCategory(ctx context.Context, orga
|
|||||||
}
|
}
|
||||||
|
|
||||||
err := query.
|
err := query.
|
||||||
Group("parent_coa.name").
|
Group("COALESCE(parent_coa.name, coa.name, 'Lain-lain')").
|
||||||
Order("parent_coa.name").
|
Order("COALESCE(parent_coa.name, coa.name, 'Lain-lain')").
|
||||||
Scan(&results).Error
|
Scan(&results).Error
|
||||||
|
|
||||||
return results, err
|
return results, err
|
||||||
|
|||||||
@ -30,6 +30,7 @@ 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
|
||||||
@ -41,6 +42,7 @@ 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 {
|
||||||
@ -107,6 +109,7 @@ 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).
|
||||||
@ -114,6 +117,165 @@ func (r *ExpenseRepositoryImpl) List(ctx context.Context, organizationID uuid.UU
|
|||||||
return expenses, total, err
|
return expenses, total, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ExpenseRepositoryImpl) GetAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, groupBy string) (*entities.ExpenseAnalytics, error) {
|
||||||
|
var summary entities.ExpenseAnalyticsSummary
|
||||||
|
|
||||||
|
summaryQuery := r.db.WithContext(ctx).
|
||||||
|
Table("expenses e").
|
||||||
|
Select(`
|
||||||
|
COALESCE(SUM(e.total), 0) as total_expenses,
|
||||||
|
COUNT(e.id) as total_expense_count,
|
||||||
|
COALESCE(SUM(e.tax), 0) as total_tax,
|
||||||
|
COALESCE(AVG(e.total), 0) as average_expense_value
|
||||||
|
`).
|
||||||
|
Where("e.organization_id = ?", organizationID).
|
||||||
|
Where("e.status = ?", "approved").
|
||||||
|
Where("e.transaction_date >= ? AND e.transaction_date <= ?", dateFrom, dateTo)
|
||||||
|
if outletID != nil {
|
||||||
|
summaryQuery = summaryQuery.Where("e.outlet_id = ?", *outletID)
|
||||||
|
}
|
||||||
|
if err := summaryQuery.Scan(&summary).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
countsQuery := r.db.WithContext(ctx).
|
||||||
|
Table("expense_items ei").
|
||||||
|
Select(`
|
||||||
|
COUNT(ei.id) as total_items,
|
||||||
|
COUNT(DISTINCT ei.purchase_category_id) as total_categories
|
||||||
|
`).
|
||||||
|
Joins("JOIN expenses e ON ei.expense_id = e.id").
|
||||||
|
Where("e.organization_id = ?", organizationID).
|
||||||
|
Where("e.status = ?", "approved").
|
||||||
|
Where("e.transaction_date >= ? AND e.transaction_date <= ?", dateFrom, dateTo)
|
||||||
|
if outletID != nil {
|
||||||
|
countsQuery = countsQuery.Where("e.outlet_id = ?", *outletID)
|
||||||
|
}
|
||||||
|
if err := countsQuery.Scan(&summary).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dateFormat := "DATE_TRUNC('day', e.transaction_date)"
|
||||||
|
switch groupBy {
|
||||||
|
case "hour":
|
||||||
|
dateFormat = "DATE_TRUNC('hour', e.transaction_date)"
|
||||||
|
case "week":
|
||||||
|
dateFormat = "DATE_TRUNC('week', e.transaction_date)"
|
||||||
|
case "month":
|
||||||
|
dateFormat = "DATE_TRUNC('month', e.transaction_date)"
|
||||||
|
}
|
||||||
|
|
||||||
|
var data []entities.ExpenseAnalyticsData
|
||||||
|
dataQuery := r.db.WithContext(ctx).
|
||||||
|
Table("expenses e").
|
||||||
|
Select(`
|
||||||
|
`+dateFormat+` as date,
|
||||||
|
COALESCE(SUM(e.total), 0) as expenses,
|
||||||
|
COUNT(e.id) as expense_count,
|
||||||
|
COALESCE(SUM(e.tax), 0) as tax,
|
||||||
|
COALESCE(SUM(item_counts.items), 0) as items,
|
||||||
|
COALESCE(SUM(item_counts.categories), 0) as categories
|
||||||
|
`).
|
||||||
|
Joins(`LEFT JOIN (
|
||||||
|
SELECT expense_id, COUNT(id) as items, COUNT(DISTINCT purchase_category_id) as categories
|
||||||
|
FROM expense_items
|
||||||
|
GROUP BY expense_id
|
||||||
|
) item_counts ON item_counts.expense_id = e.id`).
|
||||||
|
Where("e.organization_id = ?", organizationID).
|
||||||
|
Where("e.status = ?", "approved").
|
||||||
|
Where("e.transaction_date >= ? AND e.transaction_date <= ?", dateFrom, dateTo).
|
||||||
|
Group(dateFormat).
|
||||||
|
Order(dateFormat)
|
||||||
|
if outletID != nil {
|
||||||
|
dataQuery = dataQuery.Where("e.outlet_id = ?", *outletID)
|
||||||
|
}
|
||||||
|
if err := dataQuery.Scan(&data).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var categoryData []entities.ExpenseAnalyticsCategoryData
|
||||||
|
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").
|
||||||
|
Select(`
|
||||||
|
COALESCE(parent_coa.id, coa.id) as chart_of_account_id,
|
||||||
|
COALESCE(parent_coa.name, coa.name, 'Lain-lain') as chart_of_account_name,
|
||||||
|
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 chart_of_accounts coa ON ei.chart_of_account_id = coa.id").
|
||||||
|
Joins("LEFT JOIN chart_of_accounts parent_coa ON coa.parent_id = parent_coa.id").
|
||||||
|
Where("e.organization_id = ?", organizationID).
|
||||||
|
Where("e.status = ?", "approved").
|
||||||
|
Where("e.transaction_date >= ? AND e.transaction_date <= ?", dateFrom, dateTo).
|
||||||
|
Group("COALESCE(parent_coa.id, coa.id), COALESCE(parent_coa.name, coa.name, 'Lain-lain')").
|
||||||
|
Order("total_amount DESC")
|
||||||
|
if outletID != nil {
|
||||||
|
chartOfAccountQuery = chartOfAccountQuery.Where("e.outlet_id = ?", *outletID)
|
||||||
|
}
|
||||||
|
if err := chartOfAccountQuery.Scan(&chartOfAccountData).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var itemData []entities.ExpenseAnalyticsItemData
|
||||||
|
itemQuery := r.db.WithContext(ctx).
|
||||||
|
Table("expense_items ei").
|
||||||
|
Select(`
|
||||||
|
COALESCE(NULLIF(ei.item, ''), ei.description, coa.name) as item,
|
||||||
|
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 chart_of_accounts coa ON ei.chart_of_account_id = coa.id").
|
||||||
|
Where("e.organization_id = ?", organizationID).
|
||||||
|
Where("e.status = ?", "approved").
|
||||||
|
Where("e.transaction_date >= ? AND e.transaction_date <= ?", dateFrom, dateTo).
|
||||||
|
Group("COALESCE(NULLIF(ei.item, ''), ei.description, coa.name)").
|
||||||
|
Order("total_amount DESC")
|
||||||
|
if outletID != nil {
|
||||||
|
itemQuery = itemQuery.Where("e.outlet_id = ?", *outletID)
|
||||||
|
}
|
||||||
|
if err := itemQuery.Scan(&itemData).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &entities.ExpenseAnalytics{
|
||||||
|
Summary: summary,
|
||||||
|
Data: data,
|
||||||
|
CategoryData: categoryData,
|
||||||
|
ChartOfAccountData: chartOfAccountData,
|
||||||
|
ItemData: itemData,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *ExpenseRepositoryImpl) CreateItem(ctx context.Context, item *entities.ExpenseItem) error {
|
func (r *ExpenseRepositoryImpl) CreateItem(ctx context.Context, item *entities.ExpenseItem) error {
|
||||||
return r.db.WithContext(ctx).Create(item).Error
|
return r.db.WithContext(ctx).Create(item).Error
|
||||||
}
|
}
|
||||||
|
|||||||
@ -173,4 +173,3 @@ func (r *IngredientUnitConverterRepositoryImpl) ConvertQuantity(ctx context.Cont
|
|||||||
// If no converter found, return error
|
// If no converter found, return error
|
||||||
return 0, fmt.Errorf("no conversion found between units %s and %s for ingredient %s", fromUnitID, toUnitID, ingredientID)
|
return 0, fmt.Errorf("no conversion found between units %s and %s for ingredient %s", fromUnitID, toUnitID, ingredientID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
91
internal/repository/purchase_category_repository.go
Normal file
91
internal/repository/purchase_category_repository.go
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PurchaseCategoryRepositoryImpl struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPurchaseCategoryRepositoryImpl(db *gorm.DB) *PurchaseCategoryRepositoryImpl {
|
||||||
|
return &PurchaseCategoryRepositoryImpl{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PurchaseCategoryRepositoryImpl) Create(ctx context.Context, category *entities.PurchaseCategory) error {
|
||||||
|
return r.db.WithContext(ctx).Create(category).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PurchaseCategoryRepositoryImpl) GetByIDAndOrganizationID(ctx context.Context, id, organizationID uuid.UUID) (*entities.PurchaseCategory, error) {
|
||||||
|
var category entities.PurchaseCategory
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
First(&category, "id = ? AND organization_id = ?", id, organizationID).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &category, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PurchaseCategoryRepositoryImpl) Update(ctx context.Context, category *entities.PurchaseCategory) error {
|
||||||
|
return r.db.WithContext(ctx).Save(category).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PurchaseCategoryRepositoryImpl) SoftDelete(ctx context.Context, id, organizationID uuid.UUID) error {
|
||||||
|
return r.db.WithContext(ctx).
|
||||||
|
Model(&entities.PurchaseCategory{}).
|
||||||
|
Where("id = ? AND organization_id = ?", id, organizationID).
|
||||||
|
Update("is_active", false).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PurchaseCategoryRepositoryImpl) List(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, limit, offset int) ([]*entities.PurchaseCategory, int64, error) {
|
||||||
|
var categories []*entities.PurchaseCategory
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
query := r.db.WithContext(ctx).
|
||||||
|
Model(&entities.PurchaseCategory{}).
|
||||||
|
Where("organization_id = ?", organizationID)
|
||||||
|
|
||||||
|
for key, value := range filters {
|
||||||
|
switch key {
|
||||||
|
case "search":
|
||||||
|
searchValue := "%" + value.(string) + "%"
|
||||||
|
query = query.Where("name ILIKE ? OR code ILIKE ?", searchValue, searchValue)
|
||||||
|
case "parent_id":
|
||||||
|
query = query.Where("parent_id = ?", value)
|
||||||
|
case "type":
|
||||||
|
query = query.Where("type = ?", value)
|
||||||
|
case "is_active":
|
||||||
|
query = query.Where("is_active = ?", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := query.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err := query.
|
||||||
|
Order("parent_id NULLS FIRST, sort_order ASC, name ASC").
|
||||||
|
Limit(limit).
|
||||||
|
Offset(offset).
|
||||||
|
Find(&categories).Error
|
||||||
|
return categories, total, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PurchaseCategoryRepositoryImpl) ExistsByCode(ctx context.Context, organizationID uuid.UUID, code string, excludeID *uuid.UUID) (bool, error) {
|
||||||
|
query := r.db.WithContext(ctx).
|
||||||
|
Model(&entities.PurchaseCategory{}).
|
||||||
|
Where("organization_id = ? AND code = ?", organizationID, code)
|
||||||
|
|
||||||
|
if excludeID != nil {
|
||||||
|
query = query.Where("id != ?", *excludeID)
|
||||||
|
}
|
||||||
|
|
||||||
|
var count int64
|
||||||
|
err := query.Count(&count).Error
|
||||||
|
return count > 0, err
|
||||||
|
}
|
||||||
@ -31,6 +31,7 @@ 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
|
||||||
@ -45,6 +46,7 @@ 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).
|
||||||
@ -105,6 +107,7 @@ 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").
|
||||||
@ -168,6 +171,7 @@ 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
|
||||||
@ -179,6 +183,7 @@ 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
|
||||||
@ -219,6 +224,7 @@ 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
|
||||||
|
|||||||
@ -36,6 +36,7 @@ type Router struct {
|
|||||||
productRecipeHandler *handler.ProductRecipeHandler
|
productRecipeHandler *handler.ProductRecipeHandler
|
||||||
vendorHandler *handler.VendorHandler
|
vendorHandler *handler.VendorHandler
|
||||||
purchaseOrderHandler *handler.PurchaseOrderHandler
|
purchaseOrderHandler *handler.PurchaseOrderHandler
|
||||||
|
purchaseCategoryHandler *handler.PurchaseCategoryHandler
|
||||||
unitConverterHandler *handler.IngredientUnitConverterHandler
|
unitConverterHandler *handler.IngredientUnitConverterHandler
|
||||||
chartOfAccountTypeHandler *handler.ChartOfAccountTypeHandler
|
chartOfAccountTypeHandler *handler.ChartOfAccountTypeHandler
|
||||||
chartOfAccountHandler *handler.ChartOfAccountHandler
|
chartOfAccountHandler *handler.ChartOfAccountHandler
|
||||||
@ -57,7 +58,7 @@ type Router struct {
|
|||||||
redisClient *redis.Client
|
redisClient *redis.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRouter(cfg *config.Config, healthHandler *handler.HealthHandler, authService service.AuthService, authMiddleware *middleware.AuthMiddleware, userService *service.UserServiceImpl, userValidator *validator.UserValidatorImpl, organizationService service.OrganizationService, organizationValidator validator.OrganizationValidator, outletService service.OutletService, outletValidator validator.OutletValidator, outletSettingService service.OutletSettingService, categoryService service.CategoryService, categoryValidator validator.CategoryValidator, productService service.ProductService, productValidator validator.ProductValidator, productVariantService service.ProductVariantService, productVariantValidator validator.ProductVariantValidator, inventoryService service.InventoryService, inventoryValidator validator.InventoryValidator, orderService service.OrderService, orderValidator validator.OrderValidator, fileService service.FileService, fileValidator validator.FileValidator, customerService service.CustomerService, customerValidator validator.CustomerValidator, paymentMethodService service.PaymentMethodService, paymentMethodValidator validator.PaymentMethodValidator, analyticsService *service.AnalyticsServiceImpl, reportService service.ReportService, tableService *service.TableServiceImpl, tableValidator *validator.TableValidator, unitService handler.UnitService, ingredientService handler.IngredientService, productRecipeService service.ProductRecipeService, vendorService service.VendorService, vendorValidator validator.VendorValidator, purchaseOrderService service.PurchaseOrderService, purchaseOrderValidator validator.PurchaseOrderValidator, unitConverterService service.IngredientUnitConverterService, unitConverterValidator validator.IngredientUnitConverterValidator, chartOfAccountTypeService service.ChartOfAccountTypeService, chartOfAccountTypeValidator validator.ChartOfAccountTypeValidator, chartOfAccountService service.ChartOfAccountService, chartOfAccountValidator validator.ChartOfAccountValidator, accountService service.AccountService, accountValidator validator.AccountValidator, orderIngredientTransactionService service.OrderIngredientTransactionService, orderIngredientTransactionValidator validator.OrderIngredientTransactionValidator, gamificationService service.GamificationService, gamificationValidator validator.GamificationValidator, rewardService service.RewardService, rewardValidator validator.RewardValidator, campaignService service.CampaignService, campaignValidator validator.CampaignValidator, customerAuthService service.CustomerAuthService, customerAuthValidator validator.CustomerAuthValidator, customerPointsService service.CustomerPointsService, spinGameService service.SpinGameService, customerAuthMiddleware *middleware.CustomerAuthMiddleware, userDeviceService service.UserDeviceService, userDeviceValidator validator.UserDeviceValidator, notificationService service.NotificationService, notificationValidator validator.NotificationValidator, productOutletPriceService service.ProductOutletPriceService, productOutletPriceValidator validator.ProductOutletPriceValidator, selfOrderHandler *handler.SelfOrderHandler, expenseService *service.ExpenseServiceImpl, expenseValidator *validator.ExpenseValidatorImpl, redisClient *redis.Client) *Router {
|
func NewRouter(cfg *config.Config, healthHandler *handler.HealthHandler, authService service.AuthService, authMiddleware *middleware.AuthMiddleware, userService *service.UserServiceImpl, userValidator *validator.UserValidatorImpl, organizationService service.OrganizationService, organizationValidator validator.OrganizationValidator, outletService service.OutletService, outletValidator validator.OutletValidator, outletSettingService service.OutletSettingService, categoryService service.CategoryService, categoryValidator validator.CategoryValidator, productService service.ProductService, productValidator validator.ProductValidator, productVariantService service.ProductVariantService, productVariantValidator validator.ProductVariantValidator, inventoryService service.InventoryService, inventoryValidator validator.InventoryValidator, orderService service.OrderService, orderValidator validator.OrderValidator, fileService service.FileService, fileValidator validator.FileValidator, customerService service.CustomerService, customerValidator validator.CustomerValidator, paymentMethodService service.PaymentMethodService, paymentMethodValidator validator.PaymentMethodValidator, analyticsService *service.AnalyticsServiceImpl, reportService service.ReportService, tableService *service.TableServiceImpl, tableValidator *validator.TableValidator, unitService handler.UnitService, ingredientService handler.IngredientService, productRecipeService service.ProductRecipeService, vendorService service.VendorService, vendorValidator validator.VendorValidator, purchaseOrderService service.PurchaseOrderService, purchaseOrderValidator validator.PurchaseOrderValidator, purchaseCategoryService service.PurchaseCategoryService, purchaseCategoryValidator validator.PurchaseCategoryValidator, unitConverterService service.IngredientUnitConverterService, unitConverterValidator validator.IngredientUnitConverterValidator, chartOfAccountTypeService service.ChartOfAccountTypeService, chartOfAccountTypeValidator validator.ChartOfAccountTypeValidator, chartOfAccountService service.ChartOfAccountService, chartOfAccountValidator validator.ChartOfAccountValidator, accountService service.AccountService, accountValidator validator.AccountValidator, orderIngredientTransactionService service.OrderIngredientTransactionService, orderIngredientTransactionValidator validator.OrderIngredientTransactionValidator, gamificationService service.GamificationService, gamificationValidator validator.GamificationValidator, rewardService service.RewardService, rewardValidator validator.RewardValidator, campaignService service.CampaignService, campaignValidator validator.CampaignValidator, customerAuthService service.CustomerAuthService, customerAuthValidator validator.CustomerAuthValidator, customerPointsService service.CustomerPointsService, spinGameService service.SpinGameService, customerAuthMiddleware *middleware.CustomerAuthMiddleware, userDeviceService service.UserDeviceService, userDeviceValidator validator.UserDeviceValidator, notificationService service.NotificationService, notificationValidator validator.NotificationValidator, productOutletPriceService service.ProductOutletPriceService, productOutletPriceValidator validator.ProductOutletPriceValidator, selfOrderHandler *handler.SelfOrderHandler, expenseService *service.ExpenseServiceImpl, expenseValidator *validator.ExpenseValidatorImpl, redisClient *redis.Client) *Router {
|
||||||
|
|
||||||
return &Router{
|
return &Router{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
@ -82,6 +83,7 @@ func NewRouter(cfg *config.Config, healthHandler *handler.HealthHandler, authSer
|
|||||||
productRecipeHandler: handler.NewProductRecipeHandler(productRecipeService),
|
productRecipeHandler: handler.NewProductRecipeHandler(productRecipeService),
|
||||||
vendorHandler: handler.NewVendorHandler(vendorService, vendorValidator),
|
vendorHandler: handler.NewVendorHandler(vendorService, vendorValidator),
|
||||||
purchaseOrderHandler: handler.NewPurchaseOrderHandler(purchaseOrderService, purchaseOrderValidator),
|
purchaseOrderHandler: handler.NewPurchaseOrderHandler(purchaseOrderService, purchaseOrderValidator),
|
||||||
|
purchaseCategoryHandler: handler.NewPurchaseCategoryHandler(purchaseCategoryService, purchaseCategoryValidator),
|
||||||
unitConverterHandler: handler.NewIngredientUnitConverterHandler(unitConverterService, unitConverterValidator),
|
unitConverterHandler: handler.NewIngredientUnitConverterHandler(unitConverterService, unitConverterValidator),
|
||||||
chartOfAccountTypeHandler: handler.NewChartOfAccountTypeHandler(chartOfAccountTypeService, chartOfAccountTypeValidator),
|
chartOfAccountTypeHandler: handler.NewChartOfAccountTypeHandler(chartOfAccountTypeService, chartOfAccountTypeValidator),
|
||||||
chartOfAccountHandler: handler.NewChartOfAccountHandler(chartOfAccountService, chartOfAccountValidator),
|
chartOfAccountHandler: handler.NewChartOfAccountHandler(chartOfAccountService, chartOfAccountValidator),
|
||||||
@ -387,6 +389,16 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
|||||||
purchaseOrders.DELETE("/:id", r.purchaseOrderHandler.DeletePurchaseOrder)
|
purchaseOrders.DELETE("/:id", r.purchaseOrderHandler.DeletePurchaseOrder)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
purchaseCategories := protected.Group("/purchase-categories")
|
||||||
|
purchaseCategories.Use(r.authMiddleware.RequireAdminOrManager())
|
||||||
|
{
|
||||||
|
purchaseCategories.POST("", r.purchaseCategoryHandler.CreatePurchaseCategory)
|
||||||
|
purchaseCategories.GET("", r.purchaseCategoryHandler.ListPurchaseCategories)
|
||||||
|
purchaseCategories.GET("/:id", r.purchaseCategoryHandler.GetPurchaseCategory)
|
||||||
|
purchaseCategories.PUT("/:id", r.purchaseCategoryHandler.UpdatePurchaseCategory)
|
||||||
|
purchaseCategories.DELETE("/:id", r.purchaseCategoryHandler.DeletePurchaseCategory)
|
||||||
|
}
|
||||||
|
|
||||||
unitConverters := protected.Group("/unit-converters")
|
unitConverters := protected.Group("/unit-converters")
|
||||||
unitConverters.Use(r.authMiddleware.RequireAdminOrManager())
|
unitConverters.Use(r.authMiddleware.RequireAdminOrManager())
|
||||||
{
|
{
|
||||||
@ -454,6 +466,7 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
|||||||
{
|
{
|
||||||
expenses.POST("", r.expenseHandler.CreateExpense)
|
expenses.POST("", r.expenseHandler.CreateExpense)
|
||||||
expenses.GET("", r.expenseHandler.ListExpenses)
|
expenses.GET("", r.expenseHandler.ListExpenses)
|
||||||
|
expenses.GET("/analytics", r.expenseHandler.GetExpenseAnalytics)
|
||||||
expenses.GET("/:id", r.expenseHandler.GetExpense)
|
expenses.GET("/:id", r.expenseHandler.GetExpense)
|
||||||
expenses.PUT("/:id", r.expenseHandler.UpdateExpense)
|
expenses.PUT("/:id", r.expenseHandler.UpdateExpense)
|
||||||
expenses.DELETE("/:id", r.expenseHandler.DeleteExpense)
|
expenses.DELETE("/:id", r.expenseHandler.DeleteExpense)
|
||||||
|
|||||||
@ -19,6 +19,7 @@ type ExpenseService interface {
|
|||||||
DeleteExpense(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response
|
DeleteExpense(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response
|
||||||
GetExpenseByID(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response
|
GetExpenseByID(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response
|
||||||
ListExpenses(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.ListExpenseRequest) *contract.Response
|
ListExpenses(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.ListExpenseRequest) *contract.Response
|
||||||
|
GetExpenseAnalytics(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.ExpenseAnalyticsRequest) *contract.Response
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseServiceImpl struct {
|
type ExpenseServiceImpl struct {
|
||||||
@ -126,3 +127,24 @@ func (s *ExpenseServiceImpl) ListExpenses(ctx context.Context, apctx *appcontext
|
|||||||
|
|
||||||
return contract.BuildSuccessResponse(response)
|
return contract.BuildSuccessResponse(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ExpenseServiceImpl) GetExpenseAnalytics(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.ExpenseAnalyticsRequest) *contract.Response {
|
||||||
|
modelReq, err := transformer.ExpenseAnalyticsRequestToModel(req)
|
||||||
|
if err != nil {
|
||||||
|
errorResp := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.ExpenseServiceEntity, err.Error())
|
||||||
|
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
|
||||||
|
}
|
||||||
|
|
||||||
|
modelReq.OrganizationID = apctx.OrganizationID
|
||||||
|
if apctx.OutletID != uuid.Nil {
|
||||||
|
modelReq.OutletID = &apctx.OutletID
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := s.expenseProcessor.GetExpenseAnalytics(ctx, modelReq)
|
||||||
|
if err != nil {
|
||||||
|
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.ExpenseServiceEntity, err.Error())
|
||||||
|
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
|
||||||
|
}
|
||||||
|
|
||||||
|
return contract.BuildSuccessResponse(transformer.ExpenseAnalyticsModelToContract(response))
|
||||||
|
}
|
||||||
|
|||||||
@ -160,4 +160,3 @@ func (s *IngredientUnitConverterServiceImpl) GetUnitsByIngredientID(ctx context.
|
|||||||
contractResponse := transformer.IngredientUnitsModelResponseToResponse(unitsResponse)
|
contractResponse := transformer.IngredientUnitsModelResponseToResponse(unitsResponse)
|
||||||
return contract.BuildSuccessResponse(contractResponse)
|
return contract.BuildSuccessResponse(contractResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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) 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, purchaseOrderItemID *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) 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, purchaseOrderItemID *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,22 +36,23 @@ 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,
|
||||||
UserID: userID,
|
PurchaseOrderItemID: purchaseOrderItemID,
|
||||||
Reason: &reason,
|
UserID: userID,
|
||||||
CreatedAt: time.Now(),
|
Reason: &reason,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.inventoryMovementRepo.Create(ctx, movement)
|
err = s.inventoryMovementRepo.Create(ctx, movement)
|
||||||
|
|||||||
107
internal/service/purchase_category_service.go
Normal file
107
internal/service/purchase_category_service.go
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"apskel-pos-be/internal/appcontext"
|
||||||
|
"apskel-pos-be/internal/constants"
|
||||||
|
"apskel-pos-be/internal/contract"
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
"apskel-pos-be/internal/processor"
|
||||||
|
"apskel-pos-be/internal/transformer"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PurchaseCategoryService interface {
|
||||||
|
CreatePurchaseCategory(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.CreatePurchaseCategoryRequest) *contract.Response
|
||||||
|
UpdatePurchaseCategory(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, req *contract.UpdatePurchaseCategoryRequest) *contract.Response
|
||||||
|
DeletePurchaseCategory(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response
|
||||||
|
GetPurchaseCategoryByID(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response
|
||||||
|
ListPurchaseCategories(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.ListPurchaseCategoriesRequest) *contract.Response
|
||||||
|
}
|
||||||
|
|
||||||
|
type PurchaseCategoryServiceImpl struct {
|
||||||
|
purchaseCategoryProcessor processor.PurchaseCategoryProcessor
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPurchaseCategoryService(purchaseCategoryProcessor processor.PurchaseCategoryProcessor) *PurchaseCategoryServiceImpl {
|
||||||
|
return &PurchaseCategoryServiceImpl{purchaseCategoryProcessor: purchaseCategoryProcessor}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PurchaseCategoryServiceImpl) CreatePurchaseCategory(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.CreatePurchaseCategoryRequest) *contract.Response {
|
||||||
|
modelReq := transformer.CreatePurchaseCategoryRequestToModel(apctx, req)
|
||||||
|
|
||||||
|
category, err := s.purchaseCategoryProcessor.CreatePurchaseCategory(ctx, modelReq)
|
||||||
|
if err != nil {
|
||||||
|
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.PurchaseCategoryServiceEntity, err.Error())
|
||||||
|
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
|
||||||
|
}
|
||||||
|
|
||||||
|
return contract.BuildSuccessResponse(transformer.PurchaseCategoryModelResponseToResponse(category))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PurchaseCategoryServiceImpl) UpdatePurchaseCategory(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, req *contract.UpdatePurchaseCategoryRequest) *contract.Response {
|
||||||
|
modelReq := transformer.UpdatePurchaseCategoryRequestToModel(req)
|
||||||
|
|
||||||
|
category, err := s.purchaseCategoryProcessor.UpdatePurchaseCategory(ctx, id, apctx.OrganizationID, modelReq)
|
||||||
|
if err != nil {
|
||||||
|
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.PurchaseCategoryServiceEntity, err.Error())
|
||||||
|
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
|
||||||
|
}
|
||||||
|
|
||||||
|
return contract.BuildSuccessResponse(transformer.PurchaseCategoryModelResponseToResponse(category))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PurchaseCategoryServiceImpl) DeletePurchaseCategory(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response {
|
||||||
|
err := s.purchaseCategoryProcessor.DeletePurchaseCategory(ctx, id, apctx.OrganizationID)
|
||||||
|
if err != nil {
|
||||||
|
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.PurchaseCategoryServiceEntity, err.Error())
|
||||||
|
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
|
||||||
|
}
|
||||||
|
|
||||||
|
return contract.BuildSuccessResponse(map[string]interface{}{
|
||||||
|
"message": "Purchase category deleted successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PurchaseCategoryServiceImpl) GetPurchaseCategoryByID(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response {
|
||||||
|
category, err := s.purchaseCategoryProcessor.GetPurchaseCategoryByID(ctx, id, apctx.OrganizationID)
|
||||||
|
if err != nil {
|
||||||
|
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.PurchaseCategoryServiceEntity, err.Error())
|
||||||
|
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
|
||||||
|
}
|
||||||
|
|
||||||
|
return contract.BuildSuccessResponse(transformer.PurchaseCategoryModelResponseToResponse(category))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PurchaseCategoryServiceImpl) ListPurchaseCategories(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.ListPurchaseCategoriesRequest) *contract.Response {
|
||||||
|
modelReq := &models.ListPurchaseCategoriesRequest{
|
||||||
|
OrganizationID: apctx.OrganizationID,
|
||||||
|
ParentID: req.ParentID,
|
||||||
|
Type: req.Type,
|
||||||
|
Search: req.Search,
|
||||||
|
IsActive: req.IsActive,
|
||||||
|
Page: req.Page,
|
||||||
|
Limit: req.Limit,
|
||||||
|
}
|
||||||
|
|
||||||
|
categories, totalCount, err := s.purchaseCategoryProcessor.ListPurchaseCategories(ctx, modelReq)
|
||||||
|
if err != nil {
|
||||||
|
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.PurchaseCategoryServiceEntity, err.Error())
|
||||||
|
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
|
||||||
|
}
|
||||||
|
|
||||||
|
totalPages := totalCount / req.Limit
|
||||||
|
if totalCount%req.Limit > 0 {
|
||||||
|
totalPages++
|
||||||
|
}
|
||||||
|
|
||||||
|
return contract.BuildSuccessResponse(&contract.ListPurchaseCategoriesResponse{
|
||||||
|
PurchaseCategories: transformer.PurchaseCategoryModelResponsesToResponses(categories),
|
||||||
|
TotalCount: totalCount,
|
||||||
|
Page: req.Page,
|
||||||
|
Limit: req.Limit,
|
||||||
|
TotalPages: totalPages,
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -169,12 +169,16 @@ 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,
|
||||||
PurchaseOrders: item.PurchaseOrders,
|
RawMaterialPurchases: item.RawMaterialPurchases,
|
||||||
Quantity: item.Quantity,
|
ExpensePurchases: item.ExpensePurchases,
|
||||||
Ingredients: item.Ingredients,
|
PurchaseOrders: item.PurchaseOrders,
|
||||||
Vendors: item.Vendors,
|
RawMaterialPurchaseOrders: item.RawMaterialPurchaseOrders,
|
||||||
|
ExpenseCount: item.ExpenseCount,
|
||||||
|
Quantity: item.Quantity,
|
||||||
|
Ingredients: item.Ingredients,
|
||||||
|
Vendors: item.Vendors,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -211,7 +215,11 @@ 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,
|
||||||
|
|||||||
@ -52,17 +52,47 @@ 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) {
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package transformer
|
|||||||
import (
|
import (
|
||||||
"apskel-pos-be/internal/contract"
|
"apskel-pos-be/internal/contract"
|
||||||
"apskel-pos-be/internal/models"
|
"apskel-pos-be/internal/models"
|
||||||
|
"apskel-pos-be/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func CreateExpenseRequestToModel(req *contract.CreateExpenseRequest) *models.CreateExpenseRequest {
|
func CreateExpenseRequestToModel(req *contract.CreateExpenseRequest) *models.CreateExpenseRequest {
|
||||||
@ -26,10 +27,11 @@ 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,
|
||||||
Item: req.Item,
|
PurchaseCategoryID: req.PurchaseCategoryID,
|
||||||
Description: req.Description,
|
Item: req.Item,
|
||||||
Amount: req.Amount,
|
Description: req.Description,
|
||||||
|
Amount: req.Amount,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,10 +61,11 @@ 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,
|
||||||
Item: req.Item,
|
PurchaseCategoryID: req.PurchaseCategoryID,
|
||||||
Description: req.Description,
|
Item: req.Item,
|
||||||
Amount: req.Amount,
|
Description: req.Description,
|
||||||
|
Amount: req.Amount,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,15 +111,19 @@ 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,
|
||||||
Item: item.Item,
|
PurchaseCategoryID: item.PurchaseCategoryID,
|
||||||
Description: item.Description,
|
PurchaseCategoryName: item.PurchaseCategoryName,
|
||||||
Amount: item.Amount,
|
PurchaseCategoryType: item.PurchaseCategoryType,
|
||||||
CreatedAt: item.CreatedAt,
|
PurchaseCategory: PurchaseCategoryModelResponseToResponse(item.PurchaseCategory),
|
||||||
UpdatedAt: item.UpdatedAt,
|
Item: item.Item,
|
||||||
|
Description: item.Description,
|
||||||
|
Amount: item.Amount,
|
||||||
|
CreatedAt: item.CreatedAt,
|
||||||
|
UpdatedAt: item.UpdatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,3 +141,94 @@ func ExpenseModelResponsesToResponses(expenses []*models.ExpenseResponse) []cont
|
|||||||
}
|
}
|
||||||
return responses
|
return responses
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ExpenseAnalyticsRequestToModel(req *contract.ExpenseAnalyticsRequest) (*models.ExpenseAnalyticsRequest, error) {
|
||||||
|
dateFrom, dateTo, err := util.ParseDateRangeToJakartaTime(req.DateFrom, req.DateTo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
modelReq := &models.ExpenseAnalyticsRequest{
|
||||||
|
OutletID: parseOutletID(req.OutletID),
|
||||||
|
GroupBy: req.GroupBy,
|
||||||
|
}
|
||||||
|
if dateFrom != nil {
|
||||||
|
modelReq.DateFrom = *dateFrom
|
||||||
|
}
|
||||||
|
if dateTo != nil {
|
||||||
|
modelReq.DateTo = *dateTo
|
||||||
|
}
|
||||||
|
|
||||||
|
return modelReq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExpenseAnalyticsModelToContract(resp *models.ExpenseAnalyticsResponse) *contract.ExpenseAnalyticsResponse {
|
||||||
|
if resp == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make([]contract.ExpenseAnalyticsData, len(resp.Data))
|
||||||
|
for i, item := range resp.Data {
|
||||||
|
data[i] = contract.ExpenseAnalyticsData{
|
||||||
|
Date: item.Date,
|
||||||
|
Expenses: item.Expenses,
|
||||||
|
ExpenseCount: item.ExpenseCount,
|
||||||
|
Tax: item.Tax,
|
||||||
|
Items: item.Items,
|
||||||
|
Categories: item.Categories,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryData := make([]contract.ExpenseAnalyticsCategoryData, len(resp.CategoryData))
|
||||||
|
for i, item := range resp.CategoryData {
|
||||||
|
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,
|
||||||
|
ChartOfAccountName: item.ChartOfAccountName,
|
||||||
|
TotalAmount: item.TotalAmount,
|
||||||
|
ExpenseCount: item.ExpenseCount,
|
||||||
|
ItemCount: item.ItemCount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
itemData := make([]contract.ExpenseAnalyticsItemData, len(resp.ItemData))
|
||||||
|
for i, item := range resp.ItemData {
|
||||||
|
itemData[i] = contract.ExpenseAnalyticsItemData{
|
||||||
|
Item: item.Item,
|
||||||
|
TotalAmount: item.TotalAmount,
|
||||||
|
ExpenseCount: item.ExpenseCount,
|
||||||
|
ItemCount: item.ItemCount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &contract.ExpenseAnalyticsResponse{
|
||||||
|
OrganizationID: resp.OrganizationID,
|
||||||
|
OutletID: resp.OutletID,
|
||||||
|
DateFrom: resp.DateFrom,
|
||||||
|
DateTo: resp.DateTo,
|
||||||
|
GroupBy: resp.GroupBy,
|
||||||
|
Summary: contract.ExpenseAnalyticsSummary{
|
||||||
|
TotalExpenses: resp.Summary.TotalExpenses,
|
||||||
|
TotalExpenseCount: resp.Summary.TotalExpenseCount,
|
||||||
|
TotalTax: resp.Summary.TotalTax,
|
||||||
|
AverageExpenseValue: resp.Summary.AverageExpenseValue,
|
||||||
|
TotalCategories: resp.Summary.TotalCategories,
|
||||||
|
TotalItems: resp.Summary.TotalItems,
|
||||||
|
},
|
||||||
|
Data: data,
|
||||||
|
CategoryData: categoryData,
|
||||||
|
ChartOfAccountData: chartOfAccountData,
|
||||||
|
ItemData: itemData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -175,4 +175,3 @@ func IngredientUnitsModelResponseToResponse(model *models.IngredientUnitsRespons
|
|||||||
|
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
72
internal/transformer/purchase_category_transformer.go
Normal file
72
internal/transformer/purchase_category_transformer.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package transformer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/appcontext"
|
||||||
|
"apskel-pos-be/internal/contract"
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreatePurchaseCategoryRequestToModel(apctx *appcontext.ContextInfo, req *contract.CreatePurchaseCategoryRequest) *models.CreatePurchaseCategoryRequest {
|
||||||
|
sortOrder := 0
|
||||||
|
if req.SortOrder != nil {
|
||||||
|
sortOrder = *req.SortOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
isActive := true
|
||||||
|
if req.IsActive != nil {
|
||||||
|
isActive = *req.IsActive
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.CreatePurchaseCategoryRequest{
|
||||||
|
OrganizationID: apctx.OrganizationID,
|
||||||
|
ParentID: req.ParentID,
|
||||||
|
Code: req.Code,
|
||||||
|
Name: req.Name,
|
||||||
|
Type: req.Type,
|
||||||
|
SortOrder: sortOrder,
|
||||||
|
IsActive: isActive,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdatePurchaseCategoryRequestToModel(req *contract.UpdatePurchaseCategoryRequest) *models.UpdatePurchaseCategoryRequest {
|
||||||
|
return &models.UpdatePurchaseCategoryRequest{
|
||||||
|
ParentID: req.ParentID,
|
||||||
|
Code: req.Code,
|
||||||
|
Name: req.Name,
|
||||||
|
Type: req.Type,
|
||||||
|
SortOrder: req.SortOrder,
|
||||||
|
IsActive: req.IsActive,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PurchaseCategoryModelResponseToResponse(category *models.PurchaseCategoryResponse) *contract.PurchaseCategoryResponse {
|
||||||
|
if category == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &contract.PurchaseCategoryResponse{
|
||||||
|
ID: category.ID,
|
||||||
|
OrganizationID: category.OrganizationID,
|
||||||
|
PresetID: category.PresetID,
|
||||||
|
ParentID: category.ParentID,
|
||||||
|
Code: category.Code,
|
||||||
|
Name: category.Name,
|
||||||
|
Type: category.Type,
|
||||||
|
SortOrder: category.SortOrder,
|
||||||
|
IsSystem: category.IsSystem,
|
||||||
|
IsActive: category.IsActive,
|
||||||
|
CreatedAt: category.CreatedAt,
|
||||||
|
UpdatedAt: category.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PurchaseCategoryModelResponsesToResponses(categories []models.PurchaseCategoryResponse) []contract.PurchaseCategoryResponse {
|
||||||
|
responses := make([]contract.PurchaseCategoryResponse, len(categories))
|
||||||
|
for i, category := range categories {
|
||||||
|
response := PurchaseCategoryModelResponseToResponse(&category)
|
||||||
|
if response != nil {
|
||||||
|
responses[i] = *response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return responses
|
||||||
|
}
|
||||||
@ -11,11 +11,12 @@ 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,
|
||||||
Description: item.Description,
|
PurchaseCategoryID: item.PurchaseCategoryID,
|
||||||
Quantity: item.Quantity,
|
Description: item.Description,
|
||||||
UnitID: item.UnitID,
|
Quantity: item.Quantity,
|
||||||
Amount: item.Amount,
|
UnitID: item.UnitID,
|
||||||
|
Amount: item.Amount,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,12 +55,13 @@ 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,
|
||||||
Description: item.Description,
|
PurchaseCategoryID: item.PurchaseCategoryID,
|
||||||
Quantity: item.Quantity,
|
Description: item.Description,
|
||||||
UnitID: item.UnitID,
|
Quantity: item.Quantity,
|
||||||
Amount: item.Amount,
|
UnitID: item.UnitID,
|
||||||
|
Amount: item.Amount,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -154,15 +156,16 @@ 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,
|
||||||
Description: item.Description,
|
PurchaseCategoryID: item.PurchaseCategoryID,
|
||||||
Quantity: item.Quantity,
|
Description: item.Description,
|
||||||
UnitID: item.UnitID,
|
Quantity: item.Quantity,
|
||||||
Amount: item.Amount,
|
UnitID: item.UnitID,
|
||||||
CreatedAt: item.CreatedAt,
|
Amount: item.Amount,
|
||||||
UpdatedAt: item.UpdatedAt,
|
CreatedAt: item.CreatedAt,
|
||||||
|
UpdatedAt: item.UpdatedAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map ingredient if present
|
// Map ingredient if present
|
||||||
@ -173,6 +176,10 @@ 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,15 +12,19 @@ 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: uuid.New(),
|
IngredientID: &ingredientID,
|
||||||
Quantity: 1,
|
Quantity: &quantity,
|
||||||
UnitID: uuid.New(),
|
UnitID: &unitID,
|
||||||
Amount: 1000,
|
Amount: 1000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -68,12 +68,18 @@ 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
|
||||||
}
|
}
|
||||||
@ -126,6 +132,15 @@ 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,8 +21,9 @@ func TestExpenseValidatorCreateRequiresItemName(t *testing.T) {
|
|||||||
Total: 10000,
|
Total: 10000,
|
||||||
Items: []contract.CreateExpenseItemRequest{
|
Items: []contract.CreateExpenseItemRequest{
|
||||||
{
|
{
|
||||||
ChartOfAccountID: uuid.NewString(),
|
ChartOfAccountID: uuid.NewString(),
|
||||||
Amount: 10000,
|
PurchaseCategoryID: uuid.NewString(),
|
||||||
|
Amount: 10000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -45,9 +46,10 @@ func TestExpenseValidatorCreateDoesNotRequireHeaderExpenseName(t *testing.T) {
|
|||||||
Total: 10000,
|
Total: 10000,
|
||||||
Items: []contract.CreateExpenseItemRequest{
|
Items: []contract.CreateExpenseItemRequest{
|
||||||
{
|
{
|
||||||
ChartOfAccountID: uuid.NewString(),
|
ChartOfAccountID: uuid.NewString(),
|
||||||
Item: "Cleaning supplies",
|
PurchaseCategoryID: uuid.NewString(),
|
||||||
Amount: 10000,
|
Item: "Cleaning supplies",
|
||||||
|
Amount: 10000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -71,9 +73,10 @@ func TestExpenseValidatorCreateAllowsValidOptionalStatus(t *testing.T) {
|
|||||||
Total: 10000,
|
Total: 10000,
|
||||||
Items: []contract.CreateExpenseItemRequest{
|
Items: []contract.CreateExpenseItemRequest{
|
||||||
{
|
{
|
||||||
ChartOfAccountID: uuid.NewString(),
|
ChartOfAccountID: uuid.NewString(),
|
||||||
Item: "Cleaning supplies",
|
PurchaseCategoryID: uuid.NewString(),
|
||||||
Amount: 10000,
|
Item: "Cleaning supplies",
|
||||||
|
Amount: 10000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -97,9 +100,10 @@ func TestExpenseValidatorCreateRejectsInvalidStatus(t *testing.T) {
|
|||||||
Total: 10000,
|
Total: 10000,
|
||||||
Items: []contract.CreateExpenseItemRequest{
|
Items: []contract.CreateExpenseItemRequest{
|
||||||
{
|
{
|
||||||
ChartOfAccountID: uuid.NewString(),
|
ChartOfAccountID: uuid.NewString(),
|
||||||
Item: "Cleaning supplies",
|
PurchaseCategoryID: uuid.NewString(),
|
||||||
Amount: 10000,
|
Item: "Cleaning supplies",
|
||||||
|
Amount: 10000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -114,10 +118,11 @@ 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{
|
||||||
{Item: &empty},
|
{PurchaseCategoryID: &purchaseCategoryID, Item: &empty},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -119,4 +119,3 @@ func (v *IngredientUnitConverterValidatorImpl) ValidateConvertUnitRequest(req *c
|
|||||||
|
|
||||||
return nil, ""
|
return nil, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
103
internal/validator/purchase_category_validator.go
Normal file
103
internal/validator/purchase_category_validator.go
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
package validator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"apskel-pos-be/internal/constants"
|
||||||
|
"apskel-pos-be/internal/contract"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PurchaseCategoryValidator interface {
|
||||||
|
ValidateCreatePurchaseCategoryRequest(req *contract.CreatePurchaseCategoryRequest) (error, string)
|
||||||
|
ValidateUpdatePurchaseCategoryRequest(req *contract.UpdatePurchaseCategoryRequest) (error, string)
|
||||||
|
ValidateListPurchaseCategoriesRequest(req *contract.ListPurchaseCategoriesRequest) (error, string)
|
||||||
|
}
|
||||||
|
|
||||||
|
type PurchaseCategoryValidatorImpl struct{}
|
||||||
|
|
||||||
|
func NewPurchaseCategoryValidator() *PurchaseCategoryValidatorImpl {
|
||||||
|
return &PurchaseCategoryValidatorImpl{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *PurchaseCategoryValidatorImpl) ValidateCreatePurchaseCategoryRequest(req *contract.CreatePurchaseCategoryRequest) (error, string) {
|
||||||
|
if req == nil {
|
||||||
|
return errors.New("request body is required"), constants.MissingFieldErrorCode
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.TrimSpace(req.Name) == "" {
|
||||||
|
return errors.New("name is required"), constants.MissingFieldErrorCode
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Name) > 255 {
|
||||||
|
return errors.New("name cannot exceed 255 characters"), constants.MalformedFieldErrorCode
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isValidPurchaseCategoryType(req.Type) {
|
||||||
|
return errors.New("type must be raw_material or expense"), constants.MalformedFieldErrorCode
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Code != nil && len(strings.TrimSpace(*req.Code)) > 100 {
|
||||||
|
return errors.New("code cannot exceed 100 characters"), constants.MalformedFieldErrorCode
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *PurchaseCategoryValidatorImpl) ValidateUpdatePurchaseCategoryRequest(req *contract.UpdatePurchaseCategoryRequest) (error, string) {
|
||||||
|
if req == nil {
|
||||||
|
return errors.New("request body is required"), constants.MissingFieldErrorCode
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ParentID == nil && req.Code == nil && req.Name == nil && req.Type == nil && req.SortOrder == nil && req.IsActive == nil {
|
||||||
|
return errors.New("at least one field must be provided for update"), constants.MissingFieldErrorCode
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Name != nil {
|
||||||
|
if strings.TrimSpace(*req.Name) == "" {
|
||||||
|
return errors.New("name cannot be empty"), constants.MalformedFieldErrorCode
|
||||||
|
}
|
||||||
|
if len(*req.Name) > 255 {
|
||||||
|
return errors.New("name cannot exceed 255 characters"), constants.MalformedFieldErrorCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Type != nil && !isValidPurchaseCategoryType(*req.Type) {
|
||||||
|
return errors.New("type must be raw_material or expense"), constants.MalformedFieldErrorCode
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Code != nil && len(strings.TrimSpace(*req.Code)) > 100 {
|
||||||
|
return errors.New("code cannot exceed 100 characters"), constants.MalformedFieldErrorCode
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *PurchaseCategoryValidatorImpl) ValidateListPurchaseCategoriesRequest(req *contract.ListPurchaseCategoriesRequest) (error, string) {
|
||||||
|
if req == nil {
|
||||||
|
return errors.New("request is required"), constants.MissingFieldErrorCode
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Page < 1 {
|
||||||
|
return errors.New("page must be at least 1"), constants.MalformedFieldErrorCode
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Limit < 1 || req.Limit > 100 {
|
||||||
|
return errors.New("limit must be between 1 and 100"), constants.MalformedFieldErrorCode
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Type != "" && !isValidPurchaseCategoryType(req.Type) {
|
||||||
|
return errors.New("type must be raw_material or expense"), constants.MalformedFieldErrorCode
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValidPurchaseCategoryType(categoryType string) bool {
|
||||||
|
switch categoryType {
|
||||||
|
case "raw_material", "expense":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,11 +2,14 @@ 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 {
|
||||||
@ -26,7 +29,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.String() == "" {
|
if req.VendorID == uuid.Nil {
|
||||||
return errors.New("vendor_id is required"), constants.MissingFieldErrorCode
|
return errors.New("vendor_id is required"), constants.MissingFieldErrorCode
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,32 +181,48 @@ 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.IngredientID.String() == "" {
|
if item.PurchaseCategoryID == uuid.Nil {
|
||||||
return errors.New("items[" + string(rune(index)) + "].ingredient_id is required"), constants.MissingFieldErrorCode
|
return errors.New("items[" + strconv.Itoa(index) + "].purchase_category_id is required"), constants.MissingFieldErrorCode
|
||||||
}
|
}
|
||||||
|
|
||||||
if item.Quantity <= 0 {
|
if item.IngredientID != nil && *item.IngredientID == uuid.Nil {
|
||||||
return errors.New("items[" + string(rune(index)) + "].quantity must be greater than 0"), constants.MalformedFieldErrorCode
|
return errors.New("items[" + strconv.Itoa(index) + "].ingredient_id cannot be empty"), constants.MalformedFieldErrorCode
|
||||||
}
|
}
|
||||||
|
|
||||||
if item.UnitID.String() == "" {
|
if item.Quantity != nil && *item.Quantity <= 0 {
|
||||||
return errors.New("items[" + string(rune(index)) + "].unit_id is required"), constants.MissingFieldErrorCode
|
return errors.New("items[" + strconv.Itoa(index) + "].quantity must be greater than 0"), 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.Amount < 0 {
|
if item.Amount < 0 {
|
||||||
return errors.New("items[" + string(rune(index)) + "].amount must be greater than or equal to 0"), constants.MalformedFieldErrorCode
|
return errors.New("items[" + strconv.Itoa(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[" + string(rune(index)) + "].quantity must be greater than 0"), constants.MalformedFieldErrorCode
|
return errors.New("items[" + strconv.Itoa(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[" + string(rune(index)) + "].amount must be greater than or equal to 0"), constants.MalformedFieldErrorCode
|
return errors.New("items[" + strconv.Itoa(index) + "].amount must be greater than or equal to 0"), constants.MalformedFieldErrorCode
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, ""
|
return nil, ""
|
||||||
|
|||||||
@ -11,16 +11,21 @@ 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: uuid.New(),
|
IngredientID: &ingredientID,
|
||||||
Quantity: 1,
|
PurchaseCategoryID: uuid.New(),
|
||||||
UnitID: uuid.New(),
|
Quantity: &quantity,
|
||||||
Amount: 1000,
|
UnitID: &unitID,
|
||||||
|
Amount: 1000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
2
migrations/000075_create_purchase_categories.down.sql
Normal file
2
migrations/000075_create_purchase_categories.down.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
DROP TABLE IF EXISTS purchase_categories;
|
||||||
|
DROP TABLE IF EXISTS purchase_category_presets;
|
||||||
60
migrations/000075_create_purchase_categories.up.sql
Normal file
60
migrations/000075_create_purchase_categories.up.sql
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
CREATE TABLE purchase_category_presets (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
parent_id UUID REFERENCES purchase_category_presets(id) ON DELETE SET NULL,
|
||||||
|
code VARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
type VARCHAR(20) NOT NULL CHECK (type IN ('raw_material', 'expense')),
|
||||||
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE purchase_categories (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||||
|
preset_id UUID REFERENCES purchase_category_presets(id) ON DELETE SET NULL,
|
||||||
|
parent_id UUID REFERENCES purchase_categories(id) ON DELETE SET NULL,
|
||||||
|
code VARCHAR(100) NOT NULL,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
type VARCHAR(20) NOT NULL CHECK (type IN ('raw_material', 'expense')),
|
||||||
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||||
|
is_system BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
UNIQUE (organization_id, code)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_purchase_category_presets_parent_id ON purchase_category_presets(parent_id);
|
||||||
|
CREATE INDEX idx_purchase_category_presets_type ON purchase_category_presets(type);
|
||||||
|
CREATE INDEX idx_purchase_category_presets_is_active ON purchase_category_presets(is_active);
|
||||||
|
|
||||||
|
CREATE INDEX idx_purchase_categories_organization_id ON purchase_categories(organization_id);
|
||||||
|
CREATE INDEX idx_purchase_categories_preset_id ON purchase_categories(preset_id);
|
||||||
|
CREATE INDEX idx_purchase_categories_parent_id ON purchase_categories(parent_id);
|
||||||
|
CREATE INDEX idx_purchase_categories_type ON purchase_categories(type);
|
||||||
|
CREATE INDEX idx_purchase_categories_is_active ON purchase_categories(is_active);
|
||||||
|
|
||||||
|
INSERT INTO purchase_category_presets (code, name, type, sort_order)
|
||||||
|
VALUES
|
||||||
|
('bahan_baku', 'Bahan Baku', 'raw_material', 1),
|
||||||
|
('biaya_lain_lain', 'Biaya Lain-lain', 'expense', 2)
|
||||||
|
ON CONFLICT (code) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO purchase_category_presets (parent_id, code, name, type, sort_order)
|
||||||
|
SELECT parent.id, child.code, child.name, child.type, child.sort_order
|
||||||
|
FROM purchase_category_presets parent
|
||||||
|
JOIN (
|
||||||
|
VALUES
|
||||||
|
('biaya_lain_lain', 'biaya_atk_perlengkapan', 'ATK & Perlengkapan', 'expense', 1),
|
||||||
|
('biaya_lain_lain', 'biaya_makan_karyawan', 'Makan Karyawan', 'expense', 2),
|
||||||
|
('biaya_lain_lain', 'biaya_bensin_parkir', 'Bensin & Parkir', 'expense', 3),
|
||||||
|
('biaya_lain_lain', 'biaya_kebersihan_keamanan', 'Kebersihan & Keamanan', 'expense', 4),
|
||||||
|
('biaya_lain_lain', 'biaya_gaji_dw', 'Gaji DW', 'expense', 5),
|
||||||
|
('biaya_lain_lain', 'biaya_gaji_staff', 'Gaji Staff', 'expense', 6),
|
||||||
|
('biaya_lain_lain', 'biaya_internet_server', 'Internet & Server', 'expense', 7),
|
||||||
|
('biaya_lain_lain', 'biaya_air_listrik', 'Air & Listrik', 'expense', 8),
|
||||||
|
('biaya_lain_lain', 'biaya_promosi', 'Promosi', 'expense', 9)
|
||||||
|
) AS child(parent_code, code, name, type, sort_order) ON parent.code = child.parent_code
|
||||||
|
ON CONFLICT (code) DO NOTHING;
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
DROP TRIGGER IF EXISTS trigger_create_default_purchase_categories ON organizations;
|
||||||
|
DROP FUNCTION IF EXISTS create_default_purchase_categories();
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
CREATE OR REPLACE FUNCTION create_default_purchase_categories()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO purchase_categories (organization_id, preset_id, parent_id, code, name, type, sort_order, is_system, is_active, created_at, updated_at)
|
||||||
|
SELECT
|
||||||
|
NEW.id,
|
||||||
|
preset.id,
|
||||||
|
NULL,
|
||||||
|
preset.code,
|
||||||
|
preset.name,
|
||||||
|
preset.type,
|
||||||
|
preset.sort_order,
|
||||||
|
true,
|
||||||
|
preset.is_active,
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
FROM purchase_category_presets preset
|
||||||
|
WHERE preset.parent_id IS NULL
|
||||||
|
AND preset.is_active = true
|
||||||
|
ON CONFLICT (organization_id, code) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO purchase_categories (organization_id, preset_id, parent_id, code, name, type, sort_order, is_system, is_active, created_at, updated_at)
|
||||||
|
SELECT
|
||||||
|
NEW.id,
|
||||||
|
child_preset.id,
|
||||||
|
parent_category.id,
|
||||||
|
child_preset.code,
|
||||||
|
child_preset.name,
|
||||||
|
child_preset.type,
|
||||||
|
child_preset.sort_order,
|
||||||
|
true,
|
||||||
|
child_preset.is_active,
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
FROM purchase_category_presets child_preset
|
||||||
|
JOIN purchase_category_presets parent_preset ON child_preset.parent_id = parent_preset.id
|
||||||
|
JOIN purchase_categories parent_category ON parent_category.organization_id = NEW.id
|
||||||
|
AND parent_category.code = parent_preset.code
|
||||||
|
WHERE child_preset.is_active = true
|
||||||
|
ON CONFLICT (organization_id, code) DO NOTHING;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER trigger_create_default_purchase_categories
|
||||||
|
AFTER INSERT ON organizations
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION create_default_purchase_categories();
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
DELETE FROM purchase_categories
|
||||||
|
WHERE is_system = true
|
||||||
|
AND preset_id IS NOT NULL;
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
INSERT INTO purchase_categories (organization_id, preset_id, parent_id, code, name, type, sort_order, is_system, is_active, created_at, updated_at)
|
||||||
|
SELECT
|
||||||
|
org.id,
|
||||||
|
preset.id,
|
||||||
|
NULL,
|
||||||
|
preset.code,
|
||||||
|
preset.name,
|
||||||
|
preset.type,
|
||||||
|
preset.sort_order,
|
||||||
|
true,
|
||||||
|
preset.is_active,
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
FROM organizations org
|
||||||
|
CROSS JOIN purchase_category_presets preset
|
||||||
|
WHERE preset.parent_id IS NULL
|
||||||
|
AND preset.is_active = true
|
||||||
|
ON CONFLICT (organization_id, code) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO purchase_categories (organization_id, preset_id, parent_id, code, name, type, sort_order, is_system, is_active, created_at, updated_at)
|
||||||
|
SELECT
|
||||||
|
org.id,
|
||||||
|
child_preset.id,
|
||||||
|
parent_category.id,
|
||||||
|
child_preset.code,
|
||||||
|
child_preset.name,
|
||||||
|
child_preset.type,
|
||||||
|
child_preset.sort_order,
|
||||||
|
true,
|
||||||
|
child_preset.is_active,
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
FROM organizations org
|
||||||
|
JOIN purchase_category_presets child_preset ON child_preset.parent_id IS NOT NULL
|
||||||
|
JOIN purchase_category_presets parent_preset ON child_preset.parent_id = parent_preset.id
|
||||||
|
JOIN purchase_categories parent_category ON parent_category.organization_id = org.id
|
||||||
|
AND parent_category.code = parent_preset.code
|
||||||
|
WHERE child_preset.is_active = true
|
||||||
|
ON CONFLICT (organization_id, code) DO NOTHING;
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
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;
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
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);
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
DROP INDEX IF EXISTS idx_expense_items_purchase_category_id;
|
||||||
|
ALTER TABLE expense_items DROP COLUMN IF EXISTS purchase_category_id;
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
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);
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
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'));
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
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;
|
||||||
Loading…
x
Reference in New Issue
Block a user