Compare commits
No commits in common. "c3db9195310161ec53d384ca49dc371ee1a644d1" and "e7dd9660daf45574d018680bc3673e476e2e39d8" have entirely different histories.
c3db919531
...
e7dd9660da
@ -372,7 +372,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),
|
||||
purchaseOrderProcessor: processor.NewPurchaseOrderProcessorImpl(repos.purchaseOrderRepo, repos.vendorRepo, repos.ingredientRepo, repos.unitRepo, repos.fileRepo, inventoryMovementService, repos.unitConverterRepo),
|
||||
purchaseCategoryProcessor: processor.NewPurchaseCategoryProcessorImpl(repos.purchaseCategoryRepo),
|
||||
unitConverterProcessor: processor.NewIngredientUnitConverterProcessorImpl(repos.unitConverterRepo, repos.ingredientRepo, repos.unitRepo),
|
||||
chartOfAccountTypeProcessor: processor.NewChartOfAccountTypeProcessorImpl(repos.chartOfAccountTypeRepo),
|
||||
@ -396,7 +396,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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
@ -106,16 +100,15 @@ type ExpenseAnalyticsRequest struct {
|
||||
}
|
||||
|
||||
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"`
|
||||
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"`
|
||||
ItemData []ExpenseAnalyticsItemData `json:"item_data"`
|
||||
}
|
||||
|
||||
type ExpenseAnalyticsSummary struct {
|
||||
@ -137,15 +130,6 @@ type ExpenseAnalyticsData struct {
|
||||
}
|
||||
|
||||
type ExpenseAnalyticsCategoryData struct {
|
||||
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||
PurchaseCategoryName string `json:"purchase_category_name"`
|
||||
PurchaseCategoryType string `json:"purchase_category_type"`
|
||||
TotalAmount float64 `json:"total_amount"`
|
||||
ExpenseCount int64 `json:"expense_count"`
|
||||
ItemCount int64 `json:"item_count"`
|
||||
}
|
||||
|
||||
type ExpenseAnalyticsChartOfAccountData struct {
|
||||
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
||||
ChartOfAccountName string `json:"chart_of_account_name"`
|
||||
TotalAmount float64 `json:"total_amount"`
|
||||
|
||||
@ -19,12 +19,11 @@ type CreatePurchaseOrderRequest struct {
|
||||
}
|
||||
|
||||
type CreatePurchaseOrderItemRequest struct {
|
||||
IngredientID uuid.UUID `json:"ingredient_id" validate:"required"`
|
||||
PurchaseCategoryID uuid.UUID `json:"purchase_category_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"`
|
||||
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 {
|
||||
|
||||
@ -29,11 +29,10 @@ type Expense struct {
|
||||
}
|
||||
|
||||
type ExpenseAnalytics struct {
|
||||
Summary ExpenseAnalyticsSummary
|
||||
Data []ExpenseAnalyticsData
|
||||
CategoryData []ExpenseAnalyticsCategoryData
|
||||
ChartOfAccountData []ExpenseAnalyticsChartOfAccountData
|
||||
ItemData []ExpenseAnalyticsItemData
|
||||
Summary ExpenseAnalyticsSummary
|
||||
Data []ExpenseAnalyticsData
|
||||
CategoryData []ExpenseAnalyticsCategoryData
|
||||
ItemData []ExpenseAnalyticsItemData
|
||||
}
|
||||
|
||||
type ExpenseAnalyticsSummary struct {
|
||||
@ -55,15 +54,6 @@ type ExpenseAnalyticsData struct {
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;not null" json:"ingredient_id" validate:"required"`
|
||||
PurchaseCategoryID uuid.UUID `gorm:"type:uuid;not null;index" json:"purchase_category_id" validate:"required"`
|
||||
Description *string `gorm:"type:text" json:"description" validate:"omitempty"`
|
||||
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"`
|
||||
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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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{
|
||||
|
||||
@ -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 {
|
||||
@ -135,16 +128,15 @@ type ExpenseAnalyticsRequest struct {
|
||||
}
|
||||
|
||||
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"`
|
||||
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"`
|
||||
ItemData []ExpenseAnalyticsItemData `json:"item_data"`
|
||||
}
|
||||
|
||||
type ExpenseAnalyticsSummary struct {
|
||||
@ -166,15 +158,6 @@ type ExpenseAnalyticsData struct {
|
||||
}
|
||||
|
||||
type ExpenseAnalyticsCategoryData struct {
|
||||
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||
PurchaseCategoryName string `json:"purchase_category_name"`
|
||||
PurchaseCategoryType string `json:"purchase_category_type"`
|
||||
TotalAmount float64 `json:"total_amount"`
|
||||
ExpenseCount int64 `json:"expense_count"`
|
||||
ItemCount int64 `json:"item_count"`
|
||||
}
|
||||
|
||||
type ExpenseAnalyticsChartOfAccountData struct {
|
||||
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
|
||||
ChartOfAccountName string `json:"chart_of_account_name"`
|
||||
TotalAmount float64 `json:"total_amount"`
|
||||
|
||||
@ -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"`
|
||||
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
UnitID uuid.UUID `json:"unit_id"`
|
||||
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 {
|
||||
|
||||
@ -23,14 +23,12 @@ type ExpenseProcessor interface {
|
||||
}
|
||||
|
||||
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 +48,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.validateNonInventoryPurchaseCategory(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 +65,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 +135,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 +150,6 @@ func (p *ExpenseProcessorImpl) UpdateExpense(ctx context.Context, id, organizati
|
||||
}
|
||||
}
|
||||
|
||||
if itemReq.PurchaseCategoryID == nil {
|
||||
return nil, fmt.Errorf("purchase_category_id is required for item")
|
||||
}
|
||||
purchaseCategoryID, err := uuid.Parse(*itemReq.PurchaseCategoryID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid purchase_category_id for item: %w", err)
|
||||
}
|
||||
if err := p.validateNonInventoryPurchaseCategory(ctx, purchaseCategoryID, organizationID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
amount := 0.0
|
||||
if itemReq.Amount != nil {
|
||||
amount = *itemReq.Amount
|
||||
@ -182,23 +159,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)
|
||||
}
|
||||
@ -283,18 +252,6 @@ func (p *ExpenseProcessorImpl) GetExpenseAnalytics(ctx context.Context, req *mod
|
||||
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,
|
||||
@ -327,26 +284,8 @@ func (p *ExpenseProcessorImpl) GetExpenseAnalytics(ctx context.Context, req *mod
|
||||
TotalCategories: result.Summary.TotalCategories,
|
||||
TotalItems: result.Summary.TotalItems,
|
||||
},
|
||||
Data: data,
|
||||
CategoryData: categoryData,
|
||||
ChartOfAccountData: chartOfAccountData,
|
||||
ItemData: itemData,
|
||||
Data: data,
|
||||
CategoryData: categoryData,
|
||||
ItemData: itemData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *ExpenseProcessorImpl) validateNonInventoryPurchaseCategory(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.PurchaseCategoryTypeNonInventory {
|
||||
return fmt.Errorf("purchase category must be non_inventory")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -18,45 +18,6 @@ type expenseRepositoryCaptureStub struct {
|
||||
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 {
|
||||
if expense.ID == uuid.Nil {
|
||||
expense.ID = uuid.New()
|
||||
@ -101,8 +62,7 @@ func (*expenseRepositoryCaptureStub) DeleteItemsByExpenseID(context.Context, uui
|
||||
|
||||
func TestExpenseProcessorCreatePersistsItemName(t *testing.T) {
|
||||
repo := &expenseRepositoryCaptureStub{}
|
||||
purchaseCategoryID := uuid.New()
|
||||
p := NewExpenseProcessorImpl(repo, newExpensePurchaseCategoryRepo(purchaseCategoryID, entities.PurchaseCategoryTypeNonInventory))
|
||||
p := NewExpenseProcessorImpl(repo)
|
||||
chartOfAccountID := uuid.New()
|
||||
|
||||
resp, err := p.CreateExpense(context.Background(), uuid.New(), &models.CreateExpenseRequest{
|
||||
@ -113,10 +73,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 +84,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.PurchaseCategoryTypeNonInventory))
|
||||
p := NewExpenseProcessorImpl(repo)
|
||||
|
||||
resp, err := p.CreateExpense(context.Background(), uuid.New(), &models.CreateExpenseRequest{
|
||||
Receiver: "Cashier",
|
||||
@ -143,10 +100,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 +115,7 @@ func TestExpenseProcessorCreateDefaultsStatusToDraft(t *testing.T) {
|
||||
|
||||
func TestExpenseProcessorCreatePersistsProvidedStatus(t *testing.T) {
|
||||
repo := &expenseRepositoryCaptureStub{}
|
||||
purchaseCategoryID := uuid.New()
|
||||
p := NewExpenseProcessorImpl(repo, newExpensePurchaseCategoryRepo(purchaseCategoryID, entities.PurchaseCategoryTypeNonInventory))
|
||||
p := NewExpenseProcessorImpl(repo)
|
||||
status := "approved"
|
||||
|
||||
resp, err := p.CreateExpense(context.Background(), uuid.New(), &models.CreateExpenseRequest{
|
||||
@ -172,10 +127,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,
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -186,35 +140,8 @@ func TestExpenseProcessorCreatePersistsProvidedStatus(t *testing.T) {
|
||||
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(), "non_inventory")
|
||||
}
|
||||
|
||||
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{
|
||||
@ -238,16 +165,6 @@ func TestExpenseProcessorGetExpenseAnalyticsDefaultsGroupByAndMapsResponse(t *te
|
||||
},
|
||||
},
|
||||
CategoryData: []entities.ExpenseAnalyticsCategoryData{
|
||||
{
|
||||
PurchaseCategoryID: purchaseCategoryID,
|
||||
PurchaseCategoryName: "Operational Supplies",
|
||||
PurchaseCategoryType: "non_inventory",
|
||||
TotalAmount: 100000,
|
||||
ExpenseCount: 2,
|
||||
ItemCount: 2,
|
||||
},
|
||||
},
|
||||
ChartOfAccountData: []entities.ExpenseAnalyticsChartOfAccountData{
|
||||
{
|
||||
ChartOfAccountID: coaID,
|
||||
ChartOfAccountName: "Operational",
|
||||
@ -266,7 +183,7 @@ func TestExpenseProcessorGetExpenseAnalyticsDefaultsGroupByAndMapsResponse(t *te
|
||||
},
|
||||
},
|
||||
}
|
||||
p := NewExpenseProcessorImpl(repo, newExpensePurchaseCategoryRepo(purchaseCategoryID, entities.PurchaseCategoryTypeNonInventory))
|
||||
p := NewExpenseProcessorImpl(repo)
|
||||
|
||||
resp, err := p.GetExpenseAnalytics(context.Background(), &models.ExpenseAnalyticsRequest{
|
||||
OrganizationID: uuid.New(),
|
||||
@ -283,9 +200,7 @@ func TestExpenseProcessorGetExpenseAnalyticsDefaultsGroupByAndMapsResponse(t *te
|
||||
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.Equal(t, coaID, resp.CategoryData[0].ChartOfAccountID)
|
||||
require.Len(t, resp.ItemData, 1)
|
||||
require.Equal(t, "Cleaning supplies", resp.ItemData[0].Item)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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,17 +64,13 @@ 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 ingredients, raw-material categories, and units exist
|
||||
// Validate ingredients and units exist
|
||||
for i, item := range req.Items {
|
||||
_, err := p.ingredientRepo.GetByID(ctx, item.IngredientID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ingredient not found for item %d: %w", i, err)
|
||||
}
|
||||
|
||||
if err := p.validateRawMaterialPurchaseCategory(ctx, item.PurchaseCategoryID, organizationID, i); err != nil {
|
||||
return nil, 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)
|
||||
@ -116,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)
|
||||
@ -205,7 +197,7 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrder(ctx context.Context, id
|
||||
|
||||
// Create new items
|
||||
totalAmount := 0.0
|
||||
for i, itemReq := range req.Items {
|
||||
for _, itemReq := range req.Items {
|
||||
// Validate ingredients and units exist
|
||||
if itemReq.IngredientID != nil {
|
||||
_, err := p.ingredientRepo.GetByID(ctx, *itemReq.IngredientID, organizationID)
|
||||
@ -221,15 +213,8 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrder(ctx context.Context, id
|
||||
}
|
||||
}
|
||||
|
||||
if itemReq.PurchaseCategoryID != nil {
|
||||
if err := p.validateRawMaterialPurchaseCategory(ctx, *itemReq.PurchaseCategoryID, organizationID, i); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Use existing values if not provided
|
||||
ingredientID := poEntity.Items[0].IngredientID // This is a simplified approach
|
||||
purchaseCategoryID := poEntity.Items[0].PurchaseCategoryID
|
||||
unitID := poEntity.Items[0].UnitID
|
||||
quantity := poEntity.Items[0].Quantity
|
||||
amount := poEntity.Items[0].Amount
|
||||
@ -241,9 +226,6 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrder(ctx context.Context, id
|
||||
if itemReq.UnitID != nil {
|
||||
unitID = *itemReq.UnitID
|
||||
}
|
||||
if itemReq.PurchaseCategoryID != nil {
|
||||
purchaseCategoryID = *itemReq.PurchaseCategoryID
|
||||
}
|
||||
if itemReq.Quantity != nil {
|
||||
quantity = *itemReq.Quantity
|
||||
}
|
||||
@ -255,13 +237,12 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrder(ctx context.Context, id
|
||||
}
|
||||
|
||||
itemEntity := &entities.PurchaseOrderItem{
|
||||
PurchaseOrderID: poEntity.ID,
|
||||
IngredientID: ingredientID,
|
||||
PurchaseCategoryID: purchaseCategoryID,
|
||||
Description: description,
|
||||
Quantity: quantity,
|
||||
UnitID: unitID,
|
||||
Amount: amount,
|
||||
PurchaseOrderID: poEntity.ID,
|
||||
IngredientID: ingredientID,
|
||||
Description: description,
|
||||
Quantity: quantity,
|
||||
UnitID: unitID,
|
||||
Amount: amount,
|
||||
}
|
||||
|
||||
err = p.purchaseOrderRepo.CreateItem(ctx, itemEntity)
|
||||
@ -438,7 +419,6 @@ 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)
|
||||
@ -460,20 +440,3 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrderStatus(ctx context.Conte
|
||||
|
||||
return mappers.PurchaseOrderEntityToResponse(updatedPO), nil
|
||||
}
|
||||
|
||||
func (p *PurchaseOrderProcessorImpl) validateRawMaterialPurchaseCategory(ctx context.Context, categoryID, organizationID uuid.UUID, itemIndex int) error {
|
||||
category, err := p.purchaseCategoryRepo.GetByIDAndOrganizationID(ctx, categoryID, organizationID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("purchase category not found for item %d: %w", itemIndex, err)
|
||||
}
|
||||
|
||||
if !category.IsActive {
|
||||
return fmt.Errorf("purchase category for item %d is inactive", itemIndex)
|
||||
}
|
||||
|
||||
if category.Type != entities.PurchaseCategoryTypeRawMaterial {
|
||||
return fmt.Errorf("purchase category for item %d must be raw_material", itemIndex)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -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).
|
||||
@ -142,7 +139,7 @@ func (r *ExpenseRepositoryImpl) GetAnalytics(ctx context.Context, organizationID
|
||||
Table("expense_items ei").
|
||||
Select(`
|
||||
COUNT(ei.id) as total_items,
|
||||
COUNT(DISTINCT ei.purchase_category_id) as total_categories
|
||||
COUNT(DISTINCT ei.chart_of_account_id) as total_categories
|
||||
`).
|
||||
Joins("JOIN expenses e ON ei.expense_id = e.id").
|
||||
Where("e.organization_id = ?", organizationID).
|
||||
@ -177,7 +174,7 @@ func (r *ExpenseRepositoryImpl) GetAnalytics(ctx context.Context, organizationID
|
||||
COALESCE(SUM(item_counts.categories), 0) as categories
|
||||
`).
|
||||
Joins(`LEFT JOIN (
|
||||
SELECT expense_id, COUNT(id) as items, COUNT(DISTINCT purchase_category_id) as categories
|
||||
SELECT expense_id, COUNT(id) as items, COUNT(DISTINCT chart_of_account_id) as categories
|
||||
FROM expense_items
|
||||
GROUP BY expense_id
|
||||
) item_counts ON item_counts.expense_id = e.id`).
|
||||
@ -195,32 +192,6 @@ func (r *ExpenseRepositoryImpl) GetAnalytics(ctx context.Context, organizationID
|
||||
|
||||
var categoryData []entities.ExpenseAnalyticsCategoryData
|
||||
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.PurchaseCategoryTypeNonInventory).
|
||||
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,
|
||||
@ -238,9 +209,9 @@ func (r *ExpenseRepositoryImpl) GetAnalytics(ctx context.Context, organizationID
|
||||
Group("COALESCE(parent_coa.id, coa.id), COALESCE(parent_coa.name, coa.name, 'Lain-lain')").
|
||||
Order("total_amount DESC")
|
||||
if outletID != nil {
|
||||
chartOfAccountQuery = chartOfAccountQuery.Where("e.outlet_id = ?", *outletID)
|
||||
categoryQuery = categoryQuery.Where("e.outlet_id = ?", *outletID)
|
||||
}
|
||||
if err := chartOfAccountQuery.Scan(&chartOfAccountData).Error; err != nil {
|
||||
if err := categoryQuery.Scan(&categoryData).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -268,11 +239,10 @@ func (r *ExpenseRepositoryImpl) GetAnalytics(ctx context.Context, organizationID
|
||||
}
|
||||
|
||||
return &entities.ExpenseAnalytics{
|
||||
Summary: summary,
|
||||
Data: data,
|
||||
CategoryData: categoryData,
|
||||
ChartOfAccountData: chartOfAccountData,
|
||||
ItemData: itemData,
|
||||
Summary: summary,
|
||||
Data: data,
|
||||
CategoryData: categoryData,
|
||||
ItemData: itemData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -27,11 +27,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 +60,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 +109,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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -182,18 +176,6 @@ func ExpenseAnalyticsModelToContract(resp *models.ExpenseAnalyticsResponse) *con
|
||||
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,
|
||||
@ -226,9 +208,8 @@ func ExpenseAnalyticsModelToContract(resp *models.ExpenseAnalyticsResponse) *con
|
||||
TotalCategories: resp.Summary.TotalCategories,
|
||||
TotalItems: resp.Summary.TotalItems,
|
||||
},
|
||||
Data: data,
|
||||
CategoryData: categoryData,
|
||||
ChartOfAccountData: chartOfAccountData,
|
||||
ItemData: itemData,
|
||||
Data: data,
|
||||
CategoryData: categoryData,
|
||||
ItemData: itemData,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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{
|
||||
|
||||
@ -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},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -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,40 +178,32 @@ func (v *PurchaseOrderValidatorImpl) ValidateListPurchaseOrdersRequest(req *cont
|
||||
}
|
||||
|
||||
func (v *PurchaseOrderValidatorImpl) validatePurchaseOrderItem(item *contract.CreatePurchaseOrderItemRequest, index int) (error, string) {
|
||||
if item.IngredientID == uuid.Nil {
|
||||
return errors.New("items[" + strconv.Itoa(index) + "].ingredient_id is required"), constants.MissingFieldErrorCode
|
||||
}
|
||||
|
||||
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.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.UnitID == uuid.Nil {
|
||||
return errors.New("items[" + strconv.Itoa(index) + "].unit_id is required"), constants.MissingFieldErrorCode
|
||||
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.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, ""
|
||||
|
||||
@ -17,11 +17,10 @@ func validCreatePurchaseOrderRequest() *contract.CreatePurchaseOrderRequest {
|
||||
TransactionDate: "2026-05-29",
|
||||
Items: []contract.CreatePurchaseOrderItemRequest{
|
||||
{
|
||||
IngredientID: uuid.New(),
|
||||
PurchaseCategoryID: uuid.New(),
|
||||
Quantity: 1,
|
||||
UnitID: uuid.New(),
|
||||
Amount: 1000,
|
||||
IngredientID: uuid.New(),
|
||||
Quantity: 1,
|
||||
UnitID: uuid.New(),
|
||||
Amount: 1000,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -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);
|
||||
Loading…
x
Reference in New Issue
Block a user