package service import ( "context" "errors" "fmt" "strings" 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" m "gitlab.com/mbugroup/lti-api.git/internal/middleware" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/payments/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/payments/validations" "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 PaymentService interface { GetOne(ctx *fiber.Ctx, id uint) (*entity.Payment, error) CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Payment, error) UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Payment, error) } type paymentService struct { Log *logrus.Logger Validate *validator.Validate Repository repository.PaymentRepository ApprovalSvc commonSvc.ApprovalService approvalWorkflow approvalutils.ApprovalWorkflowKey } func NewPaymentService( repo repository.PaymentRepository, approvalSvc commonSvc.ApprovalService, validate *validator.Validate, ) PaymentService { return &paymentService{ Log: utils.Log, Validate: validate, Repository: repo, ApprovalSvc: approvalSvc, approvalWorkflow: utils.ApprovalWorkflowPayment, } } func (s paymentService) withRelations(db *gorm.DB) *gorm.DB { return db.Preload("CreatedUser"). Preload("BankWarehouse"). Preload("Customer"). Preload("Supplier") } func (s paymentService) GetOne(c *fiber.Ctx, id uint) (*entity.Payment, error) { payment, err := s.Repository.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Payment not found") } if err != nil { s.Log.Errorf("Failed get payment by id: %+v", err) return nil, err } if s.ApprovalSvc != nil { approval, err := s.ApprovalSvc.LatestByTarget(c.Context(), s.approvalWorkflow, id, s.approvalQueryModifier()) if err != nil { s.Log.Warnf("Unable to load latest approval for payment %d: %+v", id, err) } else { payment.LatestApproval = approval } } return payment, nil } func (s *paymentService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Payment, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } //! CHECK PARTY TYPE party, err := normalizePartyType(req.PartyType) if err != nil { return nil, err } //! CHECK EXISTS if err := s.ensurePartyExists(c.Context(), party, req.PartyId); err != nil { return nil, err } if err := s.ensureBankExists(c.Context(), req.BankId); err != nil { return nil, err } //? NORMALIZE paymentDate, err := utils.ParseDateString(req.PaymentDate) if err != nil { return nil, utils.BadRequest(err.Error()) } method, err := normalizePaymentMethod(req.PaymentMethod) if err != nil { return nil, err } transactionType, err := s.resolveTransactionType(c.Context(), party, req.PartyId) if err != nil { return nil, err } //? GET CREATED BY actorID, err := m.ActorIDFromContext(c) if err != nil { return nil, err } code, err := s.generatePaymentCode(c.Context(), party) if err != nil { return nil, err } createBody := &entity.Payment{ PaymentCode: code, ReferenceNumber: req.ReferenceNumber, TransactionType: transactionType, PartyType: party, PartyId: req.PartyId, PaymentDate: paymentDate, PaymentMethod: method, BankId: req.BankId, Direction: directionForParty(party), Nominal: req.Nominal, Notes: req.Notes, CreatedBy: actorID, } err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { paymentRepoTx := repository.NewPaymentRepository(dbTransaction) if err := paymentRepoTx.CreateOne(c.Context(), createBody, nil); err != nil { return err } if s.ApprovalSvc != nil { action := entity.ApprovalActionCreated approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) _, err := approvalSvcTx.CreateApproval( c.Context(), s.approvalWorkflow, createBody.Id, utils.PaymentStepPengajuan, &action, actorID, nil, ) if err != nil { return err } } return nil }) if err != nil { s.Log.Errorf("Failed to create payment: %+v", err) return nil, err } return s.GetOne(c, createBody.Id) } func (s paymentService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Payment, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } updateBody := make(map[string]any) if req.PaymentDate != nil { parsedDate, err := utils.ParseDateString(*req.PaymentDate) if err != nil { return nil, utils.BadRequest(err.Error()) } updateBody["payment_date"] = parsedDate } if req.Nominal != nil { updateBody["nominal"] = *req.Nominal } if req.ReferenceNumber != nil { updateBody["reference_number"] = *req.ReferenceNumber } if req.PaymentMethod != nil { method, err := normalizePaymentMethod(*req.PaymentMethod) if err != nil { return nil, err } updateBody["payment_method"] = method } if req.BankId != nil { if err := s.ensureBankExists(c.Context(), req.BankId); err != nil { return nil, err } updateBody["bank_id"] = *req.BankId } if req.Notes != nil { updateBody["notes"] = *req.Notes } if req.PartyType != nil || req.PartyId != nil { existing, err := s.Repository.GetByID(c.Context(), id, nil) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Payment not found") } if err != nil { s.Log.Errorf("Failed get payment by id: %+v", err) return nil, err } partyType := existing.PartyType partyId := existing.PartyId if req.PartyType != nil { normalized, err := normalizePartyType(*req.PartyType) if err != nil { return nil, err } partyType = normalized updateBody["party_type"] = partyType updateBody["direction"] = directionForParty(partyType) } if req.PartyId != nil { partyId = *req.PartyId updateBody["party_id"] = partyId } if err := s.ensurePartyExists(c.Context(), partyType, partyId); err != nil { return nil, err } transactionType, err := s.resolveTransactionType(c.Context(), partyType, partyId) if err != nil { return nil, err } updateBody["transaction_type"] = transactionType } if len(updateBody) == 0 { return s.GetOne(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, "Payment not found") } s.Log.Errorf("Failed to update payment: %+v", err) return nil, err } return s.GetOne(c, id) } func normalizePartyType(partyType string) (string, error) { party := strings.ToUpper(strings.TrimSpace(partyType)) if !utils.IsValidPaymentParty(party) { return "", utils.BadRequest("`party_type` must be `customer` or `supplier`") } return party, nil } func normalizePaymentMethod(method string) (string, error) { normalized := strings.ToUpper(strings.TrimSpace(method)) if !utils.IsValidPaymentMethod(normalized) { return "", utils.BadRequest("Invalid payment_method") } return normalized, nil } func directionForParty(partyType string) string { if utils.PaymentParty(partyType) == utils.PaymentPartyCustomer { return "IN" } return "OUT" } func (s paymentService) resolveTransactionType(ctx context.Context, partyType string, partyId uint) (string, error) { switch utils.PaymentParty(partyType) { case utils.PaymentPartyCustomer: return string(utils.TransactionTypePenjualan), nil case utils.PaymentPartySupplier: category, err := s.getSupplierCategory(ctx, partyId) if err != nil { return "", err } if isSupplierCategoryBiaya(category) { return string(utils.TransactionTypeBiaya), nil } return string(utils.TransactionTypePembelian), nil default: return "", utils.BadRequest("`party_type` must be `customer` or `supplier`") } } func (s paymentService) generatePaymentCode(ctx context.Context, partyType string) (string, error) { prefix := "PAY" switch utils.PaymentParty(partyType) { case utils.PaymentPartyCustomer: prefix = "PAY-IN" case utils.PaymentPartySupplier: prefix = "PAY-OUT" } sequence, err := s.Repository.NextPaymentSequence(ctx) if err != nil { return "", err } return fmt.Sprintf("%s-%05d", prefix, sequence), nil } func (s paymentService) approvalQueryModifier() func(*gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { return db.Preload("ActionUser") } } func (s paymentService) ensurePartyExists(ctx context.Context, partyType string, partyId uint) error { switch utils.PaymentParty(partyType) { case utils.PaymentPartyCustomer: return commonSvc.EnsureRelations(ctx, commonSvc.RelationCheck{Name: "Customer", ID: &partyId, Exists: s.Repository.CustomerExists}, ) case utils.PaymentPartySupplier: return commonSvc.EnsureRelations(ctx, commonSvc.RelationCheck{Name: "Supplier", ID: &partyId, Exists: s.Repository.SupplierExists}, ) default: return utils.BadRequest("`party_type` must be `customer` or `supplier`") } } func (s paymentService) ensureBankExists(ctx context.Context, bankId *uint) error { return commonSvc.EnsureRelations(ctx, commonSvc.RelationCheck{Name: "Bank", ID: bankId, Exists: s.Repository.BankExists}, ) } func (s paymentService) getSupplierCategory(ctx context.Context, supplierId uint) (string, error) { category, err := s.Repository.SupplierCategory(ctx, supplierId) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return "", utils.NotFound("Supplier not found") } return "", err } return strings.ToUpper(strings.TrimSpace(category)), nil } func isSupplierCategoryBiaya(category string) bool { switch strings.ToUpper(strings.TrimSpace(category)) { case string(utils.SupplierCategoryBOP), "BIAYA": return true default: return false } }