diff --git a/internal/contract/purchase_order_contract.go b/internal/contract/purchase_order_contract.go index 70e722c..5ab9024 100644 --- a/internal/contract/purchase_order_contract.go +++ b/internal/contract/purchase_order_contract.go @@ -40,12 +40,12 @@ type UpdatePurchaseOrderRequest struct { } type UpdatePurchaseOrderItemRequest struct { - ID *uuid.UUID `json:"id,omitempty"` // For existing items - IngredientID *uuid.UUID `json:"ingredient_id,omitempty" validate:"omitempty"` - PurchaseCategoryID *uuid.UUID `json:"purchase_category_id,omitempty" validate:"omitempty"` + ID *uuid.UUID `json:"id,omitempty"` // Ignored. Supplying items replaces all existing PO items. + IngredientID *uuid.UUID `json:"ingredient_id" validate:"required"` + PurchaseCategoryID *uuid.UUID `json:"purchase_category_id" validate:"required"` Description *string `json:"description,omitempty" validate:"omitempty"` - Quantity *float64 `json:"quantity,omitempty" validate:"omitempty,gt=0"` - UnitID *uuid.UUID `json:"unit_id,omitempty" validate:"omitempty"` + Quantity *float64 `json:"quantity" validate:"required,gt=0"` + UnitID *uuid.UUID `json:"unit_id" validate:"required"` Amount *float64 `json:"amount,omitempty" validate:"omitempty,gte=0"` } diff --git a/internal/models/purchase_order.go b/internal/models/purchase_order.go index 1afa23f..452536c 100644 --- a/internal/models/purchase_order.go +++ b/internal/models/purchase_order.go @@ -117,7 +117,7 @@ type UpdatePurchaseOrderRequest struct { } type UpdatePurchaseOrderItemRequest struct { - ID *uuid.UUID `json:"id,omitempty"` // For existing items + ID *uuid.UUID `json:"id,omitempty"` // Ignored. Supplying items replaces all existing PO items. IngredientID *uuid.UUID `json:"ingredient_id,omitempty"` PurchaseCategoryID *uuid.UUID `json:"purchase_category_id,omitempty"` Description *string `json:"description,omitempty"` diff --git a/internal/processor/analytics_processor.go b/internal/processor/analytics_processor.go index 928d9eb..e4894e3 100644 --- a/internal/processor/analytics_processor.go +++ b/internal/processor/analytics_processor.go @@ -842,10 +842,10 @@ func exclusiveSummarySalaryBreakdown(transactions []entities.ExclusiveSummaryDai classification := strings.ToLower(transaction.CategoryCode + " " + transaction.CategoryName + " " + transaction.Description) switch { - case strings.Contains(classification, "staff") || strings.Contains(classification, "kary") || strings.Contains(classification, "karyawan"): - salaryStaff += transaction.Amount case strings.Contains(classification, "dw"): salaryDW += transaction.Amount + case strings.Contains(classification, "staff") || strings.Contains(classification, "kary") || strings.Contains(classification, "karyawan"): + salaryStaff += transaction.Amount default: salaryOther += transaction.Amount } diff --git a/internal/processor/analytics_processor_test.go b/internal/processor/analytics_processor_test.go index 4cdd910..11373de 100644 --- a/internal/processor/analytics_processor_test.go +++ b/internal/processor/analytics_processor_test.go @@ -293,7 +293,7 @@ func TestAnalyticsProcessorGetExclusiveSummaryPeriodCalculatesSummaryAndReimburs }, DailyTransactions: []entities.ExclusiveSummaryDailyTransaction{ {Date: now, CategoryCode: "biaya_gaji", CategoryName: "Gaji", Description: "gaji kary", Amount: 48203333, Source: "expense"}, - {Date: now, CategoryCode: "biaya_gaji", CategoryName: "Gaji", Description: "DW", Amount: 3555000, Source: "expense"}, + {Date: now, CategoryCode: "biaya_gaji_dw", CategoryName: "Gaji DW", Description: "gaji karyawan", Amount: 3555000, Source: "expense"}, }, }, }, expenseRepositoryStub{}) diff --git a/internal/repository/analytics_repository.go b/internal/repository/analytics_repository.go index 8f429d2..6b2be98 100644 --- a/internal/repository/analytics_repository.go +++ b/internal/repository/analytics_repository.go @@ -173,7 +173,7 @@ func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx contex Joins("JOIN units u ON poi.unit_id = u.id"). Where("po.organization_id = ?", organizationID). Where("pc.type = ?", entities.PurchaseCategoryTypeRawMaterial). - Where("po.status != ?", "cancelled"). + Where("po.status = ?", "received"). Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo) summaryQuery = r.applyPurchaseOrderItemOutletFilter(summaryQuery, outletID) @@ -214,7 +214,7 @@ func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx contex Joins("JOIN units u ON poi.unit_id = u.id"). Where("po.organization_id = ?", organizationID). Where("pc.type = ?", entities.PurchaseCategoryTypeRawMaterial). - Where("po.status != ?", "cancelled"). + Where("po.status = ?", "received"). Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo). Group(dateFormat). Order(dateFormat) @@ -245,7 +245,7 @@ func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx contex Joins("LEFT JOIN units u ON poi.unit_id = u.id"). Where("po.organization_id = ?", organizationID). Where("pc.type = ?", entities.PurchaseCategoryTypeRawMaterial). - Where("po.status != ?", "cancelled"). + Where("po.status = ?", "received"). Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo). Group("i.id, i.name"). Order("total_cost DESC") @@ -273,7 +273,7 @@ func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx contex Joins("JOIN units u ON poi.unit_id = u.id"). Where("po.organization_id = ?", organizationID). Where("pc.type = ?", entities.PurchaseCategoryTypeRawMaterial). - Where("po.status != ?", "cancelled"). + Where("po.status = ?", "received"). Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo). Group("v.id, v.name"). Order("total_cost DESC") @@ -296,7 +296,15 @@ func (r *AnalyticsRepositoryImpl) applyPurchaseOrderItemOutletFilter(query *gorm if outletID == nil { return query } - return query.Where("(i.outlet_id = ? OR u.outlet_id = ?)", *outletID, *outletID) + return query.Where(` + EXISTS ( + SELECT 1 + FROM inventory_movements im + WHERE im.purchase_order_item_id = poi.id + AND im.movement_type = ? + AND im.outlet_id = ? + ) + `, entities.InventoryMovementTypePurchase, *outletID) } func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, limit int) ([]*entities.ProductAnalytics, error) { @@ -307,6 +315,7 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organ Select(` p.id as product_id, p.name as product_name, + p.price as product_price, c.id as category_id, c.name as category_name, c.order as category_order, @@ -365,7 +374,7 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organ query = r.resolveOutletID(query, outletID, "o.outlet_id") 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"). Limit(limit). Scan(&results).Error @@ -854,8 +863,14 @@ func (r *AnalyticsRepositoryImpl) exclusiveSummaryTransactionUnionQuery(organiza } if outletID != nil { - poOutletFilter = "AND (i.outlet_id = ? OR u.outlet_id = ?)" - args = append(args, *outletID, *outletID) + poOutletFilter = `AND EXISTS ( + SELECT 1 + FROM inventory_movements im + WHERE im.purchase_order_item_id = poi.id + AND im.movement_type = 'purchase' + AND im.outlet_id = ? + )` + args = append(args, *outletID) } args = append(args, diff --git a/internal/validator/purchase_order_validator_test.go b/internal/validator/purchase_order_validator_test.go index c07f9a7..34c39f2 100644 --- a/internal/validator/purchase_order_validator_test.go +++ b/internal/validator/purchase_order_validator_test.go @@ -73,3 +73,31 @@ func TestPurchaseOrderValidatorCreateRejectsDueDateBeforeTransactionDate(t *test require.Equal(t, constants.MalformedFieldErrorCode, code) require.Contains(t, err.Error(), "due_date must be after transaction_date") } + +func TestPurchaseOrderValidatorUpdateItemsRequireFullReplacementFields(t *testing.T) { + validator := NewPurchaseOrderValidator() + req := &contract.UpdatePurchaseOrderRequest{ + Items: []contract.UpdatePurchaseOrderItemRequest{ + { + PurchaseCategoryID: ptrUUID(uuid.New()), + Quantity: ptrFloat64(1), + UnitID: ptrUUID(uuid.New()), + Amount: ptrFloat64(1000), + }, + }, + } + + err, code := validator.ValidateUpdatePurchaseOrderRequest(req) + + require.Error(t, err) + require.Equal(t, constants.MissingFieldErrorCode, code) + require.Contains(t, err.Error(), "ingredient_id is required") +} + +func ptrUUID(id uuid.UUID) *uuid.UUID { + return &id +} + +func ptrFloat64(value float64) *float64 { + return &value +} diff --git a/migrations/000081_enforce_raw_material_purchase_order_items.down.sql b/migrations/000081_enforce_raw_material_purchase_order_items.down.sql index e413239..802b92d 100644 --- a/migrations/000081_enforce_raw_material_purchase_order_items.down.sql +++ b/migrations/000081_enforce_raw_material_purchase_order_items.down.sql @@ -2,6 +2,7 @@ DROP TRIGGER IF EXISTS trigger_validate_purchase_order_item_raw_material ON purc DROP FUNCTION IF EXISTS validate_purchase_order_item_raw_material(); ALTER TABLE purchase_order_items + ALTER COLUMN purchase_category_id DROP NOT NULL, ALTER COLUMN ingredient_id DROP NOT NULL, ALTER COLUMN quantity DROP NOT NULL, ALTER COLUMN unit_id DROP NOT NULL; diff --git a/migrations/000081_enforce_raw_material_purchase_order_items.up.sql b/migrations/000081_enforce_raw_material_purchase_order_items.up.sql index b3fcae7..d394149 100644 --- a/migrations/000081_enforce_raw_material_purchase_order_items.up.sql +++ b/migrations/000081_enforce_raw_material_purchase_order_items.up.sql @@ -1,10 +1,21 @@ +UPDATE purchase_order_items poi +SET purchase_category_id = pc.id +FROM purchase_orders po +JOIN purchase_categories pc ON pc.organization_id = po.organization_id + AND pc.code = 'bahan_baku' + AND pc.type = 'raw_material' +WHERE poi.purchase_order_id = po.id + AND poi.purchase_category_id IS NULL; + DO $$ BEGIN IF EXISTS ( SELECT 1 FROM purchase_order_items poi - JOIN purchase_categories pc ON pc.id = poi.purchase_category_id - WHERE pc.type <> 'raw_material' + LEFT JOIN purchase_categories pc ON pc.id = poi.purchase_category_id + WHERE poi.purchase_category_id IS NULL + OR pc.id IS NULL + OR pc.type <> 'raw_material' OR poi.ingredient_id IS NULL OR poi.quantity IS NULL OR poi.unit_id IS NULL @@ -14,6 +25,7 @@ BEGIN END $$; ALTER TABLE purchase_order_items + ALTER COLUMN purchase_category_id SET NOT NULL, ALTER COLUMN ingredient_id SET NOT NULL, ALTER COLUMN quantity SET NOT NULL, ALTER COLUMN unit_id SET NOT NULL; diff --git a/migrations/000082_enforce_expense_items_expense_categories.up.sql b/migrations/000082_enforce_expense_items_expense_categories.up.sql index 73f59ae..e218504 100644 --- a/migrations/000082_enforce_expense_items_expense_categories.up.sql +++ b/migrations/000082_enforce_expense_items_expense_categories.up.sql @@ -1,3 +1,20 @@ +UPDATE expense_items ei +SET purchase_category_id = pc.id +FROM expenses e +JOIN purchase_categories pc ON pc.organization_id = e.organization_id + AND pc.code = 'biaya_lain_lain' + AND pc.type = 'expense' +WHERE ei.expense_id = e.id + AND ( + ei.purchase_category_id IS NULL + OR NOT EXISTS ( + SELECT 1 + FROM purchase_categories current_pc + WHERE current_pc.id = ei.purchase_category_id + AND current_pc.type = 'expense' + ) + ); + DO $$ BEGIN IF EXISTS (