diff --git a/internal/app/app.go b/internal/app/app.go index 3b6e833..fbb32dd 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -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.unitRepo, repos.fileRepo, inventoryMovementService, repos.unitConverterRepo), + purchaseOrderProcessor: processor.NewPurchaseOrderProcessorImpl(repos.purchaseOrderRepo, repos.vendorRepo, repos.ingredientRepo, repos.purchaseCategoryRepo, repos.unitRepo, repos.fileRepo, inventoryMovementService, repos.unitConverterRepo), purchaseCategoryProcessor: processor.NewPurchaseCategoryProcessorImpl(repos.purchaseCategoryRepo), unitConverterProcessor: processor.NewIngredientUnitConverterProcessorImpl(repos.unitConverterRepo, repos.ingredientRepo, repos.unitRepo), chartOfAccountTypeProcessor: processor.NewChartOfAccountTypeProcessorImpl(repos.chartOfAccountTypeRepo), diff --git a/internal/contract/purchase_order_contract.go b/internal/contract/purchase_order_contract.go index e6f1c92..70e722c 100644 --- a/internal/contract/purchase_order_contract.go +++ b/internal/contract/purchase_order_contract.go @@ -19,11 +19,12 @@ type CreatePurchaseOrderRequest struct { } type CreatePurchaseOrderItemRequest struct { - 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"` + 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"` } type UpdatePurchaseOrderRequest struct { @@ -39,12 +40,13 @@ type UpdatePurchaseOrderRequest struct { } type UpdatePurchaseOrderItemRequest struct { - 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"` + 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"` } type PurchaseOrderResponse struct { @@ -66,17 +68,19 @@ type PurchaseOrderResponse struct { } type PurchaseOrderItemResponse struct { - 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"` + 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"` } type PurchaseOrderAttachmentResponse struct { diff --git a/internal/entities/inventory_movement.go b/internal/entities/inventory_movement.go index cc4faa4..7baef02 100644 --- a/internal/entities/inventory_movement.go +++ b/internal/entities/inventory_movement.go @@ -36,34 +36,36 @@ 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"` - 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"` + 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"` - 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"` + 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"` } func (im *InventoryMovement) BeforeCreate(tx *gorm.DB) error { diff --git a/internal/entities/purchase_order.go b/internal/entities/purchase_order.go index 8fe74c6..3a455db 100644 --- a/internal/entities/purchase_order.go +++ b/internal/entities/purchase_order.go @@ -41,19 +41,21 @@ 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"` - 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"` + 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"` - 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"` + 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"` } func (poi *PurchaseOrderItem) BeforeCreate(tx *gorm.DB) error { diff --git a/internal/mappers/purchase_order_mapper.go b/internal/mappers/purchase_order_mapper.go index 8b3cf9e..b08327f 100644 --- a/internal/mappers/purchase_order_mapper.go +++ b/internal/mappers/purchase_order_mapper.go @@ -91,15 +91,16 @@ func PurchaseOrderItemEntityToModel(entity *entities.PurchaseOrderItem) *models. } return &models.PurchaseOrderItem{ - 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, + 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, } } @@ -109,15 +110,16 @@ func PurchaseOrderItemModelToEntity(model *models.PurchaseOrderItem) *entities.P } return &entities.PurchaseOrderItem{ - 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, + 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, } } @@ -127,15 +129,16 @@ func PurchaseOrderItemEntityToResponse(entity *entities.PurchaseOrderItem) *mode } response := &models.PurchaseOrderItemResponse{ - 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, + 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, } // Map ingredient if present @@ -146,6 +149,10 @@ func PurchaseOrderItemEntityToResponse(entity *entities.PurchaseOrderItem) *mode } } + if entity.PurchaseCategory != nil { + response.PurchaseCategory = PurchaseCategoryEntityToResponse(entity.PurchaseCategory) + } + // Map unit if present if entity.Unit != nil { response.Unit = &models.UnitResponse{ diff --git a/internal/models/purchase_order.go b/internal/models/purchase_order.go index 95b598b..1afa23f 100644 --- a/internal/models/purchase_order.go +++ b/internal/models/purchase_order.go @@ -22,15 +22,16 @@ type PurchaseOrder struct { } type PurchaseOrderItem struct { - 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"` + 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"` } type PurchaseOrderAttachment struct { @@ -59,17 +60,19 @@ type PurchaseOrderResponse struct { } type PurchaseOrderItemResponse struct { - 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"` + 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"` } type PurchaseOrderAttachmentResponse struct { @@ -93,11 +96,12 @@ type CreatePurchaseOrderRequest struct { } type CreatePurchaseOrderItemRequest struct { - 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"` + 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"` } type UpdatePurchaseOrderRequest struct { @@ -113,12 +117,13 @@ type UpdatePurchaseOrderRequest struct { } type UpdatePurchaseOrderItemRequest struct { - 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"` + 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"` } type ListPurchaseOrdersRequest struct { diff --git a/internal/processor/order_processor.go b/internal/processor/order_processor.go index 3d22958..6065005 100644 --- a/internal/processor/order_processor.go +++ b/internal/processor/order_processor.go @@ -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) error + CreateIngredientMovement(ctx context.Context, ingredientID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID, purchaseOrderItemID *uuid.UUID) error CreateProductMovement(ctx context.Context, productID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error } diff --git a/internal/processor/purchase_order_processor.go b/internal/processor/purchase_order_processor.go index fd5863e..b666d9b 100644 --- a/internal/processor/purchase_order_processor.go +++ b/internal/processor/purchase_order_processor.go @@ -25,6 +25,7 @@ type PurchaseOrderProcessorImpl struct { purchaseOrderRepo PurchaseOrderRepository vendorRepo VendorRepository ingredientRepo IngredientRepository + purchaseCategoryRepo PurchaseCategoryRepository unitRepo UnitRepository fileRepo FileRepository inventoryMovementService InventoryMovementService @@ -35,6 +36,7 @@ func NewPurchaseOrderProcessorImpl( purchaseOrderRepo PurchaseOrderRepository, vendorRepo VendorRepository, ingredientRepo IngredientRepository, + purchaseCategoryRepo PurchaseCategoryRepository, unitRepo UnitRepository, fileRepo FileRepository, inventoryMovementService InventoryMovementService, @@ -44,6 +46,7 @@ func NewPurchaseOrderProcessorImpl( purchaseOrderRepo: purchaseOrderRepo, vendorRepo: vendorRepo, ingredientRepo: ingredientRepo, + purchaseCategoryRepo: purchaseCategoryRepo, unitRepo: unitRepo, fileRepo: fileRepo, inventoryMovementService: inventoryMovementService, @@ -64,13 +67,17 @@ 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 and units exist + // Validate ingredients, raw-material categories, 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) @@ -109,12 +116,13 @@ 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, - Description: itemReq.Description, - Quantity: itemReq.Quantity, - UnitID: itemReq.UnitID, - Amount: itemReq.Amount, + PurchaseOrderID: poEntity.ID, + IngredientID: itemReq.IngredientID, + PurchaseCategoryID: itemReq.PurchaseCategoryID, + Description: itemReq.Description, + Quantity: itemReq.Quantity, + UnitID: itemReq.UnitID, + Amount: itemReq.Amount, } err = p.purchaseOrderRepo.CreateItem(ctx, itemEntity) @@ -197,7 +205,7 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrder(ctx context.Context, id // Create new items totalAmount := 0.0 - for _, itemReq := range req.Items { + for i, itemReq := range req.Items { // Validate ingredients and units exist if itemReq.IngredientID != nil { _, err := p.ingredientRepo.GetByID(ctx, *itemReq.IngredientID, organizationID) @@ -213,8 +221,15 @@ 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 @@ -226,6 +241,9 @@ 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 } @@ -237,12 +255,13 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrder(ctx context.Context, id } itemEntity := &entities.PurchaseOrderItem{ - PurchaseOrderID: poEntity.ID, - IngredientID: ingredientID, - Description: description, - Quantity: quantity, - UnitID: unitID, - Amount: amount, + PurchaseOrderID: poEntity.ID, + IngredientID: ingredientID, + PurchaseCategoryID: purchaseCategoryID, + Description: description, + Quantity: quantity, + UnitID: unitID, + Amount: amount, } err = p.purchaseOrderRepo.CreateItem(ctx, itemEntity) @@ -419,6 +438,7 @@ 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) @@ -440,3 +460,20 @@ 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 +} diff --git a/internal/repository/purchase_order_repository.go b/internal/repository/purchase_order_repository.go index 0d51a8e..8383eec 100644 --- a/internal/repository/purchase_order_repository.go +++ b/internal/repository/purchase_order_repository.go @@ -31,6 +31,7 @@ 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 @@ -45,6 +46,7 @@ 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). @@ -105,6 +107,7 @@ 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"). @@ -168,6 +171,7 @@ 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 @@ -179,6 +183,7 @@ func (r *PurchaseOrderRepositoryImpl) GetOverdue(ctx context.Context, organizati Where("organization_id = ? AND due_date < ? AND status IN (?)", organizationID, time.Now(), []string{"draft", "sent", "approved"}). Preload("Vendor"). Preload("Items.Ingredient"). + Preload("Items.PurchaseCategory"). Preload("Items.Unit"). Find(&pos).Error return pos, err @@ -219,6 +224,7 @@ 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 diff --git a/internal/service/inventory_movement_service.go b/internal/service/inventory_movement_service.go index 9300a5a..4cda860 100644 --- a/internal/service/inventory_movement_service.go +++ b/internal/service/inventory_movement_service.go @@ -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) error + CreateIngredientMovement(ctx context.Context, ingredientID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID, purchaseOrderItemID *uuid.UUID) error CreateProductMovement(ctx context.Context, productID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error } @@ -26,7 +26,7 @@ func NewInventoryMovementService(inventoryMovementRepo repository.InventoryMovem } } -func (s *InventoryMovementServiceImpl) CreateIngredientMovement(ctx context.Context, ingredientID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error { +func (s *InventoryMovementServiceImpl) CreateIngredientMovement(ctx context.Context, ingredientID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID, purchaseOrderItemID *uuid.UUID) error { ingredient, err := s.ingredientRepo.GetByID(ctx, ingredientID, organizationID) if err != nil { return err @@ -36,22 +36,23 @@ 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, - 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, + PurchaseOrderItemID: purchaseOrderItemID, + UserID: userID, + Reason: &reason, + CreatedAt: time.Now(), } err = s.inventoryMovementRepo.Create(ctx, movement) diff --git a/internal/transformer/purchase_order_transformer.go b/internal/transformer/purchase_order_transformer.go index 9860f8f..b61e2ef 100644 --- a/internal/transformer/purchase_order_transformer.go +++ b/internal/transformer/purchase_order_transformer.go @@ -11,11 +11,12 @@ 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, - Description: item.Description, - Quantity: item.Quantity, - UnitID: item.UnitID, - Amount: item.Amount, + IngredientID: item.IngredientID, + PurchaseCategoryID: item.PurchaseCategoryID, + Description: item.Description, + Quantity: item.Quantity, + UnitID: item.UnitID, + Amount: item.Amount, } } @@ -54,12 +55,13 @@ 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, - Description: item.Description, - Quantity: item.Quantity, - UnitID: item.UnitID, - Amount: item.Amount, + ID: item.ID, + IngredientID: item.IngredientID, + PurchaseCategoryID: item.PurchaseCategoryID, + Description: item.Description, + Quantity: item.Quantity, + UnitID: item.UnitID, + Amount: item.Amount, } } } @@ -154,15 +156,16 @@ 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, - 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, + PurchaseCategoryID: item.PurchaseCategoryID, + Description: item.Description, + Quantity: item.Quantity, + UnitID: item.UnitID, + Amount: item.Amount, + CreatedAt: item.CreatedAt, + UpdatedAt: item.UpdatedAt, } // Map ingredient if present @@ -173,6 +176,10 @@ func PurchaseOrderModelResponseToResponse(po *models.PurchaseOrderResponse) *con } } + if item.PurchaseCategory != nil { + response.Items[i].PurchaseCategory = PurchaseCategoryModelResponseToResponse(item.PurchaseCategory) + } + // Map unit if present if item.Unit != nil { response.Items[i].Unit = &contract.UnitResponse{ diff --git a/internal/validator/purchase_order_validator.go b/internal/validator/purchase_order_validator.go index 824ea1b..b6c3f07 100644 --- a/internal/validator/purchase_order_validator.go +++ b/internal/validator/purchase_order_validator.go @@ -2,11 +2,14 @@ package validator import ( "errors" + "strconv" "strings" "time" "apskel-pos-be/internal/constants" "apskel-pos-be/internal/contract" + + "github.com/google/uuid" ) type PurchaseOrderValidator interface { @@ -26,7 +29,7 @@ func (v *PurchaseOrderValidatorImpl) ValidateCreatePurchaseOrderRequest(req *con return errors.New("request body is required"), constants.MissingFieldErrorCode } - if req.VendorID.String() == "" { + if req.VendorID == uuid.Nil { return errors.New("vendor_id is required"), constants.MissingFieldErrorCode } @@ -178,32 +181,40 @@ func (v *PurchaseOrderValidatorImpl) ValidateListPurchaseOrdersRequest(req *cont } func (v *PurchaseOrderValidatorImpl) validatePurchaseOrderItem(item *contract.CreatePurchaseOrderItemRequest, index int) (error, string) { - if item.IngredientID.String() == "" { - return errors.New("items[" + string(rune(index)) + "].ingredient_id is required"), constants.MissingFieldErrorCode + 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.Quantity <= 0 { - return errors.New("items[" + string(rune(index)) + "].quantity must be greater than 0"), constants.MalformedFieldErrorCode + return errors.New("items[" + strconv.Itoa(index) + "].quantity must be greater than 0"), constants.MalformedFieldErrorCode } - if item.UnitID.String() == "" { - return errors.New("items[" + string(rune(index)) + "].unit_id is required"), constants.MissingFieldErrorCode + if item.UnitID == uuid.Nil { + return errors.New("items[" + strconv.Itoa(index) + "].unit_id is required"), constants.MissingFieldErrorCode } if item.Amount < 0 { - return errors.New("items[" + string(rune(index)) + "].amount must be greater than or equal to 0"), constants.MalformedFieldErrorCode + return errors.New("items[" + strconv.Itoa(index) + "].amount must be greater than or equal to 0"), constants.MalformedFieldErrorCode } return nil, "" } 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[" + string(rune(index)) + "].quantity must be greater than 0"), constants.MalformedFieldErrorCode + return errors.New("items[" + strconv.Itoa(index) + "].quantity must be greater than 0"), constants.MalformedFieldErrorCode } if item.Amount != nil && *item.Amount < 0 { - return errors.New("items[" + string(rune(index)) + "].amount must be greater than or equal to 0"), constants.MalformedFieldErrorCode + return errors.New("items[" + strconv.Itoa(index) + "].amount must be greater than or equal to 0"), constants.MalformedFieldErrorCode } return nil, "" diff --git a/internal/validator/purchase_order_validator_test.go b/internal/validator/purchase_order_validator_test.go index 3cd821c..1fde2f9 100644 --- a/internal/validator/purchase_order_validator_test.go +++ b/internal/validator/purchase_order_validator_test.go @@ -17,10 +17,11 @@ func validCreatePurchaseOrderRequest() *contract.CreatePurchaseOrderRequest { TransactionDate: "2026-05-29", Items: []contract.CreatePurchaseOrderItemRequest{ { - IngredientID: uuid.New(), - Quantity: 1, - UnitID: uuid.New(), - Amount: 1000, + IngredientID: uuid.New(), + PurchaseCategoryID: uuid.New(), + Quantity: 1, + UnitID: uuid.New(), + Amount: 1000, }, }, } diff --git a/migrations/000078_add_purchase_category_to_purchase_order_items.down.sql b/migrations/000078_add_purchase_category_to_purchase_order_items.down.sql new file mode 100644 index 0000000..1cf91b9 --- /dev/null +++ b/migrations/000078_add_purchase_category_to_purchase_order_items.down.sql @@ -0,0 +1,5 @@ +DROP INDEX IF EXISTS idx_inventory_movements_purchase_order_item_id; +ALTER TABLE inventory_movements DROP COLUMN IF EXISTS purchase_order_item_id; + +DROP INDEX IF EXISTS idx_purchase_order_items_purchase_category_id; +ALTER TABLE purchase_order_items DROP COLUMN IF EXISTS purchase_category_id; diff --git a/migrations/000078_add_purchase_category_to_purchase_order_items.up.sql b/migrations/000078_add_purchase_category_to_purchase_order_items.up.sql new file mode 100644 index 0000000..0d2af9b --- /dev/null +++ b/migrations/000078_add_purchase_category_to_purchase_order_items.up.sql @@ -0,0 +1,11 @@ +ALTER TABLE purchase_order_items +ADD COLUMN IF NOT EXISTS purchase_category_id UUID REFERENCES purchase_categories(id) ON DELETE RESTRICT; + +CREATE INDEX IF NOT EXISTS idx_purchase_order_items_purchase_category_id +ON purchase_order_items(purchase_category_id); + +ALTER TABLE inventory_movements +ADD COLUMN IF NOT EXISTS purchase_order_item_id UUID REFERENCES purchase_order_items(id) ON DELETE SET NULL; + +CREATE INDEX IF NOT EXISTS idx_inventory_movements_purchase_order_item_id +ON inventory_movements(purchase_order_item_id);