mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 21:41:55 +00:00
Fix transfer to laying delete and fix chikin delete with response recording
This commit is contained in:
@@ -45,4 +45,6 @@ type Recording struct {
|
||||
StandardFcr *float64 `gorm:"-"`
|
||||
PopulationCanChange *bool `gorm:"-"`
|
||||
TransferExecuted *bool `gorm:"-"`
|
||||
IsTransition *bool `gorm:"-"`
|
||||
IsLaying *bool `gorm:"-"`
|
||||
}
|
||||
|
||||
@@ -151,25 +151,25 @@ func (u *ChickinController) GetOne(c *fiber.Ctx) error {
|
||||
// })
|
||||
// }
|
||||
|
||||
// func (u *ChickinController) DeleteOne(c *fiber.Ctx) error {
|
||||
// param := c.Params("id")
|
||||
func (u *ChickinController) DeleteOne(c *fiber.Ctx) error {
|
||||
param := c.Params("id")
|
||||
|
||||
// id, err := strconv.Atoi(param)
|
||||
// if err != nil {
|
||||
// return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
// }
|
||||
id, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
}
|
||||
|
||||
// if err := u.ChickinService.DeleteOne(c, uint(id)); err != nil {
|
||||
// return err
|
||||
// }
|
||||
if err := u.ChickinService.DeleteOne(c, uint(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// return c.Status(fiber.StatusOK).
|
||||
// JSON(response.Common{
|
||||
// Code: fiber.StatusOK,
|
||||
// Status: "success",
|
||||
// Message: "Delete chickin successfully",
|
||||
// })
|
||||
// }
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Common{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Delete chickin successfully",
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ChickinController) Approval(c *fiber.Ctx) error {
|
||||
req := new(validation.Approve)
|
||||
|
||||
@@ -19,6 +19,6 @@ func ChickinRoutes(v1 fiber.Router, u user.UserService, s chickin.ChickinService
|
||||
route.Post("/",m.RequirePermissions(m.P_ChickinsCreateOne), ctrl.CreateOne)
|
||||
route.Get("/:id",m.RequirePermissions(m.P_ChickinsGetOne), ctrl.GetOne)
|
||||
// route.Patch("/:id", ctrl.UpdateOne)
|
||||
// route.Delete("/:id", ctrl.DeleteOne)
|
||||
route.Delete("/:id", ctrl.DeleteOne)
|
||||
route.Post("/approvals",m.RequirePermissions(m.P_ChickinsApproval), ctrl.Approval)
|
||||
}
|
||||
|
||||
@@ -33,7 +33,10 @@ import (
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
const chickinDeletePopulationGuardMessage = "Chickin tidak dapat dihapus karena sudah memiliki population. Lakukan rollback/penyesuaian population terlebih dahulu"
|
||||
const (
|
||||
chickinDeletePopulationGuardMessage = "Chickin tidak dapat dihapus karena masih memiliki population aktif"
|
||||
chickinDeleteRecordingGuardMessage = "Chickin tidak dapat dihapus karena masih terkait recording. Lakukan rollback/delete recording terlebih dahulu"
|
||||
)
|
||||
|
||||
type ChickinService interface {
|
||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectChickin, int64, error)
|
||||
@@ -133,16 +136,31 @@ func (s chickinService) ensureNotTransferred(ctx context.Context, projectFlockKa
|
||||
return nil
|
||||
}
|
||||
|
||||
transfer, err := s.TransferLayingRepo.GetLatestApprovedBySourceKandang(ctx, projectFlockKandangID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
// Restriction transfer->laying untuk chickin hanya berlaku pada kandang kategori growing.
|
||||
if s.ProjectflockKandangRepo != nil {
|
||||
pfk, err := s.ProjectflockKandangRepo.GetByIDLight(ctx, projectFlockKandangID)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
s.Log.Errorf("Failed to resolve project flock kandang %d: %+v", projectFlockKandangID, err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi transfer laying")
|
||||
}
|
||||
if err == nil && pfk != nil {
|
||||
category := strings.ToUpper(strings.TrimSpace(pfk.ProjectFlock.Category))
|
||||
if category != strings.ToUpper(string(utils.ProjectFlockCategoryGrowing)) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkExecuted := func(transfer *entity.LayingTransfer) bool {
|
||||
return transfer != nil && transfer.ExecutedAt != nil && !transfer.ExecutedAt.IsZero()
|
||||
}
|
||||
|
||||
sourceTransfer, err := s.TransferLayingRepo.GetLatestApprovedBySourceKandang(ctx, projectFlockKandangID)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
s.Log.Errorf("Failed to resolve transfer laying by source kandang %d: %+v", projectFlockKandangID, err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi transfer laying")
|
||||
}
|
||||
|
||||
if transfer != nil && transfer.ExecutedAt != nil && !transfer.ExecutedAt.IsZero() {
|
||||
if checkExecuted(sourceTransfer) {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Project flock kandang sudah dipindahkan ke laying")
|
||||
}
|
||||
|
||||
@@ -175,6 +193,7 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
|
||||
|
||||
newChikins := make([]*entity.ProjectChickin, 0)
|
||||
chickinQtyMap := make(map[uint]float64)
|
||||
flockCategory := strings.ToUpper(strings.TrimSpace(projectFlockKandang.ProjectFlock.Category))
|
||||
|
||||
for idx, chickinReq := range req.ChickinRequests {
|
||||
|
||||
@@ -194,8 +213,7 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
|
||||
}
|
||||
|
||||
if productWarehouse.Product.Id != 0 {
|
||||
category := strings.ToUpper(strings.TrimSpace(projectFlockKandang.ProjectFlock.Category))
|
||||
if category != string(utils.ProjectFlockCategoryGrowing) && category != string(utils.ProjectFlockCategoryLaying) {
|
||||
if flockCategory != string(utils.ProjectFlockCategoryGrowing) && flockCategory != string(utils.ProjectFlockCategoryLaying) {
|
||||
return nil, fmt.Errorf("invalid flock category for chickin")
|
||||
}
|
||||
|
||||
@@ -244,6 +262,19 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
|
||||
if availableQty < 0 {
|
||||
availableQty = 0
|
||||
}
|
||||
if flockCategory == strings.ToUpper(string(utils.ProjectFlockCategoryLaying)) {
|
||||
transferAvailable, err := s.resolveLayingTransferAvailableQty(c.Context(), nil, req.ProjectFlockKandangId, chickinReq.ProductWarehouseId)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to resolve laying transfer availability for pfk=%d pw=%d: %+v", req.ProjectFlockKandangId, chickinReq.ProductWarehouseId, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi stok transfer laying")
|
||||
}
|
||||
if transferAvailable < 0 {
|
||||
transferAvailable = 0
|
||||
}
|
||||
if transferAvailable < availableQty {
|
||||
availableQty = transferAvailable
|
||||
}
|
||||
}
|
||||
|
||||
chickinQtyMap[uint(idx)] = availableQty
|
||||
}
|
||||
@@ -253,6 +284,9 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
|
||||
}
|
||||
|
||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||
if err := s.ensurePopulationRouteScope(c.Context(), dbTransaction); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repositoryTx := repository.NewChickinRepository(dbTransaction)
|
||||
existingChikins, err := repositoryTx.GetByProjectFlockKandangIDForUpdate(c.Context(), req.ProjectFlockKandangId)
|
||||
@@ -425,13 +459,8 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
if err := s.ensureNotTransferred(c.Context(), chickin.ProjectFlockKandangId); err != nil {
|
||||
return err
|
||||
}
|
||||
hasPopulation, err := s.ProjectflockPopulationRepo.ExistsByProjectChickinID(c.Context(), chickin.Id)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to check population by chickin %d: %+v", chickin.Id, err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi population chickin")
|
||||
}
|
||||
if hasPopulation {
|
||||
return fiber.NewError(fiber.StatusBadRequest, chickinDeletePopulationGuardMessage)
|
||||
if err := s.ensureNoExecutedTransferForDelete(c.Context(), chickin.ProjectFlockKandangId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
actorID, err := m.ActorIDFromContext(c)
|
||||
@@ -440,27 +469,51 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
}
|
||||
|
||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
||||
if err := s.ensurePopulationRouteScope(c.Context(), tx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
chickinRepoTx := repository.NewChickinRepository(tx)
|
||||
if chickin.UsageQty > 0 || chickin.PendingUsageQty > 0 {
|
||||
if err := s.ReleaseChickinStocks(c.Context(), tx, chickin, actorID); err != nil {
|
||||
lockedChickin, err := chickinRepoTx.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
||||
return db.Clauses(clause.Locking{Strength: "UPDATE"})
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, "Chickin not found")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
consumeAllocBefore, traceAllocBefore, err := s.countActiveChickinAllocations(c.Context(), tx, lockedChickin.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.Log.Infof(
|
||||
"Delete chickin start id=%d usage=%.3f pending=%.3f active_consume_alloc=%d active_trace_alloc=%d",
|
||||
lockedChickin.Id,
|
||||
lockedChickin.UsageQty,
|
||||
lockedChickin.PendingUsageQty,
|
||||
consumeAllocBefore,
|
||||
traceAllocBefore,
|
||||
)
|
||||
|
||||
if err := s.ensureNoRelatedRecording(c.Context(), tx, lockedChickin); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hasActiveConsumeAlloc, err := s.hasActiveChickinConsumeAllocations(c.Context(), tx, lockedChickin.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if lockedChickin.UsageQty > 0 || lockedChickin.PendingUsageQty > 0 || hasActiveConsumeAlloc {
|
||||
if err := s.ReleaseChickinStocks(c.Context(), tx, lockedChickin, actorID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
note := "delete chickin rollback"
|
||||
if err := tx.WithContext(c.Context()).
|
||||
Model(&entity.StockAllocation{}).
|
||||
Where("usable_type = ? AND usable_id = ? AND status = ?",
|
||||
fifo.UsableKeyProjectChickin.String(),
|
||||
chickin.Id,
|
||||
entity.StockAllocationStatusActive,
|
||||
).
|
||||
Updates(map[string]any{
|
||||
"status": entity.StockAllocationStatusReleased,
|
||||
"released_at": now,
|
||||
"note": note,
|
||||
}).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal release alokasi FIFO chickin")
|
||||
|
||||
if err := s.rollbackChickinPopulation(c.Context(), tx, lockedChickin.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := chickinRepoTx.DeleteOne(c.Context(), id); err != nil {
|
||||
@@ -473,10 +526,29 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.syncChickinTraceForProductWarehouse(c.Context(), tx, chickin.ProductWarehouseId); err != nil {
|
||||
reflowAsOf := normalizeDateOnlyUTC(lockedChickin.ChickInDate)
|
||||
if reflowAsOf.IsZero() {
|
||||
reflowAsOf = time.Now().UTC()
|
||||
}
|
||||
if err := s.reflowWarehouseAfterChickinDelete(c.Context(), tx, lockedChickin.ProductWarehouseId, reflowAsOf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.syncChickinTraceForProductWarehouse(c.Context(), tx, lockedChickin.ProductWarehouseId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
consumeAllocAfter, traceAllocAfter, err := s.countActiveChickinAllocations(c.Context(), tx, lockedChickin.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.Log.Infof(
|
||||
"Delete chickin complete id=%d active_consume_alloc=%d active_trace_alloc=%d",
|
||||
lockedChickin.Id,
|
||||
consumeAllocAfter,
|
||||
traceAllocAfter,
|
||||
)
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
@@ -489,6 +561,325 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s chickinService) ensureNoExecutedTransferForDelete(ctx context.Context, projectFlockKandangID uint) error {
|
||||
if projectFlockKandangID == 0 || s.TransferLayingRepo == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete guard by executed transfer hanya untuk kandang kategori growing.
|
||||
if s.ProjectflockKandangRepo != nil {
|
||||
pfk, err := s.ProjectflockKandangRepo.GetByIDLight(ctx, projectFlockKandangID)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
s.Log.Errorf("Failed to resolve project flock kandang %d for delete guard: %+v", projectFlockKandangID, err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi transfer laying")
|
||||
}
|
||||
if err == nil && pfk != nil {
|
||||
category := strings.ToUpper(strings.TrimSpace(pfk.ProjectFlock.Category))
|
||||
if category != strings.ToUpper(string(utils.ProjectFlockCategoryGrowing)) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isExecuted := func(transfer *entity.LayingTransfer) bool {
|
||||
return transfer != nil && transfer.ExecutedAt != nil && !transfer.ExecutedAt.IsZero()
|
||||
}
|
||||
|
||||
sourceTransfer, err := s.TransferLayingRepo.GetLatestApprovedBySourceKandang(ctx, projectFlockKandangID)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
s.Log.Errorf("Failed to resolve transfer laying by source kandang %d for delete guard: %+v", projectFlockKandangID, err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi transfer laying")
|
||||
}
|
||||
targetTransfer, err := s.TransferLayingRepo.GetLatestApprovedByTargetKandang(ctx, projectFlockKandangID)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
s.Log.Errorf("Failed to resolve transfer laying by target kandang %d for delete guard: %+v", projectFlockKandangID, err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi transfer laying")
|
||||
}
|
||||
|
||||
if isExecuted(sourceTransfer) || isExecuted(targetTransfer) {
|
||||
return fiber.NewError(
|
||||
fiber.StatusBadRequest,
|
||||
"Chickin tidak dapat dihapus karena transfer laying sudah dieksekusi. Lakukan unexecute transfer terlebih dahulu",
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *chickinService) resolveLayingTransferAvailableQty(ctx context.Context, tx *gorm.DB, targetProjectFlockKandangID, productWarehouseID uint) (float64, error) {
|
||||
if targetProjectFlockKandangID == 0 || productWarehouseID == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
db := s.Repository.DB().WithContext(ctx)
|
||||
if tx != nil {
|
||||
db = tx.WithContext(ctx)
|
||||
}
|
||||
|
||||
var available float64
|
||||
err := db.Table("laying_transfer_targets ltt").
|
||||
Select("COALESCE(SUM(GREATEST(0, COALESCE(ltt.total_qty,0) - COALESCE(ltt.total_used,0))), 0) AS available").
|
||||
Joins("JOIN laying_transfers lt ON lt.id = ltt.laying_transfer_id AND lt.deleted_at IS NULL").
|
||||
Where("ltt.deleted_at IS NULL").
|
||||
Where("ltt.target_project_flock_kandang_id = ?", targetProjectFlockKandangID).
|
||||
Where("ltt.product_warehouse_id = ?", productWarehouseID).
|
||||
Where("lt.executed_at IS NOT NULL").
|
||||
Where(`(
|
||||
SELECT a.action
|
||||
FROM approvals a
|
||||
WHERE a.approvable_type = ?
|
||||
AND a.approvable_id = lt.id
|
||||
ORDER BY a.id DESC
|
||||
LIMIT 1
|
||||
) = ?`, string(utils.ApprovalWorkflowTransferToLaying), entity.ApprovalActionApproved).
|
||||
Scan(&available).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return available, nil
|
||||
}
|
||||
|
||||
func (s chickinService) ensurePopulationRouteScope(ctx context.Context, tx *gorm.DB) error {
|
||||
db := tx
|
||||
if db == nil {
|
||||
db = s.Repository.DB()
|
||||
}
|
||||
if db == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
result := db.WithContext(ctx).
|
||||
Table("fifo_stock_v2_route_rules").
|
||||
Where("is_active = TRUE").
|
||||
Where("lane = ?", "STOCKABLE").
|
||||
Where("function_code = ?", "POPULATION_IN").
|
||||
Where("source_table = ?", "project_flock_populations").
|
||||
Where("(scope_sql IS NULL OR TRIM(scope_sql) = '')").
|
||||
Updates(map[string]any{
|
||||
"scope_sql": "deleted_at IS NULL",
|
||||
"updated_at": now,
|
||||
})
|
||||
if result.Error != nil {
|
||||
s.Log.Errorf("Failed to enforce FIFO population route scope: %+v", result.Error)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi konfigurasi FIFO chickin")
|
||||
}
|
||||
if result.RowsAffected > 0 {
|
||||
s.Log.Warnf(
|
||||
"Auto-fixed FIFO population route scope for chickin flow (rows=%d)",
|
||||
result.RowsAffected,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *chickinService) ensureNoRelatedRecording(ctx context.Context, tx *gorm.DB, chickin *entity.ProjectChickin) error {
|
||||
if chickin == nil || chickin.ProjectFlockKandangId == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
db := s.Repository.DB().WithContext(ctx)
|
||||
if tx != nil {
|
||||
db = tx.WithContext(ctx)
|
||||
}
|
||||
|
||||
recordDateFloor := normalizeDateOnlyUTC(chickin.ChickInDate)
|
||||
var earliest entity.Recording
|
||||
query := db.Model(&entity.Recording{}).
|
||||
Where("project_flock_kandangs_id = ?", chickin.ProjectFlockKandangId).
|
||||
Where("deleted_at IS NULL")
|
||||
if !recordDateFloor.IsZero() {
|
||||
query = query.Where("record_datetime >= ?", recordDateFloor)
|
||||
}
|
||||
if err := query.Order("record_datetime ASC, id ASC").Take(&earliest).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
s.Log.Errorf("Failed to validate related recordings for chickin %d: %+v", chickin.Id, err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi recording terkait chickin")
|
||||
}
|
||||
|
||||
return fiber.NewError(
|
||||
fiber.StatusBadRequest,
|
||||
fmt.Sprintf(
|
||||
"%s (recording tanggal %s)",
|
||||
chickinDeleteRecordingGuardMessage,
|
||||
normalizeDateOnlyUTC(earliest.RecordDatetime).Format("2006-01-02"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (s *chickinService) hasActiveChickinConsumeAllocations(ctx context.Context, tx *gorm.DB, chickinID uint) (bool, error) {
|
||||
if tx == nil || chickinID == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
var count int64
|
||||
if err := tx.WithContext(ctx).
|
||||
Model(&entity.StockAllocation{}).
|
||||
Where("usable_type = ? AND usable_id = ? AND status = ? AND allocation_purpose = ?",
|
||||
fifo.UsableKeyProjectChickin.String(),
|
||||
chickinID,
|
||||
entity.StockAllocationStatusActive,
|
||||
entity.StockAllocationPurposeConsume,
|
||||
).
|
||||
Count(&count).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (s *chickinService) countActiveChickinAllocations(ctx context.Context, tx *gorm.DB, chickinID uint) (consume int64, trace int64, err error) {
|
||||
if tx == nil || chickinID == 0 {
|
||||
return 0, 0, nil
|
||||
}
|
||||
|
||||
baseQuery := tx.WithContext(ctx).Model(&entity.StockAllocation{}).
|
||||
Where("usable_type = ? AND usable_id = ? AND status = ?",
|
||||
fifo.UsableKeyProjectChickin.String(),
|
||||
chickinID,
|
||||
entity.StockAllocationStatusActive,
|
||||
)
|
||||
if err := baseQuery.Session(&gorm.Session{}).
|
||||
Where("allocation_purpose = ?", entity.StockAllocationPurposeConsume).
|
||||
Count(&consume).Error; err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
if err := baseQuery.Session(&gorm.Session{}).
|
||||
Where("allocation_purpose = ?", entity.StockAllocationPurposeTraceChickin).
|
||||
Count(&trace).Error; err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
return consume, trace, nil
|
||||
}
|
||||
|
||||
func (s *chickinService) reflowWarehouseAfterChickinDelete(ctx context.Context, tx *gorm.DB, productWarehouseID uint, asOf time.Time) error {
|
||||
if tx == nil || productWarehouseID == 0 || s.FifoStockV2Svc == nil {
|
||||
return nil
|
||||
}
|
||||
if err := s.ensurePopulationRouteScope(ctx, tx); err != nil {
|
||||
return err
|
||||
}
|
||||
qtyBefore, hasQtyBefore := s.tryLoadWarehouseQty(ctx, tx, productWarehouseID)
|
||||
|
||||
flagGroupCode, err := s.resolveFlagGroupByProductWarehouse(ctx, tx, productWarehouseID)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to resolve flag group for delete chickin reflow pw=%d: %+v", productWarehouseID, err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal sinkronisasi stok setelah delete chickin")
|
||||
}
|
||||
if strings.TrimSpace(flagGroupCode) == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
result, err := s.FifoStockV2Svc.Reflow(ctx, commonSvc.FifoStockV2ReflowRequest{
|
||||
FlagGroupCode: flagGroupCode,
|
||||
ProductWarehouseID: productWarehouseID,
|
||||
AsOf: &asOf,
|
||||
Tx: tx,
|
||||
})
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to reflow warehouse after delete chickin pw=%d: %+v", productWarehouseID, err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal sinkronisasi stok setelah delete chickin")
|
||||
}
|
||||
|
||||
processedUsables := 0
|
||||
rollbackQty := 0.0
|
||||
allocateQty := 0.0
|
||||
if result != nil {
|
||||
processedUsables = result.ProcessedUsables
|
||||
rollbackQty = result.Rollback.ReleasedQty
|
||||
allocateQty = result.Allocate.AllocatedQty
|
||||
}
|
||||
s.Log.Infof(
|
||||
"Delete chickin warehouse reflow pw=%d processed_usables=%d rollback_qty=%.3f allocate_qty=%.3f",
|
||||
productWarehouseID,
|
||||
processedUsables,
|
||||
rollbackQty,
|
||||
allocateQty,
|
||||
)
|
||||
s.logWarehouseQtySnapshot(
|
||||
ctx,
|
||||
tx,
|
||||
productWarehouseID,
|
||||
"reflow_after_delete_chickin",
|
||||
0,
|
||||
hasQtyBefore,
|
||||
qtyBefore,
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *chickinService) rollbackChickinPopulation(ctx context.Context, tx *gorm.DB, chickinID uint) error {
|
||||
if tx == nil || chickinID == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var populationIDs []uint
|
||||
if err := tx.WithContext(ctx).
|
||||
Model(&entity.ProjectFlockPopulation{}).
|
||||
Where("project_chickin_id = ?", chickinID).
|
||||
Pluck("id", &populationIDs).Error; err != nil {
|
||||
s.Log.Errorf("Failed to list population ids for chickin %d: %+v", chickinID, err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil population chickin")
|
||||
}
|
||||
if len(populationIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
note := "delete chickin rollback population"
|
||||
releaseResult := tx.WithContext(ctx).
|
||||
Model(&entity.StockAllocation{}).
|
||||
Where("stockable_type = ? AND stockable_id IN ? AND status = ?",
|
||||
fifo.StockableKeyProjectFlockPopulation.String(),
|
||||
populationIDs,
|
||||
entity.StockAllocationStatusActive,
|
||||
).
|
||||
Where("NOT (usable_type = ? AND usable_id = ?)",
|
||||
fifo.UsableKeyProjectChickin.String(),
|
||||
chickinID,
|
||||
).
|
||||
Updates(map[string]any{
|
||||
"status": entity.StockAllocationStatusReleased,
|
||||
"released_at": now,
|
||||
"note": note,
|
||||
})
|
||||
if releaseResult.Error != nil {
|
||||
err := releaseResult.Error
|
||||
s.Log.Errorf("Failed to release population allocation for chickin %d: %+v", chickinID, err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal release alokasi population chickin")
|
||||
}
|
||||
if releaseResult.RowsAffected > 0 {
|
||||
s.Log.Infof(
|
||||
"Delete chickin rollback population id=%d released_population_alloc=%d",
|
||||
chickinID,
|
||||
releaseResult.RowsAffected,
|
||||
)
|
||||
}
|
||||
|
||||
deleteResult := tx.WithContext(ctx).
|
||||
Where("id IN ?", populationIDs).
|
||||
Delete(&entity.ProjectFlockPopulation{})
|
||||
if deleteResult.Error != nil {
|
||||
err := deleteResult.Error
|
||||
s.Log.Errorf("Failed to delete populations for chickin %d: %+v", chickinID, err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal menghapus population chickin")
|
||||
}
|
||||
if deleteResult.RowsAffected > 0 {
|
||||
s.Log.Infof(
|
||||
"Delete chickin rollback population id=%d deleted_population=%d",
|
||||
chickinID,
|
||||
deleteResult.RowsAffected,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isForeignKeyViolation(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
@@ -561,6 +952,9 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
|
||||
}
|
||||
|
||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||
if err := s.ensurePopulationRouteScope(c.Context(), dbTransaction); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||
chickinRepoTx := repository.NewChickinRepository(dbTransaction)
|
||||
@@ -818,6 +1212,9 @@ func (s *chickinService) ReplenishChickinStocks(ctx context.Context, tx *gorm.DB
|
||||
if asOf.IsZero() {
|
||||
asOf = chickin.CreatedAt
|
||||
}
|
||||
if err := s.ensurePopulationRouteScope(ctx, tx); err != nil {
|
||||
return err
|
||||
}
|
||||
return reflowChickinScope(ctx, s.FifoStockV2Svc, tx, targetPW.Id, &asOf)
|
||||
}
|
||||
|
||||
@@ -828,14 +1225,298 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
|
||||
if tx == nil {
|
||||
return errors.New("transaction is required")
|
||||
}
|
||||
if chickin.ProductWarehouseId == 0 {
|
||||
return s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, 0, 0)
|
||||
}
|
||||
qtyBefore, hasQtyBefore := s.tryLoadWarehouseQty(ctx, tx, chickin.ProductWarehouseId)
|
||||
|
||||
if err := s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, 0, 0); err != nil {
|
||||
var activeConsumeCount int64
|
||||
if err := tx.WithContext(ctx).
|
||||
Model(&entity.StockAllocation{}).
|
||||
Where("usable_type = ? AND usable_id = ? AND status = ? AND allocation_purpose = ?",
|
||||
fifo.UsableKeyProjectChickin.String(),
|
||||
chickin.Id,
|
||||
entity.StockAllocationStatusActive,
|
||||
entity.StockAllocationPurposeConsume,
|
||||
).
|
||||
Count(&activeConsumeCount).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if activeConsumeCount == 0 || s.FifoStockV2Svc == nil {
|
||||
s.Log.Infof(
|
||||
"Release chickin stock fallback id=%d active_consume_alloc=%d fifo_available=%t",
|
||||
chickin.Id,
|
||||
activeConsumeCount,
|
||||
s.FifoStockV2Svc != nil,
|
||||
)
|
||||
if err := s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, 0, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
s.logWarehouseQtySnapshot(
|
||||
ctx,
|
||||
tx,
|
||||
chickin.ProductWarehouseId,
|
||||
"release_chickin_fallback_no_active_alloc",
|
||||
chickin.Id,
|
||||
hasQtyBefore,
|
||||
qtyBefore,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
shouldRestoreWarehouseQty := true
|
||||
if s.ProjectflockKandangRepo != nil && chickin.ProjectFlockKandangId != 0 {
|
||||
pfk, err := s.ProjectflockKandangRepo.GetByIDLight(ctx, chickin.ProjectFlockKandangId)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
if err == nil && pfk != nil {
|
||||
category := strings.ToUpper(strings.TrimSpace(pfk.ProjectFlock.Category))
|
||||
if category == strings.ToUpper(string(utils.ProjectFlockCategoryLaying)) {
|
||||
shouldRestoreWarehouseQty = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !shouldRestoreWarehouseQty {
|
||||
var affectedTransferTargetIDs []uint
|
||||
if err := tx.WithContext(ctx).
|
||||
Model(&entity.StockAllocation{}).
|
||||
Where("usable_type = ? AND usable_id = ? AND status = ? AND allocation_purpose = ? AND stockable_type = ?",
|
||||
fifo.UsableKeyProjectChickin.String(),
|
||||
chickin.Id,
|
||||
entity.StockAllocationStatusActive,
|
||||
entity.StockAllocationPurposeConsume,
|
||||
fifo.StockableKeyTransferToLayingIn.String(),
|
||||
).
|
||||
Pluck("stockable_id", &affectedTransferTargetIDs).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
releaseResult := tx.WithContext(ctx).
|
||||
Model(&entity.StockAllocation{}).
|
||||
Where("usable_type = ? AND usable_id = ? AND status = ? AND allocation_purpose = ?",
|
||||
fifo.UsableKeyProjectChickin.String(),
|
||||
chickin.Id,
|
||||
entity.StockAllocationStatusActive,
|
||||
entity.StockAllocationPurposeConsume,
|
||||
).
|
||||
Updates(map[string]any{
|
||||
"status": entity.StockAllocationStatusReleased,
|
||||
"released_at": now,
|
||||
"note": "chickin rollback without qty adjust",
|
||||
})
|
||||
if releaseResult.Error != nil {
|
||||
err := releaseResult.Error
|
||||
return err
|
||||
}
|
||||
s.Log.Infof(
|
||||
"Release chickin stock laying id=%d released_consume_alloc=%d transfer_targets=%d",
|
||||
chickin.Id,
|
||||
releaseResult.RowsAffected,
|
||||
len(affectedTransferTargetIDs),
|
||||
)
|
||||
if err := s.resyncTransferTargetUsageFromAllocations(ctx, tx, affectedTransferTargetIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, 0, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
s.logWarehouseQtySnapshot(
|
||||
ctx,
|
||||
tx,
|
||||
chickin.ProductWarehouseId,
|
||||
"release_chickin_laying_no_restore",
|
||||
chickin.Id,
|
||||
hasQtyBefore,
|
||||
qtyBefore,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
rollbackResult, err := s.FifoStockV2Svc.Rollback(ctx, commonSvc.FifoStockV2RollbackRequest{
|
||||
ProductWarehouseID: chickin.ProductWarehouseId,
|
||||
Usable: commonSvc.FifoStockV2Ref{
|
||||
ID: chickin.Id,
|
||||
LegacyTypeKey: fifo.UsableKeyProjectChickin.String(),
|
||||
FunctionCode: "CHICKIN_OUT",
|
||||
},
|
||||
Reason: "delete/reject chickin rollback",
|
||||
Tx: tx,
|
||||
})
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to rollback FIFO v2 for chickin %d: %+v", chickin.Id, err)
|
||||
return err
|
||||
}
|
||||
releasedQty := 0.0
|
||||
detailCount := 0
|
||||
if rollbackResult != nil {
|
||||
releasedQty = rollbackResult.ReleasedQty
|
||||
detailCount = len(rollbackResult.Details)
|
||||
}
|
||||
s.Log.Infof(
|
||||
"Release chickin stock fifo rollback id=%d released_qty=%.3f detail_count=%d",
|
||||
chickin.Id,
|
||||
releasedQty,
|
||||
detailCount,
|
||||
)
|
||||
s.logWarehouseQtySnapshot(
|
||||
ctx,
|
||||
tx,
|
||||
chickin.ProductWarehouseId,
|
||||
"release_chickin_fifo_rollback",
|
||||
chickin.Id,
|
||||
hasQtyBefore,
|
||||
qtyBefore,
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *chickinService) tryLoadWarehouseQty(ctx context.Context, tx *gorm.DB, productWarehouseID uint) (float64, bool) {
|
||||
if tx == nil || productWarehouseID == 0 {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
type row struct {
|
||||
Qty float64 `gorm:"column:qty"`
|
||||
}
|
||||
out := row{}
|
||||
if err := tx.WithContext(ctx).
|
||||
Table("product_warehouses").
|
||||
Select("COALESCE(qty, 0) AS qty").
|
||||
Where("id = ?", productWarehouseID).
|
||||
Take(&out).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return 0, false
|
||||
}
|
||||
errText := strings.ToLower(strings.TrimSpace(err.Error()))
|
||||
if strings.Contains(errText, "no such column") && strings.Contains(errText, "qty") {
|
||||
return 0, false
|
||||
}
|
||||
if strings.Contains(errText, "column") && strings.Contains(errText, "qty") && strings.Contains(errText, "does not exist") {
|
||||
return 0, false
|
||||
}
|
||||
s.Log.Warnf("Failed to load warehouse qty snapshot pw=%d: %+v", productWarehouseID, err)
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return out.Qty, true
|
||||
}
|
||||
|
||||
func (s *chickinService) logWarehouseQtySnapshot(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
productWarehouseID uint,
|
||||
stage string,
|
||||
chickinID uint,
|
||||
hasBefore bool,
|
||||
beforeQty float64,
|
||||
) {
|
||||
afterQty, hasAfter := s.tryLoadWarehouseQty(ctx, tx, productWarehouseID)
|
||||
if !hasBefore && !hasAfter {
|
||||
return
|
||||
}
|
||||
|
||||
if hasBefore && hasAfter {
|
||||
s.Log.Infof(
|
||||
"Warehouse qty snapshot stage=%s chickin_id=%d pw=%d before=%.3f after=%.3f delta=%.3f",
|
||||
stage,
|
||||
chickinID,
|
||||
productWarehouseID,
|
||||
beforeQty,
|
||||
afterQty,
|
||||
afterQty-beforeQty,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if hasAfter {
|
||||
s.Log.Infof(
|
||||
"Warehouse qty snapshot stage=%s chickin_id=%d pw=%d after=%.3f",
|
||||
stage,
|
||||
chickinID,
|
||||
productWarehouseID,
|
||||
afterQty,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
s.Log.Infof(
|
||||
"Warehouse qty snapshot stage=%s chickin_id=%d pw=%d before=%.3f",
|
||||
stage,
|
||||
chickinID,
|
||||
productWarehouseID,
|
||||
beforeQty,
|
||||
)
|
||||
}
|
||||
|
||||
func (s *chickinService) resyncTransferTargetUsageFromAllocations(ctx context.Context, tx *gorm.DB, transferTargetIDs []uint) error {
|
||||
if tx == nil || len(transferTargetIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
unique := make([]uint, 0, len(transferTargetIDs))
|
||||
seen := make(map[uint]struct{}, len(transferTargetIDs))
|
||||
for _, id := range transferTargetIDs {
|
||||
if id == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
unique = append(unique, id)
|
||||
}
|
||||
if len(unique) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := tx.WithContext(ctx).
|
||||
Model(&entity.LayingTransferTarget{}).
|
||||
Where("id IN ?", unique).
|
||||
Update("total_used", 0).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type usageRow struct {
|
||||
StockableID uint `gorm:"column:stockable_id"`
|
||||
Used float64 `gorm:"column:used"`
|
||||
}
|
||||
var usageRows []usageRow
|
||||
if err := tx.WithContext(ctx).
|
||||
Table("stock_allocations").
|
||||
Select("stockable_id, COALESCE(SUM(qty), 0) AS used").
|
||||
Where("stockable_type = ?", fifo.StockableKeyTransferToLayingIn.String()).
|
||||
Where("status = ?", entity.StockAllocationStatusActive).
|
||||
Where("allocation_purpose = ?", entity.StockAllocationPurposeConsume).
|
||||
Where("stockable_id IN ?", unique).
|
||||
Group("stockable_id").
|
||||
Scan(&usageRows).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, row := range usageRows {
|
||||
if err := tx.WithContext(ctx).
|
||||
Model(&entity.LayingTransferTarget{}).
|
||||
Where("id = ?", row.StockableID).
|
||||
Update("total_used", row.Used).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeDateOnlyUTC(value time.Time) time.Time {
|
||||
if value.IsZero() {
|
||||
return time.Time{}
|
||||
}
|
||||
return time.Date(value.UTC().Year(), value.UTC().Month(), value.UTC().Day(), 0, 0, 0, 0, time.UTC)
|
||||
}
|
||||
|
||||
func (s *chickinService) syncChickinTraceForProductWarehouse(ctx context.Context, tx *gorm.DB, productWarehouseID uint) error {
|
||||
if productWarehouseID == 0 {
|
||||
return nil
|
||||
@@ -849,14 +1530,9 @@ func (s *chickinService) syncChickinTraceForProductWarehouse(ctx context.Context
|
||||
return s.syncChickinTraceForProductWarehouse(ctx, innerTx, productWarehouseID)
|
||||
})
|
||||
}
|
||||
|
||||
flagGroupCode, err := s.resolveFlagGroupByProductWarehouse(ctx, tx, productWarehouseID)
|
||||
if err != nil {
|
||||
if err := s.ensurePopulationRouteScope(ctx, tx); err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(flagGroupCode) == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if err := tx.WithContext(ctx).
|
||||
@@ -874,6 +1550,14 @@ func (s *chickinService) syncChickinTraceForProductWarehouse(ctx context.Context
|
||||
return err
|
||||
}
|
||||
|
||||
flagGroupCode, err := s.resolveFlagGroupByProductWarehouse(ctx, tx, productWarehouseID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(flagGroupCode) == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
type chickinTraceRow struct {
|
||||
ID uint `gorm:"column:id"`
|
||||
UsageQty float64 `gorm:"column:usage_qty"`
|
||||
|
||||
@@ -300,6 +300,12 @@ func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error {
|
||||
mapped := warehouseDTO.ToWarehouseRelationDTO(*warehouse)
|
||||
dtoResult.Warehouse = &mapped
|
||||
}
|
||||
if isTransition, isLaying, serr := u.ProjectflockService.GetProjectFlockKandangTransferState(c, result.Id); serr != nil {
|
||||
return serr
|
||||
} else {
|
||||
dtoResult.IsTransition = isTransition
|
||||
dtoResult.IsLaying = isLaying
|
||||
}
|
||||
if withPopulation {
|
||||
population := dtoResult.AvailableQuantity
|
||||
dtoResult.Population = &population
|
||||
|
||||
@@ -40,6 +40,8 @@ type ProjectFlockKandangDTO struct {
|
||||
AvailableQuantity float64 `json:"available_quantity"`
|
||||
Population *float64 `json:"population,omitempty"`
|
||||
ChickInDate *time.Time `json:"chick_in_date,omitempty"`
|
||||
IsTransition bool `json:"is_transition"`
|
||||
IsLaying bool `json:"is_laying"`
|
||||
}
|
||||
|
||||
func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDTO {
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
rProjectBudget "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
rProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
||||
rTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories"
|
||||
|
||||
sProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services"
|
||||
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
@@ -35,6 +36,7 @@ func (ProjectflockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, valid
|
||||
projectflockKandangRepo := rProjectflock.NewProjectFlockKandangRepository(db)
|
||||
projectFlockPopulationRepo := rProjectflock.NewProjectFlockPopulationRepository(db)
|
||||
recordingRepo := rRecording.NewRecordingRepository(db)
|
||||
transferLayingRepo := rTransferLaying.NewTransferLayingRepository(db)
|
||||
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||
projectBudgetRepo := rProjectBudget.NewProjectBudgetRepository(db)
|
||||
@@ -46,7 +48,7 @@ func (ProjectflockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, valid
|
||||
panic(fmt.Sprintf("failed to register project flock approval workflow: %v", err))
|
||||
}
|
||||
|
||||
projectflockService := sProjectflock.NewProjectflockService(projectflockRepo, flockRepo, kandangRepo, projectflockKandangRepo, warehouseRepo, productWarehouseRepo, projectBudgetRepo, nonstockRepo, projectFlockPopulationRepo, recordingRepo, approvalService, validate)
|
||||
projectflockService := sProjectflock.NewProjectflockService(projectflockRepo, flockRepo, kandangRepo, projectflockKandangRepo, warehouseRepo, productWarehouseRepo, projectBudgetRepo, nonstockRepo, projectFlockPopulationRepo, recordingRepo, transferLayingRepo, approvalService, validate)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
ProjectflockRoutes(router, userService, projectflockService)
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
|
||||
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"
|
||||
transferLayingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/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"
|
||||
@@ -44,6 +45,7 @@ type ProjectflockService interface {
|
||||
GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error)
|
||||
GetProjectFlockKandangPopulation(ctx *fiber.Ctx, projectFlockKandangID uint) (float64, error)
|
||||
GetProjectFlockKandangChickinDate(ctx *fiber.Ctx, projectFlockKandangID uint) (*time.Time, error)
|
||||
GetProjectFlockKandangTransferState(ctx *fiber.Ctx, projectFlockKandangID uint) (bool, bool, error)
|
||||
GetPeriodSummary(ctx *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error)
|
||||
GetProjectPeriods(ctx *fiber.Ctx, projectIDs []uint) (map[uint]int, error)
|
||||
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error)
|
||||
@@ -64,6 +66,7 @@ type projectflockService struct {
|
||||
PivotRepo repository.ProjectFlockKandangRepository
|
||||
PopulationRepo repository.ProjectFlockPopulationRepository
|
||||
RecordingRepo recordingRepo.RecordingRepository
|
||||
TransferLayingRepo transferLayingRepo.TransferLayingRepository
|
||||
ApprovalSvc commonSvc.ApprovalService
|
||||
approvalWorkflow approvalutils.ApprovalWorkflowKey
|
||||
}
|
||||
@@ -85,6 +88,7 @@ func NewProjectflockService(
|
||||
nonstockRepo nonstockRepository.NonstockRepository,
|
||||
populationRepo repository.ProjectFlockPopulationRepository,
|
||||
recordingRepo recordingRepo.RecordingRepository,
|
||||
transferLayingRepo transferLayingRepo.TransferLayingRepository,
|
||||
approvalSvc commonSvc.ApprovalService,
|
||||
validate *validator.Validate,
|
||||
|
||||
@@ -102,6 +106,7 @@ func NewProjectflockService(
|
||||
PivotRepo: pivotRepo,
|
||||
PopulationRepo: populationRepo,
|
||||
RecordingRepo: recordingRepo,
|
||||
TransferLayingRepo: transferLayingRepo,
|
||||
ApprovalSvc: approvalSvc,
|
||||
approvalWorkflow: utils.ApprovalWorkflowProjectFlock,
|
||||
}
|
||||
@@ -538,6 +543,63 @@ func (s projectflockService) GetProjectFlockKandangChickinDate(ctx *fiber.Ctx, p
|
||||
return earliest, nil
|
||||
}
|
||||
|
||||
func (s projectflockService) GetProjectFlockKandangTransferState(ctx *fiber.Ctx, projectFlockKandangID uint) (bool, bool, error) {
|
||||
if projectFlockKandangID == 0 || s.TransferLayingRepo == nil || s.PivotRepo == nil {
|
||||
return false, false, nil
|
||||
}
|
||||
|
||||
pfk, err := s.PivotRepo.GetByIDLight(ctx.Context(), projectFlockKandangID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return false, false, nil
|
||||
}
|
||||
s.Log.Errorf("Failed to resolve project flock kandang %d for transfer state: %+v", projectFlockKandangID, err)
|
||||
return false, false, fiber.NewError(fiber.StatusInternalServerError, "Failed to resolve transfer state")
|
||||
}
|
||||
|
||||
category := strings.ToUpper(strings.TrimSpace(pfk.ProjectFlock.Category))
|
||||
var transfer *entity.LayingTransfer
|
||||
switch category {
|
||||
case strings.ToUpper(string(utils.ProjectFlockCategoryGrowing)):
|
||||
transfer, err = s.TransferLayingRepo.GetLatestApprovedBySourceKandang(ctx.Context(), projectFlockKandangID)
|
||||
case strings.ToUpper(string(utils.ProjectFlockCategoryLaying)):
|
||||
transfer, err = s.TransferLayingRepo.GetLatestApprovedByTargetKandang(ctx.Context(), projectFlockKandangID)
|
||||
default:
|
||||
return false, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return false, false, nil
|
||||
}
|
||||
s.Log.Errorf("Failed to resolve transfer state for project flock kandang %d: %+v", projectFlockKandangID, err)
|
||||
return false, false, fiber.NewError(fiber.StatusInternalServerError, "Failed to resolve transfer state")
|
||||
}
|
||||
if transfer == nil {
|
||||
return false, false, nil
|
||||
}
|
||||
|
||||
physicalMoveDate := normalizeDateOnlyUTC(transfer.TransferDate)
|
||||
if physicalMoveDate.IsZero() {
|
||||
return false, false, nil
|
||||
}
|
||||
|
||||
economicCutoffDate := physicalMoveDate
|
||||
if transfer.EconomicCutoffDate != nil && !transfer.EconomicCutoffDate.IsZero() {
|
||||
economicCutoffDate = normalizeDateOnlyUTC(*transfer.EconomicCutoffDate)
|
||||
} else if transfer.EffectiveMoveDate != nil && !transfer.EffectiveMoveDate.IsZero() {
|
||||
economicCutoffDate = normalizeDateOnlyUTC(*transfer.EffectiveMoveDate)
|
||||
}
|
||||
if economicCutoffDate.Before(physicalMoveDate) {
|
||||
economicCutoffDate = physicalMoveDate
|
||||
}
|
||||
|
||||
referenceDate := normalizeDateOnlyUTC(time.Now().UTC())
|
||||
isTransition := !referenceDate.Before(physicalMoveDate) && referenceDate.Before(economicCutoffDate)
|
||||
isLaying := !referenceDate.Before(economicCutoffDate)
|
||||
|
||||
return isTransition, isLaying, nil
|
||||
}
|
||||
|
||||
func (s projectflockService) GetProjectFlockKandangByParams(ctx *fiber.Ctx, idStr string, projectFlockIdStr string, kandangIdStr string) (*entity.ProjectFlockKandang, float64, error) {
|
||||
idStr = strings.TrimSpace(idStr)
|
||||
projectFlockIdStr = strings.TrimSpace(projectFlockIdStr)
|
||||
@@ -579,6 +641,10 @@ func (s projectflockService) GetProjectFlockKandangByParams(ctx *fiber.Ctx, idSt
|
||||
return s.GetProjectFlockKandangByProjectAndKandang(ctx, uint(pfid), uint(kid))
|
||||
}
|
||||
|
||||
func normalizeDateOnlyUTC(value time.Time) time.Time {
|
||||
return time.Date(value.UTC().Year(), value.UTC().Month(), value.UTC().Day(), 0, 0, 0, 0, time.UTC)
|
||||
}
|
||||
|
||||
func (s projectflockService) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error) {
|
||||
if s.PopulationRepo == nil {
|
||||
return 0, fiber.NewError(fiber.StatusInternalServerError, "Project flock population repository is not configured")
|
||||
|
||||
@@ -86,6 +86,8 @@ type RecordingRelationDTO struct {
|
||||
EggWeight float64 `json:"egg_weight"`
|
||||
PopulationCanChange bool `json:"population_can_change"`
|
||||
TransferExecuted *bool `json:"transfer_executed,omitempty"`
|
||||
IsTransition bool `json:"is_transition"`
|
||||
IsLaying bool `json:"is_laying"`
|
||||
Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
|
||||
}
|
||||
|
||||
@@ -247,6 +249,8 @@ func toRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
|
||||
EggWeight: floatValue(e.EggWeight),
|
||||
PopulationCanChange: boolValueDefault(e.PopulationCanChange, true),
|
||||
TransferExecuted: e.TransferExecuted,
|
||||
IsTransition: boolValueDefault(e.IsTransition, false),
|
||||
IsLaying: boolValueDefault(e.IsLaying, false),
|
||||
Approval: latestApproval,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +125,7 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
||||
nonstockRepo,
|
||||
projectFlockPopulationRepo,
|
||||
recordingRepo,
|
||||
transferLayingRepo,
|
||||
approvalService,
|
||||
validate,
|
||||
)
|
||||
|
||||
@@ -185,12 +185,14 @@ func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
|
||||
rate := recordingutil.ComputeDepletionRate(prev, current, totalChick)
|
||||
recordings[i].DepletionRate = &rate
|
||||
|
||||
populationCanChange, transferExecuted, _, _, stateErr := s.evaluatePopulationMutationState(c.Context(), &recordings[i])
|
||||
populationCanChange, transferExecuted, isTransition, isLaying, _, _, stateErr := s.evaluatePopulationMutationState(c.Context(), &recordings[i])
|
||||
if stateErr != nil {
|
||||
return nil, 0, stateErr
|
||||
}
|
||||
recordings[i].PopulationCanChange = boolPtr(populationCanChange)
|
||||
recordings[i].TransferExecuted = boolPtr(transferExecuted)
|
||||
recordings[i].IsTransition = boolPtr(isTransition)
|
||||
recordings[i].IsLaying = boolPtr(isLaying)
|
||||
}
|
||||
return recordings, total, nil
|
||||
}
|
||||
@@ -251,12 +253,14 @@ func (s recordingService) GetOne(c *fiber.Ctx, id uint) (*entity.Recording, erro
|
||||
recording.DepletionRate = &rate
|
||||
}
|
||||
|
||||
populationCanChange, transferExecuted, _, _, stateErr := s.evaluatePopulationMutationState(c.Context(), recording)
|
||||
populationCanChange, transferExecuted, isTransition, isLaying, _, _, stateErr := s.evaluatePopulationMutationState(c.Context(), recording)
|
||||
if stateErr != nil {
|
||||
return nil, stateErr
|
||||
}
|
||||
recording.PopulationCanChange = boolPtr(populationCanChange)
|
||||
recording.TransferExecuted = boolPtr(transferExecuted)
|
||||
recording.IsTransition = boolPtr(isTransition)
|
||||
recording.IsLaying = boolPtr(isLaying)
|
||||
|
||||
return recording, nil
|
||||
}
|
||||
@@ -990,46 +994,58 @@ func (s *recordingService) resolveRecordingCategory(ctx context.Context, recordi
|
||||
return strings.ToUpper(strings.TrimSpace(pfk.ProjectFlock.Category)), nil
|
||||
}
|
||||
|
||||
func (s *recordingService) evaluatePopulationMutationState(ctx context.Context, recording *entity.Recording) (bool, bool, *entity.LayingTransfer, time.Time, error) {
|
||||
func (s *recordingService) evaluatePopulationMutationState(ctx context.Context, recording *entity.Recording) (bool, bool, bool, bool, *entity.LayingTransfer, time.Time, error) {
|
||||
if recording == nil || recording.ProjectFlockKandangId == 0 || s.TransferLayingRepo == nil {
|
||||
return true, false, nil, time.Time{}, nil
|
||||
return true, false, false, false, nil, time.Time{}, nil
|
||||
}
|
||||
|
||||
category, err := s.resolveRecordingCategory(ctx, recording)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to resolve recording category for population mutation check (recording=%d): %+v", recording.Id, err)
|
||||
return true, false, nil, time.Time{}, fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi perubahan populasi recording")
|
||||
}
|
||||
if category != strings.ToUpper(string(utils.ProjectFlockCategoryGrowing)) {
|
||||
return true, false, nil, time.Time{}, nil
|
||||
return true, false, false, false, nil, time.Time{}, fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi perubahan populasi recording")
|
||||
}
|
||||
|
||||
transfer, err := s.TransferLayingRepo.GetLatestApprovedBySourceKandang(ctx, recording.ProjectFlockKandangId)
|
||||
var transfer *entity.LayingTransfer
|
||||
switch category {
|
||||
case strings.ToUpper(string(utils.ProjectFlockCategoryGrowing)):
|
||||
transfer, err = s.TransferLayingRepo.GetLatestApprovedBySourceKandang(ctx, recording.ProjectFlockKandangId)
|
||||
case strings.ToUpper(string(utils.ProjectFlockCategoryLaying)):
|
||||
transfer, err = s.TransferLayingRepo.GetLatestApprovedByTargetKandang(ctx, recording.ProjectFlockKandangId)
|
||||
default:
|
||||
return true, false, false, false, nil, time.Time{}, nil
|
||||
}
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return true, false, nil, time.Time{}, nil
|
||||
return true, false, false, false, nil, time.Time{}, nil
|
||||
}
|
||||
s.Log.Errorf("Failed to resolve approved transfer by source kandang for recording %d: %+v", recording.Id, err)
|
||||
return true, false, nil, time.Time{}, fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi perubahan populasi recording")
|
||||
s.Log.Errorf("Failed to resolve approved transfer for recording %d: %+v", recording.Id, err)
|
||||
return true, false, false, false, nil, time.Time{}, fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi perubahan populasi recording")
|
||||
}
|
||||
if transfer == nil {
|
||||
return true, false, nil, time.Time{}, nil
|
||||
return true, false, false, false, nil, time.Time{}, nil
|
||||
}
|
||||
|
||||
transferDate := transferPhysicalMoveDate(transfer)
|
||||
if transferDate.IsZero() {
|
||||
return true, false, transfer, transferDate, nil
|
||||
return true, false, false, false, transfer, transferDate, nil
|
||||
}
|
||||
|
||||
transferExecuted := transfer.ExecutedAt != nil && !transfer.ExecutedAt.IsZero()
|
||||
recordDate := normalizeDateOnlyUTC(recording.RecordDatetime)
|
||||
populationCanChange := !(transferExecuted && !recordDate.Before(transferDate))
|
||||
_, economicCutoffDate := transferRecordingWindow(transfer)
|
||||
isTransition := !recordDate.Before(transferDate) && recordDate.Before(economicCutoffDate)
|
||||
isLaying := !recordDate.Before(economicCutoffDate)
|
||||
|
||||
return populationCanChange, transferExecuted, transfer, transferDate, nil
|
||||
populationCanChange := true
|
||||
if category == strings.ToUpper(string(utils.ProjectFlockCategoryGrowing)) {
|
||||
populationCanChange = !(transferExecuted && !recordDate.Before(transferDate))
|
||||
}
|
||||
|
||||
return populationCanChange, transferExecuted, isTransition, isLaying, transfer, transferDate, nil
|
||||
}
|
||||
|
||||
func (s *recordingService) ensurePopulationMutationAllowed(ctx context.Context, recording *entity.Recording, operation string) error {
|
||||
populationCanChange, _, transfer, transferDate, err := s.evaluatePopulationMutationState(ctx, recording)
|
||||
populationCanChange, _, _, _, transfer, transferDate, err := s.evaluatePopulationMutationState(ctx, recording)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1590,7 +1606,7 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
|
||||
|
||||
var feedIntake float64
|
||||
if remainingChick > 0 && usageInGrams > 0 {
|
||||
feedIntake = (usageInGrams / remainingChick) * 1000
|
||||
feedIntake = usageInGrams / remainingChick
|
||||
updates["feed_intake"] = feedIntake
|
||||
recording.FeedIntake = &feedIntake
|
||||
} else {
|
||||
|
||||
@@ -15,6 +15,11 @@ const (
|
||||
transferLayingInFunctionCode = "TRANSFER_TO_LAYING_IN"
|
||||
transferLayingStockableLane = "STOCKABLE"
|
||||
transferLayingSourceTable = "laying_transfer_targets"
|
||||
|
||||
transferLayingOutFunctionCode = "TRANSFER_TO_LAYING_OUT"
|
||||
transferLayingUsableLane = "USABLE"
|
||||
transferLayingUsableSourceTable = "laying_transfers"
|
||||
transferLayingLegacyUsableSourceTable = "laying_transfer_sources"
|
||||
)
|
||||
|
||||
func reflowTransferLayingScope(
|
||||
@@ -85,3 +90,90 @@ func resolveTransferLayingFlagGroupByProductWarehouse(ctx context.Context, tx *g
|
||||
|
||||
return strings.TrimSpace(selected.FlagGroupCode), nil
|
||||
}
|
||||
|
||||
type transferLayingUsableRouteRule struct {
|
||||
FlagGroupCode string `gorm:"column:flag_group_code"`
|
||||
SourceTable string `gorm:"column:source_table"`
|
||||
}
|
||||
|
||||
func resolveTransferLayingUsableFlagGroupByProductWarehouse(ctx context.Context, tx *gorm.DB, productWarehouseID uint) (string, error) {
|
||||
rows := make([]transferLayingUsableRouteRule, 0)
|
||||
err := tx.WithContext(ctx).
|
||||
Table("fifo_stock_v2_route_rules rr").
|
||||
Select("rr.flag_group_code, rr.source_table").
|
||||
Joins("JOIN fifo_stock_v2_flag_groups fg ON fg.code = rr.flag_group_code AND fg.is_active = TRUE").
|
||||
Where("rr.is_active = TRUE").
|
||||
Where("rr.lane = ?", transferLayingUsableLane).
|
||||
Where("rr.function_code = ?", transferLayingOutFunctionCode).
|
||||
Where(`
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM product_warehouses pw
|
||||
JOIN flags f ON f.flagable_id = pw.product_id
|
||||
JOIN fifo_stock_v2_flag_members fm ON fm.flag_name = f.name AND fm.is_active = TRUE
|
||||
WHERE pw.id = ?
|
||||
AND f.flagable_type = ?
|
||||
AND fm.flag_group_code = rr.flag_group_code
|
||||
)
|
||||
`, productWarehouseID, entity.FlagableTypeProduct).
|
||||
Order("rr.id ASC").
|
||||
Find(&rows).Error
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return validateTransferLayingUsableRouteRules(rows, productWarehouseID)
|
||||
}
|
||||
|
||||
func validateTransferLayingUsableRouteRules(rows []transferLayingUsableRouteRule, productWarehouseID uint) (string, error) {
|
||||
if len(rows) == 0 {
|
||||
return "", fmt.Errorf(
|
||||
"konfigurasi FIFO v2 TRANSFER_TO_LAYING_OUT tidak ditemukan untuk source warehouse %d",
|
||||
productWarehouseID,
|
||||
)
|
||||
}
|
||||
|
||||
var selectedFlagGroup string
|
||||
hasHeaderRule := false
|
||||
hasLegacyRule := false
|
||||
|
||||
for _, row := range rows {
|
||||
sourceTable := strings.ToLower(strings.TrimSpace(row.SourceTable))
|
||||
flagGroupCode := strings.TrimSpace(row.FlagGroupCode)
|
||||
|
||||
switch sourceTable {
|
||||
case transferLayingUsableSourceTable:
|
||||
if flagGroupCode == "" {
|
||||
return "", fmt.Errorf("konfigurasi FIFO v2 TRANSFER_TO_LAYING_OUT memiliki flag_group_code kosong")
|
||||
}
|
||||
hasHeaderRule = true
|
||||
if selectedFlagGroup == "" {
|
||||
selectedFlagGroup = flagGroupCode
|
||||
continue
|
||||
}
|
||||
if selectedFlagGroup != flagGroupCode {
|
||||
return "", fmt.Errorf(
|
||||
"konfigurasi FIFO v2 TRANSFER_TO_LAYING_OUT ambigu untuk source warehouse %d",
|
||||
productWarehouseID,
|
||||
)
|
||||
}
|
||||
case transferLayingLegacyUsableSourceTable:
|
||||
hasLegacyRule = true
|
||||
}
|
||||
}
|
||||
|
||||
if hasLegacyRule {
|
||||
return "", fmt.Errorf(
|
||||
"konfigurasi FIFO v2 legacy untuk TRANSFER_TO_LAYING_OUT masih aktif (source_table=%s)",
|
||||
transferLayingLegacyUsableSourceTable,
|
||||
)
|
||||
}
|
||||
if !hasHeaderRule {
|
||||
return "", fmt.Errorf(
|
||||
"konfigurasi FIFO v2 TRANSFER_TO_LAYING_OUT aktif untuk source_table=%s tidak ditemukan",
|
||||
transferLayingUsableSourceTable,
|
||||
)
|
||||
}
|
||||
|
||||
return selectedFlagGroup, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateTransferLayingUsableRouteRules(t *testing.T) {
|
||||
t.Run("valid header rule", func(t *testing.T) {
|
||||
flagGroup, err := validateTransferLayingUsableRouteRules([]transferLayingUsableRouteRule{
|
||||
{FlagGroupCode: "AYAM", SourceTable: transferLayingUsableSourceTable},
|
||||
}, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if flagGroup != "AYAM" {
|
||||
t.Fatalf("unexpected flag group: %s", flagGroup)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("missing usable header rule", func(t *testing.T) {
|
||||
_, err := validateTransferLayingUsableRouteRules(nil, 10)
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if !strings.Contains(strings.ToLower(err.Error()), "tidak ditemukan") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("legacy rule still active", func(t *testing.T) {
|
||||
_, err := validateTransferLayingUsableRouteRules([]transferLayingUsableRouteRule{
|
||||
{FlagGroupCode: "AYAM", SourceTable: transferLayingUsableSourceTable},
|
||||
{FlagGroupCode: "AYAM", SourceTable: transferLayingLegacyUsableSourceTable},
|
||||
}, 10)
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if !strings.Contains(strings.ToLower(err.Error()), "legacy") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ambiguous active header rules", func(t *testing.T) {
|
||||
_, err := validateTransferLayingUsableRouteRules([]transferLayingUsableRouteRule{
|
||||
{FlagGroupCode: "AYAM", SourceTable: transferLayingUsableSourceTable},
|
||||
{FlagGroupCode: "PAKAN", SourceTable: transferLayingUsableSourceTable},
|
||||
}, 10)
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if !strings.Contains(strings.ToLower(err.Error()), "ambigu") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1026,6 +1026,33 @@ func (s transferLayingService) Unexecute(c *fiber.Ctx, id uint) (*entity.LayingT
|
||||
}
|
||||
}
|
||||
|
||||
flagGroupCode, err := resolveTransferLayingUsableFlagGroupByProductWarehouse(
|
||||
c.Context(),
|
||||
dbTransaction,
|
||||
*transfer.SourceProductWarehouseId,
|
||||
)
|
||||
if err != nil {
|
||||
return fiber.NewError(
|
||||
fiber.StatusBadRequest,
|
||||
fmt.Sprintf("Konfigurasi FIFO v2 transfer laying tidak valid: %v", err),
|
||||
)
|
||||
}
|
||||
activeConsumeAllocCount, err := s.countActiveTransferSourceConsumeAllocations(
|
||||
c.Context(),
|
||||
dbTransaction,
|
||||
transfer.Id,
|
||||
*transfer.SourceProductWarehouseId,
|
||||
)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi alokasi FIFO source transfer laying")
|
||||
}
|
||||
if transfer.SourceUsageQty > 1e-6 && activeConsumeAllocCount == 0 {
|
||||
return fiber.NewError(
|
||||
fiber.StatusBadRequest,
|
||||
fmt.Sprintf("Unexecute transfer laying %s gagal: alokasi FIFO source tidak ditemukan", transfer.TransferNumber),
|
||||
)
|
||||
}
|
||||
|
||||
for _, target := range targets {
|
||||
if target.ProductWarehouseId == nil || *target.ProductWarehouseId == 0 {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Target product warehouse tidak ditemukan untuk transfer %d", transfer.Id))
|
||||
@@ -1067,21 +1094,40 @@ func (s transferLayingService) Unexecute(c *fiber.Ctx, id uint) (*entity.LayingT
|
||||
}
|
||||
}
|
||||
|
||||
asOf := normalizeDateOnlyUTC(transfer.TransferDate)
|
||||
rollbackResult, err := s.FifoStockV2Svc.Rollback(c.Context(), commonSvc.FifoStockV2RollbackRequest{
|
||||
ProductWarehouseID: *transfer.SourceProductWarehouseId,
|
||||
Usable: commonSvc.FifoStockV2Ref{
|
||||
ID: transfer.Id,
|
||||
LegacyTypeKey: fifo.UsableKeyTransferToLayingOut.String(),
|
||||
FunctionCode: transferLayingOutFunctionCode,
|
||||
},
|
||||
Reason: fmt.Sprintf("transfer laying unexecute #%s [%s]", transfer.TransferNumber, flagGroupCode),
|
||||
Tx: dbTransaction,
|
||||
})
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gagal rollback FIFO v2 source transfer laying: %v", err))
|
||||
}
|
||||
releasedQty := 0.0
|
||||
if rollbackResult != nil {
|
||||
releasedQty = rollbackResult.ReleasedQty
|
||||
}
|
||||
if transfer.SourceUsageQty > 1e-6 && releasedQty < transfer.SourceUsageQty-1e-6 {
|
||||
return fiber.NewError(
|
||||
fiber.StatusBadRequest,
|
||||
fmt.Sprintf(
|
||||
"Rollback FIFO v2 source transfer laying tidak lengkap. Dibutuhkan %.3f, terlepas %.3f",
|
||||
transfer.SourceUsageQty,
|
||||
releasedQty,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if err := repoTx.PatchOne(c.Context(), transfer.Id, map[string]any{
|
||||
"source_usage_qty": 0,
|
||||
"source_pending_usage_qty": 0,
|
||||
}, nil); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal reset kuantitas source transfer laying")
|
||||
}
|
||||
if _, err := s.FifoStockV2Svc.Reflow(c.Context(), commonSvc.FifoStockV2ReflowRequest{
|
||||
FlagGroupCode: transferToLayingFlagGroupCode,
|
||||
ProductWarehouseID: *transfer.SourceProductWarehouseId,
|
||||
AsOf: &asOf,
|
||||
Tx: dbTransaction,
|
||||
}); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gagal rollback FIFO v2 source transfer laying: %v", err))
|
||||
}
|
||||
if err := fifoV2.ReleasePopulationConsumptionByUsable(
|
||||
c.Context(),
|
||||
dbTransaction,
|
||||
@@ -1576,6 +1622,34 @@ func (s *transferLayingService) hasDownstreamRecordingOnTarget(
|
||||
return true, normalizeDateOnlyUTC(earliest.RecordDatetime), nil
|
||||
}
|
||||
|
||||
func (s *transferLayingService) countActiveTransferSourceConsumeAllocations(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
transferID uint,
|
||||
productWarehouseID uint,
|
||||
) (int64, error) {
|
||||
if transferID == 0 || productWarehouseID == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
if tx == nil {
|
||||
return 0, errors.New("transaction is required")
|
||||
}
|
||||
|
||||
var count int64
|
||||
if err := tx.WithContext(ctx).
|
||||
Model(&entity.StockAllocation{}).
|
||||
Where("product_warehouse_id = ?", productWarehouseID).
|
||||
Where("usable_type = ?", fifo.UsableKeyTransferToLayingOut.String()).
|
||||
Where("usable_id = ?", transferID).
|
||||
Where("status = ?", entity.StockAllocationStatusActive).
|
||||
Where("allocation_purpose = ?", entity.StockAllocationPurposeConsume).
|
||||
Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (s *transferLayingService) resyncPopulationUsageByProjectFlockKandang(ctx context.Context, tx *gorm.DB, projectFlockKandangID uint) error {
|
||||
if projectFlockKandangID == 0 {
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user