From 272367d8efd8e82a78f2f6d12796f657aa6df853 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Sun, 11 Jan 2026 12:51:37 +0700 Subject: [PATCH] FIX[BE]: fixing transfer to laying and implement correct fifo stock --- ...laying_transfers_to_detail_tables.down.sql | 79 +++ ...m_laying_transfers_to_detail_tables.up.sql | 73 +++ internal/entities/laying_transfer.go | 13 - internal/entities/laying_transfer_source.go | 3 +- internal/entities/laying_transfer_target.go | 3 +- .../project_flock_population_repository.go | 6 +- .../controllers/transfer_laying.controller.go | 4 +- .../dto/transfer_laying.dto.go | 8 +- .../production/transfer_layings/module.go | 43 +- .../services/transfer_laying.service.go | 495 ++++++++---------- internal/utils/fifo/constants.go | 20 +- 11 files changed, 403 insertions(+), 344 deletions(-) create mode 100644 internal/database/migrations/20260109145555_move_fifo_fields_from_laying_transfers_to_detail_tables.down.sql create mode 100644 internal/database/migrations/20260109145555_move_fifo_fields_from_laying_transfers_to_detail_tables.up.sql diff --git a/internal/database/migrations/20260109145555_move_fifo_fields_from_laying_transfers_to_detail_tables.down.sql b/internal/database/migrations/20260109145555_move_fifo_fields_from_laying_transfers_to_detail_tables.down.sql new file mode 100644 index 00000000..a74e2882 --- /dev/null +++ b/internal/database/migrations/20260109145555_move_fifo_fields_from_laying_transfers_to_detail_tables.down.sql @@ -0,0 +1,79 @@ +-- Rollback: Revert FIFO fields back to laying_transfers from detail tables + +-- ============================================================================ +-- PART 1: Remove FIFO columns from detail tables +-- ============================================================================ + +-- Add back old qty column first +ALTER TABLE laying_transfer_sources + ADD COLUMN IF NOT EXISTS qty NUMERIC(15, 3) NOT NULL DEFAULT 0; + +ALTER TABLE laying_transfer_targets + ADD COLUMN IF NOT EXISTS qty NUMERIC(15, 3) NOT NULL DEFAULT 0; + +-- Now drop FIFO columns +ALTER TABLE laying_transfer_sources + DROP COLUMN IF EXISTS usage_qty, + DROP COLUMN IF EXISTS pending_usage_qty; + +ALTER TABLE laying_transfer_targets + DROP COLUMN IF EXISTS total_qty, + DROP COLUMN IF EXISTS total_used; + +-- ============================================================================ +-- PART 2: Add back FIFO columns to laying_transfers table +-- ============================================================================ + +-- Add columns back for USABLE role (source warehouse) +ALTER TABLE laying_transfers + ADD COLUMN product_warehouse_id BIGINT, + ADD COLUMN pending_usage_qty NUMERIC(15, 3), + ADD COLUMN usage_qty NUMERIC(15, 3); + +-- Add columns back for STOCKABLE role (destination warehouse) +ALTER TABLE laying_transfers + ADD COLUMN dest_product_warehouse_id BIGINT, + ADD COLUMN total_qty NUMERIC(15, 3) DEFAULT 0 NOT NULL, + ADD COLUMN total_used NUMERIC(15, 3) DEFAULT 0 NOT NULL; + +-- ============================================================================ +-- PART 3: Recreate foreign key constraints +-- ============================================================================ + +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN + -- Add source product warehouse FK + ALTER TABLE laying_transfers + ADD CONSTRAINT fk_laying_transfers_product_warehouse_id + FOREIGN KEY (product_warehouse_id) + REFERENCES product_warehouses(id) + ON DELETE SET NULL; + + -- Add destination product warehouse FK + ALTER TABLE laying_transfers + ADD CONSTRAINT fk_laying_transfers_dest_product_warehouse_id + FOREIGN KEY (dest_product_warehouse_id) + REFERENCES product_warehouses(id) + ON DELETE SET NULL; + END IF; +END $$; + +-- ============================================================================ +-- PART 4: Recreate indexes for performance +-- ============================================================================ + +CREATE INDEX idx_laying_transfers_product_warehouse_id + ON laying_transfers(product_warehouse_id); + +CREATE INDEX idx_laying_transfers_dest_product_warehouse_id + ON laying_transfers(dest_product_warehouse_id); + +-- ============================================================================ +-- PART 5: Recreate comments for documentation +-- ============================================================================ + +COMMENT ON COLUMN laying_transfers.product_warehouse_id IS 'Product warehouse at source (Growing flock) - for USABLE role'; +COMMENT ON COLUMN laying_transfers.dest_product_warehouse_id IS 'Product warehouse at destination (Laying flock) - for STOCKABLE role'; +COMMENT ON COLUMN laying_transfers.total_qty IS 'Total lot quantity introduced to destination warehouse - for STOCKABLE role'; +COMMENT ON COLUMN laying_transfers.total_used IS 'Quantity already consumed from this lot at destination - for FIFO STOCKABLE role'; diff --git a/internal/database/migrations/20260109145555_move_fifo_fields_from_laying_transfers_to_detail_tables.up.sql b/internal/database/migrations/20260109145555_move_fifo_fields_from_laying_transfers_to_detail_tables.up.sql new file mode 100644 index 00000000..19c710d9 --- /dev/null +++ b/internal/database/migrations/20260109145555_move_fifo_fields_from_laying_transfers_to_detail_tables.up.sql @@ -0,0 +1,73 @@ +-- Move FIFO fields from laying_transfers to detail tables (sources & targets) +-- This enables proper FIFO integration for transfer laying with multiple sources and targets + +-- ============================================================================ +-- PART 1: Remove FIFO-related columns from laying_transfers table +-- ============================================================================ + +-- Drop foreign key constraints first +DO $$ +BEGIN + -- Drop source product warehouse FK + IF EXISTS ( + SELECT 1 FROM pg_constraint + WHERE conname = 'fk_laying_transfers_product_warehouse_id' + ) THEN + ALTER TABLE laying_transfers + DROP CONSTRAINT fk_laying_transfers_product_warehouse_id; + END IF; + + -- Drop destination product warehouse FK + IF EXISTS ( + SELECT 1 FROM pg_constraint + WHERE conname = 'fk_laying_transfers_dest_product_warehouse_id' + ) THEN + ALTER TABLE laying_transfers + DROP CONSTRAINT fk_laying_transfers_dest_product_warehouse_id; + END IF; +END $$; + +-- Drop indexes +DROP INDEX IF EXISTS idx_laying_transfers_product_warehouse_id; +DROP INDEX IF EXISTS idx_laying_transfers_dest_product_warehouse_id; + +-- Remove columns from laying_transfers +ALTER TABLE laying_transfers + DROP COLUMN IF EXISTS product_warehouse_id, + DROP COLUMN IF EXISTS dest_product_warehouse_id, + DROP COLUMN IF EXISTS pending_usage_qty, + DROP COLUMN IF EXISTS usage_qty, + DROP COLUMN IF EXISTS total_qty, + DROP COLUMN IF EXISTS total_used; + +-- ============================================================================ +-- PART 2: Add FIFO columns to laying_transfer_sources (USABLE role) +-- ============================================================================ + +ALTER TABLE laying_transfer_sources + ADD COLUMN usage_qty NUMERIC(15, 3) DEFAULT 0 NOT NULL, + ADD COLUMN pending_usage_qty NUMERIC(15, 3) DEFAULT 0 NOT NULL; + +-- Add comments for documentation +COMMENT ON COLUMN laying_transfer_sources.usage_qty IS 'Quantity consumed from this source - for FIFO USABLE role'; +COMMENT ON COLUMN laying_transfer_sources.pending_usage_qty IS 'Quantity pending to consume from this source - for FIFO USABLE role'; + +-- Drop old qty column as it's replaced by usage_qty +ALTER TABLE laying_transfer_sources + DROP COLUMN IF EXISTS qty; + +-- ============================================================================ +-- PART 3: Add FIFO columns to laying_transfer_targets (STOCKABLE role) +-- ============================================================================ + +ALTER TABLE laying_transfer_targets + ADD COLUMN total_qty NUMERIC(15, 3) DEFAULT 0 NOT NULL, + ADD COLUMN total_used NUMERIC(15, 3) DEFAULT 0 NOT NULL; + +-- Add comments for documentation +COMMENT ON COLUMN laying_transfer_targets.total_qty IS 'Total lot quantity introduced to this target warehouse - for FIFO STOCKABLE role'; +COMMENT ON COLUMN laying_transfer_targets.total_used IS 'Quantity already consumed from this lot at target warehouse - for FIFO STOCKABLE role'; + +-- Drop old qty column as it's replaced by total_qty +ALTER TABLE laying_transfer_targets + DROP COLUMN IF EXISTS qty; diff --git a/internal/entities/laying_transfer.go b/internal/entities/laying_transfer.go index 97a7df12..f983519f 100644 --- a/internal/entities/laying_transfer.go +++ b/internal/entities/laying_transfer.go @@ -12,17 +12,6 @@ type LayingTransfer struct { FromProjectFlockId uint `gorm:"not null"` ToProjectFlockId uint `gorm:"not null"` TransferDate time.Time `gorm:"type:date;not null"` - - - PendingUsageQty *float64 `gorm:"type:numeric(15,3)"` - UsageQty *float64 `gorm:"type:numeric(15,3)"` - ProductWarehouseId *uint `gorm:"type:bigint"` // Source PW (PULLET) - - - DestProductWarehouseID *uint `gorm:"column:dest_product_warehouse_id;type:bigint"` // Destination PW (LAYER) - TotalQty float64 `gorm:"column:total_qty;type:numeric(15,3);default:0"` // Total lot introduced to destination - TotalUsed float64 `gorm:"column:total_used;type:numeric(15,3);default:0"` // Already consumed from this lot - Notes string `gorm:"type:text"` CreatedBy uint `gorm:"not null"` CreatedAt time.Time `gorm:"autoCreateTime"` @@ -31,8 +20,6 @@ type LayingTransfer struct { FromProjectFlock *ProjectFlock `gorm:"foreignKey:FromProjectFlockId;references:Id"` ToProjectFlock *ProjectFlock `gorm:"foreignKey:ToProjectFlockId;references:Id"` - ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` // Source PW - DestProductWarehouse *ProductWarehouse `gorm:"foreignKey:DestProductWarehouseID;references:Id"` // Destination PW CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` Sources []LayingTransferSource `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"` Targets []LayingTransferTarget `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"` diff --git a/internal/entities/laying_transfer_source.go b/internal/entities/laying_transfer_source.go index 6b54bd84..e0b85774 100644 --- a/internal/entities/laying_transfer_source.go +++ b/internal/entities/laying_transfer_source.go @@ -11,7 +11,8 @@ type LayingTransferSource struct { LayingTransferId uint `gorm:"index;not null"` SourceProjectFlockKandangId uint `gorm:"not null"` ProductWarehouseId *uint `gorm:""` - Qty float64 `gorm:"type:numeric(15,3);not null"` + UsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO USABLE field + PendingUsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO USABLE field Note string `gorm:"type:text"` CreatedAt time.Time `gorm:"autoCreateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"` diff --git a/internal/entities/laying_transfer_target.go b/internal/entities/laying_transfer_target.go index dec98f1f..560e09f7 100644 --- a/internal/entities/laying_transfer_target.go +++ b/internal/entities/laying_transfer_target.go @@ -10,7 +10,8 @@ type LayingTransferTarget struct { Id uint `gorm:"primaryKey"` LayingTransferId uint `gorm:"index;not null"` TargetProjectFlockKandangId uint `gorm:"not null"` - Qty float64 `gorm:"type:numeric(15,3);not null"` + TotalQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO STOCKABLE field + TotalUsed float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO STOCKABLE field ProductWarehouseId *uint `gorm:""` Note string `gorm:"type:text"` CreatedAt time.Time `gorm:"autoCreateTime"` diff --git a/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go b/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go index 04ae56e1..022da6a3 100644 --- a/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go +++ b/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go @@ -96,9 +96,9 @@ func (r *projectFlockPopulationRepositoryImpl) GetTotalQtyByProjectFlockKandangI var total float64 err := r.DB().WithContext(ctx). Table("project_flock_populations"). - Select("COALESCE(SUM(total_qty), 0) AS total_qty"). - Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id"). - Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangID). + Select("COALESCE(SUM(total_qty - total_used_qty), 0) AS available_qty"). + Joins("JOIN product_warehouses pw ON project_flock_populations.product_warehouse_id = pw.id"). + Where("pw.project_flock_kandang_id = ?", projectFlockKandangID). Scan(&total).Error if err != nil { return 0, err diff --git a/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go b/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go index c69f4ff5..d2ab6d0a 100644 --- a/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go +++ b/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go @@ -84,7 +84,7 @@ func (u *TransferLayingController) CreateOne(c *fiber.Ctx) error { req := new(validation.Create) if err := c.BodyParser(req); err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + return fiber.NewError(fiber.StatusBadRequest, "Format permintaan tidak valid") } result, err := u.TransferLayingService.CreateOne(c, req) @@ -96,7 +96,7 @@ func (u *TransferLayingController) CreateOne(c *fiber.Ctx) error { JSON(response.Success{ Code: fiber.StatusCreated, Status: "success", - Message: "Create transferLaying successfully", + Message: "Berhasil membuat transfer laying", Data: dto.ToTransferLayingListDTO(*result), }) } diff --git a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go index aeb12e5e..e81d6cc5 100644 --- a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go +++ b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go @@ -67,8 +67,6 @@ type TransferLayingListDTO struct { TransferLayingRelationDTO FromProjectFlock *ProjectFlockSummaryDTO `json:"from_project_flock,omitempty"` ToProjectFlock *ProjectFlockSummaryDTO `json:"to_project_flock,omitempty"` - PendingUsageQty *float64 `json:"pending_usage_qty"` - UsageQty *float64 `json:"usage_qty"` CreatedBy uint `json:"created_by"` CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` CreatedAt time.Time `json:"created_at"` @@ -166,7 +164,7 @@ func ToProductWarehouseSummaryDTO(pw *entity.ProductWarehouse) *ProductWarehouse func ToLayingTransferSourceDTO(source entity.LayingTransferSource) LayingTransferSourceDTO { return LayingTransferSourceDTO{ SourceProjectFlockKandang: ToProjectFlockKandangSummaryDTO(source.SourceProjectFlockKandang), - Qty: source.Qty, + Qty: source.UsageQty, // Ambil dari UsageQty (FIFO consumed quantity) ProductWarehouse: ToProductWarehouseSummaryDTO(source.ProductWarehouse), Note: source.Note, } @@ -186,7 +184,7 @@ func ToLayingTransferSourceDTOs(sources []entity.LayingTransferSource) []LayingT func ToLayingTransferTargetDTO(target entity.LayingTransferTarget) LayingTransferTargetDTO { return LayingTransferTargetDTO{ TargetProjectFlockKandang: ToProjectFlockKandangSummaryDTO(target.TargetProjectFlockKandang), - Qty: target.Qty, + Qty: target.TotalQty, // Ambil dari TotalQty (FIFO replenished quantity) ProductWarehouse: ToProductWarehouseSummaryDTO(target.ProductWarehouse), Note: target.Note, } @@ -223,8 +221,6 @@ func ToTransferLayingListDTO(e entity.LayingTransfer) TransferLayingListDTO { TransferLayingRelationDTO: ToTransferLayingRelationDTO(e), FromProjectFlock: ToProjectFlockSummaryDTO(e.FromProjectFlock), ToProjectFlock: ToProjectFlockSummaryDTO(e.ToProjectFlock), - PendingUsageQty: e.PendingUsageQty, - UsageQty: e.UsageQty, CreatedBy: e.CreatedBy, CreatedUser: createdUser, CreatedAt: e.CreatedAt, diff --git a/internal/modules/production/transfer_layings/module.go b/internal/modules/production/transfer_layings/module.go index 381f2492..2068ccd7 100644 --- a/internal/modules/production/transfer_layings/module.go +++ b/internal/modules/production/transfer_layings/module.go @@ -36,30 +36,13 @@ func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, val stockAllocationRepo := commonRepo.NewStockAllocationRepository(db) fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log) - - if err := fifoService.RegisterUsable(fifo.UsableConfig{ - Key: fifo.UsableKeyTransferToLaying, - Table: "laying_transfers", - Columns: fifo.UsableColumns{ - ID: "id", - ProductWarehouseID: "product_warehouse_id", - UsageQuantity: "usage_qty", - PendingQuantity: "pending_usage_qty", - CreatedAt: "created_at", - }, - }); err != nil { - if !strings.Contains(strings.ToLower(err.Error()), "already registered") { - panic(fmt.Sprintf("failed to register transfer to laying usable workflow: %v", err)) - } - } - - + // daftarin jadi stockable if err := fifoService.RegisterStockable(fifo.StockableConfig{ - Key: fifo.StockableKeyTransferToLaying, - Table: "laying_transfers", + Key: fifo.StockableKeyTransferToLayingIn, + Table: "laying_transfer_targets", Columns: fifo.StockableColumns{ ID: "id", - ProductWarehouseID: "dest_product_warehouse_id", + ProductWarehouseID: "product_warehouse_id", TotalQuantity: "total_qty", TotalUsedQuantity: "total_used", CreatedAt: "created_at", @@ -71,6 +54,24 @@ func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, val } } + // daftarin jadi usable + if err := fifoService.RegisterUsable(fifo.UsableConfig{ + Key: fifo.UsableKeyTransferToLayingOut, + Table: "laying_transfer_sources", + Columns: fifo.UsableColumns{ + ID: "id", + ProductWarehouseID: "product_warehouse_id", + UsageQuantity: "usage_qty", + PendingQuantity: "pending_usage_qty", + CreatedAt: "created_at", + }, + OrderBy: []string{"created_at ASC", "id ASC"}, + }); err != nil { + if !strings.Contains(strings.ToLower(err.Error()), "already registered") { + panic(fmt.Sprintf("failed to register transfer to laying usable workflow: %v", err)) + } + } + approvalRepo := commonRepo.NewApprovalRepository(db) approvalService := commonSvc.NewApprovalService(approvalRepo) if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowTransferToLaying, utils.TransferToLayingApprovalSteps); err != nil { diff --git a/internal/modules/production/transfer_layings/services/transfer_laying.service.go b/internal/modules/production/transfer_layings/services/transfer_laying.service.go index 22c712e5..e62d4722 100644 --- a/internal/modules/production/transfer_layings/services/transfer_laying.service.go +++ b/internal/modules/production/transfer_layings/services/transfer_laying.service.go @@ -16,6 +16,7 @@ import ( ProjectFlockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/validations" + rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils/fifo" @@ -45,6 +46,7 @@ type transferLayingService struct { ProjectFlockPopulationRepo ProjectFlockRepository.ProjectFlockPopulationRepository ProductWarehouseRepo rInventory.ProductWarehouseRepository WarehouseRepo rWarehouse.WarehouseRepository + StockLogRepo rStockLogs.StockLogRepository ApprovalService commonSvc.ApprovalService FifoSvc commonSvc.FifoService } @@ -69,6 +71,7 @@ func NewTransferLayingService( ProjectFlockPopulationRepo: projectFlockPopulationRepo, ProductWarehouseRepo: productWarehouseRepo, WarehouseRepo: warehouseRepo, + StockLogRepo: rStockLogs.NewStockLogRepository(repo.DB()), ApprovalService: approvalService, FifoSvc: fifoSvc, } @@ -164,55 +167,42 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) return nil, err } - if _, err := s.ProjectFlockRepo.GetByID(c.Context(), req.SourceProjectFlockId, nil); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusNotFound, "Source Project Flock not found") - } - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate source project flock") + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Source Project Flock", ID: &req.SourceProjectFlockId, Exists: s.ProjectFlockRepo.IdExists}, + commonSvc.RelationCheck{Name: "Target Project Flock", ID: &req.TargetProjectFlockId, Exists: s.ProjectFlockRepo.IdExists}, + ); err != nil { + return nil, err } - if _, err := s.ProjectFlockRepo.GetByID(c.Context(), req.TargetProjectFlockId, nil); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusNotFound, "Target Project Flock not found") - } - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate target project flock") + sourceKandangIDs := make([]uint, len(req.SourceKandangs)) + for i, detail := range req.SourceKandangs { + sourceKandangIDs[i] = detail.ProjectFlockKandangId } - for _, detail := range req.SourceKandangs { - if err := commonSvc.EnsureRelations(c.Context(), - commonSvc.RelationCheck{Name: "Source Project Flock Kandang", ID: &detail.ProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists}, - ); err != nil { - return nil, err - } - - pfk, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), detail.ProjectFlockKandangId) - if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get source project flock kandang") - } - if pfk.ProjectFlockId != req.SourceProjectFlockId { - return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d does not belong to source project flock %d", detail.ProjectFlockKandangId, req.SourceProjectFlockId)) - } + if err := s.validateKandangOwnership( + c.Context(), + req.SourceProjectFlockId, + sourceKandangIDs, + ); err != nil { + return nil, err } - for _, detail := range req.TargetKandangs { - if err := commonSvc.EnsureRelations(c.Context(), - commonSvc.RelationCheck{Name: "Target Project Flock Kandang", ID: &detail.ProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists}, - ); err != nil { - return nil, err - } + targetKandangIDs := make([]uint, len(req.TargetKandangs)) + for i, detail := range req.TargetKandangs { + targetKandangIDs[i] = detail.ProjectFlockKandangId + } - pfk, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), detail.ProjectFlockKandangId) - if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get target project flock kandang") - } - if pfk.ProjectFlockId != req.TargetProjectFlockId { - return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Target kandang %d does not belong to target project flock %d", detail.ProjectFlockKandangId, req.TargetProjectFlockId)) - } + if err := s.validateKandangOwnership( + c.Context(), + req.TargetProjectFlockId, + targetKandangIDs, + ); err != nil { + return nil, err } transferDate, err := utils.ParseDateString(req.TransferDate) if err != nil { - return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transfer date format") + return nil, fiber.NewError(fiber.StatusBadRequest, "Format tanggal transfer tidak valid") } var totalSourceQty, totalTargetQty float64 @@ -220,7 +210,7 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) for _, sourceDetail := range req.SourceKandangs { if sourceDetail.Quantity <= 0 { - return nil, fiber.NewError(fiber.StatusBadRequest, "Source kandang quantity must be greater than 0") + return nil, fiber.NewError(fiber.StatusBadRequest, "Jumlah kandang sumber harus lebih dari 0") } totalSourceQty += sourceDetail.Quantity @@ -239,11 +229,11 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) } if totalPopulation == 0 { - return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d has no population available for transfer", sourceDetail.ProjectFlockKandangId)) + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang sumber %d tidak memiliki populasi untuk ditransfer", sourceDetail.ProjectFlockKandangId)) } if totalPopulation < sourceDetail.Quantity { - return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d has insufficient quantity. Available: %.0f, Requested: %.0f", sourceDetail.ProjectFlockKandangId, totalPopulation, sourceDetail.Quantity)) + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang sumber %d jumlah tidak mencukupi. Tersedia: %.0f, Diminta: %.0f", sourceDetail.ProjectFlockKandangId, totalPopulation, sourceDetail.Quantity)) } sourceWarehouseMap[sourceDetail.ProjectFlockKandangId] = productWarehouseId @@ -251,13 +241,13 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) for _, targetDetail := range req.TargetKandangs { if targetDetail.Quantity <= 0 { - return nil, fiber.NewError(fiber.StatusBadRequest, "Target kandang quantity must be greater than 0") + return nil, fiber.NewError(fiber.StatusBadRequest, "Jumlah kandang tujuan harus lebih dari 0") } totalTargetQty += targetDetail.Quantity } if totalSourceQty != totalTargetQty { - return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Total source quantity (%f) must equal total target quantity (%f)", totalSourceQty, totalTargetQty)) + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Jumlah total sumber (%.0f) harus sama dengan jumlah total tujuan (%.0f)", totalSourceQty, totalTargetQty)) } transferNumber := fmt.Sprintf("TL-%d", time.Now().UnixNano()) @@ -268,22 +258,14 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) FromProjectFlockId: req.SourceProjectFlockId, ToProjectFlockId: req.TargetProjectFlockId, TransferDate: transferDate, - PendingUsageQty: &totalSourceQty, CreatedBy: actorID, } - if len(sourceWarehouseMap) > 0 { - for _, pwID := range sourceWarehouseMap { - createBody.ProductWarehouseId = &pwID - break - } - } - err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { repoTx := s.Repository.WithTx(dbTransaction) if err := repoTx.CreateOne(c.Context(), createBody, nil); err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer laying record") + return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat record transfer laying") } for _, sourceDetail := range req.SourceKandangs { @@ -292,78 +274,91 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) source := entity.LayingTransferSource{ LayingTransferId: createBody.Id, SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId, - Qty: sourceDetail.Quantity, + UsageQty: 0, + PendingUsageQty: 0, // Di-set 0, biarkan FIFO Consume yang handle saat Approval ProductWarehouseId: &productWarehouseId, } if err := dbTransaction.Create(&source).Error; err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer source") + return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat sumber transfer") } } - var firstTargetProductWarehouseID *uint + for _, targetDetail := range req.TargetKandangs { - for i, targetDetail := range req.TargetKandangs { - - targetPFK, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId) + targetprojectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId) if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target project flock kandang") + return fiber.NewError(fiber.StatusInternalServerError, "Gagal mendapatkan project flock kandang tujuan") } - targetWarehouse, err := s.WarehouseRepo.GetLatestByKandangID(c.Context(), targetPFK.KandangId) + targetWarehouse, err := s.WarehouseRepo.GetLatestByKandangID(c.Context(), targetprojectFlockKandang.KandangId) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No warehouse found for target kandang %d", targetDetail.ProjectFlockKandangId)) + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Warehouse tidak ditemukan untuk kandang tujuan %d", targetDetail.ProjectFlockKandangId)) } - return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target warehouse") + return fiber.NewError(fiber.StatusInternalServerError, "Gagal mendapatkan warehouse tujuan") } + // Ambil product ID dari salah satu source warehouse (harusnya semua sources product-nya sama) + var sourceProductID uint + for _, sourceDetail := range req.SourceKandangs { + if pwID, ok := sourceWarehouseMap[sourceDetail.ProjectFlockKandangId]; ok { + // Get product warehouse untuk ambil product ID + var sourcePW entity.ProductWarehouse + if err := dbTransaction.First(&sourcePW, pwID).Error; err == nil { + sourceProductID = sourcePW.ProductId + break + } + } + } + + if sourceProductID == 0 { + return fiber.NewError(fiber.StatusInternalServerError, "Gagal mendapatkan product dari source warehouse") + } + + // Cari product warehouse di target berdasarkan: warehouse + project_flock_kandang + PRODUCT var targetPW entity.ProductWarehouse - err = dbTransaction.Where("warehouse_id = ? AND project_flock_kandang_id = ?", targetWarehouse.Id, targetDetail.ProjectFlockKandangId). + err = dbTransaction.Where("warehouse_id = ? AND project_flock_kandang_id = ? AND product_id = ?", + targetWarehouse.Id, targetDetail.ProjectFlockKandangId, sourceProductID). First(&targetPW).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No product warehouse found for target kandang %d in warehouse %d", targetDetail.ProjectFlockKandangId, targetWarehouse.Id)) + // Create baru dengan product yang sama dengan source + targetPW = entity.ProductWarehouse{ + ProductId: sourceProductID, + WarehouseId: targetWarehouse.Id, + ProjectFlockKandangId: &targetDetail.ProjectFlockKandangId, + Quantity: 0, + } + if err := dbTransaction.Create(&targetPW).Error; err != nil { + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal membuat product warehouse untuk kandang tujuan %d: %v", targetDetail.ProjectFlockKandangId, err)) + } + } else { + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mendapatkan product warehouse untuk kandang tujuan %d: %v", targetDetail.ProjectFlockKandangId, err)) } - return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get product warehouse for target kandang %d: %v", targetDetail.ProjectFlockKandangId, err)) } target := entity.LayingTransferTarget{ LayingTransferId: createBody.Id, TargetProjectFlockKandangId: targetDetail.ProjectFlockKandangId, - Qty: targetDetail.Quantity, + TotalQty: targetDetail.Quantity, + TotalUsed: 0, ProductWarehouseId: &targetPW.Id, } if err := dbTransaction.Create(&target).Error; err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer target") - } - - if i == 0 { - firstTargetProductWarehouseID = &targetPW.Id - } - } - - // Set DestProductWarehouseID untuk STOCKABLE role (ambil dari target pertama) - if firstTargetProductWarehouseID != nil { - createBody.DestProductWarehouseID = firstTargetProductWarehouseID - - // Update DestProductWarehouseID ke database - if err := dbTransaction.Model(&entity.LayingTransfer{}). - Where("id = ?", createBody.Id). - Update("dest_product_warehouse_id", *firstTargetProductWarehouseID).Error; err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to update DestProductWarehouseID") + return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat target transfer") } } if err := createApprovalTransferLaying(c.Context(), dbTransaction, createBody.Id, createBody.CreatedBy); err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer approval") + return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat approval transfer") } return nil }) if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer laying") + return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat transfer laying") } return s.GetOne(c, createBody.Id) @@ -412,24 +407,8 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { repoTx := s.Repository.WithTx(dbTransaction) - projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction) - productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction) - - for _, oldSource := range existingTransfer.Sources { - if oldSource.ProductWarehouseId != nil && oldSource.Qty > 0 { - - if err := productWarehouseRepoTx.PatchOne(c.Context(), *oldSource.ProductWarehouseId, map[string]any{ - "qty": gorm.Expr("qty + ?", oldSource.Qty), - }, nil); err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to restore warehouse quantity") - } - - if err := s.restoreProjectFlockPopulation(c.Context(), projectFlockPopulationRepoTx, oldSource.SourceProjectFlockKandangId, oldSource.Qty); err != nil { - return err - } - } - } + // Hapus old sources dan targets for _, oldSource := range existingTransfer.Sources { if err := dbTransaction.Delete(&oldSource).Error; err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete old source") @@ -442,23 +421,16 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, } } - totalSourceQty := 0.0 - for _, source := range req.SourceKandangs { - totalSourceQty += source.Quantity - } - if err := repoTx.PatchOne(c.Context(), id, map[string]any{ - "transfer_date": transferDate, - "notes": req.Reason, - "pending_usage_qty": &totalSourceQty, + "transfer_date": transferDate, + "notes": req.Reason, }, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to update transfer header") } - sourceWarehouseMap := make(map[uint]uint) + // Create new sources dengan pending quantity for _, sourceDetail := range req.SourceKandangs { - - populations, err := projectFlockPopulationRepoTx.GetByProjectFlockKandangID(c.Context(), sourceDetail.ProjectFlockKandangId) + populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(c.Context(), sourceDetail.ProjectFlockKandangId) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to get populations") } @@ -467,48 +439,37 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d has no population available", sourceDetail.ProjectFlockKandangId)) } - var totalPopulation float64 var productWarehouseId uint - for _, pop := range populations { - totalPopulation += pop.TotalQty if pop.ProductWarehouseId > 0 { productWarehouseId = pop.ProductWarehouseId + break } } - if totalPopulation < sourceDetail.Quantity { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d has insufficient quantity. Available: %.0f, Requested: %.0f", sourceDetail.ProjectFlockKandangId, totalPopulation, sourceDetail.Quantity)) + if productWarehouseId == 0 { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d has no product warehouse", sourceDetail.ProjectFlockKandangId)) } - sourceWarehouseMap[sourceDetail.ProjectFlockKandangId] = productWarehouseId - source := entity.LayingTransferSource{ LayingTransferId: id, SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId, - Qty: sourceDetail.Quantity, + UsageQty: 0, + PendingUsageQty: sourceDetail.Quantity, ProductWarehouseId: &productWarehouseId, } if err := dbTransaction.Create(&source).Error; err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer source") } - - if err := s.reduceProjectFlockPopulation(c.Context(), projectFlockPopulationRepoTx, sourceDetail.ProjectFlockKandangId, sourceDetail.Quantity); err != nil { - return err - } - - if err := productWarehouseRepoTx.PatchOne(c.Context(), productWarehouseId, map[string]any{"qty": gorm.Expr("qty - ?", sourceDetail.Quantity)}, nil); err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to update source warehouse quantity") - } } for _, targetDetail := range req.TargetKandangs { - targetPFK, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId) + targetprojectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target project flock kandang") } - targetWarehouse, err := s.WarehouseRepo.GetLatestByKandangID(c.Context(), targetPFK.KandangId) + targetWarehouse, err := s.WarehouseRepo.GetLatestByKandangID(c.Context(), targetprojectFlockKandang.KandangId) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No warehouse found for target kandang %d", targetDetail.ProjectFlockKandangId)) @@ -516,20 +477,50 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target warehouse") } + // Ambil product ID dari source yang pertama (semua sources seharusnya product-nya sama) + var sourceProductID uint + if len(req.SourceKandangs) > 0 { + firstSourceKandangID := req.SourceKandangs[0].ProjectFlockKandangId + populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(c.Context(), firstSourceKandangID) + if err == nil && len(populations) > 0 && populations[0].ProductWarehouseId > 0 { + var sourcePW entity.ProductWarehouse + if err := dbTransaction.First(&sourcePW, populations[0].ProductWarehouseId).Error; err == nil { + sourceProductID = sourcePW.ProductId + } + } + } + + if sourceProductID == 0 { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get product from source warehouse") + } + + // Cari product warehouse di target berdasarkan: warehouse + project_flock_kandang + PRODUCT var targetPW entity.ProductWarehouse - err = dbTransaction.Where("warehouse_id = ? AND project_flock_kandang_id = ?", targetWarehouse.Id, targetDetail.ProjectFlockKandangId). + err = dbTransaction.Where("warehouse_id = ? AND project_flock_kandang_id = ? AND product_id = ?", + targetWarehouse.Id, targetDetail.ProjectFlockKandangId, sourceProductID). First(&targetPW).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No product warehouse found for target kandang %d in warehouse %d", targetDetail.ProjectFlockKandangId, targetWarehouse.Id)) + // Create baru dengan product yang sama dengan source + targetPW = entity.ProductWarehouse{ + ProductId: sourceProductID, + WarehouseId: targetWarehouse.Id, + ProjectFlockKandangId: &targetDetail.ProjectFlockKandangId, + Quantity: 0, + } + if err := dbTransaction.Create(&targetPW).Error; err != nil { + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to create product warehouse for target kandang %d: %v", targetDetail.ProjectFlockKandangId, err)) + } + } else { + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get product warehouse for target kandang %d: %v", targetDetail.ProjectFlockKandangId, err)) } - return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get product warehouse for target kandang %d: %v", targetDetail.ProjectFlockKandangId, err)) } target := entity.LayingTransferTarget{ LayingTransferId: id, TargetProjectFlockKandangId: targetDetail.ProjectFlockKandangId, - Qty: targetDetail.Quantity, + TotalQty: targetDetail.Quantity, + TotalUsed: 0, ProductWarehouseId: &targetPW.Id, } if err := dbTransaction.Create(&target).Error; err != nil { @@ -573,49 +564,9 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error { } err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { repoTx := s.Repository.WithTx(dbTransaction) - productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction) - projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction) - - sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction) - sources, err := sourceRepoTx.GetByLayingTransferId(c.Context(), id) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer sources") - } - - for _, source := range sources { - if source.ProductWarehouseId != nil && source.Qty > 0 { - - if err := productWarehouseRepoTx.PatchOne(c.Context(), *source.ProductWarehouseId, map[string]any{ - "qty": gorm.Expr("qty + ?", source.Qty), - }, nil); err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to restore source warehouse quantity") - } - } - } - - for _, source := range sources { - populations, err := projectFlockPopulationRepoTx.GetByProjectFlockKandangID(c.Context(), source.SourceProjectFlockKandangId) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to get populations for restoration") - } - - remainingToRestore := source.Qty - for i := len(populations) - 1; i >= 0 && remainingToRestore > 0; i-- { - pop := populations[i] - restoreAmount := remainingToRestore - if pop.TotalQty < remainingToRestore { - restoreAmount = pop.TotalQty - } - - newQty := pop.TotalQty + restoreAmount - if err := projectFlockPopulationRepoTx.PatchOne(c.Context(), pop.Id, map[string]any{"total_qty": newQty}, nil); err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to restore population quantity") - } - - remainingToRestore -= restoreAmount - } - } + // Delete transfer - cascade akan menghapus sources dan targets + // FIFO akan menangani stock allocation cleanup via foreign key constraints if err := repoTx.DeleteOne(c.Context(), id); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete transfer laying") } @@ -667,7 +618,6 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) ( err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { repoTx := s.Repository.WithTx(dbTransaction) approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) - sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction) targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction) for _, approvableID := range approvableIDs { @@ -691,70 +641,77 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) ( return fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval") } - if action == entity.ApprovalActionApproved && transfer.PendingUsageQty != nil && *transfer.PendingUsageQty > 0 { + if action == entity.ApprovalActionApproved { - sources, err := sourceRepoTx.GetByLayingTransferId(c.Context(), approvableID) + sources, err := repository.NewLayingTransferSourceRepository(dbTransaction).GetByLayingTransferId(c.Context(), approvableID) if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer sources") + return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil sources transfer") } targets, err := targetRepoTx.GetByLayingTransferId(c.Context(), approvableID) if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer targets") + return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil targets transfer") } - if len(sources) > 0 && len(targets) > 0 { + // Hitung total quantity dari targets untuk di-consume dari sources + totalTargetQty := 0.0 + for _, target := range targets { + totalTargetQty += target.TotalQty + } - for _, source := range sources { - if source.ProductWarehouseId == nil { - return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Source product warehouse not found for transfer %d", approvableID)) - } - - _, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{ - UsableKey: fifo.UsableKeyTransferToLaying, - UsableID: approvableID, - ProductWarehouseID: *source.ProductWarehouseId, - Quantity: source.Qty, - AllowPending: false, - Tx: dbTransaction, - }) - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to consume FIFO stock for source %d: %v", source.ProductWarehouseId, err)) - } + // Consume dari laying_transfer_sources (Usable) - akan consume dari ProjectFlockPopulation (Stockable) + for _, source := range sources { + if source.ProductWarehouseId == nil { + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Source product warehouse tidak ditemukan untuk transfer %d", approvableID)) } - if transfer.DestProductWarehouseID != nil { - note := fmt.Sprintf("Transfer to Laying #%s", transfer.TransferNumber) - replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{ - StockableKey: fifo.StockableKeyTransferToLaying, - StockableID: approvableID, - ProductWarehouseID: *transfer.DestProductWarehouseID, - Quantity: *transfer.PendingUsageQty, - Note: ¬e, - Tx: dbTransaction, - }) - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to replenish stock to destination warehouse: %v", err)) - } + consumeResult, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{ + UsableKey: fifo.UsableKeyTransferToLayingOut, + UsableID: source.Id, + ProductWarehouseID: *source.ProductWarehouseId, + Quantity: totalTargetQty, + AllowPending: false, + Tx: dbTransaction, + }) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal consume FIFO stock: %v", err)) + } - if err := dbTransaction.Model(&entity.LayingTransfer{}). - Where("id = ?", approvableID). - Updates(map[string]interface{}{ - "total_qty": replenishResult.AddedQuantity, - }).Error; err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to update total quantity for transfer") - } + // Update source usage tracking + if err := dbTransaction.Model(&entity.LayingTransferSource{}). + Where("id = ?", source.Id). + Updates(map[string]interface{}{ + "usage_qty": source.UsageQty + consumeResult.UsageQuantity, + "pending_usage_qty": consumeResult.PendingQuantity, + }).Error; err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Gagal update source usage qty") } } - usageQty := *transfer.PendingUsageQty - updateData := map[string]any{ - "usage_qty": usageQty, - "total_qty": usageQty, // Same as usage_qty for initial transfer - "pending_usage_qty": nil, - } - if err := repoTx.PatchOne(c.Context(), approvableID, updateData, nil); err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to update transfer laying status") + // Replenish ke target warehouse + for _, target := range targets { + if target.ProductWarehouseId == nil { + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Target product warehouse tidak ditemukan untuk transfer %d", approvableID)) + } + + note := fmt.Sprintf("Transfer to Laying #%s - Target Kandang", transfer.TransferNumber) + replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{ + StockableKey: fifo.StockableKeyTransferToLayingIn, + StockableID: target.Id, + ProductWarehouseID: *target.ProductWarehouseId, + Quantity: target.TotalQty, + Note: ¬e, + Tx: dbTransaction, + }) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal replenish stock ke target warehouse: %v", err)) + } + + if err := dbTransaction.Model(&entity.LayingTransferTarget{}). + Where("id = ?", target.Id). + Update("total_qty", replenishResult.AddedQuantity).Error; err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Gagal update target total qty") + } } } } @@ -832,66 +789,6 @@ func (s *transferLayingService) getOrCreateProductWarehouse(ctx context.Context, return newWarehouse, nil } -func (s *transferLayingService) reduceProjectFlockPopulation(ctx context.Context, populationRepo ProjectFlockRepository.ProjectFlockPopulationRepository, projectFlockKandangID uint, quantityToReduce float64) error { - - populations, err := populationRepo.GetByProjectFlockKandangID(ctx, projectFlockKandangID) - if err != nil { - return err - } - - if len(populations) == 0 { - return fiber.NewError(fiber.StatusBadRequest, "No populations found for reduction") - } - - remainingToReduce := quantityToReduce - - for i := len(populations) - 1; i >= 0; i-- { - if remainingToReduce <= 0 { - break - } - - pop := populations[i] - reductionAmount := remainingToReduce - if pop.TotalQty < remainingToReduce { - reductionAmount = pop.TotalQty - } - - newQty := pop.TotalQty - reductionAmount - if err := populationRepo.PatchOne(ctx, pop.Id, map[string]any{"total_qty": newQty}, nil); err != nil { - return err - } - - remainingToReduce -= reductionAmount - } - - if remainingToReduce > 0 { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient population to reduce. Still need to reduce: %.0f", remainingToReduce)) - } - - return nil -} - -func (s *transferLayingService) restoreProjectFlockPopulation(ctx context.Context, populationRepo ProjectFlockRepository.ProjectFlockPopulationRepository, projectFlockKandangID uint, quantityToRestore float64) error { - populations, err := populationRepo.GetByProjectFlockKandangID(ctx, projectFlockKandangID) - if err != nil { - return err - } - - if len(populations) == 0 { - return fiber.NewError(fiber.StatusBadRequest, "No populations found for restoration") - } - - if len(populations) > 0 { - lastPop := populations[len(populations)-1] - newQty := lastPop.TotalQty + quantityToRestore - if err := populationRepo.PatchOne(ctx, lastPop.Id, map[string]any{"total_qty": newQty}, nil); err != nil { - return err - } - } - - return nil -} - func (s transferLayingService) GetAvailableQtyPerKandang(ctx *fiber.Ctx, projectFlockID uint) (*entity.ProjectFlock, map[uint]float64, error) { pf, err := s.ProjectFlockRepo.GetByID(ctx.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB { @@ -925,3 +822,27 @@ func (s transferLayingService) GetAvailableQtyPerKandang(ctx *fiber.Ctx, project return pf, kandangAvailableQty, nil } + +func (s *transferLayingService) validateKandangOwnership( + ctx context.Context, + projectFlockID uint, + kandangIDs []uint, +) error { + + for _, kandangID := range kandangIDs { + // validasi terlebih dahulu apakah kandangnya itu ada atau gak + projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(ctx, kandangID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Kandang %d tidak ditemukan", kandangID)) + } + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get project flock kandang") + } + + if projectFlockKandang.ProjectFlockId != projectFlockID { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d tidak terhubung ke project flock %d", kandangID, projectFlockID)) + } + } + + return nil +} diff --git a/internal/utils/fifo/constants.go b/internal/utils/fifo/constants.go index 2f96beaa..03f61f82 100644 --- a/internal/utils/fifo/constants.go +++ b/internal/utils/fifo/constants.go @@ -2,17 +2,17 @@ package fifo const ( // Usable Keys - UsableKeyRecordingStock UsableKey = "RECORDING_STOCK" - UsableKeyProjectChickin UsableKey = "PROJECT_CHICKIN" - UsableKeyMarketingDelivery UsableKey = "MARKETING_DELIVERY" - UsableKeyTransferToLaying UsableKey = "TRANSFER_TO_LAYING" - UsableKeyStockTransferOut UsableKey = "STOCK_TRANSFER_OUT" - UsableKeyAdjustmentOut UsableKey = "ADJUSTMENT_OUT" + UsableKeyRecordingStock UsableKey = "RECORDING_STOCK" + UsableKeyProjectChickin UsableKey = "PROJECT_CHICKIN" + UsableKeyMarketingDelivery UsableKey = "MARKETING_DELIVERY" + UsableKeyTransferToLayingOut UsableKey = "TRANSFERTOLAYING_OUT" + UsableKeyStockTransferOut UsableKey = "STOCK_TRANSFER_OUT" + UsableKeyAdjustmentOut UsableKey = "ADJUSTMENT_OUT" // Stockable Keys - StockableKeyTransferToLaying StockableKey = "TRANSFER_TO_LAYING" - StockableKeyStockTransferIn StockableKey = "STOCK_TRANSFER_IN" - StockableKeyAdjustmentIn StockableKey = "ADJUSTMENT_IN" - StockableKeyPurchaseItems StockableKey = "PURCHASE_ITEMS" + StockableKeyTransferToLayingIn StockableKey = "TRANSFERTOLAYING_IN" + StockableKeyStockTransferIn StockableKey = "STOCK_TRANSFER_IN" + StockableKeyAdjustmentIn StockableKey = "ADJUSTMENT_IN" + StockableKeyPurchaseItems StockableKey = "PURCHASE_ITEMS" StockableKeyProjectFlockPopulation StockableKey = "PROJECT_FLOCK_POPULATION" )