Compare commits

..

2 Commits

Author SHA1 Message Date
giovanni f64839dfe1 add delete snapshoot if change chickin date 2026-06-04 23:46:25 +07:00
giovanni 0ff720453f add api for edit chickin date 2026-06-03 11:56:32 +07:00
8 changed files with 71 additions and 77 deletions
@@ -1,17 +0,0 @@
BEGIN;
-- Revert the TELUR / TELUR_GRADE marketing over-sell block. Removing these rows
-- makes resolveOverConsume() fall back to the default allow rule again (the
-- post-20260313061525 behaviour). The reasons are unique to this migration, so
-- the DELETE only touches rows created here.
DELETE FROM fifo_stock_v2_overconsume_rules
WHERE lane = 'USABLE'
AND function_code = 'MARKETING_OUT'
AND flag_group_code IN ('TELUR', 'TELUR_GRADE')
AND reason IN (
'fifo_v2_exception_marketing_block_telur',
'fifo_v2_exception_marketing_block_telur_grade'
);
COMMIT;
@@ -1,54 +0,0 @@
BEGIN;
-- Restore the marketing over-sell block for TELUR and TELUR_GRADE only.
--
-- Migration 20260313061525 narrowed the MARKETING_OUT over-sell block to
-- flag_group_code='AYAM' and deactivated the global rule. That left TELUR /
-- TELUR_GRADE with no matching block, so resolveOverConsume() fell back to the
-- default rule 'fifo_v2_default_allow' (allow_overconsume=TRUE) and egg
-- Delivery Orders could over-sell silently into marketing_delivery_products.pending_qty.
--
-- These rules make resolveOverConsume('TELUR'|'TELUR_GRADE','MARKETING_OUT') = FALSE,
-- so an egg DO that exceeds available stock is REJECTED (ErrInsufficientStock)
-- instead of being recorded as pending. Scope is "Telur saja" — AYAM and
-- transfer behaviour are intentionally left unchanged.
--
-- NOTE: run the total_used reconciliation (cmd/reconcile-fifo-total-used) BEFORE
-- applying this in production. Enabling the block while phantom total_used still
-- inflates consumption would reject otherwise-valid egg orders.
INSERT INTO fifo_stock_v2_overconsume_rules(flag_group_code, function_code, lane, allow_overconsume, priority, reason, is_active)
SELECT 'TELUR', 'MARKETING_OUT', 'USABLE', FALSE, 20, 'fifo_v2_exception_marketing_block_telur', TRUE
WHERE NOT EXISTS (
SELECT 1 FROM fifo_stock_v2_overconsume_rules
WHERE lane = 'USABLE'
AND function_code = 'MARKETING_OUT'
AND flag_group_code = 'TELUR'
AND reason = 'fifo_v2_exception_marketing_block_telur'
);
UPDATE fifo_stock_v2_overconsume_rules
SET allow_overconsume = FALSE, priority = 20, is_active = TRUE
WHERE lane = 'USABLE'
AND function_code = 'MARKETING_OUT'
AND flag_group_code = 'TELUR'
AND reason = 'fifo_v2_exception_marketing_block_telur';
INSERT INTO fifo_stock_v2_overconsume_rules(flag_group_code, function_code, lane, allow_overconsume, priority, reason, is_active)
SELECT 'TELUR_GRADE', 'MARKETING_OUT', 'USABLE', FALSE, 20, 'fifo_v2_exception_marketing_block_telur_grade', TRUE
WHERE NOT EXISTS (
SELECT 1 FROM fifo_stock_v2_overconsume_rules
WHERE lane = 'USABLE'
AND function_code = 'MARKETING_OUT'
AND flag_group_code = 'TELUR_GRADE'
AND reason = 'fifo_v2_exception_marketing_block_telur_grade'
);
UPDATE fifo_stock_v2_overconsume_rules
SET allow_overconsume = FALSE, priority = 20, is_active = TRUE
WHERE lane = 'USABLE'
AND function_code = 'MARKETING_OUT'
AND flag_group_code = 'TELUR_GRADE'
AND reason = 'fifo_v2_exception_marketing_block_telur_grade';
COMMIT;
@@ -964,10 +964,7 @@ func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gor
marketingProduct.ProductWarehouseId,
resolveMarketingAsOf(deliveryProduct.DeliveryDate, deliveryProduct.CreatedAt),
); err != nil {
if errors.Is(err, fifoV2.ErrInsufficientStock) {
return fiber.NewError(fiber.StatusBadRequest, "Stok tidak mencukupi untuk memenuhi permintaan delivery order ini")
}
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gagal mengalokasikan stok: %v", err))
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock for product warehouse %d: %v", marketingProduct.ProductWarehouseId, err))
}
refreshed, err := deliveryProductRepo.GetByID(ctx, deliveryProduct.Id, nil)
@@ -172,6 +172,23 @@ func (u *ChickinController) DeleteOne(c *fiber.Ctx) error {
})
}
func (u *ChickinController) UpdateChickInDate(c *fiber.Ctx) error {
req := new(validation.UpdateChickInDate)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
if err := u.ChickinService.UpdateChickInDate(c, req); err != nil {
return err
}
return c.Status(fiber.StatusOK).JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Chick in date berhasil diperbarui",
})
}
func (u *ChickinController) Approval(c *fiber.Ctx) error {
req := new(validation.Approve)
@@ -2,6 +2,7 @@ package repository
import (
"context"
"time"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -18,6 +19,7 @@ type ProjectChickinRepository interface {
GetTotalChickinQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error)
GetByProjectFlockKandangIDForUpdate(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error)
UpdateUsageFields(ctx context.Context, tx *gorm.DB, chickinID uint, usageQty, pendingUsageQty float64) error
UpdateChickInDateByProjectFlockKandangID(ctx context.Context, tx *gorm.DB, pfkID uint, newDate time.Time) error
}
type ChickinRepositoryImpl struct {
@@ -134,3 +136,10 @@ func (r *ChickinRepositoryImpl) UpdateUsageFields(ctx context.Context, tx *gorm.
"pending_usage_qty": pendingUsageQty,
}).Error
}
func (r *ChickinRepositoryImpl) UpdateChickInDateByProjectFlockKandangID(ctx context.Context, tx *gorm.DB, pfkID uint, newDate time.Time) error {
return tx.WithContext(ctx).
Model(&entity.ProjectChickin{}).
Where("project_flock_kandang_id = ? AND deleted_at IS NULL", pfkID).
Update("chick_in_date", newDate).Error
}
@@ -17,6 +17,7 @@ func ChickinRoutes(v1 fiber.Router, u user.UserService, s chickin.ChickinService
route.Get("/", m.RequirePermissions(m.P_ChickinsGetAll), ctrl.GetAll)
route.Post("/", m.RequirePermissions(m.P_ChickinsCreateOne), ctrl.CreateOne)
route.Patch("/chick-in-date", m.RequirePermissions(m.P_ChickinsCreateOne), ctrl.UpdateChickInDate)
route.Get("/:id", m.RequirePermissions(m.P_ChickinsGetOne), ctrl.GetOne)
// route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
@@ -48,6 +48,7 @@ type ChickinService interface {
DeleteOne(ctx *fiber.Ctx, id uint) error
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectChickin, error)
EnsureChickInExists(ctx context.Context, projectFlockKandangID uint) error
UpdateChickInDate(ctx *fiber.Ctx, req *validation.UpdateChickInDate) error
}
type chickinService struct {
@@ -2110,3 +2111,38 @@ func (s chickinService) EnsureChickInExists(ctx context.Context, projectFlockKan
return fiber.NewError(fiber.StatusBadRequest, "Chick in project flock belum disetujui sehingga belum dapat membuat recording")
}
func (s chickinService) UpdateChickInDate(ctx *fiber.Ctx, req *validation.UpdateChickInDate) error {
if err := s.Validate.Struct(req); err != nil {
return err
}
newDate, err := time.Parse("2006-01-02", req.ChickInDate)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Format tanggal tidak valid, gunakan YYYY-MM-DD")
}
_, err = s.ProjectflockKandangRepo.GetByID(ctx.Context(), req.ProjectFlockKandangId)
if err != nil {
return fiber.NewError(fiber.StatusNotFound, "Project flock kandang tidak ditemukan")
}
if err := s.Repository.DB().WithContext(ctx.Context()).Transaction(func(tx *gorm.DB) error {
if err := s.Repository.UpdateChickInDateByProjectFlockKandangID(ctx.Context(), tx, req.ProjectFlockKandangId, newDate); err != nil {
return err
}
return tx.Exec(`
UPDATE recordings
SET day = GREATEST(0, (record_datetime::date - ?::date)::int),
updated_at = NOW()
WHERE project_flock_kandangs_id = ?
AND deleted_at IS NULL
`, req.ChickInDate, req.ProjectFlockKandangId).Error
}); err != nil {
return err
}
s.invalidateDepreciationSnapshots(ctx.Context(), nil, []uint{req.ProjectFlockKandangId}, newDate)
return nil
}
@@ -27,3 +27,8 @@ type Approve struct {
ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
}
type UpdateChickInDate struct {
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,gt=0"`
ChickInDate string `json:"chick_in_date" validate:"required,datetime=2006-01-02"`
}