fix: adjusment module depletion, chickin, recording refactor

This commit is contained in:
Hafizh A. Y
2026-02-28 21:35:07 +07:00
parent 944604adad
commit dd61b66af0
11 changed files with 1906 additions and 1343 deletions
@@ -46,6 +46,7 @@ type adjustmentService struct {
const (
adjustmentLaneStockable = "STOCKABLE"
adjustmentLaneUsable = "USABLE"
flagGroupAyam = "AYAM"
)
func NewAdjustmentService(
@@ -129,8 +130,11 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
if functionCode == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "Transaction subtype is required")
}
if functionCode == string(utils.AdjustmentTransactionSubtypeRecordingDepletionIn) {
functionCode = string(utils.AdjustmentTransactionSubtypeRecordingDepletionOut)
if functionCode == string(utils.AdjustmentTransactionSubtypeRecordingDepletionOut) {
return nil, fiber.NewError(
fiber.StatusBadRequest,
"RECORDING_DEPLETION_OUT tidak boleh diinput manual. Gunakan RECORDING_DEPLETION_IN, sistem akan otomatis membuat depletion-out AYAM",
)
}
warehouseID, err := s.resolveWarehouseID(c.Context(), req)
@@ -211,6 +215,133 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
}
if functionCode == string(utils.AdjustmentTransactionSubtypeRecordingDepletionIn) {
if routeMeta.Lane != adjustmentLaneStockable {
return fiber.NewError(fiber.StatusBadRequest, "Transaction subtype depletion in harus lane STOCKABLE")
}
if projectFlockKandangID == nil || *projectFlockKandangID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id aktif wajib tersedia untuk depletion conversion")
}
if s.FifoStockV2Svc == nil {
return fiber.NewError(fiber.StatusInternalServerError, "FIFO v2 service is not available")
}
sourcePW, err := s.resolveAyamSourceProductWarehouse(ctx, tx, warehouseID, *projectFlockKandangID)
if err != nil {
return err
}
if err := common.EnsureProjectFlockNotClosedForProductWarehouses(
ctx,
tx,
[]uint{productWarehouse.Id, sourcePW.Id},
); err != nil {
return err
}
sourceRoute, err := s.resolveRouteByFunctionCode(
ctx,
sourcePW.ProductId,
string(utils.AdjustmentTransactionSubtypeRecordingDepletionOut),
)
if err != nil {
return err
}
if sourceRoute.Lane != adjustmentLaneUsable {
return fiber.NewError(fiber.StatusBadRequest, "Route depletion out untuk produk AYAM tidak valid")
}
sourceCode, err := adjustmentStockRepoTX.GenerateSequentialNumber(ctx, utils.AdjustmentStockNumberPrefix)
if err != nil {
return err
}
sourceAdjustment := &entity.AdjustmentStock{
ProductWarehouseId: sourcePW.Id,
TransactionType: transactionType,
FunctionCode: sourceRoute.FunctionCode,
UsageQty: qty,
Price: req.Price,
GrandTotal: grandTotal,
AdjNumber: sourceCode,
}
if err := adjustmentStockRepoTX.CreateOne(ctx, sourceAdjustment, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create depletion source adjustment stock record")
}
destCode, err := adjustmentStockRepoTX.GenerateSequentialNumber(ctx, utils.AdjustmentStockNumberPrefix)
if err != nil {
return err
}
destinationAdjustment := &entity.AdjustmentStock{
ProductWarehouseId: productWarehouse.Id,
TransactionType: transactionType,
FunctionCode: routeMeta.FunctionCode,
TotalQty: qty,
Price: req.Price,
GrandTotal: grandTotal,
AdjNumber: destCode,
}
if err := adjustmentStockRepoTX.CreateOne(ctx, destinationAdjustment, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create depletion destination adjustment stock record")
}
sourceAsOf := sourceAdjustment.CreatedAt
if _, err := s.FifoStockV2Svc.Reflow(ctx, common.FifoStockV2ReflowRequest{
FlagGroupCode: sourceRoute.FlagGroupCode,
ProductWarehouseID: sourcePW.Id,
AsOf: &sourceAsOf,
Tx: tx,
}); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to auto depletion-out AYAM via FIFO v2: %v", err))
}
destinationAsOf := destinationAdjustment.CreatedAt
if _, err := s.FifoStockV2Svc.Reflow(ctx, common.FifoStockV2ReflowRequest{
FlagGroupCode: routeMeta.FlagGroupCode,
ProductWarehouseID: destinationAdjustment.ProductWarehouseId,
AsOf: &destinationAsOf,
Tx: tx,
}); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to auto depletion-in destination via FIFO v2: %v", err))
}
refreshedSource, err := adjustmentStockRepoTX.GetByID(ctx, sourceAdjustment.Id, nil)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to refresh depletion source adjustment stock")
}
refreshedDestination, err := adjustmentStockRepoTX.GetByID(ctx, destinationAdjustment.Id, nil)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to refresh depletion destination adjustment stock")
}
if err := s.createAdjustmentStockLog(
ctx,
stockLogRepoTX,
refreshedSource.Id,
refreshedSource.ProductWarehouseId,
note,
actorID,
0,
refreshedSource.UsageQty+refreshedSource.PendingQty,
); err != nil {
return err
}
if err := s.createAdjustmentStockLog(
ctx,
stockLogRepoTX,
refreshedDestination.Id,
refreshedDestination.ProductWarehouseId,
note,
actorID,
refreshedDestination.TotalQty,
0,
); err != nil {
return err
}
createdAdjustmentStockId = destinationAdjustment.Id
return nil
}
adjustmentStock := &entity.AdjustmentStock{
ProductWarehouseId: productWarehouse.Id,
TransactionType: transactionType,
@@ -264,29 +395,16 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
decreaseQty = refreshedAdjustment.UsageQty
}
stockLogs, err := stockLogRepoTX.GetByProductWarehouse(ctx, productWarehouse.Id, 1)
if err != nil {
s.Log.Errorf("Failed to get stock logs: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
}
currentStock := 0.0
if len(stockLogs) > 0 {
currentStock = stockLogs[0].Stock
}
newLog := &entity.StockLog{
LoggableType: string(utils.StockLogTypeAdjustment),
LoggableId: adjustmentStock.Id,
Notes: note,
ProductWarehouseId: productWarehouse.Id,
CreatedBy: actorID,
Increase: increaseQty,
Decrease: decreaseQty,
Stock: currentStock + increaseQty - decreaseQty,
}
if err := stockLogRepoTX.CreateOne(ctx, newLog, nil); err != nil {
if err := s.createAdjustmentStockLog(
ctx,
stockLogRepoTX,
adjustmentStock.Id,
productWarehouse.Id,
note,
actorID,
increaseQty,
decreaseQty,
); err != nil {
return err
}
@@ -417,6 +535,88 @@ func (s *adjustmentService) getActiveProjectFlockKandangID(ctx context.Context,
return uint(projectFlockKandang.Id), nil
}
func (s *adjustmentService) resolveAyamSourceProductWarehouse(
ctx context.Context,
tx *gorm.DB,
warehouseID uint,
projectFlockKandangID uint,
) (*entity.ProductWarehouse, error) {
if tx == nil {
return nil, fmt.Errorf("transaction is required")
}
if projectFlockKandangID == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id tidak valid untuk depletion conversion")
}
var sourcePW entity.ProductWarehouse
err := tx.WithContext(ctx).
Model(&entity.ProductWarehouse{}).
Where("project_flock_kandang_id = ?", projectFlockKandangID).
Where(`
EXISTS (
SELECT 1
FROM flags f
JOIN fifo_stock_v2_flag_members fm ON fm.flag_name = f.name AND fm.is_active = TRUE
WHERE f.flagable_type = ?
AND f.flagable_id = product_warehouses.product_id
AND fm.flag_group_code = ?
)
`, entity.FlagableTypeProduct, flagGroupAyam).
Order(gorm.Expr("CASE WHEN warehouse_id = ? THEN 0 ELSE 1 END ASC", warehouseID)).
Order("id ASC").
Take(&sourcePW).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Produk sumber AYAM pada project flock kandang yang sama tidak ditemukan")
}
return nil, err
}
return &sourcePW, nil
}
func (s *adjustmentService) createAdjustmentStockLog(
ctx context.Context,
stockLogRepo stockLogsRepo.StockLogRepository,
adjustmentID uint,
productWarehouseID uint,
note string,
actorID uint,
increaseQty float64,
decreaseQty float64,
) error {
if stockLogRepo == nil || adjustmentID == 0 || productWarehouseID == 0 {
return nil
}
if increaseQty == 0 && decreaseQty == 0 {
return nil
}
stockLogs, err := stockLogRepo.GetByProductWarehouse(ctx, productWarehouseID, 1)
if err != nil {
s.Log.Errorf("Failed to get stock logs: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
}
currentStock := 0.0
if len(stockLogs) > 0 {
currentStock = stockLogs[0].Stock
}
newLog := &entity.StockLog{
LoggableType: string(utils.StockLogTypeAdjustment),
LoggableId: adjustmentID,
Notes: note,
ProductWarehouseId: productWarehouseID,
CreatedBy: actorID,
Increase: increaseQty,
Decrease: decreaseQty,
Stock: currentStock + increaseQty - decreaseQty,
}
return stockLogRepo.CreateOne(ctx, newLog, nil)
}
func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Query) ([]*entity.AdjustmentStock, int64, error) {
if err := s.Validate.Struct(query); err != nil {
return nil, 0, err