Files
lti-api/internal/modules/finance/initials/services/initial.service.go
T
2026-01-09 16:03:41 +07:00

338 lines
9.5 KiB
Go

package service
import (
"context"
"errors"
"fmt"
"math"
"strings"
"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"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/initials/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/initials/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 InitialService 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 initialService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.InitialRepository
ApprovalSvc commonSvc.ApprovalService
approvalWorkflow approvalutils.ApprovalWorkflowKey
}
func NewInitialService(
repo repository.InitialRepository,
approvalSvc commonSvc.ApprovalService,
validate *validator.Validate,
) InitialService {
return &initialService{
Log: utils.Log,
Validate: validate,
Repository: repo,
ApprovalSvc: approvalSvc,
approvalWorkflow: utils.ApprovalWorkflowInitial,
}
}
func (s initialService) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser").
Preload("BankWarehouse").
Preload("Customer").
Preload("Supplier")
}
func (s initialService) GetOne(c *fiber.Ctx, id uint) (*entity.Payment, error) {
initial, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Initial not found")
}
if err != nil {
s.Log.Errorf("Failed get initial by id: %+v", err)
return nil, err
}
if !isInitialTransaction(initial.TransactionType) {
return nil, fiber.NewError(fiber.StatusNotFound, "Initial not found")
}
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 initial %d: %+v", id, err)
} else {
initial.LatestApproval = approval
}
}
return initial, nil
}
func (s *initialService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Payment, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
party, err := normalizePartyType(req.PartyType)
if err != nil {
return nil, err
}
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
}
balanceType, err := normalizeInitialBalanceType(req.InitialBalanceType)
if err != nil {
return nil, err
}
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
code, err := s.generateInitialCode(c.Context())
if err != nil {
return nil, err
}
reference := req.ReferenceNumber
createBody := &entity.Payment{
PaymentCode: code,
ReferenceNumber: &reference,
TransactionType: string(utils.TransactionTypeSaldoAwal),
PartyType: party,
PartyId: req.PartyId,
PartyAccountNumber: nil,
PaymentDate: time.Now(),
PaymentMethod: string(utils.PaymentMethodSaldo),
BankId: req.BankId,
Direction: directionForInitialType(balanceType),
Nominal: signedNominal(balanceType, req.Nominal),
Notes: req.Note,
CreatedBy: actorID,
}
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
initialRepoTx := repository.NewInitialRepository(dbTransaction)
if err := initialRepoTx.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.InitialStepPengajuan,
&action,
actorID,
nil,
)
if err != nil {
return err
}
}
return nil
})
if err != nil {
s.Log.Errorf("Failed to create initial: %+v", err)
return nil, err
}
return s.GetOne(c, createBody.Id)
}
func (s initialService) 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.ReferenceNumber != nil {
updateBody["reference_number"] = *req.ReferenceNumber
}
if req.Note != nil {
updateBody["notes"] = *req.Note
}
if req.BankId != nil {
if err := s.ensureBankExists(c.Context(), req.BankId); err != nil {
return nil, err
}
updateBody["bank_id"] = *req.BankId
}
requiresExisting := req.PartyType != nil || req.PartyId != nil || req.InitialBalanceType != nil || req.Nominal != nil
requiresVerification := requiresExisting || req.ReferenceNumber != nil || req.Note != nil || req.BankId != nil
var existing *entity.Payment
if requiresVerification {
current, err := s.Repository.GetByID(c.Context(), id, nil)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Initial not found")
}
if err != nil {
s.Log.Errorf("Failed get initial by id: %+v", err)
return nil, err
}
if !isInitialTransaction(current.TransactionType) {
return nil, fiber.NewError(fiber.StatusNotFound, "Initial not found")
}
existing = current
}
if req.PartyType != nil || req.PartyId != nil {
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
}
if req.PartyId != nil {
partyId = *req.PartyId
updateBody["party_id"] = partyId
}
if err := s.ensurePartyExists(c.Context(), partyType, partyId); err != nil {
return nil, err
}
}
if req.InitialBalanceType != nil || req.Nominal != nil {
balanceType := balanceTypeFromPayment(existing)
if req.InitialBalanceType != nil {
normalized, err := normalizeInitialBalanceType(*req.InitialBalanceType)
if err != nil {
return nil, err
}
balanceType = normalized
}
nominal := math.Abs(existing.Nominal)
if req.Nominal != nil {
nominal = *req.Nominal
}
updateBody["direction"] = directionForInitialType(balanceType)
updateBody["nominal"] = signedNominal(balanceType, nominal)
}
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, "Initial not found")
}
s.Log.Errorf("Failed to update initial: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func isInitialTransaction(transactionType string) bool {
return strings.EqualFold(transactionType, string(utils.TransactionTypeSaldoAwal))
}
func balanceTypeFromPayment(payment *entity.Payment) string {
if strings.EqualFold(payment.Direction, "OUT") || payment.Nominal < 0 {
return "NEGATIVE"
}
return "POSITIVE"
}
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 normalizeInitialBalanceType(balanceType string) (string, error) {
normalized := strings.ToUpper(strings.TrimSpace(balanceType))
switch normalized {
case "NEGATIVE", "POSITIVE":
return normalized, nil
default:
return "", utils.BadRequest("`initial_balance_type` must be `NEGATIVE` or `POSITIVE`")
}
}
func directionForInitialType(balanceType string) string {
if strings.EqualFold(balanceType, "NEGATIVE") {
return "OUT"
}
return "IN"
}
func signedNominal(balanceType string, nominal float64) float64 {
normalized := math.Abs(nominal)
if strings.EqualFold(balanceType, "NEGATIVE") {
return -normalized
}
return normalized
}
func (s initialService) generateInitialCode(ctx context.Context) (string, error) {
sequence, err := s.Repository.NextPaymentSequence(ctx)
if err != nil {
return "", err
}
return fmt.Sprintf("INIT-%05d", sequence), nil
}
func (s initialService) approvalQueryModifier() func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return db.Preload("ActionUser")
}
}
func (s initialService) 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 initialService) ensureBankExists(ctx context.Context, bankId *uint) error {
return commonSvc.EnsureRelations(ctx,
commonSvc.RelationCheck{Name: "Bank", ID: bankId, Exists: s.Repository.BankExists},
)
}