feat(BE): finance (payment, initial_balance, injection). fix(BE): kandang capacity

This commit is contained in:
Hafizh A. Y
2025-12-27 14:30:03 +07:00
parent e33b23a2aa
commit 1c875a916b
46 changed files with 3068 additions and 23 deletions
@@ -0,0 +1,96 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/finance/transactions/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/transactions/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/transactions/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type TransactionController struct {
TransactionService service.TransactionService
}
func NewTransactionController(transactionService service.TransactionService) *TransactionController {
return &TransactionController{
TransactionService: transactionService,
}
}
func (u *TransactionController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
}
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.TransactionService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.TransactionListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all transactions successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToTransactionListDTOs(result),
})
}
func (u *TransactionController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
result, err := u.TransactionService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get transaction successfully",
Data: dto.ToTransactionListDTO(*result),
})
}
func (u *TransactionController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.TransactionService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete transaction successfully",
})
}
@@ -0,0 +1,189 @@
package dto
import (
"strings"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
bankDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
)
// === DTO Structs ===
type TransactionRelationDTO struct {
Id uint `json:"id"`
PaymentCode string `json:"payment_code"`
ReferenceNumber *string `json:"reference_number,omitempty"`
TransactionType string `json:"transaction_type"`
Party Party `json:"party"`
PaymentDate time.Time `json:"payment_date"`
PaymentMethod string `json:"payment_method"`
Bank bankDTO.BankRelationDTO `json:"bank,omitempty"`
ExpenseAmount float64 `json:"expense_amount"`
IncomeAmount float64 `json:"income_amount"`
Nominal float64 `json:"nominal"`
Notes string `json:"notes"`
CreatedUser userDTO.UserRelationDTO `json:"created_user,omitempty"`
}
type TransactionListDTO struct {
Id uint `json:"id"`
PaymentCode string `json:"payment_code"`
ReferenceNumber *string `json:"reference_number"`
TransactionType string `json:"transaction_type"`
Party Party `json:"party"`
PaymentDate time.Time `json:"payment_date"`
PaymentMethod string `json:"payment_method"`
Bank bankDTO.BankRelationDTO `json:"bank"`
ExpenseAmount float64 `json:"expense_amount"`
IncomeAmount float64 `json:"income_amount"`
Nominal float64 `json:"nominal"`
Notes string `json:"notes"`
CreatedUser userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
}
type TransactionDetailDTO struct {
TransactionListDTO
}
type Party struct {
Id uint `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
AccountNumber string `json:"account_number"`
}
// === Mapper Functions ===
func ToTransactionRelationDTO(e entity.Payment) TransactionRelationDTO {
expenseAmount, incomeAmount := paymentAmounts(e.Direction, e.Nominal)
return TransactionRelationDTO{
Id: e.Id,
PaymentCode: paymentCodeFromPayment(e),
ReferenceNumber: e.ReferenceNumber,
TransactionType: transactionTypeFromPayment(e),
Party: partyFromPayment(e),
PaymentDate: e.PaymentDate,
PaymentMethod: e.PaymentMethod,
Bank: bankFromPayment(e),
ExpenseAmount: expenseAmount,
IncomeAmount: incomeAmount,
Nominal: e.Nominal,
Notes: e.Notes,
CreatedUser: userFromPayment(e),
}
}
func ToTransactionListDTO(e entity.Payment) TransactionListDTO {
expenseAmount, incomeAmount := paymentAmounts(e.Direction, e.Nominal)
approval := approvalDTO.ApprovalRelationDTO{}
if e.LatestApproval != nil {
approval = approvalDTO.ToApprovalDTO(*e.LatestApproval)
}
return TransactionListDTO{
Id: e.Id,
PaymentCode: paymentCodeFromPayment(e),
ReferenceNumber: e.ReferenceNumber,
TransactionType: transactionTypeFromPayment(e),
Party: partyFromPayment(e),
PaymentDate: e.PaymentDate,
PaymentMethod: e.PaymentMethod,
Bank: bankFromPayment(e),
ExpenseAmount: expenseAmount,
IncomeAmount: incomeAmount,
Nominal: e.Nominal,
Notes: e.Notes,
CreatedUser: userFromPayment(e),
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
Approval: approval,
}
}
func ToTransactionListDTOs(e []entity.Payment) []TransactionListDTO {
result := make([]TransactionListDTO, len(e))
for i, r := range e {
result[i] = ToTransactionListDTO(r)
}
return result
}
func ToTransactionDetailDTO(e entity.Payment) TransactionDetailDTO {
return TransactionDetailDTO{
TransactionListDTO: ToTransactionListDTO(e),
}
}
func partyFromPayment(e entity.Payment) Party {
party := Party{
Id: e.PartyId,
Type: e.PartyType,
}
switch utils.PaymentParty(e.PartyType) {
case utils.PaymentPartyCustomer:
if e.Customer != nil && e.Customer.Id != 0 {
party.Name = e.Customer.Name
party.AccountNumber = e.Customer.AccountNumber
}
case utils.PaymentPartySupplier:
if e.Supplier != nil && e.Supplier.Id != 0 {
party.Name = e.Supplier.Name
if e.Supplier.AccountNumber != nil {
party.AccountNumber = *e.Supplier.AccountNumber
}
}
}
return party
}
func bankFromPayment(e entity.Payment) bankDTO.BankRelationDTO {
if e.BankWarehouse.Id == 0 {
return bankDTO.BankRelationDTO{}
}
return bankDTO.ToBankRelationDTO(e.BankWarehouse)
}
func userFromPayment(e entity.Payment) userDTO.UserRelationDTO {
if e.CreatedUser.Id == 0 {
return userDTO.UserRelationDTO{}
}
return userDTO.ToUserRelationDTO(e.CreatedUser)
}
func paymentCodeFromPayment(e entity.Payment) string {
if e.PaymentCode != "" {
return e.PaymentCode
}
if e.ReferenceNumber != nil {
return *e.ReferenceNumber
}
return ""
}
func transactionTypeFromPayment(e entity.Payment) string {
if e.TransactionType != "" {
return e.TransactionType
}
return e.Direction
}
func paymentAmounts(direction string, nominal float64) (float64, float64) {
switch strings.ToUpper(direction) {
case "IN":
return 0, nominal
case "OUT":
return nominal, 0
default:
return 0, 0
}
}
@@ -0,0 +1,42 @@
package transactions
import (
"fmt"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gorm.io/gorm"
rTransaction "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/transactions/repositories"
sTransaction "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/transactions/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type TransactionModule struct{}
func (TransactionModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
transactionRepo := rTransaction.NewTransactionRepository(db)
userRepo := rUser.NewUserRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowPayment, utils.PaymentApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register payment approval workflow: %v", err))
}
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowInitial, utils.InitialApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register initial approval workflow: %v", err))
}
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowInjection, utils.InjectionApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register injection approval workflow: %v", err))
}
transactionService := sTransaction.NewTransactionService(transactionRepo, approvalService, validate)
userService := sUser.NewUserService(userRepo, validate)
TransactionRoutes(router, userService, transactionService)
}
@@ -0,0 +1,21 @@
package repository
import (
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type TransactionRepository interface {
repository.BaseRepository[entity.Payment]
}
type TransactionRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Payment]
}
func NewTransactionRepository(db *gorm.DB) TransactionRepository {
return &TransactionRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Payment](db),
}
}
@@ -0,0 +1,21 @@
package transactions
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/transactions/controllers"
transaction "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/transactions/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func TransactionRoutes(v1 fiber.Router, u user.UserService, s transaction.TransactionService) {
ctrl := controller.NewTransactionController(s)
route := v1.Group("/transactions")
// route.Use(m.Auth(u))
route.Get("/", ctrl.GetAll)
route.Get("/:id", ctrl.GetOne)
route.Delete("/:id", ctrl.DeleteOne)
}
@@ -0,0 +1,175 @@
package service
import (
"context"
"errors"
"strings"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/transactions/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/transactions/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 TransactionService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Payment, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.Payment, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
}
type transactionService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.TransactionRepository
ApprovalSvc commonSvc.ApprovalService
approvalWorkflows map[string]approvalutils.ApprovalWorkflowKey
}
func NewTransactionService(
repo repository.TransactionRepository,
approvalSvc commonSvc.ApprovalService,
validate *validator.Validate,
) TransactionService {
return &transactionService{
Log: utils.Log,
Validate: validate,
Repository: repo,
ApprovalSvc: approvalSvc,
approvalWorkflows: map[string]approvalutils.ApprovalWorkflowKey{
string(utils.TransactionTypeSaldoAwal): utils.ApprovalWorkflowInitial,
string(utils.TransactionTypeInjection): utils.ApprovalWorkflowInjection,
},
}
}
func (s transactionService) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser").
Preload("BankWarehouse").
Preload("Customer").
Preload("Supplier")
}
func (s transactionService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Payment, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
transactions, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
like := "%" + strings.ToLower(strings.TrimSpace(params.Search)) + "%"
return db.Where(
`LOWER(payment_code) LIKE ? OR
LOWER(COALESCE(reference_number, '')) LIKE ? OR
LOWER(COALESCE(transaction_type, '')) LIKE ? OR
LOWER(COALESCE(notes, '')) LIKE ?`,
like, like, like, like,
)
}
return db.Order("payment_date DESC").Order("created_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get transactions: %+v", err)
return nil, 0, err
}
s.attachApprovals(c.Context(), transactions)
return transactions, total, nil
}
func (s transactionService) GetOne(c *fiber.Ctx, id uint) (*entity.Payment, error) {
transaction, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Transaction not found")
}
if err != nil {
s.Log.Errorf("Failed get transaction by id: %+v", err)
return nil, err
}
if s.ApprovalSvc != nil {
approval, err := s.ApprovalSvc.LatestByTarget(
c.Context(),
s.workflowForTransaction(transaction),
id,
s.approvalQueryModifier(),
)
if err != nil {
s.Log.Warnf("Unable to load latest approval for transaction %d: %+v", id, err)
} else {
transaction.LatestApproval = approval
}
}
return transaction, nil
}
func (s transactionService) DeleteOne(c *fiber.Ctx, id uint) error {
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Transaction not found")
}
s.Log.Errorf("Failed to delete transaction: %+v", err)
return err
}
return nil
}
func (s transactionService) attachApprovals(ctx context.Context, transactions []entity.Payment) {
if s.ApprovalSvc == nil || len(transactions) == 0 {
return
}
workflowIDs := map[approvalutils.ApprovalWorkflowKey][]uint{}
for _, transaction := range transactions {
workflow := s.workflowForTransaction(&transaction)
workflowIDs[workflow] = append(workflowIDs[workflow], transaction.Id)
}
approvalByWorkflow := make(map[approvalutils.ApprovalWorkflowKey]map[uint]*entity.Approval, len(workflowIDs))
for workflow, ids := range workflowIDs {
if len(ids) == 0 {
continue
}
approvals, err := s.ApprovalSvc.LatestByTargets(ctx, workflow, ids, s.approvalQueryModifier())
if err != nil {
s.Log.Warnf("Unable to load latest approvals for transactions: %+v", err)
continue
}
approvalByWorkflow[workflow] = approvals
}
for i := range transactions {
workflow := s.workflowForTransaction(&transactions[i])
if approvals, ok := approvalByWorkflow[workflow]; ok {
transactions[i].LatestApproval = approvals[transactions[i].Id]
}
}
}
func (s transactionService) workflowForTransaction(transaction *entity.Payment) approvalutils.ApprovalWorkflowKey {
if transaction == nil {
return utils.ApprovalWorkflowPayment
}
transactionType := strings.TrimSpace(strings.ToUpper(transaction.TransactionType))
if transactionType == "" {
return utils.ApprovalWorkflowPayment
}
if workflow, ok := s.approvalWorkflows[transactionType]; ok {
return workflow
}
return utils.ApprovalWorkflowPayment
}
func (s transactionService) approvalQueryModifier() func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return db.Preload("ActionUser")
}
}
@@ -0,0 +1,15 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
Search string `query:"search" validate:"omitempty,max=50"`
}