Compare commits

..
Author SHA1 Message Date
ryan 664847098e Merge pull request from main into feature/expense 2026-06-09 13:21:56 +07:00
43 changed files with 539 additions and 1121 deletions
+2 -2
View File
@@ -372,7 +372,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
ingredientProcessor: processor.NewIngredientProcessor(repos.ingredientRepo, repos.unitRepo, repos.ingredientCompositionRepo), ingredientProcessor: processor.NewIngredientProcessor(repos.ingredientRepo, repos.unitRepo, repos.ingredientCompositionRepo),
productRecipeProcessor: processor.NewProductRecipeProcessor(repos.productRecipeRepo, repos.productRepo, repos.ingredientRepo), productRecipeProcessor: processor.NewProductRecipeProcessor(repos.productRecipeRepo, repos.productRepo, repos.ingredientRepo),
vendorProcessor: processor.NewVendorProcessorImpl(repos.vendorRepo), vendorProcessor: processor.NewVendorProcessorImpl(repos.vendorRepo),
purchaseOrderProcessor: processor.NewPurchaseOrderProcessorImpl(repos.purchaseOrderRepo, repos.vendorRepo, repos.ingredientRepo, repos.purchaseCategoryRepo, repos.unitRepo, repos.fileRepo, inventoryMovementService, repos.unitConverterRepo), purchaseOrderProcessor: processor.NewPurchaseOrderProcessorImpl(repos.purchaseOrderRepo, repos.vendorRepo, repos.ingredientRepo, repos.unitRepo, repos.fileRepo, inventoryMovementService, repos.unitConverterRepo),
purchaseCategoryProcessor: processor.NewPurchaseCategoryProcessorImpl(repos.purchaseCategoryRepo), purchaseCategoryProcessor: processor.NewPurchaseCategoryProcessorImpl(repos.purchaseCategoryRepo),
unitConverterProcessor: processor.NewIngredientUnitConverterProcessorImpl(repos.unitConverterRepo, repos.ingredientRepo, repos.unitRepo), unitConverterProcessor: processor.NewIngredientUnitConverterProcessorImpl(repos.unitConverterRepo, repos.ingredientRepo, repos.unitRepo),
chartOfAccountTypeProcessor: processor.NewChartOfAccountTypeProcessorImpl(repos.chartOfAccountTypeRepo), chartOfAccountTypeProcessor: processor.NewChartOfAccountTypeProcessorImpl(repos.chartOfAccountTypeRepo),
@@ -396,7 +396,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
userDeviceProcessor: processor.NewUserDeviceProcessorImpl(repos.userDeviceRepo), userDeviceProcessor: processor.NewUserDeviceProcessorImpl(repos.userDeviceRepo),
notificationProcessor: buildNotificationProcessor(cfg, repos), notificationProcessor: buildNotificationProcessor(cfg, repos),
productOutletPriceProcessor: processor.NewProductOutletPriceProcessorImpl(repos.productOutletPriceRepo, repos.productRepo, repos.outletRepo), productOutletPriceProcessor: processor.NewProductOutletPriceProcessorImpl(repos.productOutletPriceRepo, repos.productRepo, repos.outletRepo),
expenseProcessor: processor.NewExpenseProcessorImpl(repos.expenseRepo, repos.purchaseCategoryRepo), expenseProcessor: processor.NewExpenseProcessorImpl(repos.expenseRepo),
} }
} }
+6 -14
View File
@@ -106,11 +106,7 @@ 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"`
ExpensePurchases float64 `json:"expense_purchases"`
TotalPurchaseOrders int64 `json:"total_purchase_orders"` TotalPurchaseOrders int64 `json:"total_purchase_orders"`
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
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"`
@@ -118,16 +114,12 @@ type PurchasingSummary struct {
} }
type PurchasingAnalyticsData struct { 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"` PurchaseOrders int64 `json:"purchase_orders"`
ExpensePurchases float64 `json:"expense_purchases"` Quantity float64 `json:"quantity"`
PurchaseOrders int64 `json:"purchase_orders"` Ingredients int64 `json:"ingredients"`
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"` Vendors int64 `json:"vendors"`
ExpenseCount int64 `json:"expense_count"`
Quantity float64 `json:"quantity"`
Ingredients int64 `json:"ingredients"`
Vendors int64 `json:"vendors"`
} }
type PurchasingIngredientData struct { type PurchasingIngredientData struct {
+26 -42
View File
@@ -19,11 +19,10 @@ type CreateExpenseRequest struct {
} }
type CreateExpenseItemRequest struct { type CreateExpenseItemRequest struct {
ChartOfAccountID string `json:"chart_of_account_id" validate:"required"` ChartOfAccountID string `json:"chart_of_account_id" validate:"required"`
PurchaseCategoryID string `json:"purchase_category_id" validate:"required"` Item string `json:"item" validate:"required"`
Item string `json:"item" validate:"required"` Description *string `json:"description,omitempty"`
Description *string `json:"description,omitempty"` Amount float64 `json:"amount" validate:"required"`
Amount float64 `json:"amount" validate:"required"`
} }
type UpdateExpenseRequest struct { type UpdateExpenseRequest struct {
@@ -40,11 +39,10 @@ type UpdateExpenseRequest struct {
} }
type UpdateExpenseItemRequest struct { type UpdateExpenseItemRequest struct {
ChartOfAccountID *string `json:"chart_of_account_id,omitempty"` ChartOfAccountID *string `json:"chart_of_account_id,omitempty"`
PurchaseCategoryID *string `json:"purchase_category_id,omitempty"` Item *string `json:"item,omitempty"`
Item *string `json:"item,omitempty"` Description *string `json:"description,omitempty"`
Description *string `json:"description,omitempty"` Amount *float64 `json:"amount,omitempty"`
Amount *float64 `json:"amount,omitempty"`
} }
type ExpenseResponse struct { type ExpenseResponse struct {
@@ -65,19 +63,15 @@ type ExpenseResponse struct {
} }
type ExpenseItemResponse struct { type ExpenseItemResponse struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
ExpenseID uuid.UUID `json:"expense_id"` ExpenseID uuid.UUID `json:"expense_id"`
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"` ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
ChartOfAccountName string `json:"chart_of_account_name,omitempty"` ChartOfAccountName string `json:"chart_of_account_name,omitempty"`
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"` Item string `json:"item"`
PurchaseCategoryName string `json:"purchase_category_name,omitempty"` Description *string `json:"description"`
PurchaseCategoryType string `json:"purchase_category_type,omitempty"` Amount float64 `json:"amount"`
PurchaseCategory *PurchaseCategoryResponse `json:"purchase_category,omitempty"` CreatedAt time.Time `json:"created_at"`
Item string `json:"item"` UpdatedAt time.Time `json:"updated_at"`
Description *string `json:"description"`
Amount float64 `json:"amount"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
} }
type ListExpenseRequest struct { type ListExpenseRequest struct {
@@ -106,16 +100,15 @@ type ExpenseAnalyticsRequest struct {
} }
type ExpenseAnalyticsResponse struct { type ExpenseAnalyticsResponse struct {
OrganizationID uuid.UUID `json:"organization_id"` OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id,omitempty"` OutletID *uuid.UUID `json:"outlet_id,omitempty"`
DateFrom time.Time `json:"date_from"` DateFrom time.Time `json:"date_from"`
DateTo time.Time `json:"date_to"` DateTo time.Time `json:"date_to"`
GroupBy string `json:"group_by"` GroupBy string `json:"group_by"`
Summary ExpenseAnalyticsSummary `json:"summary"` Summary ExpenseAnalyticsSummary `json:"summary"`
Data []ExpenseAnalyticsData `json:"data"` Data []ExpenseAnalyticsData `json:"data"`
CategoryData []ExpenseAnalyticsCategoryData `json:"category_data"` CategoryData []ExpenseAnalyticsCategoryData `json:"category_data"`
ChartOfAccountData []ExpenseAnalyticsChartOfAccountData `json:"chart_of_account_data"` ItemData []ExpenseAnalyticsItemData `json:"item_data"`
ItemData []ExpenseAnalyticsItemData `json:"item_data"`
} }
type ExpenseAnalyticsSummary struct { type ExpenseAnalyticsSummary struct {
@@ -137,15 +130,6 @@ type ExpenseAnalyticsData struct {
} }
type ExpenseAnalyticsCategoryData struct { type ExpenseAnalyticsCategoryData struct {
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
PurchaseCategoryName string `json:"purchase_category_name"`
PurchaseCategoryType string `json:"purchase_category_type"`
TotalAmount float64 `json:"total_amount"`
ExpenseCount int64 `json:"expense_count"`
ItemCount int64 `json:"item_count"`
}
type ExpenseAnalyticsChartOfAccountData struct {
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"` ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
ChartOfAccountName string `json:"chart_of_account_name"` ChartOfAccountName string `json:"chart_of_account_name"`
TotalAmount float64 `json:"total_amount"` TotalAmount float64 `json:"total_amount"`
@@ -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 expense"` Type string `json:"type" validate:"required,oneof=raw_material non_inventory"`
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 expense"` Type *string `json:"type,omitempty" validate:"omitempty,oneof=raw_material non_inventory"`
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 expense"` Type string `json:"type,omitempty" validate:"omitempty,oneof=raw_material non_inventory"`
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"`
+22 -26
View File
@@ -19,12 +19,11 @@ type CreatePurchaseOrderRequest struct {
} }
type CreatePurchaseOrderItemRequest struct { type CreatePurchaseOrderItemRequest struct {
IngredientID *uuid.UUID `json:"ingredient_id,omitempty" validate:"omitempty"` IngredientID uuid.UUID `json:"ingredient_id" validate:"required"`
PurchaseCategoryID uuid.UUID `json:"purchase_category_id" validate:"required"` Description *string `json:"description,omitempty" validate:"omitempty"`
Description *string `json:"description,omitempty" validate:"omitempty"` Quantity float64 `json:"quantity" validate:"required,gt=0"`
Quantity *float64 `json:"quantity,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"`
} }
type UpdatePurchaseOrderRequest struct { type UpdatePurchaseOrderRequest struct {
@@ -40,13 +39,12 @@ type UpdatePurchaseOrderRequest struct {
} }
type UpdatePurchaseOrderItemRequest struct { type UpdatePurchaseOrderItemRequest struct {
ID *uuid.UUID `json:"id,omitempty"` // For existing items ID *uuid.UUID `json:"id,omitempty"` // For existing items
IngredientID *uuid.UUID `json:"ingredient_id,omitempty" validate:"omitempty"` IngredientID *uuid.UUID `json:"ingredient_id,omitempty" validate:"omitempty"`
PurchaseCategoryID *uuid.UUID `json:"purchase_category_id,omitempty" validate:"omitempty"` Description *string `json:"description,omitempty" validate:"omitempty"`
Description *string `json:"description,omitempty" validate:"omitempty"` Quantity *float64 `json:"quantity,omitempty" validate:"omitempty,gt=0"`
Quantity *float64 `json:"quantity,omitempty" validate:"omitempty,gt=0"` UnitID *uuid.UUID `json:"unit_id,omitempty" validate:"omitempty"`
UnitID *uuid.UUID `json:"unit_id,omitempty" validate:"omitempty"` Amount *float64 `json:"amount,omitempty" validate:"omitempty,gte=0"`
Amount *float64 `json:"amount,omitempty" validate:"omitempty,gte=0"`
} }
type PurchaseOrderResponse struct { type PurchaseOrderResponse struct {
@@ -68,19 +66,17 @@ 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"` 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"` Ingredient *IngredientResponse `json:"ingredient,omitempty"`
Ingredient *IngredientResponse `json:"ingredient,omitempty"` Unit *UnitResponse `json:"unit,omitempty"`
PurchaseCategory *PurchaseCategoryResponse `json:"purchase_category,omitempty"`
Unit *UnitResponse `json:"unit,omitempty"`
} }
type PurchaseOrderAttachmentResponse struct { type PurchaseOrderAttachmentResponse struct {
+6 -14
View File
@@ -38,11 +38,7 @@ 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"`
ExpensePurchases float64 `json:"expense_purchases"`
TotalPurchaseOrders int64 `json:"total_purchase_orders"` TotalPurchaseOrders int64 `json:"total_purchase_orders"`
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
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"`
@@ -50,16 +46,12 @@ type PurchasingSummary struct {
} }
type PurchasingAnalyticsData struct { 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"` PurchaseOrders int64 `json:"purchase_orders"`
ExpensePurchases float64 `json:"expense_purchases"` Quantity float64 `json:"quantity"`
PurchaseOrders int64 `json:"purchase_orders"` Ingredients int64 `json:"ingredients"`
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"` Vendors int64 `json:"vendors"`
ExpenseCount int64 `json:"expense_count"`
Quantity float64 `json:"quantity"`
Ingredients int64 `json:"ingredients"`
Vendors int64 `json:"vendors"`
} }
type PurchasingIngredientData struct { type PurchasingIngredientData struct {
+4 -14
View File
@@ -29,11 +29,10 @@ type Expense struct {
} }
type ExpenseAnalytics struct { type ExpenseAnalytics struct {
Summary ExpenseAnalyticsSummary Summary ExpenseAnalyticsSummary
Data []ExpenseAnalyticsData Data []ExpenseAnalyticsData
CategoryData []ExpenseAnalyticsCategoryData CategoryData []ExpenseAnalyticsCategoryData
ChartOfAccountData []ExpenseAnalyticsChartOfAccountData ItemData []ExpenseAnalyticsItemData
ItemData []ExpenseAnalyticsItemData
} }
type ExpenseAnalyticsSummary struct { type ExpenseAnalyticsSummary struct {
@@ -55,15 +54,6 @@ type ExpenseAnalyticsData struct {
} }
type ExpenseAnalyticsCategoryData struct { type ExpenseAnalyticsCategoryData struct {
PurchaseCategoryID uuid.UUID
PurchaseCategoryName string
PurchaseCategoryType string
TotalAmount float64
ExpenseCount int64
ItemCount int64
}
type ExpenseAnalyticsChartOfAccountData struct {
ChartOfAccountID uuid.UUID ChartOfAccountID uuid.UUID
ChartOfAccountName string ChartOfAccountName string
TotalAmount float64 TotalAmount float64
+10 -12
View File
@@ -9,19 +9,17 @@ import (
) )
type ExpenseItem struct { type ExpenseItem 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"`
ExpenseID uuid.UUID `gorm:"type:uuid;not null;index" json:"expense_id"` ExpenseID uuid.UUID `gorm:"type:uuid;not null;index" json:"expense_id"`
ChartOfAccountID uuid.UUID `gorm:"type:uuid;not null;index" json:"chart_of_account_id"` ChartOfAccountID uuid.UUID `gorm:"type:uuid;not null;index" json:"chart_of_account_id"`
PurchaseCategoryID uuid.UUID `gorm:"type:uuid;not null;index" json:"purchase_category_id"` Item string `gorm:"not null;size:255" json:"item"`
Item string `gorm:"not null;size:255" json:"item"` Description *string `gorm:"type:text" json:"description"`
Description *string `gorm:"type:text" json:"description"` Amount float64 `gorm:"type:decimal(15,2);not null;default:0" json:"amount"`
Amount float64 `gorm:"type:decimal(15,2);not null;default:0" json:"amount"` 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"`
Expense *Expense `gorm:"foreignKey:ExpenseID" json:"expense,omitempty"` Expense *Expense `gorm:"foreignKey:ExpenseID" json:"expense,omitempty"`
ChartOfAccount *ChartOfAccount `gorm:"foreignKey:ChartOfAccountID" json:"chart_of_account,omitempty"` ChartOfAccount *ChartOfAccount `gorm:"foreignKey:ChartOfAccountID" json:"chart_of_account,omitempty"`
PurchaseCategory *PurchaseCategory `gorm:"foreignKey:PurchaseCategoryID" json:"purchase_category,omitempty"`
} }
func (e *ExpenseItem) BeforeCreate(tx *gorm.DB) error { func (e *ExpenseItem) BeforeCreate(tx *gorm.DB) error {
+27 -29
View File
@@ -36,36 +36,34 @@ const (
) )
type InventoryMovement struct { type InventoryMovement 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"`
OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id" validate:"required"` OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id" validate:"required"`
OutletID uuid.UUID `gorm:"type:uuid;not null;index" json:"outlet_id" validate:"required"` OutletID uuid.UUID `gorm:"type:uuid;not null;index" json:"outlet_id" validate:"required"`
ItemID uuid.UUID `gorm:"type:uuid;not null;index" json:"item_id" validate:"required"` ItemID uuid.UUID `gorm:"type:uuid;not null;index" json:"item_id" validate:"required"`
ItemType string `gorm:"not null;size:20" json:"item_type" validate:"required"` // "PRODUCT" or "INGREDIENT" ItemType string `gorm:"not null;size:20" json:"item_type" validate:"required"` // "PRODUCT" or "INGREDIENT"
MovementType InventoryMovementType `gorm:"not null;size:50" json:"movement_type" validate:"required"` MovementType InventoryMovementType `gorm:"not null;size:50" json:"movement_type" validate:"required"`
Quantity float64 `gorm:"type:decimal(12,3);not null" json:"quantity" validate:"required"` Quantity float64 `gorm:"type:decimal(12,3);not null" json:"quantity" validate:"required"`
PreviousQuantity float64 `gorm:"type:decimal(12,3)" json:"previous_quantity"` PreviousQuantity float64 `gorm:"type:decimal(12,3)" json:"previous_quantity"`
NewQuantity float64 `gorm:"type:decimal(12,3)" json:"new_quantity"` NewQuantity float64 `gorm:"type:decimal(12,3)" json:"new_quantity"`
UnitCost float64 `gorm:"type:decimal(12,2);default:0.00" json:"unit_cost"` UnitCost float64 `gorm:"type:decimal(12,2);default:0.00" json:"unit_cost"`
TotalCost float64 `gorm:"type:decimal(12,2);default:0.00" json:"total_cost"` TotalCost float64 `gorm:"type:decimal(12,2);default:0.00" json:"total_cost"`
ReferenceType *InventoryMovementReferenceType `gorm:"size:50" json:"reference_type"` ReferenceType *InventoryMovementReferenceType `gorm:"size:50" json:"reference_type"`
ReferenceID *uuid.UUID `gorm:"type:uuid;index" json:"reference_id"` ReferenceID *uuid.UUID `gorm:"type:uuid;index" json:"reference_id"`
PurchaseOrderItemID *uuid.UUID `gorm:"type:uuid;index" json:"purchase_order_item_id"` OrderID *uuid.UUID `gorm:"type:uuid;index" json:"order_id"`
OrderID *uuid.UUID `gorm:"type:uuid;index" json:"order_id"` PaymentID *uuid.UUID `gorm:"type:uuid;index" json:"payment_id"`
PaymentID *uuid.UUID `gorm:"type:uuid;index" json:"payment_id"` UserID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_id" validate:"required"`
UserID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_id" validate:"required"` Reason *string `gorm:"size:255" json:"reason"`
Reason *string `gorm:"size:255" json:"reason"` Notes *string `gorm:"type:text" json:"notes"`
Notes *string `gorm:"type:text" json:"notes"` Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"` CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"` Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
Outlet Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"` Outlet Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
Product *Product `gorm:"foreignKey:ItemID" json:"product,omitempty"` Product *Product `gorm:"foreignKey:ItemID" json:"product,omitempty"`
Ingredient *Ingredient `gorm:"foreignKey:ItemID" json:"ingredient,omitempty"` Ingredient *Ingredient `gorm:"foreignKey:ItemID" json:"ingredient,omitempty"`
PurchaseOrderItem *PurchaseOrderItem `gorm:"foreignKey:PurchaseOrderItemID" json:"purchase_order_item,omitempty"` Order *Order `gorm:"foreignKey:OrderID" json:"order,omitempty"`
Order *Order `gorm:"foreignKey:OrderID" json:"order,omitempty"` Payment *Payment `gorm:"foreignKey:PaymentID" json:"payment,omitempty"`
Payment *Payment `gorm:"foreignKey:PaymentID" json:"payment,omitempty"` User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
} }
func (im *InventoryMovement) BeforeCreate(tx *gorm.DB) error { func (im *InventoryMovement) BeforeCreate(tx *gorm.DB) error {
+2 -2
View File
@@ -10,8 +10,8 @@ import (
type PurchaseCategoryType string type PurchaseCategoryType string
const ( const (
PurchaseCategoryTypeRawMaterial PurchaseCategoryType = "raw_material" PurchaseCategoryTypeRawMaterial PurchaseCategoryType = "raw_material"
PurchaseCategoryTypeExpense PurchaseCategoryType = "expense" PurchaseCategoryTypeNonInventory PurchaseCategoryType = "non_inventory"
) )
type PurchaseCategoryPreset struct { type PurchaseCategoryPreset struct {
+12 -14
View File
@@ -41,21 +41,19 @@ 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" json:"ingredient_id" validate:"omitempty"` IngredientID uuid.UUID `gorm:"type:uuid;not null" json:"ingredient_id" validate:"required"`
PurchaseCategoryID uuid.UUID `gorm:"type:uuid;not null;index" json:"purchase_category_id" validate:"required"` Description *string `gorm:"type:text" json:"description" validate:"omitempty"`
Description *string `gorm:"type:text" json:"description" validate:"omitempty"` Quantity float64 `gorm:"type:decimal(10,3);not null" json:"quantity" validate:"required,gt=0"`
Quantity *float64 `gorm:"type:decimal(10,3)" 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"`
PurchaseOrder *PurchaseOrder `gorm:"foreignKey:PurchaseOrderID" json:"purchase_order,omitempty"` PurchaseOrder *PurchaseOrder `gorm:"foreignKey:PurchaseOrderID" json:"purchase_order,omitempty"`
Ingredient *Ingredient `gorm:"foreignKey:IngredientID" json:"ingredient,omitempty"` Ingredient *Ingredient `gorm:"foreignKey:IngredientID" json:"ingredient,omitempty"`
PurchaseCategory *PurchaseCategory `gorm:"foreignKey:PurchaseCategoryID" json:"purchase_category,omitempty"` Unit *Unit `gorm:"foreignKey:UnitID" json:"unit,omitempty"`
Unit *Unit `gorm:"foreignKey:UnitID" json:"unit,omitempty"`
} }
func (poi *PurchaseOrderItem) BeforeCreate(tx *gorm.DB) error { func (poi *PurchaseOrderItem) BeforeCreate(tx *gorm.DB) error {
+8 -15
View File
@@ -95,27 +95,20 @@ func ExpenseItemEntityToResponse(entity *entities.ExpenseItem) *models.ExpenseIt
} }
response := &models.ExpenseItemResponse{ response := &models.ExpenseItemResponse{
ID: entity.ID, ID: entity.ID,
ExpenseID: entity.ExpenseID, ExpenseID: entity.ExpenseID,
ChartOfAccountID: entity.ChartOfAccountID, ChartOfAccountID: entity.ChartOfAccountID,
PurchaseCategoryID: entity.PurchaseCategoryID, Item: entity.Item,
Item: entity.Item, Description: entity.Description,
Description: entity.Description, Amount: entity.Amount,
Amount: entity.Amount, CreatedAt: entity.CreatedAt,
CreatedAt: entity.CreatedAt, UpdatedAt: entity.UpdatedAt,
UpdatedAt: entity.UpdatedAt,
} }
if entity.ChartOfAccount != nil { if entity.ChartOfAccount != nil {
response.ChartOfAccountName = entity.ChartOfAccount.Name response.ChartOfAccountName = entity.ChartOfAccount.Name
} }
if entity.PurchaseCategory != nil {
response.PurchaseCategoryName = entity.PurchaseCategory.Name
response.PurchaseCategoryType = string(entity.PurchaseCategory.Type)
response.PurchaseCategory = PurchaseCategoryEntityToResponse(entity.PurchaseCategory)
}
return response return response
} }
+27 -34
View File
@@ -91,16 +91,15 @@ func PurchaseOrderItemEntityToModel(entity *entities.PurchaseOrderItem) *models.
} }
return &models.PurchaseOrderItem{ return &models.PurchaseOrderItem{
ID: entity.ID, ID: entity.ID,
PurchaseOrderID: entity.PurchaseOrderID, PurchaseOrderID: entity.PurchaseOrderID,
IngredientID: entity.IngredientID, IngredientID: entity.IngredientID,
PurchaseCategoryID: entity.PurchaseCategoryID, Description: entity.Description,
Description: entity.Description, Quantity: entity.Quantity,
Quantity: entity.Quantity, UnitID: entity.UnitID,
UnitID: entity.UnitID, Amount: entity.Amount,
Amount: entity.Amount, CreatedAt: entity.CreatedAt,
CreatedAt: entity.CreatedAt, UpdatedAt: entity.UpdatedAt,
UpdatedAt: entity.UpdatedAt,
} }
} }
@@ -110,16 +109,15 @@ func PurchaseOrderItemModelToEntity(model *models.PurchaseOrderItem) *entities.P
} }
return &entities.PurchaseOrderItem{ return &entities.PurchaseOrderItem{
ID: model.ID, ID: model.ID,
PurchaseOrderID: model.PurchaseOrderID, PurchaseOrderID: model.PurchaseOrderID,
IngredientID: model.IngredientID, IngredientID: model.IngredientID,
PurchaseCategoryID: model.PurchaseCategoryID, Description: model.Description,
Description: model.Description, Quantity: model.Quantity,
Quantity: model.Quantity, UnitID: model.UnitID,
UnitID: model.UnitID, Amount: model.Amount,
Amount: model.Amount, CreatedAt: model.CreatedAt,
CreatedAt: model.CreatedAt, UpdatedAt: model.UpdatedAt,
UpdatedAt: model.UpdatedAt,
} }
} }
@@ -129,16 +127,15 @@ func PurchaseOrderItemEntityToResponse(entity *entities.PurchaseOrderItem) *mode
} }
response := &models.PurchaseOrderItemResponse{ response := &models.PurchaseOrderItemResponse{
ID: entity.ID, ID: entity.ID,
PurchaseOrderID: entity.PurchaseOrderID, PurchaseOrderID: entity.PurchaseOrderID,
IngredientID: entity.IngredientID, IngredientID: entity.IngredientID,
PurchaseCategoryID: entity.PurchaseCategoryID, Description: entity.Description,
Description: entity.Description, Quantity: entity.Quantity,
Quantity: entity.Quantity, UnitID: entity.UnitID,
UnitID: entity.UnitID, Amount: entity.Amount,
Amount: entity.Amount, CreatedAt: entity.CreatedAt,
CreatedAt: entity.CreatedAt, UpdatedAt: entity.UpdatedAt,
UpdatedAt: entity.UpdatedAt,
} }
// Map ingredient if present // Map ingredient if present
@@ -149,10 +146,6 @@ func PurchaseOrderItemEntityToResponse(entity *entities.PurchaseOrderItem) *mode
} }
} }
if entity.PurchaseCategory != nil {
response.PurchaseCategory = PurchaseCategoryEntityToResponse(entity.PurchaseCategory)
}
// Map unit if present // Map unit if present
if entity.Unit != nil { if entity.Unit != nil {
response.Unit = &models.UnitResponse{ response.Unit = &models.UnitResponse{
+6 -14
View File
@@ -113,11 +113,7 @@ type PurchasingAnalyticsResponse struct {
// PurchasingSummary represents the summary of purchasing analytics // PurchasingSummary represents the summary of purchasing analytics
type PurchasingSummary struct { type PurchasingSummary struct {
TotalPurchases float64 `json:"total_purchases"` TotalPurchases float64 `json:"total_purchases"`
RawMaterialPurchases float64 `json:"raw_material_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"`
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"`
@@ -126,16 +122,12 @@ type PurchasingSummary struct {
// PurchasingAnalyticsData represents purchasing analytics by time period // PurchasingAnalyticsData represents purchasing analytics by time period
type PurchasingAnalyticsData struct { 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"` PurchaseOrders int64 `json:"purchase_orders"`
ExpensePurchases float64 `json:"expense_purchases"` Quantity float64 `json:"quantity"`
PurchaseOrders int64 `json:"purchase_orders"` Ingredients int64 `json:"ingredients"`
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"` Vendors int64 `json:"vendors"`
ExpenseCount int64 `json:"expense_count"`
Quantity float64 `json:"quantity"`
Ingredients int64 `json:"ingredients"`
Vendors int64 `json:"vendors"`
} }
// PurchasingIngredientData represents purchasing analytics for an ingredient // PurchasingIngredientData represents purchasing analytics for an ingredient
+34 -51
View File
@@ -23,15 +23,14 @@ type Expense struct {
} }
type ExpenseItem struct { type ExpenseItem struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
ExpenseID uuid.UUID `json:"expense_id"` ExpenseID uuid.UUID `json:"expense_id"`
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"` ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"` Item string `json:"item"`
Item string `json:"item"` Description *string `json:"description"`
Description *string `json:"description"` 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"`
} }
type ExpenseResponse struct { type ExpenseResponse struct {
@@ -52,19 +51,15 @@ type ExpenseResponse struct {
} }
type ExpenseItemResponse struct { type ExpenseItemResponse struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
ExpenseID uuid.UUID `json:"expense_id"` ExpenseID uuid.UUID `json:"expense_id"`
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"` ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
ChartOfAccountName string `json:"chart_of_account_name,omitempty"` ChartOfAccountName string `json:"chart_of_account_name,omitempty"`
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"` Item string `json:"item"`
PurchaseCategoryName string `json:"purchase_category_name,omitempty"` Description *string `json:"description"`
PurchaseCategoryType string `json:"purchase_category_type,omitempty"` Amount float64 `json:"amount"`
PurchaseCategory *PurchaseCategoryResponse `json:"purchase_category,omitempty"` CreatedAt time.Time `json:"created_at"`
Item string `json:"item"` UpdatedAt time.Time `json:"updated_at"`
Description *string `json:"description"`
Amount float64 `json:"amount"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
} }
type CreateExpenseRequest struct { type CreateExpenseRequest struct {
@@ -80,11 +75,10 @@ type CreateExpenseRequest struct {
} }
type CreateExpenseItemRequest struct { type CreateExpenseItemRequest struct {
ChartOfAccountID string `json:"chart_of_account_id"` ChartOfAccountID string `json:"chart_of_account_id"`
PurchaseCategoryID string `json:"purchase_category_id"` Item string `json:"item"`
Item string `json:"item"` Description *string `json:"description,omitempty"`
Description *string `json:"description,omitempty"` Amount float64 `json:"amount"`
Amount float64 `json:"amount"`
} }
type UpdateExpenseRequest struct { type UpdateExpenseRequest struct {
@@ -101,11 +95,10 @@ type UpdateExpenseRequest struct {
} }
type UpdateExpenseItemRequest struct { type UpdateExpenseItemRequest struct {
ChartOfAccountID *string `json:"chart_of_account_id,omitempty"` ChartOfAccountID *string `json:"chart_of_account_id,omitempty"`
PurchaseCategoryID *string `json:"purchase_category_id,omitempty"` Item *string `json:"item,omitempty"`
Item *string `json:"item,omitempty"` Description *string `json:"description,omitempty"`
Description *string `json:"description,omitempty"` Amount *float64 `json:"amount,omitempty"`
Amount *float64 `json:"amount,omitempty"`
} }
type ListExpenseRequest struct { type ListExpenseRequest struct {
@@ -135,16 +128,15 @@ type ExpenseAnalyticsRequest struct {
} }
type ExpenseAnalyticsResponse struct { type ExpenseAnalyticsResponse struct {
OrganizationID uuid.UUID `json:"organization_id"` OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id,omitempty"` OutletID *uuid.UUID `json:"outlet_id,omitempty"`
DateFrom time.Time `json:"date_from"` DateFrom time.Time `json:"date_from"`
DateTo time.Time `json:"date_to"` DateTo time.Time `json:"date_to"`
GroupBy string `json:"group_by"` GroupBy string `json:"group_by"`
Summary ExpenseAnalyticsSummary `json:"summary"` Summary ExpenseAnalyticsSummary `json:"summary"`
Data []ExpenseAnalyticsData `json:"data"` Data []ExpenseAnalyticsData `json:"data"`
CategoryData []ExpenseAnalyticsCategoryData `json:"category_data"` CategoryData []ExpenseAnalyticsCategoryData `json:"category_data"`
ChartOfAccountData []ExpenseAnalyticsChartOfAccountData `json:"chart_of_account_data"` ItemData []ExpenseAnalyticsItemData `json:"item_data"`
ItemData []ExpenseAnalyticsItemData `json:"item_data"`
} }
type ExpenseAnalyticsSummary struct { type ExpenseAnalyticsSummary struct {
@@ -166,15 +158,6 @@ type ExpenseAnalyticsData struct {
} }
type ExpenseAnalyticsCategoryData struct { type ExpenseAnalyticsCategoryData struct {
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"`
PurchaseCategoryName string `json:"purchase_category_name"`
PurchaseCategoryType string `json:"purchase_category_type"`
TotalAmount float64 `json:"total_amount"`
ExpenseCount int64 `json:"expense_count"`
ItemCount int64 `json:"item_count"`
}
type ExpenseAnalyticsChartOfAccountData struct {
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"` ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
ChartOfAccountName string `json:"chart_of_account_name"` ChartOfAccountName string `json:"chart_of_account_name"`
TotalAmount float64 `json:"total_amount"` TotalAmount float64 `json:"total_amount"`
+31 -36
View File
@@ -22,16 +22,15 @@ 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"` 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"`
} }
type PurchaseOrderAttachment struct { type PurchaseOrderAttachment struct {
@@ -60,19 +59,17 @@ 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"` 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"` Ingredient *IngredientResponse `json:"ingredient,omitempty"`
Ingredient *IngredientResponse `json:"ingredient,omitempty"` Unit *UnitResponse `json:"unit,omitempty"`
PurchaseCategory *PurchaseCategoryResponse `json:"purchase_category,omitempty"`
Unit *UnitResponse `json:"unit,omitempty"`
} }
type PurchaseOrderAttachmentResponse struct { type PurchaseOrderAttachmentResponse struct {
@@ -96,12 +93,11 @@ type CreatePurchaseOrderRequest struct {
} }
type CreatePurchaseOrderItemRequest struct { type CreatePurchaseOrderItemRequest struct {
IngredientID *uuid.UUID `json:"ingredient_id,omitempty"` IngredientID uuid.UUID `json:"ingredient_id"`
PurchaseCategoryID uuid.UUID `json:"purchase_category_id"` Description *string `json:"description,omitempty"`
Description *string `json:"description,omitempty"` Quantity float64 `json:"quantity"`
Quantity *float64 `json:"quantity,omitempty"` UnitID uuid.UUID `json:"unit_id"`
UnitID *uuid.UUID `json:"unit_id,omitempty"` Amount float64 `json:"amount"`
Amount float64 `json:"amount"`
} }
type UpdatePurchaseOrderRequest struct { type UpdatePurchaseOrderRequest struct {
@@ -117,13 +113,12 @@ type UpdatePurchaseOrderRequest struct {
} }
type UpdatePurchaseOrderItemRequest struct { type UpdatePurchaseOrderItemRequest struct {
ID *uuid.UUID `json:"id,omitempty"` // For existing items ID *uuid.UUID `json:"id,omitempty"` // For existing items
IngredientID *uuid.UUID `json:"ingredient_id,omitempty"` IngredientID *uuid.UUID `json:"ingredient_id,omitempty"`
PurchaseCategoryID *uuid.UUID `json:"purchase_category_id,omitempty"` Description *string `json:"description,omitempty"`
Description *string `json:"description,omitempty"` Quantity *float64 `json:"quantity,omitempty"`
Quantity *float64 `json:"quantity,omitempty"` UnitID *uuid.UUID `json:"unit_id,omitempty"`
UnitID *uuid.UUID `json:"unit_id,omitempty"` Amount *float64 `json:"amount,omitempty"`
Amount *float64 `json:"amount,omitempty"`
} }
type ListPurchaseOrdersRequest struct { type ListPurchaseOrdersRequest struct {
+6 -14
View File
@@ -185,16 +185,12 @@ func (p *AnalyticsProcessorImpl) GetPurchasingAnalytics(ctx context.Context, req
data := make([]models.PurchasingAnalyticsData, len(result.Data)) data := make([]models.PurchasingAnalyticsData, len(result.Data))
for i, item := range result.Data { for i, item := range result.Data {
data[i] = models.PurchasingAnalyticsData{ data[i] = models.PurchasingAnalyticsData{
Date: item.Date, Date: item.Date,
Purchases: item.Purchases, Purchases: item.Purchases,
RawMaterialPurchases: item.RawMaterialPurchases, PurchaseOrders: item.PurchaseOrders,
ExpensePurchases: item.ExpensePurchases, Quantity: item.Quantity,
PurchaseOrders: item.PurchaseOrders, Ingredients: item.Ingredients,
RawMaterialPurchaseOrders: item.RawMaterialPurchaseOrders, Vendors: item.Vendors,
ExpenseCount: item.ExpenseCount,
Quantity: item.Quantity,
Ingredients: item.Ingredients,
Vendors: item.Vendors,
} }
} }
@@ -231,11 +227,7 @@ func (p *AnalyticsProcessorImpl) GetPurchasingAnalytics(ctx context.Context, req
GroupBy: req.GroupBy, GroupBy: req.GroupBy,
Summary: models.PurchasingSummary{ Summary: models.PurchasingSummary{
TotalPurchases: result.Summary.TotalPurchases, TotalPurchases: result.Summary.TotalPurchases,
RawMaterialPurchases: result.Summary.RawMaterialPurchases,
ExpensePurchases: result.Summary.ExpensePurchases,
TotalPurchaseOrders: result.Summary.TotalPurchaseOrders, TotalPurchaseOrders: result.Summary.TotalPurchaseOrders,
RawMaterialPurchaseOrders: result.Summary.RawMaterialPurchaseOrders,
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,
+2 -27
View File
@@ -75,23 +75,7 @@ func TestAnalyticsProcessorGetPurchasingAnalyticsPassesOutletName(t *testing.T)
purchasingResult: &entities.PurchasingAnalytics{ purchasingResult: &entities.PurchasingAnalytics{
OutletName: &outletName, OutletName: &outletName,
Summary: entities.PurchasingSummary{ Summary: entities.PurchasingSummary{
TotalPurchases: 300, TotalPurchases: 125,
RawMaterialPurchases: 125,
ExpensePurchases: 175,
TotalPurchaseOrders: 3,
RawMaterialPurchaseOrders: 1,
ExpenseCount: 2,
},
Data: []entities.PurchasingAnalyticsData{
{
Date: now,
Purchases: 300,
RawMaterialPurchases: 125,
ExpensePurchases: 175,
PurchaseOrders: 3,
RawMaterialPurchaseOrders: 1,
ExpenseCount: 2,
},
}, },
}, },
}, expenseRepositoryStub{}) }, expenseRepositoryStub{})
@@ -108,16 +92,7 @@ func TestAnalyticsProcessorGetPurchasingAnalyticsPassesOutletName(t *testing.T)
require.Equal(t, &outletID, result.OutletID) require.Equal(t, &outletID, result.OutletID)
require.NotNil(t, result.OutletName) require.NotNil(t, result.OutletName)
require.Equal(t, outletName, *result.OutletName) require.Equal(t, outletName, *result.OutletName)
require.Equal(t, float64(300), result.Summary.TotalPurchases) require.Equal(t, float64(125), result.Summary.TotalPurchases)
require.Equal(t, float64(125), result.Summary.RawMaterialPurchases)
require.Equal(t, float64(175), result.Summary.ExpensePurchases)
require.Equal(t, int64(3), result.Summary.TotalPurchaseOrders)
require.Equal(t, int64(1), result.Summary.RawMaterialPurchaseOrders)
require.Equal(t, int64(2), result.Summary.ExpenseCount)
require.Len(t, result.Data, 1)
require.Equal(t, float64(300), result.Data[0].Purchases)
require.Equal(t, float64(125), result.Data[0].RawMaterialPurchases)
require.Equal(t, float64(175), result.Data[0].ExpensePurchases)
} }
func TestAnalyticsProcessorGetProfitLossAnalyticsMapsOverviewAndReportFields(t *testing.T) { func TestAnalyticsProcessorGetProfitLossAnalyticsMapsOverviewAndReportFields(t *testing.T) {
+33 -94
View File
@@ -23,14 +23,12 @@ type ExpenseProcessor interface {
} }
type ExpenseProcessorImpl struct { type ExpenseProcessorImpl struct {
expenseRepo ExpenseRepository expenseRepo ExpenseRepository
purchaseCategoryRepo PurchaseCategoryRepository
} }
func NewExpenseProcessorImpl(expenseRepo ExpenseRepository, purchaseCategoryRepo PurchaseCategoryRepository) *ExpenseProcessorImpl { func NewExpenseProcessorImpl(expenseRepo ExpenseRepository) *ExpenseProcessorImpl {
return &ExpenseProcessorImpl{ return &ExpenseProcessorImpl{
expenseRepo: expenseRepo, expenseRepo: expenseRepo,
purchaseCategoryRepo: purchaseCategoryRepo,
} }
} }
@@ -50,30 +48,6 @@ func (p *ExpenseProcessorImpl) CreateExpense(ctx context.Context, organizationID
status = *req.Status status = *req.Status
} }
items := make([]entities.ExpenseItem, len(req.Items))
for i, itemReq := range req.Items {
chartOfAccountID, err := uuid.Parse(itemReq.ChartOfAccountID)
if err != nil {
return nil, fmt.Errorf("invalid chart_of_account_id for item: %w", err)
}
purchaseCategoryID, err := uuid.Parse(itemReq.PurchaseCategoryID)
if err != nil {
return nil, fmt.Errorf("invalid purchase_category_id for item: %w", err)
}
if err := p.validateExpensePurchaseCategory(ctx, purchaseCategoryID, organizationID); err != nil {
return nil, err
}
items[i] = entities.ExpenseItem{
ChartOfAccountID: chartOfAccountID,
PurchaseCategoryID: purchaseCategoryID,
Item: itemReq.Item,
Description: itemReq.Description,
Amount: itemReq.Amount,
}
}
expenseEntity := &entities.Expense{ expenseEntity := &entities.Expense{
OrganizationID: organizationID, OrganizationID: organizationID,
OutletID: outletID, OutletID: outletID,
@@ -91,10 +65,21 @@ func (p *ExpenseProcessorImpl) CreateExpense(ctx context.Context, organizationID
return nil, fmt.Errorf("failed to create expense: %w", err) return nil, fmt.Errorf("failed to create expense: %w", err)
} }
for i := range items { for _, itemReq := range req.Items {
items[i].ExpenseID = expenseEntity.ID chartOfAccountID, err := uuid.Parse(itemReq.ChartOfAccountID)
if err != nil {
return nil, fmt.Errorf("invalid chart_of_account_id for item: %w", err)
}
err = p.expenseRepo.CreateItem(ctx, &items[i]) itemEntity := &entities.ExpenseItem{
ExpenseID: expenseEntity.ID,
ChartOfAccountID: chartOfAccountID,
Item: itemReq.Item,
Description: itemReq.Description,
Amount: itemReq.Amount,
}
err = p.expenseRepo.CreateItem(ctx, itemEntity)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create expense item: %w", err) return nil, fmt.Errorf("failed to create expense item: %w", err)
} }
@@ -150,10 +135,13 @@ func (p *ExpenseProcessorImpl) UpdateExpense(ctx context.Context, id, organizati
expenseEntity.Reserved1 = req.Reserved1 expenseEntity.Reserved1 = req.Reserved1
} }
var items []entities.ExpenseItem
if req.Items != nil { if req.Items != nil {
items = make([]entities.ExpenseItem, len(req.Items)) err = p.expenseRepo.DeleteItemsByExpenseID(ctx, expenseEntity.ID)
for i, itemReq := range req.Items { if err != nil {
return nil, fmt.Errorf("failed to delete existing items: %w", err)
}
for _, itemReq := range req.Items {
chartOfAccountID := uuid.Nil chartOfAccountID := uuid.Nil
if itemReq.ChartOfAccountID != nil { if itemReq.ChartOfAccountID != nil {
chartOfAccountID, err = uuid.Parse(*itemReq.ChartOfAccountID) chartOfAccountID, err = uuid.Parse(*itemReq.ChartOfAccountID)
@@ -162,17 +150,6 @@ func (p *ExpenseProcessorImpl) UpdateExpense(ctx context.Context, id, organizati
} }
} }
if itemReq.PurchaseCategoryID == nil {
return nil, fmt.Errorf("purchase_category_id is required for item")
}
purchaseCategoryID, err := uuid.Parse(*itemReq.PurchaseCategoryID)
if err != nil {
return nil, fmt.Errorf("invalid purchase_category_id for item: %w", err)
}
if err := p.validateExpensePurchaseCategory(ctx, purchaseCategoryID, organizationID); err != nil {
return nil, err
}
amount := 0.0 amount := 0.0
if itemReq.Amount != nil { if itemReq.Amount != nil {
amount = *itemReq.Amount amount = *itemReq.Amount
@@ -182,23 +159,15 @@ func (p *ExpenseProcessorImpl) UpdateExpense(ctx context.Context, id, organizati
item = *itemReq.Item item = *itemReq.Item
} }
items[i] = entities.ExpenseItem{ itemEntity := &entities.ExpenseItem{
ExpenseID: expenseEntity.ID, ExpenseID: expenseEntity.ID,
ChartOfAccountID: chartOfAccountID, ChartOfAccountID: chartOfAccountID,
PurchaseCategoryID: purchaseCategoryID, Item: item,
Item: item, Description: itemReq.Description,
Description: itemReq.Description, Amount: amount,
Amount: amount,
} }
}
err = p.expenseRepo.DeleteItemsByExpenseID(ctx, expenseEntity.ID) err = p.expenseRepo.CreateItem(ctx, itemEntity)
if err != nil {
return nil, fmt.Errorf("failed to delete existing items: %w", err)
}
for i := range items {
err = p.expenseRepo.CreateItem(ctx, &items[i])
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create expense item: %w", err) return nil, fmt.Errorf("failed to create expense item: %w", err)
} }
@@ -283,18 +252,6 @@ func (p *ExpenseProcessorImpl) GetExpenseAnalytics(ctx context.Context, req *mod
categoryData := make([]models.ExpenseAnalyticsCategoryData, len(result.CategoryData)) categoryData := make([]models.ExpenseAnalyticsCategoryData, len(result.CategoryData))
for i, item := range result.CategoryData { for i, item := range result.CategoryData {
categoryData[i] = models.ExpenseAnalyticsCategoryData{ categoryData[i] = models.ExpenseAnalyticsCategoryData{
PurchaseCategoryID: item.PurchaseCategoryID,
PurchaseCategoryName: item.PurchaseCategoryName,
PurchaseCategoryType: item.PurchaseCategoryType,
TotalAmount: item.TotalAmount,
ExpenseCount: item.ExpenseCount,
ItemCount: item.ItemCount,
}
}
chartOfAccountData := make([]models.ExpenseAnalyticsChartOfAccountData, len(result.ChartOfAccountData))
for i, item := range result.ChartOfAccountData {
chartOfAccountData[i] = models.ExpenseAnalyticsChartOfAccountData{
ChartOfAccountID: item.ChartOfAccountID, ChartOfAccountID: item.ChartOfAccountID,
ChartOfAccountName: item.ChartOfAccountName, ChartOfAccountName: item.ChartOfAccountName,
TotalAmount: item.TotalAmount, TotalAmount: item.TotalAmount,
@@ -327,26 +284,8 @@ func (p *ExpenseProcessorImpl) GetExpenseAnalytics(ctx context.Context, req *mod
TotalCategories: result.Summary.TotalCategories, TotalCategories: result.Summary.TotalCategories,
TotalItems: result.Summary.TotalItems, TotalItems: result.Summary.TotalItems,
}, },
Data: data, Data: data,
CategoryData: categoryData, CategoryData: categoryData,
ChartOfAccountData: chartOfAccountData, ItemData: itemData,
ItemData: itemData,
}, nil }, nil
} }
func (p *ExpenseProcessorImpl) validateExpensePurchaseCategory(ctx context.Context, categoryID, organizationID uuid.UUID) error {
category, err := p.purchaseCategoryRepo.GetByIDAndOrganizationID(ctx, categoryID, organizationID)
if err != nil {
return fmt.Errorf("purchase category not found: %w", err)
}
if !category.IsActive {
return fmt.Errorf("purchase category is inactive")
}
if category.Type != entities.PurchaseCategoryTypeExpense {
return fmt.Errorf("purchase category must be expense")
}
return nil
}
+14 -99
View File
@@ -18,45 +18,6 @@ type expenseRepositoryCaptureStub struct {
analytics *entities.ExpenseAnalytics analytics *entities.ExpenseAnalytics
} }
type expensePurchaseCategoryRepositoryStub struct {
category *entities.PurchaseCategory
}
func (*expensePurchaseCategoryRepositoryStub) Create(context.Context, *entities.PurchaseCategory) error {
return nil
}
func (s *expensePurchaseCategoryRepositoryStub) GetByIDAndOrganizationID(context.Context, uuid.UUID, uuid.UUID) (*entities.PurchaseCategory, error) {
return s.category, nil
}
func (*expensePurchaseCategoryRepositoryStub) Update(context.Context, *entities.PurchaseCategory) error {
return nil
}
func (*expensePurchaseCategoryRepositoryStub) SoftDelete(context.Context, uuid.UUID, uuid.UUID) error {
return nil
}
func (*expensePurchaseCategoryRepositoryStub) List(context.Context, uuid.UUID, map[string]interface{}, int, int) ([]*entities.PurchaseCategory, int64, error) {
return nil, 0, nil
}
func (*expensePurchaseCategoryRepositoryStub) ExistsByCode(context.Context, uuid.UUID, string, *uuid.UUID) (bool, error) {
return false, nil
}
func newExpensePurchaseCategoryRepo(categoryID uuid.UUID, categoryType entities.PurchaseCategoryType) *expensePurchaseCategoryRepositoryStub {
return &expensePurchaseCategoryRepositoryStub{
category: &entities.PurchaseCategory{
ID: categoryID,
Name: "Operational",
Type: categoryType,
IsActive: true,
},
}
}
func (s *expenseRepositoryCaptureStub) Create(_ context.Context, expense *entities.Expense) error { func (s *expenseRepositoryCaptureStub) Create(_ context.Context, expense *entities.Expense) error {
if expense.ID == uuid.Nil { if expense.ID == uuid.Nil {
expense.ID = uuid.New() expense.ID = uuid.New()
@@ -101,8 +62,7 @@ func (*expenseRepositoryCaptureStub) DeleteItemsByExpenseID(context.Context, uui
func TestExpenseProcessorCreatePersistsItemName(t *testing.T) { func TestExpenseProcessorCreatePersistsItemName(t *testing.T) {
repo := &expenseRepositoryCaptureStub{} repo := &expenseRepositoryCaptureStub{}
purchaseCategoryID := uuid.New() p := NewExpenseProcessorImpl(repo)
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{
@@ -113,10 +73,9 @@ func TestExpenseProcessorCreatePersistsItemName(t *testing.T) {
Total: 10000, Total: 10000,
Items: []models.CreateExpenseItemRequest{ Items: []models.CreateExpenseItemRequest{
{ {
ChartOfAccountID: chartOfAccountID.String(), ChartOfAccountID: chartOfAccountID.String(),
PurchaseCategoryID: purchaseCategoryID.String(), Item: "Cleaning supplies",
Item: "Cleaning supplies", Amount: 10000,
Amount: 10000,
}, },
}, },
}) })
@@ -125,15 +84,13 @@ func TestExpenseProcessorCreatePersistsItemName(t *testing.T) {
require.NotNil(t, resp) require.NotNil(t, resp)
require.Len(t, repo.createdItems, 1) require.Len(t, repo.createdItems, 1)
require.Equal(t, "Cleaning supplies", repo.createdItems[0].Item) require.Equal(t, "Cleaning supplies", repo.createdItems[0].Item)
require.Equal(t, purchaseCategoryID, repo.createdItems[0].PurchaseCategoryID)
require.Len(t, resp.Items, 1) require.Len(t, resp.Items, 1)
require.Equal(t, "Cleaning supplies", resp.Items[0].Item) require.Equal(t, "Cleaning supplies", resp.Items[0].Item)
} }
func TestExpenseProcessorCreateDefaultsStatusToDraft(t *testing.T) { func TestExpenseProcessorCreateDefaultsStatusToDraft(t *testing.T) {
repo := &expenseRepositoryCaptureStub{} repo := &expenseRepositoryCaptureStub{}
purchaseCategoryID := uuid.New() p := NewExpenseProcessorImpl(repo)
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",
@@ -143,10 +100,9 @@ func TestExpenseProcessorCreateDefaultsStatusToDraft(t *testing.T) {
Total: 10000, Total: 10000,
Items: []models.CreateExpenseItemRequest{ Items: []models.CreateExpenseItemRequest{
{ {
ChartOfAccountID: uuid.NewString(), ChartOfAccountID: uuid.NewString(),
PurchaseCategoryID: purchaseCategoryID.String(), Item: "Cleaning supplies",
Item: "Cleaning supplies", Amount: 10000,
Amount: 10000,
}, },
}, },
}) })
@@ -159,8 +115,7 @@ func TestExpenseProcessorCreateDefaultsStatusToDraft(t *testing.T) {
func TestExpenseProcessorCreatePersistsProvidedStatus(t *testing.T) { func TestExpenseProcessorCreatePersistsProvidedStatus(t *testing.T) {
repo := &expenseRepositoryCaptureStub{} repo := &expenseRepositoryCaptureStub{}
purchaseCategoryID := uuid.New() p := NewExpenseProcessorImpl(repo)
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{
@@ -172,10 +127,9 @@ func TestExpenseProcessorCreatePersistsProvidedStatus(t *testing.T) {
Total: 10000, Total: 10000,
Items: []models.CreateExpenseItemRequest{ Items: []models.CreateExpenseItemRequest{
{ {
ChartOfAccountID: uuid.NewString(), ChartOfAccountID: uuid.NewString(),
PurchaseCategoryID: purchaseCategoryID.String(), Item: "Cleaning supplies",
Item: "Cleaning supplies", Amount: 10000,
Amount: 10000,
}, },
}, },
}) })
@@ -186,35 +140,8 @@ func TestExpenseProcessorCreatePersistsProvidedStatus(t *testing.T) {
require.Equal(t, "approved", resp.Status) require.Equal(t, "approved", resp.Status)
} }
func TestExpenseProcessorCreateRejectsRawMaterialPurchaseCategory(t *testing.T) {
repo := &expenseRepositoryCaptureStub{}
purchaseCategoryID := uuid.New()
p := NewExpenseProcessorImpl(repo, newExpensePurchaseCategoryRepo(purchaseCategoryID, entities.PurchaseCategoryTypeRawMaterial))
resp, err := p.CreateExpense(context.Background(), uuid.New(), &models.CreateExpenseRequest{
Receiver: "Cashier",
TransactionDate: "2026-05-29",
CodeNumber: "EXP-001",
OutletID: uuid.NewString(),
Total: 10000,
Items: []models.CreateExpenseItemRequest{
{
ChartOfAccountID: uuid.NewString(),
PurchaseCategoryID: purchaseCategoryID.String(),
Item: "Cleaning supplies",
Amount: 10000,
},
},
})
require.Error(t, err)
require.Nil(t, resp)
require.Contains(t, err.Error(), "expense")
}
func TestExpenseProcessorGetExpenseAnalyticsDefaultsGroupByAndMapsResponse(t *testing.T) { func TestExpenseProcessorGetExpenseAnalyticsDefaultsGroupByAndMapsResponse(t *testing.T) {
coaID := uuid.New() coaID := uuid.New()
purchaseCategoryID := uuid.New()
outletID := uuid.New() outletID := uuid.New()
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC) now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
repo := &expenseRepositoryCaptureStub{ repo := &expenseRepositoryCaptureStub{
@@ -238,16 +165,6 @@ func TestExpenseProcessorGetExpenseAnalyticsDefaultsGroupByAndMapsResponse(t *te
}, },
}, },
CategoryData: []entities.ExpenseAnalyticsCategoryData{ CategoryData: []entities.ExpenseAnalyticsCategoryData{
{
PurchaseCategoryID: purchaseCategoryID,
PurchaseCategoryName: "Operational Supplies",
PurchaseCategoryType: "expense",
TotalAmount: 100000,
ExpenseCount: 2,
ItemCount: 2,
},
},
ChartOfAccountData: []entities.ExpenseAnalyticsChartOfAccountData{
{ {
ChartOfAccountID: coaID, ChartOfAccountID: coaID,
ChartOfAccountName: "Operational", ChartOfAccountName: "Operational",
@@ -266,7 +183,7 @@ func TestExpenseProcessorGetExpenseAnalyticsDefaultsGroupByAndMapsResponse(t *te
}, },
}, },
} }
p := NewExpenseProcessorImpl(repo, newExpensePurchaseCategoryRepo(purchaseCategoryID, entities.PurchaseCategoryTypeExpense)) p := NewExpenseProcessorImpl(repo)
resp, err := p.GetExpenseAnalytics(context.Background(), &models.ExpenseAnalyticsRequest{ resp, err := p.GetExpenseAnalytics(context.Background(), &models.ExpenseAnalyticsRequest{
OrganizationID: uuid.New(), OrganizationID: uuid.New(),
@@ -283,9 +200,7 @@ func TestExpenseProcessorGetExpenseAnalyticsDefaultsGroupByAndMapsResponse(t *te
require.Len(t, resp.Data, 1) require.Len(t, resp.Data, 1)
require.Equal(t, int64(2), resp.Data[0].ExpenseCount) require.Equal(t, int64(2), resp.Data[0].ExpenseCount)
require.Len(t, resp.CategoryData, 1) require.Len(t, resp.CategoryData, 1)
require.Equal(t, purchaseCategoryID, resp.CategoryData[0].PurchaseCategoryID) require.Equal(t, coaID, resp.CategoryData[0].ChartOfAccountID)
require.Len(t, resp.ChartOfAccountData, 1)
require.Equal(t, coaID, resp.ChartOfAccountData[0].ChartOfAccountID)
require.Len(t, resp.ItemData, 1) require.Len(t, resp.ItemData, 1)
require.Equal(t, "Cleaning supplies", resp.ItemData[0].Item) require.Equal(t, "Cleaning supplies", resp.ItemData[0].Item)
} }
+1 -1
View File
@@ -86,7 +86,7 @@ type CustomerRepository interface {
} }
type InventoryMovementService interface { type InventoryMovementService interface {
CreateIngredientMovement(ctx context.Context, ingredientID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID, purchaseOrderItemID *uuid.UUID) error CreateIngredientMovement(ctx context.Context, ingredientID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error
CreateProductMovement(ctx context.Context, productID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error CreateProductMovement(ctx context.Context, productID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error
} }
+73 -139
View File
@@ -25,7 +25,6 @@ type PurchaseOrderProcessorImpl struct {
purchaseOrderRepo PurchaseOrderRepository purchaseOrderRepo PurchaseOrderRepository
vendorRepo VendorRepository vendorRepo VendorRepository
ingredientRepo IngredientRepository ingredientRepo IngredientRepository
purchaseCategoryRepo PurchaseCategoryRepository
unitRepo UnitRepository unitRepo UnitRepository
fileRepo FileRepository fileRepo FileRepository
inventoryMovementService InventoryMovementService inventoryMovementService InventoryMovementService
@@ -36,7 +35,6 @@ func NewPurchaseOrderProcessorImpl(
purchaseOrderRepo PurchaseOrderRepository, purchaseOrderRepo PurchaseOrderRepository,
vendorRepo VendorRepository, vendorRepo VendorRepository,
ingredientRepo IngredientRepository, ingredientRepo IngredientRepository,
purchaseCategoryRepo PurchaseCategoryRepository,
unitRepo UnitRepository, unitRepo UnitRepository,
fileRepo FileRepository, fileRepo FileRepository,
inventoryMovementService InventoryMovementService, inventoryMovementService InventoryMovementService,
@@ -46,7 +44,6 @@ func NewPurchaseOrderProcessorImpl(
purchaseOrderRepo: purchaseOrderRepo, purchaseOrderRepo: purchaseOrderRepo,
vendorRepo: vendorRepo, vendorRepo: vendorRepo,
ingredientRepo: ingredientRepo, ingredientRepo: ingredientRepo,
purchaseCategoryRepo: purchaseCategoryRepo,
unitRepo: unitRepo, unitRepo: unitRepo,
fileRepo: fileRepo, fileRepo: fileRepo,
inventoryMovementService: inventoryMovementService, inventoryMovementService: inventoryMovementService,
@@ -67,40 +64,16 @@ 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 categories and inventory fields per item type. // Validate ingredients and units exist
for i, item := range req.Items { for i, item := range req.Items {
category, err := p.validatePurchaseCategory(ctx, item.PurchaseCategoryID, organizationID, i) _, err := p.ingredientRepo.GetByID(ctx, item.IngredientID, organizationID)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("ingredient not found for item %d: %w", i, err)
} }
switch category.Type { _, err = p.unitRepo.GetByID(ctx, item.UnitID, organizationID)
case entities.PurchaseCategoryTypeRawMaterial: if err != nil {
if item.IngredientID == nil { return nil, fmt.Errorf("unit not found for item %d: %w", i, err)
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.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)
} }
} }
@@ -136,13 +109,12 @@ func (p *PurchaseOrderProcessorImpl) CreatePurchaseOrder(ctx context.Context, or
// Create purchase order items // Create purchase order items
for _, itemReq := range req.Items { for _, itemReq := range req.Items {
itemEntity := &entities.PurchaseOrderItem{ itemEntity := &entities.PurchaseOrderItem{
PurchaseOrderID: poEntity.ID, PurchaseOrderID: poEntity.ID,
IngredientID: itemReq.IngredientID, IngredientID: itemReq.IngredientID,
PurchaseCategoryID: itemReq.PurchaseCategoryID, Description: itemReq.Description,
Description: itemReq.Description, Quantity: itemReq.Quantity,
Quantity: itemReq.Quantity, UnitID: itemReq.UnitID,
UnitID: itemReq.UnitID, Amount: itemReq.Amount,
Amount: itemReq.Amount,
} }
err = p.purchaseOrderRepo.CreateItem(ctx, itemEntity) err = p.purchaseOrderRepo.CreateItem(ctx, itemEntity)
@@ -217,80 +189,68 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrder(ctx context.Context, id
// Update items if provided // Update items if provided
if req.Items != nil { if req.Items != nil {
totalAmount := 0.0 // Delete existing items
items := make([]*entities.PurchaseOrderItem, len(req.Items))
for i, itemReq := range req.Items {
if itemReq.PurchaseCategoryID == nil {
return nil, fmt.Errorf("purchase_category_id is required for item %d", i)
}
ingredientID := itemReq.IngredientID
purchaseCategoryID := *itemReq.PurchaseCategoryID
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 {
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.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,
IngredientID: ingredientID,
PurchaseCategoryID: purchaseCategoryID,
Description: description,
Quantity: quantity,
UnitID: unitID,
Amount: amount,
}
totalAmount += amount
}
// Delete and recreate only after all replacement items are valid.
err = p.purchaseOrderRepo.DeleteItemsByPurchaseOrderID(ctx, poEntity.ID) err = p.purchaseOrderRepo.DeleteItemsByPurchaseOrderID(ctx, poEntity.ID)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to delete existing items: %w", err) return nil, fmt.Errorf("failed to delete existing items: %w", err)
} }
for _, itemEntity := range items { // Create new items
totalAmount := 0.0
for _, itemReq := range req.Items {
// Validate ingredients and units exist
if itemReq.IngredientID != nil {
_, err := p.ingredientRepo.GetByID(ctx, *itemReq.IngredientID, organizationID)
if err != nil {
return nil, fmt.Errorf("ingredient not found: %w", err)
}
}
if itemReq.UnitID != nil {
_, err := p.unitRepo.GetByID(ctx, *itemReq.UnitID, organizationID)
if err != nil {
return nil, fmt.Errorf("unit not found: %w", err)
}
}
// Use existing values if not provided
ingredientID := poEntity.Items[0].IngredientID // This is a simplified approach
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.Quantity != nil {
quantity = *itemReq.Quantity
}
if itemReq.Amount != nil {
amount = *itemReq.Amount
}
if itemReq.Description != nil {
description = itemReq.Description
}
itemEntity := &entities.PurchaseOrderItem{
PurchaseOrderID: poEntity.ID,
IngredientID: ingredientID,
Description: description,
Quantity: quantity,
UnitID: unitID,
Amount: amount,
}
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
@@ -419,27 +379,19 @@ 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
} }
@@ -457,7 +409,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,
@@ -467,10 +419,9 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrderStatus(ctx context.Conte
reason, reason,
&referenceType, &referenceType,
referenceID, referenceID,
&item.ID,
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create inventory movement for ingredient %s: %w", *item.IngredientID, err) return nil, fmt.Errorf("failed to create inventory movement for ingredient %s: %w", item.IngredientID, err)
} }
} }
} }
@@ -489,20 +440,3 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrderStatus(ctx context.Conte
return mappers.PurchaseOrderEntityToResponse(updatedPO), nil return mappers.PurchaseOrderEntityToResponse(updatedPO), nil
} }
func (p *PurchaseOrderProcessorImpl) validatePurchaseCategory(ctx context.Context, categoryID, organizationID uuid.UUID, itemIndex int) (*entities.PurchaseCategory, 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)
}
if !category.IsActive {
return nil, 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)
}
return category, nil
}
+62 -67
View File
@@ -124,6 +124,7 @@ func (r *AnalyticsRepositoryImpl) GetSalesAnalytics(ctx context.Context, organiz
} }
func (r *AnalyticsRepositoryImpl) GetPurchasingAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, groupBy string) (*entities.PurchasingAnalytics, error) { func (r *AnalyticsRepositoryImpl) GetPurchasingAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, groupBy string) (*entities.PurchasingAnalytics, error) {
var summary entities.PurchasingSummary
var outletName *string var outletName *string
if outletID != nil { if outletID != nil {
@@ -143,32 +144,29 @@ func (r *AnalyticsRepositoryImpl) GetPurchasingAnalytics(ctx context.Context, or
outletName = &outlet.Name outletName = &outlet.Name
} }
} }
return r.getPurchaseOrderPurchasingAnalytics(ctx, organizationID, outletID, outletName, dateFrom, dateTo, groupBy)
}
func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, outletName *string, dateFrom, dateTo time.Time, groupBy string) (*entities.PurchasingAnalytics, error) {
var summary entities.PurchasingSummary
summaryQuery := r.db.WithContext(ctx). summaryQuery := r.db.WithContext(ctx).
Table("purchase_orders po"). Table("inventory_movements im").
Select(` Select(`
COALESCE(SUM(poi.amount), 0) as total_purchases, COALESCE(SUM(im.total_cost), 0) as total_purchases,
COUNT(DISTINCT po.id) as total_purchase_orders, COUNT(DISTINCT im.reference_id) as total_purchase_orders,
COALESCE(SUM(poi.quantity), 0) as total_quantity, COALESCE(SUM(im.quantity), 0) as total_quantity,
CASE CASE
WHEN COUNT(DISTINCT po.id) > 0 WHEN COUNT(DISTINCT im.reference_id) > 0
THEN COALESCE(SUM(poi.amount), 0) / COUNT(DISTINCT po.id) THEN COALESCE(SUM(im.total_cost), 0) / COUNT(DISTINCT im.reference_id)
ELSE 0 ELSE 0
END as average_purchase_order_value, END as average_purchase_order_value,
COUNT(DISTINCT i.id) as total_ingredients, COUNT(DISTINCT im.item_id) as total_ingredients,
COUNT(DISTINCT po.vendor_id) as total_vendors 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 purchase_orders po ON im.reference_id = po.id").
Joins("LEFT JOIN ingredients i ON poi.ingredient_id = i.id"). Where("im.organization_id = ?", organizationID).
Joins("LEFT JOIN units u ON poi.unit_id = u.id"). Where("im.movement_type = ?", entities.InventoryMovementTypePurchase).
Where("po.organization_id = ?", organizationID). Where("im.item_type = ?", "INGREDIENT").
Where("po.status != ?", "cancelled"). Where("im.reference_type = ?", entities.InventoryMovementReferenceTypePurchaseOrder).
Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo) Where("im.created_at >= ? AND im.created_at <= ?", dateFrom, dateTo)
summaryQuery = r.applyPurchaseOrderItemOutletFilter(summaryQuery, outletID)
summaryQuery = r.resolveOutletID(summaryQuery, outletID, "im.outlet_id")
if err := summaryQuery.Scan(&summary).Error; err != nil { if err := summaryQuery.Scan(&summary).Error; err != nil {
return nil, err return nil, err
@@ -177,35 +175,36 @@ func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx contex
var dateFormat string var dateFormat string
switch groupBy { switch groupBy {
case "hour": case "hour":
dateFormat = "DATE_TRUNC('hour', po.created_at)" dateFormat = "DATE_TRUNC('hour', im.created_at)"
case "week": case "week":
dateFormat = "DATE_TRUNC('week', po.transaction_date::timestamp)" dateFormat = "DATE_TRUNC('week', im.created_at)"
case "month": case "month":
dateFormat = "DATE_TRUNC('month', po.transaction_date::timestamp)" dateFormat = "DATE_TRUNC('month', im.created_at)"
default: default:
dateFormat = "DATE_TRUNC('day', po.transaction_date::timestamp)" dateFormat = "DATE_TRUNC('day', im.created_at)"
} }
var data []entities.PurchasingAnalyticsData var data []entities.PurchasingAnalyticsData
dataQuery := r.db.WithContext(ctx). dataQuery := r.db.WithContext(ctx).
Table("purchase_orders po"). Table("inventory_movements im").
Select(` Select(`
`+dateFormat+` as date, `+dateFormat+` as date,
COALESCE(SUM(poi.amount), 0) as purchases, COALESCE(SUM(im.total_cost), 0) as purchases,
COUNT(DISTINCT po.id) as purchase_orders, COUNT(DISTINCT im.reference_id) as purchase_orders,
COALESCE(SUM(poi.quantity), 0) as quantity, COALESCE(SUM(im.quantity), 0) as quantity,
COUNT(DISTINCT i.id) as ingredients, COUNT(DISTINCT im.item_id) as ingredients,
COUNT(DISTINCT po.vendor_id) as vendors COUNT(DISTINCT po.vendor_id) as vendors
`). `).
Joins("LEFT JOIN purchase_order_items poi ON poi.purchase_order_id = po.id"). Joins("LEFT JOIN purchase_orders po ON im.reference_id = po.id").
Joins("LEFT JOIN ingredients i ON poi.ingredient_id = i.id"). Where("im.organization_id = ?", organizationID).
Joins("LEFT JOIN units u ON poi.unit_id = u.id"). Where("im.movement_type = ?", entities.InventoryMovementTypePurchase).
Where("po.organization_id = ?", organizationID). Where("im.item_type = ?", "INGREDIENT").
Where("po.status != ?", "cancelled"). Where("im.reference_type = ?", entities.InventoryMovementReferenceTypePurchaseOrder).
Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo). Where("im.created_at >= ? AND im.created_at <= ?", dateFrom, dateTo).
Group(dateFormat). Group(dateFormat).
Order(dateFormat) Order(dateFormat)
dataQuery = r.applyPurchaseOrderItemOutletFilter(dataQuery, outletID)
dataQuery = r.resolveOutletID(dataQuery, outletID, "im.outlet_id")
if err := dataQuery.Scan(&data).Error; err != nil { if err := dataQuery.Scan(&data).Error; err != nil {
return nil, err return nil, err
@@ -213,28 +212,29 @@ func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx contex
var ingredientData []entities.PurchasingIngredientData var ingredientData []entities.PurchasingIngredientData
ingredientQuery := r.db.WithContext(ctx). ingredientQuery := r.db.WithContext(ctx).
Table("purchase_order_items poi"). Table("inventory_movements im").
Select(` Select(`
i.id as ingredient_id, i.id as ingredient_id,
i.name as ingredient_name, i.name as ingredient_name,
COALESCE(SUM(poi.quantity), 0) as quantity, COALESCE(SUM(im.quantity), 0) as quantity,
COALESCE(SUM(poi.amount), 0) as total_cost, COALESCE(SUM(im.total_cost), 0) as total_cost,
CASE CASE
WHEN SUM(poi.quantity) > 0 WHEN SUM(im.quantity) > 0
THEN COALESCE(SUM(poi.amount), 0) / SUM(poi.quantity) THEN COALESCE(SUM(im.total_cost), 0) / SUM(im.quantity)
ELSE 0 ELSE 0
END as average_unit_cost, END as average_unit_cost,
COUNT(DISTINCT po.id) as purchase_order_count COUNT(DISTINCT im.reference_id) as purchase_order_count
`). `).
Joins("JOIN purchase_orders po ON poi.purchase_order_id = po.id"). Joins("JOIN ingredients i ON im.item_id = i.id").
Joins("JOIN ingredients i ON poi.ingredient_id = i.id"). Where("im.organization_id = ?", organizationID).
Joins("LEFT JOIN units u ON poi.unit_id = u.id"). Where("im.movement_type = ?", entities.InventoryMovementTypePurchase).
Where("po.organization_id = ?", organizationID). Where("im.item_type = ?", "INGREDIENT").
Where("po.status != ?", "cancelled"). Where("im.reference_type = ?", entities.InventoryMovementReferenceTypePurchaseOrder).
Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo). Where("im.created_at >= ? AND im.created_at <= ?", dateFrom, dateTo).
Group("i.id, i.name"). Group("i.id, i.name").
Order("total_cost DESC") Order("total_cost DESC")
ingredientQuery = r.applyPurchaseOrderItemOutletFilter(ingredientQuery, outletID)
ingredientQuery = r.resolveOutletID(ingredientQuery, outletID, "im.outlet_id")
if err := ingredientQuery.Scan(&ingredientData).Error; err != nil { if err := ingredientQuery.Scan(&ingredientData).Error; err != nil {
return nil, err return nil, err
@@ -242,25 +242,26 @@ func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx contex
var vendorData []entities.PurchasingVendorData var vendorData []entities.PurchasingVendorData
vendorQuery := r.db.WithContext(ctx). vendorQuery := r.db.WithContext(ctx).
Table("purchase_orders po"). Table("inventory_movements im").
Select(` Select(`
v.id as vendor_id, v.id as vendor_id,
v.name as vendor_name, v.name as vendor_name,
COALESCE(SUM(poi.amount), 0) as total_cost, COALESCE(SUM(im.total_cost), 0) as total_cost,
COUNT(DISTINCT po.id) as purchase_order_count, COUNT(DISTINCT im.reference_id) as purchase_order_count,
COUNT(DISTINCT i.id) as ingredient_count, COUNT(DISTINCT im.item_id) as ingredient_count,
COALESCE(SUM(poi.quantity), 0) as quantity COALESCE(SUM(im.quantity), 0) as quantity
`). `).
Joins("JOIN purchase_orders po ON im.reference_id = po.id").
Joins("JOIN vendors v ON po.vendor_id = v.id"). Joins("JOIN vendors v ON po.vendor_id = v.id").
Joins("LEFT JOIN purchase_order_items poi ON poi.purchase_order_id = po.id"). Where("im.organization_id = ?", organizationID).
Joins("LEFT JOIN ingredients i ON poi.ingredient_id = i.id"). Where("im.movement_type = ?", entities.InventoryMovementTypePurchase).
Joins("LEFT JOIN units u ON poi.unit_id = u.id"). Where("im.item_type = ?", "INGREDIENT").
Where("po.organization_id = ?", organizationID). Where("im.reference_type = ?", entities.InventoryMovementReferenceTypePurchaseOrder).
Where("po.status != ?", "cancelled"). Where("im.created_at >= ? AND im.created_at <= ?", dateFrom, dateTo).
Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo).
Group("v.id, v.name"). Group("v.id, v.name").
Order("total_cost DESC") Order("total_cost DESC")
vendorQuery = r.applyPurchaseOrderItemOutletFilter(vendorQuery, outletID)
vendorQuery = r.resolveOutletID(vendorQuery, outletID, "im.outlet_id")
if err := vendorQuery.Scan(&vendorData).Error; err != nil { if err := vendorQuery.Scan(&vendorData).Error; err != nil {
return nil, err return nil, err
@@ -275,13 +276,6 @@ func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx contex
}, nil }, nil
} }
func (r *AnalyticsRepositoryImpl) applyPurchaseOrderItemOutletFilter(query *gorm.DB, outletID *uuid.UUID) *gorm.DB {
if outletID == nil {
return query
}
return query.Where("(i.outlet_id = ? OR u.outlet_id = ?)", *outletID, *outletID)
}
func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, limit int) ([]*entities.ProductAnalytics, error) { func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, limit int) ([]*entities.ProductAnalytics, error) {
var results []*entities.ProductAnalytics var results []*entities.ProductAnalytics
@@ -290,6 +284,7 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organ
Select(` Select(`
p.id as product_id, p.id as product_id,
p.name as product_name, p.name as product_name,
p.price as product_price,
c.id as category_id, c.id as category_id,
c.name as category_name, c.name as category_name,
c.order as category_order, c.order as category_order,
@@ -348,7 +343,7 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organ
query = r.resolveOutletID(query, outletID, "o.outlet_id") query = r.resolveOutletID(query, outletID, "o.outlet_id")
err := query. err := query.
Group("p.id, p.name, p.cost, c.id, c.name, c.order, mahpp.hpp_per_unit"). Group("p.id, p.name, p.price, p.cost, c.id, c.name, c.order, mahpp.hpp_per_unit").
Order("revenue DESC"). Order("revenue DESC").
Limit(limit). Limit(limit).
Scan(&results).Error Scan(&results).Error
+8 -38
View File
@@ -30,7 +30,6 @@ func (r *ExpenseRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*ent
var expense entities.Expense var expense entities.Expense
err := r.db.WithContext(ctx). err := r.db.WithContext(ctx).
Preload("Items.ChartOfAccount"). Preload("Items.ChartOfAccount").
Preload("Items.PurchaseCategory").
First(&expense, "id = ?", id).Error First(&expense, "id = ?", id).Error
if err != nil { if err != nil {
return nil, err return nil, err
@@ -42,7 +41,6 @@ func (r *ExpenseRepositoryImpl) GetByIDAndOrganizationID(ctx context.Context, id
var expense entities.Expense var expense entities.Expense
err := r.db.WithContext(ctx). err := r.db.WithContext(ctx).
Preload("Items.ChartOfAccount"). Preload("Items.ChartOfAccount").
Preload("Items.PurchaseCategory").
Where("id = ? AND organization_id = ?", id, organizationID). Where("id = ? AND organization_id = ?", id, organizationID).
First(&expense).Error First(&expense).Error
if err != nil { if err != nil {
@@ -109,7 +107,6 @@ func (r *ExpenseRepositoryImpl) List(ctx context.Context, organizationID uuid.UU
err := query. err := query.
Preload("Items.ChartOfAccount"). Preload("Items.ChartOfAccount").
Preload("Items.PurchaseCategory").
Order("created_at DESC"). Order("created_at DESC").
Limit(limit). Limit(limit).
Offset(offset). Offset(offset).
@@ -142,7 +139,7 @@ func (r *ExpenseRepositoryImpl) GetAnalytics(ctx context.Context, organizationID
Table("expense_items ei"). Table("expense_items ei").
Select(` Select(`
COUNT(ei.id) as total_items, COUNT(ei.id) as total_items,
COUNT(DISTINCT ei.purchase_category_id) as total_categories COUNT(DISTINCT ei.chart_of_account_id) as total_categories
`). `).
Joins("JOIN expenses e ON ei.expense_id = e.id"). Joins("JOIN expenses e ON ei.expense_id = e.id").
Where("e.organization_id = ?", organizationID). Where("e.organization_id = ?", organizationID).
@@ -177,7 +174,7 @@ func (r *ExpenseRepositoryImpl) GetAnalytics(ctx context.Context, organizationID
COALESCE(SUM(item_counts.categories), 0) as categories COALESCE(SUM(item_counts.categories), 0) as categories
`). `).
Joins(`LEFT JOIN ( Joins(`LEFT JOIN (
SELECT expense_id, COUNT(id) as items, COUNT(DISTINCT purchase_category_id) as categories SELECT expense_id, COUNT(id) as items, COUNT(DISTINCT chart_of_account_id) as categories
FROM expense_items FROM expense_items
GROUP BY expense_id GROUP BY expense_id
) item_counts ON item_counts.expense_id = e.id`). ) item_counts ON item_counts.expense_id = e.id`).
@@ -195,32 +192,6 @@ func (r *ExpenseRepositoryImpl) GetAnalytics(ctx context.Context, organizationID
var categoryData []entities.ExpenseAnalyticsCategoryData var categoryData []entities.ExpenseAnalyticsCategoryData
categoryQuery := r.db.WithContext(ctx). categoryQuery := r.db.WithContext(ctx).
Table("expense_items ei").
Select(`
pc.id as purchase_category_id,
pc.name as purchase_category_name,
pc.type as purchase_category_type,
COALESCE(SUM(ei.amount), 0) as total_amount,
COUNT(DISTINCT e.id) as expense_count,
COUNT(ei.id) as item_count
`).
Joins("JOIN expenses e ON ei.expense_id = e.id").
Joins("JOIN purchase_categories pc ON ei.purchase_category_id = pc.id").
Where("e.organization_id = ?", organizationID).
Where("pc.type = ?", entities.PurchaseCategoryTypeExpense).
Where("e.status = ?", "approved").
Where("e.transaction_date >= ? AND e.transaction_date <= ?", dateFrom, dateTo).
Group("pc.id, pc.name, pc.type").
Order("total_amount DESC")
if outletID != nil {
categoryQuery = categoryQuery.Where("e.outlet_id = ?", *outletID)
}
if err := categoryQuery.Scan(&categoryData).Error; err != nil {
return nil, err
}
var chartOfAccountData []entities.ExpenseAnalyticsChartOfAccountData
chartOfAccountQuery := r.db.WithContext(ctx).
Table("expense_items ei"). Table("expense_items ei").
Select(` Select(`
COALESCE(parent_coa.id, coa.id) as chart_of_account_id, COALESCE(parent_coa.id, coa.id) as chart_of_account_id,
@@ -238,9 +209,9 @@ func (r *ExpenseRepositoryImpl) GetAnalytics(ctx context.Context, organizationID
Group("COALESCE(parent_coa.id, coa.id), COALESCE(parent_coa.name, coa.name, 'Lain-lain')"). Group("COALESCE(parent_coa.id, coa.id), COALESCE(parent_coa.name, coa.name, 'Lain-lain')").
Order("total_amount DESC") Order("total_amount DESC")
if outletID != nil { if outletID != nil {
chartOfAccountQuery = chartOfAccountQuery.Where("e.outlet_id = ?", *outletID) categoryQuery = categoryQuery.Where("e.outlet_id = ?", *outletID)
} }
if err := chartOfAccountQuery.Scan(&chartOfAccountData).Error; err != nil { if err := categoryQuery.Scan(&categoryData).Error; err != nil {
return nil, err return nil, err
} }
@@ -268,11 +239,10 @@ func (r *ExpenseRepositoryImpl) GetAnalytics(ctx context.Context, organizationID
} }
return &entities.ExpenseAnalytics{ return &entities.ExpenseAnalytics{
Summary: summary, Summary: summary,
Data: data, Data: data,
CategoryData: categoryData, CategoryData: categoryData,
ChartOfAccountData: chartOfAccountData, ItemData: itemData,
ItemData: itemData,
}, nil }, nil
} }
@@ -31,7 +31,6 @@ func (r *PurchaseOrderRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID)
err := r.db.WithContext(ctx). err := r.db.WithContext(ctx).
Preload("Vendor"). Preload("Vendor").
Preload("Items.Ingredient"). Preload("Items.Ingredient").
Preload("Items.PurchaseCategory").
Preload("Items.Unit"). Preload("Items.Unit").
Preload("Attachments.File"). Preload("Attachments.File").
First(&po, "id = ?", id).Error First(&po, "id = ?", id).Error
@@ -46,7 +45,6 @@ func (r *PurchaseOrderRepositoryImpl) GetByIDAndOrganizationID(ctx context.Conte
err := r.db.WithContext(ctx). err := r.db.WithContext(ctx).
Preload("Vendor"). Preload("Vendor").
Preload("Items.Ingredient"). Preload("Items.Ingredient").
Preload("Items.PurchaseCategory").
Preload("Items.Unit"). Preload("Items.Unit").
Preload("Attachments.File"). Preload("Attachments.File").
Where("id = ? AND organization_id = ?", id, organizationID). Where("id = ? AND organization_id = ?", id, organizationID).
@@ -107,7 +105,6 @@ func (r *PurchaseOrderRepositoryImpl) List(ctx context.Context, organizationID u
err := query. err := query.
Preload("Vendor"). Preload("Vendor").
Preload("Items.Ingredient"). Preload("Items.Ingredient").
Preload("Items.PurchaseCategory").
Preload("Items.Unit"). Preload("Items.Unit").
Preload("Attachments.File"). Preload("Attachments.File").
Order("created_at DESC"). Order("created_at DESC").
@@ -171,7 +168,6 @@ func (r *PurchaseOrderRepositoryImpl) GetByStatus(ctx context.Context, organizat
Where("organization_id = ? AND status = ?", organizationID, status). Where("organization_id = ? AND status = ?", organizationID, status).
Preload("Vendor"). Preload("Vendor").
Preload("Items.Ingredient"). Preload("Items.Ingredient").
Preload("Items.PurchaseCategory").
Preload("Items.Unit"). Preload("Items.Unit").
Find(&pos).Error Find(&pos).Error
return pos, err return pos, err
@@ -183,7 +179,6 @@ func (r *PurchaseOrderRepositoryImpl) GetOverdue(ctx context.Context, organizati
Where("organization_id = ? AND due_date < ? AND status IN (?)", organizationID, time.Now(), []string{"draft", "sent", "approved"}). Where("organization_id = ? AND due_date < ? AND status IN (?)", organizationID, time.Now(), []string{"draft", "sent", "approved"}).
Preload("Vendor"). Preload("Vendor").
Preload("Items.Ingredient"). Preload("Items.Ingredient").
Preload("Items.PurchaseCategory").
Preload("Items.Unit"). Preload("Items.Unit").
Find(&pos).Error Find(&pos).Error
return pos, err return pos, err
@@ -224,7 +219,6 @@ func (r *PurchaseOrderRepositoryImpl) GetItemsByPurchaseOrderID(ctx context.Cont
var items []*entities.PurchaseOrderItem var items []*entities.PurchaseOrderItem
err := r.db.WithContext(ctx). err := r.db.WithContext(ctx).
Preload("Ingredient"). Preload("Ingredient").
Preload("PurchaseCategory").
Preload("Unit"). Preload("Unit").
Where("purchase_order_id = ?", purchaseOrderID). Where("purchase_order_id = ?", purchaseOrderID).
Find(&items).Error Find(&items).Error
+18 -19
View File
@@ -10,7 +10,7 @@ import (
) )
type InventoryMovementService interface { type InventoryMovementService interface {
CreateIngredientMovement(ctx context.Context, ingredientID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID, purchaseOrderItemID *uuid.UUID) error CreateIngredientMovement(ctx context.Context, ingredientID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error
CreateProductMovement(ctx context.Context, productID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error CreateProductMovement(ctx context.Context, productID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error
} }
@@ -26,7 +26,7 @@ func NewInventoryMovementService(inventoryMovementRepo repository.InventoryMovem
} }
} }
func (s *InventoryMovementServiceImpl) CreateIngredientMovement(ctx context.Context, ingredientID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID, purchaseOrderItemID *uuid.UUID) error { func (s *InventoryMovementServiceImpl) CreateIngredientMovement(ctx context.Context, ingredientID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error {
ingredient, err := s.ingredientRepo.GetByID(ctx, ingredientID, organizationID) ingredient, err := s.ingredientRepo.GetByID(ctx, ingredientID, organizationID)
if err != nil { if err != nil {
return err return err
@@ -36,23 +36,22 @@ func (s *InventoryMovementServiceImpl) CreateIngredientMovement(ctx context.Cont
newQuantity := previousQuantity + quantity newQuantity := previousQuantity + quantity
movement := &entities.InventoryMovement{ movement := &entities.InventoryMovement{
ID: uuid.New(), ID: uuid.New(),
OrganizationID: organizationID, OrganizationID: organizationID,
OutletID: outletID, OutletID: outletID,
ItemID: ingredientID, ItemID: ingredientID,
ItemType: "INGREDIENT", ItemType: "INGREDIENT",
MovementType: movementType, MovementType: movementType,
Quantity: quantity, Quantity: quantity,
PreviousQuantity: previousQuantity, PreviousQuantity: previousQuantity,
NewQuantity: newQuantity, NewQuantity: newQuantity,
UnitCost: unitCost, UnitCost: unitCost,
TotalCost: unitCost * quantity, TotalCost: unitCost * quantity,
ReferenceType: referenceType, ReferenceType: referenceType,
ReferenceID: referenceID, ReferenceID: referenceID,
PurchaseOrderItemID: purchaseOrderItemID, UserID: userID,
UserID: userID, Reason: &reason,
Reason: &reason, CreatedAt: time.Now(),
CreatedAt: time.Now(),
} }
err = s.inventoryMovementRepo.Create(ctx, movement) err = s.inventoryMovementRepo.Create(ctx, movement)
+6 -14
View File
@@ -169,16 +169,12 @@ func PurchasingAnalyticsModelToContract(resp *models.PurchasingAnalyticsResponse
data := make([]contract.PurchasingAnalyticsData, len(resp.Data)) data := make([]contract.PurchasingAnalyticsData, len(resp.Data))
for i, item := range resp.Data { for i, item := range resp.Data {
data[i] = contract.PurchasingAnalyticsData{ data[i] = contract.PurchasingAnalyticsData{
Date: item.Date, Date: item.Date,
Purchases: item.Purchases, Purchases: item.Purchases,
RawMaterialPurchases: item.RawMaterialPurchases, PurchaseOrders: item.PurchaseOrders,
ExpensePurchases: item.ExpensePurchases, Quantity: item.Quantity,
PurchaseOrders: item.PurchaseOrders, Ingredients: item.Ingredients,
RawMaterialPurchaseOrders: item.RawMaterialPurchaseOrders, Vendors: item.Vendors,
ExpenseCount: item.ExpenseCount,
Quantity: item.Quantity,
Ingredients: item.Ingredients,
Vendors: item.Vendors,
} }
} }
@@ -215,11 +211,7 @@ func PurchasingAnalyticsModelToContract(resp *models.PurchasingAnalyticsResponse
GroupBy: resp.GroupBy, GroupBy: resp.GroupBy,
Summary: contract.PurchasingSummary{ Summary: contract.PurchasingSummary{
TotalPurchases: resp.Summary.TotalPurchases, TotalPurchases: resp.Summary.TotalPurchases,
RawMaterialPurchases: resp.Summary.RawMaterialPurchases,
ExpensePurchases: resp.Summary.ExpensePurchases,
TotalPurchaseOrders: resp.Summary.TotalPurchaseOrders, TotalPurchaseOrders: resp.Summary.TotalPurchaseOrders,
RawMaterialPurchaseOrders: resp.Summary.RawMaterialPurchaseOrders,
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,
@@ -52,47 +52,17 @@ func TestPurchasingAnalyticsContractToModelIgnoresInvalidOutlet(t *testing.T) {
func TestPurchasingAnalyticsModelToContractCopiesOutletName(t *testing.T) { func TestPurchasingAnalyticsModelToContractCopiesOutletName(t *testing.T) {
outletID := uuid.New() outletID := uuid.New()
outletName := "Main Outlet" outletName := "Main Outlet"
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
result := PurchasingAnalyticsModelToContract(&models.PurchasingAnalyticsResponse{ result := PurchasingAnalyticsModelToContract(&models.PurchasingAnalyticsResponse{
OrganizationID: uuid.New(), OrganizationID: uuid.New(),
OutletID: &outletID, OutletID: &outletID,
OutletName: &outletName, OutletName: &outletName,
Summary: models.PurchasingSummary{
TotalPurchases: 300,
RawMaterialPurchases: 125,
ExpensePurchases: 175,
TotalPurchaseOrders: 3,
RawMaterialPurchaseOrders: 1,
ExpenseCount: 2,
},
Data: []models.PurchasingAnalyticsData{
{
Date: now,
Purchases: 300,
RawMaterialPurchases: 125,
ExpensePurchases: 175,
PurchaseOrders: 3,
RawMaterialPurchaseOrders: 1,
ExpenseCount: 2,
},
},
}) })
require.NotNil(t, result) require.NotNil(t, result)
require.Equal(t, &outletID, result.OutletID) require.Equal(t, &outletID, result.OutletID)
require.NotNil(t, result.OutletName) require.NotNil(t, result.OutletName)
require.Equal(t, outletName, *result.OutletName) require.Equal(t, outletName, *result.OutletName)
require.Equal(t, float64(300), result.Summary.TotalPurchases)
require.Equal(t, float64(125), result.Summary.RawMaterialPurchases)
require.Equal(t, float64(175), result.Summary.ExpensePurchases)
require.Equal(t, int64(3), result.Summary.TotalPurchaseOrders)
require.Equal(t, int64(1), result.Summary.RawMaterialPurchaseOrders)
require.Equal(t, int64(2), result.Summary.ExpenseCount)
require.Len(t, result.Data, 1)
require.Equal(t, float64(300), result.Data[0].Purchases)
require.Equal(t, float64(125), result.Data[0].RawMaterialPurchases)
require.Equal(t, float64(175), result.Data[0].ExpensePurchases)
} }
func TestPurchasingAnalyticsModelToContractOmitsNilOutletName(t *testing.T) { func TestPurchasingAnalyticsModelToContractOmitsNilOutletName(t *testing.T) {
+20 -39
View File
@@ -27,11 +27,10 @@ func CreateExpenseRequestToModel(req *contract.CreateExpenseRequest) *models.Cre
func CreateExpenseItemRequestToModel(req *contract.CreateExpenseItemRequest) models.CreateExpenseItemRequest { func CreateExpenseItemRequestToModel(req *contract.CreateExpenseItemRequest) models.CreateExpenseItemRequest {
return models.CreateExpenseItemRequest{ return models.CreateExpenseItemRequest{
ChartOfAccountID: req.ChartOfAccountID, ChartOfAccountID: req.ChartOfAccountID,
PurchaseCategoryID: req.PurchaseCategoryID, Item: req.Item,
Item: req.Item, Description: req.Description,
Description: req.Description, Amount: req.Amount,
Amount: req.Amount,
} }
} }
@@ -61,11 +60,10 @@ func UpdateExpenseRequestToModel(req *contract.UpdateExpenseRequest) *models.Upd
func UpdateExpenseItemRequestToModel(req *contract.UpdateExpenseItemRequest) models.UpdateExpenseItemRequest { func UpdateExpenseItemRequestToModel(req *contract.UpdateExpenseItemRequest) models.UpdateExpenseItemRequest {
return models.UpdateExpenseItemRequest{ return models.UpdateExpenseItemRequest{
ChartOfAccountID: req.ChartOfAccountID, ChartOfAccountID: req.ChartOfAccountID,
PurchaseCategoryID: req.PurchaseCategoryID, Item: req.Item,
Item: req.Item, Description: req.Description,
Description: req.Description, Amount: req.Amount,
Amount: req.Amount,
} }
} }
@@ -111,19 +109,15 @@ func ExpenseModelResponseToResponse(expense *models.ExpenseResponse) *contract.E
func ExpenseItemModelResponseToResponse(item *models.ExpenseItemResponse) contract.ExpenseItemResponse { func ExpenseItemModelResponseToResponse(item *models.ExpenseItemResponse) contract.ExpenseItemResponse {
return contract.ExpenseItemResponse{ return contract.ExpenseItemResponse{
ID: item.ID, ID: item.ID,
ExpenseID: item.ExpenseID, ExpenseID: item.ExpenseID,
ChartOfAccountID: item.ChartOfAccountID, ChartOfAccountID: item.ChartOfAccountID,
ChartOfAccountName: item.ChartOfAccountName, ChartOfAccountName: item.ChartOfAccountName,
PurchaseCategoryID: item.PurchaseCategoryID, Item: item.Item,
PurchaseCategoryName: item.PurchaseCategoryName, Description: item.Description,
PurchaseCategoryType: item.PurchaseCategoryType, Amount: item.Amount,
PurchaseCategory: PurchaseCategoryModelResponseToResponse(item.PurchaseCategory), CreatedAt: item.CreatedAt,
Item: item.Item, UpdatedAt: item.UpdatedAt,
Description: item.Description,
Amount: item.Amount,
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
} }
} }
@@ -182,18 +176,6 @@ func ExpenseAnalyticsModelToContract(resp *models.ExpenseAnalyticsResponse) *con
categoryData := make([]contract.ExpenseAnalyticsCategoryData, len(resp.CategoryData)) categoryData := make([]contract.ExpenseAnalyticsCategoryData, len(resp.CategoryData))
for i, item := range resp.CategoryData { for i, item := range resp.CategoryData {
categoryData[i] = contract.ExpenseAnalyticsCategoryData{ categoryData[i] = contract.ExpenseAnalyticsCategoryData{
PurchaseCategoryID: item.PurchaseCategoryID,
PurchaseCategoryName: item.PurchaseCategoryName,
PurchaseCategoryType: item.PurchaseCategoryType,
TotalAmount: item.TotalAmount,
ExpenseCount: item.ExpenseCount,
ItemCount: item.ItemCount,
}
}
chartOfAccountData := make([]contract.ExpenseAnalyticsChartOfAccountData, len(resp.ChartOfAccountData))
for i, item := range resp.ChartOfAccountData {
chartOfAccountData[i] = contract.ExpenseAnalyticsChartOfAccountData{
ChartOfAccountID: item.ChartOfAccountID, ChartOfAccountID: item.ChartOfAccountID,
ChartOfAccountName: item.ChartOfAccountName, ChartOfAccountName: item.ChartOfAccountName,
TotalAmount: item.TotalAmount, TotalAmount: item.TotalAmount,
@@ -226,9 +208,8 @@ func ExpenseAnalyticsModelToContract(resp *models.ExpenseAnalyticsResponse) *con
TotalCategories: resp.Summary.TotalCategories, TotalCategories: resp.Summary.TotalCategories,
TotalItems: resp.Summary.TotalItems, TotalItems: resp.Summary.TotalItems,
}, },
Data: data, Data: data,
CategoryData: categoryData, CategoryData: categoryData,
ChartOfAccountData: chartOfAccountData, ItemData: itemData,
ItemData: itemData,
} }
} }
@@ -11,12 +11,11 @@ func CreatePurchaseOrderRequestToModel(req *contract.CreatePurchaseOrderRequest)
items := make([]models.CreatePurchaseOrderItemRequest, len(req.Items)) items := make([]models.CreatePurchaseOrderItemRequest, len(req.Items))
for i, item := range req.Items { for i, item := range req.Items {
items[i] = models.CreatePurchaseOrderItemRequest{ items[i] = models.CreatePurchaseOrderItemRequest{
IngredientID: item.IngredientID, IngredientID: item.IngredientID,
PurchaseCategoryID: item.PurchaseCategoryID, Description: item.Description,
Description: item.Description, Quantity: item.Quantity,
Quantity: item.Quantity, UnitID: item.UnitID,
UnitID: item.UnitID, Amount: item.Amount,
Amount: item.Amount,
} }
} }
@@ -55,13 +54,12 @@ func UpdatePurchaseOrderRequestToModel(req *contract.UpdatePurchaseOrderRequest)
items = make([]models.UpdatePurchaseOrderItemRequest, len(req.Items)) items = make([]models.UpdatePurchaseOrderItemRequest, len(req.Items))
for i, item := range req.Items { for i, item := range req.Items {
items[i] = models.UpdatePurchaseOrderItemRequest{ items[i] = models.UpdatePurchaseOrderItemRequest{
ID: item.ID, ID: item.ID,
IngredientID: item.IngredientID, IngredientID: item.IngredientID,
PurchaseCategoryID: item.PurchaseCategoryID, Description: item.Description,
Description: item.Description, Quantity: item.Quantity,
Quantity: item.Quantity, UnitID: item.UnitID,
UnitID: item.UnitID, Amount: item.Amount,
Amount: item.Amount,
} }
} }
} }
@@ -156,16 +154,15 @@ func PurchaseOrderModelResponseToResponse(po *models.PurchaseOrderResponse) *con
response.Items = make([]contract.PurchaseOrderItemResponse, len(po.Items)) response.Items = make([]contract.PurchaseOrderItemResponse, len(po.Items))
for i, item := range po.Items { for i, item := range po.Items {
response.Items[i] = contract.PurchaseOrderItemResponse{ response.Items[i] = contract.PurchaseOrderItemResponse{
ID: item.ID, ID: item.ID,
PurchaseOrderID: item.PurchaseOrderID, PurchaseOrderID: item.PurchaseOrderID,
IngredientID: item.IngredientID, IngredientID: item.IngredientID,
PurchaseCategoryID: item.PurchaseCategoryID, Description: item.Description,
Description: item.Description, Quantity: item.Quantity,
Quantity: item.Quantity, UnitID: item.UnitID,
UnitID: item.UnitID, Amount: item.Amount,
Amount: item.Amount, CreatedAt: item.CreatedAt,
CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt,
UpdatedAt: item.UpdatedAt,
} }
// Map ingredient if present // Map ingredient if present
@@ -176,10 +173,6 @@ func PurchaseOrderModelResponseToResponse(po *models.PurchaseOrderResponse) *con
} }
} }
if item.PurchaseCategory != nil {
response.Items[i].PurchaseCategory = PurchaseCategoryModelResponseToResponse(item.PurchaseCategory)
}
// Map unit if present // Map unit if present
if item.Unit != nil { if item.Unit != nil {
response.Items[i].Unit = &contract.UnitResponse{ response.Items[i].Unit = &contract.UnitResponse{
@@ -12,19 +12,15 @@ 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: &ingredientID, IngredientID: uuid.New(),
Quantity: &quantity, Quantity: 1,
UnitID: &unitID, UnitID: uuid.New(),
Amount: 1000, Amount: 1000,
}, },
}, },
-15
View File
@@ -68,18 +68,12 @@ func (v *ExpenseValidatorImpl) ValidateCreateExpenseRequest(req *contract.Create
if strings.TrimSpace(item.ChartOfAccountID) == "" { if strings.TrimSpace(item.ChartOfAccountID) == "" {
return fmt.Errorf("item %d: chart_of_account_id is required", i), constants.MissingFieldErrorCode return fmt.Errorf("item %d: chart_of_account_id is required", i), constants.MissingFieldErrorCode
} }
if strings.TrimSpace(item.PurchaseCategoryID) == "" {
return fmt.Errorf("item %d: purchase_category_id is required", i), constants.MissingFieldErrorCode
}
if strings.TrimSpace(item.Item) == "" { if strings.TrimSpace(item.Item) == "" {
return fmt.Errorf("item %d: item is required", i), constants.MissingFieldErrorCode return fmt.Errorf("item %d: item is required", i), constants.MissingFieldErrorCode
} }
if _, err := uuid.Parse(item.ChartOfAccountID); err != nil { if _, err := uuid.Parse(item.ChartOfAccountID); err != nil {
return fmt.Errorf("item %d: chart_of_account_id must be a valid UUID", i), constants.MalformedFieldErrorCode return fmt.Errorf("item %d: chart_of_account_id must be a valid UUID", i), constants.MalformedFieldErrorCode
} }
if _, err := uuid.Parse(item.PurchaseCategoryID); err != nil {
return fmt.Errorf("item %d: purchase_category_id must be a valid UUID", i), constants.MalformedFieldErrorCode
}
if item.Amount <= 0 { if item.Amount <= 0 {
return fmt.Errorf("item %d: amount must be greater than 0", i), constants.MalformedFieldErrorCode return fmt.Errorf("item %d: amount must be greater than 0", i), constants.MalformedFieldErrorCode
} }
@@ -132,15 +126,6 @@ func (v *ExpenseValidatorImpl) ValidateUpdateExpenseRequest(req *contract.Update
return fmt.Errorf("item %d: chart_of_account_id must be a valid UUID", i), constants.MalformedFieldErrorCode return fmt.Errorf("item %d: chart_of_account_id must be a valid UUID", i), constants.MalformedFieldErrorCode
} }
} }
if item.PurchaseCategoryID == nil {
return fmt.Errorf("item %d: purchase_category_id is required", i), constants.MissingFieldErrorCode
}
if strings.TrimSpace(*item.PurchaseCategoryID) == "" {
return fmt.Errorf("item %d: purchase_category_id cannot be empty", i), constants.MalformedFieldErrorCode
}
if _, err := uuid.Parse(*item.PurchaseCategoryID); err != nil {
return fmt.Errorf("item %d: purchase_category_id must be a valid UUID", i), constants.MalformedFieldErrorCode
}
if item.Item != nil && strings.TrimSpace(*item.Item) == "" { if item.Item != nil && strings.TrimSpace(*item.Item) == "" {
return fmt.Errorf("item %d: item cannot be empty", i), constants.MalformedFieldErrorCode return fmt.Errorf("item %d: item cannot be empty", i), constants.MalformedFieldErrorCode
} }
+12 -17
View File
@@ -21,9 +21,8 @@ func TestExpenseValidatorCreateRequiresItemName(t *testing.T) {
Total: 10000, Total: 10000,
Items: []contract.CreateExpenseItemRequest{ Items: []contract.CreateExpenseItemRequest{
{ {
ChartOfAccountID: uuid.NewString(), ChartOfAccountID: uuid.NewString(),
PurchaseCategoryID: uuid.NewString(), Amount: 10000,
Amount: 10000,
}, },
}, },
} }
@@ -46,10 +45,9 @@ func TestExpenseValidatorCreateDoesNotRequireHeaderExpenseName(t *testing.T) {
Total: 10000, Total: 10000,
Items: []contract.CreateExpenseItemRequest{ Items: []contract.CreateExpenseItemRequest{
{ {
ChartOfAccountID: uuid.NewString(), ChartOfAccountID: uuid.NewString(),
PurchaseCategoryID: uuid.NewString(), Item: "Cleaning supplies",
Item: "Cleaning supplies", Amount: 10000,
Amount: 10000,
}, },
}, },
} }
@@ -73,10 +71,9 @@ func TestExpenseValidatorCreateAllowsValidOptionalStatus(t *testing.T) {
Total: 10000, Total: 10000,
Items: []contract.CreateExpenseItemRequest{ Items: []contract.CreateExpenseItemRequest{
{ {
ChartOfAccountID: uuid.NewString(), ChartOfAccountID: uuid.NewString(),
PurchaseCategoryID: uuid.NewString(), Item: "Cleaning supplies",
Item: "Cleaning supplies", Amount: 10000,
Amount: 10000,
}, },
}, },
} }
@@ -100,10 +97,9 @@ func TestExpenseValidatorCreateRejectsInvalidStatus(t *testing.T) {
Total: 10000, Total: 10000,
Items: []contract.CreateExpenseItemRequest{ Items: []contract.CreateExpenseItemRequest{
{ {
ChartOfAccountID: uuid.NewString(), ChartOfAccountID: uuid.NewString(),
PurchaseCategoryID: uuid.NewString(), Item: "Cleaning supplies",
Item: "Cleaning supplies", Amount: 10000,
Amount: 10000,
}, },
}, },
} }
@@ -118,11 +114,10 @@ func TestExpenseValidatorCreateRejectsInvalidStatus(t *testing.T) {
func TestExpenseValidatorUpdateRejectsEmptyItemNameWhenProvided(t *testing.T) { func TestExpenseValidatorUpdateRejectsEmptyItemNameWhenProvided(t *testing.T) {
v := NewExpenseValidator() v := NewExpenseValidator()
empty := " " empty := " "
purchaseCategoryID := uuid.NewString()
req := &contract.UpdateExpenseRequest{ req := &contract.UpdateExpenseRequest{
Items: []contract.UpdateExpenseItemRequest{ Items: []contract.UpdateExpenseItemRequest{
{PurchaseCategoryID: &purchaseCategoryID, Item: &empty}, {Item: &empty},
}, },
} }
@@ -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 expense"), constants.MalformedFieldErrorCode return errors.New("type must be raw_material or non_inventory"), 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 expense"), constants.MalformedFieldErrorCode return errors.New("type must be raw_material or non_inventory"), 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 expense"), constants.MalformedFieldErrorCode return errors.New("type must be raw_material or non_inventory"), 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", "expense": case "raw_material", "non_inventory":
return true return true
default: default:
return false return false
+10 -29
View File
@@ -2,14 +2,11 @@ package validator
import ( import (
"errors" "errors"
"strconv"
"strings" "strings"
"time" "time"
"apskel-pos-be/internal/constants" "apskel-pos-be/internal/constants"
"apskel-pos-be/internal/contract" "apskel-pos-be/internal/contract"
"github.com/google/uuid"
) )
type PurchaseOrderValidator interface { type PurchaseOrderValidator interface {
@@ -29,7 +26,7 @@ func (v *PurchaseOrderValidatorImpl) ValidateCreatePurchaseOrderRequest(req *con
return errors.New("request body is required"), constants.MissingFieldErrorCode return errors.New("request body is required"), constants.MissingFieldErrorCode
} }
if req.VendorID == uuid.Nil { if req.VendorID.String() == "" {
return errors.New("vendor_id is required"), constants.MissingFieldErrorCode return errors.New("vendor_id is required"), constants.MissingFieldErrorCode
} }
@@ -181,48 +178,32 @@ 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.PurchaseCategoryID == uuid.Nil { if item.IngredientID.String() == "" {
return errors.New("items[" + strconv.Itoa(index) + "].purchase_category_id is required"), constants.MissingFieldErrorCode return errors.New("items[" + string(rune(index)) + "].ingredient_id is required"), constants.MissingFieldErrorCode
} }
if item.IngredientID != nil && *item.IngredientID == uuid.Nil { if item.Quantity <= 0 {
return errors.New("items[" + strconv.Itoa(index) + "].ingredient_id cannot be empty"), constants.MalformedFieldErrorCode return errors.New("items[" + string(rune(index)) + "].quantity must be greater than 0"), constants.MalformedFieldErrorCode
} }
if item.Quantity != nil && *item.Quantity <= 0 { if item.UnitID.String() == "" {
return errors.New("items[" + strconv.Itoa(index) + "].quantity must be greater than 0"), constants.MalformedFieldErrorCode return errors.New("items[" + string(rune(index)) + "].unit_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.Amount < 0 { if item.Amount < 0 {
return errors.New("items[" + strconv.Itoa(index) + "].amount must be greater than or equal to 0"), constants.MalformedFieldErrorCode return errors.New("items[" + string(rune(index)) + "].amount must be greater than or equal to 0"), constants.MalformedFieldErrorCode
} }
return nil, "" return nil, ""
} }
func (v *PurchaseOrderValidatorImpl) validateUpdatePurchaseOrderItem(item *contract.UpdatePurchaseOrderItemRequest, index int) (error, string) { func (v *PurchaseOrderValidatorImpl) validateUpdatePurchaseOrderItem(item *contract.UpdatePurchaseOrderItemRequest, index int) (error, string) {
if item.PurchaseCategoryID == nil || *item.PurchaseCategoryID == uuid.Nil {
return errors.New("items[" + strconv.Itoa(index) + "].purchase_category_id is required"), constants.MissingFieldErrorCode
}
if item.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[" + string(rune(index)) + "].quantity must be greater than 0"), constants.MalformedFieldErrorCode
} }
if item.Amount != nil && *item.Amount < 0 { if item.Amount != nil && *item.Amount < 0 {
return errors.New("items[" + strconv.Itoa(index) + "].amount must be greater than or equal to 0"), constants.MalformedFieldErrorCode return errors.New("items[" + string(rune(index)) + "].amount must be greater than or equal to 0"), constants.MalformedFieldErrorCode
} }
return nil, "" return nil, ""
@@ -11,21 +11,16 @@ 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: &ingredientID, IngredientID: 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', 'expense')), type VARCHAR(20) NOT NULL CHECK (type IN ('raw_material', 'non_inventory')),
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', 'expense')), type VARCHAR(20) NOT NULL CHECK (type IN ('raw_material', 'non_inventory')),
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
('bahan_baku', 'Bahan Baku', 'raw_material', 1), ('hpp', 'HPP', 'raw_material', 1),
('biaya_lain_lain', 'Biaya Lain-lain', 'expense', 2) ('biaya_lain_lain', 'Biaya Lain-lain', 'non_inventory', 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,14 +47,18 @@ 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
('biaya_lain_lain', 'biaya_atk_perlengkapan', 'ATK & Perlengkapan', 'expense', 1), ('hpp', 'hpp_bakso_mie_ayam', 'Bakso & Mie Ayam', 'raw_material', 1),
('biaya_lain_lain', 'biaya_makan_karyawan', 'Makan Karyawan', 'expense', 2), ('hpp', 'hpp_nusantara', 'Nusantara', 'raw_material', 2),
('biaya_lain_lain', 'biaya_bensin_parkir', 'Bensin & Parkir', 'expense', 3), ('hpp', 'hpp_ramen', 'Ramen', 'raw_material', 3),
('biaya_lain_lain', 'biaya_kebersihan_keamanan', 'Kebersihan & Keamanan', 'expense', 4), ('hpp', 'hpp_minuman_kopi', 'Minuman/Kopi', 'raw_material', 4),
('biaya_lain_lain', 'biaya_gaji_dw', 'Gaji DW', 'expense', 5), ('biaya_lain_lain', 'biaya_atk_perlengkapan', 'ATK & Perlengkapan', 'non_inventory', 1),
('biaya_lain_lain', 'biaya_gaji_staff', 'Gaji Staff', 'expense', 6), ('biaya_lain_lain', 'biaya_makan_karyawan', 'Makan Karyawan', 'non_inventory', 2),
('biaya_lain_lain', 'biaya_internet_server', 'Internet & Server', 'expense', 7), ('biaya_lain_lain', 'biaya_bensin_parkir', 'Bensin & Parkir', 'non_inventory', 3),
('biaya_lain_lain', 'biaya_air_listrik', 'Air & Listrik', 'expense', 8), ('biaya_lain_lain', 'biaya_kebersihan_keamanan', 'Kebersihan & Keamanan', 'non_inventory', 4),
('biaya_lain_lain', 'biaya_promosi', 'Promosi', 'expense', 9) ('biaya_lain_lain', 'biaya_gaji_dw', 'Gaji DW', 'non_inventory', 5),
('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;
@@ -1,5 +0,0 @@
DROP INDEX IF EXISTS idx_inventory_movements_purchase_order_item_id;
ALTER TABLE inventory_movements DROP COLUMN IF EXISTS purchase_order_item_id;
DROP INDEX IF EXISTS idx_purchase_order_items_purchase_category_id;
ALTER TABLE purchase_order_items DROP COLUMN IF EXISTS purchase_category_id;
@@ -1,11 +0,0 @@
ALTER TABLE purchase_order_items
ADD COLUMN IF NOT EXISTS purchase_category_id UUID REFERENCES purchase_categories(id) ON DELETE RESTRICT;
CREATE INDEX IF NOT EXISTS idx_purchase_order_items_purchase_category_id
ON purchase_order_items(purchase_category_id);
ALTER TABLE inventory_movements
ADD COLUMN IF NOT EXISTS purchase_order_item_id UUID REFERENCES purchase_order_items(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS idx_inventory_movements_purchase_order_item_id
ON inventory_movements(purchase_order_item_id);
@@ -1,2 +0,0 @@
DROP INDEX IF EXISTS idx_expense_items_purchase_category_id;
ALTER TABLE expense_items DROP COLUMN IF EXISTS purchase_category_id;
@@ -1,5 +0,0 @@
ALTER TABLE expense_items
ADD COLUMN IF NOT EXISTS purchase_category_id UUID REFERENCES purchase_categories(id) ON DELETE RESTRICT;
CREATE INDEX IF NOT EXISTS idx_expense_items_purchase_category_id
ON expense_items(purchase_category_id);
@@ -1,32 +0,0 @@
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'));
@@ -1,36 +0,0 @@
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;