Compare commits
No commits in common. "feature/expense" and "main" have entirely different histories.
feature/ex
...
main
@ -108,8 +108,6 @@ func (a *App) Initialize(cfg *config.Config) error {
|
||||
validators.vendorValidator,
|
||||
services.purchaseOrderService,
|
||||
validators.purchaseOrderValidator,
|
||||
services.purchaseCategoryService,
|
||||
validators.purchaseCategoryValidator,
|
||||
services.unitConverterService,
|
||||
validators.unitConverterValidator,
|
||||
services.chartOfAccountTypeService,
|
||||
@ -218,7 +216,6 @@ type repositories struct {
|
||||
productRecipeRepo *repository.ProductRecipeRepository
|
||||
vendorRepo *repository.VendorRepositoryImpl
|
||||
purchaseOrderRepo *repository.PurchaseOrderRepositoryImpl
|
||||
purchaseCategoryRepo *repository.PurchaseCategoryRepositoryImpl
|
||||
unitConverterRepo *repository.IngredientUnitConverterRepositoryImpl
|
||||
chartOfAccountTypeRepo *repository.ChartOfAccountTypeRepositoryImpl
|
||||
chartOfAccountRepo *repository.ChartOfAccountRepositoryImpl
|
||||
@ -272,7 +269,6 @@ func (a *App) initRepositories() *repositories {
|
||||
productRecipeRepo: repository.NewProductRecipeRepository(a.db),
|
||||
vendorRepo: repository.NewVendorRepositoryImpl(a.db),
|
||||
purchaseOrderRepo: repository.NewPurchaseOrderRepositoryImpl(a.db),
|
||||
purchaseCategoryRepo: repository.NewPurchaseCategoryRepositoryImpl(a.db),
|
||||
unitConverterRepo: repository.NewIngredientUnitConverterRepositoryImpl(a.db).(*repository.IngredientUnitConverterRepositoryImpl),
|
||||
chartOfAccountTypeRepo: repository.NewChartOfAccountTypeRepositoryImpl(a.db),
|
||||
chartOfAccountRepo: repository.NewChartOfAccountRepositoryImpl(a.db),
|
||||
@ -321,7 +317,6 @@ type processors struct {
|
||||
productRecipeProcessor *processor.ProductRecipeProcessorImpl
|
||||
vendorProcessor *processor.VendorProcessorImpl
|
||||
purchaseOrderProcessor *processor.PurchaseOrderProcessorImpl
|
||||
purchaseCategoryProcessor *processor.PurchaseCategoryProcessorImpl
|
||||
unitConverterProcessor *processor.IngredientUnitConverterProcessorImpl
|
||||
chartOfAccountTypeProcessor *processor.ChartOfAccountTypeProcessorImpl
|
||||
chartOfAccountProcessor *processor.ChartOfAccountProcessorImpl
|
||||
@ -372,8 +367,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
||||
ingredientProcessor: processor.NewIngredientProcessor(repos.ingredientRepo, repos.unitRepo, repos.ingredientCompositionRepo),
|
||||
productRecipeProcessor: processor.NewProductRecipeProcessor(repos.productRecipeRepo, repos.productRepo, repos.ingredientRepo),
|
||||
vendorProcessor: processor.NewVendorProcessorImpl(repos.vendorRepo),
|
||||
purchaseOrderProcessor: processor.NewPurchaseOrderProcessorImpl(repos.purchaseOrderRepo, repos.vendorRepo, repos.ingredientRepo, repos.purchaseCategoryRepo, repos.unitRepo, repos.fileRepo, inventoryMovementService, repos.unitConverterRepo),
|
||||
purchaseCategoryProcessor: processor.NewPurchaseCategoryProcessorImpl(repos.purchaseCategoryRepo),
|
||||
purchaseOrderProcessor: processor.NewPurchaseOrderProcessorImpl(repos.purchaseOrderRepo, repos.vendorRepo, repos.ingredientRepo, repos.unitRepo, repos.fileRepo, inventoryMovementService, repos.unitConverterRepo),
|
||||
unitConverterProcessor: processor.NewIngredientUnitConverterProcessorImpl(repos.unitConverterRepo, repos.ingredientRepo, repos.unitRepo),
|
||||
chartOfAccountTypeProcessor: processor.NewChartOfAccountTypeProcessorImpl(repos.chartOfAccountTypeRepo),
|
||||
chartOfAccountProcessor: processor.NewChartOfAccountProcessorImpl(repos.chartOfAccountRepo, repos.chartOfAccountTypeRepo),
|
||||
@ -396,7 +390,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
||||
userDeviceProcessor: processor.NewUserDeviceProcessorImpl(repos.userDeviceRepo),
|
||||
notificationProcessor: buildNotificationProcessor(cfg, repos),
|
||||
productOutletPriceProcessor: processor.NewProductOutletPriceProcessorImpl(repos.productOutletPriceRepo, repos.productRepo, repos.outletRepo),
|
||||
expenseProcessor: processor.NewExpenseProcessorImpl(repos.expenseRepo, repos.purchaseCategoryRepo),
|
||||
expenseProcessor: processor.NewExpenseProcessorImpl(repos.expenseRepo),
|
||||
}
|
||||
}
|
||||
|
||||
@ -422,7 +416,6 @@ type services struct {
|
||||
productRecipeService *service.ProductRecipeServiceImpl
|
||||
vendorService *service.VendorServiceImpl
|
||||
purchaseOrderService *service.PurchaseOrderServiceImpl
|
||||
purchaseCategoryService service.PurchaseCategoryService
|
||||
unitConverterService *service.IngredientUnitConverterServiceImpl
|
||||
chartOfAccountTypeService service.ChartOfAccountTypeService
|
||||
chartOfAccountService service.ChartOfAccountService
|
||||
@ -462,7 +455,6 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
||||
productRecipeService := service.NewProductRecipeService(processors.productRecipeProcessor)
|
||||
vendorService := service.NewVendorService(processors.vendorProcessor)
|
||||
purchaseOrderService := service.NewPurchaseOrderService(processors.purchaseOrderProcessor)
|
||||
purchaseCategoryService := service.NewPurchaseCategoryService(processors.purchaseCategoryProcessor)
|
||||
unitConverterService := service.NewIngredientUnitConverterService(processors.unitConverterProcessor)
|
||||
chartOfAccountTypeService := service.NewChartOfAccountTypeService(processors.chartOfAccountTypeProcessor)
|
||||
chartOfAccountService := service.NewChartOfAccountService(processors.chartOfAccountProcessor)
|
||||
@ -502,7 +494,6 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
||||
productRecipeService: productRecipeService,
|
||||
vendorService: vendorService,
|
||||
purchaseOrderService: purchaseOrderService,
|
||||
purchaseCategoryService: purchaseCategoryService,
|
||||
unitConverterService: unitConverterService,
|
||||
chartOfAccountTypeService: chartOfAccountTypeService,
|
||||
chartOfAccountService: chartOfAccountService,
|
||||
@ -548,7 +539,6 @@ type validators struct {
|
||||
tableValidator *validator.TableValidator
|
||||
vendorValidator *validator.VendorValidatorImpl
|
||||
purchaseOrderValidator *validator.PurchaseOrderValidatorImpl
|
||||
purchaseCategoryValidator *validator.PurchaseCategoryValidatorImpl
|
||||
unitConverterValidator *validator.IngredientUnitConverterValidatorImpl
|
||||
chartOfAccountTypeValidator *validator.ChartOfAccountTypeValidatorImpl
|
||||
chartOfAccountValidator *validator.ChartOfAccountValidatorImpl
|
||||
@ -580,7 +570,6 @@ func (a *App) initValidators() *validators {
|
||||
tableValidator: validator.NewTableValidator(),
|
||||
vendorValidator: validator.NewVendorValidator(),
|
||||
purchaseOrderValidator: validator.NewPurchaseOrderValidator(),
|
||||
purchaseCategoryValidator: validator.NewPurchaseCategoryValidator(),
|
||||
unitConverterValidator: validator.NewIngredientUnitConverterValidator().(*validator.IngredientUnitConverterValidatorImpl),
|
||||
chartOfAccountTypeValidator: validator.NewChartOfAccountTypeValidator().(*validator.ChartOfAccountTypeValidatorImpl),
|
||||
chartOfAccountValidator: validator.NewChartOfAccountValidator().(*validator.ChartOfAccountValidatorImpl),
|
||||
|
||||
@ -40,7 +40,6 @@ const (
|
||||
OutletServiceEntity = "outlet_service"
|
||||
VendorServiceEntity = "vendor_service"
|
||||
PurchaseOrderServiceEntity = "purchase_order_service"
|
||||
PurchaseCategoryServiceEntity = "purchase_category_service"
|
||||
IngredientUnitConverterServiceEntity = "ingredient_unit_converter_service"
|
||||
IngredientCompositionServiceEntity = "ingredient_composition_service"
|
||||
TableEntity = "table"
|
||||
|
||||
@ -5,12 +5,12 @@ import (
|
||||
)
|
||||
|
||||
type CreateAccountRequest struct {
|
||||
ChartOfAccountID uuid.UUID `json:"chart_of_account_id" validate:"required"`
|
||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||
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"`
|
||||
OpeningBalance float64 `json:"opening_balance"`
|
||||
Description *string `json:"description"`
|
||||
ChartOfAccountID uuid.UUID `json:"chart_of_account_id" validate:"required"`
|
||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||
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"`
|
||||
OpeningBalance float64 `json:"opening_balance"`
|
||||
Description *string `json:"description"`
|
||||
}
|
||||
|
||||
type UpdateAccountRequest struct {
|
||||
@ -24,21 +24,21 @@ type UpdateAccountRequest struct {
|
||||
}
|
||||
|
||||
type AccountResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
||||
Name string `json:"name"`
|
||||
Number string `json:"number"`
|
||||
AccountType string `json:"account_type"`
|
||||
OpeningBalance float64 `json:"opening_balance"`
|
||||
CurrentBalance float64 `json:"current_balance"`
|
||||
Description *string `json:"description"`
|
||||
IsActive bool `json:"is_active"`
|
||||
IsSystem bool `json:"is_system"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
ChartOfAccount *ChartOfAccountResponse `json:"chart_of_account,omitempty"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
||||
Name string `json:"name"`
|
||||
Number string `json:"number"`
|
||||
AccountType string `json:"account_type"`
|
||||
OpeningBalance float64 `json:"opening_balance"`
|
||||
CurrentBalance float64 `json:"current_balance"`
|
||||
Description *string `json:"description"`
|
||||
IsActive bool `json:"is_active"`
|
||||
IsSystem bool `json:"is_system"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
ChartOfAccount *ChartOfAccountResponse `json:"chart_of_account,omitempty"`
|
||||
}
|
||||
|
||||
type ListAccountsRequest struct {
|
||||
|
||||
@ -106,11 +106,7 @@ type PurchasingAnalyticsResponse struct {
|
||||
|
||||
type PurchasingSummary struct {
|
||||
TotalPurchases float64 `json:"total_purchases"`
|
||||
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||
ExpensePurchases float64 `json:"expense_purchases"`
|
||||
TotalPurchaseOrders int64 `json:"total_purchase_orders"`
|
||||
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
||||
ExpenseCount int64 `json:"expense_count"`
|
||||
TotalQuantity float64 `json:"total_quantity"`
|
||||
AveragePurchaseOrderValue float64 `json:"average_purchase_order_value"`
|
||||
TotalIngredients int64 `json:"total_ingredients"`
|
||||
@ -118,16 +114,12 @@ type PurchasingSummary struct {
|
||||
}
|
||||
|
||||
type PurchasingAnalyticsData struct {
|
||||
Date time.Time `json:"date"`
|
||||
Purchases float64 `json:"purchases"`
|
||||
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||
ExpensePurchases float64 `json:"expense_purchases"`
|
||||
PurchaseOrders int64 `json:"purchase_orders"`
|
||||
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
||||
ExpenseCount int64 `json:"expense_count"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
Ingredients int64 `json:"ingredients"`
|
||||
Vendors int64 `json:"vendors"`
|
||||
Date time.Time `json:"date"`
|
||||
Purchases float64 `json:"purchases"`
|
||||
PurchaseOrders int64 `json:"purchase_orders"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
Ingredients int64 `json:"ingredients"`
|
||||
Vendors int64 `json:"vendors"`
|
||||
}
|
||||
|
||||
type PurchasingIngredientData struct {
|
||||
|
||||
@ -19,11 +19,10 @@ type CreateExpenseRequest struct {
|
||||
}
|
||||
|
||||
type CreateExpenseItemRequest struct {
|
||||
ChartOfAccountID string `json:"chart_of_account_id" validate:"required"`
|
||||
PurchaseCategoryID string `json:"purchase_category_id" validate:"required"`
|
||||
Item string `json:"item" validate:"required"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Amount float64 `json:"amount" validate:"required"`
|
||||
ChartOfAccountID string `json:"chart_of_account_id" validate:"required"`
|
||||
Item string `json:"item" validate:"required"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Amount float64 `json:"amount" validate:"required"`
|
||||
}
|
||||
|
||||
type UpdateExpenseRequest struct {
|
||||
@ -40,11 +39,10 @@ type UpdateExpenseRequest struct {
|
||||
}
|
||||
|
||||
type UpdateExpenseItemRequest struct {
|
||||
ChartOfAccountID *string `json:"chart_of_account_id,omitempty"`
|
||||
PurchaseCategoryID *string `json:"purchase_category_id,omitempty"`
|
||||
Item *string `json:"item,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Amount *float64 `json:"amount,omitempty"`
|
||||
ChartOfAccountID *string `json:"chart_of_account_id,omitempty"`
|
||||
Item *string `json:"item,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Amount *float64 `json:"amount,omitempty"`
|
||||
}
|
||||
|
||||
type ExpenseResponse struct {
|
||||
@ -65,19 +63,15 @@ type ExpenseResponse struct {
|
||||
}
|
||||
|
||||
type ExpenseItemResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
ExpenseID uuid.UUID `json:"expense_id"`
|
||||
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
||||
ChartOfAccountName string `json:"chart_of_account_name,omitempty"`
|
||||
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||
PurchaseCategoryName string `json:"purchase_category_name,omitempty"`
|
||||
PurchaseCategoryType string `json:"purchase_category_type,omitempty"`
|
||||
PurchaseCategory *PurchaseCategoryResponse `json:"purchase_category,omitempty"`
|
||||
Item string `json:"item"`
|
||||
Description *string `json:"description"`
|
||||
Amount float64 `json:"amount"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
ExpenseID uuid.UUID `json:"expense_id"`
|
||||
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
||||
ChartOfAccountName string `json:"chart_of_account_name,omitempty"`
|
||||
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 {
|
||||
@ -97,65 +91,3 @@ type ListExpenseResponse struct {
|
||||
Limit int `json:"limit"`
|
||||
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,3 +81,4 @@ type IngredientUnitsResponse struct {
|
||||
BaseUnitName string `json:"base_unit_name"`
|
||||
Units []*UnitResponse `json:"units"`
|
||||
}
|
||||
|
||||
|
||||
@ -26,9 +26,9 @@ type AdjustInventoryRequest 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"`
|
||||
Reason string `json:"reason" validate:"required,min=1,max=255"`
|
||||
Reason string `json:"reason" validate:"required,min=1,max=255"`
|
||||
}
|
||||
|
||||
type RestockItem struct {
|
||||
@ -82,10 +82,10 @@ type InventoryAdjustmentResponse struct {
|
||||
}
|
||||
|
||||
type RestockInventoryResponse struct {
|
||||
OutletID uuid.UUID `json:"outlet_id"`
|
||||
Items []RestockItemResult `json:"items"`
|
||||
Reason string `json:"reason"`
|
||||
RestockedAt time.Time `json:"restocked_at"`
|
||||
OutletID uuid.UUID `json:"outlet_id"`
|
||||
Items []RestockItemResult `json:"items"`
|
||||
Reason string `json:"reason"`
|
||||
RestockedAt time.Time `json:"restocked_at"`
|
||||
}
|
||||
|
||||
type RestockItemResult struct {
|
||||
|
||||
@ -34,34 +34,34 @@ type BulkCreateProductRecipeRequest struct {
|
||||
|
||||
// Response structures
|
||||
type ProductRecipeResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
ProductID uuid.UUID `json:"product_id"`
|
||||
VariantID *uuid.UUID `json:"variant_id"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
WastePercentage float64 `json:"waste_percentage"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Product *ProductResponse `json:"product,omitempty"`
|
||||
ProductVariant *ProductVariantResponse `json:"product_variant,omitempty"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
ProductID uuid.UUID `json:"product_id"`
|
||||
VariantID *uuid.UUID `json:"variant_id"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
WastePercentage float64 `json:"waste_percentage"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Product *ProductResponse `json:"product,omitempty"`
|
||||
ProductVariant *ProductVariantResponse `json:"product_variant,omitempty"`
|
||||
Ingredient *ProductRecipeIngredientResponse `json:"ingredient,omitempty"`
|
||||
}
|
||||
|
||||
type ProductRecipeIngredientResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
Name string `json:"name"`
|
||||
UnitID uuid.UUID `json:"unit_id"`
|
||||
Cost float64 `json:"cost"`
|
||||
Stock float64 `json:"stock"`
|
||||
IsSemiFinished bool `json:"is_semi_finished"`
|
||||
IsActive bool `json:"is_active"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
Name string `json:"name"`
|
||||
UnitID uuid.UUID `json:"unit_id"`
|
||||
Cost float64 `json:"cost"`
|
||||
Stock float64 `json:"stock"`
|
||||
IsSemiFinished bool `json:"is_semi_finished"`
|
||||
IsActive bool `json:"is_active"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Unit *ProductRecipeUnitResponse `json:"unit,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
@ -1,57 +0,0 @@
|
||||
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,12 +19,11 @@ type CreatePurchaseOrderRequest struct {
|
||||
}
|
||||
|
||||
type CreatePurchaseOrderItemRequest struct {
|
||||
IngredientID *uuid.UUID `json:"ingredient_id,omitempty" validate:"omitempty"`
|
||||
PurchaseCategoryID uuid.UUID `json:"purchase_category_id" validate:"required"`
|
||||
Description *string `json:"description,omitempty" validate:"omitempty"`
|
||||
Quantity *float64 `json:"quantity,omitempty" validate:"omitempty,gt=0"`
|
||||
UnitID *uuid.UUID `json:"unit_id,omitempty" validate:"omitempty"`
|
||||
Amount float64 `json:"amount" validate:"required,gte=0"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id" validate:"required"`
|
||||
Description *string `json:"description,omitempty" validate:"omitempty"`
|
||||
Quantity float64 `json:"quantity" validate:"required,gt=0"`
|
||||
UnitID uuid.UUID `json:"unit_id" validate:"required"`
|
||||
Amount float64 `json:"amount" validate:"required,gte=0"`
|
||||
}
|
||||
|
||||
type UpdatePurchaseOrderRequest struct {
|
||||
@ -40,13 +39,12 @@ type UpdatePurchaseOrderRequest struct {
|
||||
}
|
||||
|
||||
type UpdatePurchaseOrderItemRequest struct {
|
||||
ID *uuid.UUID `json:"id,omitempty"` // For existing items
|
||||
IngredientID *uuid.UUID `json:"ingredient_id,omitempty" validate:"omitempty"`
|
||||
PurchaseCategoryID *uuid.UUID `json:"purchase_category_id,omitempty" validate:"omitempty"`
|
||||
Description *string `json:"description,omitempty" validate:"omitempty"`
|
||||
Quantity *float64 `json:"quantity,omitempty" validate:"omitempty,gt=0"`
|
||||
UnitID *uuid.UUID `json:"unit_id,omitempty" validate:"omitempty"`
|
||||
Amount *float64 `json:"amount,omitempty" validate:"omitempty,gte=0"`
|
||||
ID *uuid.UUID `json:"id,omitempty"` // For existing items
|
||||
IngredientID *uuid.UUID `json:"ingredient_id,omitempty" validate:"omitempty"`
|
||||
Description *string `json:"description,omitempty" validate:"omitempty"`
|
||||
Quantity *float64 `json:"quantity,omitempty" validate:"omitempty,gt=0"`
|
||||
UnitID *uuid.UUID `json:"unit_id,omitempty" validate:"omitempty"`
|
||||
Amount *float64 `json:"amount,omitempty" validate:"omitempty,gte=0"`
|
||||
}
|
||||
|
||||
type PurchaseOrderResponse struct {
|
||||
@ -68,19 +66,17 @@ type PurchaseOrderResponse struct {
|
||||
}
|
||||
|
||||
type PurchaseOrderItemResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
PurchaseOrderID uuid.UUID `json:"purchase_order_id"`
|
||||
IngredientID *uuid.UUID `json:"ingredient_id"`
|
||||
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||
Description *string `json:"description"`
|
||||
Quantity *float64 `json:"quantity"`
|
||||
UnitID *uuid.UUID `json:"unit_id"`
|
||||
Amount float64 `json:"amount"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Ingredient *IngredientResponse `json:"ingredient,omitempty"`
|
||||
PurchaseCategory *PurchaseCategoryResponse `json:"purchase_category,omitempty"`
|
||||
Unit *UnitResponse `json:"unit,omitempty"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
PurchaseOrderID uuid.UUID `json:"purchase_order_id"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
Description *string `json:"description"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
UnitID uuid.UUID `json:"unit_id"`
|
||||
Amount float64 `json:"amount"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Ingredient *IngredientResponse `json:"ingredient,omitempty"`
|
||||
Unit *UnitResponse `json:"unit,omitempty"`
|
||||
}
|
||||
|
||||
type PurchaseOrderAttachmentResponse struct {
|
||||
|
||||
@ -38,11 +38,7 @@ type PurchasingAnalytics struct {
|
||||
|
||||
type PurchasingSummary struct {
|
||||
TotalPurchases float64 `json:"total_purchases"`
|
||||
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||
ExpensePurchases float64 `json:"expense_purchases"`
|
||||
TotalPurchaseOrders int64 `json:"total_purchase_orders"`
|
||||
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
||||
ExpenseCount int64 `json:"expense_count"`
|
||||
TotalQuantity float64 `json:"total_quantity"`
|
||||
AveragePurchaseOrderValue float64 `json:"average_purchase_order_value"`
|
||||
TotalIngredients int64 `json:"total_ingredients"`
|
||||
@ -50,16 +46,12 @@ type PurchasingSummary struct {
|
||||
}
|
||||
|
||||
type PurchasingAnalyticsData struct {
|
||||
Date time.Time `json:"date"`
|
||||
Purchases float64 `json:"purchases"`
|
||||
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||
ExpensePurchases float64 `json:"expense_purchases"`
|
||||
PurchaseOrders int64 `json:"purchase_orders"`
|
||||
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
||||
ExpenseCount int64 `json:"expense_count"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
Ingredients int64 `json:"ingredients"`
|
||||
Vendors int64 `json:"vendors"`
|
||||
Date time.Time `json:"date"`
|
||||
Purchases float64 `json:"purchases"`
|
||||
PurchaseOrders int64 `json:"purchase_orders"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
Ingredients int64 `json:"ingredients"`
|
||||
Vendors int64 `json:"vendors"`
|
||||
}
|
||||
|
||||
type PurchasingIngredientData struct {
|
||||
|
||||
@ -28,56 +28,6 @@ type Expense struct {
|
||||
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 {
|
||||
if e.ID == uuid.Nil {
|
||||
e.ID = uuid.New()
|
||||
|
||||
@ -9,19 +9,17 @@ import (
|
||||
)
|
||||
|
||||
type ExpenseItem struct {
|
||||
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"`
|
||||
ChartOfAccountID uuid.UUID `gorm:"type:uuid;not null;index" json:"chart_of_account_id"`
|
||||
PurchaseCategoryID uuid.UUID `gorm:"type:uuid;not null;index" json:"purchase_category_id"`
|
||||
Item string `gorm:"not null;size:255" json:"item"`
|
||||
Description *string `gorm:"type:text" json:"description"`
|
||||
Amount float64 `gorm:"type:decimal(15,2);not null;default:0" json:"amount"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
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"`
|
||||
ChartOfAccountID uuid.UUID `gorm:"type:uuid;not null;index" json:"chart_of_account_id"`
|
||||
Item string `gorm:"not null;size:255" json:"item"`
|
||||
Description *string `gorm:"type:text" json:"description"`
|
||||
Amount float64 `gorm:"type:decimal(15,2);not null;default:0" json:"amount"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
|
||||
Expense *Expense `gorm:"foreignKey:ExpenseID" json:"expense,omitempty"`
|
||||
ChartOfAccount *ChartOfAccount `gorm:"foreignKey:ChartOfAccountID" json:"chart_of_account,omitempty"`
|
||||
PurchaseCategory *PurchaseCategory `gorm:"foreignKey:PurchaseCategoryID" json:"purchase_category,omitempty"`
|
||||
Expense *Expense `gorm:"foreignKey:ExpenseID" json:"expense,omitempty"`
|
||||
ChartOfAccount *ChartOfAccount `gorm:"foreignKey:ChartOfAccountID" json:"chart_of_account,omitempty"`
|
||||
}
|
||||
|
||||
func (e *ExpenseItem) BeforeCreate(tx *gorm.DB) error {
|
||||
|
||||
@ -39,3 +39,4 @@ func (iuc *IngredientUnitConverter) BeforeCreate() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -36,36 +36,34 @@ const (
|
||||
)
|
||||
|
||||
type InventoryMovement 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" 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"`
|
||||
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"`
|
||||
Quantity float64 `gorm:"type:decimal(12,3);not null" json:"quantity" validate:"required"`
|
||||
PreviousQuantity float64 `gorm:"type:decimal(12,3)" json:"previous_quantity"`
|
||||
NewQuantity float64 `gorm:"type:decimal(12,3)" json:"new_quantity"`
|
||||
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"`
|
||||
ReferenceType *InventoryMovementReferenceType `gorm:"size:50" json:"reference_type"`
|
||||
ReferenceID *uuid.UUID `gorm:"type:uuid;index" json:"reference_id"`
|
||||
PurchaseOrderItemID *uuid.UUID `gorm:"type:uuid;index" json:"purchase_order_item_id"`
|
||||
OrderID *uuid.UUID `gorm:"type:uuid;index" json:"order_id"`
|
||||
PaymentID *uuid.UUID `gorm:"type:uuid;index" json:"payment_id"`
|
||||
UserID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_id" validate:"required"`
|
||||
Reason *string `gorm:"size:255" json:"reason"`
|
||||
Notes *string `gorm:"type:text" json:"notes"`
|
||||
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
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"`
|
||||
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"`
|
||||
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"`
|
||||
Quantity float64 `gorm:"type:decimal(12,3);not null" json:"quantity" validate:"required"`
|
||||
PreviousQuantity float64 `gorm:"type:decimal(12,3)" json:"previous_quantity"`
|
||||
NewQuantity float64 `gorm:"type:decimal(12,3)" json:"new_quantity"`
|
||||
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"`
|
||||
ReferenceType *InventoryMovementReferenceType `gorm:"size:50" json:"reference_type"`
|
||||
ReferenceID *uuid.UUID `gorm:"type:uuid;index" json:"reference_id"`
|
||||
OrderID *uuid.UUID `gorm:"type:uuid;index" json:"order_id"`
|
||||
PaymentID *uuid.UUID `gorm:"type:uuid;index" json:"payment_id"`
|
||||
UserID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_id" validate:"required"`
|
||||
Reason *string `gorm:"size:255" json:"reason"`
|
||||
Notes *string `gorm:"type:text" json:"notes"`
|
||||
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
|
||||
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
||||
Outlet Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
|
||||
Product *Product `gorm:"foreignKey:ItemID" json:"product,omitempty"`
|
||||
Ingredient *Ingredient `gorm:"foreignKey:ItemID" json:"ingredient,omitempty"`
|
||||
PurchaseOrderItem *PurchaseOrderItem `gorm:"foreignKey:PurchaseOrderItemID" json:"purchase_order_item,omitempty"`
|
||||
Order *Order `gorm:"foreignKey:OrderID" json:"order,omitempty"`
|
||||
Payment *Payment `gorm:"foreignKey:PaymentID" json:"payment,omitempty"`
|
||||
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
||||
Outlet Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
|
||||
Product *Product `gorm:"foreignKey:ItemID" json:"product,omitempty"`
|
||||
Ingredient *Ingredient `gorm:"foreignKey:ItemID" json:"ingredient,omitempty"`
|
||||
Order *Order `gorm:"foreignKey:OrderID" json:"order,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 {
|
||||
|
||||
@ -26,14 +26,14 @@ type OrderIngredientTransaction struct {
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
|
||||
// Relations
|
||||
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
||||
Outlet *Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
|
||||
Order Order `gorm:"foreignKey:OrderID" json:"order,omitempty"`
|
||||
OrderItem *OrderItem `gorm:"foreignKey:OrderItemID" json:"order_item,omitempty"`
|
||||
Product Product `gorm:"foreignKey:ProductID" json:"product,omitempty"`
|
||||
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
||||
Outlet *Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
|
||||
Order Order `gorm:"foreignKey:OrderID" json:"order,omitempty"`
|
||||
OrderItem *OrderItem `gorm:"foreignKey:OrderItemID" json:"order_item,omitempty"`
|
||||
Product Product `gorm:"foreignKey:ProductID" json:"product,omitempty"`
|
||||
ProductVariant *ProductVariant `gorm:"foreignKey:ProductVariantID" json:"product_variant,omitempty"`
|
||||
Ingredient Ingredient `gorm:"foreignKey:IngredientID" json:"ingredient,omitempty"`
|
||||
CreatedByUser User `gorm:"foreignKey:CreatedBy" json:"created_by_user,omitempty"`
|
||||
Ingredient Ingredient `gorm:"foreignKey:IngredientID" json:"ingredient,omitempty"`
|
||||
CreatedByUser User `gorm:"foreignKey:CreatedBy" json:"created_by_user,omitempty"`
|
||||
}
|
||||
|
||||
func (oit *OrderIngredientTransaction) BeforeCreate(tx *gorm.DB) error {
|
||||
|
||||
@ -7,15 +7,15 @@ import (
|
||||
)
|
||||
|
||||
type ProductIngredient struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id" db:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id" db:"outlet_id"`
|
||||
ProductID uuid.UUID `json:"product_id" db:"product_id"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id" db:"ingredient_id"`
|
||||
Quantity float64 `json:"quantity" db:"quantity"`
|
||||
WastePercentage float64 `json:"waste_percentage" db:"waste_percentage"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id" db:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id" db:"outlet_id"`
|
||||
ProductID uuid.UUID `json:"product_id" db:"product_id"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id" db:"ingredient_id"`
|
||||
Quantity float64 `json:"quantity" db:"quantity"`
|
||||
WastePercentage float64 `json:"waste_percentage" db:"waste_percentage"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
|
||||
// Relations
|
||||
Product *Product `json:"product,omitempty"`
|
||||
|
||||
@ -1,71 +0,0 @@
|
||||
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,21 +41,19 @@ func (PurchaseOrder) TableName() string {
|
||||
}
|
||||
|
||||
type PurchaseOrderItem struct {
|
||||
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"`
|
||||
IngredientID *uuid.UUID `gorm:"type:uuid" json:"ingredient_id" validate:"omitempty"`
|
||||
PurchaseCategoryID uuid.UUID `gorm:"type:uuid;not null;index" json:"purchase_category_id" validate:"required"`
|
||||
Description *string `gorm:"type:text" json:"description" validate:"omitempty"`
|
||||
Quantity *float64 `gorm:"type:decimal(10,3)" json:"quantity" validate:"omitempty,gt=0"`
|
||||
UnitID *uuid.UUID `gorm:"type:uuid" json:"unit_id" validate:"omitempty"`
|
||||
Amount float64 `gorm:"type:decimal(15,2);not null" json:"amount" validate:"required,gte=0"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
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"`
|
||||
IngredientID uuid.UUID `gorm:"type:uuid;not null" json:"ingredient_id" validate:"required"`
|
||||
Description *string `gorm:"type:text" json:"description" validate:"omitempty"`
|
||||
Quantity float64 `gorm:"type:decimal(10,3);not null" json:"quantity" validate:"required,gt=0"`
|
||||
UnitID uuid.UUID `gorm:"type:uuid;not null" json:"unit_id" validate:"required"`
|
||||
Amount float64 `gorm:"type:decimal(15,2);not null" json:"amount" validate:"required,gte=0"`
|
||||
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"`
|
||||
Ingredient *Ingredient `gorm:"foreignKey:IngredientID" json:"ingredient,omitempty"`
|
||||
PurchaseCategory *PurchaseCategory `gorm:"foreignKey:PurchaseCategoryID" json:"purchase_category,omitempty"`
|
||||
Unit *Unit `gorm:"foreignKey:UnitID" json:"unit,omitempty"`
|
||||
PurchaseOrder *PurchaseOrder `gorm:"foreignKey:PurchaseOrderID" json:"purchase_order,omitempty"`
|
||||
Ingredient *Ingredient `gorm:"foreignKey:IngredientID" json:"ingredient,omitempty"`
|
||||
Unit *Unit `gorm:"foreignKey:UnitID" json:"unit,omitempty"`
|
||||
}
|
||||
|
||||
func (poi *PurchaseOrderItem) BeforeCreate(tx *gorm.DB) error {
|
||||
|
||||
@ -199,31 +199,3 @@ func (h *ExpenseHandler) ListExpenses(c *gin.Context) {
|
||||
|
||||
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,3 +275,4 @@ func (h *IngredientUnitConverterHandler) GetUnitsByIngredientID(c *gin.Context)
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, unitsResponse, "IngredientUnitConverterHandler::GetUnitsByIngredientID")
|
||||
}
|
||||
|
||||
|
||||
@ -1,160 +0,0 @@
|
||||
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,27 +95,20 @@ func ExpenseItemEntityToResponse(entity *entities.ExpenseItem) *models.ExpenseIt
|
||||
}
|
||||
|
||||
response := &models.ExpenseItemResponse{
|
||||
ID: entity.ID,
|
||||
ExpenseID: entity.ExpenseID,
|
||||
ChartOfAccountID: entity.ChartOfAccountID,
|
||||
PurchaseCategoryID: entity.PurchaseCategoryID,
|
||||
Item: entity.Item,
|
||||
Description: entity.Description,
|
||||
Amount: entity.Amount,
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
ID: entity.ID,
|
||||
ExpenseID: entity.ExpenseID,
|
||||
ChartOfAccountID: entity.ChartOfAccountID,
|
||||
Item: entity.Item,
|
||||
Description: entity.Description,
|
||||
Amount: entity.Amount,
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
}
|
||||
|
||||
if entity.ChartOfAccount != nil {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@ -11,17 +11,17 @@ func MapProductIngredientEntityToModel(entity *entities.ProductIngredient) *mode
|
||||
}
|
||||
|
||||
return &models.ProductIngredient{
|
||||
ID: entity.ID,
|
||||
OrganizationID: entity.OrganizationID,
|
||||
OutletID: entity.OutletID,
|
||||
ProductID: entity.ProductID,
|
||||
IngredientID: entity.IngredientID,
|
||||
Quantity: entity.Quantity,
|
||||
WastePercentage: entity.WastePercentage,
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
Product: ProductEntityToModel(entity.Product),
|
||||
Ingredient: MapIngredientEntityToModel(entity.Ingredient),
|
||||
ID: entity.ID,
|
||||
OrganizationID: entity.OrganizationID,
|
||||
OutletID: entity.OutletID,
|
||||
ProductID: entity.ProductID,
|
||||
IngredientID: entity.IngredientID,
|
||||
Quantity: entity.Quantity,
|
||||
WastePercentage: entity.WastePercentage,
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
Product: ProductEntityToModel(entity.Product),
|
||||
Ingredient: MapIngredientEntityToModel(entity.Ingredient),
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,17 +31,17 @@ func MapProductIngredientModelToEntity(model *models.ProductIngredient) *entitie
|
||||
}
|
||||
|
||||
return &entities.ProductIngredient{
|
||||
ID: model.ID,
|
||||
OrganizationID: model.OrganizationID,
|
||||
OutletID: model.OutletID,
|
||||
ProductID: model.ProductID,
|
||||
IngredientID: model.IngredientID,
|
||||
Quantity: model.Quantity,
|
||||
WastePercentage: model.WastePercentage,
|
||||
CreatedAt: model.CreatedAt,
|
||||
UpdatedAt: model.UpdatedAt,
|
||||
Product: ProductModelToEntity(model.Product),
|
||||
Ingredient: MapIngredientModelToEntity(model.Ingredient),
|
||||
ID: model.ID,
|
||||
OrganizationID: model.OrganizationID,
|
||||
OutletID: model.OutletID,
|
||||
ProductID: model.ProductID,
|
||||
IngredientID: model.IngredientID,
|
||||
Quantity: model.Quantity,
|
||||
WastePercentage: model.WastePercentage,
|
||||
CreatedAt: model.CreatedAt,
|
||||
UpdatedAt: model.UpdatedAt,
|
||||
Product: ProductModelToEntity(model.Product),
|
||||
Ingredient: MapIngredientModelToEntity(model.Ingredient),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,53 +0,0 @@
|
||||
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,16 +91,15 @@ func PurchaseOrderItemEntityToModel(entity *entities.PurchaseOrderItem) *models.
|
||||
}
|
||||
|
||||
return &models.PurchaseOrderItem{
|
||||
ID: entity.ID,
|
||||
PurchaseOrderID: entity.PurchaseOrderID,
|
||||
IngredientID: entity.IngredientID,
|
||||
PurchaseCategoryID: entity.PurchaseCategoryID,
|
||||
Description: entity.Description,
|
||||
Quantity: entity.Quantity,
|
||||
UnitID: entity.UnitID,
|
||||
Amount: entity.Amount,
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
ID: entity.ID,
|
||||
PurchaseOrderID: entity.PurchaseOrderID,
|
||||
IngredientID: entity.IngredientID,
|
||||
Description: entity.Description,
|
||||
Quantity: entity.Quantity,
|
||||
UnitID: entity.UnitID,
|
||||
Amount: entity.Amount,
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
@ -110,16 +109,15 @@ func PurchaseOrderItemModelToEntity(model *models.PurchaseOrderItem) *entities.P
|
||||
}
|
||||
|
||||
return &entities.PurchaseOrderItem{
|
||||
ID: model.ID,
|
||||
PurchaseOrderID: model.PurchaseOrderID,
|
||||
IngredientID: model.IngredientID,
|
||||
PurchaseCategoryID: model.PurchaseCategoryID,
|
||||
Description: model.Description,
|
||||
Quantity: model.Quantity,
|
||||
UnitID: model.UnitID,
|
||||
Amount: model.Amount,
|
||||
CreatedAt: model.CreatedAt,
|
||||
UpdatedAt: model.UpdatedAt,
|
||||
ID: model.ID,
|
||||
PurchaseOrderID: model.PurchaseOrderID,
|
||||
IngredientID: model.IngredientID,
|
||||
Description: model.Description,
|
||||
Quantity: model.Quantity,
|
||||
UnitID: model.UnitID,
|
||||
Amount: model.Amount,
|
||||
CreatedAt: model.CreatedAt,
|
||||
UpdatedAt: model.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
@ -129,16 +127,15 @@ func PurchaseOrderItemEntityToResponse(entity *entities.PurchaseOrderItem) *mode
|
||||
}
|
||||
|
||||
response := &models.PurchaseOrderItemResponse{
|
||||
ID: entity.ID,
|
||||
PurchaseOrderID: entity.PurchaseOrderID,
|
||||
IngredientID: entity.IngredientID,
|
||||
PurchaseCategoryID: entity.PurchaseCategoryID,
|
||||
Description: entity.Description,
|
||||
Quantity: entity.Quantity,
|
||||
UnitID: entity.UnitID,
|
||||
Amount: entity.Amount,
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
ID: entity.ID,
|
||||
PurchaseOrderID: entity.PurchaseOrderID,
|
||||
IngredientID: entity.IngredientID,
|
||||
Description: entity.Description,
|
||||
Quantity: entity.Quantity,
|
||||
UnitID: entity.UnitID,
|
||||
Amount: entity.Amount,
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
}
|
||||
|
||||
// Map ingredient if present
|
||||
@ -149,10 +146,6 @@ func PurchaseOrderItemEntityToResponse(entity *entities.PurchaseOrderItem) *mode
|
||||
}
|
||||
}
|
||||
|
||||
if entity.PurchaseCategory != nil {
|
||||
response.PurchaseCategory = PurchaseCategoryEntityToResponse(entity.PurchaseCategory)
|
||||
}
|
||||
|
||||
// Map unit if present
|
||||
if entity.Unit != nil {
|
||||
response.Unit = &models.UnitResponse{
|
||||
|
||||
@ -25,12 +25,12 @@ type AccountResponse struct {
|
||||
}
|
||||
|
||||
type CreateAccountRequest struct {
|
||||
ChartOfAccountID uuid.UUID `json:"chart_of_account_id" validate:"required"`
|
||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||
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"`
|
||||
OpeningBalance float64 `json:"opening_balance"`
|
||||
Description *string `json:"description"`
|
||||
ChartOfAccountID uuid.UUID `json:"chart_of_account_id" validate:"required"`
|
||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||
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"`
|
||||
OpeningBalance float64 `json:"opening_balance"`
|
||||
Description *string `json:"description"`
|
||||
}
|
||||
|
||||
type UpdateAccountRequest struct {
|
||||
|
||||
@ -113,11 +113,7 @@ type PurchasingAnalyticsResponse struct {
|
||||
// PurchasingSummary represents the summary of purchasing analytics
|
||||
type PurchasingSummary struct {
|
||||
TotalPurchases float64 `json:"total_purchases"`
|
||||
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||
ExpensePurchases float64 `json:"expense_purchases"`
|
||||
TotalPurchaseOrders int64 `json:"total_purchase_orders"`
|
||||
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
||||
ExpenseCount int64 `json:"expense_count"`
|
||||
TotalQuantity float64 `json:"total_quantity"`
|
||||
AveragePurchaseOrderValue float64 `json:"average_purchase_order_value"`
|
||||
TotalIngredients int64 `json:"total_ingredients"`
|
||||
@ -126,16 +122,12 @@ type PurchasingSummary struct {
|
||||
|
||||
// PurchasingAnalyticsData represents purchasing analytics by time period
|
||||
type PurchasingAnalyticsData struct {
|
||||
Date time.Time `json:"date"`
|
||||
Purchases float64 `json:"purchases"`
|
||||
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||
ExpensePurchases float64 `json:"expense_purchases"`
|
||||
PurchaseOrders int64 `json:"purchase_orders"`
|
||||
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
||||
ExpenseCount int64 `json:"expense_count"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
Ingredients int64 `json:"ingredients"`
|
||||
Vendors int64 `json:"vendors"`
|
||||
Date time.Time `json:"date"`
|
||||
Purchases float64 `json:"purchases"`
|
||||
PurchaseOrders int64 `json:"purchase_orders"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
Ingredients int64 `json:"ingredients"`
|
||||
Vendors int64 `json:"vendors"`
|
||||
}
|
||||
|
||||
// PurchasingIngredientData represents purchasing analytics for an ingredient
|
||||
|
||||
@ -23,17 +23,17 @@ type UpdateCustomerRequest struct {
|
||||
}
|
||||
|
||||
type CustomerResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
Name string `json:"name"`
|
||||
Email *string `json:"email,omitempty"`
|
||||
Phone *string `json:"phone,omitempty"`
|
||||
Address *string `json:"address,omitempty"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
IsActive bool `json:"is_active"`
|
||||
Metadata entities.Metadata `json:"metadata"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
Name string `json:"name"`
|
||||
Email *string `json:"email,omitempty"`
|
||||
Phone *string `json:"phone,omitempty"`
|
||||
Address *string `json:"address,omitempty"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
IsActive bool `json:"is_active"`
|
||||
Metadata entities.Metadata `json:"metadata"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// ListCustomersQuery represents query parameters for listing customers
|
||||
|
||||
@ -23,15 +23,14 @@ type Expense struct {
|
||||
}
|
||||
|
||||
type ExpenseItem struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
ExpenseID uuid.UUID `json:"expense_id"`
|
||||
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
||||
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||
Item string `json:"item"`
|
||||
Description *string `json:"description"`
|
||||
Amount float64 `json:"amount"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
ExpenseID uuid.UUID `json:"expense_id"`
|
||||
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
||||
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 ExpenseResponse struct {
|
||||
@ -52,19 +51,15 @@ type ExpenseResponse struct {
|
||||
}
|
||||
|
||||
type ExpenseItemResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
ExpenseID uuid.UUID `json:"expense_id"`
|
||||
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
||||
ChartOfAccountName string `json:"chart_of_account_name,omitempty"`
|
||||
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||
PurchaseCategoryName string `json:"purchase_category_name,omitempty"`
|
||||
PurchaseCategoryType string `json:"purchase_category_type,omitempty"`
|
||||
PurchaseCategory *PurchaseCategoryResponse `json:"purchase_category,omitempty"`
|
||||
Item string `json:"item"`
|
||||
Description *string `json:"description"`
|
||||
Amount float64 `json:"amount"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
ExpenseID uuid.UUID `json:"expense_id"`
|
||||
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
||||
ChartOfAccountName string `json:"chart_of_account_name,omitempty"`
|
||||
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 {
|
||||
@ -80,11 +75,10 @@ type CreateExpenseRequest struct {
|
||||
}
|
||||
|
||||
type CreateExpenseItemRequest struct {
|
||||
ChartOfAccountID string `json:"chart_of_account_id"`
|
||||
PurchaseCategoryID string `json:"purchase_category_id"`
|
||||
Item string `json:"item"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Amount float64 `json:"amount"`
|
||||
ChartOfAccountID string `json:"chart_of_account_id"`
|
||||
Item string `json:"item"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Amount float64 `json:"amount"`
|
||||
}
|
||||
|
||||
type UpdateExpenseRequest struct {
|
||||
@ -101,11 +95,10 @@ type UpdateExpenseRequest struct {
|
||||
}
|
||||
|
||||
type UpdateExpenseItemRequest struct {
|
||||
ChartOfAccountID *string `json:"chart_of_account_id,omitempty"`
|
||||
PurchaseCategoryID *string `json:"purchase_category_id,omitempty"`
|
||||
Item *string `json:"item,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Amount *float64 `json:"amount,omitempty"`
|
||||
ChartOfAccountID *string `json:"chart_of_account_id,omitempty"`
|
||||
Item *string `json:"item,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Amount *float64 `json:"amount,omitempty"`
|
||||
}
|
||||
|
||||
type ListExpenseRequest struct {
|
||||
@ -125,66 +118,3 @@ type ListExpenseResponse struct {
|
||||
Limit int `json:"limit"`
|
||||
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,3 +101,4 @@ type IngredientUnitsResponse struct {
|
||||
BaseUnitName string `json:"base_unit_name"`
|
||||
Units []*UnitResponse `json:"units"`
|
||||
}
|
||||
|
||||
|
||||
@ -52,8 +52,8 @@ type UpdateOrderIngredientTransactionRequest struct {
|
||||
GrossQty *float64 `json:"gross_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"`
|
||||
Unit *string `json:"unit,omitempty" validate:"omitempty,max=50"`
|
||||
TransactionDate *time.Time `json:"transaction_date,omitempty"`
|
||||
Unit *string `json:"unit,omitempty" validate:"omitempty,max=50"`
|
||||
TransactionDate *time.Time `json:"transaction_date,omitempty"`
|
||||
}
|
||||
|
||||
type OrderIngredientTransactionResponse struct {
|
||||
@ -98,11 +98,11 @@ type ListOrderIngredientTransactionsRequest struct {
|
||||
}
|
||||
|
||||
type OrderIngredientTransactionSummary struct {
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
IngredientName string `json:"ingredient_name"`
|
||||
TotalGrossQty float64 `json:"total_gross_qty"`
|
||||
TotalNetQty float64 `json:"total_net_qty"`
|
||||
TotalWasteQty float64 `json:"total_waste_qty"`
|
||||
WastePercentage float64 `json:"waste_percentage"`
|
||||
Unit string `json:"unit"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
IngredientName string `json:"ingredient_name"`
|
||||
TotalGrossQty float64 `json:"total_gross_qty"`
|
||||
TotalNetQty float64 `json:"total_net_qty"`
|
||||
TotalWasteQty float64 `json:"total_waste_qty"`
|
||||
WastePercentage float64 `json:"waste_percentage"`
|
||||
Unit string `json:"unit"`
|
||||
}
|
||||
|
||||
@ -7,15 +7,15 @@ import (
|
||||
)
|
||||
|
||||
type ProductIngredient struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
ProductID uuid.UUID `json:"product_id"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
WastePercentage float64 `json:"waste_percentage"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
ProductID uuid.UUID `json:"product_id"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
WastePercentage float64 `json:"waste_percentage"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
// Relations
|
||||
Product *Product `json:"product,omitempty"`
|
||||
@ -37,15 +37,15 @@ type UpdateProductIngredientRequest struct {
|
||||
}
|
||||
|
||||
type ProductIngredientResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
ProductID uuid.UUID `json:"product_id"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
WastePercentage float64 `json:"waste_percentage"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
ProductID uuid.UUID `json:"product_id"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
WastePercentage float64 `json:"waste_percentage"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
// Relations
|
||||
Product *Product `json:"product,omitempty"`
|
||||
|
||||
@ -1,51 +0,0 @@
|
||||
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,16 +22,15 @@ type PurchaseOrder struct {
|
||||
}
|
||||
|
||||
type PurchaseOrderItem struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
PurchaseOrderID uuid.UUID `json:"purchase_order_id"`
|
||||
IngredientID *uuid.UUID `json:"ingredient_id"`
|
||||
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||
Description *string `json:"description"`
|
||||
Quantity *float64 `json:"quantity"`
|
||||
UnitID *uuid.UUID `json:"unit_id"`
|
||||
Amount float64 `json:"amount"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
PurchaseOrderID uuid.UUID `json:"purchase_order_id"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
Description *string `json:"description"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
UnitID uuid.UUID `json:"unit_id"`
|
||||
Amount float64 `json:"amount"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type PurchaseOrderAttachment struct {
|
||||
@ -60,19 +59,17 @@ type PurchaseOrderResponse struct {
|
||||
}
|
||||
|
||||
type PurchaseOrderItemResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
PurchaseOrderID uuid.UUID `json:"purchase_order_id"`
|
||||
IngredientID *uuid.UUID `json:"ingredient_id"`
|
||||
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||
Description *string `json:"description"`
|
||||
Quantity *float64 `json:"quantity"`
|
||||
UnitID *uuid.UUID `json:"unit_id"`
|
||||
Amount float64 `json:"amount"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Ingredient *IngredientResponse `json:"ingredient,omitempty"`
|
||||
PurchaseCategory *PurchaseCategoryResponse `json:"purchase_category,omitempty"`
|
||||
Unit *UnitResponse `json:"unit,omitempty"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
PurchaseOrderID uuid.UUID `json:"purchase_order_id"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
Description *string `json:"description"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
UnitID uuid.UUID `json:"unit_id"`
|
||||
Amount float64 `json:"amount"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Ingredient *IngredientResponse `json:"ingredient,omitempty"`
|
||||
Unit *UnitResponse `json:"unit,omitempty"`
|
||||
}
|
||||
|
||||
type PurchaseOrderAttachmentResponse struct {
|
||||
@ -96,12 +93,11 @@ type CreatePurchaseOrderRequest struct {
|
||||
}
|
||||
|
||||
type CreatePurchaseOrderItemRequest struct {
|
||||
IngredientID *uuid.UUID `json:"ingredient_id,omitempty"`
|
||||
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Quantity *float64 `json:"quantity,omitempty"`
|
||||
UnitID *uuid.UUID `json:"unit_id,omitempty"`
|
||||
Amount float64 `json:"amount"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
UnitID uuid.UUID `json:"unit_id"`
|
||||
Amount float64 `json:"amount"`
|
||||
}
|
||||
|
||||
type UpdatePurchaseOrderRequest struct {
|
||||
@ -117,13 +113,12 @@ type UpdatePurchaseOrderRequest struct {
|
||||
}
|
||||
|
||||
type UpdatePurchaseOrderItemRequest struct {
|
||||
ID *uuid.UUID `json:"id,omitempty"` // For existing items
|
||||
IngredientID *uuid.UUID `json:"ingredient_id,omitempty"`
|
||||
PurchaseCategoryID *uuid.UUID `json:"purchase_category_id,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Quantity *float64 `json:"quantity,omitempty"`
|
||||
UnitID *uuid.UUID `json:"unit_id,omitempty"`
|
||||
Amount *float64 `json:"amount,omitempty"`
|
||||
ID *uuid.UUID `json:"id,omitempty"` // For existing items
|
||||
IngredientID *uuid.UUID `json:"ingredient_id,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Quantity *float64 `json:"quantity,omitempty"`
|
||||
UnitID *uuid.UUID `json:"unit_id,omitempty"`
|
||||
Amount *float64 `json:"amount,omitempty"`
|
||||
}
|
||||
|
||||
type ListPurchaseOrdersRequest struct {
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/models"
|
||||
"apskel-pos-be/internal/repository"
|
||||
)
|
||||
@ -185,16 +186,12 @@ func (p *AnalyticsProcessorImpl) GetPurchasingAnalytics(ctx context.Context, req
|
||||
data := make([]models.PurchasingAnalyticsData, len(result.Data))
|
||||
for i, item := range result.Data {
|
||||
data[i] = models.PurchasingAnalyticsData{
|
||||
Date: item.Date,
|
||||
Purchases: item.Purchases,
|
||||
RawMaterialPurchases: item.RawMaterialPurchases,
|
||||
ExpensePurchases: item.ExpensePurchases,
|
||||
PurchaseOrders: item.PurchaseOrders,
|
||||
RawMaterialPurchaseOrders: item.RawMaterialPurchaseOrders,
|
||||
ExpenseCount: item.ExpenseCount,
|
||||
Quantity: item.Quantity,
|
||||
Ingredients: item.Ingredients,
|
||||
Vendors: item.Vendors,
|
||||
Date: item.Date,
|
||||
Purchases: item.Purchases,
|
||||
PurchaseOrders: item.PurchaseOrders,
|
||||
Quantity: item.Quantity,
|
||||
Ingredients: item.Ingredients,
|
||||
Vendors: item.Vendors,
|
||||
}
|
||||
}
|
||||
|
||||
@ -231,11 +228,7 @@ func (p *AnalyticsProcessorImpl) GetPurchasingAnalytics(ctx context.Context, req
|
||||
GroupBy: req.GroupBy,
|
||||
Summary: models.PurchasingSummary{
|
||||
TotalPurchases: result.Summary.TotalPurchases,
|
||||
RawMaterialPurchases: result.Summary.RawMaterialPurchases,
|
||||
ExpensePurchases: result.Summary.ExpensePurchases,
|
||||
TotalPurchaseOrders: result.Summary.TotalPurchaseOrders,
|
||||
RawMaterialPurchaseOrders: result.Summary.RawMaterialPurchaseOrders,
|
||||
ExpenseCount: result.Summary.ExpenseCount,
|
||||
TotalQuantity: result.Summary.TotalQuantity,
|
||||
AveragePurchaseOrderValue: result.Summary.AveragePurchaseOrderValue,
|
||||
TotalIngredients: result.Summary.TotalIngredients,
|
||||
@ -461,46 +454,15 @@ func (p *AnalyticsProcessorImpl) GetProfitLossAnalytics(ctx context.Context, req
|
||||
}
|
||||
}
|
||||
|
||||
type categoryAmount struct {
|
||||
Name string
|
||||
TodayAmt float64
|
||||
MtdAmt float64
|
||||
}
|
||||
todayPromosi := getExpenseAmountByCategory(result.TodayExpenseByCategory, "promosi")
|
||||
todayLainLain := getExpenseAmountByCategory(result.TodayExpenseByCategory, "lain")
|
||||
todayTotalOps := todayPromosi + todayLainLain
|
||||
todayGaji := getExpenseAmountByCategory(result.TodayExpenseByCategory, "gaji")
|
||||
|
||||
categoryMap := make(map[string]*categoryAmount)
|
||||
var categoryOrder []string
|
||||
|
||||
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
|
||||
}
|
||||
mtdPromosi := getExpenseAmountByCategory(result.MtdExpenseByCategory, "promosi")
|
||||
mtdLainLain := getExpenseAmountByCategory(result.MtdExpenseByCategory, "lain")
|
||||
mtdTotalOps := mtdPromosi + mtdLainLain
|
||||
mtdGaji := getExpenseAmountByCategory(result.MtdExpenseByCategory, "gaji")
|
||||
|
||||
todayGrossProfit := result.TodayRevenue - result.TodayCost
|
||||
mtdGrossProfit := result.MtdRevenue - result.MtdCost
|
||||
@ -524,33 +486,6 @@ func (p *AnalyticsProcessorImpl) GetProfitLossAnalytics(ctx context.Context, req
|
||||
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{
|
||||
{
|
||||
ID: "total_omset", Label: "TOTAL OMSET",
|
||||
@ -571,7 +506,23 @@ func (p *AnalyticsProcessorImpl) GetProfitLossAnalytics(ctx context.Context, req
|
||||
ID: "biaya_ops", Label: "BIAYA OPS",
|
||||
TodayNominal: todayTotalOps, TodayPct: todayPct(todayTotalOps),
|
||||
MtdNominal: mtdTotalOps, MtdPct: mtdPct(mtdTotalOps),
|
||||
SubItems: opsSubItems,
|
||||
SubItems: []models.ProfitLossSummaryRow{
|
||||
{
|
||||
ID: "by_promosi", Label: "1. By Promosi",
|
||||
TodayNominal: todayPromosi, TodayPct: todayPct(todayPromosi),
|
||||
MtdNominal: mtdPromosi, MtdPct: mtdPct(mtdPromosi),
|
||||
},
|
||||
{
|
||||
ID: "by_lain_lain", Label: "2. By Lain lain",
|
||||
TodayNominal: todayLainLain, TodayPct: todayPct(todayLainLain),
|
||||
MtdNominal: mtdLainLain, MtdPct: mtdPct(mtdLainLain),
|
||||
},
|
||||
{
|
||||
ID: "total_biaya_ops", Label: "Total Biaya OPS (4.1+4.2)", IsBold: true,
|
||||
TodayNominal: todayTotalOps, TodayPct: todayPct(todayTotalOps),
|
||||
MtdNominal: mtdTotalOps, MtdPct: mtdPct(mtdTotalOps),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "laba_rugi_sblm_gaji", Label: "Laba/Rugi sblm Gaji (3-4)",
|
||||
@ -627,27 +578,11 @@ func (p *AnalyticsProcessorImpl) GetProfitLossAnalytics(ctx context.Context, req
|
||||
}, nil
|
||||
}
|
||||
|
||||
func isSalaryExpenseCategory(name string) bool {
|
||||
name = strings.ToLower(name)
|
||||
return strings.Contains(name, "gaji") || strings.Contains(name, "salary")
|
||||
}
|
||||
|
||||
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, '_')
|
||||
}
|
||||
func getExpenseAmountByCategory(categories []entities.ExpenseCategoryTotal, keyword string) float64 {
|
||||
for _, cat := range categories {
|
||||
if strings.Contains(strings.ToLower(cat.CategoryName), keyword) {
|
||||
return cat.Amount
|
||||
}
|
||||
}
|
||||
return string(result)
|
||||
return 0
|
||||
}
|
||||
|
||||
@ -61,9 +61,6 @@ func (expenseRepositoryStub) Delete(context.Context, uuid.UUID) error {
|
||||
func (expenseRepositoryStub) List(context.Context, uuid.UUID, map[string]interface{}, int, int) ([]*entities.Expense, int64, error) {
|
||||
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) DeleteItemsByExpenseID(context.Context, uuid.UUID) error { return nil }
|
||||
|
||||
@ -75,23 +72,7 @@ func TestAnalyticsProcessorGetPurchasingAnalyticsPassesOutletName(t *testing.T)
|
||||
purchasingResult: &entities.PurchasingAnalytics{
|
||||
OutletName: &outletName,
|
||||
Summary: entities.PurchasingSummary{
|
||||
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,
|
||||
},
|
||||
TotalPurchases: 125,
|
||||
},
|
||||
},
|
||||
}, expenseRepositoryStub{})
|
||||
@ -108,16 +89,7 @@ func TestAnalyticsProcessorGetPurchasingAnalyticsPassesOutletName(t *testing.T)
|
||||
require.Equal(t, &outletID, result.OutletID)
|
||||
require.NotNil(t, 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)
|
||||
require.Equal(t, float64(125), result.Summary.TotalPurchases)
|
||||
}
|
||||
|
||||
func TestAnalyticsProcessorGetProfitLossAnalyticsMapsOverviewAndReportFields(t *testing.T) {
|
||||
@ -193,83 +165,3 @@ func TestAnalyticsProcessorGetProfitLossAnalyticsMapsOverviewAndReportFields(t *
|
||||
require.NotEmpty(t, result.MainSummary)
|
||||
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,18 +19,15 @@ type ExpenseProcessor interface {
|
||||
DeleteExpense(ctx context.Context, id, organizationID uuid.UUID) 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)
|
||||
GetExpenseAnalytics(ctx context.Context, req *models.ExpenseAnalyticsRequest) (*models.ExpenseAnalyticsResponse, error)
|
||||
}
|
||||
|
||||
type ExpenseProcessorImpl struct {
|
||||
expenseRepo ExpenseRepository
|
||||
purchaseCategoryRepo PurchaseCategoryRepository
|
||||
expenseRepo ExpenseRepository
|
||||
}
|
||||
|
||||
func NewExpenseProcessorImpl(expenseRepo ExpenseRepository, purchaseCategoryRepo PurchaseCategoryRepository) *ExpenseProcessorImpl {
|
||||
func NewExpenseProcessorImpl(expenseRepo ExpenseRepository) *ExpenseProcessorImpl {
|
||||
return &ExpenseProcessorImpl{
|
||||
expenseRepo: expenseRepo,
|
||||
purchaseCategoryRepo: purchaseCategoryRepo,
|
||||
expenseRepo: expenseRepo,
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,30 +47,6 @@ func (p *ExpenseProcessorImpl) CreateExpense(ctx context.Context, organizationID
|
||||
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{
|
||||
OrganizationID: organizationID,
|
||||
OutletID: outletID,
|
||||
@ -91,10 +64,21 @@ func (p *ExpenseProcessorImpl) CreateExpense(ctx context.Context, organizationID
|
||||
return nil, fmt.Errorf("failed to create expense: %w", err)
|
||||
}
|
||||
|
||||
for i := range items {
|
||||
items[i].ExpenseID = expenseEntity.ID
|
||||
for _, 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)
|
||||
}
|
||||
|
||||
err = p.expenseRepo.CreateItem(ctx, &items[i])
|
||||
itemEntity := &entities.ExpenseItem{
|
||||
ExpenseID: expenseEntity.ID,
|
||||
ChartOfAccountID: chartOfAccountID,
|
||||
Item: itemReq.Item,
|
||||
Description: itemReq.Description,
|
||||
Amount: itemReq.Amount,
|
||||
}
|
||||
|
||||
err = p.expenseRepo.CreateItem(ctx, itemEntity)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create expense item: %w", err)
|
||||
}
|
||||
@ -150,10 +134,13 @@ func (p *ExpenseProcessorImpl) UpdateExpense(ctx context.Context, id, organizati
|
||||
expenseEntity.Reserved1 = req.Reserved1
|
||||
}
|
||||
|
||||
var items []entities.ExpenseItem
|
||||
if req.Items != nil {
|
||||
items = make([]entities.ExpenseItem, len(req.Items))
|
||||
for i, itemReq := range req.Items {
|
||||
err = p.expenseRepo.DeleteItemsByExpenseID(ctx, expenseEntity.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to delete existing items: %w", err)
|
||||
}
|
||||
|
||||
for _, itemReq := range req.Items {
|
||||
chartOfAccountID := uuid.Nil
|
||||
if itemReq.ChartOfAccountID != nil {
|
||||
chartOfAccountID, err = uuid.Parse(*itemReq.ChartOfAccountID)
|
||||
@ -162,17 +149,6 @@ func (p *ExpenseProcessorImpl) UpdateExpense(ctx context.Context, id, organizati
|
||||
}
|
||||
}
|
||||
|
||||
if itemReq.PurchaseCategoryID == nil {
|
||||
return nil, fmt.Errorf("purchase_category_id is required for item")
|
||||
}
|
||||
purchaseCategoryID, err := uuid.Parse(*itemReq.PurchaseCategoryID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid purchase_category_id for item: %w", err)
|
||||
}
|
||||
if err := p.validateExpensePurchaseCategory(ctx, purchaseCategoryID, organizationID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
amount := 0.0
|
||||
if itemReq.Amount != nil {
|
||||
amount = *itemReq.Amount
|
||||
@ -182,23 +158,15 @@ func (p *ExpenseProcessorImpl) UpdateExpense(ctx context.Context, id, organizati
|
||||
item = *itemReq.Item
|
||||
}
|
||||
|
||||
items[i] = entities.ExpenseItem{
|
||||
ExpenseID: expenseEntity.ID,
|
||||
ChartOfAccountID: chartOfAccountID,
|
||||
PurchaseCategoryID: purchaseCategoryID,
|
||||
Item: item,
|
||||
Description: itemReq.Description,
|
||||
Amount: amount,
|
||||
itemEntity := &entities.ExpenseItem{
|
||||
ExpenseID: expenseEntity.ID,
|
||||
ChartOfAccountID: chartOfAccountID,
|
||||
Item: item,
|
||||
Description: itemReq.Description,
|
||||
Amount: amount,
|
||||
}
|
||||
}
|
||||
|
||||
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])
|
||||
err = p.expenseRepo.CreateItem(ctx, itemEntity)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create expense item: %w", err)
|
||||
}
|
||||
@ -253,100 +221,3 @@ func (p *ExpenseProcessorImpl) ListExpenses(ctx context.Context, organizationID
|
||||
|
||||
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,7 +3,6 @@ package processor
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/models"
|
||||
@ -15,46 +14,6 @@ import (
|
||||
type expenseRepositoryCaptureStub struct {
|
||||
createdExpense *entities.Expense
|
||||
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 {
|
||||
@ -85,9 +44,6 @@ func (*expenseRepositoryCaptureStub) Delete(context.Context, uuid.UUID) error
|
||||
func (*expenseRepositoryCaptureStub) List(context.Context, uuid.UUID, map[string]interface{}, int, int) ([]*entities.Expense, int64, error) {
|
||||
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 {
|
||||
if item.ID == uuid.Nil {
|
||||
item.ID = uuid.New()
|
||||
@ -101,8 +57,7 @@ func (*expenseRepositoryCaptureStub) DeleteItemsByExpenseID(context.Context, uui
|
||||
|
||||
func TestExpenseProcessorCreatePersistsItemName(t *testing.T) {
|
||||
repo := &expenseRepositoryCaptureStub{}
|
||||
purchaseCategoryID := uuid.New()
|
||||
p := NewExpenseProcessorImpl(repo, newExpensePurchaseCategoryRepo(purchaseCategoryID, entities.PurchaseCategoryTypeExpense))
|
||||
p := NewExpenseProcessorImpl(repo)
|
||||
chartOfAccountID := uuid.New()
|
||||
|
||||
resp, err := p.CreateExpense(context.Background(), uuid.New(), &models.CreateExpenseRequest{
|
||||
@ -113,10 +68,9 @@ func TestExpenseProcessorCreatePersistsItemName(t *testing.T) {
|
||||
Total: 10000,
|
||||
Items: []models.CreateExpenseItemRequest{
|
||||
{
|
||||
ChartOfAccountID: chartOfAccountID.String(),
|
||||
PurchaseCategoryID: purchaseCategoryID.String(),
|
||||
Item: "Cleaning supplies",
|
||||
Amount: 10000,
|
||||
ChartOfAccountID: chartOfAccountID.String(),
|
||||
Item: "Cleaning supplies",
|
||||
Amount: 10000,
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -125,15 +79,13 @@ func TestExpenseProcessorCreatePersistsItemName(t *testing.T) {
|
||||
require.NotNil(t, resp)
|
||||
require.Len(t, repo.createdItems, 1)
|
||||
require.Equal(t, "Cleaning supplies", repo.createdItems[0].Item)
|
||||
require.Equal(t, purchaseCategoryID, repo.createdItems[0].PurchaseCategoryID)
|
||||
require.Len(t, resp.Items, 1)
|
||||
require.Equal(t, "Cleaning supplies", resp.Items[0].Item)
|
||||
}
|
||||
|
||||
func TestExpenseProcessorCreateDefaultsStatusToDraft(t *testing.T) {
|
||||
repo := &expenseRepositoryCaptureStub{}
|
||||
purchaseCategoryID := uuid.New()
|
||||
p := NewExpenseProcessorImpl(repo, newExpensePurchaseCategoryRepo(purchaseCategoryID, entities.PurchaseCategoryTypeExpense))
|
||||
p := NewExpenseProcessorImpl(repo)
|
||||
|
||||
resp, err := p.CreateExpense(context.Background(), uuid.New(), &models.CreateExpenseRequest{
|
||||
Receiver: "Cashier",
|
||||
@ -143,10 +95,9 @@ func TestExpenseProcessorCreateDefaultsStatusToDraft(t *testing.T) {
|
||||
Total: 10000,
|
||||
Items: []models.CreateExpenseItemRequest{
|
||||
{
|
||||
ChartOfAccountID: uuid.NewString(),
|
||||
PurchaseCategoryID: purchaseCategoryID.String(),
|
||||
Item: "Cleaning supplies",
|
||||
Amount: 10000,
|
||||
ChartOfAccountID: uuid.NewString(),
|
||||
Item: "Cleaning supplies",
|
||||
Amount: 10000,
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -159,8 +110,7 @@ func TestExpenseProcessorCreateDefaultsStatusToDraft(t *testing.T) {
|
||||
|
||||
func TestExpenseProcessorCreatePersistsProvidedStatus(t *testing.T) {
|
||||
repo := &expenseRepositoryCaptureStub{}
|
||||
purchaseCategoryID := uuid.New()
|
||||
p := NewExpenseProcessorImpl(repo, newExpensePurchaseCategoryRepo(purchaseCategoryID, entities.PurchaseCategoryTypeExpense))
|
||||
p := NewExpenseProcessorImpl(repo)
|
||||
status := "approved"
|
||||
|
||||
resp, err := p.CreateExpense(context.Background(), uuid.New(), &models.CreateExpenseRequest{
|
||||
@ -172,10 +122,9 @@ func TestExpenseProcessorCreatePersistsProvidedStatus(t *testing.T) {
|
||||
Total: 10000,
|
||||
Items: []models.CreateExpenseItemRequest{
|
||||
{
|
||||
ChartOfAccountID: uuid.NewString(),
|
||||
PurchaseCategoryID: purchaseCategoryID.String(),
|
||||
Item: "Cleaning supplies",
|
||||
Amount: 10000,
|
||||
ChartOfAccountID: uuid.NewString(),
|
||||
Item: "Cleaning supplies",
|
||||
Amount: 10000,
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -185,107 +134,3 @@ func TestExpenseProcessorCreatePersistsProvidedStatus(t *testing.T) {
|
||||
require.Equal(t, "approved", repo.createdExpense.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,7 +3,6 @@ package processor
|
||||
import (
|
||||
"apskel-pos-be/internal/entities"
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
@ -15,7 +14,6 @@ type ExpenseRepository interface {
|
||||
Update(ctx context.Context, expense *entities.Expense) 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)
|
||||
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
|
||||
DeleteItemsByExpenseID(ctx context.Context, expenseID uuid.UUID) error
|
||||
}
|
||||
|
||||
@ -86,7 +86,7 @@ type CustomerRepository interface {
|
||||
}
|
||||
|
||||
type InventoryMovementService interface {
|
||||
CreateIngredientMovement(ctx context.Context, ingredientID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID, purchaseOrderItemID *uuid.UUID) error
|
||||
CreateIngredientMovement(ctx context.Context, ingredientID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error
|
||||
CreateProductMovement(ctx context.Context, productID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error
|
||||
}
|
||||
|
||||
|
||||
@ -1,210 +0,0 @@
|
||||
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,7 +25,6 @@ type PurchaseOrderProcessorImpl struct {
|
||||
purchaseOrderRepo PurchaseOrderRepository
|
||||
vendorRepo VendorRepository
|
||||
ingredientRepo IngredientRepository
|
||||
purchaseCategoryRepo PurchaseCategoryRepository
|
||||
unitRepo UnitRepository
|
||||
fileRepo FileRepository
|
||||
inventoryMovementService InventoryMovementService
|
||||
@ -36,7 +35,6 @@ func NewPurchaseOrderProcessorImpl(
|
||||
purchaseOrderRepo PurchaseOrderRepository,
|
||||
vendorRepo VendorRepository,
|
||||
ingredientRepo IngredientRepository,
|
||||
purchaseCategoryRepo PurchaseCategoryRepository,
|
||||
unitRepo UnitRepository,
|
||||
fileRepo FileRepository,
|
||||
inventoryMovementService InventoryMovementService,
|
||||
@ -46,7 +44,6 @@ func NewPurchaseOrderProcessorImpl(
|
||||
purchaseOrderRepo: purchaseOrderRepo,
|
||||
vendorRepo: vendorRepo,
|
||||
ingredientRepo: ingredientRepo,
|
||||
purchaseCategoryRepo: purchaseCategoryRepo,
|
||||
unitRepo: unitRepo,
|
||||
fileRepo: fileRepo,
|
||||
inventoryMovementService: inventoryMovementService,
|
||||
@ -67,40 +64,16 @@ func (p *PurchaseOrderProcessorImpl) CreatePurchaseOrder(ctx context.Context, or
|
||||
return nil, fmt.Errorf("purchase order with PO number %s already exists in this organization", req.PONumber)
|
||||
}
|
||||
|
||||
// Validate categories and inventory fields per item type.
|
||||
// Validate ingredients and units exist
|
||||
for i, item := range req.Items {
|
||||
category, err := p.validatePurchaseCategory(ctx, item.PurchaseCategoryID, organizationID, i)
|
||||
_, err := p.ingredientRepo.GetByID(ctx, item.IngredientID, organizationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("ingredient not found for item %d: %w", i, err)
|
||||
}
|
||||
|
||||
switch category.Type {
|
||||
case entities.PurchaseCategoryTypeRawMaterial:
|
||||
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)
|
||||
_, err = p.unitRepo.GetByID(ctx, item.UnitID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unit not found for item %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -136,13 +109,12 @@ func (p *PurchaseOrderProcessorImpl) CreatePurchaseOrder(ctx context.Context, or
|
||||
// Create purchase order items
|
||||
for _, itemReq := range req.Items {
|
||||
itemEntity := &entities.PurchaseOrderItem{
|
||||
PurchaseOrderID: poEntity.ID,
|
||||
IngredientID: itemReq.IngredientID,
|
||||
PurchaseCategoryID: itemReq.PurchaseCategoryID,
|
||||
Description: itemReq.Description,
|
||||
Quantity: itemReq.Quantity,
|
||||
UnitID: itemReq.UnitID,
|
||||
Amount: itemReq.Amount,
|
||||
PurchaseOrderID: poEntity.ID,
|
||||
IngredientID: itemReq.IngredientID,
|
||||
Description: itemReq.Description,
|
||||
Quantity: itemReq.Quantity,
|
||||
UnitID: itemReq.UnitID,
|
||||
Amount: itemReq.Amount,
|
||||
}
|
||||
|
||||
err = p.purchaseOrderRepo.CreateItem(ctx, itemEntity)
|
||||
@ -217,80 +189,68 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrder(ctx context.Context, id
|
||||
|
||||
// Update items if provided
|
||||
if req.Items != nil {
|
||||
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.
|
||||
// Delete existing items
|
||||
err = p.purchaseOrderRepo.DeleteItemsByPurchaseOrderID(ctx, poEntity.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to delete existing items: %w", err)
|
||||
}
|
||||
|
||||
for _, itemEntity := range items {
|
||||
// Create new items
|
||||
totalAmount := 0.0
|
||||
for _, itemReq := range req.Items {
|
||||
// Validate ingredients and units exist
|
||||
if itemReq.IngredientID != nil {
|
||||
_, err := p.ingredientRepo.GetByID(ctx, *itemReq.IngredientID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ingredient not found: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if itemReq.UnitID != nil {
|
||||
_, err := p.unitRepo.GetByID(ctx, *itemReq.UnitID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unit not found: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Use existing values if not provided
|
||||
ingredientID := poEntity.Items[0].IngredientID // This is a simplified approach
|
||||
unitID := poEntity.Items[0].UnitID
|
||||
quantity := poEntity.Items[0].Quantity
|
||||
amount := poEntity.Items[0].Amount
|
||||
description := poEntity.Items[0].Description
|
||||
|
||||
if itemReq.IngredientID != nil {
|
||||
ingredientID = *itemReq.IngredientID
|
||||
}
|
||||
if itemReq.UnitID != nil {
|
||||
unitID = *itemReq.UnitID
|
||||
}
|
||||
if itemReq.Quantity != nil {
|
||||
quantity = *itemReq.Quantity
|
||||
}
|
||||
if itemReq.Amount != nil {
|
||||
amount = *itemReq.Amount
|
||||
}
|
||||
if itemReq.Description != nil {
|
||||
description = itemReq.Description
|
||||
}
|
||||
|
||||
itemEntity := &entities.PurchaseOrderItem{
|
||||
PurchaseOrderID: poEntity.ID,
|
||||
IngredientID: ingredientID,
|
||||
Description: description,
|
||||
Quantity: quantity,
|
||||
UnitID: unitID,
|
||||
Amount: amount,
|
||||
}
|
||||
|
||||
err = p.purchaseOrderRepo.CreateItem(ctx, itemEntity)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create purchase order item: %w", err)
|
||||
}
|
||||
|
||||
totalAmount += amount
|
||||
}
|
||||
|
||||
poEntity.TotalAmount = totalAmount
|
||||
@ -419,27 +379,19 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrderStatus(ctx context.Conte
|
||||
|
||||
// Update inventory for each item
|
||||
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
|
||||
ingredient, err := p.ingredientRepo.GetByID(ctx, *item.IngredientID, organizationID)
|
||||
ingredient, err := p.ingredientRepo.GetByID(ctx, item.IngredientID, organizationID)
|
||||
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
|
||||
quantityToAdd := *item.Quantity
|
||||
if *item.UnitID != ingredient.UnitID {
|
||||
quantityToAdd := item.Quantity
|
||||
if item.UnitID != ingredient.UnitID {
|
||||
// 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 {
|
||||
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
|
||||
}
|
||||
@ -457,7 +409,7 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrderStatus(ctx context.Conte
|
||||
|
||||
err = p.inventoryMovementService.CreateIngredientMovement(
|
||||
ctx,
|
||||
*item.IngredientID,
|
||||
item.IngredientID,
|
||||
organizationID,
|
||||
outletID,
|
||||
userID,
|
||||
@ -467,10 +419,9 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrderStatus(ctx context.Conte
|
||||
reason,
|
||||
&referenceType,
|
||||
referenceID,
|
||||
&item.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create inventory movement for ingredient %s: %w", *item.IngredientID, err)
|
||||
return nil, fmt.Errorf("failed to create inventory movement for ingredient %s: %w", item.IngredientID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -489,20 +440,3 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrderStatus(ctx context.Conte
|
||||
|
||||
return mappers.PurchaseOrderEntityToResponse(updatedPO), nil
|
||||
}
|
||||
|
||||
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,6 +22,8 @@ const (
|
||||
MetadataKeyLastSplitQuantities = "last_split_quantities"
|
||||
)
|
||||
|
||||
|
||||
|
||||
type SplitBillValidation struct {
|
||||
OrderItems map[uuid.UUID]*entities.OrderItem
|
||||
PaidQuantities map[uuid.UUID]int
|
||||
|
||||
@ -145,179 +145,68 @@ func (r *AnalyticsRepositoryImpl) GetPurchasingAnalytics(ctx context.Context, or
|
||||
}
|
||||
}
|
||||
|
||||
rawMaterialOutletFilter := ""
|
||||
expenseOutletFilter := ""
|
||||
rawMaterialSummaryArgs := []interface{}{
|
||||
organizationID,
|
||||
entities.InventoryMovementTypePurchase,
|
||||
"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,
|
||||
summaryQuery := r.db.WithContext(ctx).
|
||||
Table("inventory_movements im").
|
||||
Select(`
|
||||
COALESCE(SUM(im.total_cost), 0) as total_purchases,
|
||||
COUNT(DISTINCT im.reference_id) as total_purchase_orders,
|
||||
COALESCE(SUM(im.quantity), 0) as total_quantity,
|
||||
CASE
|
||||
WHEN rm.raw_material_purchase_orders + ex.expense_count > 0
|
||||
THEN (rm.raw_material_purchases + ex.expense_purchases) / (rm.raw_material_purchase_orders + ex.expense_count)
|
||||
WHEN COUNT(DISTINCT im.reference_id) > 0
|
||||
THEN COALESCE(SUM(im.total_cost), 0) / COUNT(DISTINCT im.reference_id)
|
||||
ELSE 0
|
||||
END as average_purchase_order_value,
|
||||
rm.total_ingredients,
|
||||
rm.total_vendors
|
||||
FROM raw_material rm
|
||||
CROSS JOIN expense ex
|
||||
`
|
||||
COUNT(DISTINCT im.item_id) as total_ingredients,
|
||||
COUNT(DISTINCT po.vendor_id) as total_vendors
|
||||
`).
|
||||
Joins("LEFT JOIN purchase_orders po ON im.reference_id = po.id").
|
||||
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)
|
||||
|
||||
if err := r.db.WithContext(ctx).Raw(summaryQuery, summaryArgs...).Scan(&summary).Error; err != nil {
|
||||
summaryQuery = r.resolveOutletID(summaryQuery, outletID, "im.outlet_id")
|
||||
|
||||
if err := summaryQuery.Scan(&summary).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var dateFormat string
|
||||
switch groupBy {
|
||||
case "hour":
|
||||
dateFormat = "DATE_TRUNC('hour', im.created_at)::timestamp"
|
||||
dateFormat = "DATE_TRUNC('hour', im.created_at)"
|
||||
case "week":
|
||||
dateFormat = "DATE_TRUNC('week', im.created_at)::timestamp"
|
||||
dateFormat = "DATE_TRUNC('week', im.created_at)"
|
||||
case "month":
|
||||
dateFormat = "DATE_TRUNC('month', im.created_at)::timestamp"
|
||||
dateFormat = "DATE_TRUNC('month', im.created_at)"
|
||||
default:
|
||||
dateFormat = "DATE_TRUNC('day', im.created_at)::timestamp"
|
||||
dateFormat = "DATE_TRUNC('day', im.created_at)"
|
||||
}
|
||||
|
||||
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
|
||||
dataQuery := `
|
||||
WITH raw_material AS (
|
||||
SELECT
|
||||
` + dateFormat + ` as date,
|
||||
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 quantity,
|
||||
COUNT(DISTINCT im.item_id) as ingredients,
|
||||
COUNT(DISTINCT po.vendor_id) as 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 + `
|
||||
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.db.WithContext(ctx).
|
||||
Table("inventory_movements im").
|
||||
Select(`
|
||||
`+dateFormat+` as date,
|
||||
COALESCE(SUM(im.total_cost), 0) as purchases,
|
||||
COUNT(DISTINCT im.reference_id) as purchase_orders,
|
||||
COALESCE(SUM(im.quantity), 0) as quantity,
|
||||
COUNT(DISTINCT im.item_id) as ingredients,
|
||||
COUNT(DISTINCT po.vendor_id) as vendors
|
||||
`).
|
||||
Joins("LEFT JOIN purchase_orders po ON im.reference_id = po.id").
|
||||
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).
|
||||
Group(dateFormat).
|
||||
Order(dateFormat)
|
||||
|
||||
if err := r.db.WithContext(ctx).Raw(dataQuery, dataArgs...).Scan(&data).Error; err != nil {
|
||||
dataQuery = r.resolveOutletID(dataQuery, outletID, "im.outlet_id")
|
||||
|
||||
if err := dataQuery.Scan(&data).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -749,7 +638,7 @@ func (r *AnalyticsRepositoryImpl) getExpenseByCategory(ctx context.Context, orga
|
||||
|
||||
query := r.db.WithContext(ctx).
|
||||
Table("expense_items ei").
|
||||
Select(`COALESCE(parent_coa.name, coa.name, 'Lain-lain') as category_name, COALESCE(SUM(ei.amount), 0) as amount`).
|
||||
Select(`COALESCE(parent_coa.name, 'Lain-lain') as category_name, COALESCE(SUM(ei.amount), 0) as amount`).
|
||||
Joins("JOIN expenses e ON ei.expense_id = e.id").
|
||||
Joins("JOIN chart_of_accounts coa ON ei.chart_of_account_id = coa.id").
|
||||
Joins("LEFT JOIN chart_of_accounts parent_coa ON coa.parent_id = parent_coa.id").
|
||||
@ -762,8 +651,8 @@ func (r *AnalyticsRepositoryImpl) getExpenseByCategory(ctx context.Context, orga
|
||||
}
|
||||
|
||||
err := query.
|
||||
Group("COALESCE(parent_coa.name, coa.name, 'Lain-lain')").
|
||||
Order("COALESCE(parent_coa.name, coa.name, 'Lain-lain')").
|
||||
Group("parent_coa.name").
|
||||
Order("parent_coa.name").
|
||||
Scan(&results).Error
|
||||
|
||||
return results, err
|
||||
|
||||
@ -30,7 +30,6 @@ func (r *ExpenseRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*ent
|
||||
var expense entities.Expense
|
||||
err := r.db.WithContext(ctx).
|
||||
Preload("Items.ChartOfAccount").
|
||||
Preload("Items.PurchaseCategory").
|
||||
First(&expense, "id = ?", id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -42,7 +41,6 @@ func (r *ExpenseRepositoryImpl) GetByIDAndOrganizationID(ctx context.Context, id
|
||||
var expense entities.Expense
|
||||
err := r.db.WithContext(ctx).
|
||||
Preload("Items.ChartOfAccount").
|
||||
Preload("Items.PurchaseCategory").
|
||||
Where("id = ? AND organization_id = ?", id, organizationID).
|
||||
First(&expense).Error
|
||||
if err != nil {
|
||||
@ -109,7 +107,6 @@ func (r *ExpenseRepositoryImpl) List(ctx context.Context, organizationID uuid.UU
|
||||
|
||||
err := query.
|
||||
Preload("Items.ChartOfAccount").
|
||||
Preload("Items.PurchaseCategory").
|
||||
Order("created_at DESC").
|
||||
Limit(limit).
|
||||
Offset(offset).
|
||||
@ -117,165 +114,6 @@ func (r *ExpenseRepositoryImpl) List(ctx context.Context, organizationID uuid.UU
|
||||
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 {
|
||||
return r.db.WithContext(ctx).Create(item).Error
|
||||
}
|
||||
|
||||
@ -173,3 +173,4 @@ func (r *IngredientUnitConverterRepositoryImpl) ConvertQuantity(ctx context.Cont
|
||||
// If no converter found, return error
|
||||
return 0, fmt.Errorf("no conversion found between units %s and %s for ingredient %s", fromUnitID, toUnitID, ingredientID)
|
||||
}
|
||||
|
||||
|
||||
@ -1,91 +0,0 @@
|
||||
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,7 +31,6 @@ func (r *PurchaseOrderRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID)
|
||||
err := r.db.WithContext(ctx).
|
||||
Preload("Vendor").
|
||||
Preload("Items.Ingredient").
|
||||
Preload("Items.PurchaseCategory").
|
||||
Preload("Items.Unit").
|
||||
Preload("Attachments.File").
|
||||
First(&po, "id = ?", id).Error
|
||||
@ -46,7 +45,6 @@ func (r *PurchaseOrderRepositoryImpl) GetByIDAndOrganizationID(ctx context.Conte
|
||||
err := r.db.WithContext(ctx).
|
||||
Preload("Vendor").
|
||||
Preload("Items.Ingredient").
|
||||
Preload("Items.PurchaseCategory").
|
||||
Preload("Items.Unit").
|
||||
Preload("Attachments.File").
|
||||
Where("id = ? AND organization_id = ?", id, organizationID).
|
||||
@ -107,7 +105,6 @@ func (r *PurchaseOrderRepositoryImpl) List(ctx context.Context, organizationID u
|
||||
err := query.
|
||||
Preload("Vendor").
|
||||
Preload("Items.Ingredient").
|
||||
Preload("Items.PurchaseCategory").
|
||||
Preload("Items.Unit").
|
||||
Preload("Attachments.File").
|
||||
Order("created_at DESC").
|
||||
@ -171,7 +168,6 @@ func (r *PurchaseOrderRepositoryImpl) GetByStatus(ctx context.Context, organizat
|
||||
Where("organization_id = ? AND status = ?", organizationID, status).
|
||||
Preload("Vendor").
|
||||
Preload("Items.Ingredient").
|
||||
Preload("Items.PurchaseCategory").
|
||||
Preload("Items.Unit").
|
||||
Find(&pos).Error
|
||||
return pos, err
|
||||
@ -183,7 +179,6 @@ func (r *PurchaseOrderRepositoryImpl) GetOverdue(ctx context.Context, organizati
|
||||
Where("organization_id = ? AND due_date < ? AND status IN (?)", organizationID, time.Now(), []string{"draft", "sent", "approved"}).
|
||||
Preload("Vendor").
|
||||
Preload("Items.Ingredient").
|
||||
Preload("Items.PurchaseCategory").
|
||||
Preload("Items.Unit").
|
||||
Find(&pos).Error
|
||||
return pos, err
|
||||
@ -224,7 +219,6 @@ func (r *PurchaseOrderRepositoryImpl) GetItemsByPurchaseOrderID(ctx context.Cont
|
||||
var items []*entities.PurchaseOrderItem
|
||||
err := r.db.WithContext(ctx).
|
||||
Preload("Ingredient").
|
||||
Preload("PurchaseCategory").
|
||||
Preload("Unit").
|
||||
Where("purchase_order_id = ?", purchaseOrderID).
|
||||
Find(&items).Error
|
||||
|
||||
@ -36,7 +36,6 @@ type Router struct {
|
||||
productRecipeHandler *handler.ProductRecipeHandler
|
||||
vendorHandler *handler.VendorHandler
|
||||
purchaseOrderHandler *handler.PurchaseOrderHandler
|
||||
purchaseCategoryHandler *handler.PurchaseCategoryHandler
|
||||
unitConverterHandler *handler.IngredientUnitConverterHandler
|
||||
chartOfAccountTypeHandler *handler.ChartOfAccountTypeHandler
|
||||
chartOfAccountHandler *handler.ChartOfAccountHandler
|
||||
@ -58,7 +57,7 @@ type Router struct {
|
||||
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, 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 {
|
||||
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 {
|
||||
|
||||
return &Router{
|
||||
config: cfg,
|
||||
@ -83,7 +82,6 @@ func NewRouter(cfg *config.Config, healthHandler *handler.HealthHandler, authSer
|
||||
productRecipeHandler: handler.NewProductRecipeHandler(productRecipeService),
|
||||
vendorHandler: handler.NewVendorHandler(vendorService, vendorValidator),
|
||||
purchaseOrderHandler: handler.NewPurchaseOrderHandler(purchaseOrderService, purchaseOrderValidator),
|
||||
purchaseCategoryHandler: handler.NewPurchaseCategoryHandler(purchaseCategoryService, purchaseCategoryValidator),
|
||||
unitConverterHandler: handler.NewIngredientUnitConverterHandler(unitConverterService, unitConverterValidator),
|
||||
chartOfAccountTypeHandler: handler.NewChartOfAccountTypeHandler(chartOfAccountTypeService, chartOfAccountTypeValidator),
|
||||
chartOfAccountHandler: handler.NewChartOfAccountHandler(chartOfAccountService, chartOfAccountValidator),
|
||||
@ -389,16 +387,6 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
||||
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.Use(r.authMiddleware.RequireAdminOrManager())
|
||||
{
|
||||
@ -466,7 +454,6 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
||||
{
|
||||
expenses.POST("", r.expenseHandler.CreateExpense)
|
||||
expenses.GET("", r.expenseHandler.ListExpenses)
|
||||
expenses.GET("/analytics", r.expenseHandler.GetExpenseAnalytics)
|
||||
expenses.GET("/:id", r.expenseHandler.GetExpense)
|
||||
expenses.PUT("/:id", r.expenseHandler.UpdateExpense)
|
||||
expenses.DELETE("/:id", r.expenseHandler.DeleteExpense)
|
||||
|
||||
@ -19,7 +19,6 @@ type ExpenseService interface {
|
||||
DeleteExpense(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
|
||||
GetExpenseAnalytics(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.ExpenseAnalyticsRequest) *contract.Response
|
||||
}
|
||||
|
||||
type ExpenseServiceImpl struct {
|
||||
@ -127,24 +126,3 @@ func (s *ExpenseServiceImpl) ListExpenses(ctx context.Context, apctx *appcontext
|
||||
|
||||
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,3 +160,4 @@ func (s *IngredientUnitConverterServiceImpl) GetUnitsByIngredientID(ctx context.
|
||||
contractResponse := transformer.IngredientUnitsModelResponseToResponse(unitsResponse)
|
||||
return contract.BuildSuccessResponse(contractResponse)
|
||||
}
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
type InventoryMovementService interface {
|
||||
CreateIngredientMovement(ctx context.Context, ingredientID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID, purchaseOrderItemID *uuid.UUID) error
|
||||
CreateIngredientMovement(ctx context.Context, ingredientID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error
|
||||
CreateProductMovement(ctx context.Context, productID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error
|
||||
}
|
||||
|
||||
@ -26,7 +26,7 @@ func NewInventoryMovementService(inventoryMovementRepo repository.InventoryMovem
|
||||
}
|
||||
}
|
||||
|
||||
func (s *InventoryMovementServiceImpl) CreateIngredientMovement(ctx context.Context, ingredientID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID, purchaseOrderItemID *uuid.UUID) error {
|
||||
func (s *InventoryMovementServiceImpl) CreateIngredientMovement(ctx context.Context, ingredientID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error {
|
||||
ingredient, err := s.ingredientRepo.GetByID(ctx, ingredientID, organizationID)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -36,23 +36,22 @@ func (s *InventoryMovementServiceImpl) CreateIngredientMovement(ctx context.Cont
|
||||
newQuantity := previousQuantity + quantity
|
||||
|
||||
movement := &entities.InventoryMovement{
|
||||
ID: uuid.New(),
|
||||
OrganizationID: organizationID,
|
||||
OutletID: outletID,
|
||||
ItemID: ingredientID,
|
||||
ItemType: "INGREDIENT",
|
||||
MovementType: movementType,
|
||||
Quantity: quantity,
|
||||
PreviousQuantity: previousQuantity,
|
||||
NewQuantity: newQuantity,
|
||||
UnitCost: unitCost,
|
||||
TotalCost: unitCost * quantity,
|
||||
ReferenceType: referenceType,
|
||||
ReferenceID: referenceID,
|
||||
PurchaseOrderItemID: purchaseOrderItemID,
|
||||
UserID: userID,
|
||||
Reason: &reason,
|
||||
CreatedAt: time.Now(),
|
||||
ID: uuid.New(),
|
||||
OrganizationID: organizationID,
|
||||
OutletID: outletID,
|
||||
ItemID: ingredientID,
|
||||
ItemType: "INGREDIENT",
|
||||
MovementType: movementType,
|
||||
Quantity: quantity,
|
||||
PreviousQuantity: previousQuantity,
|
||||
NewQuantity: newQuantity,
|
||||
UnitCost: unitCost,
|
||||
TotalCost: unitCost * quantity,
|
||||
ReferenceType: referenceType,
|
||||
ReferenceID: referenceID,
|
||||
UserID: userID,
|
||||
Reason: &reason,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err = s.inventoryMovementRepo.Create(ctx, movement)
|
||||
|
||||
@ -1,107 +0,0 @@
|
||||
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,16 +169,12 @@ func PurchasingAnalyticsModelToContract(resp *models.PurchasingAnalyticsResponse
|
||||
data := make([]contract.PurchasingAnalyticsData, len(resp.Data))
|
||||
for i, item := range resp.Data {
|
||||
data[i] = contract.PurchasingAnalyticsData{
|
||||
Date: item.Date,
|
||||
Purchases: item.Purchases,
|
||||
RawMaterialPurchases: item.RawMaterialPurchases,
|
||||
ExpensePurchases: item.ExpensePurchases,
|
||||
PurchaseOrders: item.PurchaseOrders,
|
||||
RawMaterialPurchaseOrders: item.RawMaterialPurchaseOrders,
|
||||
ExpenseCount: item.ExpenseCount,
|
||||
Quantity: item.Quantity,
|
||||
Ingredients: item.Ingredients,
|
||||
Vendors: item.Vendors,
|
||||
Date: item.Date,
|
||||
Purchases: item.Purchases,
|
||||
PurchaseOrders: item.PurchaseOrders,
|
||||
Quantity: item.Quantity,
|
||||
Ingredients: item.Ingredients,
|
||||
Vendors: item.Vendors,
|
||||
}
|
||||
}
|
||||
|
||||
@ -215,11 +211,7 @@ func PurchasingAnalyticsModelToContract(resp *models.PurchasingAnalyticsResponse
|
||||
GroupBy: resp.GroupBy,
|
||||
Summary: contract.PurchasingSummary{
|
||||
TotalPurchases: resp.Summary.TotalPurchases,
|
||||
RawMaterialPurchases: resp.Summary.RawMaterialPurchases,
|
||||
ExpensePurchases: resp.Summary.ExpensePurchases,
|
||||
TotalPurchaseOrders: resp.Summary.TotalPurchaseOrders,
|
||||
RawMaterialPurchaseOrders: resp.Summary.RawMaterialPurchaseOrders,
|
||||
ExpenseCount: resp.Summary.ExpenseCount,
|
||||
TotalQuantity: resp.Summary.TotalQuantity,
|
||||
AveragePurchaseOrderValue: resp.Summary.AveragePurchaseOrderValue,
|
||||
TotalIngredients: resp.Summary.TotalIngredients,
|
||||
|
||||
@ -52,47 +52,17 @@ func TestPurchasingAnalyticsContractToModelIgnoresInvalidOutlet(t *testing.T) {
|
||||
func TestPurchasingAnalyticsModelToContractCopiesOutletName(t *testing.T) {
|
||||
outletID := uuid.New()
|
||||
outletName := "Main Outlet"
|
||||
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
result := PurchasingAnalyticsModelToContract(&models.PurchasingAnalyticsResponse{
|
||||
OrganizationID: uuid.New(),
|
||||
OutletID: &outletID,
|
||||
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.Equal(t, &outletID, result.OutletID)
|
||||
require.NotNil(t, 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) {
|
||||
|
||||
@ -3,7 +3,6 @@ package transformer
|
||||
import (
|
||||
"apskel-pos-be/internal/contract"
|
||||
"apskel-pos-be/internal/models"
|
||||
"apskel-pos-be/internal/util"
|
||||
)
|
||||
|
||||
func CreateExpenseRequestToModel(req *contract.CreateExpenseRequest) *models.CreateExpenseRequest {
|
||||
@ -27,11 +26,10 @@ func CreateExpenseRequestToModel(req *contract.CreateExpenseRequest) *models.Cre
|
||||
|
||||
func CreateExpenseItemRequestToModel(req *contract.CreateExpenseItemRequest) models.CreateExpenseItemRequest {
|
||||
return models.CreateExpenseItemRequest{
|
||||
ChartOfAccountID: req.ChartOfAccountID,
|
||||
PurchaseCategoryID: req.PurchaseCategoryID,
|
||||
Item: req.Item,
|
||||
Description: req.Description,
|
||||
Amount: req.Amount,
|
||||
ChartOfAccountID: req.ChartOfAccountID,
|
||||
Item: req.Item,
|
||||
Description: req.Description,
|
||||
Amount: req.Amount,
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,11 +59,10 @@ func UpdateExpenseRequestToModel(req *contract.UpdateExpenseRequest) *models.Upd
|
||||
|
||||
func UpdateExpenseItemRequestToModel(req *contract.UpdateExpenseItemRequest) models.UpdateExpenseItemRequest {
|
||||
return models.UpdateExpenseItemRequest{
|
||||
ChartOfAccountID: req.ChartOfAccountID,
|
||||
PurchaseCategoryID: req.PurchaseCategoryID,
|
||||
Item: req.Item,
|
||||
Description: req.Description,
|
||||
Amount: req.Amount,
|
||||
ChartOfAccountID: req.ChartOfAccountID,
|
||||
Item: req.Item,
|
||||
Description: req.Description,
|
||||
Amount: req.Amount,
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,19 +108,15 @@ func ExpenseModelResponseToResponse(expense *models.ExpenseResponse) *contract.E
|
||||
|
||||
func ExpenseItemModelResponseToResponse(item *models.ExpenseItemResponse) contract.ExpenseItemResponse {
|
||||
return contract.ExpenseItemResponse{
|
||||
ID: item.ID,
|
||||
ExpenseID: item.ExpenseID,
|
||||
ChartOfAccountID: item.ChartOfAccountID,
|
||||
ChartOfAccountName: item.ChartOfAccountName,
|
||||
PurchaseCategoryID: item.PurchaseCategoryID,
|
||||
PurchaseCategoryName: item.PurchaseCategoryName,
|
||||
PurchaseCategoryType: item.PurchaseCategoryType,
|
||||
PurchaseCategory: PurchaseCategoryModelResponseToResponse(item.PurchaseCategory),
|
||||
Item: item.Item,
|
||||
Description: item.Description,
|
||||
Amount: item.Amount,
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdatedAt: item.UpdatedAt,
|
||||
ID: item.ID,
|
||||
ExpenseID: item.ExpenseID,
|
||||
ChartOfAccountID: item.ChartOfAccountID,
|
||||
ChartOfAccountName: item.ChartOfAccountName,
|
||||
Item: item.Item,
|
||||
Description: item.Description,
|
||||
Amount: item.Amount,
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdatedAt: item.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
@ -141,94 +134,3 @@ func ExpenseModelResponsesToResponses(expenses []*models.ExpenseResponse) []cont
|
||||
}
|
||||
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,3 +175,4 @@ func IngredientUnitsModelResponseToResponse(model *models.IngredientUnitsRespons
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
|
||||
@ -1,72 +0,0 @@
|
||||
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,12 +11,11 @@ func CreatePurchaseOrderRequestToModel(req *contract.CreatePurchaseOrderRequest)
|
||||
items := make([]models.CreatePurchaseOrderItemRequest, len(req.Items))
|
||||
for i, item := range req.Items {
|
||||
items[i] = models.CreatePurchaseOrderItemRequest{
|
||||
IngredientID: item.IngredientID,
|
||||
PurchaseCategoryID: item.PurchaseCategoryID,
|
||||
Description: item.Description,
|
||||
Quantity: item.Quantity,
|
||||
UnitID: item.UnitID,
|
||||
Amount: item.Amount,
|
||||
IngredientID: item.IngredientID,
|
||||
Description: item.Description,
|
||||
Quantity: item.Quantity,
|
||||
UnitID: item.UnitID,
|
||||
Amount: item.Amount,
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,13 +54,12 @@ func UpdatePurchaseOrderRequestToModel(req *contract.UpdatePurchaseOrderRequest)
|
||||
items = make([]models.UpdatePurchaseOrderItemRequest, len(req.Items))
|
||||
for i, item := range req.Items {
|
||||
items[i] = models.UpdatePurchaseOrderItemRequest{
|
||||
ID: item.ID,
|
||||
IngredientID: item.IngredientID,
|
||||
PurchaseCategoryID: item.PurchaseCategoryID,
|
||||
Description: item.Description,
|
||||
Quantity: item.Quantity,
|
||||
UnitID: item.UnitID,
|
||||
Amount: item.Amount,
|
||||
ID: item.ID,
|
||||
IngredientID: item.IngredientID,
|
||||
Description: item.Description,
|
||||
Quantity: item.Quantity,
|
||||
UnitID: item.UnitID,
|
||||
Amount: item.Amount,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -156,16 +154,15 @@ func PurchaseOrderModelResponseToResponse(po *models.PurchaseOrderResponse) *con
|
||||
response.Items = make([]contract.PurchaseOrderItemResponse, len(po.Items))
|
||||
for i, item := range po.Items {
|
||||
response.Items[i] = contract.PurchaseOrderItemResponse{
|
||||
ID: item.ID,
|
||||
PurchaseOrderID: item.PurchaseOrderID,
|
||||
IngredientID: item.IngredientID,
|
||||
PurchaseCategoryID: item.PurchaseCategoryID,
|
||||
Description: item.Description,
|
||||
Quantity: item.Quantity,
|
||||
UnitID: item.UnitID,
|
||||
Amount: item.Amount,
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdatedAt: item.UpdatedAt,
|
||||
ID: item.ID,
|
||||
PurchaseOrderID: item.PurchaseOrderID,
|
||||
IngredientID: item.IngredientID,
|
||||
Description: item.Description,
|
||||
Quantity: item.Quantity,
|
||||
UnitID: item.UnitID,
|
||||
Amount: item.Amount,
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdatedAt: item.UpdatedAt,
|
||||
}
|
||||
|
||||
// Map ingredient if present
|
||||
@ -176,10 +173,6 @@ func PurchaseOrderModelResponseToResponse(po *models.PurchaseOrderResponse) *con
|
||||
}
|
||||
}
|
||||
|
||||
if item.PurchaseCategory != nil {
|
||||
response.Items[i].PurchaseCategory = PurchaseCategoryModelResponseToResponse(item.PurchaseCategory)
|
||||
}
|
||||
|
||||
// Map unit if present
|
||||
if item.Unit != nil {
|
||||
response.Items[i].Unit = &contract.UnitResponse{
|
||||
|
||||
@ -12,19 +12,15 @@ import (
|
||||
)
|
||||
|
||||
func TestCreatePurchaseOrderRequestToModelAllowsMissingDueDate(t *testing.T) {
|
||||
ingredientID := uuid.New()
|
||||
quantity := 1.0
|
||||
unitID := uuid.New()
|
||||
|
||||
result, err := CreatePurchaseOrderRequestToModel(&contract.CreatePurchaseOrderRequest{
|
||||
VendorID: uuid.New(),
|
||||
PONumber: "PO-001",
|
||||
TransactionDate: "2026-05-29",
|
||||
Items: []contract.CreatePurchaseOrderItemRequest{
|
||||
{
|
||||
IngredientID: &ingredientID,
|
||||
Quantity: &quantity,
|
||||
UnitID: &unitID,
|
||||
IngredientID: uuid.New(),
|
||||
Quantity: 1,
|
||||
UnitID: uuid.New(),
|
||||
Amount: 1000,
|
||||
},
|
||||
},
|
||||
|
||||
@ -68,18 +68,12 @@ func (v *ExpenseValidatorImpl) ValidateCreateExpenseRequest(req *contract.Create
|
||||
if strings.TrimSpace(item.ChartOfAccountID) == "" {
|
||||
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) == "" {
|
||||
return fmt.Errorf("item %d: item is required", i), constants.MissingFieldErrorCode
|
||||
}
|
||||
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
|
||||
}
|
||||
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 {
|
||||
return fmt.Errorf("item %d: amount must be greater than 0", i), constants.MalformedFieldErrorCode
|
||||
}
|
||||
@ -132,15 +126,6 @@ func (v *ExpenseValidatorImpl) ValidateUpdateExpenseRequest(req *contract.Update
|
||||
return fmt.Errorf("item %d: chart_of_account_id must be a valid UUID", i), constants.MalformedFieldErrorCode
|
||||
}
|
||||
}
|
||||
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) == "" {
|
||||
return fmt.Errorf("item %d: item cannot be empty", i), constants.MalformedFieldErrorCode
|
||||
}
|
||||
|
||||
@ -21,9 +21,8 @@ func TestExpenseValidatorCreateRequiresItemName(t *testing.T) {
|
||||
Total: 10000,
|
||||
Items: []contract.CreateExpenseItemRequest{
|
||||
{
|
||||
ChartOfAccountID: uuid.NewString(),
|
||||
PurchaseCategoryID: uuid.NewString(),
|
||||
Amount: 10000,
|
||||
ChartOfAccountID: uuid.NewString(),
|
||||
Amount: 10000,
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -46,10 +45,9 @@ func TestExpenseValidatorCreateDoesNotRequireHeaderExpenseName(t *testing.T) {
|
||||
Total: 10000,
|
||||
Items: []contract.CreateExpenseItemRequest{
|
||||
{
|
||||
ChartOfAccountID: uuid.NewString(),
|
||||
PurchaseCategoryID: uuid.NewString(),
|
||||
Item: "Cleaning supplies",
|
||||
Amount: 10000,
|
||||
ChartOfAccountID: uuid.NewString(),
|
||||
Item: "Cleaning supplies",
|
||||
Amount: 10000,
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -73,10 +71,9 @@ func TestExpenseValidatorCreateAllowsValidOptionalStatus(t *testing.T) {
|
||||
Total: 10000,
|
||||
Items: []contract.CreateExpenseItemRequest{
|
||||
{
|
||||
ChartOfAccountID: uuid.NewString(),
|
||||
PurchaseCategoryID: uuid.NewString(),
|
||||
Item: "Cleaning supplies",
|
||||
Amount: 10000,
|
||||
ChartOfAccountID: uuid.NewString(),
|
||||
Item: "Cleaning supplies",
|
||||
Amount: 10000,
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -100,10 +97,9 @@ func TestExpenseValidatorCreateRejectsInvalidStatus(t *testing.T) {
|
||||
Total: 10000,
|
||||
Items: []contract.CreateExpenseItemRequest{
|
||||
{
|
||||
ChartOfAccountID: uuid.NewString(),
|
||||
PurchaseCategoryID: uuid.NewString(),
|
||||
Item: "Cleaning supplies",
|
||||
Amount: 10000,
|
||||
ChartOfAccountID: uuid.NewString(),
|
||||
Item: "Cleaning supplies",
|
||||
Amount: 10000,
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -118,11 +114,10 @@ func TestExpenseValidatorCreateRejectsInvalidStatus(t *testing.T) {
|
||||
func TestExpenseValidatorUpdateRejectsEmptyItemNameWhenProvided(t *testing.T) {
|
||||
v := NewExpenseValidator()
|
||||
empty := " "
|
||||
purchaseCategoryID := uuid.NewString()
|
||||
|
||||
req := &contract.UpdateExpenseRequest{
|
||||
Items: []contract.UpdateExpenseItemRequest{
|
||||
{PurchaseCategoryID: &purchaseCategoryID, Item: &empty},
|
||||
{Item: &empty},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -119,3 +119,4 @@ func (v *IngredientUnitConverterValidatorImpl) ValidateConvertUnitRequest(req *c
|
||||
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
|
||||
@ -1,103 +0,0 @@
|
||||
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,14 +2,11 @@ package validator
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"apskel-pos-be/internal/constants"
|
||||
"apskel-pos-be/internal/contract"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type PurchaseOrderValidator interface {
|
||||
@ -29,7 +26,7 @@ func (v *PurchaseOrderValidatorImpl) ValidateCreatePurchaseOrderRequest(req *con
|
||||
return errors.New("request body is required"), constants.MissingFieldErrorCode
|
||||
}
|
||||
|
||||
if req.VendorID == uuid.Nil {
|
||||
if req.VendorID.String() == "" {
|
||||
return errors.New("vendor_id is required"), constants.MissingFieldErrorCode
|
||||
}
|
||||
|
||||
@ -181,48 +178,32 @@ func (v *PurchaseOrderValidatorImpl) ValidateListPurchaseOrdersRequest(req *cont
|
||||
}
|
||||
|
||||
func (v *PurchaseOrderValidatorImpl) validatePurchaseOrderItem(item *contract.CreatePurchaseOrderItemRequest, index int) (error, string) {
|
||||
if item.PurchaseCategoryID == uuid.Nil {
|
||||
return errors.New("items[" + strconv.Itoa(index) + "].purchase_category_id is required"), constants.MissingFieldErrorCode
|
||||
if item.IngredientID.String() == "" {
|
||||
return errors.New("items[" + string(rune(index)) + "].ingredient_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.Quantity <= 0 {
|
||||
return errors.New("items[" + string(rune(index)) + "].quantity must be greater than 0"), constants.MalformedFieldErrorCode
|
||||
}
|
||||
|
||||
if item.Quantity != nil && *item.Quantity <= 0 {
|
||||
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.UnitID.String() == "" {
|
||||
return errors.New("items[" + string(rune(index)) + "].unit_id is required"), constants.MissingFieldErrorCode
|
||||
}
|
||||
|
||||
if item.Amount < 0 {
|
||||
return errors.New("items[" + strconv.Itoa(index) + "].amount must be greater than or equal to 0"), constants.MalformedFieldErrorCode
|
||||
return errors.New("items[" + string(rune(index)) + "].amount must be greater than or equal to 0"), constants.MalformedFieldErrorCode
|
||||
}
|
||||
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
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 {
|
||||
return errors.New("items[" + strconv.Itoa(index) + "].quantity must be greater than 0"), constants.MalformedFieldErrorCode
|
||||
return errors.New("items[" + string(rune(index)) + "].quantity must be greater than 0"), constants.MalformedFieldErrorCode
|
||||
}
|
||||
|
||||
if item.Amount != nil && *item.Amount < 0 {
|
||||
return errors.New("items[" + strconv.Itoa(index) + "].amount must be greater than or equal to 0"), constants.MalformedFieldErrorCode
|
||||
return errors.New("items[" + string(rune(index)) + "].amount must be greater than or equal to 0"), constants.MalformedFieldErrorCode
|
||||
}
|
||||
|
||||
return nil, ""
|
||||
|
||||
@ -11,21 +11,16 @@ import (
|
||||
)
|
||||
|
||||
func validCreatePurchaseOrderRequest() *contract.CreatePurchaseOrderRequest {
|
||||
ingredientID := uuid.New()
|
||||
quantity := 1.0
|
||||
unitID := uuid.New()
|
||||
|
||||
return &contract.CreatePurchaseOrderRequest{
|
||||
VendorID: uuid.New(),
|
||||
PONumber: "PO-001",
|
||||
TransactionDate: "2026-05-29",
|
||||
Items: []contract.CreatePurchaseOrderItemRequest{
|
||||
{
|
||||
IngredientID: &ingredientID,
|
||||
PurchaseCategoryID: uuid.New(),
|
||||
Quantity: &quantity,
|
||||
UnitID: &unitID,
|
||||
Amount: 1000,
|
||||
IngredientID: uuid.New(),
|
||||
Quantity: 1,
|
||||
UnitID: uuid.New(),
|
||||
Amount: 1000,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
DROP TABLE IF EXISTS purchase_categories;
|
||||
DROP TABLE IF EXISTS purchase_category_presets;
|
||||
@ -1,60 +0,0 @@
|
||||
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;
|
||||
@ -1,2 +0,0 @@
|
||||
DROP TRIGGER IF EXISTS trigger_create_default_purchase_categories ON organizations;
|
||||
DROP FUNCTION IF EXISTS create_default_purchase_categories();
|
||||
@ -1,49 +0,0 @@
|
||||
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();
|
||||
@ -1,3 +0,0 @@
|
||||
DELETE FROM purchase_categories
|
||||
WHERE is_system = true
|
||||
AND preset_id IS NOT NULL;
|
||||
@ -1,39 +0,0 @@
|
||||
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;
|
||||
@ -1,5 +0,0 @@
|
||||
DROP INDEX IF EXISTS idx_inventory_movements_purchase_order_item_id;
|
||||
ALTER TABLE inventory_movements DROP COLUMN IF EXISTS purchase_order_item_id;
|
||||
|
||||
DROP INDEX IF EXISTS idx_purchase_order_items_purchase_category_id;
|
||||
ALTER TABLE purchase_order_items DROP COLUMN IF EXISTS purchase_category_id;
|
||||
@ -1,11 +0,0 @@
|
||||
ALTER TABLE purchase_order_items
|
||||
ADD COLUMN IF NOT EXISTS purchase_category_id UUID REFERENCES purchase_categories(id) ON DELETE RESTRICT;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_purchase_order_items_purchase_category_id
|
||||
ON purchase_order_items(purchase_category_id);
|
||||
|
||||
ALTER TABLE inventory_movements
|
||||
ADD COLUMN IF NOT EXISTS purchase_order_item_id UUID REFERENCES purchase_order_items(id) ON DELETE SET NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_inventory_movements_purchase_order_item_id
|
||||
ON inventory_movements(purchase_order_item_id);
|
||||
@ -1,2 +0,0 @@
|
||||
DROP INDEX IF EXISTS idx_expense_items_purchase_category_id;
|
||||
ALTER TABLE expense_items DROP COLUMN IF EXISTS purchase_category_id;
|
||||
@ -1,5 +0,0 @@
|
||||
ALTER TABLE expense_items
|
||||
ADD COLUMN IF NOT EXISTS purchase_category_id UUID REFERENCES purchase_categories(id) ON DELETE RESTRICT;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_expense_items_purchase_category_id
|
||||
ON expense_items(purchase_category_id);
|
||||
@ -1,32 +0,0 @@
|
||||
ALTER TABLE purchase_category_presets DROP CONSTRAINT IF EXISTS purchase_category_presets_type_check;
|
||||
ALTER TABLE purchase_categories DROP CONSTRAINT IF EXISTS purchase_categories_type_check;
|
||||
|
||||
UPDATE purchase_category_presets
|
||||
SET type = 'non_inventory'
|
||||
WHERE type = 'expense';
|
||||
|
||||
UPDATE purchase_categories
|
||||
SET type = 'non_inventory'
|
||||
WHERE type = 'expense';
|
||||
|
||||
UPDATE purchase_category_presets
|
||||
SET code = 'hpp', name = 'HPP'
|
||||
WHERE code = 'bahan_baku' AND type = 'raw_material';
|
||||
|
||||
UPDATE purchase_categories
|
||||
SET code = 'hpp', name = 'HPP'
|
||||
WHERE code = 'bahan_baku' AND type = 'raw_material';
|
||||
|
||||
UPDATE purchase_category_presets
|
||||
SET is_active = true
|
||||
WHERE code LIKE 'hpp\_%' ESCAPE '\' AND type = 'raw_material';
|
||||
|
||||
UPDATE purchase_categories
|
||||
SET is_active = true
|
||||
WHERE code LIKE 'hpp\_%' ESCAPE '\' AND type = 'raw_material';
|
||||
|
||||
ALTER TABLE purchase_category_presets
|
||||
ADD CONSTRAINT purchase_category_presets_type_check CHECK (type IN ('raw_material', 'non_inventory'));
|
||||
|
||||
ALTER TABLE purchase_categories
|
||||
ADD CONSTRAINT purchase_categories_type_check CHECK (type IN ('raw_material', 'non_inventory'));
|
||||
@ -1,36 +0,0 @@
|
||||
ALTER TABLE purchase_category_presets DROP CONSTRAINT IF EXISTS purchase_category_presets_type_check;
|
||||
ALTER TABLE purchase_categories DROP CONSTRAINT IF EXISTS purchase_categories_type_check;
|
||||
|
||||
UPDATE purchase_category_presets
|
||||
SET type = 'expense'
|
||||
WHERE type = 'non_inventory';
|
||||
|
||||
UPDATE purchase_categories
|
||||
SET type = 'expense'
|
||||
WHERE type = 'non_inventory';
|
||||
|
||||
UPDATE purchase_category_presets
|
||||
SET code = 'bahan_baku', name = 'Bahan Baku'
|
||||
WHERE code = 'hpp' AND type = 'raw_material';
|
||||
|
||||
UPDATE purchase_categories
|
||||
SET code = 'bahan_baku', name = 'Bahan Baku'
|
||||
WHERE code = 'hpp' AND type = 'raw_material';
|
||||
|
||||
UPDATE purchase_category_presets
|
||||
SET is_active = false
|
||||
WHERE code LIKE 'hpp\_%' ESCAPE '\' AND type = 'raw_material';
|
||||
|
||||
UPDATE purchase_categories
|
||||
SET is_active = false
|
||||
WHERE code LIKE 'hpp\_%' ESCAPE '\' AND type = 'raw_material';
|
||||
|
||||
ALTER TABLE purchase_category_presets
|
||||
ADD CONSTRAINT purchase_category_presets_type_check CHECK (type IN ('raw_material', 'expense'));
|
||||
|
||||
ALTER TABLE purchase_categories
|
||||
ADD CONSTRAINT purchase_categories_type_check CHECK (type IN ('raw_material', 'expense'));
|
||||
|
||||
ALTER TABLE purchase_order_items ALTER COLUMN ingredient_id DROP NOT NULL;
|
||||
ALTER TABLE purchase_order_items ALTER COLUMN quantity DROP NOT NULL;
|
||||
ALTER TABLE purchase_order_items ALTER COLUMN unit_id DROP NOT NULL;
|
||||
Loading…
x
Reference in New Issue
Block a user