diff --git a/internal/modules/production/project_flocks/services/projectflock.service.go b/internal/modules/production/project_flocks/services/projectflock.service.go index 7224a691..ee18f0d8 100644 --- a/internal/modules/production/project_flocks/services/projectflock.service.go +++ b/internal/modules/production/project_flocks/services/projectflock.service.go @@ -208,7 +208,6 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (* } createBody := &entity.ProjectFlock{ - FlockName: "", AreaId: req.AreaId, Category: cat, FcrId: req.FcrId, diff --git a/internal/modules/production/recordings/services/recording.service.go b/internal/modules/production/recordings/services/recording.service.go index ee2670db..e8836590 100644 --- a/internal/modules/production/recordings/services/recording.service.go +++ b/internal/modules/production/recordings/services/recording.service.go @@ -170,104 +170,90 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent return nil, err } - tx := s.Repository.DB().WithContext(ctx).Begin() - if tx.Error != nil { - s.Log.Errorf("Failed to start recording transaction: %+v", tx.Error) - return nil, tx.Error - } - defer func() { - if r := recover(); r != nil { - _ = tx.Rollback() - panic(r) + var createdRecording entity.Recording + transactionErr := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { + nextDay, err := s.Repository.GenerateNextDay(tx, req.ProjectFlockKandangId) + if err != nil { + s.Log.Errorf("Failed to determine recording day: %+v", err) + return err } - }() - nextDay, err := s.Repository.GenerateNextDay(tx, req.ProjectFlockKandangId) - if err != nil { - _ = tx.Rollback() - s.Log.Errorf("Failed to determine recording day: %+v", err) - return nil, err - } - - recordTime := time.Now().UTC() - - existsToday, err := s.Repository.ExistsOnDate(ctx, req.ProjectFlockKandangId, recordTime) - if err != nil { - _ = tx.Rollback() - s.Log.Errorf("Failed to verify existing recording on date: %+v", err) - return nil, err - } - if existsToday { - _ = tx.Rollback() - return nil, fiber.NewError(fiber.StatusBadRequest, "Recording for this project flock today already exists") - } - - recording := &entity.Recording{ - ProjectFlockKandangId: req.ProjectFlockKandangId, - RecordDatetime: recordTime, - Day: &nextDay, - CreatedBy: 1, // TODO: replace with authenticated user - } - - if err := s.Repository.CreateOne(ctx, recording, func(*gorm.DB) *gorm.DB { return tx }); err != nil { - _ = tx.Rollback() - if errors.Is(err, gorm.ErrDuplicatedKey) { - return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Recording for project flock kandang %d already exists", req.ProjectFlockKandangId)) + recordTime := time.Now().UTC() + existsToday, err := s.Repository.ExistsOnDate(ctx, req.ProjectFlockKandangId, recordTime) + if err != nil { + s.Log.Errorf("Failed to verify existing recording on date: %+v", err) + return err } - s.Log.Errorf("Failed to create recording: %+v", err) - return nil, err + if existsToday { + return fiber.NewError(fiber.StatusBadRequest, "Recording for this project flock today already exists") + } + + day := nextDay + createdRecording = entity.Recording{ + ProjectFlockKandangId: req.ProjectFlockKandangId, + RecordDatetime: recordTime, + Day: &day, + CreatedBy: 1, // TODO: replace with authenticated user + } + + if err := s.Repository.CreateOne(ctx, &createdRecording, func(*gorm.DB) *gorm.DB { return tx }); err != nil { + if errors.Is(err, gorm.ErrDuplicatedKey) { + return fiber.NewError( + fiber.StatusBadRequest, + fmt.Sprintf("Recording for project flock kandang %d already exists", req.ProjectFlockKandangId), + ) + } + s.Log.Errorf("Failed to create recording: %+v", err) + return err + } + + mappedBodyWeights := recordingutil.MapBodyWeights(createdRecording.Id, req.BodyWeights) + if err := s.Repository.CreateBodyWeights(tx, mappedBodyWeights); err != nil { + s.Log.Errorf("Failed to persist body weights: %+v", err) + return err + } + + mappedStocks := recordingutil.MapStocks(createdRecording.Id, req.Stocks) + if err := s.Repository.CreateStocks(tx, mappedStocks); err != nil { + s.Log.Errorf("Failed to persist stocks: %+v", err) + 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) + return err + } + + mappedEggs := recordingutil.MapEggs(createdRecording.Id, createdRecording.CreatedBy, req.Eggs) + if err := s.Repository.CreateEggs(tx, mappedEggs); err != nil { + s.Log.Errorf("Failed to persist eggs: %+v", err) + return err + } + + if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(nil, mappedDepletions, nil, mappedStocks, nil, mappedEggs)); err != nil { + s.Log.Errorf("Failed to adjust product warehouses: %+v", err) + return err + } + + if err := s.computeAndUpdateMetrics(ctx, tx, &createdRecording); err != nil { + s.Log.Errorf("Failed to compute recording metrics: %+v", err) + return err + } + + action := entity.ApprovalActionCreated + if err := s.createRecordingApproval(ctx, tx, createdRecording.Id, utils.RecordingStepGradingTelur, action, createdRecording.CreatedBy, nil); err != nil { + s.Log.Errorf("Failed to create recording approval for %d: %+v", createdRecording.Id, err) + return err + } + + return nil + }) + if transactionErr != nil { + return nil, transactionErr } - mappedBodyWeights := recordingutil.MapBodyWeights(recording.Id, req.BodyWeights) - if err := s.Repository.CreateBodyWeights(tx, mappedBodyWeights); err != nil { - _ = tx.Rollback() - s.Log.Errorf("Failed to persist body weights: %+v", err) - return nil, err - } - mappedStocks := recordingutil.MapStocks(recording.Id, req.Stocks) - if err := s.Repository.CreateStocks(tx, mappedStocks); err != nil { - _ = tx.Rollback() - s.Log.Errorf("Failed to persist stocks: %+v", err) - return nil, err - } - mappedDepletions := recordingutil.MapDepletions(recording.Id, req.Depletions) - if err := s.Repository.CreateDepletions(tx, mappedDepletions); err != nil { - _ = tx.Rollback() - s.Log.Errorf("Failed to persist depletions: %+v", err) - return nil, err - } - mappedEggs := recordingutil.MapEggs(recording.Id, recording.CreatedBy, req.Eggs) - if err := s.Repository.CreateEggs(tx, mappedEggs); err != nil { - _ = tx.Rollback() - s.Log.Errorf("Failed to persist eggs: %+v", err) - return nil, err - } - - if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(nil, mappedDepletions, nil, mappedStocks, nil, mappedEggs)); err != nil { - _ = tx.Rollback() - s.Log.Errorf("Failed to adjust product warehouses: %+v", err) - return nil, err - } - - if err := s.computeAndUpdateMetrics(ctx, tx, recording); err != nil { - _ = tx.Rollback() - s.Log.Errorf("Failed to compute recording metrics: %+v", err) - return nil, err - } - - action := entity.ApprovalActionCreated - if err := s.createRecordingApproval(ctx, tx, recording.Id, utils.RecordingStepGradingTelur, action, recording.CreatedBy, nil); err != nil { - _ = tx.Rollback() - s.Log.Errorf("Failed to create recording approval for %d: %+v", recording.Id, err) - return nil, err - } - - if err := tx.Commit().Error; err != nil { - s.Log.Errorf("Failed to commit recording transaction: %+v", err) - return nil, err - } - - return s.GetOne(c, recording.Id) + return s.GetOne(c, createdRecording.Id) } func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Recording, error) { @@ -277,165 +263,146 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin ctx := c.Context() - tx := s.Repository.DB().WithContext(ctx).Begin() - if tx.Error != nil { - s.Log.Errorf("Failed to start recording transaction: %+v", tx.Error) - return nil, tx.Error - } - defer func() { - if r := recover(); r != nil { - _ = tx.Rollback() - panic(r) + var recordingEntity *entity.Recording + transactionErr := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { + recording, err := s.Repository.GetByID(ctx, id, func(db *gorm.DB) *gorm.DB { + return s.Repository.WithRelations(tx) + }) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Recording not found") + } + s.Log.Errorf("Failed to find recording: %+v", err) + return err } - }() + recordingEntity = recording - recording, err := s.Repository.GetByID(ctx, id, func(db *gorm.DB) *gorm.DB { - return s.Repository.WithRelations(tx) + var category string + if recordingEntity.ProjectFlockKandang != nil && recordingEntity.ProjectFlockKandang.ProjectFlock.Id != 0 { + category = strings.ToUpper(recordingEntity.ProjectFlockKandang.ProjectFlock.Category) + } + isLaying := category == strings.ToUpper(string(utils.ProjectFlockCategoryLaying)) + if req.Eggs != nil { + if !isLaying && len(req.Eggs) > 0 { + return fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks") + } + if isLaying && len(req.Eggs) == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Egg details are required for laying project flocks") + } + } + + if req.BodyWeights != nil { + if err := s.Repository.DeleteBodyWeights(tx, recordingEntity.Id); err != nil { + s.Log.Errorf("Failed to clear body weights: %+v", err) + return err + } + if err := s.Repository.CreateBodyWeights(tx, recordingutil.MapBodyWeights(recordingEntity.Id, req.BodyWeights)); err != nil { + s.Log.Errorf("Failed to update body weights: %+v", err) + return err + } + } + + if req.Stocks != nil { + if err := s.ensureProductWarehousesExist(c, req.Stocks, nil, nil); err != nil { + return err + } + + existingStocks, err := s.Repository.ListStocks(tx, recordingEntity.Id) + if err != nil { + s.Log.Errorf("Failed to list existing stocks: %+v", err) + return err + } + + if err := s.Repository.DeleteStocks(tx, recordingEntity.Id); err != nil { + s.Log.Errorf("Failed to clear stocks: %+v", err) + return err + } + + mappedStocks := recordingutil.MapStocks(recordingEntity.Id, req.Stocks) + if err := s.Repository.CreateStocks(tx, mappedStocks); err != nil { + s.Log.Errorf("Failed to update stocks: %+v", err) + 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) + return err + } + } + + if req.Eggs != nil && req.Depletions == nil { + if err := s.ensureProductWarehousesExist(c, nil, nil, req.Eggs); err != nil { + return err + } + } + + if req.Depletions != nil { + if err := s.ensureProductWarehousesExist(c, nil, req.Depletions, req.Eggs); err != nil { + return err + } + + existingDepletions, err := s.Repository.ListDepletions(tx, recordingEntity.Id) + if err != nil { + s.Log.Errorf("Failed to list existing depletions: %+v", err) + return err + } + + if err := s.Repository.DeleteDepletions(tx, recordingEntity.Id); err != nil { + s.Log.Errorf("Failed to clear depletions: %+v", err) + return err + } + + mappedDepletions := recordingutil.MapDepletions(recordingEntity.Id, req.Depletions) + if err := s.Repository.CreateDepletions(tx, mappedDepletions); err != nil { + s.Log.Errorf("Failed to update depletions: %+v", err) + return err + } + + if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(existingDepletions, mappedDepletions, nil, nil, nil, nil)); err != nil { + s.Log.Errorf("Failed to adjust product warehouses for depletions: %+v", err) + return err + } + } + + if req.Eggs != nil { + existingEggs, err := s.Repository.ListEggs(tx, recordingEntity.Id) + if err != nil { + s.Log.Errorf("Failed to list existing eggs: %+v", err) + return err + } + + if err := s.Repository.DeleteEggs(tx, recordingEntity.Id); err != nil { + s.Log.Errorf("Failed to clear eggs: %+v", err) + return err + } + + mappedEggs := recordingutil.MapEggs(recordingEntity.Id, recordingEntity.CreatedBy, req.Eggs) + if err := s.Repository.CreateEggs(tx, mappedEggs); err != nil { + s.Log.Errorf("Failed to update eggs: %+v", err) + return err + } + + if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(nil, nil, nil, nil, existingEggs, mappedEggs)); err != nil { + s.Log.Errorf("Failed to adjust product warehouses for eggs: %+v", err) + return err + } + } + + if err := s.computeAndUpdateMetrics(ctx, tx, recordingEntity); err != nil { + s.Log.Errorf("Failed to recompute recording metrics: %+v", err) + return err + } + + action := entity.ApprovalActionUpdated + if err := s.createRecordingApproval(ctx, tx, recordingEntity.Id, utils.RecordingStepPengajuan, action, recordingEntity.CreatedBy, nil); err != nil { + s.Log.Errorf("Failed to create approval after recording update %d: %+v", recordingEntity.Id, err) + return err + } + + return nil }) - if err != nil { - _ = tx.Rollback() - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusNotFound, "Recording not found") - } - s.Log.Errorf("Failed to find recording: %+v", err) - return nil, err - } - recordingEntity := recording - - var category string - if recordingEntity.ProjectFlockKandang != nil && recordingEntity.ProjectFlockKandang.ProjectFlock.Id != 0 { - category = strings.ToUpper(recordingEntity.ProjectFlockKandang.ProjectFlock.Category) - } - isLaying := category == strings.ToUpper(string(utils.ProjectFlockCategoryLaying)) - if req.Eggs != nil { - if !isLaying && len(req.Eggs) > 0 { - _ = tx.Rollback() - return nil, fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks") - } - if isLaying && len(req.Eggs) == 0 { - _ = tx.Rollback() - return nil, fiber.NewError(fiber.StatusBadRequest, "Egg details are required for laying project flocks") - } - } - - if req.BodyWeights != nil { - if err := s.Repository.DeleteBodyWeights(tx, recordingEntity.Id); err != nil { - _ = tx.Rollback() - s.Log.Errorf("Failed to clear body weights: %+v", err) - return nil, err - } - if err := s.Repository.CreateBodyWeights(tx, recordingutil.MapBodyWeights(recordingEntity.Id, req.BodyWeights)); err != nil { - _ = tx.Rollback() - s.Log.Errorf("Failed to update body weights: %+v", err) - return nil, err - } - } - if req.Stocks != nil { - if err := s.ensureProductWarehousesExist(c, req.Stocks, nil, nil); err != nil { - _ = tx.Rollback() - return nil, err - } - existingStocks, err := s.Repository.ListStocks(tx, recordingEntity.Id) - if err != nil { - _ = tx.Rollback() - s.Log.Errorf("Failed to list existing stocks: %+v", err) - return nil, err - } - - if err := s.Repository.DeleteStocks(tx, recordingEntity.Id); err != nil { - _ = tx.Rollback() - s.Log.Errorf("Failed to clear stocks: %+v", err) - return nil, err - } - mappedStocks := recordingutil.MapStocks(recordingEntity.Id, req.Stocks) - if err := s.Repository.CreateStocks(tx, mappedStocks); err != nil { - _ = tx.Rollback() - s.Log.Errorf("Failed to update stocks: %+v", err) - return nil, err - } - if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(nil, nil, existingStocks, mappedStocks, nil, nil)); err != nil { - _ = tx.Rollback() - s.Log.Errorf("Failed to adjust product warehouses for stocks: %+v", err) - return nil, err - } - } - if req.Eggs != nil && req.Depletions == nil { - if err := s.ensureProductWarehousesExist(c, nil, nil, req.Eggs); err != nil { - _ = tx.Rollback() - return nil, err - } - } - var existingDepletions []entity.RecordingDepletion - if req.Depletions != nil { - if err := s.ensureProductWarehousesExist(c, nil, req.Depletions, req.Eggs); err != nil { - _ = tx.Rollback() - return nil, err - } - var err error - existingDepletions, err = s.Repository.ListDepletions(tx, recordingEntity.Id) - if err != nil { - _ = tx.Rollback() - s.Log.Errorf("Failed to list existing depletions: %+v", err) - return nil, err - } - if err := s.Repository.DeleteDepletions(tx, recordingEntity.Id); err != nil { - _ = tx.Rollback() - s.Log.Errorf("Failed to clear depletions: %+v", err) - return nil, err - } - mappedDepletions := recordingutil.MapDepletions(recordingEntity.Id, req.Depletions) - if err := s.Repository.CreateDepletions(tx, mappedDepletions); err != nil { - _ = tx.Rollback() - s.Log.Errorf("Failed to update depletions: %+v", err) - return nil, err - } - if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(existingDepletions, mappedDepletions, nil, nil, nil, nil)); err != nil { - _ = tx.Rollback() - s.Log.Errorf("Failed to adjust product warehouses for depletions: %+v", err) - return nil, err - } - } - if req.Eggs != nil { - existingEggs, err := s.Repository.ListEggs(tx, recordingEntity.Id) - if err != nil { - _ = tx.Rollback() - s.Log.Errorf("Failed to list existing eggs: %+v", err) - return nil, err - } - if err := s.Repository.DeleteEggs(tx, recordingEntity.Id); err != nil { - _ = tx.Rollback() - s.Log.Errorf("Failed to clear eggs: %+v", err) - return nil, err - } - mappedEggs := recordingutil.MapEggs(recordingEntity.Id, recordingEntity.CreatedBy, req.Eggs) - if err := s.Repository.CreateEggs(tx, mappedEggs); err != nil { - _ = tx.Rollback() - s.Log.Errorf("Failed to update eggs: %+v", err) - return nil, err - } - if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(nil, nil, nil, nil, existingEggs, mappedEggs)); err != nil { - _ = tx.Rollback() - s.Log.Errorf("Failed to adjust product warehouses for eggs: %+v", err) - return nil, err - } - } - - if err := s.computeAndUpdateMetrics(ctx, tx, recordingEntity); err != nil { - _ = tx.Rollback() - s.Log.Errorf("Failed to recompute recording metrics: %+v", err) - return nil, err - } - - action := entity.ApprovalActionUpdated - if err := s.createRecordingApproval(ctx, tx, recordingEntity.Id, utils.RecordingStepPengajuan, action, recordingEntity.CreatedBy, nil); err != nil { - _ = tx.Rollback() - s.Log.Errorf("Failed to create approval after recording update %d: %+v", recordingEntity.Id, err) - return nil, err - } - - if err := tx.Commit().Error; err != nil { - s.Log.Errorf("Failed to commit recording transaction: %+v", err) - return nil, err + if transactionErr != nil { + return nil, transactionErr } return s.GetOne(c, id) @@ -446,107 +413,102 @@ func (s *recordingService) SubmitGrading(c *fiber.Ctx, req *validation.SubmitGra return nil, err } - ctx := c.Context() - tx := s.Repository.DB().WithContext(ctx).Begin() - if tx.Error != nil { - s.Log.Errorf("Failed to start grading transaction: %+v", tx.Error) - return nil, tx.Error + if len(req.EggsGrading) == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "eggs_grading must contain at least one item") } - defer func() { - if r := recover(); r != nil { - _ = tx.Rollback() - panic(r) + + recordingEggID := req.EggsGrading[0].RecordingEggId + for _, grading := range req.EggsGrading[1:] { + if grading.RecordingEggId != recordingEggID { + return nil, fiber.NewError(fiber.StatusBadRequest, "semua grading harus untuk recording egg yang sama") } - }() - - recordingEgg, err := s.Repository.GetRecordingEggByID(ctx, req.RecordingEggId, func(db *gorm.DB) *gorm.DB { - return tx - }) - if errors.Is(err, gorm.ErrRecordNotFound) { - _ = tx.Rollback() - return nil, fiber.NewError(fiber.StatusNotFound, "Recording egg not found") - } - if err != nil { - _ = tx.Rollback() - s.Log.Errorf("Failed to get recording egg %d: %+v", req.RecordingEggId, err) - return nil, err } - var category string - if recordingEgg.Recording.ProjectFlockKandang != nil && recordingEgg.Recording.ProjectFlockKandang.ProjectFlock.Id != 0 { - category = strings.ToUpper(recordingEgg.Recording.ProjectFlockKandang.ProjectFlock.Category) - } - if category != strings.ToUpper(string(utils.ProjectFlockCategoryLaying)) { - _ = tx.Rollback() - return nil, fiber.NewError(fiber.StatusBadRequest, "Grading eggs hanya diperbolehkan pada project flock dengan kategori laying") - } + ctx := c.Context() + var recordingID uint + transactionErr := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { + recordingEgg, err := s.Repository.GetRecordingEggByID(ctx, recordingEggID, func(db *gorm.DB) *gorm.DB { + return tx + }) + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Recording egg not found") + } + if err != nil { + s.Log.Errorf("Failed to get recording egg %d: %+v", recordingEggID, err) + return err + } - totalGradingQty := 0.0 - for _, grading := range req.EggsGrading { - totalGradingQty += grading.Qty - } + var category string + if recordingEgg.Recording.ProjectFlockKandang != nil && recordingEgg.Recording.ProjectFlockKandang.ProjectFlock.Id != 0 { + category = strings.ToUpper(recordingEgg.Recording.ProjectFlockKandang.ProjectFlock.Category) + } + if category != strings.ToUpper(string(utils.ProjectFlockCategoryLaying)) { + return fiber.NewError(fiber.StatusBadRequest, "Grading eggs hanya diperbolehkan pada project flock dengan kategori laying") + } - availableRecorded := float64(recordingEgg.Qty) - if totalGradingQty > availableRecorded { - _ = tx.Rollback() - return nil, fiber.NewError( - fiber.StatusBadRequest, - fmt.Sprintf("Total grading (%.2f) melebihi jumlah telur tercatat (%.2f)", totalGradingQty, availableRecorded), - ) - } + totalGradingQty := 0.0 + for _, grading := range req.EggsGrading { + totalGradingQty += grading.Qty + } - if recordingEgg.ProductWarehouse.Id != 0 { - availableWarehouse := recordingEgg.ProductWarehouse.Quantity - if totalGradingQty > availableWarehouse { - _ = tx.Rollback() - return nil, fiber.NewError( + availableRecorded := float64(recordingEgg.Qty) + if totalGradingQty > availableRecorded { + return fiber.NewError( fiber.StatusBadRequest, - fmt.Sprintf("Total grading (%.2f) melebihi stok telur baik (%.2f)", totalGradingQty, availableWarehouse), + fmt.Sprintf("Total grading (%.2f) melebihi jumlah telur tercatat (%.2f)", totalGradingQty, availableRecorded), ) } - } - if err := s.Repository.DeleteGradingEggs(tx, recordingEgg.Id); err != nil { - _ = tx.Rollback() - s.Log.Errorf("Failed to clear grading eggs for recording egg %d: %+v", recordingEgg.Id, err) - return nil, err - } - - gradings := make([]entity.GradingEgg, 0, len(req.EggsGrading)) - createdBy := recordingEgg.CreatedBy - if createdBy == 0 { - createdBy = recordingEgg.Recording.CreatedBy - } - for _, item := range req.EggsGrading { - gradings = append(gradings, entity.GradingEgg{ - RecordingEggId: recordingEgg.Id, - Grade: strings.TrimSpace(item.Grade), - Qty: item.Qty, - CreatedBy: createdBy, - }) - } - - if len(gradings) > 0 { - if err := s.Repository.CreateGradingEggs(tx, gradings); err != nil { - _ = tx.Rollback() - s.Log.Errorf("Failed to persist grading eggs for recording egg %d: %+v", recordingEgg.Id, err) - return nil, err + if recordingEgg.ProductWarehouse.Id != 0 { + availableWarehouse := recordingEgg.ProductWarehouse.Quantity + if totalGradingQty > availableWarehouse { + return fiber.NewError( + fiber.StatusBadRequest, + fmt.Sprintf("Total grading (%.2f) melebihi stok telur baik (%.2f)", totalGradingQty, availableWarehouse), + ) + } } + + if err := s.Repository.DeleteGradingEggs(tx, recordingEgg.Id); err != nil { + s.Log.Errorf("Failed to clear grading eggs for recording egg %d: %+v", recordingEgg.Id, err) + return err + } + + gradings := make([]entity.GradingEgg, 0, len(req.EggsGrading)) + createdBy := recordingEgg.CreatedBy + if createdBy == 0 { + createdBy = recordingEgg.Recording.CreatedBy + } + for _, item := range req.EggsGrading { + gradings = append(gradings, entity.GradingEgg{ + RecordingEggId: recordingEgg.Id, + Grade: strings.TrimSpace(item.Grade), + Qty: item.Qty, + CreatedBy: createdBy, + }) + } + + if len(gradings) > 0 { + if err := s.Repository.CreateGradingEggs(tx, gradings); err != nil { + s.Log.Errorf("Failed to persist grading eggs for recording egg %d: %+v", recordingEgg.Id, err) + return err + } + } + + action := entity.ApprovalActionUpdated + if err := s.createRecordingApproval(ctx, tx, recordingEgg.RecordingId, utils.RecordingStepPengajuan, action, createdBy, nil); err != nil { + s.Log.Errorf("Failed to create approval after grading for recording %d: %+v", recordingEgg.RecordingId, err) + return err + } + + recordingID = recordingEgg.RecordingId + return nil + }) + if transactionErr != nil { + return nil, transactionErr } - action := entity.ApprovalActionUpdated - if err := s.createRecordingApproval(ctx, tx, recordingEgg.RecordingId, utils.RecordingStepPengajuan, action, createdBy, nil); err != nil { - _ = tx.Rollback() - s.Log.Errorf("Failed to create approval after grading for recording %d: %+v", recordingEgg.RecordingId, err) - return nil, err - } - - if err := tx.Commit().Error; err != nil { - s.Log.Errorf("Failed to commit grading transaction: %+v", err) - return nil, err - } - - return s.GetOne(c, recordingEgg.RecordingId) + return s.GetOne(c, recordingID) } func (s recordingService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.Recording, error) { @@ -629,49 +591,39 @@ func (s recordingService) Approval(c *fiber.Ctx, req *validation.Approve) ([]ent func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error { ctx := c.Context() - tx := s.Repository.DB().WithContext(ctx).Begin() - if tx.Error != nil { - return tx.Error - } - - oldDepletions, err := s.Repository.ListDepletions(tx, id) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - _ = tx.Rollback() - s.Log.Errorf("Failed to list depletions before delete: %+v", err) - return err - } - oldEggs, err := s.Repository.ListEggs(tx, id) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - _ = tx.Rollback() - s.Log.Errorf("Failed to list eggs before delete: %+v", err) - return err - } - oldStocks, err := s.Repository.ListStocks(tx, id) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - _ = tx.Rollback() - s.Log.Errorf("Failed to list stocks before delete: %+v", err) - return err - } - - if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(oldDepletions, nil, oldStocks, nil, oldEggs, nil)); err != nil { - _ = tx.Rollback() - return err - } - - if err := s.Repository.WithTx(tx).DeleteOne(ctx, id); err != nil { - _ = tx.Rollback() - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, "Recording not found") + return s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { + oldDepletions, err := s.Repository.ListDepletions(tx, id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + s.Log.Errorf("Failed to list depletions before delete: %+v", err) + return err } - s.Log.Errorf("Failed to delete recording: %+v", err) - return err - } - if err := tx.Commit().Error; err != nil { - s.Log.Errorf("Failed to commit delete recording transaction: %+v", err) - return err - } - return nil + oldEggs, err := s.Repository.ListEggs(tx, id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + s.Log.Errorf("Failed to list eggs before delete: %+v", err) + return err + } + + oldStocks, err := s.Repository.ListStocks(tx, id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + s.Log.Errorf("Failed to list stocks before delete: %+v", err) + return err + } + + if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(oldDepletions, nil, oldStocks, nil, oldEggs, nil)); err != nil { + return err + } + + if err := s.Repository.WithTx(tx).DeleteOne(ctx, id); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Recording not found") + } + s.Log.Errorf("Failed to delete recording: %+v", err) + return err + } + + return nil + }) } // === Persistence Helpers === diff --git a/internal/modules/production/recordings/validations/recording.validation.go b/internal/modules/production/recordings/validations/recording.validation.go index d760c0ba..f058248c 100644 --- a/internal/modules/production/recordings/validations/recording.validation.go +++ b/internal/modules/production/recordings/validations/recording.validation.go @@ -9,7 +9,7 @@ type ( Stock struct { ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"` - UsageAmount *float64 `json:"usage_amount,omitempty" validate:"omitempty,gte=0"` + Qty *float64 `json:"qty,omitempty" validate:"required_without=UsageAmount,gte=0"` PendingQty *float64 `json:"pending_qty,omitempty" validate:"omitempty,gte=0"` } @@ -46,13 +46,13 @@ type Query struct { } type EggGrading struct { - Grade string `json:"grade" validate:"required"` - Qty float64 `json:"qty" validate:"required,gte=0"` + RecordingEggId uint `json:"recording_egg_id" validate:"required,number,min=1"` + Grade string `json:"grade" validate:"required"` + Qty float64 `json:"qty" validate:"required,gte=0"` } type SubmitGrading struct { - RecordingEggId uint `json:"recording_egg_id" validate:"required,number,min=1"` - EggsGrading []EggGrading `json:"eggs_grading" validate:"required,dive"` + EggsGrading []EggGrading `json:"eggs_grading" validate:"required,dive"` } type Approve struct { diff --git a/internal/utils/recording/util.recording.go b/internal/utils/recording/util.recording.go index e5467aaf..fd463cf9 100644 --- a/internal/utils/recording/util.recording.go +++ b/internal/utils/recording/util.recording.go @@ -35,11 +35,21 @@ func MapStocks(recordingID uint, items []validation.Stock) []entity.RecordingSto result := make([]entity.RecordingStock, 0, len(items)) for _, item := range items { + var usageAmount float64 + if item.Qty != nil { + usageAmount = *item.Qty + } + usagePtr := new(float64) + *usagePtr = usageAmount + pending := item.PendingQty + if pending == nil { + pending = new(float64) + } result = append(result, entity.RecordingStock{ RecordingId: recordingID, ProductWarehouseId: item.ProductWarehouseId, - UsageQty: item.UsageAmount, - PendingQty: item.PendingQty, + UsageQty: usagePtr, + PendingQty: pending, }) } return result