From f1f7edb9abd5544360eef282332c3c94ee929846 Mon Sep 17 00:00:00 2001 From: MacBook Air M1 Date: Thu, 8 Jan 2026 11:50:34 +0700 Subject: [PATCH 01/19] Adjust limit for get all employee --- .../master/employees/validations/employees.validation.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/modules/master/employees/validations/employees.validation.go b/internal/modules/master/employees/validations/employees.validation.go index 2e2cc879..83608071 100644 --- a/internal/modules/master/employees/validations/employees.validation.go +++ b/internal/modules/master/employees/validations/employees.validation.go @@ -14,7 +14,7 @@ type Update struct { type Query struct { Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` - Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=500,gt=0"` Search string `query:"search" validate:"omitempty,max=50"` KandangId *uint `query:"kandang_id" validate:"omitempty"` IsActive *bool `query:"is_active" validate:"omitempty"` From 6c08fe23ca472a891e6d79fb8e6a5abdb5232c45 Mon Sep 17 00:00:00 2001 From: ragilap Date: Thu, 8 Jan 2026 12:40:36 +0700 Subject: [PATCH 02/19] feat(BE-281): add types uniformity --- .../services/uniformity.calculate.go | 42 ++++++++++++++++++- .../uniformities/types/uniformity.types.go | 4 +- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/internal/modules/production/uniformities/services/uniformity.calculate.go b/internal/modules/production/uniformities/services/uniformity.calculate.go index 1b99afa7..24eb107a 100644 --- a/internal/modules/production/uniformities/services/uniformity.calculate.go +++ b/internal/modules/production/uniformities/services/uniformity.calculate.go @@ -346,17 +346,31 @@ func buildChartWeekSummary(weights []float64) utypes.UniformityChartWeek { minBucket := bucket maxBucket := bucket + bucketSize - 1 count := 0.0 + bucketWeights := make([]float64, 0) + idealWeights := make([]float64, 0) + outsideWeights := make([]float64, 0) for _, w := range weights { if w >= minBucket && w < minBucket+bucketSize { count++ + bucketWeights = append(bucketWeights, w) + if w >= idealMin && w <= idealMax { + idealWeights = append(idealWeights, w) + } else { + outsideWeights = append(outsideWeights, w) + } } } + idealRangeLabel := rangeFromValues(idealWeights) + outsideRangeLabel := rangeFromValues(outsideWeights) + isIdealRange := idealRangeLabel != "" distribution = append(distribution, utypes.UniformityChartRange{ Range: fmt.Sprintf("%d-%d", int(minBucket), int(maxBucket)), MinWeight: minBucket, MaxWeight: maxBucket, BirdCount: count, - IsIdealRange: minBucket >= idealMin && maxBucket <= idealMax, + IsIdealRange: isIdealRange, + IdealRange: idealRangeLabel, + OutsideRange: outsideRangeLabel, }) } @@ -391,3 +405,29 @@ func roundToPrecision(value float64, precision int) float64 { } return math.Floor(scaled) / scale } + +func rangeFromValues(values []float64) string { + if len(values) == 0 { + return "" + } + minValue := values[0] + maxValue := values[0] + for _, v := range values[1:] { + if v < minValue { + minValue = v + } + if v > maxValue { + maxValue = v + } + } + return formatRange(minValue, maxValue) +} + +func formatRange(minValue, maxValue float64) string { + minInt := int(math.Round(minValue)) + maxInt := int(math.Round(maxValue)) + if minInt == maxInt { + return fmt.Sprintf("%d", minInt) + } + return fmt.Sprintf("%d-%d", minInt, maxInt) +} \ No newline at end of file diff --git a/internal/modules/production/uniformities/types/uniformity.types.go b/internal/modules/production/uniformities/types/uniformity.types.go index 17fcf305..877795f3 100644 --- a/internal/modules/production/uniformities/types/uniformity.types.go +++ b/internal/modules/production/uniformities/types/uniformity.types.go @@ -29,6 +29,8 @@ type UniformityChartRange struct { MaxWeight float64 `json:"max_weight"` BirdCount float64 `json:"bird_count"` IsIdealRange bool `json:"is_ideal_range"` + IdealRange string `json:"ideal_range,omitempty"` + OutsideRange string `json:"outside_range,omitempty"` } type UniformityChartIdealRange struct { @@ -82,4 +84,4 @@ type UniformityChartGauge struct { type UniformityChartData struct { BarChart UniformityChartBar `json:"bar_chart"` GaugeChart UniformityChartGauge `json:"gauge_chart"` -} +} \ No newline at end of file From f079bee92a979a524598ee74a8d6e403707568ec Mon Sep 17 00:00:00 2001 From: MacBook Air M1 Date: Thu, 8 Jan 2026 13:37:53 +0700 Subject: [PATCH 03/19] adjust get all phase activity --- .../controllers/phase-activity.controller.go | 10 +------- .../services/phase-activity.service.go | 23 +++++++++++++++++-- .../validations/phase-activity.validation.go | 8 +++---- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/internal/modules/master/phase-activities/controllers/phase-activity.controller.go b/internal/modules/master/phase-activities/controllers/phase-activity.controller.go index 455ff1e4..3cbc68f2 100644 --- a/internal/modules/master/phase-activities/controllers/phase-activity.controller.go +++ b/internal/modules/master/phase-activities/controllers/phase-activity.controller.go @@ -28,20 +28,12 @@ func (u *PhaseActivityController) GetAll(c *fiber.Ctx) error { Limit: c.QueryInt("limit", 10), Search: c.Query("search", ""), } + query.PhaseIDs = c.Query("phase_ids", "") if query.Page < 1 || query.Limit < 1 { return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") } - if phaseParam := c.Query("phase_id", ""); phaseParam != "" { - id, err := strconv.Atoi(phaseParam) - if err != nil || id <= 0 { - return fiber.NewError(fiber.StatusBadRequest, "invalid phase_id") - } - temp := uint(id) - query.PhaseId = &temp - } - result, totalResults, err := u.PhaseActivityService.GetAll(c, query) if err != nil { return err diff --git a/internal/modules/master/phase-activities/services/phase-activity.service.go b/internal/modules/master/phase-activities/services/phase-activity.service.go index 3cedb4fc..24b8272e 100644 --- a/internal/modules/master/phase-activities/services/phase-activity.service.go +++ b/internal/modules/master/phase-activities/services/phase-activity.service.go @@ -2,6 +2,7 @@ package service import ( "errors" + "strconv" "strings" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" @@ -57,8 +58,11 @@ func (s phaseActivityService) GetAll(c *fiber.Ctx, params *validation.Query) ([] if params.Search != "" { db = db.Where("name LIKE ?", "%"+params.Search+"%") } - if params.PhaseId != nil { - db = db.Where("phase_id = ?", *params.PhaseId) + if params.PhaseIDs != "" { + ids := parseIDs(params.PhaseIDs) + if len(ids) > 0 { + db = db.Where("phase_id IN ?", ids) + } } return db.Order("created_at DESC").Order("updated_at DESC") }) @@ -166,3 +170,18 @@ func (s phaseActivityService) DeleteOne(c *fiber.Ctx, id uint) error { } return nil } + +func parseIDs(raw string) []uint { + parts := strings.Split(raw, ",") + results := make([]uint, 0, len(parts)) + for _, part := range parts { + value := strings.TrimSpace(part) + if value == "" { + continue + } + if n, err := strconv.ParseUint(value, 10, 64); err == nil { + results = append(results, uint(n)) + } + } + return results +} diff --git a/internal/modules/master/phase-activities/validations/phase-activity.validation.go b/internal/modules/master/phase-activities/validations/phase-activity.validation.go index a2ab8e1b..54186315 100644 --- a/internal/modules/master/phase-activities/validations/phase-activity.validation.go +++ b/internal/modules/master/phase-activities/validations/phase-activity.validation.go @@ -14,8 +14,8 @@ type Update struct { } type Query struct { - Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` - Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` - Search string `query:"search" validate:"omitempty,max=50"` - PhaseId *uint `query:"phase_id" validate:"omitempty"` + Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` + Search string `query:"search" validate:"omitempty,max=50"` + PhaseIDs string `query:"phase_ids" validate:"omitempty"` } From c729067ab5d6301cb556f90b51d78589d1a7f34d Mon Sep 17 00:00:00 2001 From: MacBook Air M1 Date: Thu, 8 Jan 2026 15:08:40 +0700 Subject: [PATCH 04/19] adjust validate pactch daily checklist --- .../daily-checklists/validations/daily-checklist.validation.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/modules/daily-checklists/validations/daily-checklist.validation.go b/internal/modules/daily-checklists/validations/daily-checklist.validation.go index a42d424a..81bb5eff 100644 --- a/internal/modules/daily-checklists/validations/daily-checklist.validation.go +++ b/internal/modules/daily-checklists/validations/daily-checklist.validation.go @@ -9,7 +9,7 @@ type Create struct { type Update struct { Status string `json:"status" validate:"required"` - RejectReason *string `json:"reject_reason" validate:"required"` + RejectReason *string `json:"reject_reason"` } type Query struct { From 2650e919e7f5bbe24cbe79b891d910a80875c274 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 8 Jan 2026 15:14:06 +0700 Subject: [PATCH 05/19] feat(BE): implement expense tracking for stock transfers and enhance related services --- ...44_create_daily_checklists_tables.down.sql | 4 +- ...expense_to_stock_transfer_details.down.sql | 4 + ...d_expense_to_stock_transfer_details.up.sql | 10 + internal/entities/expense_nonstock.go | 18 +- internal/entities/stock_transfer_detail.go | 21 +- .../controllers/closing.controller.go | 2 +- .../closings/services/closing.service.go | 2 +- .../inventory/transfers/dto/transfer.dto.go | 48 +- .../modules/inventory/transfers/module.go | 36 +- .../transfers/services/transfer.service.go | 60 ++- .../services/transfer_expense_bridge.go | 452 ++++++++++++++++++ .../purchases/services/expense_bridge.go | 2 +- 12 files changed, 622 insertions(+), 37 deletions(-) create mode 100644 internal/database/migrations/20260107080257_add_expense_to_stock_transfer_details.down.sql create mode 100644 internal/database/migrations/20260107080257_add_expense_to_stock_transfer_details.up.sql create mode 100644 internal/modules/inventory/transfers/services/transfer_expense_bridge.go diff --git a/internal/database/migrations/20260105131644_create_daily_checklists_tables.down.sql b/internal/database/migrations/20260105131644_create_daily_checklists_tables.down.sql index 7be30be1..20182fe3 100644 --- a/internal/database/migrations/20260105131644_create_daily_checklists_tables.down.sql +++ b/internal/database/migrations/20260105131644_create_daily_checklists_tables.down.sql @@ -1,6 +1,8 @@ -DROP TABLE IF EXISTS daily_checklist_tasks; +-- Drop tables in correct order (child tables before parent tables) +DROP TABLE IF EXISTS daily_checklist_activity_task_assignments; -- Child table with FK to daily_checklist_activity_tasks DROP TABLE IF EXISTS daily_checklist_activity_task_assignees; DROP TABLE IF EXISTS daily_checklist_activity_tasks; +DROP TABLE IF EXISTS daily_checklist_tasks; DROP TABLE IF EXISTS daily_checklist_phases; DROP TABLE IF EXISTS daily_checklists; DROP TABLE IF EXISTS checklists; diff --git a/internal/database/migrations/20260107080257_add_expense_to_stock_transfer_details.down.sql b/internal/database/migrations/20260107080257_add_expense_to_stock_transfer_details.down.sql new file mode 100644 index 00000000..a8b3dfaa --- /dev/null +++ b/internal/database/migrations/20260107080257_add_expense_to_stock_transfer_details.down.sql @@ -0,0 +1,4 @@ +-- Remove expense_nonstock_id from stock_transfer_details +ALTER TABLE stock_transfer_details DROP CONSTRAINT IF EXISTS fk_stock_transfer_details_expense_nonstock; +ALTER TABLE stock_transfer_details DROP COLUMN IF EXISTS expense_nonstock_id; +DROP INDEX IF EXISTS idx_stock_transfer_details_expense_nonstock_id; diff --git a/internal/database/migrations/20260107080257_add_expense_to_stock_transfer_details.up.sql b/internal/database/migrations/20260107080257_add_expense_to_stock_transfer_details.up.sql new file mode 100644 index 00000000..25d6b199 --- /dev/null +++ b/internal/database/migrations/20260107080257_add_expense_to_stock_transfer_details.up.sql @@ -0,0 +1,10 @@ +-- Add expense_nonstock_id to stock_transfer_details +-- This allows tracking expedition/transport costs for stock transfers (same as purchase) + +ALTER TABLE stock_transfer_details +ADD COLUMN expense_nonstock_id BIGINT, +ADD CONSTRAINT fk_stock_transfer_details_expense_nonstock +FOREIGN KEY (expense_nonstock_id) REFERENCES expense_nonstocks(id) ON DELETE SET NULL; + +-- Create index for better query performance +CREATE INDEX idx_stock_transfer_details_expense_nonstock_id ON stock_transfer_details(expense_nonstock_id); diff --git a/internal/entities/expense_nonstock.go b/internal/entities/expense_nonstock.go index ccd4194c..946b7a08 100644 --- a/internal/entities/expense_nonstock.go +++ b/internal/entities/expense_nonstock.go @@ -5,15 +5,15 @@ import ( ) type ExpenseNonstock struct { - Id uint64 `gorm:"primaryKey;autoIncrement"` - ExpenseId *uint64 `gorm:""` - ProjectFlockKandangId *uint64 `gorm:""` - KandangId *uint64 `gorm:""` - NonstockId *uint64 `gorm:""` - Qty float64 `gorm:"type:numeric(15,3);not null"` - Price float64 `gorm:"type:numeric(15,3);not null;column:price"` - Notes string `gorm:"type:text;column:notes"` - CreatedAt time.Time `gorm:"type:timestamptz;default:CURRENT_TIMESTAMP"` + Id uint64 `gorm:"primaryKey;autoIncrement"` + ExpenseId *uint64 `gorm:""` + ProjectFlockKandangId *uint64 `gorm:""` + KandangId *uint64 `gorm:""` + NonstockId *uint64 `gorm:""` + Qty float64 `gorm:"type:numeric(15,3);not null"` + Price float64 `gorm:"type:numeric(15,3);not null;column:price"` + Notes string `gorm:"type:text;column:notes"` + CreatedAt time.Time `gorm:"type:timestamptz;default:CURRENT_TIMESTAMP"` Expense *Expense `gorm:"foreignKey:ExpenseId;references:Id"` ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"` diff --git a/internal/entities/stock_transfer_detail.go b/internal/entities/stock_transfer_detail.go index 9ab27824..dd24aadb 100644 --- a/internal/entities/stock_transfer_detail.go +++ b/internal/entities/stock_transfer_detail.go @@ -8,27 +8,22 @@ type StockTransferDetail struct { StockTransferId uint64 ProductId uint64 - // === FIFO FIELDS - SOURCE WAREHOUSE (Usable) === - // Tracking stock yang DIAMBIL dari source warehouse SourceProductWarehouseID *uint64 `gorm:"column:source_product_warehouse_id"` UsageQty float64 `gorm:"column:usage_qty;default:0"` // Actual yang berhasil diambil PendingQty float64 `gorm:"column:pending_qty;default:0"` // Yang pending (nunggu stock) - - // === FIFO FIELDS - DESTINATION WAREHOUSE (Stockable) === - // Tracking stock yang DITAMBAHKAN ke destination warehouse - DestProductWarehouseID *uint64 `gorm:"column:dest_product_warehouse_id"` - TotalQty float64 `gorm:"column:total_qty;default:0"` // Total lot yang tersedia - TotalUsed float64 `gorm:"column:total_used;default:0"` // Yang sudah dipakai dari lot ini - - // === METADATA === - CreatedAt time.Time - UpdatedAt time.Time - DeletedAt *time.Time `gorm:"index"` + DestProductWarehouseID *uint64 `gorm:"column:dest_product_warehouse_id"` + TotalQty float64 `gorm:"column:total_qty;default:0"` // Total lot yang tersedia + TotalUsed float64 `gorm:"column:total_used;default:0"` // Yang sudah dipakai dari lot ini + ExpenseNonstockId *uint64 `gorm:"column:expense_nonstock_id"` + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt *time.Time `gorm:"index"` // === RELATIONS === StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"` Product *Product `gorm:"foreignKey:ProductId"` SourceProductWarehouse *ProductWarehouse `gorm:"foreignKey:SourceProductWarehouseID"` DestProductWarehouse *ProductWarehouse `gorm:"foreignKey:DestProductWarehouseID"` + ExpenseNonstock *ExpenseNonstock `gorm:"foreignKey:ExpenseNonstockId;references:Id"` DeliveryItems []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDetailId"` } diff --git a/internal/modules/closings/controllers/closing.controller.go b/internal/modules/closings/controllers/closing.controller.go index c4580efb..6ab2d398 100644 --- a/internal/modules/closings/controllers/closing.controller.go +++ b/internal/modules/closings/controllers/closing.controller.go @@ -247,7 +247,7 @@ func (u *ClosingController) GetSapronakByKandang(c *fiber.Ctx) error { } func (u *ClosingController) GetClosingKeuangan(c *fiber.Ctx) error { - param := c.Params("project_flock_id") + param := c.Params("projectFlockId") projectFlockID, err := strconv.Atoi(param) if err != nil { diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index ddf52b49..9659601e 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -151,7 +151,7 @@ func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint) ([]entit return nil, err } if len(realisasi) == 0 { - return nil, fiber.NewError(fiber.StatusNotFound, "Penjualan realisasi not found") + return []entity.MarketingDeliveryProduct{}, nil } return realisasi, nil } diff --git a/internal/modules/inventory/transfers/dto/transfer.dto.go b/internal/modules/inventory/transfers/dto/transfer.dto.go index 652b2a70..8fa4d158 100644 --- a/internal/modules/inventory/transfers/dto/transfer.dto.go +++ b/internal/modules/inventory/transfers/dto/transfer.dto.go @@ -71,9 +71,11 @@ type TransferDetailDTO struct { } type TransferDetailItemDTO struct { - Id uint64 `json:"id"` - Product ProductSimpleDTO `json:"product"` - Quantity float64 `json:"quantity"` + Id uint64 `json:"id"` + Product ProductSimpleDTO `json:"product"` + Quantity float64 `json:"quantity"` + TransportPerItem *float64 `json:"transport_per_item,omitempty"` // Biaya ekspedisi per item + ExpeditionVendor *SupplierSimpleDTO `json:"expedition_vendor,omitempty"` // Vendor ekspedisi } type TransferDeliveryDTO struct { @@ -153,14 +155,30 @@ func ToTransferListDTO(e entity.StockTransfer) TransferListDTO { var details []TransferDetailItemDTO for _, d := range e.Details { - details = append(details, TransferDetailItemDTO{ + detailDTO := TransferDetailItemDTO{ Id: d.Id, Product: ProductSimpleDTO{ Id: d.Product.Id, Name: d.Product.Name, }, Quantity: d.UsageQty + d.PendingQty, // Total actual quantity allocated - }) + } + + + if d.ExpenseNonstock != nil { + priceCopy := d.ExpenseNonstock.Price + detailDTO.TransportPerItem = &priceCopy + + if d.ExpenseNonstock.Expense != nil && d.ExpenseNonstock.Expense.Supplier != nil && d.ExpenseNonstock.Expense.Supplier.Id != 0 { + exp := d.ExpenseNonstock.Expense + detailDTO.ExpeditionVendor = &SupplierSimpleDTO{ + Id: exp.Supplier.Id, + Name: exp.Supplier.Name, + } + } + } + + details = append(details, detailDTO) } var deliveries []TransferDeliveryDTO @@ -223,14 +241,30 @@ func ToTransferListDTOs(e []entity.StockTransfer) []TransferListDTO { func ToTransferDetailDTO(e entity.StockTransfer) TransferDetailDTO { var details []TransferDetailItemDTO for _, d := range e.Details { - details = append(details, TransferDetailItemDTO{ + detailDTO := TransferDetailItemDTO{ Id: d.Id, Product: ProductSimpleDTO{ Id: d.Product.Id, Name: d.Product.Name, }, Quantity: d.UsageQty + d.PendingQty, // Total actual quantity allocated - }) + } + + + if d.ExpenseNonstock != nil { + priceCopy := d.ExpenseNonstock.Price + detailDTO.TransportPerItem = &priceCopy + + if d.ExpenseNonstock.Expense != nil && d.ExpenseNonstock.Expense.Supplier != nil && d.ExpenseNonstock.Expense.Supplier.Id != 0 { + exp := d.ExpenseNonstock.Expense + detailDTO.ExpeditionVendor = &SupplierSimpleDTO{ + Id: exp.Supplier.Id, + Name: exp.Supplier.Name, + } + } + } + + details = append(details, detailDTO) } var deliveries []TransferDeliveryDTO diff --git a/internal/modules/inventory/transfers/module.go b/internal/modules/inventory/transfers/module.go index bbb3c4aa..fde5e55a 100644 --- a/internal/modules/inventory/transfers/module.go +++ b/internal/modules/inventory/transfers/module.go @@ -2,6 +2,7 @@ package transfers import ( "context" + "fmt" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" @@ -9,9 +10,13 @@ import ( commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" + expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" + expenseService "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services" rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories" sTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services" + rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" + rNonstock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/repositories" rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories" rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" @@ -35,15 +40,44 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate userRepo := rUser.NewUserRepository(db) warehouseRepo := rWarehouse.NewWarehouseRepository(db) projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db) + kandangRepo := rKandang.NewKandangRepository(db) + nonstockRepo := rNonstock.NewNonstockRepository(db) documentRepo := commonRepo.NewDocumentRepository(db) stockAllocRepo := commonRepo.NewStockAllocationRepository(db) + expenseRepository := expenseRepo.NewExpenseRepository(db) + expenseRealizationRepo := expenseRepo.NewExpenseRealizationRepository(db) documentSvc, err := commonSvc.NewDocumentServiceFromConfig(context.Background(), documentRepo) if err != nil { panic(err) } + + approvalRepo := commonRepo.NewApprovalRepository(db) + approvalSvc := commonSvc.NewApprovalService(approvalRepo) + if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowExpense, utils.ExpenseApprovalSteps); err != nil { + panic(fmt.Sprintf("failed to register expense approval workflow: %v", err)) + } + + expenseServiceInstance := expenseService.NewExpenseService( + expenseRepository, + supplierRepo, + nonstockRepo, + approvalSvc, + expenseRealizationRepo, + projectFlockKandangRepo, + documentSvc, + validate, + ) + fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log) + expenseBridge := sTransfer.NewTransferExpenseBridge( + db, + stockTransferRepo, + projectFlockKandangRepo, + kandangRepo, + expenseServiceInstance, + ) err = fifoService.RegisterStockable(fifo.StockableConfig{ Key: fifo.StockableKeyStockTransferIn, @@ -77,7 +111,7 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate panic(err) } - transferService := sTransfer.NewTransferService(validate, stockTransferRepo, stockTransferDetailRepo, stockTransferDeliveryRepo, StockTransferDeliveryItemRepo, stockLogsRepo, productWarehouseRepo, supplierRepo, warehouseRepo, projectFlockKandangRepo, documentSvc, fifoService) + transferService := sTransfer.NewTransferService(validate, stockTransferRepo, stockTransferDetailRepo, stockTransferDeliveryRepo, StockTransferDeliveryItemRepo, stockLogsRepo, productWarehouseRepo, supplierRepo, warehouseRepo, projectFlockKandangRepo, documentSvc, fifoService, expenseBridge) userService := sUser.NewUserService(userRepo, validate) TransferRoutes(router, userService, transferService) diff --git a/internal/modules/inventory/transfers/services/transfer.service.go b/internal/modules/inventory/transfers/services/transfer.service.go index afbb4627..86fdc69a 100644 --- a/internal/modules/inventory/transfers/services/transfer.service.go +++ b/internal/modules/inventory/transfers/services/transfer.service.go @@ -46,9 +46,10 @@ type transferService struct { ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository DocumentSvc commonSvc.DocumentService FifoSvc commonSvc.FifoService + ExpenseBridge TransferExpenseBridge } -func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, supplierRepo rSupplier.SupplierRepository, warehouseRepo warehouseRepo.WarehouseRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, documentSvc commonSvc.DocumentService, fifoSvc commonSvc.FifoService) TransferService { +func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, supplierRepo rSupplier.SupplierRepository, warehouseRepo warehouseRepo.WarehouseRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, documentSvc commonSvc.DocumentService, fifoSvc commonSvc.FifoService, expenseBridge TransferExpenseBridge) TransferService { return &transferService{ Log: utils.Log, Validate: validate, @@ -63,6 +64,7 @@ func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTr ProjectFlockKandangRepo: projectFlockKandangRepo, DocumentSvc: documentSvc, FifoSvc: fifoSvc, + ExpenseBridge: expenseBridge, } } @@ -77,6 +79,9 @@ func (s transferService) withRelations(db *gorm.DB) *gorm.DB { Preload("ToWarehouse.Area"). Preload("Details"). Preload("Details.Product"). + Preload("Details.ExpenseNonstock"). + Preload("Details.ExpenseNonstock.Expense"). + Preload("Details.ExpenseNonstock.Expense.Supplier"). Preload("Deliveries.Items"). Preload("Deliveries.Supplier"). Preload("Deliveries.Documents", func(db *gorm.DB) *gorm.DB { @@ -192,7 +197,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques } return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal cek data supplier") } - if supplier.Category != "BOP" { + if supplier.Category != string(utils.SupplierCategoryBOP) { return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d bukan kategori BOP", delivery.SupplierID)) } } @@ -213,6 +218,8 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques CreatedBy: uint64(actorID), } + expensePayloads := make([]TransferExpenseReceivingPayload, 0) + err = s.StockTransferRepo.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { if err := s.StockTransferRepo.WithTx(tx).CreateOne(c.Context(), entityTransfer, nil); err != nil { @@ -362,7 +369,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques return fmt.Errorf("gagal update usage tracking: %w", err) } - // Step 2: Replenish stock to destination warehouse (STOCK_TRANSFER_IN) note := fmt.Sprintf("Transfer #%s", entityTransfer.MovementNumber) replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{ StockableKey: fifo.StockableKeyStockTransferIn, @@ -385,6 +391,32 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques } } + if len(req.Deliveries) > 0 { + for _, delivery := range req.Deliveries { + for _, prod := range delivery.Products { + detail := detailMap[uint64(prod.ProductID)] + if detail == nil { + continue + } + + warehouseID := uint(req.DestinationWarehouseID) + supplierID := uint(delivery.SupplierID) + deliveredDate := transferDate + deliveredQty := prod.ProductQty + + payload := TransferExpenseReceivingPayload{ + TransferDetailID: detail.Id, + ProductID: uint64(prod.ProductID), + WarehouseID: uint64(warehouseID), + SupplierID: uint64(supplierID), + DeliveredQty: deliveredQty, + DeliveredDate: &deliveredDate, + } + expensePayloads = append(expensePayloads, payload) + } + } + } + return nil }) @@ -396,9 +428,31 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques if err != nil { return nil, err } + + if len(expensePayloads) > 0 { + if err := s.notifyExpenseItemsDelivered(c, entityTransfer.Id, expensePayloads); err != nil { + s.Log.Errorf("Failed to sync expense for transfer %d: %+v", entityTransfer.Id, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to sync expense: %v", err)) + } + } + return result, nil } +func (s *transferService) notifyExpenseItemsDelivered(c *fiber.Ctx, transferID uint64, payloads []TransferExpenseReceivingPayload) error { + if s.ExpenseBridge == nil || transferID == 0 || len(payloads) == 0 { + return nil + } + return s.ExpenseBridge.OnItemsDelivered(c, transferID, payloads) +} + +func (s *transferService) notifyExpenseDetailsDeleted(ctx context.Context, transferID uint64, items []entity.StockTransferDetail) error { + if s.ExpenseBridge == nil || transferID == 0 || len(items) == 0 { + return nil + } + return s.ExpenseBridge.OnItemsDeleted(ctx, transferID, items) +} + func (s *transferService) getActiveProjectFlockKandangID(ctx context.Context, warehouseID uint) (uint, error) { warehouse, err := s.WarehouseRepo.GetByID(ctx, warehouseID, nil) if err != nil { diff --git a/internal/modules/inventory/transfers/services/transfer_expense_bridge.go b/internal/modules/inventory/transfers/services/transfer_expense_bridge.go new file mode 100644 index 00000000..b16cfd4f --- /dev/null +++ b/internal/modules/inventory/transfers/services/transfer_expense_bridge.go @@ -0,0 +1,452 @@ +package service + +import ( + "context" + "errors" + "fmt" + "strconv" + "strings" + "time" + + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" + + commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + expenseDto "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/dto" + expenseSvc "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services" + expenseValidation "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/validations" + rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories" + kandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" + projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" + "gitlab.com/mbugroup/lti-api.git/internal/utils" +) + +type TransferExpenseBridge interface { + OnItemsDeleted(ctx context.Context, transferID uint64, items []entity.StockTransferDetail) error + OnItemsDelivered(c *fiber.Ctx, transferID uint64, updates []TransferExpenseReceivingPayload) error +} + +type TransferExpenseReceivingPayload struct { + TransferDetailID uint64 + ProductID uint64 + WarehouseID uint64 + SupplierID uint64 + TransportPerItem *float64 + DeliveredQty float64 + DeliveredDate *time.Time +} + +type groupedTransferItem struct { + detail *entity.StockTransferDetail + payload TransferExpenseReceivingPayload + projectFK *uint + kandangID *uint + totalPrice float64 +} + +func groupingKey(supplierID uint, date time.Time, warehouseID uint) string { + return fmt.Sprintf("%d:%s:%d", supplierID, utils.FormatDate(date), warehouseID) +} + +type transferExpenseBridge struct { + db *gorm.DB + transferRepo rStockTransfer.StockTransferRepository + projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository + kandangRepo kandangRepo.KandangRepository + expenseSvc expenseSvc.ExpenseService +} + +func NewTransferExpenseBridge( + db *gorm.DB, + transferRepo rStockTransfer.StockTransferRepository, + projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, + kandangRepo kandangRepo.KandangRepository, + expenseSvc expenseSvc.ExpenseService, +) TransferExpenseBridge { + return &transferExpenseBridge{ + db: db, + transferRepo: transferRepo, + projectFlockKandangRepo: projectFlockKandangRepo, + kandangRepo: kandangRepo, + expenseSvc: expenseSvc, + } +} + +func (b *transferExpenseBridge) OnItemsDeleted(ctx context.Context, _ uint64, items []entity.StockTransferDetail) error { + if len(items) == 0 { + return nil + } + + return b.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + expenseIDs := make(map[uint64]struct{}) + expenseNonstockIDs := make([]uint64, 0) + + // Collect expense nonstock IDs from items + for _, item := range items { + if item.ExpenseNonstockId != nil && *item.ExpenseNonstockId != 0 { + expenseNonstockIDs = append(expenseNonstockIDs, *item.ExpenseNonstockId) + } + } + + if len(expenseNonstockIDs) > 0 { + // Get expense IDs from expense nonstocks + for _, nsID := range expenseNonstockIDs { + var expenseID uint64 + if err := tx.Model(&entity.ExpenseNonstock{}). + Select("expense_id"). + Where("id = ?", nsID). + Scan(&expenseID).Error; err != nil { + return err + } + if expenseID != 0 { + expenseIDs[expenseID] = struct{}{} + } + } + + // Delete expense nonstocks + if err := tx.Delete(&entity.ExpenseNonstock{}, expenseNonstockIDs).Error; err != nil { + return err + } + } + + // Check remaining expense nonstocks for each expense + approvalRepoTx := commonRepo.NewApprovalRepository(tx) + for expenseID := range expenseIDs { + var count int64 + if err := tx.Model(&entity.ExpenseNonstock{}). + Where("expense_id = ?", expenseID). + Count(&count).Error; err != nil { + return err + } + + // If no more expense nonstocks, delete expense and approval + if count == 0 { + if err := approvalRepoTx.DeleteByTarget(ctx, utils.ApprovalWorkflowExpense.String(), uint(expenseID)); err != nil { + return err + } + if err := tx.Delete(&entity.Expense{}, expenseID).Error; err != nil { + return err + } + } + } + + return nil + }) +} + +func (b *transferExpenseBridge) markExpensesUpdated(ctx context.Context, expenseIDs map[uint64]struct{}, actorID uint) error { + if len(expenseIDs) == 0 { + return nil + } + if actorID == 0 { + actorID = 1 + } + svc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db)) + action := entity.ApprovalActionUpdated + for id := range expenseIDs { + if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil { + return err + } + } + return nil +} + +func (b *transferExpenseBridge) findExpeditionNonstockID(ctx context.Context, supplierID uint) (uint64, error) { + var id uint64 + err := b.db.WithContext(ctx). + Table("nonstocks AS ns"). + Select("ns.id"). + Joins("JOIN nonstock_suppliers nss ON nss.nonstock_id = ns.id"). + Joins("JOIN flags f ON f.flagable_id = ns.id AND f.flagable_type = ?", entity.FlagableTypeNonstock). + Where("UPPER(f.name) = ?", strings.ToUpper(string(utils.FlagEkspedisi))). + Where("nss.supplier_id = ?", supplierID). + Order("ns.id"). + Limit(1). + Scan(&id).Error + if err != nil { + return 0, err + } + if id == 0 { + return 0, fiber.NewError(fiber.StatusBadRequest, "supplier id tidak sesuai dengan expedisi") + } + return id, nil +} + +func (b *transferExpenseBridge) createExpenseViaService( + c *fiber.Ctx, + transfer *entity.StockTransfer, + items []groupedTransferItem, + expenseDate time.Time, + expeditionNonstockID uint64, + movementNumber string, + supplierID uint, +) (*expenseDto.ExpenseDetailDTO, error) { + ctx := c.Context() + if b.expenseSvc == nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "expense service not available") + } + if len(items) == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "no items to create expense") + } + + kandangID := items[0].kandangID + var locationID uint64 + var expenseKandangID *uint64 + if kandangID != nil && *kandangID != 0 { + kandang, err := b.kandangRepo.GetByID(ctx, *kandangID, func(db *gorm.DB) *gorm.DB { + return db.Select("id, location_id") + }) + if err != nil { + return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Kandang not found: %d", *kandangID)) + } + if kandang == nil { + return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Kandang not found: %d", *kandangID)) + } + locationID = uint64(kandang.LocationId) + id := uint64(*kandangID) + expenseKandangID = &id + } else { + // For transfer, use destination warehouse location + if transfer.ToWarehouse == nil || transfer.ToWarehouse.LocationId == nil || *transfer.ToWarehouse.LocationId == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Destination warehouse location is required for expense") + } + locationID = uint64(*transfer.ToWarehouse.LocationId) + } + + costItems := make([]expenseValidation.CostItem, 0, len(items)) + for _, gi := range items { + note := fmt.Sprintf("stock_transfer_detail:%d", gi.detail.Id) + price := gi.detail.TotalQty + if gi.payload.TransportPerItem != nil { + price = *gi.payload.TransportPerItem + } + costItems = append(costItems, expenseValidation.CostItem{ + NonstockID: expeditionNonstockID, + Quantity: gi.payload.DeliveredQty, + Price: price, + Notes: note, + }) + } + + req := &expenseValidation.Create{ + PoNumber: "", + TransactionDate: utils.FormatDate(expenseDate), + Category: string(utils.ExpenseCategoryBOP), + SupplierID: uint64(supplierID), + LocationID: locationID, + ExpenseNonstocks: []expenseValidation.ExpenseNonstock{{ + KandangID: expenseKandangID, + CostItems: costItems, + }}, + } + + detail, err := b.expenseSvc.CreateOne(c, req) + if err != nil { + return nil, err + } + + // Mark approvals up to Finance so latest is Manager Finance + action := entity.ApprovalActionApproved + actorID := uint(transfer.CreatedBy) + if actorID == 0 { + actorID = 1 + } + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db)) + if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepManager, &action, actorID, nil); err != nil { + return nil, err + } + if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil { + return nil, err + } + + return detail, nil +} + +func (b *transferExpenseBridge) linkExpenseNonstocksToDetails(ctx context.Context, detail *expenseDto.ExpenseDetailDTO, items []groupedTransferItem) error { + if detail == nil || len(items) == 0 { + return nil + } + + noteToExpenseNonstock := mapExpenseNotesForTransfer(detail) + + if len(noteToExpenseNonstock) == 0 { + return nil + } + + for _, gi := range items { + expenseNonstockID, ok := noteToExpenseNonstock[gi.detail.Id] + if !ok { + continue + } + if err := b.db.WithContext(ctx). + Model(&entity.StockTransferDetail{}). + Where("id = ?", gi.detail.Id). + Update("expense_nonstock_id", expenseNonstockID).Error; err != nil { + return err + } + } + + return nil +} + +func mapExpenseNotesForTransfer(detail *expenseDto.ExpenseDetailDTO) map[uint64]uint64 { + result := make(map[uint64]uint64) + if detail == nil { + return result + } + for _, kandang := range detail.Kandangs { + for _, pengajuan := range kandang.Pengajuans { + note := strings.TrimSpace(pengajuan.Notes) + if note == "" { + continue + } + const prefix = "stock_transfer_detail:" + if !strings.HasPrefix(note, prefix) { + continue + } + idStr := strings.TrimPrefix(note, prefix) + var detailID uint64 + if _, err := fmt.Sscanf(idStr, "%d", &detailID); err != nil { + continue + } + result[detailID] = pengajuan.Id + } + } + return result +} + +func (b *transferExpenseBridge) OnItemsDelivered(c *fiber.Ctx, transferID uint64, updates []TransferExpenseReceivingPayload) error { + if transferID == 0 || len(updates) == 0 { + return nil + } + + ctx := c.Context() + + // Load transfer with details + transfer, err := b.transferRepo.GetByID(ctx, uint(transferID), func(db *gorm.DB) *gorm.DB { + return db. + Preload("Details"). + Preload("Details.Product"). + Preload("Details.DestProductWarehouse"). + Preload("ToWarehouse") + }) + if err != nil { + return err + } + + detailMap := make(map[uint64]*entity.StockTransferDetail, len(transfer.Details)) + for i := range transfer.Details { + detailMap[transfer.Details[i].Id] = &transfer.Details[i] + } + + groups := make(map[string][]groupedTransferItem) + + for _, payload := range updates { + if payload.DeliveredDate == nil { + return fiber.NewError(fiber.StatusBadRequest, "delivered_date is required") + } + detail := detailMap[payload.TransferDetailID] + if detail == nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Transfer detail %d not found", payload.TransferDetailID)) + } + if payload.DeliveredQty <= 0 { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Delivered quantity for detail %d must be greater than 0", payload.TransferDetailID)) + } + + deliveredDate := payload.DeliveredDate.UTC().Truncate(24 * time.Hour) + supplierID := payload.SupplierID + if supplierID == 0 { + supplierID = 1 // Default supplier + } + + var kandangID *uint + var projectFK *uint + if detail.DestProductWarehouse.WarehouseId != 0 { + var kandangIDResult uint + if err := b.db.WithContext(ctx). + Table("warehouses"). + Select("kandang_id"). + Where("id = ?", detail.DestProductWarehouse.WarehouseId). + Scan(&kandangIDResult).Error; err == nil && kandangIDResult != 0 { + id := uint(kandangIDResult) + kandangID = &id + if project, err := b.projectFlockKandangRepo.GetActiveByKandangID(ctx, kandangIDResult); err == nil && project != nil { + pid := uint(project.Id) + projectFK = &pid + } + } + } + + pricePerItem := detail.TotalQty + if payload.TransportPerItem != nil { + pricePerItem = *payload.TransportPerItem + } + totalPrice := pricePerItem * payload.DeliveredQty + + // Group by supplier:date:warehouse + warehouseID := uint(payload.WarehouseID) + if warehouseID == 0 && transfer.ToWarehouse != nil { + warehouseID = uint(transfer.ToWarehouse.Id) + } + if warehouseID == 0 && detail.DestProductWarehouse != nil { + warehouseID = uint(detail.DestProductWarehouse.WarehouseId) + } + + key := groupingKey(uint(supplierID), deliveredDate, warehouseID) + groups[key] = append(groups[key], groupedTransferItem{ + detail: detail, + payload: payload, + projectFK: projectFK, + kandangID: kandangID, + totalPrice: totalPrice, + }) + } + + updatedExpenses := make(map[uint64]struct{}) + + for key, items := range groups { + if len(items) == 0 { + continue + } + parts := strings.Split(key, ":") + if len(parts) < 3 { + return errors.New("invalid expense grouping key") + } + expenseDate, err := utils.ParseDateString(parts[1]) + if err != nil { + return err + } + + supplierID, err := strconv.ParseUint(parts[0], 10, 64) + if err != nil { + return err + } + + expeditionNonstockID, err := b.findExpeditionNonstockID(ctx, uint(supplierID)) + if err != nil { + return err + } + + expenseDetail, err := b.createExpenseViaService(c, transfer, items, expenseDate, expeditionNonstockID, transfer.MovementNumber, uint(supplierID)) + if err != nil { + return err + } + if err := b.linkExpenseNonstocksToDetails(ctx, expenseDetail, items); err != nil { + return err + } + if expenseDetail != nil && expenseDetail.Id != 0 { + updatedExpenses[uint64(expenseDetail.Id)] = struct{}{} + } + } + + if len(updatedExpenses) > 0 { + actorID := uint(1) // Default actor + if err := b.markExpensesUpdated(ctx, updatedExpenses, actorID); err != nil { + return err + } + } + + return nil +} diff --git a/internal/modules/purchases/services/expense_bridge.go b/internal/modules/purchases/services/expense_bridge.go index 6c74a1fc..23b95c58 100644 --- a/internal/modules/purchases/services/expense_bridge.go +++ b/internal/modules/purchases/services/expense_bridge.go @@ -594,7 +594,7 @@ func (b *expenseBridge) createExpenseViaService( req := &expenseValidation.Create{ PoNumber: "", TransactionDate: utils.FormatDate(expenseDate), - Category: "BOP", + Category: string(utils.ExpenseCategoryBOP), SupplierID: uint64(supplierID), LocationID: locationID, ExpenseNonstocks: []expenseValidation.ExpenseNonstock{{ From 7af78d04dde33043cb1ca3798e3502d8b08622d9 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 8 Jan 2026 18:52:12 +0700 Subject: [PATCH 06/19] feat(BE): add file size validation and improve document indexing for transfer creation --- .../controllers/transfer.controller.go | 10 +++++-- .../transfers/services/transfer.service.go | 30 ++++++++++++------- .../validations/transfer.validation.go | 2 +- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/internal/modules/inventory/transfers/controllers/transfer.controller.go b/internal/modules/inventory/transfers/controllers/transfer.controller.go index 4f060dc2..530d70dc 100644 --- a/internal/modules/inventory/transfers/controllers/transfer.controller.go +++ b/internal/modules/inventory/transfers/controllers/transfer.controller.go @@ -75,6 +75,8 @@ func (u *TransferController) GetOne(c *fiber.Ctx) error { func (u *TransferController) CreateOne(c *fiber.Ctx) error { data := c.FormValue("data") + const maxFileSize = 5 * 1024 * 1024 + var req validation.TransferRequest if err := json.Unmarshal([]byte(data), &req); err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") @@ -87,9 +89,11 @@ func (u *TransferController) CreateOne(c *fiber.Ctx) error { files := form.File["documents"] - if len(files) != len(req.Deliveries) { - return fiber.NewError(fiber.StatusBadRequest, - fiber.NewError(fiber.StatusBadRequest, "Jumlah dokumen harus sama dengan jumlah deliveries").Message) + for i, file := range files { + if file.Size > maxFileSize { + return fiber.NewError(fiber.StatusBadRequest, + "Dokumen ke-"+strconv.Itoa(i+1)+" melebihi ukuran maksimal 5MB") + } } result, err := u.TransferService.CreateOne(c, &req, files) diff --git a/internal/modules/inventory/transfers/services/transfer.service.go b/internal/modules/inventory/transfers/services/transfer.service.go index 86fdc69a..d14125ed 100644 --- a/internal/modules/inventory/transfers/services/transfer.service.go +++ b/internal/modules/inventory/transfers/services/transfer.service.go @@ -128,7 +128,6 @@ func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, e func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferRequest, files []*multipart.FileHeader) (*entity.StockTransfer, error) { - // === VALIDASI SOURCE WAREHOUSE === pwIDs := make([]uint, 0, len(req.Products)) for _, product := range req.Products { @@ -201,7 +200,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d bukan kategori BOP", delivery.SupplierID)) } } - seqNum, err := s.StockTransferRepo.GetNextMovementNumber(c.Context()) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate movement number") @@ -226,12 +224,11 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques return err } - // Prepare details and fetch product warehouses details := make([]*entity.StockTransferDetail, 0, len(req.Products)) detailMap := make(map[uint64]*entity.StockTransferDetail) for _, product := range req.Products { - // Get source product warehouse + sourcePW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID( c.Context(), uint(product.ProductID), uint(req.SourceWarehouseID), ) @@ -242,7 +239,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data product warehouse source") } - // Get or create destination product warehouse destPW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID( c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID), ) @@ -323,24 +319,38 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques if s.DocumentSvc != nil && len(files) > 0 { - for idx, file := range files { + for deliveryIdx, delivery := range deliveries { + reqDelivery := req.Deliveries[deliveryIdx] + + if reqDelivery.DocumentIndex < 0 { + continue + } + + if reqDelivery.DocumentIndex >= len(files) { + return fiber.NewError(fiber.StatusBadRequest, + fmt.Sprintf("DocumentIndex %d untuk delivery %d melebihi jumlah file yang diupload (%d)", + reqDelivery.DocumentIndex, deliveryIdx+1, len(files))) + } + + file := files[reqDelivery.DocumentIndex] + documentFiles := []commonSvc.DocumentFile{ { File: file, Type: string(utils.DocumentTypeTransfer), - Index: &idx, + Index: &reqDelivery.DocumentIndex, }, } _, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{ DocumentableType: string(utils.DocumentableTypeTransfer), - DocumentableID: deliveries[idx].Id, + DocumentableID: delivery.Id, CreatedBy: &actorID, Files: documentFiles, }) if err != nil { s.Log.WithError(err).Errorf("Failed to upload document for delivery %d (delivery_id: %d, filename: %s)", - idx+1, deliveries[idx].Id, file.Filename) - return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to upload document for delivery %d: %v", idx+1, err)) + deliveryIdx+1, delivery.Id, file.Filename) + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to upload document for delivery %d: %v", deliveryIdx+1, err)) } } } diff --git a/internal/modules/inventory/transfers/validations/transfer.validation.go b/internal/modules/inventory/transfers/validations/transfer.validation.go index c64077ff..785295e2 100644 --- a/internal/modules/inventory/transfers/validations/transfer.validation.go +++ b/internal/modules/inventory/transfers/validations/transfer.validation.go @@ -23,7 +23,7 @@ type TransferDeliveryProduct struct { type TransferDelivery struct { DeliveryCost float64 `json:"delivery_cost" validate:"required"` DeliveryCostPerItem float64 `json:"delivery_cost_per_item" validate:"required"` - DocumentIndex int `json:"document_index" validate:"min=0"` + DocumentIndex int `json:"document_index" validate:"omitempty,min=-1" default:"-1"` DriverName string `json:"driver_name" validate:"required"` VehiclePlate string `json:"vehicle_plate" validate:"required"` SupplierID uint `json:"supplier_id" validate:"required"` From 5606b9c4a3118303142f27e030a4b5e4eb6a2254 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 8 Jan 2026 20:44:56 +0700 Subject: [PATCH 07/19] FIX(BE): fix closing marketing 500 --- .../modules/closings/dto/closingMarketing.dto.go | 9 +++++++-- .../modules/closings/services/closing.service.go | 12 +++++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/internal/modules/closings/dto/closingMarketing.dto.go b/internal/modules/closings/dto/closingMarketing.dto.go index 4c7b4d35..42d95be2 100644 --- a/internal/modules/closings/dto/closingMarketing.dto.go +++ b/internal/modules/closings/dto/closingMarketing.dto.go @@ -55,16 +55,21 @@ func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO { kandang = &mapped } + var realizationDate time.Time + if e.DeliveryDate != nil { + realizationDate = *e.DeliveryDate + } + doNumber := deliveryOrdersDTO.GenerateDeliveryOrderNumber(e.MarketingProduct.Marketing.SoNumber, e.DeliveryDate, e.MarketingProduct.ProductWarehouse.Warehouse.Id) return SalesDTO{ Id: e.Id, - RealizationDate: *e.DeliveryDate, + RealizationDate: realizationDate, Age: age, DoNumber: doNumber, Product: product, Customer: customer, - Qty: e.UsageQty, // Show allocated quantity from FIFO + Qty: e.UsageQty, Weight: e.TotalWeight, AvgWeight: e.AvgWeight, Price: e.UnitPrice, diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index 9659601e..c3e3108d 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -153,7 +153,17 @@ func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint) ([]entit if len(realisasi) == 0 { return []entity.MarketingDeliveryProduct{}, nil } - return realisasi, nil + + filtered := make([]entity.MarketingDeliveryProduct, 0, len(realisasi)) + for _, item := range realisasi { + + if item.UsageQty != 0 || item.TotalWeight != 0 || item.AvgWeight != 0 || + item.UnitPrice != 0 || item.TotalPrice != 0 { + filtered = append(filtered, item) + } + } + + return filtered, nil } func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error) { From 857a3c284b3ab398347d317eb90129f8e4d8eaa2 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 8 Jan 2026 22:00:32 +0700 Subject: [PATCH 08/19] feat(BE): implement movement number generation and refactor transfer creation logic --- .../repositories/stock_transfer.repository.go | 11 ++++++ .../transfers/services/transfer.service.go | 39 +++++++++++-------- 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/internal/modules/inventory/transfers/repositories/stock_transfer.repository.go b/internal/modules/inventory/transfers/repositories/stock_transfer.repository.go index e79d6310..cd314901 100644 --- a/internal/modules/inventory/transfers/repositories/stock_transfer.repository.go +++ b/internal/modules/inventory/transfers/repositories/stock_transfer.repository.go @@ -2,6 +2,7 @@ package repositories import ( "context" + "fmt" "gitlab.com/mbugroup/lti-api.git/internal/common/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" @@ -12,6 +13,7 @@ type StockTransferRepository interface { repository.BaseRepository[entity.StockTransfer] // get sequence for movement number GetNextMovementNumber(ctx context.Context) (int64, error) + GenerateMovementNumber(ctx context.Context) (string, error) } type StockTransferRepositoryImpl struct { @@ -32,3 +34,12 @@ func (r *StockTransferRepositoryImpl) GetNextMovementNumber(ctx context.Context) } return seq, nil } + +func (r *StockTransferRepositoryImpl) GenerateMovementNumber(ctx context.Context) (string, error) { + seq, err := r.GetNextMovementNumber(ctx) + if err != nil { + return "", err + } + movementNumber := fmt.Sprintf("ST-%05d", seq) + return movementNumber, nil +} diff --git a/internal/modules/inventory/transfers/services/transfer.service.go b/internal/modules/inventory/transfers/services/transfer.service.go index d14125ed..dc6399d5 100644 --- a/internal/modules/inventory/transfers/services/transfer.service.go +++ b/internal/modules/inventory/transfers/services/transfer.service.go @@ -159,14 +159,12 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques return nil, err } - if s.ProjectFlockKandangRepo != nil { - projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), destPfkID) - if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock") - } - if projectFlockKandang.ClosedAt != nil { - return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock tujuan sudah closing") - } + projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), destPfkID) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock") + } + if projectFlockKandang.ClosedAt != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock tujuan sudah closing") } actorID, err := m.ActorIDFromContext(c) @@ -200,11 +198,12 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d bukan kategori BOP", delivery.SupplierID)) } } - seqNum, err := s.StockTransferRepo.GetNextMovementNumber(c.Context()) + + movementNumber, err := s.StockTransferRepo.GenerateMovementNumber(c.Context()) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate movement number") } - movementNumber := fmt.Sprintf("PND-MBU-%05d", seqNum) + transferDate, _ := utils.ParseDateString(req.TransferDate) entityTransfer := &entity.StockTransfer{ @@ -220,7 +219,13 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques err = s.StockTransferRepo.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { - if err := s.StockTransferRepo.WithTx(tx).CreateOne(c.Context(), entityTransfer, nil); err != nil { + stockTransferRepoTX := s.StockTransferRepo.WithTx(tx) + stockTransferDetailRepoTX := s.StockTransferDetailRepo.WithTx(tx) + stockTransferDeliveryRepoTX := s.StockTransferDeliveryRepo.WithTx(tx) + stockTransferDeliveryItemRepoTX := s.StockTransferDeliveryItemRepo.WithTx(tx) + productWarehouseRepoTX := rProductWarehouse.NewProductWarehouseRepository(tx) + + if err := stockTransferRepoTX.CreateOne(c.Context(), entityTransfer, nil); err != nil { return err } @@ -229,7 +234,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques for _, product := range req.Products { - sourcePW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID( + sourcePW, err := productWarehouseRepoTX.GetProductWarehouseByProductAndWarehouseID( c.Context(), uint(product.ProductID), uint(req.SourceWarehouseID), ) if err != nil { @@ -239,7 +244,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data product warehouse source") } - destPW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID( + destPW, err := productWarehouseRepoTX.GetProductWarehouseByProductAndWarehouseID( c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID), ) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { @@ -257,7 +262,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques Quantity: 0, ProjectFlockKandangId: &projectFlockKandangID, } - if err := s.ProductWarehouseRepo.WithTx(tx).CreateOne(c.Context(), destPW, nil); err != nil { + if err := productWarehouseRepoTX.CreateOne(c.Context(), destPW, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat product warehouse destination") } } @@ -278,7 +283,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques detailMap[uint64(product.ProductID)] = detail } - if err := s.StockTransferDetailRepo.WithTx(tx).CreateMany(c.Context(), details, nil); err != nil { + if err := stockTransferDetailRepoTX.CreateMany(c.Context(), details, nil); err != nil { return err } @@ -293,7 +298,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques ShippingCostTotal: delivery.DeliveryCost, }) } - if err := s.StockTransferDeliveryRepo.WithTx(tx).CreateMany(c.Context(), deliveries, nil); err != nil { + if err := stockTransferDeliveryRepoTX.CreateMany(c.Context(), deliveries, nil); err != nil { return err } @@ -313,7 +318,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques }) } } - if err := s.StockTransferDeliveryItemRepo.WithTx(tx).CreateMany(c.Context(), deliveryItems, nil); err != nil { + if err := stockTransferDeliveryItemRepoTX.CreateMany(c.Context(), deliveryItems, nil); err != nil { return err } From b11f03dfda56e7ff1b65b42f18e3f0d5a4d9db17 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Fri, 9 Jan 2026 09:15:53 +0700 Subject: [PATCH 09/19] feat(BE): fixing wrong perhitungan biaya --- .../repositories/closing.repository.go | 8 +- .../closings/services/closing.service.go | 13 +--- .../services/transfer_expense_bridge.go | 75 ++++++++++++------- .../repositories/projectflock.repository.go | 5 ++ .../hpp_per_kandang.repository.go | 10 +-- 5 files changed, 62 insertions(+), 49 deletions(-) diff --git a/internal/modules/closings/repositories/closing.repository.go b/internal/modules/closings/repositories/closing.repository.go index 4948ae5e..9d08d083 100644 --- a/internal/modules/closings/repositories/closing.repository.go +++ b/internal/modules/closings/repositories/closing.repository.go @@ -10,6 +10,7 @@ import ( entity "gitlab.com/mbugroup/lti-api.git/internal/entities" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations" "gitlab.com/mbugroup/lti-api.git/internal/utils" + "gitlab.com/mbugroup/lti-api.git/internal/utils/fifo" "gorm.io/gorm" ) @@ -914,9 +915,8 @@ func (r *ClosingRepositoryImpl) GetActualUsageCostByProjectFlockID(ctx context.C var rows []ActualUsageCostRow - // Part 1: Get usage from recording_stocks (PAKAN, OVK, Vitamin, Obat, Kimia, dll) - purchaseStockableKey := "PURCHASE_ITEMS" - transferStockableKey := "STOCK_TRANSFER_DETAILS" + purchaseStockableKey := fifo.StockableKeyPurchaseItems.String() + transferStockableKey := fifo.StockableKeyStockTransferIn.String() recordingQuery := db. Table("recordings AS r"). @@ -982,7 +982,6 @@ func (r *ClosingRepositoryImpl) GetActualUsageCostByProjectFlockID(ctx context.C return nil, err } - // Part 2: Get usage from project_chickins (DOC, Pullet) chickinQuery := db. Table("project_chickins AS pc"). Select(` @@ -1006,7 +1005,6 @@ func (r *ClosingRepositoryImpl) GetActualUsageCostByProjectFlockID(ctx context.C return nil, err } - // Merge results rows = append(rows, chickinRows...) return rows, nil diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index c3e3108d..a76085c4 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -413,18 +413,9 @@ func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint) (*dto.Ove } func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error) { - if projectFlockID == 0 { - return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") - } if err := commonSvc.EnsureRelations(c.Context(), - commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: func(ctx context.Context, id uint) (bool, error) { - _, err := s.ProjectFlockRepo.GetByID(ctx, id, nil) - if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { - return false, nil - } - return err == nil, err - }}, + commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists}, ); err != nil { return nil, err } @@ -439,13 +430,11 @@ func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (* return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets") } - // Get actual usage cost instead of purchase items actualUsageRows, err := s.Repository.GetActualUsageCostByProjectFlockID(c.Context(), projectFlockID) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch actual usage cost") } - // Convert actual usage rows to pseudo purchase items purchaseItems := s.convertActualUsageToPurchaseItems(c.Context(), actualUsageRows) realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID) diff --git a/internal/modules/inventory/transfers/services/transfer_expense_bridge.go b/internal/modules/inventory/transfers/services/transfer_expense_bridge.go index b16cfd4f..90350c18 100644 --- a/internal/modules/inventory/transfers/services/transfer_expense_bridge.go +++ b/internal/modules/inventory/transfers/services/transfer_expense_bridge.go @@ -39,11 +39,12 @@ type TransferExpenseReceivingPayload struct { } type groupedTransferItem struct { - detail *entity.StockTransferDetail - payload TransferExpenseReceivingPayload - projectFK *uint - kandangID *uint - totalPrice float64 + detail *entity.StockTransferDetail + payload TransferExpenseReceivingPayload + projectFK *uint + kandangID *uint + totalPrice float64 + shippingCostTotal float64 } func groupingKey(supplierID uint, date time.Time, warehouseID uint) string { @@ -83,7 +84,7 @@ func (b *transferExpenseBridge) OnItemsDeleted(ctx context.Context, _ uint64, it expenseIDs := make(map[uint64]struct{}) expenseNonstockIDs := make([]uint64, 0) - // Collect expense nonstock IDs from items + for _, item := range items { if item.ExpenseNonstockId != nil && *item.ExpenseNonstockId != 0 { expenseNonstockIDs = append(expenseNonstockIDs, *item.ExpenseNonstockId) @@ -91,7 +92,7 @@ func (b *transferExpenseBridge) OnItemsDeleted(ctx context.Context, _ uint64, it } if len(expenseNonstockIDs) > 0 { - // Get expense IDs from expense nonstocks + for _, nsID := range expenseNonstockIDs { var expenseID uint64 if err := tx.Model(&entity.ExpenseNonstock{}). @@ -105,13 +106,13 @@ func (b *transferExpenseBridge) OnItemsDeleted(ctx context.Context, _ uint64, it } } - // Delete expense nonstocks + if err := tx.Delete(&entity.ExpenseNonstock{}, expenseNonstockIDs).Error; err != nil { return err } } - // Check remaining expense nonstocks for each expense + approvalRepoTx := commonRepo.NewApprovalRepository(tx) for expenseID := range expenseIDs { var count int64 @@ -121,7 +122,7 @@ func (b *transferExpenseBridge) OnItemsDeleted(ctx context.Context, _ uint64, it return err } - // If no more expense nonstocks, delete expense and approval + if count == 0 { if err := approvalRepoTx.DeleteByTarget(ctx, utils.ApprovalWorkflowExpense.String(), uint(expenseID)); err != nil { return err @@ -208,7 +209,7 @@ func (b *transferExpenseBridge) createExpenseViaService( id := uint64(*kandangID) expenseKandangID = &id } else { - // For transfer, use destination warehouse location + if transfer.ToWarehouse == nil || transfer.ToWarehouse.LocationId == nil || *transfer.ToWarehouse.LocationId == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "Destination warehouse location is required for expense") } @@ -218,13 +219,16 @@ func (b *transferExpenseBridge) createExpenseViaService( costItems := make([]expenseValidation.CostItem, 0, len(items)) for _, gi := range items { note := fmt.Sprintf("stock_transfer_detail:%d", gi.detail.Id) - price := gi.detail.TotalQty + + + price := gi.shippingCostTotal if gi.payload.TransportPerItem != nil { - price = *gi.payload.TransportPerItem + price = *gi.payload.TransportPerItem * gi.payload.DeliveredQty } + costItems = append(costItems, expenseValidation.CostItem{ NonstockID: expeditionNonstockID, - Quantity: gi.payload.DeliveredQty, + Quantity: 1, Price: price, Notes: note, }) @@ -247,7 +251,7 @@ func (b *transferExpenseBridge) createExpenseViaService( return nil, err } - // Mark approvals up to Finance so latest is Manager Finance + action := entity.ApprovalActionApproved actorID := uint(transfer.CreatedBy) if actorID == 0 { @@ -324,12 +328,14 @@ func (b *transferExpenseBridge) OnItemsDelivered(c *fiber.Ctx, transferID uint64 ctx := c.Context() - // Load transfer with details + transfer, err := b.transferRepo.GetByID(ctx, uint(transferID), func(db *gorm.DB) *gorm.DB { return db. Preload("Details"). Preload("Details.Product"). Preload("Details.DestProductWarehouse"). + Preload("Details.DeliveryItems"). + Preload("Details.DeliveryItems.StockTransferDelivery"). Preload("ToWarehouse") }) if err != nil { @@ -337,8 +343,18 @@ func (b *transferExpenseBridge) OnItemsDelivered(c *fiber.Ctx, transferID uint64 } detailMap := make(map[uint64]*entity.StockTransferDetail, len(transfer.Details)) + shippingCostMap := make(map[uint64]float64) // detailID -> ShippingCostTotal + for i := range transfer.Details { detailMap[transfer.Details[i].Id] = &transfer.Details[i] + + + for _, deliveryItem := range transfer.Details[i].DeliveryItems { + if deliveryItem.StockTransferDelivery != nil { + shippingCostMap[transfer.Details[i].Id] = deliveryItem.StockTransferDelivery.ShippingCostTotal + break + } + } } groups := make(map[string][]groupedTransferItem) @@ -379,13 +395,17 @@ func (b *transferExpenseBridge) OnItemsDelivered(c *fiber.Ctx, transferID uint64 } } - pricePerItem := detail.TotalQty - if payload.TransportPerItem != nil { - pricePerItem = *payload.TransportPerItem - } - totalPrice := pricePerItem * payload.DeliveredQty + + shippingCostTotal := shippingCostMap[detail.Id] + + + totalPrice := shippingCostTotal + if payload.TransportPerItem != nil { + + totalPrice = *payload.TransportPerItem * payload.DeliveredQty + } + - // Group by supplier:date:warehouse warehouseID := uint(payload.WarehouseID) if warehouseID == 0 && transfer.ToWarehouse != nil { warehouseID = uint(transfer.ToWarehouse.Id) @@ -396,11 +416,12 @@ func (b *transferExpenseBridge) OnItemsDelivered(c *fiber.Ctx, transferID uint64 key := groupingKey(uint(supplierID), deliveredDate, warehouseID) groups[key] = append(groups[key], groupedTransferItem{ - detail: detail, - payload: payload, - projectFK: projectFK, - kandangID: kandangID, - totalPrice: totalPrice, + detail: detail, + payload: payload, + projectFK: projectFK, + kandangID: kandangID, + totalPrice: totalPrice, + shippingCostTotal: shippingCostTotal, }) } diff --git a/internal/modules/production/project_flocks/repositories/projectflock.repository.go b/internal/modules/production/project_flocks/repositories/projectflock.repository.go index 6cd98a8f..e65dfb4a 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock.repository.go @@ -20,6 +20,7 @@ type ProjectflockRepository interface { GetCurrentProjectPeriod(ctx context.Context, projectFlockID uint) (int, error) GetKandangPeriodSummaryRows(ctx context.Context, locationID uint) ([]KandangPeriodRow, error) GetActiveByLocationID(ctx context.Context, locationID uint64) ([]entity.ProjectFlock, error) + IdExists(ctx context.Context, id uint) (bool, error) AreaExists(ctx context.Context, id uint) (bool, error) FcrExists(ctx context.Context, id uint) (bool, error) ProductionStandardExists(ctx context.Context, id uint) (bool, error) @@ -161,6 +162,10 @@ func (r *ProjectflockRepositoryImpl) applySearchFilters(db *gorm.DB, rawSearch s ) } +func (r *ProjectflockRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) { + return repository.Exists[entity.ProjectFlock](ctx, r.DB(), id) +} + func (r *ProjectflockRepositoryImpl) AreaExists(ctx context.Context, id uint) (bool, error) { return repository.Exists[entity.Area](ctx, r.DB(), id) } diff --git a/internal/modules/repports/repositories/hpp_per_kandang.repository.go b/internal/modules/repports/repositories/hpp_per_kandang.repository.go index 6d4185e8..4bd9aab4 100644 --- a/internal/modules/repports/repositories/hpp_per_kandang.repository.go +++ b/internal/modules/repports/repositories/hpp_per_kandang.repository.go @@ -107,21 +107,21 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context, recordingPfk = applyLocationFilters(recordingPfk, areaIDs, locationIDs, kandangIDs) purchaseStockableKey := fifo.StockableKeyPurchaseItems.String() - transferStockableKey := fifo.StockableKey("STOCK_TRANSFER_DETAILS").String() + transferStockableKey := fifo.StockableKeyStockTransferIn.String() query := r.db.WithContext(ctx). Table("recordings AS r"). Select(` k.id AS kandang_id, - COALESCE(SUM(CASE + COALESCE(SUM(CASE WHEN f.name = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0) WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.quantity, 0) * COALESCE(tpi.price, 0) - ELSE 0 + ELSE 0 END), 0) AS feed_cost, - COALESCE(SUM(CASE + COALESCE(SUM(CASE WHEN f.name = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0) WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.quantity, 0) * COALESCE(tpi.price, 0) - ELSE 0 + ELSE 0 END), 0) AS ovk_cost`, utils.FlagPakan, transferStockableKey, utils.FlagPakan, utils.FlagOVK, transferStockableKey, utils.FlagOVK). From f7522636e242a121bb9103765889e7c923b1913f Mon Sep 17 00:00:00 2001 From: ragilap Date: Fri, 9 Jan 2026 09:27:49 +0700 Subject: [PATCH 10/19] feat(BE-281): unique uniformity weeks --- .../services/uniformity.service.go | 45 +++++++++++++------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/internal/modules/production/uniformities/services/uniformity.service.go b/internal/modules/production/uniformities/services/uniformity.service.go index cde86694..6e03795d 100644 --- a/internal/modules/production/uniformities/services/uniformity.service.go +++ b/internal/modules/production/uniformities/services/uniformity.service.go @@ -199,14 +199,26 @@ func (s uniformityService) MapCharts(c *fiber.Ctx, items []entity.ProjectFlockKa for _, group := range grouped { allWeeks := make(map[int]utypes.UniformityChartWeek) weekOrder := make([]int, 0, len(group)) - weekSeen := make(map[int]struct{}, len(group)) weeksWithData := 0 gaugeWeeks := make([]utypes.UniformityChartGaugeWeek, 0, len(group)) + latestByWeek := make(map[int]entity.ProjectFlockKandangUniformity) for _, item := range group { if item.Week == 0 { continue } + if existing, ok := latestByWeek[item.Week]; !ok || isUniformityNewer(item, existing) { + latestByWeek[item.Week] = item + } + } + + for week := range latestByWeek { + weekOrder = append(weekOrder, week) + } + sort.Ints(weekOrder) + + for _, week := range weekOrder { + item := latestByWeek[week] var weekSummary utypes.UniformityChartWeek if len(item.ChartData) > 0 { if err := json.Unmarshal(item.ChartData, &weekSummary); err != nil { @@ -222,16 +234,11 @@ func (s uniformityService) MapCharts(c *fiber.Ctx, items []entity.ProjectFlockKa if weekSummary.HasData { weeksWithData++ } - allWeeks[item.Week] = weekSummary - - if _, ok := weekSeen[item.Week]; !ok { - weekSeen[item.Week] = struct{}{} - weekOrder = append(weekOrder, item.Week) - } + allWeeks[week] = weekSummary hasData := item.ChickQtyOfWeight > 0 gaugeWeeks = append(gaugeWeeks, utypes.UniformityChartGaugeWeek{ - Week: item.Week, + Week: week, UniformityPercent: item.Uniformity, IdealCount: item.UniformQty, OutsideIdealCount: item.NotUniformQty, @@ -240,11 +247,6 @@ func (s uniformityService) MapCharts(c *fiber.Ctx, items []entity.ProjectFlockKa }) } - sort.Ints(weekOrder) - sort.Slice(gaugeWeeks, func(i, j int) bool { - return gaugeWeeks[i].Week < gaugeWeeks[j].Week - }) - weekIndex := make(map[int]int, len(weekOrder)) for idx, week := range weekOrder { weekIndex[week] = idx @@ -280,6 +282,23 @@ func (s uniformityService) MapCharts(c *fiber.Ctx, items []entity.ProjectFlockKa return result, nil } +func isUniformityNewer(a, b entity.ProjectFlockKandangUniformity) bool { + var aDate, bDate time.Time + if a.UniformDate != nil { + aDate = *a.UniformDate + } + if b.UniformDate != nil { + bDate = *b.UniformDate + } + if !aDate.IsZero() || !bDate.IsZero() { + if aDate.Equal(bDate) { + return a.CreatedAt.After(b.CreatedAt) + } + return aDate.After(bDate) + } + return a.CreatedAt.After(b.CreatedAt) +} + func (s uniformityService) MapDocuments(c *fiber.Ctx, items []entity.ProjectFlockKandangUniformity) (map[uint]string, error) { if s.DocumentSvc == nil || len(items) == 0 { return map[uint]string{}, nil From 338312edd11513764df35fcccfe3e2359c68b87f Mon Sep 17 00:00:00 2001 From: ragilap Date: Fri, 9 Jan 2026 10:04:31 +0700 Subject: [PATCH 11/19] feat(BE-281): unique uniformity weeks --- .../production/uniformities/services/uniformity.service.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/modules/production/uniformities/services/uniformity.service.go b/internal/modules/production/uniformities/services/uniformity.service.go index 6e03795d..3c021a17 100644 --- a/internal/modules/production/uniformities/services/uniformity.service.go +++ b/internal/modules/production/uniformities/services/uniformity.service.go @@ -605,13 +605,13 @@ func (s uniformityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id ui } func (s *uniformityService) ensureUniqueUniformity(ctx context.Context, id uint, projectFlockKandangID uint, week int, uniformDate *time.Time) error { - if projectFlockKandangID == 0 || week == 0 || uniformDate == nil || uniformDate.IsZero() { + if projectFlockKandangID == 0 || week == 0 { return nil } query := s.Repository.DB().WithContext(ctx). Model(&entity.ProjectFlockKandangUniformity{}). - Where("project_flock_kandang_id = ? AND week = ? AND uniform_date = ?", projectFlockKandangID, week, *uniformDate) + Where("project_flock_kandang_id = ? AND week = ?", projectFlockKandangID, week) if id != 0 { query = query.Where("id <> ?", id) } @@ -621,7 +621,7 @@ func (s *uniformityService) ensureUniqueUniformity(ctx context.Context, id uint, return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate uniformity uniqueness") } if count > 0 { - return fiber.NewError(fiber.StatusConflict, "Uniformity already exists for the same project flock kandang, week, and date") + return fiber.NewError(fiber.StatusConflict, "Uniformity already exists for the same project flock kandang and week") } return nil } From 43eb1df118f16d823dc01a081f7266ea3aa9cb5e Mon Sep 17 00:00:00 2001 From: ragilap Date: Fri, 9 Jan 2026 10:06:22 +0700 Subject: [PATCH 12/19] feat(BE-281): fixing duplicate --- .../production/uniformities/services/uniformity.service.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/modules/production/uniformities/services/uniformity.service.go b/internal/modules/production/uniformities/services/uniformity.service.go index 3c021a17..92db84a3 100644 --- a/internal/modules/production/uniformities/services/uniformity.service.go +++ b/internal/modules/production/uniformities/services/uniformity.service.go @@ -292,11 +292,11 @@ func isUniformityNewer(a, b entity.ProjectFlockKandangUniformity) bool { } if !aDate.IsZero() || !bDate.IsZero() { if aDate.Equal(bDate) { - return a.CreatedAt.After(b.CreatedAt) + return a.Id > b.Id } return aDate.After(bDate) } - return a.CreatedAt.After(b.CreatedAt) + return a.Id > b.Id } func (s uniformityService) MapDocuments(c *fiber.Ctx, items []entity.ProjectFlockKandangUniformity) (map[uint]string, error) { From 8a57d439dc5dc200a7e9982c7889d454f0cbc39d Mon Sep 17 00:00:00 2001 From: "Hafizh A. Y" Date: Wed, 7 Jan 2026 16:09:08 +0700 Subject: [PATCH 13/19] unfinish: seeder and fix migration --- .../migrations/20251117034511_create_expenses_table.down.sql | 3 ++- .../20251210044651_create_so_number_sequence.down.sql | 2 +- internal/database/seed/seeder.go | 1 + internal/entities/product.go | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/database/migrations/20251117034511_create_expenses_table.down.sql b/internal/database/migrations/20251117034511_create_expenses_table.down.sql index bf0ea945..9b06613a 100644 --- a/internal/database/migrations/20251117034511_create_expenses_table.down.sql +++ b/internal/database/migrations/20251117034511_create_expenses_table.down.sql @@ -1 +1,2 @@ -DROP TABLE IF EXISTS expenses; \ No newline at end of file +DROP SEQUENCE IF EXISTS expenses_ref_seq; +DROP TABLE IF EXISTS expenses; diff --git a/internal/database/migrations/20251210044651_create_so_number_sequence.down.sql b/internal/database/migrations/20251210044651_create_so_number_sequence.down.sql index 4d80dd2c..53907ef1 100644 --- a/internal/database/migrations/20251210044651_create_so_number_sequence.down.sql +++ b/internal/database/migrations/20251210044651_create_so_number_sequence.down.sql @@ -1,3 +1,3 @@ -- Drop function and sequence for sales order numbers -DROP FUNCTION IF EXISTS generate_so_number(); DROP SEQUENCE IF EXISTS so_number_seq; +DROP FUNCTION IF EXISTS generate_so_number(); diff --git a/internal/database/seed/seeder.go b/internal/database/seed/seeder.go index b4f6886e..4f666812 100644 --- a/internal/database/seed/seeder.go +++ b/internal/database/seed/seeder.go @@ -299,6 +299,7 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories Tax: tax, ExpiryPeriod: seed.Expiry, CreatedBy: createdBy, + IsVisible: seed.IsVisible, } if err := tx.Create(&product).Error; err != nil { return err diff --git a/internal/entities/product.go b/internal/entities/product.go index d8ce59fc..f86d9a0a 100644 --- a/internal/entities/product.go +++ b/internal/entities/product.go @@ -21,7 +21,7 @@ type Product struct { CreatedAt time.Time `gorm:"autoCreateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - IsVisible bool `gorm:"column:is_visible;default:true"` + IsVisible bool `` CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` Uom Uom `gorm:"foreignKey:UomId;references:Id"` From b7914e8294bd42cacce9c39eff0e927c4938dd0d Mon Sep 17 00:00:00 2001 From: "Hafizh A. Y" Date: Fri, 9 Jan 2026 16:03:41 +0700 Subject: [PATCH 14/19] fix(BE): add party account number in payments --- ..._party_account_number_to_payments.down.sql | 6 ++++ ...dd_party_account_number_to_payments.up.sql | 6 ++++ internal/entities/payment.go | 33 +++++++++--------- .../finance/initials/dto/initial.dto.go | 13 ++++--- .../initials/services/initial.service.go | 1 + .../injections/services/injection.service.go | 1 + .../validations/injection.validation.go | 16 ++++----- .../finance/payments/dto/payment.dto.go | 13 ++++--- internal/modules/finance/payments/route.go | 8 ++--- .../payments/services/payment.service.go | 28 ++++++++------- .../validations/payment.validation.go | 34 ++++++++++--------- .../transactions/dto/transaction.dto.go | 13 ++++--- 12 files changed, 104 insertions(+), 68 deletions(-) create mode 100644 internal/database/migrations/20260109074006_add_party_account_number_to_payments.down.sql create mode 100644 internal/database/migrations/20260109074006_add_party_account_number_to_payments.up.sql diff --git a/internal/database/migrations/20260109074006_add_party_account_number_to_payments.down.sql b/internal/database/migrations/20260109074006_add_party_account_number_to_payments.down.sql new file mode 100644 index 00000000..64eb4839 --- /dev/null +++ b/internal/database/migrations/20260109074006_add_party_account_number_to_payments.down.sql @@ -0,0 +1,6 @@ +BEGIN; + +ALTER TABLE payments + DROP COLUMN IF EXISTS party_account_number; + +COMMIT; diff --git a/internal/database/migrations/20260109074006_add_party_account_number_to_payments.up.sql b/internal/database/migrations/20260109074006_add_party_account_number_to_payments.up.sql new file mode 100644 index 00000000..abd80665 --- /dev/null +++ b/internal/database/migrations/20260109074006_add_party_account_number_to_payments.up.sql @@ -0,0 +1,6 @@ +BEGIN; + +ALTER TABLE payments + ADD COLUMN IF NOT EXISTS party_account_number VARCHAR(50); + +COMMIT; diff --git a/internal/entities/payment.go b/internal/entities/payment.go index e48800fb..55575f20 100644 --- a/internal/entities/payment.go +++ b/internal/entities/payment.go @@ -7,22 +7,23 @@ import ( ) type Payment struct { - Id uint `gorm:"primaryKey;autoIncrement"` - PaymentCode string `gorm:"type:varchar(50);not null"` - ReferenceNumber *string `gorm:"type:varchar(100)"` - TransactionType string `gorm:"type:varchar(50)"` - PartyType string `gorm:"type:varchar(50);not null;index:payments_party_polymorphic,priority:1"` - PartyId uint `gorm:"not null;index:payments_party_polymorphic,priority:2"` - PaymentDate time.Time `gorm:"not null"` - PaymentMethod string `gorm:"type:varchar(20);not null"` - BankId *uint `gorm:"not null;index:idx_payments_bank_id"` - Direction string `gorm:"type:varchar(5);not null"` - Nominal float64 `gorm:"type:numeric(15,3);not null"` - Notes string `gorm:"type:text;not null"` - CreatedAt time.Time `gorm:"autoCreateTime"` - UpdatedAt time.Time `gorm:"autoUpdateTime"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - CreatedBy uint `gorm:"index" json:"-"` + Id uint `gorm:"primaryKey;autoIncrement"` + PaymentCode string `gorm:"type:varchar(50);not null"` + ReferenceNumber *string `gorm:"type:varchar(100)"` + TransactionType string `gorm:"type:varchar(50)"` + PartyType string `gorm:"type:varchar(50);not null;index:payments_party_polymorphic,priority:1"` + PartyId uint `gorm:"not null;index:payments_party_polymorphic,priority:2"` + PartyAccountNumber *string `gorm:"type:varchar(50)"` + PaymentDate time.Time `gorm:"not null"` + PaymentMethod string `gorm:"type:varchar(20);not null"` + BankId *uint `gorm:"not null;index:idx_payments_bank_id"` + Direction string `gorm:"type:varchar(5);not null"` + Nominal float64 `gorm:"type:numeric(15,3);not null"` + Notes string `gorm:"type:text;not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + CreatedBy uint `gorm:"index" json:"-"` BankWarehouse Bank `gorm:"foreignKey:BankId;references:Id"` CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` diff --git a/internal/modules/finance/initials/dto/initial.dto.go b/internal/modules/finance/initials/dto/initial.dto.go index 5eb76e9c..1311024f 100644 --- a/internal/modules/finance/initials/dto/initial.dto.go +++ b/internal/modules/finance/initials/dto/initial.dto.go @@ -101,20 +101,25 @@ func ToInitialDetailDTO(e entity.Payment) InitialDetailDTO { func partyFromInitial(e entity.Payment) Party { party := Party{ - Id: e.PartyId, - Type: e.PartyType, + Id: e.PartyId, + Type: e.PartyType, + } + if e.PartyAccountNumber != nil { + party.AccountNumber = *e.PartyAccountNumber } switch utils.PaymentParty(e.PartyType) { case utils.PaymentPartyCustomer: if e.Customer != nil && e.Customer.Id != 0 { party.Name = e.Customer.Name - party.AccountNumber = e.Customer.AccountNumber + if party.AccountNumber == "" { + party.AccountNumber = e.Customer.AccountNumber + } } case utils.PaymentPartySupplier: if e.Supplier != nil && e.Supplier.Id != 0 { party.Name = e.Supplier.Name - if e.Supplier.AccountNumber != nil { + if party.AccountNumber == "" && e.Supplier.AccountNumber != nil { party.AccountNumber = *e.Supplier.AccountNumber } } diff --git a/internal/modules/finance/initials/services/initial.service.go b/internal/modules/finance/initials/services/initial.service.go index 2eb15d3b..e06e99dd 100644 --- a/internal/modules/finance/initials/services/initial.service.go +++ b/internal/modules/finance/initials/services/initial.service.go @@ -120,6 +120,7 @@ func (s *initialService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit TransactionType: string(utils.TransactionTypeSaldoAwal), PartyType: party, PartyId: req.PartyId, + PartyAccountNumber: nil, PaymentDate: time.Now(), PaymentMethod: string(utils.PaymentMethodSaldo), BankId: req.BankId, diff --git a/internal/modules/finance/injections/services/injection.service.go b/internal/modules/finance/injections/services/injection.service.go index 1b1062b4..8cb80e1c 100644 --- a/internal/modules/finance/injections/services/injection.service.go +++ b/internal/modules/finance/injections/services/injection.service.go @@ -106,6 +106,7 @@ func (s *injectionService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent TransactionType: string(utils.TransactionTypeInjection), PartyType: string(utils.PaymentPartyCustomer), PartyId: 0, + PartyAccountNumber: nil, PaymentDate: adjustmentDate, PaymentMethod: string(utils.PaymentMethodSaldo), BankId: req.BankId, diff --git a/internal/modules/finance/injections/validations/injection.validation.go b/internal/modules/finance/injections/validations/injection.validation.go index eb324525..b5b75087 100644 --- a/internal/modules/finance/injections/validations/injection.validation.go +++ b/internal/modules/finance/injections/validations/injection.validation.go @@ -1,17 +1,17 @@ package validation type Create struct { - BankId *uint `json:"bank_id" validate:"required_strict,number,gt=0"` - AdjustmentDate string `json:"adjustment_date" validate:"required_strict"` - Nominal float64 `json:"nominal" validate:"required_strict,gt=0"` - Notes string `json:"notes" validate:"required_strict,max=500"` + BankId *uint `json:"bank_id" validate:"required_strict,number,gt=0"` + AdjustmentDate string `json:"adjustment_date" validate:"required_strict"` + Nominal float64 `json:"nominal" validate:"required_strict,gt=0"` + Notes string `json:"notes" validate:"required_strict,max=500"` } type Update struct { - BankId *uint `json:"bank_id,omitempty" validate:"omitempty,number,gt=0"` - AdjustmentDate *string `json:"adjustment_date,omitempty" validate:"omitempty"` - Nominal *float64 `json:"nominal,omitempty" validate:"omitempty,gt=0"` - Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"` + BankId *uint `json:"bank_id,omitempty" validate:"omitempty,number,gt=0"` + AdjustmentDate *string `json:"adjustment_date,omitempty" validate:"omitempty"` + Nominal *float64 `json:"nominal,omitempty" validate:"omitempty,gt=0"` + Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"` } type Query struct { diff --git a/internal/modules/finance/payments/dto/payment.dto.go b/internal/modules/finance/payments/dto/payment.dto.go index 23005e2d..3fbc8ad4 100644 --- a/internal/modules/finance/payments/dto/payment.dto.go +++ b/internal/modules/finance/payments/dto/payment.dto.go @@ -124,20 +124,25 @@ func ToPaymentDetailDTO(e entity.Payment) PaymentDetailDTO { func partyFromPayment(e entity.Payment) Party { party := Party{ - Id: e.PartyId, - Type: e.PartyType, + Id: e.PartyId, + Type: e.PartyType, + } + if e.PartyAccountNumber != nil { + party.AccountNumber = *e.PartyAccountNumber } switch utils.PaymentParty(e.PartyType) { case utils.PaymentPartyCustomer: if e.Customer != nil && e.Customer.Id != 0 { party.Name = e.Customer.Name - party.AccountNumber = e.Customer.AccountNumber + if party.AccountNumber == "" { + party.AccountNumber = e.Customer.AccountNumber + } } case utils.PaymentPartySupplier: if e.Supplier != nil && e.Supplier.Id != 0 { party.Name = e.Supplier.Name - if e.Supplier.AccountNumber != nil { + if party.AccountNumber == "" && e.Supplier.AccountNumber != nil { party.AccountNumber = *e.Supplier.AccountNumber } } diff --git a/internal/modules/finance/payments/route.go b/internal/modules/finance/payments/route.go index c5147fc0..b00de964 100644 --- a/internal/modules/finance/payments/route.go +++ b/internal/modules/finance/payments/route.go @@ -13,9 +13,9 @@ func PaymentRoutes(v1 fiber.Router, u user.UserService, s payment.PaymentService ctrl := controller.NewPaymentController(s) route := v1.Group("/payments") - route.Use(m.Auth(u)) + // route.Use(m.Auth(u)) - route.Post("/",m.RequirePermissions(m.P_Finances_Payments_CreateOne), ctrl.CreateOne) - route.Get("/:id",m.RequirePermissions(m.P_Finances_Payments_GetOne), ctrl.GetOne) - route.Patch("/:id",m.RequirePermissions(m.P_Finances_Payments_UpdateOne), ctrl.UpdateOne) + route.Post("/", ctrl.CreateOne) + route.Get("/:id", m.RequirePermissions(m.P_Finances_Payments_GetOne), ctrl.GetOne) + route.Patch("/:id", m.RequirePermissions(m.P_Finances_Payments_UpdateOne), ctrl.UpdateOne) } diff --git a/internal/modules/finance/payments/services/payment.service.go b/internal/modules/finance/payments/services/payment.service.go index 356288f1..8860f3f4 100644 --- a/internal/modules/finance/payments/services/payment.service.go +++ b/internal/modules/finance/payments/services/payment.service.go @@ -121,18 +121,19 @@ func (s *paymentService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit } createBody := &entity.Payment{ - PaymentCode: code, - ReferenceNumber: req.ReferenceNumber, - TransactionType: transactionType, - PartyType: party, - PartyId: req.PartyId, - PaymentDate: paymentDate, - PaymentMethod: method, - BankId: req.BankId, - Direction: directionForParty(party), - Nominal: req.Nominal, - Notes: req.Notes, - CreatedBy: actorID, + PaymentCode: code, + ReferenceNumber: req.ReferenceNumber, + TransactionType: transactionType, + PartyType: party, + PartyId: req.PartyId, + PartyAccountNumber: req.PartyAccountNumber, + PaymentDate: paymentDate, + PaymentMethod: method, + BankId: req.BankId, + Direction: directionForParty(party), + Nominal: req.Nominal, + Notes: req.Notes, + CreatedBy: actorID, } err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { @@ -188,6 +189,9 @@ func (s paymentService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) if req.ReferenceNumber != nil { updateBody["reference_number"] = *req.ReferenceNumber } + if req.PartyAccountNumber != nil { + updateBody["party_account_number"] = *req.PartyAccountNumber + } if req.PaymentMethod != nil { method, err := normalizePaymentMethod(*req.PaymentMethod) if err != nil { diff --git a/internal/modules/finance/payments/validations/payment.validation.go b/internal/modules/finance/payments/validations/payment.validation.go index 14c8f151..a2ab9950 100644 --- a/internal/modules/finance/payments/validations/payment.validation.go +++ b/internal/modules/finance/payments/validations/payment.validation.go @@ -1,25 +1,27 @@ package validation type Create struct { - PartyType string `json:"party_type" validate:"required_strict,min=1,max=50"` - PartyId uint `json:"party_id" validate:"required_strict,number,gt=0"` - PaymentDate string `json:"payment_date" validate:"required_strict,datetime=2006-01-02"` - Nominal float64 `json:"nominal" validate:"required_strict"` - ReferenceNumber *string `json:"reference_number,omitempty"` - PaymentMethod string `json:"payment_method" validate:"required_strict,max=20"` - BankId *uint `json:"bank_id" validate:"omitempty,number,gt=0"` - Notes string `json:"notes" validate:"required_strict,max=500"` + PartyType string `json:"party_type" validate:"required_strict,min=1,max=50"` + PartyId uint `json:"party_id" validate:"required_strict,number,gt=0"` + PartyAccountNumber *string `json:"party_account_number"` + PaymentDate string `json:"payment_date" validate:"required_strict,datetime=2006-01-02"` + Nominal float64 `json:"nominal" validate:"required_strict"` + ReferenceNumber *string `json:"reference_number,omitempty"` + PaymentMethod string `json:"payment_method" validate:"required_strict,max=20"` + BankId *uint `json:"bank_id" validate:"omitempty,number,gt=0"` + Notes string `json:"notes" validate:"required_strict,max=500"` } type Update struct { - PartyType *string `json:"party_type,omitempty" validate:"omitempty,max=50"` - PartyId *uint `json:"party_id,omitempty" validate:"omitempty,number,gt=0"` - PaymentDate *string `json:"payment_date,omitempty" validate:"omitempty,datetime=2006-01-02"` - Nominal *float64 `json:"nominal,omitempty" validate:"omitempty,gt=0"` - ReferenceNumber *string `json:"reference_number,omitempty"` - PaymentMethod *string `json:"payment_method,omitempty" validate:"omitempty,max=20"` - BankId *uint `json:"bank_id,omitempty" validate:"omitempty,number,gt=0"` - Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"` + PartyType *string `json:"party_type,omitempty" validate:"omitempty,max=50"` + PartyId *uint `json:"party_id,omitempty" validate:"omitempty,number,gt=0"` + PartyAccountNumber *string `json:"party_account_number,omitempty"` + PaymentDate *string `json:"payment_date,omitempty" validate:"omitempty,datetime=2006-01-02"` + Nominal *float64 `json:"nominal,omitempty" validate:"omitempty,gt=0"` + ReferenceNumber *string `json:"reference_number,omitempty"` + PaymentMethod *string `json:"payment_method,omitempty" validate:"omitempty,max=20"` + BankId *uint `json:"bank_id,omitempty" validate:"omitempty,number,gt=0"` + Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"` } type Query struct { diff --git a/internal/modules/finance/transactions/dto/transaction.dto.go b/internal/modules/finance/transactions/dto/transaction.dto.go index 25740344..07703fce 100644 --- a/internal/modules/finance/transactions/dto/transaction.dto.go +++ b/internal/modules/finance/transactions/dto/transaction.dto.go @@ -124,20 +124,25 @@ func ToTransactionDetailDTO(e entity.Payment) TransactionDetailDTO { func partyFromPayment(e entity.Payment) Party { party := Party{ - Id: e.PartyId, - Type: e.PartyType, + Id: e.PartyId, + Type: e.PartyType, + } + if e.PartyAccountNumber != nil { + party.AccountNumber = *e.PartyAccountNumber } switch utils.PaymentParty(e.PartyType) { case utils.PaymentPartyCustomer: if e.Customer != nil && e.Customer.Id != 0 { party.Name = e.Customer.Name - party.AccountNumber = e.Customer.AccountNumber + if party.AccountNumber == "" { + party.AccountNumber = e.Customer.AccountNumber + } } case utils.PaymentPartySupplier: if e.Supplier != nil && e.Supplier.Id != 0 { party.Name = e.Supplier.Name - if e.Supplier.AccountNumber != nil { + if party.AccountNumber == "" && e.Supplier.AccountNumber != nil { party.AccountNumber = *e.Supplier.AccountNumber } } From 17d55bd2c0b4b247be1b0e7f48e2798ccb7793e0 Mon Sep 17 00:00:00 2001 From: "Hafizh A. Y" Date: Fri, 9 Jan 2026 16:43:20 +0700 Subject: [PATCH 15/19] fix(BE): fix code 500 when delete master data foreign to others table --- ...20260109093155_drop_unused_tables.down.sql | 1 + .../20260109093155_drop_unused_tables.up.sql | 1 + ...32_update_master_data_fk_restrict.down.sql | 20 ++++++++++ ...3832_update_master_data_fk_restrict.up.sql | 20 ++++++++++ internal/utils/error.go | 38 +++++++++++++++++++ 5 files changed, 80 insertions(+) create mode 100644 internal/database/migrations/20260109093155_drop_unused_tables.down.sql create mode 100644 internal/database/migrations/20260109093155_drop_unused_tables.up.sql create mode 100644 internal/database/migrations/20260109093832_update_master_data_fk_restrict.down.sql create mode 100644 internal/database/migrations/20260109093832_update_master_data_fk_restrict.up.sql diff --git a/internal/database/migrations/20260109093155_drop_unused_tables.down.sql b/internal/database/migrations/20260109093155_drop_unused_tables.down.sql new file mode 100644 index 00000000..f17c3a80 --- /dev/null +++ b/internal/database/migrations/20260109093155_drop_unused_tables.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS projects; diff --git a/internal/database/migrations/20260109093155_drop_unused_tables.up.sql b/internal/database/migrations/20260109093155_drop_unused_tables.up.sql new file mode 100644 index 00000000..f17c3a80 --- /dev/null +++ b/internal/database/migrations/20260109093155_drop_unused_tables.up.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS projects; diff --git a/internal/database/migrations/20260109093832_update_master_data_fk_restrict.down.sql b/internal/database/migrations/20260109093832_update_master_data_fk_restrict.down.sql new file mode 100644 index 00000000..bc43de5c --- /dev/null +++ b/internal/database/migrations/20260109093832_update_master_data_fk_restrict.down.sql @@ -0,0 +1,20 @@ +-- Revert master data foreign keys to CASCADE delete (except FCR) +ALTER TABLE nonstock_suppliers + DROP CONSTRAINT IF EXISTS nonstock_suppliers_nonstock_id_fkey, + DROP CONSTRAINT IF EXISTS nonstock_suppliers_supplier_id_fkey; + +ALTER TABLE nonstock_suppliers + ADD CONSTRAINT nonstock_suppliers_nonstock_id_fkey FOREIGN KEY (nonstock_id) + REFERENCES nonstocks (id) ON DELETE CASCADE ON UPDATE CASCADE, + ADD CONSTRAINT nonstock_suppliers_supplier_id_fkey FOREIGN KEY (supplier_id) + REFERENCES suppliers (id) ON DELETE CASCADE ON UPDATE CASCADE; + +ALTER TABLE product_suppliers + DROP CONSTRAINT IF EXISTS product_suppliers_product_id_fkey, + DROP CONSTRAINT IF EXISTS product_suppliers_supplier_id_fkey; + +ALTER TABLE product_suppliers + ADD CONSTRAINT product_suppliers_product_id_fkey FOREIGN KEY (product_id) + REFERENCES products (id) ON DELETE CASCADE ON UPDATE CASCADE, + ADD CONSTRAINT product_suppliers_supplier_id_fkey FOREIGN KEY (supplier_id) + REFERENCES suppliers (id) ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/internal/database/migrations/20260109093832_update_master_data_fk_restrict.up.sql b/internal/database/migrations/20260109093832_update_master_data_fk_restrict.up.sql new file mode 100644 index 00000000..dbb45637 --- /dev/null +++ b/internal/database/migrations/20260109093832_update_master_data_fk_restrict.up.sql @@ -0,0 +1,20 @@ +-- Update master data foreign keys to RESTRICT delete (except FCR) +ALTER TABLE nonstock_suppliers + DROP CONSTRAINT IF EXISTS nonstock_suppliers_nonstock_id_fkey, + DROP CONSTRAINT IF EXISTS nonstock_suppliers_supplier_id_fkey; + +ALTER TABLE nonstock_suppliers + ADD CONSTRAINT nonstock_suppliers_nonstock_id_fkey FOREIGN KEY (nonstock_id) + REFERENCES nonstocks (id) ON DELETE RESTRICT ON UPDATE CASCADE, + ADD CONSTRAINT nonstock_suppliers_supplier_id_fkey FOREIGN KEY (supplier_id) + REFERENCES suppliers (id) ON DELETE RESTRICT ON UPDATE CASCADE; + +ALTER TABLE product_suppliers + DROP CONSTRAINT IF EXISTS product_suppliers_product_id_fkey, + DROP CONSTRAINT IF EXISTS product_suppliers_supplier_id_fkey; + +ALTER TABLE product_suppliers + ADD CONSTRAINT product_suppliers_product_id_fkey FOREIGN KEY (product_id) + REFERENCES products (id) ON DELETE RESTRICT ON UPDATE CASCADE, + ADD CONSTRAINT product_suppliers_supplier_id_fkey FOREIGN KEY (supplier_id) + REFERENCES suppliers (id) ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/internal/utils/error.go b/internal/utils/error.go index ead06aeb..66537c00 100644 --- a/internal/utils/error.go +++ b/internal/utils/error.go @@ -2,11 +2,14 @@ package utils import ( "errors" + "strings" "gitlab.com/mbugroup/lti-api.git/internal/common/validation" "gitlab.com/mbugroup/lti-api.git/internal/response" "github.com/gofiber/fiber/v2" + "github.com/jackc/pgconn" + pgconnv5 "github.com/jackc/pgx/v5/pgconn" ) func ErrorHandler(c *fiber.Ctx, err error) error { @@ -14,6 +17,10 @@ func ErrorHandler(c *fiber.Ctx, err error) error { return response.Error(c, fiber.StatusBadRequest, message, nil) } + if statusCode, message := mapPgError(err); statusCode != 0 { + return response.Error(c, statusCode, message, nil) + } + var fiberErr *fiber.Error if errors.As(err, &fiberErr) { return response.Error(c, fiberErr.Code, fiberErr.Message, nil) @@ -26,6 +33,37 @@ func NotFoundHandler(c *fiber.Ctx) error { return response.Error(c, fiber.StatusNotFound, "Endpoint Not Found", nil) } +func mapPgError(err error) (int, string) { + code, message := getPgErrorDetails(err) + if code == "" { + return 0, "" + } + + switch code { + case "23503": + return fiber.StatusConflict, "Data tidak bisa dihapus karena masih digunakan oleh data lain." + case "P0001": + if strings.HasPrefix(message, "Cannot soft delete") { + return fiber.StatusConflict, "Data tidak bisa dihapus karena masih digunakan oleh data lain." + } + } + + return 0, "" +} + +func getPgErrorDetails(err error) (string, string) { + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) { + return pgErr.Code, pgErr.Message + } + + var pgErrV5 *pgconnv5.PgError + if errors.As(err, &pgErrV5) { + return pgErrV5.Code, pgErrV5.Message + } + + return "", "" +} func BadRequest(msg string) error { return fiber.NewError(fiber.StatusBadRequest, msg) From bc0bf7fe16bbcb91da187cc865196a8d4d8c1afc Mon Sep 17 00:00:00 2001 From: "Hafizh A. Y" Date: Sat, 10 Jan 2026 18:25:04 +0700 Subject: [PATCH 16/19] fix(BE): not showed supplier in master data product --- .../master/products/dto/product.dto.go | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/internal/modules/master/products/dto/product.dto.go b/internal/modules/master/products/dto/product.dto.go index dfd4c86f..59f57034 100644 --- a/internal/modules/master/products/dto/product.dto.go +++ b/internal/modules/master/products/dto/product.dto.go @@ -5,6 +5,7 @@ import ( entity "gitlab.com/mbugroup/lti-api.git/internal/entities" productCategoryDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/dto" + supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto" uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" ) @@ -19,6 +20,7 @@ type ProductRelationDTO struct { Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"` Flags *[]string `json:"flags,omitempty"` ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"` + Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers"` } type ProductListDTO struct { @@ -33,6 +35,7 @@ type ProductListDTO struct { Flags []string `json:"flags"` Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"` ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"` + Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers"` CreatedUser *userDTO.UserRelationDTO `json:"created_user"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` @@ -70,6 +73,7 @@ func ToProductRelationDTO(e entity.Product) ProductRelationDTO { Flags: &flags, Uom: uomRef, ProductCategory: categoryRef, + Suppliers: toProductSupplierDTOs(e.ProductSuppliers), } } @@ -112,6 +116,7 @@ func ToProductListDTO(e entity.Product) ProductListDTO { UpdatedAt: e.UpdatedAt, CreatedUser: createdUser, ProductCategory: categoryRef, + Suppliers: toProductSupplierDTOs(e.ProductSuppliers), } } @@ -128,3 +133,23 @@ func ToProductDetailDTO(e entity.Product) ProductDetailDTO { ProductListDTO: ToProductListDTO(e), } } + +func toProductSupplierDTOs(relations []entity.ProductSupplier) []supplierDTO.SupplierRelationDTO { + if len(relations) == 0 { + return make([]supplierDTO.SupplierRelationDTO, 0) + } + + result := make([]supplierDTO.SupplierRelationDTO, 0, len(relations)) + for _, relation := range relations { + if relation.Supplier.Id == 0 { + continue + } + result = append(result, supplierDTO.ToSupplierRelationDTO(relation.Supplier)) + } + + if len(result) == 0 { + return make([]supplierDTO.SupplierRelationDTO, 0) + } + + return result +} From 3b2c6f16c339748f9ec3a4560503879d00a42b16 Mon Sep 17 00:00:00 2001 From: ragilap Date: Sat, 10 Jan 2026 21:22:54 +0700 Subject: [PATCH 17/19] feat(BE-74-76-78-278):adjustment project flock,recording,purchase getall --- .../20260110105231_adjust_recording.down.sql | 59 +++++++++++++ .../20260110105231_adjust_recording.up.sql | 57 ++++++++++++ internal/entities/recording.go | 12 +-- .../product_warehouse.repository.go | 36 ++++++++ .../services/projectflock.service.go | 86 ++++++++++--------- .../recordings/dto/recording.dto.go | 42 ++++----- .../modules/production/recordings/route.go | 2 +- .../recordings/services/recording.service.go | 54 ++++++------ .../repositories/purchase.repository.go | 39 +++++++++ .../purchases/services/purchase.service.go | 1 + 10 files changed, 294 insertions(+), 94 deletions(-) create mode 100644 internal/database/migrations/20260110105231_adjust_recording.down.sql create mode 100644 internal/database/migrations/20260110105231_adjust_recording.up.sql diff --git a/internal/database/migrations/20260110105231_adjust_recording.down.sql b/internal/database/migrations/20260110105231_adjust_recording.down.sql new file mode 100644 index 00000000..2bcbcb67 --- /dev/null +++ b/internal/database/migrations/20260110105231_adjust_recording.down.sql @@ -0,0 +1,59 @@ +BEGIN; + +ALTER TABLE recordings + DROP CONSTRAINT IF EXISTS chk_recordings_nonnegatives_v4; + +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_name = 'recordings' AND column_name = 'hen_day' + ) THEN + ALTER TABLE recordings RENAME COLUMN hen_day TO hand_day; + END IF; +END $$; + +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_name = 'recordings' AND column_name = 'hen_house' + ) THEN + ALTER TABLE recordings RENAME COLUMN hen_house TO hand_house; + END IF; +END $$; + +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_name = 'recordings' AND column_name = 'egg_mass' + ) THEN + ALTER TABLE recordings RENAME COLUMN egg_mass TO egg_mesh; + END IF; +END $$; + +ALTER TABLE recordings + ADD COLUMN IF NOT EXISTS daily_gain NUMERIC(7,3), + ADD COLUMN IF NOT EXISTS avg_daily_gain NUMERIC(7,3); + +ALTER TABLE recordings + ADD CONSTRAINT chk_recordings_nonnegatives_v3 CHECK ( + (total_depletion_qty IS NULL OR total_depletion_qty >= 0) AND + (cum_depletion_rate IS NULL OR cum_depletion_rate >= 0) AND + (daily_gain IS NULL OR daily_gain >= 0) AND + (avg_daily_gain IS NULL OR avg_daily_gain >= 0) AND + (cum_intake IS NULL OR cum_intake >= 0) AND + (fcr_value IS NULL OR fcr_value >= 0) AND + (total_chick_qty IS NULL OR total_chick_qty >= 0) AND + (hand_day IS NULL OR hand_day >= 0) AND + (hand_house IS NULL OR hand_house >= 0) AND + (feed_intake IS NULL OR feed_intake >= 0) AND + (egg_mesh IS NULL OR egg_mesh >= 0) AND + (egg_weight IS NULL OR egg_weight >= 0) + ); + +COMMIT; diff --git a/internal/database/migrations/20260110105231_adjust_recording.up.sql b/internal/database/migrations/20260110105231_adjust_recording.up.sql new file mode 100644 index 00000000..ac947910 --- /dev/null +++ b/internal/database/migrations/20260110105231_adjust_recording.up.sql @@ -0,0 +1,57 @@ +BEGIN; + +ALTER TABLE recordings + DROP CONSTRAINT IF EXISTS chk_recordings_nonnegatives_v3; + +ALTER TABLE recordings + DROP COLUMN IF EXISTS daily_gain, + DROP COLUMN IF EXISTS avg_daily_gain; + +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_name = 'recordings' AND column_name = 'hand_day' + ) THEN + ALTER TABLE recordings RENAME COLUMN hand_day TO hen_day; + END IF; +END $$; + +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_name = 'recordings' AND column_name = 'hand_house' + ) THEN + ALTER TABLE recordings RENAME COLUMN hand_house TO hen_house; + END IF; +END $$; + +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_name = 'recordings' AND column_name = 'egg_mesh' + ) THEN + ALTER TABLE recordings RENAME COLUMN egg_mesh TO egg_mass; + END IF; +END $$; + +ALTER TABLE recordings + ADD CONSTRAINT chk_recordings_nonnegatives_v4 CHECK ( + (total_depletion_qty IS NULL OR total_depletion_qty >= 0) AND + (cum_depletion_rate IS NULL OR cum_depletion_rate >= 0) AND + (cum_intake IS NULL OR cum_intake >= 0) AND + (fcr_value IS NULL OR fcr_value >= 0) AND + (total_chick_qty IS NULL OR total_chick_qty >= 0) AND + (hen_day IS NULL OR hen_day >= 0) AND + (hen_house IS NULL OR hen_house >= 0) AND + (feed_intake IS NULL OR feed_intake >= 0) AND + (egg_mass IS NULL OR egg_mass >= 0) AND + (egg_weight IS NULL OR egg_weight >= 0) + ); + +COMMIT; diff --git a/internal/entities/recording.go b/internal/entities/recording.go index 7f952a62..0cc5dc03 100644 --- a/internal/entities/recording.go +++ b/internal/entities/recording.go @@ -16,10 +16,10 @@ type Recording struct { CumIntake *int `gorm:"column:cum_intake"` FcrValue *float64 `gorm:"column:fcr_value"` TotalChickQty *float64 `gorm:"column:total_chick_qty"` - HandDay *float64 `gorm:"column:hand_day"` - HandHouse *float64 `gorm:"column:hand_house"` + HenDay *float64 `gorm:"column:hen_day"` + HenHouse *float64 `gorm:"column:hen_house"` FeedIntake *float64 `gorm:"column:feed_intake"` - EggMesh *float64 `gorm:"column:egg_mesh"` + EggMass *float64 `gorm:"column:egg_mass"` EggWeight *float64 `gorm:"column:egg_weight"` CreatedBy uint `gorm:"column:created_by"` CreatedAt time.Time `gorm:"autoCreateTime"` @@ -34,11 +34,11 @@ type Recording struct { LatestApproval *Approval `gorm:"-" json:"-"` - StandardHandDay *float64 `gorm:"-"` - StandardHandHouse *float64 `gorm:"-"` + StandardHenDay *float64 `gorm:"-"` + StandardHenHouse *float64 `gorm:"-"` StandardFeedIntake *float64 `gorm:"-"` StandardMaxDepletion *float64 `gorm:"-"` - StandardEggMesh *float64 `gorm:"-"` + StandardEggMass *float64 `gorm:"-"` StandardEggWeight *float64 `gorm:"-"` StandardFcr *float64 `gorm:"-"` } diff --git a/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go b/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go index a8a44eb7..6acb4f69 100644 --- a/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go +++ b/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go @@ -23,6 +23,7 @@ type ProductWarehouseRepository interface { GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint, db *gorm.DB) (*entity.ProductWarehouse, error) GetByFlagAndWarehouseID(ctx context.Context, flagName string, warehouseId uint) ([]entity.ProductWarehouse, error) GetFirstProductByFlag(ctx context.Context, flagName string) (*entity.Product, error) + ListProductIDsByFlagPrefixes(ctx context.Context, prefixes []string, visibleStatus *bool) ([]uint, error) ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error) @@ -380,3 +381,38 @@ func (r *ProductWarehouseRepositoryImpl) GetFirstProductByFlag(ctx context.Conte } return &product, nil } + +func (r *ProductWarehouseRepositoryImpl) ListProductIDsByFlagPrefixes(ctx context.Context, prefixes []string, visibleStatus *bool) ([]uint, error) { + if len(prefixes) == 0 { + return []uint{}, nil + } + + db := r.DB().WithContext(ctx). + Model(&entity.Product{}). + Distinct("products.id"). + Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ?", entity.FlagableTypeProduct) + + applied := false + for _, prefix := range prefixes { + if prefix == "" { + continue + } + like := prefix + "%" + if !applied { + db = db.Where("flags.name LIKE ?", like) + applied = true + continue + } + db = db.Or("flags.name LIKE ?", like) + } + + if visibleStatus != nil { + db = db.Where("products.is_visible = ?", *visibleStatus) + } + + var ids []uint + if err := db.Pluck("products.id", &ids).Error; err != nil { + return nil, err + } + return ids, nil +} diff --git a/internal/modules/production/project_flocks/services/projectflock.service.go b/internal/modules/production/project_flocks/services/projectflock.service.go index 5f643dee..3dbe3f4b 100644 --- a/internal/modules/production/project_flocks/services/projectflock.service.go +++ b/internal/modules/production/project_flocks/services/projectflock.service.go @@ -23,6 +23,7 @@ import ( validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations" recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" uniformityRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/repositories" + purchaseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories" utils "gitlab.com/mbugroup/lti-api.git/internal/utils" approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" @@ -308,12 +309,12 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (* } createBody := &entity.ProjectFlock{ - AreaId: req.AreaId, - Category: cat, - FcrId: req.FcrId, + AreaId: req.AreaId, + Category: cat, + FcrId: req.FcrId, ProductionStandardId: req.ProductionStandardId, - LocationId: req.LocationId, - CreatedBy: actorID, + LocationId: req.LocationId, + CreatedBy: actorID, } err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { @@ -823,22 +824,7 @@ func (s projectflockService) detachKandangs(ctx context.Context, dbTransaction * return nil } - blocked, err := s.pivotRepoWithTx(dbTransaction).FindKandangsWithRecordings(ctx, projectFlockID, kandangIDs) - if err != nil { - s.Log.Errorf("Failed to check recordings before detaching kandangs: %+v", err) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandang detachment") - } - if len(blocked) > 0 { - names := make([]string, 0, len(blocked)) - for _, item := range blocked { - label := fmt.Sprintf("ID %d", item.Id) - if strings.TrimSpace(item.Name) != "" { - label = fmt.Sprintf("%s (%s)", label, item.Name) - } - names = append(names, label) - } - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Tidak dapat melepas kandang karena sudah memiliki recording: %s", strings.Join(names, ", "))) - } + // NOTE: Recording constraints are enforced via FK cascade; allow detachment even if recordings exist. pfkIDs, err := s.pivotRepoWithTx(dbTransaction).ListIDsByProjectAndKandang(ctx, projectFlockID, kandangIDs) if err != nil { @@ -854,6 +840,14 @@ func (s projectflockService) detachKandangs(ctx context.Context, dbTransaction * return fiber.NewError(fiber.StatusInternalServerError, "Failed to remove uniformity data for project flock kandang") } + db := s.Repository.DB() + if dbTransaction != nil { + db = dbTransaction + } + purchaseRepo := purchaseRepository.NewPurchaseRepository(db) + if err := purchaseRepo.SoftDeleteByProjectFlockKandangIDs(ctx, pfkIDs); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to soft delete purchases for project flock kandang") + } pwRepo := s.ProductWarehouseRepo if dbTransaction != nil { pwRepo = productWarehouseRepository.NewProductWarehouseRepository(dbTransaction) @@ -906,6 +900,11 @@ func (s projectflockService) ensureProjectFlockKandangProductWarehouses(ctx cont return nil } + projectFlockID := records[0].ProjectFlockId + if projectFlockID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Project flock id tidak ditemukan") + } + pwRepo := s.ProductWarehouseRepo if dbTransaction != nil { pwRepo = productWarehouseRepository.NewProductWarehouseRepository(dbTransaction) @@ -920,24 +919,34 @@ func (s projectflockService) ensureProjectFlockKandangProductWarehouses(ctx cont warehouseRepo = warehouseRepository.NewWarehouseRepository(s.Repository.DB()) } - flags := []utils.FlagType{ - utils.FlagAyamAfkir, - utils.FlagAyamCulling, - utils.FlagAyamMati, - utils.FlagTelurPecah, - utils.FlagTelurUtuh, + db := s.Repository.DB() + if dbTransaction != nil { + db = dbTransaction + } + var category string + if err := db.WithContext(ctx). + Model(&entity.ProjectFlock{}). + Select("category"). + Where("id = ?", projectFlockID). + Scan(&category).Error; err != nil { + return err + } + if strings.TrimSpace(category) == "" { + return fiber.NewError(fiber.StatusBadRequest, "Project flock category tidak ditemukan") } - productIDs := make(map[utils.FlagType]uint, len(flags)) - for _, flag := range flags { - product, err := pwRepo.GetFirstProductByFlag(ctx, string(flag)) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product untuk flag %s tidak ditemukan", flag)) - } - return err - } - productIDs[flag] = product.Id + prefixes := []string{"AYAM-"} + if strings.EqualFold(category, string(utils.ProjectFlockCategoryLaying)) { + prefixes = append(prefixes, "TELUR") + } + + invisibleOnly := false + productIDs, err := pwRepo.ListProductIDsByFlagPrefixes(ctx, prefixes, &invisibleOnly) + if err != nil { + return err + } + if len(productIDs) == 0 { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product dengan flag %s tidak ditemukan", strings.Join(prefixes, ", "))) } for _, record := range records { @@ -953,8 +962,7 @@ func (s projectflockService) ensureProjectFlockKandangProductWarehouses(ctx cont return err } - for _, flag := range flags { - productID := productIDs[flag] + for _, productID := range productIDs { if _, err := pwRepo.GetByProductWarehouseAndProjectFlockKandang(ctx, productID, warehouse.Id, record.Id); err == nil { continue } else if !errors.Is(err, gorm.ErrRecordNotFound) { diff --git a/internal/modules/production/recordings/dto/recording.dto.go b/internal/modules/production/recordings/dto/recording.dto.go index c34651ba..f5a04821 100644 --- a/internal/modules/production/recordings/dto/recording.dto.go +++ b/internal/modules/production/recordings/dto/recording.dto.go @@ -25,16 +25,16 @@ type RecordingRelationDTO struct { CumIntake int `json:"cum_intake"` FcrValue float64 `json:"fcr_value"` TotalChickQty float64 `json:"total_chick_qty"` - HandDay float64 `json:"hand_day"` - HandHouse float64 `json:"hand_house"` + HenDay float64 `json:"hen_day"` + HenHouse float64 `json:"hen_house"` FeedIntake float64 `json:"feed_intake"` - EggMesh float64 `json:"egg_mesh"` + EggMass float64 `json:"egg_mass"` EggWeight float64 `json:"egg_weight"` - StandardHandDay *float64 `json:"hand_day_std,omitempty"` - StandardHandHouse *float64 `json:"hand_house_std,omitempty"` + StandardHenDay *float64 `json:"hen_day_std,omitempty"` + StandardHenHouse *float64 `json:"hen_house_std,omitempty"` StandardFeedIntake *float64 `json:"feed_intake_std,omitempty"` StandardMaxDepletion *float64 `json:"max_depletion_std,omitempty"` - StandardEggMesh *float64 `json:"egg_mesh_std,omitempty"` + StandardEggMass *float64 `json:"egg_mass_std,omitempty"` StandardEggWeight *float64 `json:"egg_weight_std,omitempty"` StandardFcr *float64 `json:"fcr_std,omitempty"` Approval approvalDTO.ApprovalRelationDTO `json:"approval"` @@ -94,10 +94,10 @@ func ToRecordingRelationDTO(e entity.Recording) RecordingRelationDTO { cumIntake int fcrValue float64 totalChickQty float64 - handDay float64 - handHouse float64 + henDay float64 + henHouse float64 feedIntake float64 - eggMesh float64 + eggMass float64 eggWeight float64 ) @@ -119,17 +119,17 @@ func ToRecordingRelationDTO(e entity.Recording) RecordingRelationDTO { if e.TotalChickQty != nil { totalChickQty = *e.TotalChickQty } - if e.HandDay != nil { - handDay = *e.HandDay + if e.HenDay != nil { + henDay = *e.HenDay } - if e.HandHouse != nil { - handHouse = *e.HandHouse + if e.HenHouse != nil { + henHouse = *e.HenHouse } if e.FeedIntake != nil { feedIntake = *e.FeedIntake } - if e.EggMesh != nil { - eggMesh = *e.EggMesh + if e.EggMass != nil { + eggMass = *e.EggMass } if e.EggWeight != nil { eggWeight = *e.EggWeight @@ -157,16 +157,16 @@ func ToRecordingRelationDTO(e entity.Recording) RecordingRelationDTO { CumIntake: cumIntake, FcrValue: fcrValue, TotalChickQty: totalChickQty, - HandDay: handDay, - HandHouse: handHouse, + HenDay: henDay, + HenHouse: henHouse, FeedIntake: feedIntake, - EggMesh: eggMesh, + EggMass: eggMass, EggWeight: eggWeight, - StandardHandDay: e.StandardHandDay, - StandardHandHouse: e.StandardHandHouse, + StandardHenDay: e.StandardHenDay, + StandardHenHouse: e.StandardHenHouse, StandardFeedIntake: e.StandardFeedIntake, StandardMaxDepletion: e.StandardMaxDepletion, - StandardEggMesh: e.StandardEggMesh, + StandardEggMass: e.StandardEggMass, StandardEggWeight: e.StandardEggWeight, StandardFcr: e.StandardFcr, Approval: latestApproval, diff --git a/internal/modules/production/recordings/route.go b/internal/modules/production/recordings/route.go index f05d054d..e7f1b081 100644 --- a/internal/modules/production/recordings/route.go +++ b/internal/modules/production/recordings/route.go @@ -16,10 +16,10 @@ func RecordingRoutes(v1 fiber.Router, u user.UserService, s recording.RecordingS route.Use(m.Auth(u)) route.Get("/",m.RequirePermissions(m.P_RecordingGetAll), ctrl.GetAll) + route.Get("/next-day",m.RequirePermissions(m.P_RecordingNextDay), ctrl.GetNextDay) route.Get("/:id",m.RequirePermissions(m.P_RecordingGetOne), ctrl.GetOne) route.Post("/",m.RequirePermissions(m.P_RecordingCreateOne), ctrl.CreateOne) route.Patch("/:id",m.RequirePermissions(m.P_RecordingUpdateOne), ctrl.UpdateOne) route.Delete("/:id",m.RequirePermissions(m.P_RecordingDeleteOne), ctrl.DeleteOne) - route.Get("/next-day",m.RequirePermissions(m.P_RecordingNextDay), ctrl.GetNextDay) route.Post("/approvals",m.RequirePermissions(m.P_RecordingApproval), ctrl.Approve) } diff --git a/internal/modules/production/recordings/services/recording.service.go b/internal/modules/production/recordings/services/recording.service.go index 54052518..e46056e5 100644 --- a/internal/modules/production/recordings/services/recording.service.go +++ b/internal/modules/production/recordings/services/recording.service.go @@ -1156,34 +1156,34 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm recording.FeedIntake = nil } - var handDay float64 + var henDay float64 if remainingChick > 0 && totalEggQty >= 0 { - handDay = (totalEggQty / remainingChick) * 100 - updates["hand_day"] = handDay - recording.HandDay = &handDay + henDay = (totalEggQty / remainingChick) * 100 + updates["hen_day"] = henDay + recording.HenDay = &henDay } else { - updates["hand_day"] = gorm.Expr("NULL") - recording.HandDay = nil + updates["hen_day"] = gorm.Expr("NULL") + recording.HenDay = nil } - var handHouse float64 + var henHouse float64 if initialChickin > 0 && cumulativeEggQty >= 0 { - handHouse = cumulativeEggQty / initialChickin - updates["hand_house"] = handHouse - recording.HandHouse = &handHouse + henHouse = cumulativeEggQty / initialChickin + updates["hen_house"] = henHouse + recording.HenHouse = &henHouse } else { - updates["hand_house"] = gorm.Expr("NULL") - recording.HandHouse = nil + updates["hen_house"] = gorm.Expr("NULL") + recording.HenHouse = nil } - var eggMesh float64 + var eggMass float64 if remainingChick > 0 && totalEggWeightGrams > 0 { - eggMesh = (totalEggWeightGrams / remainingChick) * 1000 - updates["egg_mesh"] = eggMesh - recording.EggMesh = &eggMesh + eggMass = (totalEggWeightGrams / remainingChick) * 1000 + updates["egg_mass"] = eggMass + recording.EggMass = &eggMass } else { - updates["egg_mesh"] = gorm.Expr("NULL") - recording.EggMesh = nil + updates["egg_mass"] = gorm.Expr("NULL") + recording.EggMass = nil } var eggWeight float64 @@ -1334,11 +1334,11 @@ func (s *recordingService) attachLatestApproval(ctx context.Context, item *entit } type productionStandardValues struct { - HandDay *float64 - HandHouse *float64 + HenDay *float64 + HenHouse *float64 FeedIntake *float64 MaxDepletion *float64 - EggMesh *float64 + EggMass *float64 EggWeight *float64 } @@ -1389,10 +1389,10 @@ func (s *recordingService) attachProductionStandard(ctx context.Context, item *e return err } if detail != nil { - standard.HandDay = detail.TargetHenDayProduction - standard.HandHouse = detail.TargetHenHouseProduction + standard.HenDay = detail.TargetHenDayProduction + standard.HenHouse = detail.TargetHenHouseProduction standard.EggWeight = detail.TargetEggWeight - standard.EggMesh = detail.TargetEggMass + standard.EggMass = detail.TargetEggMass } } @@ -1420,11 +1420,11 @@ func (s *recordingService) attachProductionStandard(ctx context.Context, item *e } } - item.StandardHandDay = standard.HandDay - item.StandardHandHouse = standard.HandHouse + item.StandardHenDay = standard.HenDay + item.StandardHenHouse = standard.HenHouse item.StandardFeedIntake = standard.FeedIntake item.StandardMaxDepletion = standard.MaxDepletion - item.StandardEggMesh = standard.EggMesh + item.StandardEggMass = standard.EggMass item.StandardEggWeight = standard.EggWeight item.StandardFcr = standardFcr diff --git a/internal/modules/purchases/repositories/purchase.repository.go b/internal/modules/purchases/repositories/purchase.repository.go index fc599877..f6e48aeb 100644 --- a/internal/modules/purchases/repositories/purchase.repository.go +++ b/internal/modules/purchases/repositories/purchase.repository.go @@ -25,6 +25,7 @@ type PurchaseRepository interface { NextPrNumber(ctx context.Context, tx *gorm.DB) (string, error) NextPoNumber(ctx context.Context, tx *gorm.DB) (string, error) BackfillProjectFlockKandang(ctx context.Context, purchaseID uint) error + SoftDeleteByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) error GetItemsByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error) GetItemsByWarehouseKandang(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error) } @@ -89,6 +90,44 @@ WHERE pi.purchase_id = ? return r.DB().WithContext(ctx).Exec(query, purchaseID).Error } +func (r *PurchaseRepositoryImpl) SoftDeleteByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) error { + if len(projectFlockKandangIDs) == 0 { + return nil + } + + return r.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { + var purchaseIDs []uint + query := ` +SELECT pi.purchase_id +FROM purchase_items pi +WHERE pi.project_flock_kandang_id IN (?) +GROUP BY pi.purchase_id +HAVING COUNT(*) = COUNT(CASE WHEN pi.project_flock_kandang_id IN (?) THEN 1 END) +` + if err := tx.Raw(query, projectFlockKandangIDs, projectFlockKandangIDs).Scan(&purchaseIDs).Error; err != nil { + return err + } + + now := time.Now().UTC() + if len(purchaseIDs) > 0 { + if err := tx.Model(&entity.Purchase{}). + Where("id IN (?) AND deleted_at IS NULL", purchaseIDs). + Update("deleted_at", now).Error; err != nil { + return err + } + if err := tx.Where("purchase_id IN (?)", purchaseIDs).Delete(&entity.PurchaseItem{}).Error; err != nil { + return err + } + } + + deleteItems := tx.Where("project_flock_kandang_id IN (?)", projectFlockKandangIDs) + if len(purchaseIDs) > 0 { + deleteItems = deleteItems.Where("purchase_id NOT IN (?)", purchaseIDs) + } + return deleteItems.Delete(&entity.PurchaseItem{}).Error + }) +} + func (r *PurchaseRepositoryImpl) CreateItems(ctx context.Context, purchaseID uint, items []*entity.PurchaseItem) error { if len(items) == 0 { return nil diff --git a/internal/modules/purchases/services/purchase.service.go b/internal/modules/purchases/services/purchase.service.go index e46788d8..35ca2f75 100644 --- a/internal/modules/purchases/services/purchase.service.go +++ b/internal/modules/purchases/services/purchase.service.go @@ -133,6 +133,7 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti purchases, total, err := s.PurchaseRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) + db = db.Where("purchases.deleted_at IS NULL") if params.SupplierID > 0 { db = db.Where("supplier_id = ?", params.SupplierID) From b42ca5e6fb17dce9f1a269eae784d84617c4c5f7 Mon Sep 17 00:00:00 2001 From: ragilap Date: Sat, 10 Jan 2026 21:34:19 +0700 Subject: [PATCH 18/19] feat(BE-74-76-78-278):delete unused code recording --- .../recordings/services/recording.service.go | 41 ------------------- 1 file changed, 41 deletions(-) diff --git a/internal/modules/production/recordings/services/recording.service.go b/internal/modules/production/recordings/services/recording.service.go index e46056e5..819552dc 100644 --- a/internal/modules/production/recordings/services/recording.service.go +++ b/internal/modules/production/recordings/services/recording.service.go @@ -901,47 +901,6 @@ type eggTotals struct { Weight float64 } -type stockTotals struct { - Usage float64 - Pending float64 - Total float64 -} - -func summarizeExistingStocks(stocks []entity.RecordingStock) map[uint]stockTotals { - totals := make(map[uint]stockTotals) - for _, stock := range stocks { - var usage float64 - var pending float64 - if stock.UsageQty != nil { - usage = *stock.UsageQty - } - if stock.PendingQty != nil { - pending = *stock.PendingQty - } - current := totals[stock.ProductWarehouseId] - current.Usage += usage - current.Pending += pending - current.Total += usage + pending - totals[stock.ProductWarehouseId] = current - } - return totals -} - -func summarizeIncomingStocks(stocks []validation.Stock) map[uint]stockTotals { - totals := make(map[uint]stockTotals) - for _, stock := range stocks { - var pending float64 - if stock.PendingQty != nil { - pending = *stock.PendingQty - } - current := totals[stock.ProductWarehouseId] - current.Usage += stock.Qty - current.Pending += pending - current.Total += stock.Qty + pending - totals[stock.ProductWarehouseId] = current - } - return totals -} func stocksMatch(existing []entity.RecordingStock, incoming []validation.Stock) bool { hasPending := false From af79db8726d12aa3eecabc1365260238801774b4 Mon Sep 17 00:00:00 2001 From: "Hafizh A. Y" Date: Sun, 11 Jan 2026 08:41:45 +0700 Subject: [PATCH 19/19] fix(BE): permission in payment route --- internal/modules/finance/payments/route.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/modules/finance/payments/route.go b/internal/modules/finance/payments/route.go index b00de964..c2931f0a 100644 --- a/internal/modules/finance/payments/route.go +++ b/internal/modules/finance/payments/route.go @@ -13,9 +13,9 @@ func PaymentRoutes(v1 fiber.Router, u user.UserService, s payment.PaymentService ctrl := controller.NewPaymentController(s) route := v1.Group("/payments") - // route.Use(m.Auth(u)) + route.Use(m.Auth(u)) - route.Post("/", ctrl.CreateOne) + route.Post("/", m.RequirePermissions(m.P_Finances_Payments_CreateOne), ctrl.CreateOne) route.Get("/:id", m.RequirePermissions(m.P_Finances_Payments_GetOne), ctrl.GetOne) route.Patch("/:id", m.RequirePermissions(m.P_Finances_Payments_UpdateOne), ctrl.UpdateOne) }