From 99688c8e114518d9497f5af56d402fd687ed1959 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 24 Nov 2025 14:35:20 +0700 Subject: [PATCH] FIX[BE]: fixing issue failed delivery order, fixing unique constraint sales order --- .../repositories/expense.repository.go | 6 +- .../expenses/services/expense.service.go | 28 ++---- .../validations/expense.validation.go | 2 + .../services/delivery-orders.service.go | 3 - .../repositories/marketings.repository.go | 85 +++++++++++++++++++ .../services/sales-orders.service.go | 32 +++---- internal/utils/constant.go | 3 + 7 files changed, 117 insertions(+), 42 deletions(-) diff --git a/internal/modules/expenses/repositories/expense.repository.go b/internal/modules/expenses/repositories/expense.repository.go index 588583da..9e97a180 100644 --- a/internal/modules/expenses/repositories/expense.repository.go +++ b/internal/modules/expenses/repositories/expense.repository.go @@ -10,7 +10,7 @@ import ( type ExpenseRepository interface { repository.BaseRepository[entity.Expense] - IdExists(ctx context.Context, id uint64) (bool, error) + IdExists(ctx context.Context, id uint) (bool, error) GetNextSequence(ctx context.Context) (int, error) GetWithSupplier(ctx context.Context, id uint64) (*entity.Expense, error) } @@ -25,8 +25,8 @@ func NewExpenseRepository(db *gorm.DB) ExpenseRepository { } } -func (r *ExpenseRepositoryImpl) IdExists(ctx context.Context, id uint64) (bool, error) { - return repository.Exists[entity.Expense](ctx, r.DB(), uint(id)) +func (r *ExpenseRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) { + return repository.Exists[entity.Expense](ctx, r.DB(), id) } func (r *ExpenseRepositoryImpl) GetNextSequence(ctx context.Context) (int, error) { diff --git a/internal/modules/expenses/services/expense.service.go b/internal/modules/expenses/services/expense.service.go index f8e8d21f..eb760494 100644 --- a/internal/modules/expenses/services/expense.service.go +++ b/internal/modules/expenses/services/expense.service.go @@ -302,9 +302,7 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) } 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)) - }}, + commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: s.Repository.IdExists}, ); err != nil { return nil, err } @@ -481,9 +479,7 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) func (s expenseService) DeleteOne(c *fiber.Ctx, id uint) 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)) - }}, + commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: s.Repository.IdExists}, ); err != nil { return err } @@ -506,9 +502,7 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va } 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)) - }}, + commonSvc.RelationCheck{Name: "Expense", ID: &expenseID, Exists: s.Repository.IdExists}, ); err != nil { return nil, err } @@ -597,9 +591,7 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va func (s *expenseService) CompleteExpense(c *fiber.Ctx, id uint, 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)) - }}, + commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: s.Repository.IdExists}, ); err != nil { return nil, err } @@ -657,9 +649,7 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va } 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)) - }}, + commonSvc.RelationCheck{Name: "Expense", ID: &expenseID, Exists: s.Repository.IdExists}, ); err != nil { return nil, err } @@ -845,9 +835,7 @@ func (s *expenseService) processDocuments(ctx *fiber.Ctx, expenseRepoTx reposito func (s *expenseService) DeleteDocument(ctx *fiber.Ctx, expenseID uint, documentID uint64, isRealization bool) error { if err := commonSvc.EnsureRelations(ctx.Context(), - commonSvc.RelationCheck{Name: "Expense", ID: &expenseID, Exists: func(ctx context.Context, id uint) (bool, error) { - return s.Repository.IdExists(ctx, uint64(id)) - }}, + commonSvc.RelationCheck{Name: "Expense", ID: &expenseID, Exists: s.Repository.IdExists}, ); err != nil { return err } @@ -929,9 +917,7 @@ func (s *expenseService) Approval(c *fiber.Ctx, req *validation.ApprovalRequest, for _, id := range req.ApprovableIds { 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)) - }}, + commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: s.Repository.IdExists}, ); err != nil { return err } diff --git a/internal/modules/expenses/validations/expense.validation.go b/internal/modules/expenses/validations/expense.validation.go index 4e909b66..cdc79ebd 100644 --- a/internal/modules/expenses/validations/expense.validation.go +++ b/internal/modules/expenses/validations/expense.validation.go @@ -27,6 +27,8 @@ type CostItem struct { type Update struct { TransactionDate *string `form:"transaction_date" json:"transaction_date" validate:"omitempty,datetime=2006-01-02"` + Category *string `form:"category" json:"category" validate:"omitempty,oneof=BOP NON-BOP"` + SupplierID *uint64 `form:"supplier_id" json:"supplier_id" validate:"omitempty,gt=0"` CostPerKandang *[]CostPerKandang `form:"cost_per_kandang" json:"cost_per_kandang" validate:"omitempty,min=1,dive"` Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"` } diff --git a/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go b/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go index 712c6ace..ad1ea8aa 100644 --- a/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go +++ b/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go @@ -190,9 +190,6 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) if latestApproval.StepNumber >= uint16(utils.MarketingDeliveryOrder) { return nil, fiber.NewError(fiber.StatusBadRequest, "Delivery order already exists for this marketing") } - if latestApproval.Action == nil || *latestApproval.Action != entity.ApprovalActionApproved { - return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Marketing is not approved - current status: %v", *latestApproval.Action)) - } err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { diff --git a/internal/modules/marketing/sales-orders/repositories/marketings.repository.go b/internal/modules/marketing/sales-orders/repositories/marketings.repository.go index dd0f99ab..df8a7c98 100644 --- a/internal/modules/marketing/sales-orders/repositories/marketings.repository.go +++ b/internal/modules/marketing/sales-orders/repositories/marketings.repository.go @@ -2,16 +2,22 @@ package repository import ( "context" + "fmt" + "strconv" + "strings" "gitlab.com/mbugroup/lti-api.git/internal/common/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gitlab.com/mbugroup/lti-api.git/internal/utils" "gorm.io/gorm" + "gorm.io/gorm/clause" ) type MarketingRepository interface { repository.BaseRepository[entity.Marketing] IdExists(ctx context.Context, id uint) (bool, error) GetNextSequence(ctx context.Context) (uint, error) + NextSoNumber(ctx context.Context, tx *gorm.DB) (string, error) } type MarketingRepositoryImpl struct { @@ -35,3 +41,82 @@ func (r *MarketingRepositoryImpl) GetNextSequence(ctx context.Context) (uint, er } return maxID + 1, nil } + +func (r *MarketingRepositoryImpl) NextSoNumber(ctx context.Context, tx *gorm.DB) (string, error) { + return r.generateSequentialNumber(ctx, tx, "so_number", utils.MarketingSoNumberPrefix, utils.MarketingNumberPadding) +} + +func parseNumericSuffix(value, prefix string) (int, bool) { + if !strings.HasPrefix(value, prefix) { + return 0, false + } + suffix := strings.TrimPrefix(value, prefix) + if suffix == "" { + return 0, false + } + trimmed := strings.TrimLeft(suffix, "0") + if trimmed == "" { + trimmed = "0" + } + number, err := strconv.Atoi(trimmed) + if err != nil { + return 0, false + } + return number, true +} + +func (r *MarketingRepositoryImpl) numberExists(ctx context.Context, db *gorm.DB, column, value string) (bool, error) { + var count int64 + if err := db.WithContext(ctx). + Model(&entity.Marketing{}). + Where(fmt.Sprintf("%s = ?", column), value). + Count(&count).Error; err != nil { + return false, err + } + return count > 0, nil +} + +func (r *MarketingRepositoryImpl) generateSequentialNumber(ctx context.Context, tx *gorm.DB, column, prefix string, padding int) (string, error) { + + db := tx + if db == nil { + db = r.DB() + } + + var values []string + err := db.WithContext(ctx). + Model(&entity.Marketing{}). + Where(fmt.Sprintf("%s LIKE ?", column), prefix+"%"). + Select(column). + Order(fmt.Sprintf("%s DESC", column)). + Limit(20). + Clauses(clause.Locking{Strength: "UPDATE"}). + Pluck(column, &values).Error + if err != nil { + return "", err + } + + next := 1 + for _, value := range values { + if number, ok := parseNumericSuffix(value, prefix); ok { + next = number + 1 + break + } + } + + const maxAttempts = 20 + for attempt := 0; attempt < maxAttempts; attempt++ { + candidate := fmt.Sprintf("%s%0*d", prefix, padding, next) + exists, err := r.numberExists(ctx, db, column, candidate) + if err != nil { + return "", err + } + if !exists { + return candidate, nil + } + next++ + } + + return "", fmt.Errorf("unable to generate unique %s", column) + +} diff --git a/internal/modules/marketing/sales-orders/services/sales-orders.service.go b/internal/modules/marketing/sales-orders/services/sales-orders.service.go index d750c4a4..05ffe8ec 100644 --- a/internal/modules/marketing/sales-orders/services/sales-orders.service.go +++ b/internal/modules/marketing/sales-orders/services/sales-orders.service.go @@ -109,11 +109,10 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid date format") } - nextSeq, err := s.MarketingRepo.GetNextSequence(c.Context()) + soNumber, err := s.MarketingRepo.NextSoNumber(context.Background(), nil) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate SO number") } - soNumber := fmt.Sprintf("SO-%05d", nextSeq) var marketing *entity.Marketing err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { @@ -321,21 +320,24 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u } } if latestApproval != nil { - actorID := uint(1) // todo: ambil dari auth context - action := entity.ApprovalActionUpdated - _, err := approvalSvcTx.CreateApproval( - c.Context(), - utils.ApprovalWorkflowMarketing, - id, - approvalutils.ApprovalStep(latestApproval.StepNumber), - &action, - actorID, - nil) - if err != nil { - if !errors.Is(err, gorm.ErrDuplicatedKey) { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to create update approval") + if *latestApproval.Action != entity.ApprovalActionUpdated { + actorID := uint(1) // todo: ambil dari auth context + action := entity.ApprovalActionUpdated + _, err := approvalSvcTx.CreateApproval( + c.Context(), + utils.ApprovalWorkflowMarketing, + id, + utils.MarketingStepPengajuan, + &action, + actorID, + nil) + if err != nil { + if !errors.Is(err, gorm.ErrDuplicatedKey) { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create update approval") + } } } + } return nil diff --git a/internal/utils/constant.go b/internal/utils/constant.go index 98381df6..e9d0d60d 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -243,6 +243,9 @@ const ( MarketingStepPengajuan approvalutils.ApprovalStep = 1 MarketingStepSalesOrder approvalutils.ApprovalStep = 2 MarketingDeliveryOrder approvalutils.ApprovalStep = 3 + + MarketingSoNumberPrefix = "SO-" + MarketingNumberPadding = 5 ) var MarketingApprovalSteps = map[approvalutils.ApprovalStep]string{