feature/exclusive-summary #15
@ -19,12 +19,12 @@ type CreatePurchaseOrderRequest struct {
|
||||
}
|
||||
|
||||
type CreatePurchaseOrderItemRequest struct {
|
||||
IngredientID *uuid.UUID `json:"ingredient_id,omitempty" validate:"omitempty"`
|
||||
PurchaseCategoryID uuid.UUID `json:"purchase_category_id" validate:"required"`
|
||||
Description *string `json:"description,omitempty" validate:"omitempty"`
|
||||
Quantity *float64 `json:"quantity,omitempty" validate:"omitempty,gt=0"`
|
||||
UnitID *uuid.UUID `json:"unit_id,omitempty" validate:"omitempty"`
|
||||
Amount float64 `json:"amount" validate:"required,gte=0"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id" validate:"required"`
|
||||
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 {
|
||||
@ -70,11 +70,11 @@ type PurchaseOrderResponse struct {
|
||||
type PurchaseOrderItemResponse struct {
|
||||
ID uuid.UUID `json:"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"`
|
||||
Quantity *float64 `json:"quantity"`
|
||||
UnitID *uuid.UUID `json:"unit_id"`
|
||||
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"`
|
||||
|
||||
@ -41,16 +41,16 @@ func (PurchaseOrder) TableName() string {
|
||||
}
|
||||
|
||||
type PurchaseOrderItem struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||
PurchaseOrderID uuid.UUID `gorm:"type:uuid;not null" json:"purchase_order_id" validate:"required"`
|
||||
IngredientID *uuid.UUID `gorm:"type:uuid" json:"ingredient_id" validate:"omitempty"`
|
||||
PurchaseCategoryID uuid.UUID `gorm:"type:uuid;not null;index" json:"purchase_category_id" validate:"required"`
|
||||
Description *string `gorm:"type:text" json:"description" validate:"omitempty"`
|
||||
Quantity *float64 `gorm:"type:decimal(10,3)" json:"quantity" validate:"omitempty,gt=0"`
|
||||
UnitID *uuid.UUID `gorm:"type:uuid" json:"unit_id" validate:"omitempty"`
|
||||
Amount float64 `gorm:"type:decimal(15,2);not null" json:"amount" validate:"required,gte=0"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||
PurchaseOrderID uuid.UUID `gorm:"type:uuid;not null" json:"purchase_order_id" validate:"required"`
|
||||
IngredientID uuid.UUID `gorm:"type:uuid;not null" json:"ingredient_id" validate:"required"`
|
||||
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"`
|
||||
|
||||
@ -22,16 +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"`
|
||||
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"`
|
||||
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 {
|
||||
@ -62,11 +62,11 @@ type PurchaseOrderResponse struct {
|
||||
type PurchaseOrderItemResponse struct {
|
||||
ID uuid.UUID `json:"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"`
|
||||
Quantity *float64 `json:"quantity"`
|
||||
UnitID *uuid.UUID `json:"unit_id"`
|
||||
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"`
|
||||
@ -96,12 +96,12 @@ type CreatePurchaseOrderRequest struct {
|
||||
}
|
||||
|
||||
type CreatePurchaseOrderItemRequest struct {
|
||||
IngredientID *uuid.UUID `json:"ingredient_id,omitempty"`
|
||||
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Quantity *float64 `json:"quantity,omitempty"`
|
||||
UnitID *uuid.UUID `json:"unit_id,omitempty"`
|
||||
Amount float64 `json:"amount"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
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 {
|
||||
|
||||
@ -67,40 +67,20 @@ func (p *PurchaseOrderProcessorImpl) CreatePurchaseOrder(ctx context.Context, or
|
||||
return nil, fmt.Errorf("purchase order with PO number %s already exists in this organization", req.PONumber)
|
||||
}
|
||||
|
||||
// Validate categories and inventory fields per item type.
|
||||
// Purchase orders are raw-material only because they affect ingredient stock.
|
||||
for i, item := range req.Items {
|
||||
category, err := p.validatePurchaseCategory(ctx, item.PurchaseCategoryID, organizationID, i)
|
||||
if err != nil {
|
||||
if err := p.validateRawMaterialPurchaseCategory(ctx, item.PurchaseCategoryID, organizationID, i); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch category.Type {
|
||||
case entities.PurchaseCategoryTypeRawMaterial:
|
||||
if item.IngredientID == nil {
|
||||
return nil, fmt.Errorf("ingredient_id is required for raw_material item %d", i)
|
||||
}
|
||||
if item.Quantity == nil {
|
||||
return nil, fmt.Errorf("quantity is required for raw_material item %d", i)
|
||||
}
|
||||
if item.UnitID == nil {
|
||||
return nil, fmt.Errorf("unit_id is required for raw_material item %d", i)
|
||||
}
|
||||
_, err := p.ingredientRepo.GetByID(ctx, item.IngredientID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ingredient not found for item %d: %w", i, err)
|
||||
}
|
||||
|
||||
_, err := p.ingredientRepo.GetByID(ctx, *item.IngredientID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ingredient not found for item %d: %w", i, err)
|
||||
}
|
||||
|
||||
_, err = p.unitRepo.GetByID(ctx, *item.UnitID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unit not found for item %d: %w", i, err)
|
||||
}
|
||||
case entities.PurchaseCategoryTypeExpense:
|
||||
if item.IngredientID != nil || item.Quantity != nil || item.UnitID != nil {
|
||||
return nil, fmt.Errorf("ingredient_id, quantity, and unit_id must be empty for expense item %d", i)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("purchase category for item %d has unsupported type %s", i, category.Type)
|
||||
_, err = p.unitRepo.GetByID(ctx, item.UnitID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unit not found for item %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -224,48 +204,38 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrder(ctx context.Context, id
|
||||
return nil, fmt.Errorf("purchase_category_id is required for item %d", i)
|
||||
}
|
||||
|
||||
ingredientID := itemReq.IngredientID
|
||||
if itemReq.IngredientID == nil {
|
||||
return nil, fmt.Errorf("ingredient_id is required for raw_material item %d", i)
|
||||
}
|
||||
if itemReq.Quantity == nil {
|
||||
return nil, fmt.Errorf("quantity is required for raw_material item %d", i)
|
||||
}
|
||||
if itemReq.UnitID == nil {
|
||||
return nil, fmt.Errorf("unit_id is required for raw_material item %d", i)
|
||||
}
|
||||
|
||||
ingredientID := *itemReq.IngredientID
|
||||
purchaseCategoryID := *itemReq.PurchaseCategoryID
|
||||
unitID := itemReq.UnitID
|
||||
quantity := itemReq.Quantity
|
||||
unitID := *itemReq.UnitID
|
||||
quantity := *itemReq.Quantity
|
||||
amount := 0.0
|
||||
if itemReq.Amount != nil {
|
||||
amount = *itemReq.Amount
|
||||
}
|
||||
description := itemReq.Description
|
||||
|
||||
category, err := p.validatePurchaseCategory(ctx, purchaseCategoryID, organizationID, i)
|
||||
if err != nil {
|
||||
if err := p.validateRawMaterialPurchaseCategory(ctx, purchaseCategoryID, organizationID, i); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch category.Type {
|
||||
case entities.PurchaseCategoryTypeRawMaterial:
|
||||
if ingredientID == nil {
|
||||
return nil, fmt.Errorf("ingredient_id is required for raw_material item %d", i)
|
||||
}
|
||||
if quantity == nil {
|
||||
return nil, fmt.Errorf("quantity is required for raw_material item %d", i)
|
||||
}
|
||||
if unitID == nil {
|
||||
return nil, fmt.Errorf("unit_id is required for raw_material item %d", i)
|
||||
}
|
||||
_, err := p.ingredientRepo.GetByID(ctx, ingredientID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ingredient not found: %w", err)
|
||||
}
|
||||
|
||||
_, err := p.ingredientRepo.GetByID(ctx, *ingredientID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ingredient not found: %w", err)
|
||||
}
|
||||
|
||||
_, err = p.unitRepo.GetByID(ctx, *unitID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unit not found: %w", err)
|
||||
}
|
||||
case entities.PurchaseCategoryTypeExpense:
|
||||
if ingredientID != nil || quantity != nil || unitID != nil {
|
||||
return nil, fmt.Errorf("ingredient_id, quantity, and unit_id must be empty for expense item %d", i)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("purchase category for item %d has unsupported type %s", i, category.Type)
|
||||
_, err = p.unitRepo.GetByID(ctx, unitID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unit not found: %w", err)
|
||||
}
|
||||
|
||||
items[i] = &entities.PurchaseOrderItem{
|
||||
@ -407,8 +377,6 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrderStatus(ctx context.Conte
|
||||
return nil, fmt.Errorf("purchase order not found: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("status:", po.Status)
|
||||
|
||||
// Check if status is changing to "received" and current status is not "received"
|
||||
if status == "received" && po.Status != "received" {
|
||||
// Get purchase order with items for inventory update
|
||||
@ -419,27 +387,19 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrderStatus(ctx context.Conte
|
||||
|
||||
// Update inventory for each item
|
||||
for _, item := range poWithItems.Items {
|
||||
if item.PurchaseCategory != nil && item.PurchaseCategory.Type == entities.PurchaseCategoryTypeExpense {
|
||||
continue
|
||||
}
|
||||
|
||||
if item.IngredientID == nil || item.UnitID == nil || item.Quantity == nil {
|
||||
return nil, fmt.Errorf("purchase order item %s is missing raw material inventory fields", item.ID)
|
||||
}
|
||||
|
||||
// Get ingredient to find its base unit
|
||||
ingredient, err := p.ingredientRepo.GetByID(ctx, *item.IngredientID, organizationID)
|
||||
ingredient, err := p.ingredientRepo.GetByID(ctx, item.IngredientID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get ingredient %s: %w", *item.IngredientID, err)
|
||||
return nil, fmt.Errorf("failed to get ingredient %s: %w", item.IngredientID, err)
|
||||
}
|
||||
|
||||
// Convert quantity to ingredient's base unit if needed
|
||||
quantityToAdd := *item.Quantity
|
||||
if *item.UnitID != ingredient.UnitID {
|
||||
quantityToAdd := item.Quantity
|
||||
if item.UnitID != ingredient.UnitID {
|
||||
// Convert from purchase unit to ingredient's base unit
|
||||
convertedQuantity, err := p.unitConverterRepo.ConvertQuantity(ctx, *item.IngredientID, *item.UnitID, ingredient.UnitID, organizationID, *item.Quantity)
|
||||
convertedQuantity, err := p.unitConverterRepo.ConvertQuantity(ctx, item.IngredientID, item.UnitID, ingredient.UnitID, organizationID, item.Quantity)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert quantity for ingredient %s from unit %s to %s: %w", *item.IngredientID, *item.UnitID, ingredient.UnitID, err)
|
||||
return nil, fmt.Errorf("failed to convert quantity for ingredient %s from unit %s to %s: %w", item.IngredientID, item.UnitID, ingredient.UnitID, err)
|
||||
}
|
||||
quantityToAdd = convertedQuantity
|
||||
}
|
||||
@ -457,7 +417,7 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrderStatus(ctx context.Conte
|
||||
|
||||
err = p.inventoryMovementService.CreateIngredientMovement(
|
||||
ctx,
|
||||
*item.IngredientID,
|
||||
item.IngredientID,
|
||||
organizationID,
|
||||
outletID,
|
||||
userID,
|
||||
@ -470,7 +430,7 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrderStatus(ctx context.Conte
|
||||
&item.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create inventory movement for ingredient %s: %w", *item.IngredientID, err)
|
||||
return nil, fmt.Errorf("failed to create inventory movement for ingredient %s: %w", item.IngredientID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -490,19 +450,19 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrderStatus(ctx context.Conte
|
||||
return mappers.PurchaseOrderEntityToResponse(updatedPO), nil
|
||||
}
|
||||
|
||||
func (p *PurchaseOrderProcessorImpl) validatePurchaseCategory(ctx context.Context, categoryID, organizationID uuid.UUID, itemIndex int) (*entities.PurchaseCategory, error) {
|
||||
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 nil, fmt.Errorf("purchase category not found for item %d: %w", itemIndex, err)
|
||||
return fmt.Errorf("purchase category not found for item %d: %w", itemIndex, err)
|
||||
}
|
||||
|
||||
if !category.IsActive {
|
||||
return nil, fmt.Errorf("purchase category for item %d is inactive", itemIndex)
|
||||
return fmt.Errorf("purchase category for item %d is inactive", itemIndex)
|
||||
}
|
||||
|
||||
if category.Type != entities.PurchaseCategoryTypeRawMaterial && category.Type != entities.PurchaseCategoryTypeExpense {
|
||||
return nil, fmt.Errorf("purchase category for item %d must be raw_material or expense", itemIndex)
|
||||
if category.Type != entities.PurchaseCategoryTypeRawMaterial {
|
||||
return fmt.Errorf("purchase category for item %d must be raw_material", itemIndex)
|
||||
}
|
||||
|
||||
return category, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -152,7 +152,11 @@ func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx contex
|
||||
Table("purchase_orders po").
|
||||
Select(`
|
||||
COALESCE(SUM(poi.amount), 0) as total_purchases,
|
||||
COALESCE(SUM(poi.amount), 0) as raw_material_purchases,
|
||||
0 as expense_purchases,
|
||||
COUNT(DISTINCT po.id) as total_purchase_orders,
|
||||
COUNT(DISTINCT po.id) as raw_material_purchase_orders,
|
||||
0 as expense_count,
|
||||
COALESCE(SUM(poi.quantity), 0) as total_quantity,
|
||||
CASE
|
||||
WHEN COUNT(DISTINCT po.id) > 0
|
||||
@ -162,10 +166,12 @@ func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx contex
|
||||
COUNT(DISTINCT i.id) as total_ingredients,
|
||||
COUNT(DISTINCT po.vendor_id) as total_vendors
|
||||
`).
|
||||
Joins("LEFT JOIN purchase_order_items poi ON poi.purchase_order_id = po.id").
|
||||
Joins("LEFT JOIN ingredients i ON poi.ingredient_id = i.id").
|
||||
Joins("LEFT JOIN units u ON poi.unit_id = u.id").
|
||||
Joins("JOIN purchase_order_items poi ON poi.purchase_order_id = po.id").
|
||||
Joins("JOIN purchase_categories pc ON poi.purchase_category_id = pc.id").
|
||||
Joins("JOIN ingredients i ON poi.ingredient_id = i.id").
|
||||
Joins("JOIN units u ON poi.unit_id = u.id").
|
||||
Where("po.organization_id = ?", organizationID).
|
||||
Where("pc.type = ?", entities.PurchaseCategoryTypeRawMaterial).
|
||||
Where("po.status != ?", "cancelled").
|
||||
Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo)
|
||||
summaryQuery = r.applyPurchaseOrderItemOutletFilter(summaryQuery, outletID)
|
||||
@ -192,15 +198,21 @@ func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx contex
|
||||
Select(`
|
||||
`+dateFormat+` as date,
|
||||
COALESCE(SUM(poi.amount), 0) as purchases,
|
||||
COALESCE(SUM(poi.amount), 0) as raw_material_purchases,
|
||||
0 as expense_purchases,
|
||||
COUNT(DISTINCT po.id) as purchase_orders,
|
||||
COUNT(DISTINCT po.id) as raw_material_purchase_orders,
|
||||
0 as expense_count,
|
||||
COALESCE(SUM(poi.quantity), 0) as quantity,
|
||||
COUNT(DISTINCT i.id) as ingredients,
|
||||
COUNT(DISTINCT po.vendor_id) as vendors
|
||||
`).
|
||||
Joins("LEFT JOIN purchase_order_items poi ON poi.purchase_order_id = po.id").
|
||||
Joins("LEFT JOIN ingredients i ON poi.ingredient_id = i.id").
|
||||
Joins("LEFT JOIN units u ON poi.unit_id = u.id").
|
||||
Joins("JOIN purchase_order_items poi ON poi.purchase_order_id = po.id").
|
||||
Joins("JOIN purchase_categories pc ON poi.purchase_category_id = pc.id").
|
||||
Joins("JOIN ingredients i ON poi.ingredient_id = i.id").
|
||||
Joins("JOIN units u ON poi.unit_id = u.id").
|
||||
Where("po.organization_id = ?", organizationID).
|
||||
Where("pc.type = ?", entities.PurchaseCategoryTypeRawMaterial).
|
||||
Where("po.status != ?", "cancelled").
|
||||
Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo).
|
||||
Group(dateFormat).
|
||||
@ -227,9 +239,11 @@ func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx contex
|
||||
COUNT(DISTINCT po.id) as purchase_order_count
|
||||
`).
|
||||
Joins("JOIN purchase_orders po ON poi.purchase_order_id = po.id").
|
||||
Joins("JOIN purchase_categories pc ON poi.purchase_category_id = pc.id").
|
||||
Joins("JOIN ingredients i ON poi.ingredient_id = i.id").
|
||||
Joins("LEFT JOIN units u ON poi.unit_id = u.id").
|
||||
Where("po.organization_id = ?", organizationID).
|
||||
Where("pc.type = ?", entities.PurchaseCategoryTypeRawMaterial).
|
||||
Where("po.status != ?", "cancelled").
|
||||
Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo).
|
||||
Group("i.id, i.name").
|
||||
@ -252,10 +266,12 @@ func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx contex
|
||||
COALESCE(SUM(poi.quantity), 0) as quantity
|
||||
`).
|
||||
Joins("JOIN vendors v ON po.vendor_id = v.id").
|
||||
Joins("LEFT JOIN purchase_order_items poi ON poi.purchase_order_id = po.id").
|
||||
Joins("LEFT JOIN ingredients i ON poi.ingredient_id = i.id").
|
||||
Joins("LEFT JOIN units u ON poi.unit_id = u.id").
|
||||
Joins("JOIN purchase_order_items poi ON poi.purchase_order_id = po.id").
|
||||
Joins("JOIN purchase_categories pc ON poi.purchase_category_id = pc.id").
|
||||
Joins("JOIN ingredients i ON poi.ingredient_id = i.id").
|
||||
Joins("JOIN units u ON poi.unit_id = u.id").
|
||||
Where("po.organization_id = ?", organizationID).
|
||||
Where("pc.type = ?", entities.PurchaseCategoryTypeRawMaterial).
|
||||
Where("po.status != ?", "cancelled").
|
||||
Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo).
|
||||
Group("v.id, v.name").
|
||||
|
||||
@ -12,19 +12,15 @@ import (
|
||||
)
|
||||
|
||||
func TestCreatePurchaseOrderRequestToModelAllowsMissingDueDate(t *testing.T) {
|
||||
ingredientID := uuid.New()
|
||||
quantity := 1.0
|
||||
unitID := uuid.New()
|
||||
|
||||
result, err := CreatePurchaseOrderRequestToModel(&contract.CreatePurchaseOrderRequest{
|
||||
VendorID: uuid.New(),
|
||||
PONumber: "PO-001",
|
||||
TransactionDate: "2026-05-29",
|
||||
Items: []contract.CreatePurchaseOrderItemRequest{
|
||||
{
|
||||
IngredientID: &ingredientID,
|
||||
Quantity: &quantity,
|
||||
UnitID: &unitID,
|
||||
IngredientID: uuid.New(),
|
||||
Quantity: 1,
|
||||
UnitID: uuid.New(),
|
||||
Amount: 1000,
|
||||
},
|
||||
},
|
||||
|
||||
@ -181,20 +181,20 @@ 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 != nil && *item.IngredientID == uuid.Nil {
|
||||
return errors.New("items[" + strconv.Itoa(index) + "].ingredient_id cannot be empty"), constants.MalformedFieldErrorCode
|
||||
}
|
||||
|
||||
if item.Quantity != nil && *item.Quantity <= 0 {
|
||||
if item.Quantity <= 0 {
|
||||
return errors.New("items[" + strconv.Itoa(index) + "].quantity must be greater than 0"), constants.MalformedFieldErrorCode
|
||||
}
|
||||
|
||||
if item.UnitID != nil && *item.UnitID == uuid.Nil {
|
||||
return errors.New("items[" + strconv.Itoa(index) + "].unit_id cannot be empty"), constants.MalformedFieldErrorCode
|
||||
if item.UnitID == uuid.Nil {
|
||||
return errors.New("items[" + strconv.Itoa(index) + "].unit_id is required"), constants.MissingFieldErrorCode
|
||||
}
|
||||
|
||||
if item.Amount < 0 {
|
||||
@ -209,15 +209,15 @@ func (v *PurchaseOrderValidatorImpl) validateUpdatePurchaseOrderItem(item *contr
|
||||
return errors.New("items[" + strconv.Itoa(index) + "].purchase_category_id is required"), constants.MissingFieldErrorCode
|
||||
}
|
||||
|
||||
if item.IngredientID != nil && *item.IngredientID == uuid.Nil {
|
||||
return errors.New("items[" + strconv.Itoa(index) + "].ingredient_id cannot be empty"), constants.MalformedFieldErrorCode
|
||||
if item.IngredientID == nil || *item.IngredientID == uuid.Nil {
|
||||
return errors.New("items[" + strconv.Itoa(index) + "].ingredient_id is required"), constants.MissingFieldErrorCode
|
||||
}
|
||||
|
||||
if item.UnitID != nil && *item.UnitID == uuid.Nil {
|
||||
return errors.New("items[" + strconv.Itoa(index) + "].unit_id cannot be empty"), constants.MalformedFieldErrorCode
|
||||
if item.UnitID == nil || *item.UnitID == uuid.Nil {
|
||||
return errors.New("items[" + strconv.Itoa(index) + "].unit_id is required"), constants.MissingFieldErrorCode
|
||||
}
|
||||
|
||||
if item.Quantity != nil && *item.Quantity <= 0 {
|
||||
if item.Quantity == nil || *item.Quantity <= 0 {
|
||||
return errors.New("items[" + strconv.Itoa(index) + "].quantity must be greater than 0"), constants.MalformedFieldErrorCode
|
||||
}
|
||||
|
||||
|
||||
@ -11,26 +11,34 @@ import (
|
||||
)
|
||||
|
||||
func validCreatePurchaseOrderRequest() *contract.CreatePurchaseOrderRequest {
|
||||
ingredientID := uuid.New()
|
||||
quantity := 1.0
|
||||
unitID := uuid.New()
|
||||
|
||||
return &contract.CreatePurchaseOrderRequest{
|
||||
VendorID: uuid.New(),
|
||||
PONumber: "PO-001",
|
||||
TransactionDate: "2026-05-29",
|
||||
Items: []contract.CreatePurchaseOrderItemRequest{
|
||||
{
|
||||
IngredientID: &ingredientID,
|
||||
IngredientID: uuid.New(),
|
||||
PurchaseCategoryID: uuid.New(),
|
||||
Quantity: &quantity,
|
||||
UnitID: &unitID,
|
||||
Quantity: 1,
|
||||
UnitID: uuid.New(),
|
||||
Amount: 1000,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestPurchaseOrderValidatorCreateRejectsMissingRawMaterialFields(t *testing.T) {
|
||||
validator := NewPurchaseOrderValidator()
|
||||
req := validCreatePurchaseOrderRequest()
|
||||
req.Items[0].IngredientID = uuid.Nil
|
||||
|
||||
err, code := validator.ValidateCreatePurchaseOrderRequest(req)
|
||||
|
||||
require.Error(t, err)
|
||||
require.Equal(t, constants.MissingFieldErrorCode, code)
|
||||
require.Contains(t, err.Error(), "ingredient_id is required")
|
||||
}
|
||||
|
||||
func TestPurchaseOrderValidatorCreateAllowsMissingDueDate(t *testing.T) {
|
||||
validator := NewPurchaseOrderValidator()
|
||||
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
DROP TRIGGER IF EXISTS trigger_validate_purchase_order_item_raw_material ON purchase_order_items;
|
||||
DROP FUNCTION IF EXISTS validate_purchase_order_item_raw_material();
|
||||
|
||||
ALTER TABLE purchase_order_items
|
||||
ALTER COLUMN ingredient_id DROP NOT NULL,
|
||||
ALTER COLUMN quantity DROP NOT NULL,
|
||||
ALTER COLUMN unit_id DROP NOT NULL;
|
||||
@ -0,0 +1,41 @@
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM purchase_order_items poi
|
||||
JOIN purchase_categories pc ON pc.id = poi.purchase_category_id
|
||||
WHERE pc.type <> 'raw_material'
|
||||
OR poi.ingredient_id IS NULL
|
||||
OR poi.quantity IS NULL
|
||||
OR poi.unit_id IS NULL
|
||||
) THEN
|
||||
RAISE EXCEPTION 'purchase_order_items contains non-raw-material or incomplete raw-material rows. Move expense rows to expenses and fill ingredient_id, quantity, and unit_id before running this migration.';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
ALTER TABLE purchase_order_items
|
||||
ALTER COLUMN ingredient_id SET NOT NULL,
|
||||
ALTER COLUMN quantity SET NOT NULL,
|
||||
ALTER COLUMN unit_id SET NOT NULL;
|
||||
|
||||
CREATE OR REPLACE FUNCTION validate_purchase_order_item_raw_material()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM purchase_categories pc
|
||||
WHERE pc.id = NEW.purchase_category_id
|
||||
AND pc.type = 'raw_material'
|
||||
) THEN
|
||||
RAISE EXCEPTION 'purchase_order_items.purchase_category_id must reference a raw_material purchase category';
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS trigger_validate_purchase_order_item_raw_material ON purchase_order_items;
|
||||
CREATE TRIGGER trigger_validate_purchase_order_item_raw_material
|
||||
BEFORE INSERT OR UPDATE OF purchase_category_id ON purchase_order_items
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION validate_purchase_order_item_raw_material();
|
||||
Loading…
x
Reference in New Issue
Block a user