Update purchase for product category (inventory type)
This commit is contained in:
parent
e7dd9660da
commit
e09feff36d
@ -372,7 +372,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
|||||||
ingredientProcessor: processor.NewIngredientProcessor(repos.ingredientRepo, repos.unitRepo, repos.ingredientCompositionRepo),
|
ingredientProcessor: processor.NewIngredientProcessor(repos.ingredientRepo, repos.unitRepo, repos.ingredientCompositionRepo),
|
||||||
productRecipeProcessor: processor.NewProductRecipeProcessor(repos.productRecipeRepo, repos.productRepo, repos.ingredientRepo),
|
productRecipeProcessor: processor.NewProductRecipeProcessor(repos.productRecipeRepo, repos.productRepo, repos.ingredientRepo),
|
||||||
vendorProcessor: processor.NewVendorProcessorImpl(repos.vendorRepo),
|
vendorProcessor: processor.NewVendorProcessorImpl(repos.vendorRepo),
|
||||||
purchaseOrderProcessor: processor.NewPurchaseOrderProcessorImpl(repos.purchaseOrderRepo, repos.vendorRepo, repos.ingredientRepo, repos.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),
|
purchaseCategoryProcessor: processor.NewPurchaseCategoryProcessorImpl(repos.purchaseCategoryRepo),
|
||||||
unitConverterProcessor: processor.NewIngredientUnitConverterProcessorImpl(repos.unitConverterRepo, repos.ingredientRepo, repos.unitRepo),
|
unitConverterProcessor: processor.NewIngredientUnitConverterProcessorImpl(repos.unitConverterRepo, repos.ingredientRepo, repos.unitRepo),
|
||||||
chartOfAccountTypeProcessor: processor.NewChartOfAccountTypeProcessorImpl(repos.chartOfAccountTypeRepo),
|
chartOfAccountTypeProcessor: processor.NewChartOfAccountTypeProcessorImpl(repos.chartOfAccountTypeRepo),
|
||||||
|
|||||||
@ -20,6 +20,7 @@ type CreatePurchaseOrderRequest struct {
|
|||||||
|
|
||||||
type CreatePurchaseOrderItemRequest struct {
|
type CreatePurchaseOrderItemRequest struct {
|
||||||
IngredientID uuid.UUID `json:"ingredient_id" validate:"required"`
|
IngredientID uuid.UUID `json:"ingredient_id" validate:"required"`
|
||||||
|
PurchaseCategoryID uuid.UUID `json:"purchase_category_id" validate:"required"`
|
||||||
Description *string `json:"description,omitempty" validate:"omitempty"`
|
Description *string `json:"description,omitempty" validate:"omitempty"`
|
||||||
Quantity float64 `json:"quantity" validate:"required,gt=0"`
|
Quantity float64 `json:"quantity" validate:"required,gt=0"`
|
||||||
UnitID uuid.UUID `json:"unit_id" validate:"required"`
|
UnitID uuid.UUID `json:"unit_id" validate:"required"`
|
||||||
@ -41,6 +42,7 @@ type UpdatePurchaseOrderRequest struct {
|
|||||||
type UpdatePurchaseOrderItemRequest struct {
|
type UpdatePurchaseOrderItemRequest struct {
|
||||||
ID *uuid.UUID `json:"id,omitempty"` // For existing items
|
ID *uuid.UUID `json:"id,omitempty"` // For existing items
|
||||||
IngredientID *uuid.UUID `json:"ingredient_id,omitempty" validate:"omitempty"`
|
IngredientID *uuid.UUID `json:"ingredient_id,omitempty" validate:"omitempty"`
|
||||||
|
PurchaseCategoryID *uuid.UUID `json:"purchase_category_id,omitempty" validate:"omitempty"`
|
||||||
Description *string `json:"description,omitempty" validate:"omitempty"`
|
Description *string `json:"description,omitempty" validate:"omitempty"`
|
||||||
Quantity *float64 `json:"quantity,omitempty" validate:"omitempty,gt=0"`
|
Quantity *float64 `json:"quantity,omitempty" validate:"omitempty,gt=0"`
|
||||||
UnitID *uuid.UUID `json:"unit_id,omitempty" validate:"omitempty"`
|
UnitID *uuid.UUID `json:"unit_id,omitempty" validate:"omitempty"`
|
||||||
@ -69,6 +71,7 @@ type PurchaseOrderItemResponse struct {
|
|||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
PurchaseOrderID uuid.UUID `json:"purchase_order_id"`
|
PurchaseOrderID uuid.UUID `json:"purchase_order_id"`
|
||||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||||
|
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
Quantity float64 `json:"quantity"`
|
Quantity float64 `json:"quantity"`
|
||||||
UnitID uuid.UUID `json:"unit_id"`
|
UnitID uuid.UUID `json:"unit_id"`
|
||||||
@ -76,6 +79,7 @@ type PurchaseOrderItemResponse struct {
|
|||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
Ingredient *IngredientResponse `json:"ingredient,omitempty"`
|
Ingredient *IngredientResponse `json:"ingredient,omitempty"`
|
||||||
|
PurchaseCategory *PurchaseCategoryResponse `json:"purchase_category,omitempty"`
|
||||||
Unit *UnitResponse `json:"unit,omitempty"`
|
Unit *UnitResponse `json:"unit,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -49,6 +49,7 @@ type InventoryMovement struct {
|
|||||||
TotalCost float64 `gorm:"type:decimal(12,2);default:0.00" json:"total_cost"`
|
TotalCost float64 `gorm:"type:decimal(12,2);default:0.00" json:"total_cost"`
|
||||||
ReferenceType *InventoryMovementReferenceType `gorm:"size:50" json:"reference_type"`
|
ReferenceType *InventoryMovementReferenceType `gorm:"size:50" json:"reference_type"`
|
||||||
ReferenceID *uuid.UUID `gorm:"type:uuid;index" json:"reference_id"`
|
ReferenceID *uuid.UUID `gorm:"type:uuid;index" json:"reference_id"`
|
||||||
|
PurchaseOrderItemID *uuid.UUID `gorm:"type:uuid;index" json:"purchase_order_item_id"`
|
||||||
OrderID *uuid.UUID `gorm:"type:uuid;index" json:"order_id"`
|
OrderID *uuid.UUID `gorm:"type:uuid;index" json:"order_id"`
|
||||||
PaymentID *uuid.UUID `gorm:"type:uuid;index" json:"payment_id"`
|
PaymentID *uuid.UUID `gorm:"type:uuid;index" json:"payment_id"`
|
||||||
UserID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_id" validate:"required"`
|
UserID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_id" validate:"required"`
|
||||||
@ -61,6 +62,7 @@ type InventoryMovement struct {
|
|||||||
Outlet Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
|
Outlet Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
|
||||||
Product *Product `gorm:"foreignKey:ItemID" json:"product,omitempty"`
|
Product *Product `gorm:"foreignKey:ItemID" json:"product,omitempty"`
|
||||||
Ingredient *Ingredient `gorm:"foreignKey:ItemID" json:"ingredient,omitempty"`
|
Ingredient *Ingredient `gorm:"foreignKey:ItemID" json:"ingredient,omitempty"`
|
||||||
|
PurchaseOrderItem *PurchaseOrderItem `gorm:"foreignKey:PurchaseOrderItemID" json:"purchase_order_item,omitempty"`
|
||||||
Order *Order `gorm:"foreignKey:OrderID" json:"order,omitempty"`
|
Order *Order `gorm:"foreignKey:OrderID" json:"order,omitempty"`
|
||||||
Payment *Payment `gorm:"foreignKey:PaymentID" json:"payment,omitempty"`
|
Payment *Payment `gorm:"foreignKey:PaymentID" json:"payment,omitempty"`
|
||||||
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||||
|
|||||||
@ -44,6 +44,7 @@ type PurchaseOrderItem struct {
|
|||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
PurchaseOrderID uuid.UUID `gorm:"type:uuid;not null" json:"purchase_order_id" validate:"required"`
|
PurchaseOrderID uuid.UUID `gorm:"type:uuid;not null" json:"purchase_order_id" validate:"required"`
|
||||||
IngredientID uuid.UUID `gorm:"type:uuid;not null" json:"ingredient_id" validate:"required"`
|
IngredientID uuid.UUID `gorm:"type:uuid;not null" json:"ingredient_id" validate:"required"`
|
||||||
|
PurchaseCategoryID uuid.UUID `gorm:"type:uuid;not null;index" json:"purchase_category_id" validate:"required"`
|
||||||
Description *string `gorm:"type:text" json:"description" validate:"omitempty"`
|
Description *string `gorm:"type:text" json:"description" validate:"omitempty"`
|
||||||
Quantity float64 `gorm:"type:decimal(10,3);not null" json:"quantity" validate:"required,gt=0"`
|
Quantity float64 `gorm:"type:decimal(10,3);not null" json:"quantity" validate:"required,gt=0"`
|
||||||
UnitID uuid.UUID `gorm:"type:uuid;not null" json:"unit_id" validate:"required"`
|
UnitID uuid.UUID `gorm:"type:uuid;not null" json:"unit_id" validate:"required"`
|
||||||
@ -53,6 +54,7 @@ type PurchaseOrderItem struct {
|
|||||||
|
|
||||||
PurchaseOrder *PurchaseOrder `gorm:"foreignKey:PurchaseOrderID" json:"purchase_order,omitempty"`
|
PurchaseOrder *PurchaseOrder `gorm:"foreignKey:PurchaseOrderID" json:"purchase_order,omitempty"`
|
||||||
Ingredient *Ingredient `gorm:"foreignKey:IngredientID" json:"ingredient,omitempty"`
|
Ingredient *Ingredient `gorm:"foreignKey:IngredientID" json:"ingredient,omitempty"`
|
||||||
|
PurchaseCategory *PurchaseCategory `gorm:"foreignKey:PurchaseCategoryID" json:"purchase_category,omitempty"`
|
||||||
Unit *Unit `gorm:"foreignKey:UnitID" json:"unit,omitempty"`
|
Unit *Unit `gorm:"foreignKey:UnitID" json:"unit,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -94,6 +94,7 @@ func PurchaseOrderItemEntityToModel(entity *entities.PurchaseOrderItem) *models.
|
|||||||
ID: entity.ID,
|
ID: entity.ID,
|
||||||
PurchaseOrderID: entity.PurchaseOrderID,
|
PurchaseOrderID: entity.PurchaseOrderID,
|
||||||
IngredientID: entity.IngredientID,
|
IngredientID: entity.IngredientID,
|
||||||
|
PurchaseCategoryID: entity.PurchaseCategoryID,
|
||||||
Description: entity.Description,
|
Description: entity.Description,
|
||||||
Quantity: entity.Quantity,
|
Quantity: entity.Quantity,
|
||||||
UnitID: entity.UnitID,
|
UnitID: entity.UnitID,
|
||||||
@ -112,6 +113,7 @@ func PurchaseOrderItemModelToEntity(model *models.PurchaseOrderItem) *entities.P
|
|||||||
ID: model.ID,
|
ID: model.ID,
|
||||||
PurchaseOrderID: model.PurchaseOrderID,
|
PurchaseOrderID: model.PurchaseOrderID,
|
||||||
IngredientID: model.IngredientID,
|
IngredientID: model.IngredientID,
|
||||||
|
PurchaseCategoryID: model.PurchaseCategoryID,
|
||||||
Description: model.Description,
|
Description: model.Description,
|
||||||
Quantity: model.Quantity,
|
Quantity: model.Quantity,
|
||||||
UnitID: model.UnitID,
|
UnitID: model.UnitID,
|
||||||
@ -130,6 +132,7 @@ func PurchaseOrderItemEntityToResponse(entity *entities.PurchaseOrderItem) *mode
|
|||||||
ID: entity.ID,
|
ID: entity.ID,
|
||||||
PurchaseOrderID: entity.PurchaseOrderID,
|
PurchaseOrderID: entity.PurchaseOrderID,
|
||||||
IngredientID: entity.IngredientID,
|
IngredientID: entity.IngredientID,
|
||||||
|
PurchaseCategoryID: entity.PurchaseCategoryID,
|
||||||
Description: entity.Description,
|
Description: entity.Description,
|
||||||
Quantity: entity.Quantity,
|
Quantity: entity.Quantity,
|
||||||
UnitID: entity.UnitID,
|
UnitID: entity.UnitID,
|
||||||
@ -146,6 +149,10 @@ func PurchaseOrderItemEntityToResponse(entity *entities.PurchaseOrderItem) *mode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if entity.PurchaseCategory != nil {
|
||||||
|
response.PurchaseCategory = PurchaseCategoryEntityToResponse(entity.PurchaseCategory)
|
||||||
|
}
|
||||||
|
|
||||||
// Map unit if present
|
// Map unit if present
|
||||||
if entity.Unit != nil {
|
if entity.Unit != nil {
|
||||||
response.Unit = &models.UnitResponse{
|
response.Unit = &models.UnitResponse{
|
||||||
|
|||||||
@ -25,6 +25,7 @@ type PurchaseOrderItem struct {
|
|||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
PurchaseOrderID uuid.UUID `json:"purchase_order_id"`
|
PurchaseOrderID uuid.UUID `json:"purchase_order_id"`
|
||||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||||
|
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
Quantity float64 `json:"quantity"`
|
Quantity float64 `json:"quantity"`
|
||||||
UnitID uuid.UUID `json:"unit_id"`
|
UnitID uuid.UUID `json:"unit_id"`
|
||||||
@ -62,6 +63,7 @@ type PurchaseOrderItemResponse struct {
|
|||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
PurchaseOrderID uuid.UUID `json:"purchase_order_id"`
|
PurchaseOrderID uuid.UUID `json:"purchase_order_id"`
|
||||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||||
|
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
Quantity float64 `json:"quantity"`
|
Quantity float64 `json:"quantity"`
|
||||||
UnitID uuid.UUID `json:"unit_id"`
|
UnitID uuid.UUID `json:"unit_id"`
|
||||||
@ -69,6 +71,7 @@ type PurchaseOrderItemResponse struct {
|
|||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
Ingredient *IngredientResponse `json:"ingredient,omitempty"`
|
Ingredient *IngredientResponse `json:"ingredient,omitempty"`
|
||||||
|
PurchaseCategory *PurchaseCategoryResponse `json:"purchase_category,omitempty"`
|
||||||
Unit *UnitResponse `json:"unit,omitempty"`
|
Unit *UnitResponse `json:"unit,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,6 +97,7 @@ type CreatePurchaseOrderRequest struct {
|
|||||||
|
|
||||||
type CreatePurchaseOrderItemRequest struct {
|
type CreatePurchaseOrderItemRequest struct {
|
||||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||||
|
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||||
Description *string `json:"description,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
Quantity float64 `json:"quantity"`
|
Quantity float64 `json:"quantity"`
|
||||||
UnitID uuid.UUID `json:"unit_id"`
|
UnitID uuid.UUID `json:"unit_id"`
|
||||||
@ -115,6 +119,7 @@ type UpdatePurchaseOrderRequest struct {
|
|||||||
type UpdatePurchaseOrderItemRequest struct {
|
type UpdatePurchaseOrderItemRequest struct {
|
||||||
ID *uuid.UUID `json:"id,omitempty"` // For existing items
|
ID *uuid.UUID `json:"id,omitempty"` // For existing items
|
||||||
IngredientID *uuid.UUID `json:"ingredient_id,omitempty"`
|
IngredientID *uuid.UUID `json:"ingredient_id,omitempty"`
|
||||||
|
PurchaseCategoryID *uuid.UUID `json:"purchase_category_id,omitempty"`
|
||||||
Description *string `json:"description,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
Quantity *float64 `json:"quantity,omitempty"`
|
Quantity *float64 `json:"quantity,omitempty"`
|
||||||
UnitID *uuid.UUID `json:"unit_id,omitempty"`
|
UnitID *uuid.UUID `json:"unit_id,omitempty"`
|
||||||
|
|||||||
@ -86,7 +86,7 @@ type CustomerRepository interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type InventoryMovementService interface {
|
type InventoryMovementService interface {
|
||||||
CreateIngredientMovement(ctx context.Context, ingredientID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error
|
CreateIngredientMovement(ctx context.Context, ingredientID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID, purchaseOrderItemID *uuid.UUID) error
|
||||||
CreateProductMovement(ctx context.Context, productID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error
|
CreateProductMovement(ctx context.Context, productID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -25,6 +25,7 @@ type PurchaseOrderProcessorImpl struct {
|
|||||||
purchaseOrderRepo PurchaseOrderRepository
|
purchaseOrderRepo PurchaseOrderRepository
|
||||||
vendorRepo VendorRepository
|
vendorRepo VendorRepository
|
||||||
ingredientRepo IngredientRepository
|
ingredientRepo IngredientRepository
|
||||||
|
purchaseCategoryRepo PurchaseCategoryRepository
|
||||||
unitRepo UnitRepository
|
unitRepo UnitRepository
|
||||||
fileRepo FileRepository
|
fileRepo FileRepository
|
||||||
inventoryMovementService InventoryMovementService
|
inventoryMovementService InventoryMovementService
|
||||||
@ -35,6 +36,7 @@ func NewPurchaseOrderProcessorImpl(
|
|||||||
purchaseOrderRepo PurchaseOrderRepository,
|
purchaseOrderRepo PurchaseOrderRepository,
|
||||||
vendorRepo VendorRepository,
|
vendorRepo VendorRepository,
|
||||||
ingredientRepo IngredientRepository,
|
ingredientRepo IngredientRepository,
|
||||||
|
purchaseCategoryRepo PurchaseCategoryRepository,
|
||||||
unitRepo UnitRepository,
|
unitRepo UnitRepository,
|
||||||
fileRepo FileRepository,
|
fileRepo FileRepository,
|
||||||
inventoryMovementService InventoryMovementService,
|
inventoryMovementService InventoryMovementService,
|
||||||
@ -44,6 +46,7 @@ func NewPurchaseOrderProcessorImpl(
|
|||||||
purchaseOrderRepo: purchaseOrderRepo,
|
purchaseOrderRepo: purchaseOrderRepo,
|
||||||
vendorRepo: vendorRepo,
|
vendorRepo: vendorRepo,
|
||||||
ingredientRepo: ingredientRepo,
|
ingredientRepo: ingredientRepo,
|
||||||
|
purchaseCategoryRepo: purchaseCategoryRepo,
|
||||||
unitRepo: unitRepo,
|
unitRepo: unitRepo,
|
||||||
fileRepo: fileRepo,
|
fileRepo: fileRepo,
|
||||||
inventoryMovementService: inventoryMovementService,
|
inventoryMovementService: inventoryMovementService,
|
||||||
@ -64,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)
|
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 {
|
for i, item := range req.Items {
|
||||||
_, err := p.ingredientRepo.GetByID(ctx, item.IngredientID, organizationID)
|
_, err := p.ingredientRepo.GetByID(ctx, item.IngredientID, organizationID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ingredient not found for item %d: %w", i, err)
|
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)
|
_, err = p.unitRepo.GetByID(ctx, item.UnitID, organizationID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unit not found for item %d: %w", i, err)
|
return nil, fmt.Errorf("unit not found for item %d: %w", i, err)
|
||||||
@ -111,6 +118,7 @@ func (p *PurchaseOrderProcessorImpl) CreatePurchaseOrder(ctx context.Context, or
|
|||||||
itemEntity := &entities.PurchaseOrderItem{
|
itemEntity := &entities.PurchaseOrderItem{
|
||||||
PurchaseOrderID: poEntity.ID,
|
PurchaseOrderID: poEntity.ID,
|
||||||
IngredientID: itemReq.IngredientID,
|
IngredientID: itemReq.IngredientID,
|
||||||
|
PurchaseCategoryID: itemReq.PurchaseCategoryID,
|
||||||
Description: itemReq.Description,
|
Description: itemReq.Description,
|
||||||
Quantity: itemReq.Quantity,
|
Quantity: itemReq.Quantity,
|
||||||
UnitID: itemReq.UnitID,
|
UnitID: itemReq.UnitID,
|
||||||
@ -197,7 +205,7 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrder(ctx context.Context, id
|
|||||||
|
|
||||||
// Create new items
|
// Create new items
|
||||||
totalAmount := 0.0
|
totalAmount := 0.0
|
||||||
for _, itemReq := range req.Items {
|
for i, itemReq := range req.Items {
|
||||||
// Validate ingredients and units exist
|
// Validate ingredients and units exist
|
||||||
if itemReq.IngredientID != nil {
|
if itemReq.IngredientID != nil {
|
||||||
_, err := p.ingredientRepo.GetByID(ctx, *itemReq.IngredientID, organizationID)
|
_, 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
|
// Use existing values if not provided
|
||||||
ingredientID := poEntity.Items[0].IngredientID // This is a simplified approach
|
ingredientID := poEntity.Items[0].IngredientID // This is a simplified approach
|
||||||
|
purchaseCategoryID := poEntity.Items[0].PurchaseCategoryID
|
||||||
unitID := poEntity.Items[0].UnitID
|
unitID := poEntity.Items[0].UnitID
|
||||||
quantity := poEntity.Items[0].Quantity
|
quantity := poEntity.Items[0].Quantity
|
||||||
amount := poEntity.Items[0].Amount
|
amount := poEntity.Items[0].Amount
|
||||||
@ -226,6 +241,9 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrder(ctx context.Context, id
|
|||||||
if itemReq.UnitID != nil {
|
if itemReq.UnitID != nil {
|
||||||
unitID = *itemReq.UnitID
|
unitID = *itemReq.UnitID
|
||||||
}
|
}
|
||||||
|
if itemReq.PurchaseCategoryID != nil {
|
||||||
|
purchaseCategoryID = *itemReq.PurchaseCategoryID
|
||||||
|
}
|
||||||
if itemReq.Quantity != nil {
|
if itemReq.Quantity != nil {
|
||||||
quantity = *itemReq.Quantity
|
quantity = *itemReq.Quantity
|
||||||
}
|
}
|
||||||
@ -239,6 +257,7 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrder(ctx context.Context, id
|
|||||||
itemEntity := &entities.PurchaseOrderItem{
|
itemEntity := &entities.PurchaseOrderItem{
|
||||||
PurchaseOrderID: poEntity.ID,
|
PurchaseOrderID: poEntity.ID,
|
||||||
IngredientID: ingredientID,
|
IngredientID: ingredientID,
|
||||||
|
PurchaseCategoryID: purchaseCategoryID,
|
||||||
Description: description,
|
Description: description,
|
||||||
Quantity: quantity,
|
Quantity: quantity,
|
||||||
UnitID: unitID,
|
UnitID: unitID,
|
||||||
@ -419,6 +438,7 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrderStatus(ctx context.Conte
|
|||||||
reason,
|
reason,
|
||||||
&referenceType,
|
&referenceType,
|
||||||
referenceID,
|
referenceID,
|
||||||
|
&item.ID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create inventory movement for ingredient %s: %w", item.IngredientID, err)
|
return nil, fmt.Errorf("failed to create inventory movement for ingredient %s: %w", item.IngredientID, err)
|
||||||
@ -440,3 +460,20 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrderStatus(ctx context.Conte
|
|||||||
|
|
||||||
return mappers.PurchaseOrderEntityToResponse(updatedPO), nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@ -31,6 +31,7 @@ func (r *PurchaseOrderRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID)
|
|||||||
err := r.db.WithContext(ctx).
|
err := r.db.WithContext(ctx).
|
||||||
Preload("Vendor").
|
Preload("Vendor").
|
||||||
Preload("Items.Ingredient").
|
Preload("Items.Ingredient").
|
||||||
|
Preload("Items.PurchaseCategory").
|
||||||
Preload("Items.Unit").
|
Preload("Items.Unit").
|
||||||
Preload("Attachments.File").
|
Preload("Attachments.File").
|
||||||
First(&po, "id = ?", id).Error
|
First(&po, "id = ?", id).Error
|
||||||
@ -45,6 +46,7 @@ func (r *PurchaseOrderRepositoryImpl) GetByIDAndOrganizationID(ctx context.Conte
|
|||||||
err := r.db.WithContext(ctx).
|
err := r.db.WithContext(ctx).
|
||||||
Preload("Vendor").
|
Preload("Vendor").
|
||||||
Preload("Items.Ingredient").
|
Preload("Items.Ingredient").
|
||||||
|
Preload("Items.PurchaseCategory").
|
||||||
Preload("Items.Unit").
|
Preload("Items.Unit").
|
||||||
Preload("Attachments.File").
|
Preload("Attachments.File").
|
||||||
Where("id = ? AND organization_id = ?", id, organizationID).
|
Where("id = ? AND organization_id = ?", id, organizationID).
|
||||||
@ -105,6 +107,7 @@ func (r *PurchaseOrderRepositoryImpl) List(ctx context.Context, organizationID u
|
|||||||
err := query.
|
err := query.
|
||||||
Preload("Vendor").
|
Preload("Vendor").
|
||||||
Preload("Items.Ingredient").
|
Preload("Items.Ingredient").
|
||||||
|
Preload("Items.PurchaseCategory").
|
||||||
Preload("Items.Unit").
|
Preload("Items.Unit").
|
||||||
Preload("Attachments.File").
|
Preload("Attachments.File").
|
||||||
Order("created_at DESC").
|
Order("created_at DESC").
|
||||||
@ -168,6 +171,7 @@ func (r *PurchaseOrderRepositoryImpl) GetByStatus(ctx context.Context, organizat
|
|||||||
Where("organization_id = ? AND status = ?", organizationID, status).
|
Where("organization_id = ? AND status = ?", organizationID, status).
|
||||||
Preload("Vendor").
|
Preload("Vendor").
|
||||||
Preload("Items.Ingredient").
|
Preload("Items.Ingredient").
|
||||||
|
Preload("Items.PurchaseCategory").
|
||||||
Preload("Items.Unit").
|
Preload("Items.Unit").
|
||||||
Find(&pos).Error
|
Find(&pos).Error
|
||||||
return pos, err
|
return pos, err
|
||||||
@ -179,6 +183,7 @@ func (r *PurchaseOrderRepositoryImpl) GetOverdue(ctx context.Context, organizati
|
|||||||
Where("organization_id = ? AND due_date < ? AND status IN (?)", organizationID, time.Now(), []string{"draft", "sent", "approved"}).
|
Where("organization_id = ? AND due_date < ? AND status IN (?)", organizationID, time.Now(), []string{"draft", "sent", "approved"}).
|
||||||
Preload("Vendor").
|
Preload("Vendor").
|
||||||
Preload("Items.Ingredient").
|
Preload("Items.Ingredient").
|
||||||
|
Preload("Items.PurchaseCategory").
|
||||||
Preload("Items.Unit").
|
Preload("Items.Unit").
|
||||||
Find(&pos).Error
|
Find(&pos).Error
|
||||||
return pos, err
|
return pos, err
|
||||||
@ -219,6 +224,7 @@ func (r *PurchaseOrderRepositoryImpl) GetItemsByPurchaseOrderID(ctx context.Cont
|
|||||||
var items []*entities.PurchaseOrderItem
|
var items []*entities.PurchaseOrderItem
|
||||||
err := r.db.WithContext(ctx).
|
err := r.db.WithContext(ctx).
|
||||||
Preload("Ingredient").
|
Preload("Ingredient").
|
||||||
|
Preload("PurchaseCategory").
|
||||||
Preload("Unit").
|
Preload("Unit").
|
||||||
Where("purchase_order_id = ?", purchaseOrderID).
|
Where("purchase_order_id = ?", purchaseOrderID).
|
||||||
Find(&items).Error
|
Find(&items).Error
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type InventoryMovementService interface {
|
type InventoryMovementService interface {
|
||||||
CreateIngredientMovement(ctx context.Context, ingredientID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error
|
CreateIngredientMovement(ctx context.Context, ingredientID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID, purchaseOrderItemID *uuid.UUID) error
|
||||||
CreateProductMovement(ctx context.Context, productID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error
|
CreateProductMovement(ctx context.Context, productID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ func NewInventoryMovementService(inventoryMovementRepo repository.InventoryMovem
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InventoryMovementServiceImpl) CreateIngredientMovement(ctx context.Context, ingredientID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error {
|
func (s *InventoryMovementServiceImpl) CreateIngredientMovement(ctx context.Context, ingredientID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID, purchaseOrderItemID *uuid.UUID) error {
|
||||||
ingredient, err := s.ingredientRepo.GetByID(ctx, ingredientID, organizationID)
|
ingredient, err := s.ingredientRepo.GetByID(ctx, ingredientID, organizationID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -49,6 +49,7 @@ func (s *InventoryMovementServiceImpl) CreateIngredientMovement(ctx context.Cont
|
|||||||
TotalCost: unitCost * quantity,
|
TotalCost: unitCost * quantity,
|
||||||
ReferenceType: referenceType,
|
ReferenceType: referenceType,
|
||||||
ReferenceID: referenceID,
|
ReferenceID: referenceID,
|
||||||
|
PurchaseOrderItemID: purchaseOrderItemID,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
Reason: &reason,
|
Reason: &reason,
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
|
|||||||
@ -12,6 +12,7 @@ func CreatePurchaseOrderRequestToModel(req *contract.CreatePurchaseOrderRequest)
|
|||||||
for i, item := range req.Items {
|
for i, item := range req.Items {
|
||||||
items[i] = models.CreatePurchaseOrderItemRequest{
|
items[i] = models.CreatePurchaseOrderItemRequest{
|
||||||
IngredientID: item.IngredientID,
|
IngredientID: item.IngredientID,
|
||||||
|
PurchaseCategoryID: item.PurchaseCategoryID,
|
||||||
Description: item.Description,
|
Description: item.Description,
|
||||||
Quantity: item.Quantity,
|
Quantity: item.Quantity,
|
||||||
UnitID: item.UnitID,
|
UnitID: item.UnitID,
|
||||||
@ -56,6 +57,7 @@ func UpdatePurchaseOrderRequestToModel(req *contract.UpdatePurchaseOrderRequest)
|
|||||||
items[i] = models.UpdatePurchaseOrderItemRequest{
|
items[i] = models.UpdatePurchaseOrderItemRequest{
|
||||||
ID: item.ID,
|
ID: item.ID,
|
||||||
IngredientID: item.IngredientID,
|
IngredientID: item.IngredientID,
|
||||||
|
PurchaseCategoryID: item.PurchaseCategoryID,
|
||||||
Description: item.Description,
|
Description: item.Description,
|
||||||
Quantity: item.Quantity,
|
Quantity: item.Quantity,
|
||||||
UnitID: item.UnitID,
|
UnitID: item.UnitID,
|
||||||
@ -157,6 +159,7 @@ func PurchaseOrderModelResponseToResponse(po *models.PurchaseOrderResponse) *con
|
|||||||
ID: item.ID,
|
ID: item.ID,
|
||||||
PurchaseOrderID: item.PurchaseOrderID,
|
PurchaseOrderID: item.PurchaseOrderID,
|
||||||
IngredientID: item.IngredientID,
|
IngredientID: item.IngredientID,
|
||||||
|
PurchaseCategoryID: item.PurchaseCategoryID,
|
||||||
Description: item.Description,
|
Description: item.Description,
|
||||||
Quantity: item.Quantity,
|
Quantity: item.Quantity,
|
||||||
UnitID: item.UnitID,
|
UnitID: item.UnitID,
|
||||||
@ -173,6 +176,10 @@ func PurchaseOrderModelResponseToResponse(po *models.PurchaseOrderResponse) *con
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if item.PurchaseCategory != nil {
|
||||||
|
response.Items[i].PurchaseCategory = PurchaseCategoryModelResponseToResponse(item.PurchaseCategory)
|
||||||
|
}
|
||||||
|
|
||||||
// Map unit if present
|
// Map unit if present
|
||||||
if item.Unit != nil {
|
if item.Unit != nil {
|
||||||
response.Items[i].Unit = &contract.UnitResponse{
|
response.Items[i].Unit = &contract.UnitResponse{
|
||||||
|
|||||||
@ -2,11 +2,14 @@ package validator
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"apskel-pos-be/internal/constants"
|
"apskel-pos-be/internal/constants"
|
||||||
"apskel-pos-be/internal/contract"
|
"apskel-pos-be/internal/contract"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PurchaseOrderValidator interface {
|
type PurchaseOrderValidator interface {
|
||||||
@ -26,7 +29,7 @@ func (v *PurchaseOrderValidatorImpl) ValidateCreatePurchaseOrderRequest(req *con
|
|||||||
return errors.New("request body is required"), constants.MissingFieldErrorCode
|
return errors.New("request body is required"), constants.MissingFieldErrorCode
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.VendorID.String() == "" {
|
if req.VendorID == uuid.Nil {
|
||||||
return errors.New("vendor_id is required"), constants.MissingFieldErrorCode
|
return errors.New("vendor_id is required"), constants.MissingFieldErrorCode
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,32 +181,40 @@ func (v *PurchaseOrderValidatorImpl) ValidateListPurchaseOrdersRequest(req *cont
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (v *PurchaseOrderValidatorImpl) validatePurchaseOrderItem(item *contract.CreatePurchaseOrderItemRequest, index int) (error, string) {
|
func (v *PurchaseOrderValidatorImpl) validatePurchaseOrderItem(item *contract.CreatePurchaseOrderItemRequest, index int) (error, string) {
|
||||||
if item.IngredientID.String() == "" {
|
if item.IngredientID == uuid.Nil {
|
||||||
return errors.New("items[" + string(rune(index)) + "].ingredient_id is required"), constants.MissingFieldErrorCode
|
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 {
|
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() == "" {
|
if item.UnitID == uuid.Nil {
|
||||||
return errors.New("items[" + string(rune(index)) + "].unit_id is required"), constants.MissingFieldErrorCode
|
return errors.New("items[" + strconv.Itoa(index) + "].unit_id is required"), constants.MissingFieldErrorCode
|
||||||
}
|
}
|
||||||
|
|
||||||
if item.Amount < 0 {
|
if item.Amount < 0 {
|
||||||
return errors.New("items[" + string(rune(index)) + "].amount must be greater than or equal to 0"), constants.MalformedFieldErrorCode
|
return errors.New("items[" + strconv.Itoa(index) + "].amount must be greater than or equal to 0"), constants.MalformedFieldErrorCode
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, ""
|
return nil, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *PurchaseOrderValidatorImpl) validateUpdatePurchaseOrderItem(item *contract.UpdatePurchaseOrderItemRequest, index int) (error, string) {
|
func (v *PurchaseOrderValidatorImpl) validateUpdatePurchaseOrderItem(item *contract.UpdatePurchaseOrderItemRequest, index int) (error, string) {
|
||||||
|
if item.PurchaseCategoryID == nil || *item.PurchaseCategoryID == uuid.Nil {
|
||||||
|
return errors.New("items[" + strconv.Itoa(index) + "].purchase_category_id is required"), constants.MissingFieldErrorCode
|
||||||
|
}
|
||||||
|
|
||||||
if item.Quantity != nil && *item.Quantity <= 0 {
|
if item.Quantity != nil && *item.Quantity <= 0 {
|
||||||
return errors.New("items[" + string(rune(index)) + "].quantity must be greater than 0"), constants.MalformedFieldErrorCode
|
return errors.New("items[" + strconv.Itoa(index) + "].quantity must be greater than 0"), constants.MalformedFieldErrorCode
|
||||||
}
|
}
|
||||||
|
|
||||||
if item.Amount != nil && *item.Amount < 0 {
|
if item.Amount != nil && *item.Amount < 0 {
|
||||||
return errors.New("items[" + string(rune(index)) + "].amount must be greater than or equal to 0"), constants.MalformedFieldErrorCode
|
return errors.New("items[" + strconv.Itoa(index) + "].amount must be greater than or equal to 0"), constants.MalformedFieldErrorCode
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, ""
|
return nil, ""
|
||||||
|
|||||||
@ -18,6 +18,7 @@ func validCreatePurchaseOrderRequest() *contract.CreatePurchaseOrderRequest {
|
|||||||
Items: []contract.CreatePurchaseOrderItemRequest{
|
Items: []contract.CreatePurchaseOrderItemRequest{
|
||||||
{
|
{
|
||||||
IngredientID: uuid.New(),
|
IngredientID: uuid.New(),
|
||||||
|
PurchaseCategoryID: uuid.New(),
|
||||||
Quantity: 1,
|
Quantity: 1,
|
||||||
UnitID: uuid.New(),
|
UnitID: uuid.New(),
|
||||||
Amount: 1000,
|
Amount: 1000,
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
DROP INDEX IF EXISTS idx_inventory_movements_purchase_order_item_id;
|
||||||
|
ALTER TABLE inventory_movements DROP COLUMN IF EXISTS purchase_order_item_id;
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS idx_purchase_order_items_purchase_category_id;
|
||||||
|
ALTER TABLE purchase_order_items DROP COLUMN IF EXISTS purchase_category_id;
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
ALTER TABLE purchase_order_items
|
||||||
|
ADD COLUMN IF NOT EXISTS purchase_category_id UUID REFERENCES purchase_categories(id) ON DELETE RESTRICT;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchase_order_items_purchase_category_id
|
||||||
|
ON purchase_order_items(purchase_category_id);
|
||||||
|
|
||||||
|
ALTER TABLE inventory_movements
|
||||||
|
ADD COLUMN IF NOT EXISTS purchase_order_item_id UUID REFERENCES purchase_order_items(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_inventory_movements_purchase_order_item_id
|
||||||
|
ON inventory_movements(purchase_order_item_id);
|
||||||
Loading…
x
Reference in New Issue
Block a user