Fix expense to be nullable without raw material.
This commit is contained in:
parent
d0c090a657
commit
1718c5adab
@ -107,10 +107,10 @@ type PurchasingAnalyticsResponse struct {
|
|||||||
type PurchasingSummary struct {
|
type PurchasingSummary struct {
|
||||||
TotalPurchases float64 `json:"total_purchases"`
|
TotalPurchases float64 `json:"total_purchases"`
|
||||||
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||||
NonInventoryPurchases float64 `json:"non_inventory_purchases"`
|
ExpensePurchases float64 `json:"expense_purchases"`
|
||||||
TotalPurchaseOrders int64 `json:"total_purchase_orders"`
|
TotalPurchaseOrders int64 `json:"total_purchase_orders"`
|
||||||
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
||||||
NonInventoryExpenseCount int64 `json:"non_inventory_expense_count"`
|
ExpenseCount int64 `json:"expense_count"`
|
||||||
TotalQuantity float64 `json:"total_quantity"`
|
TotalQuantity float64 `json:"total_quantity"`
|
||||||
AveragePurchaseOrderValue float64 `json:"average_purchase_order_value"`
|
AveragePurchaseOrderValue float64 `json:"average_purchase_order_value"`
|
||||||
TotalIngredients int64 `json:"total_ingredients"`
|
TotalIngredients int64 `json:"total_ingredients"`
|
||||||
@ -121,10 +121,10 @@ type PurchasingAnalyticsData struct {
|
|||||||
Date time.Time `json:"date"`
|
Date time.Time `json:"date"`
|
||||||
Purchases float64 `json:"purchases"`
|
Purchases float64 `json:"purchases"`
|
||||||
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||||
NonInventoryPurchases float64 `json:"non_inventory_purchases"`
|
ExpensePurchases float64 `json:"expense_purchases"`
|
||||||
PurchaseOrders int64 `json:"purchase_orders"`
|
PurchaseOrders int64 `json:"purchase_orders"`
|
||||||
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
||||||
NonInventoryExpenseCount int64 `json:"non_inventory_expense_count"`
|
ExpenseCount int64 `json:"expense_count"`
|
||||||
Quantity float64 `json:"quantity"`
|
Quantity float64 `json:"quantity"`
|
||||||
Ingredients int64 `json:"ingredients"`
|
Ingredients int64 `json:"ingredients"`
|
||||||
Vendors int64 `json:"vendors"`
|
Vendors int64 `json:"vendors"`
|
||||||
|
|||||||
@ -10,7 +10,7 @@ type CreatePurchaseCategoryRequest struct {
|
|||||||
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||||
Code *string `json:"code,omitempty"`
|
Code *string `json:"code,omitempty"`
|
||||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||||
Type string `json:"type" validate:"required,oneof=raw_material non_inventory"`
|
Type string `json:"type" validate:"required,oneof=raw_material expense"`
|
||||||
SortOrder *int `json:"sort_order,omitempty"`
|
SortOrder *int `json:"sort_order,omitempty"`
|
||||||
IsActive *bool `json:"is_active,omitempty"`
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
}
|
}
|
||||||
@ -19,14 +19,14 @@ type UpdatePurchaseCategoryRequest struct {
|
|||||||
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||||
Code *string `json:"code,omitempty"`
|
Code *string `json:"code,omitempty"`
|
||||||
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
|
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
|
||||||
Type *string `json:"type,omitempty" validate:"omitempty,oneof=raw_material non_inventory"`
|
Type *string `json:"type,omitempty" validate:"omitempty,oneof=raw_material expense"`
|
||||||
SortOrder *int `json:"sort_order,omitempty"`
|
SortOrder *int `json:"sort_order,omitempty"`
|
||||||
IsActive *bool `json:"is_active,omitempty"`
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListPurchaseCategoriesRequest struct {
|
type ListPurchaseCategoriesRequest struct {
|
||||||
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||||
Type string `json:"type,omitempty" validate:"omitempty,oneof=raw_material non_inventory"`
|
Type string `json:"type,omitempty" validate:"omitempty,oneof=raw_material expense"`
|
||||||
Search string `json:"search,omitempty"`
|
Search string `json:"search,omitempty"`
|
||||||
IsActive *bool `json:"is_active,omitempty"`
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
Page int `json:"page" validate:"required,min=1"`
|
Page int `json:"page" validate:"required,min=1"`
|
||||||
|
|||||||
@ -19,11 +19,11 @@ type CreatePurchaseOrderRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreatePurchaseOrderItemRequest struct {
|
type CreatePurchaseOrderItemRequest struct {
|
||||||
IngredientID uuid.UUID `json:"ingredient_id" validate:"required"`
|
IngredientID *uuid.UUID `json:"ingredient_id,omitempty" validate:"omitempty"`
|
||||||
PurchaseCategoryID uuid.UUID `json:"purchase_category_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,omitempty" validate:"omitempty,gt=0"`
|
||||||
UnitID uuid.UUID `json:"unit_id" validate:"required"`
|
UnitID *uuid.UUID `json:"unit_id,omitempty" validate:"omitempty"`
|
||||||
Amount float64 `json:"amount" validate:"required,gte=0"`
|
Amount float64 `json:"amount" validate:"required,gte=0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,11 +70,11 @@ type PurchaseOrderResponse struct {
|
|||||||
type PurchaseOrderItemResponse struct {
|
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"`
|
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"`
|
||||||
Amount float64 `json:"amount"`
|
Amount float64 `json:"amount"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|||||||
@ -39,10 +39,10 @@ type PurchasingAnalytics struct {
|
|||||||
type PurchasingSummary struct {
|
type PurchasingSummary struct {
|
||||||
TotalPurchases float64 `json:"total_purchases"`
|
TotalPurchases float64 `json:"total_purchases"`
|
||||||
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||||
NonInventoryPurchases float64 `json:"non_inventory_purchases"`
|
ExpensePurchases float64 `json:"expense_purchases"`
|
||||||
TotalPurchaseOrders int64 `json:"total_purchase_orders"`
|
TotalPurchaseOrders int64 `json:"total_purchase_orders"`
|
||||||
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
||||||
NonInventoryExpenseCount int64 `json:"non_inventory_expense_count"`
|
ExpenseCount int64 `json:"expense_count"`
|
||||||
TotalQuantity float64 `json:"total_quantity"`
|
TotalQuantity float64 `json:"total_quantity"`
|
||||||
AveragePurchaseOrderValue float64 `json:"average_purchase_order_value"`
|
AveragePurchaseOrderValue float64 `json:"average_purchase_order_value"`
|
||||||
TotalIngredients int64 `json:"total_ingredients"`
|
TotalIngredients int64 `json:"total_ingredients"`
|
||||||
@ -53,10 +53,10 @@ type PurchasingAnalyticsData struct {
|
|||||||
Date time.Time `json:"date"`
|
Date time.Time `json:"date"`
|
||||||
Purchases float64 `json:"purchases"`
|
Purchases float64 `json:"purchases"`
|
||||||
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||||
NonInventoryPurchases float64 `json:"non_inventory_purchases"`
|
ExpensePurchases float64 `json:"expense_purchases"`
|
||||||
PurchaseOrders int64 `json:"purchase_orders"`
|
PurchaseOrders int64 `json:"purchase_orders"`
|
||||||
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
||||||
NonInventoryExpenseCount int64 `json:"non_inventory_expense_count"`
|
ExpenseCount int64 `json:"expense_count"`
|
||||||
Quantity float64 `json:"quantity"`
|
Quantity float64 `json:"quantity"`
|
||||||
Ingredients int64 `json:"ingredients"`
|
Ingredients int64 `json:"ingredients"`
|
||||||
Vendors int64 `json:"vendors"`
|
Vendors int64 `json:"vendors"`
|
||||||
|
|||||||
@ -11,7 +11,7 @@ type PurchaseCategoryType string
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
PurchaseCategoryTypeRawMaterial PurchaseCategoryType = "raw_material"
|
PurchaseCategoryTypeRawMaterial PurchaseCategoryType = "raw_material"
|
||||||
PurchaseCategoryTypeNonInventory PurchaseCategoryType = "non_inventory"
|
PurchaseCategoryTypeExpense PurchaseCategoryType = "expense"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PurchaseCategoryPreset struct {
|
type PurchaseCategoryPreset struct {
|
||||||
|
|||||||
@ -43,11 +43,11 @@ func (PurchaseOrder) TableName() string {
|
|||||||
type PurchaseOrderItem struct {
|
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" json:"ingredient_id" validate:"omitempty"`
|
||||||
PurchaseCategoryID uuid.UUID `gorm:"type:uuid;not null;index" json:"purchase_category_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)" json:"quantity" validate:"omitempty,gt=0"`
|
||||||
UnitID uuid.UUID `gorm:"type:uuid;not null" json:"unit_id" validate:"required"`
|
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"`
|
Amount float64 `gorm:"type:decimal(15,2);not null" json:"amount" validate:"required,gte=0"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
|||||||
@ -114,10 +114,10 @@ type PurchasingAnalyticsResponse struct {
|
|||||||
type PurchasingSummary struct {
|
type PurchasingSummary struct {
|
||||||
TotalPurchases float64 `json:"total_purchases"`
|
TotalPurchases float64 `json:"total_purchases"`
|
||||||
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||||
NonInventoryPurchases float64 `json:"non_inventory_purchases"`
|
ExpensePurchases float64 `json:"expense_purchases"`
|
||||||
TotalPurchaseOrders int64 `json:"total_purchase_orders"`
|
TotalPurchaseOrders int64 `json:"total_purchase_orders"`
|
||||||
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
||||||
NonInventoryExpenseCount int64 `json:"non_inventory_expense_count"`
|
ExpenseCount int64 `json:"expense_count"`
|
||||||
TotalQuantity float64 `json:"total_quantity"`
|
TotalQuantity float64 `json:"total_quantity"`
|
||||||
AveragePurchaseOrderValue float64 `json:"average_purchase_order_value"`
|
AveragePurchaseOrderValue float64 `json:"average_purchase_order_value"`
|
||||||
TotalIngredients int64 `json:"total_ingredients"`
|
TotalIngredients int64 `json:"total_ingredients"`
|
||||||
@ -129,10 +129,10 @@ type PurchasingAnalyticsData struct {
|
|||||||
Date time.Time `json:"date"`
|
Date time.Time `json:"date"`
|
||||||
Purchases float64 `json:"purchases"`
|
Purchases float64 `json:"purchases"`
|
||||||
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||||
NonInventoryPurchases float64 `json:"non_inventory_purchases"`
|
ExpensePurchases float64 `json:"expense_purchases"`
|
||||||
PurchaseOrders int64 `json:"purchase_orders"`
|
PurchaseOrders int64 `json:"purchase_orders"`
|
||||||
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
||||||
NonInventoryExpenseCount int64 `json:"non_inventory_expense_count"`
|
ExpenseCount int64 `json:"expense_count"`
|
||||||
Quantity float64 `json:"quantity"`
|
Quantity float64 `json:"quantity"`
|
||||||
Ingredients int64 `json:"ingredients"`
|
Ingredients int64 `json:"ingredients"`
|
||||||
Vendors int64 `json:"vendors"`
|
Vendors int64 `json:"vendors"`
|
||||||
|
|||||||
@ -24,11 +24,11 @@ type PurchaseOrder struct {
|
|||||||
type PurchaseOrderItem struct {
|
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"`
|
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"`
|
||||||
Amount float64 `json:"amount"`
|
Amount float64 `json:"amount"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
@ -62,11 +62,11 @@ type PurchaseOrderResponse struct {
|
|||||||
type PurchaseOrderItemResponse struct {
|
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"`
|
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"`
|
||||||
Amount float64 `json:"amount"`
|
Amount float64 `json:"amount"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
@ -96,11 +96,11 @@ type CreatePurchaseOrderRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreatePurchaseOrderItemRequest struct {
|
type CreatePurchaseOrderItemRequest struct {
|
||||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
IngredientID *uuid.UUID `json:"ingredient_id,omitempty"`
|
||||||
PurchaseCategoryID uuid.UUID `json:"purchase_category_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,omitempty"`
|
||||||
UnitID uuid.UUID `json:"unit_id"`
|
UnitID *uuid.UUID `json:"unit_id,omitempty"`
|
||||||
Amount float64 `json:"amount"`
|
Amount float64 `json:"amount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -188,10 +188,10 @@ func (p *AnalyticsProcessorImpl) GetPurchasingAnalytics(ctx context.Context, req
|
|||||||
Date: item.Date,
|
Date: item.Date,
|
||||||
Purchases: item.Purchases,
|
Purchases: item.Purchases,
|
||||||
RawMaterialPurchases: item.RawMaterialPurchases,
|
RawMaterialPurchases: item.RawMaterialPurchases,
|
||||||
NonInventoryPurchases: item.NonInventoryPurchases,
|
ExpensePurchases: item.ExpensePurchases,
|
||||||
PurchaseOrders: item.PurchaseOrders,
|
PurchaseOrders: item.PurchaseOrders,
|
||||||
RawMaterialPurchaseOrders: item.RawMaterialPurchaseOrders,
|
RawMaterialPurchaseOrders: item.RawMaterialPurchaseOrders,
|
||||||
NonInventoryExpenseCount: item.NonInventoryExpenseCount,
|
ExpenseCount: item.ExpenseCount,
|
||||||
Quantity: item.Quantity,
|
Quantity: item.Quantity,
|
||||||
Ingredients: item.Ingredients,
|
Ingredients: item.Ingredients,
|
||||||
Vendors: item.Vendors,
|
Vendors: item.Vendors,
|
||||||
@ -232,10 +232,10 @@ func (p *AnalyticsProcessorImpl) GetPurchasingAnalytics(ctx context.Context, req
|
|||||||
Summary: models.PurchasingSummary{
|
Summary: models.PurchasingSummary{
|
||||||
TotalPurchases: result.Summary.TotalPurchases,
|
TotalPurchases: result.Summary.TotalPurchases,
|
||||||
RawMaterialPurchases: result.Summary.RawMaterialPurchases,
|
RawMaterialPurchases: result.Summary.RawMaterialPurchases,
|
||||||
NonInventoryPurchases: result.Summary.NonInventoryPurchases,
|
ExpensePurchases: result.Summary.ExpensePurchases,
|
||||||
TotalPurchaseOrders: result.Summary.TotalPurchaseOrders,
|
TotalPurchaseOrders: result.Summary.TotalPurchaseOrders,
|
||||||
RawMaterialPurchaseOrders: result.Summary.RawMaterialPurchaseOrders,
|
RawMaterialPurchaseOrders: result.Summary.RawMaterialPurchaseOrders,
|
||||||
NonInventoryExpenseCount: result.Summary.NonInventoryExpenseCount,
|
ExpenseCount: result.Summary.ExpenseCount,
|
||||||
TotalQuantity: result.Summary.TotalQuantity,
|
TotalQuantity: result.Summary.TotalQuantity,
|
||||||
AveragePurchaseOrderValue: result.Summary.AveragePurchaseOrderValue,
|
AveragePurchaseOrderValue: result.Summary.AveragePurchaseOrderValue,
|
||||||
TotalIngredients: result.Summary.TotalIngredients,
|
TotalIngredients: result.Summary.TotalIngredients,
|
||||||
|
|||||||
@ -77,20 +77,20 @@ func TestAnalyticsProcessorGetPurchasingAnalyticsPassesOutletName(t *testing.T)
|
|||||||
Summary: entities.PurchasingSummary{
|
Summary: entities.PurchasingSummary{
|
||||||
TotalPurchases: 300,
|
TotalPurchases: 300,
|
||||||
RawMaterialPurchases: 125,
|
RawMaterialPurchases: 125,
|
||||||
NonInventoryPurchases: 175,
|
ExpensePurchases: 175,
|
||||||
TotalPurchaseOrders: 3,
|
TotalPurchaseOrders: 3,
|
||||||
RawMaterialPurchaseOrders: 1,
|
RawMaterialPurchaseOrders: 1,
|
||||||
NonInventoryExpenseCount: 2,
|
ExpenseCount: 2,
|
||||||
},
|
},
|
||||||
Data: []entities.PurchasingAnalyticsData{
|
Data: []entities.PurchasingAnalyticsData{
|
||||||
{
|
{
|
||||||
Date: now,
|
Date: now,
|
||||||
Purchases: 300,
|
Purchases: 300,
|
||||||
RawMaterialPurchases: 125,
|
RawMaterialPurchases: 125,
|
||||||
NonInventoryPurchases: 175,
|
ExpensePurchases: 175,
|
||||||
PurchaseOrders: 3,
|
PurchaseOrders: 3,
|
||||||
RawMaterialPurchaseOrders: 1,
|
RawMaterialPurchaseOrders: 1,
|
||||||
NonInventoryExpenseCount: 2,
|
ExpenseCount: 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -110,14 +110,14 @@ func TestAnalyticsProcessorGetPurchasingAnalyticsPassesOutletName(t *testing.T)
|
|||||||
require.Equal(t, outletName, *result.OutletName)
|
require.Equal(t, outletName, *result.OutletName)
|
||||||
require.Equal(t, float64(300), result.Summary.TotalPurchases)
|
require.Equal(t, float64(300), result.Summary.TotalPurchases)
|
||||||
require.Equal(t, float64(125), result.Summary.RawMaterialPurchases)
|
require.Equal(t, float64(125), result.Summary.RawMaterialPurchases)
|
||||||
require.Equal(t, float64(175), result.Summary.NonInventoryPurchases)
|
require.Equal(t, float64(175), result.Summary.ExpensePurchases)
|
||||||
require.Equal(t, int64(3), result.Summary.TotalPurchaseOrders)
|
require.Equal(t, int64(3), result.Summary.TotalPurchaseOrders)
|
||||||
require.Equal(t, int64(1), result.Summary.RawMaterialPurchaseOrders)
|
require.Equal(t, int64(1), result.Summary.RawMaterialPurchaseOrders)
|
||||||
require.Equal(t, int64(2), result.Summary.NonInventoryExpenseCount)
|
require.Equal(t, int64(2), result.Summary.ExpenseCount)
|
||||||
require.Len(t, result.Data, 1)
|
require.Len(t, result.Data, 1)
|
||||||
require.Equal(t, float64(300), result.Data[0].Purchases)
|
require.Equal(t, float64(300), result.Data[0].Purchases)
|
||||||
require.Equal(t, float64(125), result.Data[0].RawMaterialPurchases)
|
require.Equal(t, float64(125), result.Data[0].RawMaterialPurchases)
|
||||||
require.Equal(t, float64(175), result.Data[0].NonInventoryPurchases)
|
require.Equal(t, float64(175), result.Data[0].ExpensePurchases)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAnalyticsProcessorGetProfitLossAnalyticsMapsOverviewAndReportFields(t *testing.T) {
|
func TestAnalyticsProcessorGetProfitLossAnalyticsMapsOverviewAndReportFields(t *testing.T) {
|
||||||
|
|||||||
@ -61,7 +61,7 @@ func (p *ExpenseProcessorImpl) CreateExpense(ctx context.Context, organizationID
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid purchase_category_id for item: %w", err)
|
return nil, fmt.Errorf("invalid purchase_category_id for item: %w", err)
|
||||||
}
|
}
|
||||||
if err := p.validateNonInventoryPurchaseCategory(ctx, purchaseCategoryID, organizationID); err != nil {
|
if err := p.validateExpensePurchaseCategory(ctx, purchaseCategoryID, organizationID); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,7 +169,7 @@ func (p *ExpenseProcessorImpl) UpdateExpense(ctx context.Context, id, organizati
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid purchase_category_id for item: %w", err)
|
return nil, fmt.Errorf("invalid purchase_category_id for item: %w", err)
|
||||||
}
|
}
|
||||||
if err := p.validateNonInventoryPurchaseCategory(ctx, purchaseCategoryID, organizationID); err != nil {
|
if err := p.validateExpensePurchaseCategory(ctx, purchaseCategoryID, organizationID); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -334,7 +334,7 @@ func (p *ExpenseProcessorImpl) GetExpenseAnalytics(ctx context.Context, req *mod
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ExpenseProcessorImpl) validateNonInventoryPurchaseCategory(ctx context.Context, categoryID, organizationID uuid.UUID) error {
|
func (p *ExpenseProcessorImpl) validateExpensePurchaseCategory(ctx context.Context, categoryID, organizationID uuid.UUID) error {
|
||||||
category, err := p.purchaseCategoryRepo.GetByIDAndOrganizationID(ctx, categoryID, organizationID)
|
category, err := p.purchaseCategoryRepo.GetByIDAndOrganizationID(ctx, categoryID, organizationID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("purchase category not found: %w", err)
|
return fmt.Errorf("purchase category not found: %w", err)
|
||||||
@ -344,8 +344,8 @@ func (p *ExpenseProcessorImpl) validateNonInventoryPurchaseCategory(ctx context.
|
|||||||
return fmt.Errorf("purchase category is inactive")
|
return fmt.Errorf("purchase category is inactive")
|
||||||
}
|
}
|
||||||
|
|
||||||
if category.Type != entities.PurchaseCategoryTypeNonInventory {
|
if category.Type != entities.PurchaseCategoryTypeExpense {
|
||||||
return fmt.Errorf("purchase category must be non_inventory")
|
return fmt.Errorf("purchase category must be expense")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@ -102,7 +102,7 @@ func (*expenseRepositoryCaptureStub) DeleteItemsByExpenseID(context.Context, uui
|
|||||||
func TestExpenseProcessorCreatePersistsItemName(t *testing.T) {
|
func TestExpenseProcessorCreatePersistsItemName(t *testing.T) {
|
||||||
repo := &expenseRepositoryCaptureStub{}
|
repo := &expenseRepositoryCaptureStub{}
|
||||||
purchaseCategoryID := uuid.New()
|
purchaseCategoryID := uuid.New()
|
||||||
p := NewExpenseProcessorImpl(repo, newExpensePurchaseCategoryRepo(purchaseCategoryID, entities.PurchaseCategoryTypeNonInventory))
|
p := NewExpenseProcessorImpl(repo, newExpensePurchaseCategoryRepo(purchaseCategoryID, entities.PurchaseCategoryTypeExpense))
|
||||||
chartOfAccountID := uuid.New()
|
chartOfAccountID := uuid.New()
|
||||||
|
|
||||||
resp, err := p.CreateExpense(context.Background(), uuid.New(), &models.CreateExpenseRequest{
|
resp, err := p.CreateExpense(context.Background(), uuid.New(), &models.CreateExpenseRequest{
|
||||||
@ -133,7 +133,7 @@ func TestExpenseProcessorCreatePersistsItemName(t *testing.T) {
|
|||||||
func TestExpenseProcessorCreateDefaultsStatusToDraft(t *testing.T) {
|
func TestExpenseProcessorCreateDefaultsStatusToDraft(t *testing.T) {
|
||||||
repo := &expenseRepositoryCaptureStub{}
|
repo := &expenseRepositoryCaptureStub{}
|
||||||
purchaseCategoryID := uuid.New()
|
purchaseCategoryID := uuid.New()
|
||||||
p := NewExpenseProcessorImpl(repo, newExpensePurchaseCategoryRepo(purchaseCategoryID, entities.PurchaseCategoryTypeNonInventory))
|
p := NewExpenseProcessorImpl(repo, newExpensePurchaseCategoryRepo(purchaseCategoryID, entities.PurchaseCategoryTypeExpense))
|
||||||
|
|
||||||
resp, err := p.CreateExpense(context.Background(), uuid.New(), &models.CreateExpenseRequest{
|
resp, err := p.CreateExpense(context.Background(), uuid.New(), &models.CreateExpenseRequest{
|
||||||
Receiver: "Cashier",
|
Receiver: "Cashier",
|
||||||
@ -160,7 +160,7 @@ func TestExpenseProcessorCreateDefaultsStatusToDraft(t *testing.T) {
|
|||||||
func TestExpenseProcessorCreatePersistsProvidedStatus(t *testing.T) {
|
func TestExpenseProcessorCreatePersistsProvidedStatus(t *testing.T) {
|
||||||
repo := &expenseRepositoryCaptureStub{}
|
repo := &expenseRepositoryCaptureStub{}
|
||||||
purchaseCategoryID := uuid.New()
|
purchaseCategoryID := uuid.New()
|
||||||
p := NewExpenseProcessorImpl(repo, newExpensePurchaseCategoryRepo(purchaseCategoryID, entities.PurchaseCategoryTypeNonInventory))
|
p := NewExpenseProcessorImpl(repo, newExpensePurchaseCategoryRepo(purchaseCategoryID, entities.PurchaseCategoryTypeExpense))
|
||||||
status := "approved"
|
status := "approved"
|
||||||
|
|
||||||
resp, err := p.CreateExpense(context.Background(), uuid.New(), &models.CreateExpenseRequest{
|
resp, err := p.CreateExpense(context.Background(), uuid.New(), &models.CreateExpenseRequest{
|
||||||
@ -209,7 +209,7 @@ func TestExpenseProcessorCreateRejectsRawMaterialPurchaseCategory(t *testing.T)
|
|||||||
|
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.Nil(t, resp)
|
require.Nil(t, resp)
|
||||||
require.Contains(t, err.Error(), "non_inventory")
|
require.Contains(t, err.Error(), "expense")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExpenseProcessorGetExpenseAnalyticsDefaultsGroupByAndMapsResponse(t *testing.T) {
|
func TestExpenseProcessorGetExpenseAnalyticsDefaultsGroupByAndMapsResponse(t *testing.T) {
|
||||||
@ -241,7 +241,7 @@ func TestExpenseProcessorGetExpenseAnalyticsDefaultsGroupByAndMapsResponse(t *te
|
|||||||
{
|
{
|
||||||
PurchaseCategoryID: purchaseCategoryID,
|
PurchaseCategoryID: purchaseCategoryID,
|
||||||
PurchaseCategoryName: "Operational Supplies",
|
PurchaseCategoryName: "Operational Supplies",
|
||||||
PurchaseCategoryType: "non_inventory",
|
PurchaseCategoryType: "expense",
|
||||||
TotalAmount: 100000,
|
TotalAmount: 100000,
|
||||||
ExpenseCount: 2,
|
ExpenseCount: 2,
|
||||||
ItemCount: 2,
|
ItemCount: 2,
|
||||||
@ -266,7 +266,7 @@ func TestExpenseProcessorGetExpenseAnalyticsDefaultsGroupByAndMapsResponse(t *te
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
p := NewExpenseProcessorImpl(repo, newExpensePurchaseCategoryRepo(purchaseCategoryID, entities.PurchaseCategoryTypeNonInventory))
|
p := NewExpenseProcessorImpl(repo, newExpensePurchaseCategoryRepo(purchaseCategoryID, entities.PurchaseCategoryTypeExpense))
|
||||||
|
|
||||||
resp, err := p.GetExpenseAnalytics(context.Background(), &models.ExpenseAnalyticsRequest{
|
resp, err := p.GetExpenseAnalytics(context.Background(), &models.ExpenseAnalyticsRequest{
|
||||||
OrganizationID: uuid.New(),
|
OrganizationID: uuid.New(),
|
||||||
|
|||||||
@ -67,21 +67,41 @@ 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, raw-material categories, and units exist
|
// Validate categories and inventory fields per item type.
|
||||||
for i, item := range req.Items {
|
for i, item := range req.Items {
|
||||||
_, err := p.ingredientRepo.GetByID(ctx, item.IngredientID, organizationID)
|
category, err := p.validatePurchaseCategory(ctx, item.PurchaseCategoryID, organizationID, i)
|
||||||
|
if 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 {
|
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 {
|
_, err = p.unitRepo.GetByID(ctx, *item.UnitID, organizationID)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, 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)
|
||||||
}
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate total amount
|
// Calculate total amount
|
||||||
@ -197,64 +217,58 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrder(ctx context.Context, id
|
|||||||
|
|
||||||
// Update items if provided
|
// Update items if provided
|
||||||
if req.Items != nil {
|
if req.Items != nil {
|
||||||
// Delete existing items
|
|
||||||
err = p.purchaseOrderRepo.DeleteItemsByPurchaseOrderID(ctx, poEntity.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to delete existing items: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new items
|
|
||||||
totalAmount := 0.0
|
totalAmount := 0.0
|
||||||
|
items := make([]*entities.PurchaseOrderItem, len(req.Items))
|
||||||
for i, itemReq := range req.Items {
|
for i, itemReq := range req.Items {
|
||||||
// Validate ingredients and units exist
|
if itemReq.PurchaseCategoryID == nil {
|
||||||
if itemReq.IngredientID != nil {
|
return nil, fmt.Errorf("purchase_category_id is required for item %d", i)
|
||||||
_, err := p.ingredientRepo.GetByID(ctx, *itemReq.IngredientID, organizationID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("ingredient not found: %w", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if itemReq.UnitID != nil {
|
ingredientID := itemReq.IngredientID
|
||||||
_, err := p.unitRepo.GetByID(ctx, *itemReq.UnitID, organizationID)
|
purchaseCategoryID := *itemReq.PurchaseCategoryID
|
||||||
if err != nil {
|
unitID := itemReq.UnitID
|
||||||
return nil, fmt.Errorf("unit not found: %w", err)
|
quantity := itemReq.Quantity
|
||||||
}
|
amount := 0.0
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
description := poEntity.Items[0].Description
|
|
||||||
|
|
||||||
if itemReq.IngredientID != nil {
|
|
||||||
ingredientID = *itemReq.IngredientID
|
|
||||||
}
|
|
||||||
if itemReq.UnitID != nil {
|
|
||||||
unitID = *itemReq.UnitID
|
|
||||||
}
|
|
||||||
if itemReq.PurchaseCategoryID != nil {
|
|
||||||
purchaseCategoryID = *itemReq.PurchaseCategoryID
|
|
||||||
}
|
|
||||||
if itemReq.Quantity != nil {
|
|
||||||
quantity = *itemReq.Quantity
|
|
||||||
}
|
|
||||||
if itemReq.Amount != nil {
|
if itemReq.Amount != nil {
|
||||||
amount = *itemReq.Amount
|
amount = *itemReq.Amount
|
||||||
}
|
}
|
||||||
if itemReq.Description != nil {
|
description := itemReq.Description
|
||||||
description = itemReq.Description
|
|
||||||
|
category, err := p.validatePurchaseCategory(ctx, purchaseCategoryID, organizationID, i)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
itemEntity := &entities.PurchaseOrderItem{
|
switch category.Type {
|
||||||
|
case entities.PurchaseCategoryTypeRawMaterial:
|
||||||
|
if ingredientID == nil {
|
||||||
|
return nil, fmt.Errorf("ingredient_id is required for raw_material item %d", i)
|
||||||
|
}
|
||||||
|
if quantity == nil {
|
||||||
|
return nil, fmt.Errorf("quantity is required for raw_material item %d", i)
|
||||||
|
}
|
||||||
|
if unitID == nil {
|
||||||
|
return nil, fmt.Errorf("unit_id is required for raw_material item %d", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := p.ingredientRepo.GetByID(ctx, *ingredientID, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ingredient not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = p.unitRepo.GetByID(ctx, *unitID, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unit not found: %w", err)
|
||||||
|
}
|
||||||
|
case entities.PurchaseCategoryTypeExpense:
|
||||||
|
if ingredientID != nil || quantity != nil || unitID != nil {
|
||||||
|
return nil, fmt.Errorf("ingredient_id, quantity, and unit_id must be empty for expense item %d", i)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("purchase category for item %d has unsupported type %s", i, category.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
items[i] = &entities.PurchaseOrderItem{
|
||||||
PurchaseOrderID: poEntity.ID,
|
PurchaseOrderID: poEntity.ID,
|
||||||
IngredientID: ingredientID,
|
IngredientID: ingredientID,
|
||||||
PurchaseCategoryID: purchaseCategoryID,
|
PurchaseCategoryID: purchaseCategoryID,
|
||||||
@ -263,13 +277,20 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrder(ctx context.Context, id
|
|||||||
UnitID: unitID,
|
UnitID: unitID,
|
||||||
Amount: amount,
|
Amount: amount,
|
||||||
}
|
}
|
||||||
|
totalAmount += amount
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete and recreate only after all replacement items are valid.
|
||||||
|
err = p.purchaseOrderRepo.DeleteItemsByPurchaseOrderID(ctx, poEntity.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to delete existing items: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, itemEntity := range items {
|
||||||
err = p.purchaseOrderRepo.CreateItem(ctx, itemEntity)
|
err = p.purchaseOrderRepo.CreateItem(ctx, itemEntity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create purchase order item: %w", err)
|
return nil, fmt.Errorf("failed to create purchase order item: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
totalAmount += amount
|
|
||||||
}
|
}
|
||||||
|
|
||||||
poEntity.TotalAmount = totalAmount
|
poEntity.TotalAmount = totalAmount
|
||||||
@ -398,19 +419,27 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrderStatus(ctx context.Conte
|
|||||||
|
|
||||||
// Update inventory for each item
|
// Update inventory for each item
|
||||||
for _, item := range poWithItems.Items {
|
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
|
// 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 {
|
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
|
// Convert quantity to ingredient's base unit if needed
|
||||||
quantityToAdd := item.Quantity
|
quantityToAdd := *item.Quantity
|
||||||
if item.UnitID != ingredient.UnitID {
|
if *item.UnitID != ingredient.UnitID {
|
||||||
// Convert from purchase unit to ingredient's base unit
|
// 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 {
|
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
|
quantityToAdd = convertedQuantity
|
||||||
}
|
}
|
||||||
@ -428,7 +457,7 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrderStatus(ctx context.Conte
|
|||||||
|
|
||||||
err = p.inventoryMovementService.CreateIngredientMovement(
|
err = p.inventoryMovementService.CreateIngredientMovement(
|
||||||
ctx,
|
ctx,
|
||||||
item.IngredientID,
|
*item.IngredientID,
|
||||||
organizationID,
|
organizationID,
|
||||||
outletID,
|
outletID,
|
||||||
userID,
|
userID,
|
||||||
@ -441,7 +470,7 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrderStatus(ctx context.Conte
|
|||||||
&item.ID,
|
&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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -461,19 +490,19 @@ 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 {
|
func (p *PurchaseOrderProcessorImpl) validatePurchaseCategory(ctx context.Context, categoryID, organizationID uuid.UUID, itemIndex int) (*entities.PurchaseCategory, error) {
|
||||||
category, err := p.purchaseCategoryRepo.GetByIDAndOrganizationID(ctx, categoryID, organizationID)
|
category, err := p.purchaseCategoryRepo.GetByIDAndOrganizationID(ctx, categoryID, organizationID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("purchase category not found for item %d: %w", itemIndex, err)
|
return nil, fmt.Errorf("purchase category not found for item %d: %w", itemIndex, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !category.IsActive {
|
if !category.IsActive {
|
||||||
return fmt.Errorf("purchase category for item %d is inactive", itemIndex)
|
return nil, fmt.Errorf("purchase category for item %d is inactive", itemIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
if category.Type != entities.PurchaseCategoryTypeRawMaterial {
|
if category.Type != entities.PurchaseCategoryTypeRawMaterial && category.Type != entities.PurchaseCategoryTypeExpense {
|
||||||
return fmt.Errorf("purchase category for item %d must be raw_material", itemIndex)
|
return nil, fmt.Errorf("purchase category for item %d must be raw_material or expense", itemIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return category, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -146,7 +146,7 @@ func (r *AnalyticsRepositoryImpl) GetPurchasingAnalytics(ctx context.Context, or
|
|||||||
}
|
}
|
||||||
|
|
||||||
rawMaterialOutletFilter := ""
|
rawMaterialOutletFilter := ""
|
||||||
nonInventoryOutletFilter := ""
|
expenseOutletFilter := ""
|
||||||
rawMaterialSummaryArgs := []interface{}{
|
rawMaterialSummaryArgs := []interface{}{
|
||||||
organizationID,
|
organizationID,
|
||||||
entities.InventoryMovementTypePurchase,
|
entities.InventoryMovementTypePurchase,
|
||||||
@ -155,20 +155,20 @@ func (r *AnalyticsRepositoryImpl) GetPurchasingAnalytics(ctx context.Context, or
|
|||||||
dateFrom,
|
dateFrom,
|
||||||
dateTo,
|
dateTo,
|
||||||
}
|
}
|
||||||
nonInventorySummaryArgs := []interface{}{
|
expenseSummaryArgs := []interface{}{
|
||||||
organizationID,
|
organizationID,
|
||||||
entities.PurchaseCategoryTypeNonInventory,
|
entities.PurchaseCategoryTypeExpense,
|
||||||
"approved",
|
"approved",
|
||||||
dateFrom,
|
dateFrom,
|
||||||
dateTo,
|
dateTo,
|
||||||
}
|
}
|
||||||
if outletID != nil {
|
if outletID != nil {
|
||||||
rawMaterialOutletFilter = "AND im.outlet_id = ?"
|
rawMaterialOutletFilter = "AND im.outlet_id = ?"
|
||||||
nonInventoryOutletFilter = "AND e.outlet_id = ?"
|
expenseOutletFilter = "AND e.outlet_id = ?"
|
||||||
rawMaterialSummaryArgs = append(rawMaterialSummaryArgs, *outletID)
|
rawMaterialSummaryArgs = append(rawMaterialSummaryArgs, *outletID)
|
||||||
nonInventorySummaryArgs = append(nonInventorySummaryArgs, *outletID)
|
expenseSummaryArgs = append(expenseSummaryArgs, *outletID)
|
||||||
}
|
}
|
||||||
summaryArgs := append(rawMaterialSummaryArgs, nonInventorySummaryArgs...)
|
summaryArgs := append(rawMaterialSummaryArgs, expenseSummaryArgs...)
|
||||||
|
|
||||||
summaryQuery := `
|
summaryQuery := `
|
||||||
WITH raw_material AS (
|
WITH raw_material AS (
|
||||||
@ -187,10 +187,10 @@ func (r *AnalyticsRepositoryImpl) GetPurchasingAnalytics(ctx context.Context, or
|
|||||||
AND im.created_at >= ? AND im.created_at <= ?
|
AND im.created_at >= ? AND im.created_at <= ?
|
||||||
` + rawMaterialOutletFilter + `
|
` + rawMaterialOutletFilter + `
|
||||||
),
|
),
|
||||||
non_inventory AS (
|
expense AS (
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(SUM(ei.amount), 0) as non_inventory_purchases,
|
COALESCE(SUM(ei.amount), 0) as expense_purchases,
|
||||||
COUNT(DISTINCT e.id) as non_inventory_expense_count
|
COUNT(DISTINCT e.id) as expense_count
|
||||||
FROM expense_items ei
|
FROM expense_items ei
|
||||||
JOIN expenses e ON ei.expense_id = e.id
|
JOIN expenses e ON ei.expense_id = e.id
|
||||||
JOIN purchase_categories pc ON ei.purchase_category_id = pc.id
|
JOIN purchase_categories pc ON ei.purchase_category_id = pc.id
|
||||||
@ -198,25 +198,25 @@ func (r *AnalyticsRepositoryImpl) GetPurchasingAnalytics(ctx context.Context, or
|
|||||||
AND pc.type = ?
|
AND pc.type = ?
|
||||||
AND e.status = ?
|
AND e.status = ?
|
||||||
AND e.transaction_date >= ? AND e.transaction_date <= ?
|
AND e.transaction_date >= ? AND e.transaction_date <= ?
|
||||||
` + nonInventoryOutletFilter + `
|
` + expenseOutletFilter + `
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
rm.raw_material_purchases + ni.non_inventory_purchases as total_purchases,
|
rm.raw_material_purchases + ex.expense_purchases as total_purchases,
|
||||||
rm.raw_material_purchases,
|
rm.raw_material_purchases,
|
||||||
ni.non_inventory_purchases,
|
ex.expense_purchases,
|
||||||
rm.raw_material_purchase_orders + ni.non_inventory_expense_count as total_purchase_orders,
|
rm.raw_material_purchase_orders + ex.expense_count as total_purchase_orders,
|
||||||
rm.raw_material_purchase_orders,
|
rm.raw_material_purchase_orders,
|
||||||
ni.non_inventory_expense_count,
|
ex.expense_count,
|
||||||
rm.total_quantity,
|
rm.total_quantity,
|
||||||
CASE
|
CASE
|
||||||
WHEN rm.raw_material_purchase_orders + ni.non_inventory_expense_count > 0
|
WHEN rm.raw_material_purchase_orders + ex.expense_count > 0
|
||||||
THEN (rm.raw_material_purchases + ni.non_inventory_purchases) / (rm.raw_material_purchase_orders + ni.non_inventory_expense_count)
|
THEN (rm.raw_material_purchases + ex.expense_purchases) / (rm.raw_material_purchase_orders + ex.expense_count)
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END as average_purchase_order_value,
|
END as average_purchase_order_value,
|
||||||
rm.total_ingredients,
|
rm.total_ingredients,
|
||||||
rm.total_vendors
|
rm.total_vendors
|
||||||
FROM raw_material rm
|
FROM raw_material rm
|
||||||
CROSS JOIN non_inventory ni
|
CROSS JOIN expense ex
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := r.db.WithContext(ctx).Raw(summaryQuery, summaryArgs...).Scan(&summary).Error; err != nil {
|
if err := r.db.WithContext(ctx).Raw(summaryQuery, summaryArgs...).Scan(&summary).Error; err != nil {
|
||||||
@ -235,14 +235,14 @@ func (r *AnalyticsRepositoryImpl) GetPurchasingAnalytics(ctx context.Context, or
|
|||||||
dateFormat = "DATE_TRUNC('day', im.created_at)::timestamp"
|
dateFormat = "DATE_TRUNC('day', im.created_at)::timestamp"
|
||||||
}
|
}
|
||||||
|
|
||||||
nonInventoryDateFormat := "DATE_TRUNC('day', e.transaction_date)::timestamp"
|
expenseDateFormat := "DATE_TRUNC('day', e.transaction_date)::timestamp"
|
||||||
switch groupBy {
|
switch groupBy {
|
||||||
case "hour":
|
case "hour":
|
||||||
nonInventoryDateFormat = "DATE_TRUNC('hour', e.transaction_date)::timestamp"
|
expenseDateFormat = "DATE_TRUNC('hour', e.transaction_date)::timestamp"
|
||||||
case "week":
|
case "week":
|
||||||
nonInventoryDateFormat = "DATE_TRUNC('week', e.transaction_date)::timestamp"
|
expenseDateFormat = "DATE_TRUNC('week', e.transaction_date)::timestamp"
|
||||||
case "month":
|
case "month":
|
||||||
nonInventoryDateFormat = "DATE_TRUNC('month', e.transaction_date)::timestamp"
|
expenseDateFormat = "DATE_TRUNC('month', e.transaction_date)::timestamp"
|
||||||
}
|
}
|
||||||
|
|
||||||
rawMaterialDataArgs := []interface{}{
|
rawMaterialDataArgs := []interface{}{
|
||||||
@ -253,18 +253,18 @@ func (r *AnalyticsRepositoryImpl) GetPurchasingAnalytics(ctx context.Context, or
|
|||||||
dateFrom,
|
dateFrom,
|
||||||
dateTo,
|
dateTo,
|
||||||
}
|
}
|
||||||
nonInventoryDataArgs := []interface{}{
|
expenseDataArgs := []interface{}{
|
||||||
organizationID,
|
organizationID,
|
||||||
entities.PurchaseCategoryTypeNonInventory,
|
entities.PurchaseCategoryTypeExpense,
|
||||||
"approved",
|
"approved",
|
||||||
dateFrom,
|
dateFrom,
|
||||||
dateTo,
|
dateTo,
|
||||||
}
|
}
|
||||||
if outletID != nil {
|
if outletID != nil {
|
||||||
rawMaterialDataArgs = append(rawMaterialDataArgs, *outletID)
|
rawMaterialDataArgs = append(rawMaterialDataArgs, *outletID)
|
||||||
nonInventoryDataArgs = append(nonInventoryDataArgs, *outletID)
|
expenseDataArgs = append(expenseDataArgs, *outletID)
|
||||||
}
|
}
|
||||||
dataArgs := append(rawMaterialDataArgs, nonInventoryDataArgs...)
|
dataArgs := append(rawMaterialDataArgs, expenseDataArgs...)
|
||||||
|
|
||||||
var data []entities.PurchasingAnalyticsData
|
var data []entities.PurchasingAnalyticsData
|
||||||
dataQuery := `
|
dataQuery := `
|
||||||
@ -286,11 +286,11 @@ func (r *AnalyticsRepositoryImpl) GetPurchasingAnalytics(ctx context.Context, or
|
|||||||
` + rawMaterialOutletFilter + `
|
` + rawMaterialOutletFilter + `
|
||||||
GROUP BY 1
|
GROUP BY 1
|
||||||
),
|
),
|
||||||
non_inventory AS (
|
expense AS (
|
||||||
SELECT
|
SELECT
|
||||||
` + nonInventoryDateFormat + ` as date,
|
` + expenseDateFormat + ` as date,
|
||||||
COALESCE(SUM(ei.amount), 0) as non_inventory_purchases,
|
COALESCE(SUM(ei.amount), 0) as expense_purchases,
|
||||||
COUNT(DISTINCT e.id) as non_inventory_expense_count
|
COUNT(DISTINCT e.id) as expense_count
|
||||||
FROM expense_items ei
|
FROM expense_items ei
|
||||||
JOIN expenses e ON ei.expense_id = e.id
|
JOIN expenses e ON ei.expense_id = e.id
|
||||||
JOIN purchase_categories pc ON ei.purchase_category_id = pc.id
|
JOIN purchase_categories pc ON ei.purchase_category_id = pc.id
|
||||||
@ -298,22 +298,22 @@ func (r *AnalyticsRepositoryImpl) GetPurchasingAnalytics(ctx context.Context, or
|
|||||||
AND pc.type = ?
|
AND pc.type = ?
|
||||||
AND e.status = ?
|
AND e.status = ?
|
||||||
AND e.transaction_date >= ? AND e.transaction_date <= ?
|
AND e.transaction_date >= ? AND e.transaction_date <= ?
|
||||||
` + nonInventoryOutletFilter + `
|
` + expenseOutletFilter + `
|
||||||
GROUP BY 1
|
GROUP BY 1
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(rm.date, ni.date) as date,
|
COALESCE(rm.date, ex.date) as date,
|
||||||
COALESCE(rm.raw_material_purchases, 0) + COALESCE(ni.non_inventory_purchases, 0) as purchases,
|
COALESCE(rm.raw_material_purchases, 0) + COALESCE(ex.expense_purchases, 0) as purchases,
|
||||||
COALESCE(rm.raw_material_purchases, 0) as raw_material_purchases,
|
COALESCE(rm.raw_material_purchases, 0) as raw_material_purchases,
|
||||||
COALESCE(ni.non_inventory_purchases, 0) as non_inventory_purchases,
|
COALESCE(ex.expense_purchases, 0) as expense_purchases,
|
||||||
COALESCE(rm.raw_material_purchase_orders, 0) + COALESCE(ni.non_inventory_expense_count, 0) as purchase_orders,
|
COALESCE(rm.raw_material_purchase_orders, 0) + COALESCE(ex.expense_count, 0) as purchase_orders,
|
||||||
COALESCE(rm.raw_material_purchase_orders, 0) as raw_material_purchase_orders,
|
COALESCE(rm.raw_material_purchase_orders, 0) as raw_material_purchase_orders,
|
||||||
COALESCE(ni.non_inventory_expense_count, 0) as non_inventory_expense_count,
|
COALESCE(ex.expense_count, 0) as expense_count,
|
||||||
COALESCE(rm.quantity, 0) as quantity,
|
COALESCE(rm.quantity, 0) as quantity,
|
||||||
COALESCE(rm.ingredients, 0) as ingredients,
|
COALESCE(rm.ingredients, 0) as ingredients,
|
||||||
COALESCE(rm.vendors, 0) as vendors
|
COALESCE(rm.vendors, 0) as vendors
|
||||||
FROM raw_material rm
|
FROM raw_material rm
|
||||||
FULL OUTER JOIN non_inventory ni ON rm.date = ni.date
|
FULL OUTER JOIN expense ex ON rm.date = ex.date
|
||||||
ORDER BY date
|
ORDER BY date
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|||||||
@ -207,7 +207,7 @@ func (r *ExpenseRepositoryImpl) GetAnalytics(ctx context.Context, organizationID
|
|||||||
Joins("JOIN expenses e ON ei.expense_id = e.id").
|
Joins("JOIN expenses e ON ei.expense_id = e.id").
|
||||||
Joins("JOIN purchase_categories pc ON ei.purchase_category_id = pc.id").
|
Joins("JOIN purchase_categories pc ON ei.purchase_category_id = pc.id").
|
||||||
Where("e.organization_id = ?", organizationID).
|
Where("e.organization_id = ?", organizationID).
|
||||||
Where("pc.type = ?", entities.PurchaseCategoryTypeNonInventory).
|
Where("pc.type = ?", entities.PurchaseCategoryTypeExpense).
|
||||||
Where("e.status = ?", "approved").
|
Where("e.status = ?", "approved").
|
||||||
Where("e.transaction_date >= ? AND e.transaction_date <= ?", dateFrom, dateTo).
|
Where("e.transaction_date >= ? AND e.transaction_date <= ?", dateFrom, dateTo).
|
||||||
Group("pc.id, pc.name, pc.type").
|
Group("pc.id, pc.name, pc.type").
|
||||||
|
|||||||
@ -172,10 +172,10 @@ func PurchasingAnalyticsModelToContract(resp *models.PurchasingAnalyticsResponse
|
|||||||
Date: item.Date,
|
Date: item.Date,
|
||||||
Purchases: item.Purchases,
|
Purchases: item.Purchases,
|
||||||
RawMaterialPurchases: item.RawMaterialPurchases,
|
RawMaterialPurchases: item.RawMaterialPurchases,
|
||||||
NonInventoryPurchases: item.NonInventoryPurchases,
|
ExpensePurchases: item.ExpensePurchases,
|
||||||
PurchaseOrders: item.PurchaseOrders,
|
PurchaseOrders: item.PurchaseOrders,
|
||||||
RawMaterialPurchaseOrders: item.RawMaterialPurchaseOrders,
|
RawMaterialPurchaseOrders: item.RawMaterialPurchaseOrders,
|
||||||
NonInventoryExpenseCount: item.NonInventoryExpenseCount,
|
ExpenseCount: item.ExpenseCount,
|
||||||
Quantity: item.Quantity,
|
Quantity: item.Quantity,
|
||||||
Ingredients: item.Ingredients,
|
Ingredients: item.Ingredients,
|
||||||
Vendors: item.Vendors,
|
Vendors: item.Vendors,
|
||||||
@ -216,10 +216,10 @@ func PurchasingAnalyticsModelToContract(resp *models.PurchasingAnalyticsResponse
|
|||||||
Summary: contract.PurchasingSummary{
|
Summary: contract.PurchasingSummary{
|
||||||
TotalPurchases: resp.Summary.TotalPurchases,
|
TotalPurchases: resp.Summary.TotalPurchases,
|
||||||
RawMaterialPurchases: resp.Summary.RawMaterialPurchases,
|
RawMaterialPurchases: resp.Summary.RawMaterialPurchases,
|
||||||
NonInventoryPurchases: resp.Summary.NonInventoryPurchases,
|
ExpensePurchases: resp.Summary.ExpensePurchases,
|
||||||
TotalPurchaseOrders: resp.Summary.TotalPurchaseOrders,
|
TotalPurchaseOrders: resp.Summary.TotalPurchaseOrders,
|
||||||
RawMaterialPurchaseOrders: resp.Summary.RawMaterialPurchaseOrders,
|
RawMaterialPurchaseOrders: resp.Summary.RawMaterialPurchaseOrders,
|
||||||
NonInventoryExpenseCount: resp.Summary.NonInventoryExpenseCount,
|
ExpenseCount: resp.Summary.ExpenseCount,
|
||||||
TotalQuantity: resp.Summary.TotalQuantity,
|
TotalQuantity: resp.Summary.TotalQuantity,
|
||||||
AveragePurchaseOrderValue: resp.Summary.AveragePurchaseOrderValue,
|
AveragePurchaseOrderValue: resp.Summary.AveragePurchaseOrderValue,
|
||||||
TotalIngredients: resp.Summary.TotalIngredients,
|
TotalIngredients: resp.Summary.TotalIngredients,
|
||||||
|
|||||||
@ -61,20 +61,20 @@ func TestPurchasingAnalyticsModelToContractCopiesOutletName(t *testing.T) {
|
|||||||
Summary: models.PurchasingSummary{
|
Summary: models.PurchasingSummary{
|
||||||
TotalPurchases: 300,
|
TotalPurchases: 300,
|
||||||
RawMaterialPurchases: 125,
|
RawMaterialPurchases: 125,
|
||||||
NonInventoryPurchases: 175,
|
ExpensePurchases: 175,
|
||||||
TotalPurchaseOrders: 3,
|
TotalPurchaseOrders: 3,
|
||||||
RawMaterialPurchaseOrders: 1,
|
RawMaterialPurchaseOrders: 1,
|
||||||
NonInventoryExpenseCount: 2,
|
ExpenseCount: 2,
|
||||||
},
|
},
|
||||||
Data: []models.PurchasingAnalyticsData{
|
Data: []models.PurchasingAnalyticsData{
|
||||||
{
|
{
|
||||||
Date: now,
|
Date: now,
|
||||||
Purchases: 300,
|
Purchases: 300,
|
||||||
RawMaterialPurchases: 125,
|
RawMaterialPurchases: 125,
|
||||||
NonInventoryPurchases: 175,
|
ExpensePurchases: 175,
|
||||||
PurchaseOrders: 3,
|
PurchaseOrders: 3,
|
||||||
RawMaterialPurchaseOrders: 1,
|
RawMaterialPurchaseOrders: 1,
|
||||||
NonInventoryExpenseCount: 2,
|
ExpenseCount: 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -85,14 +85,14 @@ func TestPurchasingAnalyticsModelToContractCopiesOutletName(t *testing.T) {
|
|||||||
require.Equal(t, outletName, *result.OutletName)
|
require.Equal(t, outletName, *result.OutletName)
|
||||||
require.Equal(t, float64(300), result.Summary.TotalPurchases)
|
require.Equal(t, float64(300), result.Summary.TotalPurchases)
|
||||||
require.Equal(t, float64(125), result.Summary.RawMaterialPurchases)
|
require.Equal(t, float64(125), result.Summary.RawMaterialPurchases)
|
||||||
require.Equal(t, float64(175), result.Summary.NonInventoryPurchases)
|
require.Equal(t, float64(175), result.Summary.ExpensePurchases)
|
||||||
require.Equal(t, int64(3), result.Summary.TotalPurchaseOrders)
|
require.Equal(t, int64(3), result.Summary.TotalPurchaseOrders)
|
||||||
require.Equal(t, int64(1), result.Summary.RawMaterialPurchaseOrders)
|
require.Equal(t, int64(1), result.Summary.RawMaterialPurchaseOrders)
|
||||||
require.Equal(t, int64(2), result.Summary.NonInventoryExpenseCount)
|
require.Equal(t, int64(2), result.Summary.ExpenseCount)
|
||||||
require.Len(t, result.Data, 1)
|
require.Len(t, result.Data, 1)
|
||||||
require.Equal(t, float64(300), result.Data[0].Purchases)
|
require.Equal(t, float64(300), result.Data[0].Purchases)
|
||||||
require.Equal(t, float64(125), result.Data[0].RawMaterialPurchases)
|
require.Equal(t, float64(125), result.Data[0].RawMaterialPurchases)
|
||||||
require.Equal(t, float64(175), result.Data[0].NonInventoryPurchases)
|
require.Equal(t, float64(175), result.Data[0].ExpensePurchases)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPurchasingAnalyticsModelToContractOmitsNilOutletName(t *testing.T) {
|
func TestPurchasingAnalyticsModelToContractOmitsNilOutletName(t *testing.T) {
|
||||||
|
|||||||
@ -12,15 +12,19 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestCreatePurchaseOrderRequestToModelAllowsMissingDueDate(t *testing.T) {
|
func TestCreatePurchaseOrderRequestToModelAllowsMissingDueDate(t *testing.T) {
|
||||||
|
ingredientID := uuid.New()
|
||||||
|
quantity := 1.0
|
||||||
|
unitID := uuid.New()
|
||||||
|
|
||||||
result, err := CreatePurchaseOrderRequestToModel(&contract.CreatePurchaseOrderRequest{
|
result, err := CreatePurchaseOrderRequestToModel(&contract.CreatePurchaseOrderRequest{
|
||||||
VendorID: uuid.New(),
|
VendorID: uuid.New(),
|
||||||
PONumber: "PO-001",
|
PONumber: "PO-001",
|
||||||
TransactionDate: "2026-05-29",
|
TransactionDate: "2026-05-29",
|
||||||
Items: []contract.CreatePurchaseOrderItemRequest{
|
Items: []contract.CreatePurchaseOrderItemRequest{
|
||||||
{
|
{
|
||||||
IngredientID: uuid.New(),
|
IngredientID: &ingredientID,
|
||||||
Quantity: 1,
|
Quantity: &quantity,
|
||||||
UnitID: uuid.New(),
|
UnitID: &unitID,
|
||||||
Amount: 1000,
|
Amount: 1000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -34,7 +34,7 @@ func (v *PurchaseCategoryValidatorImpl) ValidateCreatePurchaseCategoryRequest(re
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !isValidPurchaseCategoryType(req.Type) {
|
if !isValidPurchaseCategoryType(req.Type) {
|
||||||
return errors.New("type must be raw_material or non_inventory"), constants.MalformedFieldErrorCode
|
return errors.New("type must be raw_material or expense"), constants.MalformedFieldErrorCode
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Code != nil && len(strings.TrimSpace(*req.Code)) > 100 {
|
if req.Code != nil && len(strings.TrimSpace(*req.Code)) > 100 {
|
||||||
@ -63,7 +63,7 @@ func (v *PurchaseCategoryValidatorImpl) ValidateUpdatePurchaseCategoryRequest(re
|
|||||||
}
|
}
|
||||||
|
|
||||||
if req.Type != nil && !isValidPurchaseCategoryType(*req.Type) {
|
if req.Type != nil && !isValidPurchaseCategoryType(*req.Type) {
|
||||||
return errors.New("type must be raw_material or non_inventory"), constants.MalformedFieldErrorCode
|
return errors.New("type must be raw_material or expense"), constants.MalformedFieldErrorCode
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Code != nil && len(strings.TrimSpace(*req.Code)) > 100 {
|
if req.Code != nil && len(strings.TrimSpace(*req.Code)) > 100 {
|
||||||
@ -87,7 +87,7 @@ func (v *PurchaseCategoryValidatorImpl) ValidateListPurchaseCategoriesRequest(re
|
|||||||
}
|
}
|
||||||
|
|
||||||
if req.Type != "" && !isValidPurchaseCategoryType(req.Type) {
|
if req.Type != "" && !isValidPurchaseCategoryType(req.Type) {
|
||||||
return errors.New("type must be raw_material or non_inventory"), constants.MalformedFieldErrorCode
|
return errors.New("type must be raw_material or expense"), constants.MalformedFieldErrorCode
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, ""
|
return nil, ""
|
||||||
@ -95,7 +95,7 @@ func (v *PurchaseCategoryValidatorImpl) ValidateListPurchaseCategoriesRequest(re
|
|||||||
|
|
||||||
func isValidPurchaseCategoryType(categoryType string) bool {
|
func isValidPurchaseCategoryType(categoryType string) bool {
|
||||||
switch categoryType {
|
switch categoryType {
|
||||||
case "raw_material", "non_inventory":
|
case "raw_material", "expense":
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
|||||||
@ -181,20 +181,20 @@ 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 == uuid.Nil {
|
|
||||||
return errors.New("items[" + strconv.Itoa(index) + "].ingredient_id is required"), constants.MissingFieldErrorCode
|
|
||||||
}
|
|
||||||
|
|
||||||
if item.PurchaseCategoryID == uuid.Nil {
|
if item.PurchaseCategoryID == uuid.Nil {
|
||||||
return errors.New("items[" + strconv.Itoa(index) + "].purchase_category_id is required"), constants.MissingFieldErrorCode
|
return errors.New("items[" + strconv.Itoa(index) + "].purchase_category_id is required"), constants.MissingFieldErrorCode
|
||||||
}
|
}
|
||||||
|
|
||||||
if item.Quantity <= 0 {
|
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 {
|
||||||
return errors.New("items[" + strconv.Itoa(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 == uuid.Nil {
|
if item.UnitID != nil && *item.UnitID == uuid.Nil {
|
||||||
return errors.New("items[" + strconv.Itoa(index) + "].unit_id is required"), constants.MissingFieldErrorCode
|
return errors.New("items[" + strconv.Itoa(index) + "].unit_id cannot be empty"), constants.MalformedFieldErrorCode
|
||||||
}
|
}
|
||||||
|
|
||||||
if item.Amount < 0 {
|
if item.Amount < 0 {
|
||||||
@ -209,6 +209,14 @@ func (v *PurchaseOrderValidatorImpl) validateUpdatePurchaseOrderItem(item *contr
|
|||||||
return errors.New("items[" + strconv.Itoa(index) + "].purchase_category_id is required"), constants.MissingFieldErrorCode
|
return errors.New("items[" + strconv.Itoa(index) + "].purchase_category_id is required"), constants.MissingFieldErrorCode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if item.IngredientID != nil && *item.IngredientID == uuid.Nil {
|
||||||
|
return errors.New("items[" + strconv.Itoa(index) + "].ingredient_id cannot be empty"), constants.MalformedFieldErrorCode
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.UnitID != nil && *item.UnitID == uuid.Nil {
|
||||||
|
return errors.New("items[" + strconv.Itoa(index) + "].unit_id cannot be empty"), constants.MalformedFieldErrorCode
|
||||||
|
}
|
||||||
|
|
||||||
if item.Quantity != nil && *item.Quantity <= 0 {
|
if item.Quantity != nil && *item.Quantity <= 0 {
|
||||||
return errors.New("items[" + strconv.Itoa(index) + "].quantity must be greater than 0"), constants.MalformedFieldErrorCode
|
return errors.New("items[" + strconv.Itoa(index) + "].quantity must be greater than 0"), constants.MalformedFieldErrorCode
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,16 +11,20 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func validCreatePurchaseOrderRequest() *contract.CreatePurchaseOrderRequest {
|
func validCreatePurchaseOrderRequest() *contract.CreatePurchaseOrderRequest {
|
||||||
|
ingredientID := uuid.New()
|
||||||
|
quantity := 1.0
|
||||||
|
unitID := uuid.New()
|
||||||
|
|
||||||
return &contract.CreatePurchaseOrderRequest{
|
return &contract.CreatePurchaseOrderRequest{
|
||||||
VendorID: uuid.New(),
|
VendorID: uuid.New(),
|
||||||
PONumber: "PO-001",
|
PONumber: "PO-001",
|
||||||
TransactionDate: "2026-05-29",
|
TransactionDate: "2026-05-29",
|
||||||
Items: []contract.CreatePurchaseOrderItemRequest{
|
Items: []contract.CreatePurchaseOrderItemRequest{
|
||||||
{
|
{
|
||||||
IngredientID: uuid.New(),
|
IngredientID: &ingredientID,
|
||||||
PurchaseCategoryID: uuid.New(),
|
PurchaseCategoryID: uuid.New(),
|
||||||
Quantity: 1,
|
Quantity: &quantity,
|
||||||
UnitID: uuid.New(),
|
UnitID: &unitID,
|
||||||
Amount: 1000,
|
Amount: 1000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -3,7 +3,7 @@ CREATE TABLE purchase_category_presets (
|
|||||||
parent_id UUID REFERENCES purchase_category_presets(id) ON DELETE SET NULL,
|
parent_id UUID REFERENCES purchase_category_presets(id) ON DELETE SET NULL,
|
||||||
code VARCHAR(100) NOT NULL UNIQUE,
|
code VARCHAR(100) NOT NULL UNIQUE,
|
||||||
name VARCHAR(255) NOT NULL,
|
name VARCHAR(255) NOT NULL,
|
||||||
type VARCHAR(20) NOT NULL CHECK (type IN ('raw_material', 'non_inventory')),
|
type VARCHAR(20) NOT NULL CHECK (type IN ('raw_material', 'expense')),
|
||||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
@ -17,7 +17,7 @@ CREATE TABLE purchase_categories (
|
|||||||
parent_id UUID REFERENCES purchase_categories(id) ON DELETE SET NULL,
|
parent_id UUID REFERENCES purchase_categories(id) ON DELETE SET NULL,
|
||||||
code VARCHAR(100) NOT NULL,
|
code VARCHAR(100) NOT NULL,
|
||||||
name VARCHAR(255) NOT NULL,
|
name VARCHAR(255) NOT NULL,
|
||||||
type VARCHAR(20) NOT NULL CHECK (type IN ('raw_material', 'non_inventory')),
|
type VARCHAR(20) NOT NULL CHECK (type IN ('raw_material', 'expense')),
|
||||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||||
is_system BOOLEAN NOT NULL DEFAULT false,
|
is_system BOOLEAN NOT NULL DEFAULT false,
|
||||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||||
@ -38,8 +38,8 @@ CREATE INDEX idx_purchase_categories_is_active ON purchase_categories(is_active)
|
|||||||
|
|
||||||
INSERT INTO purchase_category_presets (code, name, type, sort_order)
|
INSERT INTO purchase_category_presets (code, name, type, sort_order)
|
||||||
VALUES
|
VALUES
|
||||||
('hpp', 'HPP', 'raw_material', 1),
|
('bahan_baku', 'Bahan Baku', 'raw_material', 1),
|
||||||
('biaya_lain_lain', 'Biaya Lain-lain', 'non_inventory', 2)
|
('biaya_lain_lain', 'Biaya Lain-lain', 'expense', 2)
|
||||||
ON CONFLICT (code) DO NOTHING;
|
ON CONFLICT (code) DO NOTHING;
|
||||||
|
|
||||||
INSERT INTO purchase_category_presets (parent_id, code, name, type, sort_order)
|
INSERT INTO purchase_category_presets (parent_id, code, name, type, sort_order)
|
||||||
@ -47,18 +47,14 @@ SELECT parent.id, child.code, child.name, child.type, child.sort_order
|
|||||||
FROM purchase_category_presets parent
|
FROM purchase_category_presets parent
|
||||||
JOIN (
|
JOIN (
|
||||||
VALUES
|
VALUES
|
||||||
('hpp', 'hpp_bakso_mie_ayam', 'Bakso & Mie Ayam', 'raw_material', 1),
|
('biaya_lain_lain', 'biaya_atk_perlengkapan', 'ATK & Perlengkapan', 'expense', 1),
|
||||||
('hpp', 'hpp_nusantara', 'Nusantara', 'raw_material', 2),
|
('biaya_lain_lain', 'biaya_makan_karyawan', 'Makan Karyawan', 'expense', 2),
|
||||||
('hpp', 'hpp_ramen', 'Ramen', 'raw_material', 3),
|
('biaya_lain_lain', 'biaya_bensin_parkir', 'Bensin & Parkir', 'expense', 3),
|
||||||
('hpp', 'hpp_minuman_kopi', 'Minuman/Kopi', 'raw_material', 4),
|
('biaya_lain_lain', 'biaya_kebersihan_keamanan', 'Kebersihan & Keamanan', 'expense', 4),
|
||||||
('biaya_lain_lain', 'biaya_atk_perlengkapan', 'ATK & Perlengkapan', 'non_inventory', 1),
|
('biaya_lain_lain', 'biaya_gaji_dw', 'Gaji DW', 'expense', 5),
|
||||||
('biaya_lain_lain', 'biaya_makan_karyawan', 'Makan Karyawan', 'non_inventory', 2),
|
('biaya_lain_lain', 'biaya_gaji_staff', 'Gaji Staff', 'expense', 6),
|
||||||
('biaya_lain_lain', 'biaya_bensin_parkir', 'Bensin & Parkir', 'non_inventory', 3),
|
('biaya_lain_lain', 'biaya_internet_server', 'Internet & Server', 'expense', 7),
|
||||||
('biaya_lain_lain', 'biaya_kebersihan_keamanan', 'Kebersihan & Keamanan', 'non_inventory', 4),
|
('biaya_lain_lain', 'biaya_air_listrik', 'Air & Listrik', 'expense', 8),
|
||||||
('biaya_lain_lain', 'biaya_gaji_dw', 'Gaji DW', 'non_inventory', 5),
|
('biaya_lain_lain', 'biaya_promosi', 'Promosi', 'expense', 9)
|
||||||
('biaya_lain_lain', 'biaya_gaji_staff', 'Gaji Staff', 'non_inventory', 6),
|
|
||||||
('biaya_lain_lain', 'biaya_internet_server', 'Internet & Server', 'non_inventory', 7),
|
|
||||||
('biaya_lain_lain', 'biaya_air_listrik', 'Air & Listrik', 'non_inventory', 8),
|
|
||||||
('biaya_lain_lain', 'biaya_promosi', 'Promosi', 'non_inventory', 9)
|
|
||||||
) AS child(parent_code, code, name, type, sort_order) ON parent.code = child.parent_code
|
) AS child(parent_code, code, name, type, sort_order) ON parent.code = child.parent_code
|
||||||
ON CONFLICT (code) DO NOTHING;
|
ON CONFLICT (code) DO NOTHING;
|
||||||
|
|||||||
@ -0,0 +1,32 @@
|
|||||||
|
ALTER TABLE purchase_category_presets DROP CONSTRAINT IF EXISTS purchase_category_presets_type_check;
|
||||||
|
ALTER TABLE purchase_categories DROP CONSTRAINT IF EXISTS purchase_categories_type_check;
|
||||||
|
|
||||||
|
UPDATE purchase_category_presets
|
||||||
|
SET type = 'non_inventory'
|
||||||
|
WHERE type = 'expense';
|
||||||
|
|
||||||
|
UPDATE purchase_categories
|
||||||
|
SET type = 'non_inventory'
|
||||||
|
WHERE type = 'expense';
|
||||||
|
|
||||||
|
UPDATE purchase_category_presets
|
||||||
|
SET code = 'hpp', name = 'HPP'
|
||||||
|
WHERE code = 'bahan_baku' AND type = 'raw_material';
|
||||||
|
|
||||||
|
UPDATE purchase_categories
|
||||||
|
SET code = 'hpp', name = 'HPP'
|
||||||
|
WHERE code = 'bahan_baku' AND type = 'raw_material';
|
||||||
|
|
||||||
|
UPDATE purchase_category_presets
|
||||||
|
SET is_active = true
|
||||||
|
WHERE code LIKE 'hpp\_%' ESCAPE '\' AND type = 'raw_material';
|
||||||
|
|
||||||
|
UPDATE purchase_categories
|
||||||
|
SET is_active = true
|
||||||
|
WHERE code LIKE 'hpp\_%' ESCAPE '\' AND type = 'raw_material';
|
||||||
|
|
||||||
|
ALTER TABLE purchase_category_presets
|
||||||
|
ADD CONSTRAINT purchase_category_presets_type_check CHECK (type IN ('raw_material', 'non_inventory'));
|
||||||
|
|
||||||
|
ALTER TABLE purchase_categories
|
||||||
|
ADD CONSTRAINT purchase_categories_type_check CHECK (type IN ('raw_material', 'non_inventory'));
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
ALTER TABLE purchase_category_presets DROP CONSTRAINT IF EXISTS purchase_category_presets_type_check;
|
||||||
|
ALTER TABLE purchase_categories DROP CONSTRAINT IF EXISTS purchase_categories_type_check;
|
||||||
|
|
||||||
|
UPDATE purchase_category_presets
|
||||||
|
SET type = 'expense'
|
||||||
|
WHERE type = 'non_inventory';
|
||||||
|
|
||||||
|
UPDATE purchase_categories
|
||||||
|
SET type = 'expense'
|
||||||
|
WHERE type = 'non_inventory';
|
||||||
|
|
||||||
|
UPDATE purchase_category_presets
|
||||||
|
SET code = 'bahan_baku', name = 'Bahan Baku'
|
||||||
|
WHERE code = 'hpp' AND type = 'raw_material';
|
||||||
|
|
||||||
|
UPDATE purchase_categories
|
||||||
|
SET code = 'bahan_baku', name = 'Bahan Baku'
|
||||||
|
WHERE code = 'hpp' AND type = 'raw_material';
|
||||||
|
|
||||||
|
UPDATE purchase_category_presets
|
||||||
|
SET is_active = false
|
||||||
|
WHERE code LIKE 'hpp\_%' ESCAPE '\' AND type = 'raw_material';
|
||||||
|
|
||||||
|
UPDATE purchase_categories
|
||||||
|
SET is_active = false
|
||||||
|
WHERE code LIKE 'hpp\_%' ESCAPE '\' AND type = 'raw_material';
|
||||||
|
|
||||||
|
ALTER TABLE purchase_category_presets
|
||||||
|
ADD CONSTRAINT purchase_category_presets_type_check CHECK (type IN ('raw_material', 'expense'));
|
||||||
|
|
||||||
|
ALTER TABLE purchase_categories
|
||||||
|
ADD CONSTRAINT purchase_categories_type_check CHECK (type IN ('raw_material', 'expense'));
|
||||||
|
|
||||||
|
ALTER TABLE purchase_order_items ALTER COLUMN ingredient_id DROP NOT NULL;
|
||||||
|
ALTER TABLE purchase_order_items ALTER COLUMN quantity DROP NOT NULL;
|
||||||
|
ALTER TABLE purchase_order_items ALTER COLUMN unit_id DROP NOT NULL;
|
||||||
Loading…
x
Reference in New Issue
Block a user