mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-23 06:45:43 +00:00
Fix transfer to laying delete and fix chikin delete with response recording
This commit is contained in:
@@ -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"`
|
||||
|
||||
Reference in New Issue
Block a user