package service import ( "context" "errors" "fmt" "time" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" expenseDto "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/dto" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/validations" nonstockRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/repositories" supplierRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories" projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "github.com/sirupsen/logrus" "gorm.io/gorm" ) type ExpenseService interface { GetAll(ctx *fiber.Ctx, params *validation.Query) ([]expenseDto.ExpenseListDTO, int64, error) GetOne(ctx *fiber.Ctx, id uint) (*expenseDto.ExpenseDetailDTO, error) CreateOne(ctx *fiber.Ctx, req *validation.Create) (*expenseDto.ExpenseDetailDTO, error) UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*expenseDto.ExpenseDetailDTO, error) DeleteOne(ctx *fiber.Ctx, id uint) error ApproveExpense(ctx *fiber.Ctx, id uint, step string, action string, notes *string) (*expenseDto.ExpenseDetailDTO, error) CreateRealization(ctx *fiber.Ctx, expenseID uint, req *validation.CreateRealization) (*expenseDto.ExpenseDetailDTO, error) CompleteExpense(ctx *fiber.Ctx, id uint, notes *string) (*expenseDto.ExpenseDetailDTO, error) UpdateRealization(ctx *fiber.Ctx, expenseID uint, req *validation.UpdateRealization) (*expenseDto.ExpenseDetailDTO, error) } type expenseService struct { Log *logrus.Logger Validate *validator.Validate Repository repository.ExpenseRepository SupplierRepo supplierRepo.SupplierRepository NonstockRepo nonstockRepo.NonstockRepository ApprovalSvc commonSvc.ApprovalService RealizationRepository repository.ExpenseRealizationRepository ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository } func NewExpenseService(repo repository.ExpenseRepository, supplierRepo supplierRepo.SupplierRepository, nonstockRepo nonstockRepo.NonstockRepository, approvalSvc commonSvc.ApprovalService, realizationRepo repository.ExpenseRealizationRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, validate *validator.Validate) ExpenseService { return &expenseService{ Log: utils.Log, Validate: validate, Repository: repo, SupplierRepo: supplierRepo, NonstockRepo: nonstockRepo, ApprovalSvc: approvalSvc, RealizationRepository: realizationRepo, ProjectFlockKandangRepo: projectFlockKandangRepo, } } func (s expenseService) withRelations(db *gorm.DB) *gorm.DB { return db. Preload("CreatedUser"). Preload("Supplier"). Preload("Nonstocks", func(db *gorm.DB) *gorm.DB { return db.Preload("Nonstock").Preload("Realization").Preload("ProjectFlockKandang") }) } func (s expenseService) getExpenseWithDetails(c *fiber.Ctx, expenseId uint) (*expenseDto.ExpenseDetailDTO, error) { expense, err := s.Repository.GetByID(c.Context(), expenseId, s.withRelations) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { s.Log.Errorf("Expense not found for ID %d: %+v", expenseId, err) return nil, fiber.NewError(fiber.StatusNotFound, "Expense not found") } s.Log.Errorf("Failed to get expense for ID %d: %+v", expenseId, err) return nil, err } // Load latest approval with ActionUser latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, expenseId, func(db *gorm.DB) *gorm.DB { return db.Preload("ActionUser") }) if err != nil { // Don't fail if approval loading fails, just log s.Log.Warnf("Failed to load approval for expense %d: %+v", expenseId, err) } expense.LatestApproval = latestApproval responseDTO := expenseDto.ToExpenseDetailDTO(expense) return &responseDTO, nil } func (s expenseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]expenseDto.ExpenseListDTO, int64, error) { if err := s.Validate.Struct(params); err != nil { s.Log.Errorf("Validation failed for GetAll: %+v", err) return nil, 0, err } offset := (params.Page - 1) * params.Limit expenses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) if params.Search != "" { return db.Where("category LIKE ?", "%"+params.Search+"%") } return db.Order("created_at DESC").Order("updated_at DESC") }) if err != nil { s.Log.Errorf("Failed to get expenses: %+v", err) return nil, 0, err } // Load approvals for each expense for i := range expenses { latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, uint(expenses[i].Id), func(db *gorm.DB) *gorm.DB { return db.Preload("ActionUser") }) if err != nil { s.Log.Warnf("Failed to load approval for expense %d: %+v", expenses[i].Id, err) } expenses[i].LatestApproval = latestApproval } result := expenseDto.ToExpenseListDTOs(expenses) return result, total, nil } func (s expenseService) GetOne(c *fiber.Ctx, id uint) (*expenseDto.ExpenseDetailDTO, error) { return s.getExpenseWithDetails(c, id) } func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expenseDto.ExpenseDetailDTO, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } // Validate supplier exists using common service supplierID := uint(req.SupplierID) supplierExistsFunc := func(ctx context.Context, id uint) (bool, error) { return commonRepo.Exists[entity.Supplier](ctx, s.SupplierRepo.DB(), id) } if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Supplier", ID: &supplierID, Exists: supplierExistsFunc}, ); err != nil { return nil, err } // Get supplier for category supplierEntity, err := s.SupplierRepo.GetByID(c.Context(), uint(req.SupplierID), nil) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Supplier not found") } return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get supplier") } for _, costPerKandang := range req.CostPerKandangs { for _, costItem := range costPerKandang.CostItems { nonstockId := uint(costItem.NonstockID) if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Nonstock", ID: &nonstockId, Exists: s.NonstockRepo.IdExists}, ); err != nil { return nil, err } nonstockEntity, err := s.NonstockRepo.GetByID(c.Context(), nonstockId, func(db *gorm.DB) *gorm.DB { return db.Preload("Suppliers") }) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Nonstock not found") } return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get nonstock") } supplierFound := false for _, sn := range nonstockEntity.Suppliers { if uint64(sn.Id) == req.SupplierID { supplierFound = true break } } if !supplierFound { return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Nonstock ID %d does not belong to supplier ID %d", nonstockId, req.SupplierID)) } } } expenseDate, err := utils.ParseDateString(req.TransactionDate) if err != nil { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transaction_date format") } var expense *entity.Expense err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { expenseRepoTx := repository.NewExpenseRepository(dbTransaction) expenseNonstockRepoTx := repository.NewExpenseNonstockRepository(dbTransaction) approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) referenceNumber, err := s.generateReferenceNumber(dbTransaction) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate reference number") } var grandTotal float64 for _, costPerKandang := range req.CostPerKandangs { for _, costItem := range costPerKandang.CostItems { grandTotal += costItem.TotalCost } } createdBy := uint64(1) //todo get from auth expense = &entity.Expense{ ReferenceNumber: &referenceNumber, PoNumber: req.PoNumber, Category: supplierEntity.Category, SupplierId: &req.SupplierID, ExpenseDate: expenseDate, GrandTotal: grandTotal, CreatedBy: &createdBy, } if err := expenseRepoTx.CreateOne(c.Context(), expense, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create expense") } if len(req.CostPerKandangs) > 0 { for _, costPerKandang := range req.CostPerKandangs { projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(dbTransaction) projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(costPerKandang.KandangID)) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "No active project flock kandang found for this kandang") } return fiber.NewError(fiber.StatusInternalServerError, "Failed to find project flock kandang for this kandang") } for _, costItem := range costPerKandang.CostItems { projectFlockKandangId := uint64(projectFlockKandang.Id) nonstockId := costItem.NonstockID expenseNonstock := &entity.ExpenseNonstock{ ExpenseId: &expense.Id, ProjectFlockKandangId: &projectFlockKandangId, NonstockId: &nonstockId, Qty: costItem.Quantity, TotalPrice: costItem.TotalCost, Note: &costItem.Notes, } if err := expenseNonstockRepoTx.CreateOne(c.Context(), expenseNonstock, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create expense cost item") } } } } approvalAction := entity.ApprovalActionCreated createdByUint := uint(createdBy) if _, err := approvalSvcTx.CreateApproval( c.Context(), utils.ApprovalWorkflowExpense, uint(expense.Id), utils.ExpenseStepPengajuan, &approvalAction, createdByUint, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create initial approval") } // TODO: Handle documents (save file references) return nil }) if err != nil { if fiberErr, ok := err.(*fiber.Error); ok { return nil, fiberErr } return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create expense") } return s.getExpenseWithDetails(c, uint(expense.Id)) } func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*expenseDto.ExpenseDetailDTO, error) { if err := s.Validate.Struct(req); err != nil { s.Log.Errorf("Validation failed for UpdateOne: %+v", err) return nil, err } // Validate expense exists using common service if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: func(ctx context.Context, id uint) (bool, error) { return s.Repository.IdExists(ctx, uint64(id)) }}, ); err != nil { return nil, err } updateBody := make(map[string]any) if req.SupplierID != nil { // Validate supplier exists using common service supplierID := uint(*req.SupplierID) if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Supplier", ID: &supplierID, Exists: func(ctx context.Context, id uint) (bool, error) { return commonRepo.Exists[entity.Supplier](ctx, s.SupplierRepo.DB(), id) }}, ); err != nil { return nil, err } updateBody["supplier_id"] = *req.SupplierID } if len(updateBody) == 0 { return s.getExpenseWithDetails(c, id) } if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Expense not found") } s.Log.Errorf("Failed to update expense: %+v", err) return nil, err } return s.getExpenseWithDetails(c, id) } func (s expenseService) DeleteOne(c *fiber.Ctx, id uint) error { // Validate expense exists using common service if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: func(ctx context.Context, id uint) (bool, error) { return s.Repository.IdExists(ctx, uint64(id)) }}, ); err != nil { return err } if err := s.Repository.DeleteOne(c.Context(), id); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { s.Log.Errorf("Expense not found for ID %d: %+v", id, err) return fiber.NewError(fiber.StatusNotFound, "Expense not found") } s.Log.Errorf("Failed to delete expense for ID %d: %+v", id, err) return err } s.Log.Infof("Successfully deleted expense with ID %d", id) return nil } func (s *expenseService) ApproveExpense(c *fiber.Ctx, id uint, stepName string, action string, notes *string) (*expenseDto.ExpenseDetailDTO, error) { if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: func(ctx context.Context, id uint) (bool, error) { return s.Repository.IdExists(ctx, uint64(id)) }}, ); err != nil { return nil, err } actorID := uint(1) // TODO: replace with authenticated user id var stepNumber approvalutils.ApprovalStep switch stepName { case "Manager": stepNumber = utils.ExpenseStepManager case "Finance": stepNumber = utils.ExpenseStepFinance default: s.Log.Errorf("Invalid approval step: %s", stepName) return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid approval step") } var expectedPreviousStep uint16 switch stepName { case "Manager": expectedPreviousStep = uint16(utils.ExpenseStepPengajuan) case "Finance": expectedPreviousStep = uint16(utils.ExpenseStepManager) } latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, id, nil) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate workflow") } if latestApproval == nil { return nil, fiber.NewError(fiber.StatusBadRequest, "No approval found. Please create expense first.") } if latestApproval.StepNumber != expectedPreviousStep { expectedStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(expectedPreviousStep)] currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)] return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot approve at step %s. Latest approval is at %s step. Expected previous step: %s", stepName, currentStepName, expectedStepName)) } var approvalAction entity.ApprovalAction switch action { case "APPROVED": approvalAction = entity.ApprovalActionApproved case "REJECTED": approvalAction = entity.ApprovalActionRejected default: return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid approval action") } err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx)) expenseRepoTx := repository.NewExpenseRepository(tx) if _, err := approvalSvc.CreateApproval( c.Context(), utils.ApprovalWorkflowExpense, id, stepNumber, &approvalAction, actorID, notes); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval") } if stepName == "Finance" && action == "APPROVED" { poNumber, err := s.generatePoNumber(tx, id) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate PO number") } updateData := map[string]interface{}{ "po_number": poNumber, } if err := expenseRepoTx.PatchOne(c.Context(), id, updateData, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to update PO number") } } return nil }) if err != nil { if fiberErr, ok := err.(*fiber.Error); ok { return nil, fiberErr } return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to Approve") } return s.getExpenseWithDetails(c, id) } func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *validation.CreateRealization) (*expenseDto.ExpenseDetailDTO, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Expense", ID: &expenseID, Exists: func(ctx context.Context, id uint) (bool, error) { return s.Repository.IdExists(ctx, uint64(id)) }}, ); err != nil { return nil, err } realizationDate := time.Now() createdBy := uint64(1) // TODO: replace with authenticated user id if err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx)) realizationRepoTx := repository.NewExpenseRealizationRepository(tx) expenseNonstockRepoTx := repository.NewExpenseNonstockRepository(tx) for _, realizationItem := range req.Realizations { expenseNonstockID := realizationItem.ExpenseNonstockID belongsToExpense, err := expenseNonstockRepoTx.GetByExpenseID(c.Context(), uint64(expenseID), uint64(expenseNonstockID)) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate ExpenseNonstock relation") } if !belongsToExpense { return fiber.NewError(fiber.StatusNotFound, "ExpenseNonstock not found or does not belong to this expense") } _, err = realizationRepoTx.GetByExpenseNonstockID(c.Context(), uint64(expenseNonstockID)) if err == nil { return fiber.NewError(fiber.StatusBadRequest, "Realization already exists for this expense nonstock") } else if !errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing realization") } realization := &entity.ExpenseRealization{ ExpenseNonstockId: &expenseNonstockID, RealizationQty: realizationItem.Qty, RealizationUnitPrice: realizationItem.UnitPrice, RealizationTotalPrice: realizationItem.TotalPrice, RealizationDate: realizationDate, Note: realizationItem.Notes, CreatedBy: &createdBy, } if err := realizationRepoTx.CreateOne(c.Context(), realization, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create realization") } } approvalAction := entity.ApprovalActionCreated if _, err := approvalSvc.CreateApproval( c.Context(), utils.ApprovalWorkflowExpense, expenseID, utils.ExpenseStepRealisasi, &approvalAction, uint(createdBy), nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create realization approval") } return nil }); err != nil { return nil, err } return s.getExpenseWithDetails(c, expenseID) } func (s *expenseService) CompleteExpense(c *fiber.Ctx, id uint, notes *string) (*expenseDto.ExpenseDetailDTO, error) { // Validate expense exists using common service if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: func(ctx context.Context, id uint) (bool, error) { return s.Repository.IdExists(ctx, uint64(id)) }}, ); err != nil { return nil, err } actorID := uint(1) // TODO: replace with authenticated user id // Get latest approval to validate workflow latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, id, nil) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { s.Log.Errorf("Failed to get latest approval for expense %d: %+v", id, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate workflow") } // Check if expense can be completed (must be at Realisasi step) if latestApproval == nil { s.Log.Errorf("No approval found for expense %d", id) return nil, fiber.NewError(fiber.StatusBadRequest, "No approval found. Please create expense first.") } if latestApproval.StepNumber != uint16(utils.ExpenseStepRealisasi) { currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)] s.Log.Errorf("Cannot complete expense at step %s. Must be at Realisasi step", currentStepName) return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot complete expense at %s step. Must be at Realisasi step", currentStepName)) } // Create approval for Selesai step (step 5) using transaction err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx)) approvalAction := entity.ApprovalActionApproved if _, err := approvalSvc.CreateApproval( c.Context(), utils.ApprovalWorkflowExpense, id, utils.ExpenseStepSelesai, &approvalAction, actorID, notes); err != nil { s.Log.Errorf("Failed to create Selesai approval for expense %d: %+v", id, err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to complete expense") } return nil }) if err != nil { return nil, err } return s.getExpenseWithDetails(c, id) } func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *validation.UpdateRealization) (*expenseDto.ExpenseDetailDTO, error) { if err := s.Validate.Struct(req); err != nil { s.Log.Errorf("Validation failed for UpdateRealization: %+v", err) return nil, err } // Validate Expense exists using common service if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Expense", ID: &expenseID, Exists: func(ctx context.Context, id uint) (bool, error) { return s.Repository.IdExists(ctx, uint64(id)) }}, ); err != nil { return nil, err } // Use current date for realization date realizationDate := time.Now() // Update realizations using transaction if err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { realizationRepoTx := repository.NewExpenseRealizationRepository(tx) expenseNonstockRepoTx := repository.NewExpenseNonstockRepository(tx) // Process each realization item for _, realizationItem := range req.Realizations { // Validate ExpenseNonstock exists and belongs to this expense expenseNonstockID := realizationItem.ExpenseNonstockID belongsToExpense, err := expenseNonstockRepoTx.GetByExpenseID(c.Context(), uint64(expenseID), uint64(expenseNonstockID)) if err != nil { s.Log.Errorf("Failed to validate ExpenseNonstock relation: %+v", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate ExpenseNonstock relation") } if !belongsToExpense { s.Log.Errorf("ExpenseNonstock not found or does not belong to expense %d", expenseID) return fiber.NewError(fiber.StatusNotFound, "ExpenseNonstock not found or does not belong to this expense") } // Get existing realization existingRealization, err := realizationRepoTx.GetByExpenseNonstockID(c.Context(), uint64(expenseNonstockID)) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { s.Log.Errorf("Realization not found for expense nonstock %d", expenseNonstockID) return fiber.NewError(fiber.StatusNotFound, "Realization not found for this expense nonstock") } s.Log.Errorf("Failed to get existing realization: %+v", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to get existing realization") } // Update realization updateData := map[string]interface{}{ "realization_qty": realizationItem.Qty, "realization_unit_price": realizationItem.UnitPrice, "realization_total_price": realizationItem.TotalPrice, "realization_date": realizationDate, } if realizationItem.Notes != nil { updateData["note"] = *realizationItem.Notes } if err := realizationRepoTx.PatchOne(c.Context(), uint(existingRealization.Id), updateData, nil); err != nil { s.Log.Errorf("Failed to update realization: %+v", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to update realization") } } return nil }); err != nil { return nil, err } // Return updated expense return s.getExpenseWithDetails(c, expenseID) } func (s *expenseService) generateReferenceNumber(ctx *gorm.DB) (string, error) { sequence, err := s.Repository.GetNextSequence(ctx.Statement.Context) if err != nil { return "", err } refNum := fmt.Sprintf("BOP-LTI-%05d", sequence) return refNum, nil } func (s *expenseService) generatePoNumber(ctx *gorm.DB, expenseID uint) (string, error) { expenseRepoTx := repository.NewExpenseRepository(ctx) expense, err := expenseRepoTx.GetByID(context.Background(), uint(expenseID), nil) if err != nil { return "", err } if expense.ReferenceNumber == nil { return "", errors.New("reference number is required") } poNum := fmt.Sprintf("PO-%s", *expense.ReferenceNumber) return poNum, nil }