Merge branch 'feat/BE/Sprint-6' of https://gitlab.com/mbugroup/lti-api into dev/ragil

This commit is contained in:
ragilap
2025-12-03 17:51:48 +07:00
40 changed files with 2676 additions and 272 deletions
@@ -0,0 +1,23 @@
package repository
import (
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type ProjectBudgetRepository interface {
repository.BaseRepository[entity.ProjectBudget]
}
type ProjectBudgetRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.ProjectBudget]
db *gorm.DB
}
func NewProjectBudgetRepository(db *gorm.DB) ProjectBudgetRepository {
return &ProjectBudgetRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectBudget](db),
db: db,
}
}
@@ -2,6 +2,7 @@ package recordings
import (
"fmt"
"strings"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
@@ -14,6 +15,7 @@ import (
rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
sRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -26,6 +28,25 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db)
projectFlockPopulationRepo := rProjectFlock.NewProjectFlockPopulationRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
if err := fifoService.RegisterUsable(fifo.UsableConfig{
Key: fifo.UsableKeyRecordingStock,
Table: "recording_stocks",
Columns: fifo.UsableColumns{
ID: "id",
ProductWarehouseID: "product_warehouse_id",
UsageQuantity: "usage_qty",
PendingQuantity: "pending_qty",
CreatedAt: "created_at",
},
}); err != nil {
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
panic(fmt.Sprintf("failed to register recording usable workflow: %v", err))
}
}
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowRecording, utils.RecordingApprovalSteps); err != nil {
@@ -41,6 +62,7 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
projectFlockPopulationRepo,
approvalRepo,
approvalService,
fifoService,
validate,
)
userService := sUser.NewUserService(userRepo, validate)
@@ -25,6 +25,7 @@ type RecordingRepository interface {
CreateStocks(tx *gorm.DB, stocks []entity.RecordingStock) error
DeleteStocks(tx *gorm.DB, recordingID uint) error
ListStocks(tx *gorm.DB, recordingID uint) ([]entity.RecordingStock, error)
UpdateStockUsage(tx *gorm.DB, stockID uint, usageQty, pendingQty float64) error
CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error
DeleteDepletions(tx *gorm.DB, recordingID uint) error
@@ -120,6 +121,15 @@ func (r *RecordingRepositoryImpl) ListStocks(tx *gorm.DB, recordingID uint) ([]e
return items, nil
}
func (r *RecordingRepositoryImpl) UpdateStockUsage(tx *gorm.DB, stockID uint, usageQty, pendingQty float64) error {
return tx.Model(&entity.RecordingStock{}).
Where("id = ?", stockID).
Updates(map[string]any{
"usage_qty": usageQty,
"pending_qty": pendingQty,
}).Error
}
func (r *RecordingRepositoryImpl) CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error {
if len(depletions) == 0 {
return nil
@@ -14,6 +14,7 @@ import (
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
recordingutil "gitlab.com/mbugroup/lti-api.git/internal/utils/recording"
"math"
"strings"
@@ -36,6 +37,13 @@ type RecordingService interface {
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.Recording, error)
}
type RecordingFIFOIntegrationService interface {
ConsumeRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error
ReleaseRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error
}
var recordingStockUsableKey = fifo.UsableKeyRecordingStock
type recordingService struct {
Log *logrus.Logger
Validate *validator.Validate
@@ -45,6 +53,7 @@ type recordingService struct {
ProjectFlockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository
ApprovalRepo commonRepo.ApprovalRepository
ApprovalSvc commonSvc.ApprovalService
FifoSvc commonSvc.FifoService
}
func NewRecordingService(
@@ -54,6 +63,7 @@ func NewRecordingService(
projectFlockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository,
approvalRepo commonRepo.ApprovalRepository,
approvalSvc commonSvc.ApprovalService,
fifoSvc commonSvc.FifoService,
validate *validator.Validate,
) RecordingService {
return &recordingService{
@@ -65,6 +75,20 @@ func NewRecordingService(
ProjectFlockPopulationRepo: projectFlockPopulationRepo,
ApprovalRepo: approvalRepo,
ApprovalSvc: approvalSvc,
FifoSvc: fifoSvc,
}
}
func NewRecordingFIFOIntegrationService(
repo repository.RecordingRepository,
productWarehouseRepo rProductWarehouse.ProductWarehouseRepository,
fifoSvc commonSvc.FifoService,
) RecordingFIFOIntegrationService {
return &recordingService{
Log: utils.Log,
Repository: repo,
ProductWarehouseRepo: productWarehouseRepo,
FifoSvc: fifoSvc,
}
}
@@ -222,6 +246,10 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
return err
}
if err := s.consumeRecordingStocks(ctx, tx, mappedStocks); err != nil {
return err
}
mappedDepletions := recordingutil.MapDepletions(createdRecording.Id, req.Depletions)
if err := s.Repository.CreateDepletions(tx, mappedDepletions); err != nil {
s.Log.Errorf("Failed to persist depletions: %+v", err)
@@ -234,7 +262,7 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
return err
}
if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(nil, mappedDepletions, nil, mappedStocks, nil, mappedEggs)); err != nil {
if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(nil, mappedDepletions, nil, nil, nil, mappedEggs)); err != nil {
s.Log.Errorf("Failed to adjust product warehouses: %+v", err)
return err
}
@@ -347,6 +375,10 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
return err
}
if err := s.releaseRecordingStocks(ctx, tx, existingStocks); err != nil {
return err
}
if err := s.Repository.DeleteStocks(tx, recordingEntity.Id); err != nil {
s.Log.Errorf("Failed to clear stocks: %+v", err)
return err
@@ -358,8 +390,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
return err
}
if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(nil, nil, existingStocks, mappedStocks, nil, nil)); err != nil {
s.Log.Errorf("Failed to adjust product warehouses for stocks: %+v", err)
if err := s.consumeRecordingStocks(ctx, tx, mappedStocks); err != nil {
return err
}
}
@@ -691,7 +722,11 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error {
return err
}
if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(oldDepletions, nil, oldStocks, nil, oldEggs, nil)); err != nil {
if err := s.releaseRecordingStocks(ctx, tx, oldStocks); err != nil {
return err
}
if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(oldDepletions, nil, nil, nil, oldEggs, nil)); err != nil {
return err
}
@@ -746,6 +781,77 @@ func (s *recordingService) ensureProductWarehousesExist(c *fiber.Ctx, stocks []v
return nil
}
func (s *recordingService) consumeRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error {
if len(stocks) == 0 || s.FifoSvc == nil {
return nil
}
for _, stock := range stocks {
if stock.Id == 0 {
continue
}
var desired float64
if stock.UsageQty != nil {
desired = *stock.UsageQty
}
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
UsableKey: recordingStockUsableKey,
UsableID: stock.Id,
ProductWarehouseID: stock.ProductWarehouseId,
Quantity: desired,
AllowPending: true,
Tx: tx,
})
if err != nil {
s.Log.Errorf("Failed to consume FIFO stock for recording stock %d: %+v", stock.Id, err)
return err
}
if err := s.Repository.UpdateStockUsage(tx, stock.Id, result.UsageQuantity, result.PendingQuantity); err != nil {
return err
}
}
return nil
}
func (s *recordingService) ConsumeRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error {
return s.consumeRecordingStocks(ctx, tx, stocks)
}
func (s *recordingService) releaseRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error {
if len(stocks) == 0 || s.FifoSvc == nil {
return nil
}
for _, stock := range stocks {
if stock.Id == 0 {
continue
}
if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{
UsableKey: recordingStockUsableKey,
UsableID: stock.Id,
Tx: tx,
}); err != nil {
s.Log.Errorf("Failed to release FIFO stock for recording stock %d: %+v", stock.Id, err)
return err
}
if err := s.Repository.UpdateStockUsage(tx, stock.Id, 0, 0); err != nil {
return err
}
}
return nil
}
func (s *recordingService) ReleaseRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error {
return s.releaseRecordingStocks(ctx, tx, stocks)
}
func buildWarehouseDeltas(
oldDepletions, newDepletions []entity.RecordingDepletion,
oldStocks, newStocks []entity.RecordingStock,
@@ -758,12 +864,6 @@ func buildWarehouseDeltas(
for _, item := range newDepletions {
accumulateWarehouseDelta(deltas, item.ProductWarehouseId, item.Qty)
}
for _, item := range oldStocks {
accumulateWarehouseDelta(deltas, item.ProductWarehouseId, usageQtyValue(item.UsageQty))
}
for _, item := range newStocks {
accumulateWarehouseDelta(deltas, item.ProductWarehouseId, -usageQtyValue(item.UsageQty))
}
for _, item := range oldEggs {
accumulateWarehouseDelta(deltas, item.ProductWarehouseId, -float64(item.Qty))
}
@@ -773,13 +873,6 @@ func buildWarehouseDeltas(
return deltas
}
func usageQtyValue(val *float64) float64 {
if val == nil {
return 0
}
return *val
}
func accumulateWarehouseDelta(deltas map[uint]float64, id uint, value float64) {
if id == 0 || value == 0 {
return