mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 05:21:57 +00:00
Merge branch 'fix/customer-finance' into 'development'
[FIX][BE] Edit customer, finance: bank optional, nominal minus, and filter See merge request mbugroup/lti-api!249
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE payments
|
||||
ALTER COLUMN bank_id SET NOT NULL;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,6 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE payments
|
||||
ALTER COLUMN bank_id DROP NOT NULL;
|
||||
|
||||
COMMIT;
|
||||
@@ -20,7 +20,7 @@ type InitialRelationDTO struct {
|
||||
InitialBalanceType string `json:"initial_balance_type"`
|
||||
InitialBalanceTypeLabel string `json:"initial_balance_type_label"`
|
||||
Party Party `json:"party"`
|
||||
Bank bankDTO.BankRelationDTO `json:"bank,omitempty"`
|
||||
Bank *bankDTO.BankRelationDTO `json:"bank"`
|
||||
Direction string `json:"direction"`
|
||||
Nominal float64 `json:"nominal"`
|
||||
Notes string `json:"notes"`
|
||||
@@ -128,11 +128,12 @@ func partyFromInitial(e entity.Payment) Party {
|
||||
return party
|
||||
}
|
||||
|
||||
func bankFromInitial(e entity.Payment) bankDTO.BankRelationDTO {
|
||||
func bankFromInitial(e entity.Payment) *bankDTO.BankRelationDTO {
|
||||
if e.BankWarehouse.Id == 0 {
|
||||
return bankDTO.BankRelationDTO{}
|
||||
return nil
|
||||
}
|
||||
return bankDTO.ToBankRelationDTO(e.BankWarehouse)
|
||||
bank := bankDTO.ToBankRelationDTO(e.BankWarehouse)
|
||||
return &bank
|
||||
}
|
||||
|
||||
func userFromInitial(e entity.Payment) userDTO.UserRelationDTO {
|
||||
@@ -161,7 +162,7 @@ func initialBalanceLabel(balanceType string) string {
|
||||
}
|
||||
|
||||
func initialBalanceTypeFromPayment(e entity.Payment) string {
|
||||
if strings.EqualFold(e.Direction, "OUT") || e.Nominal < 0 {
|
||||
if e.Nominal < 0 {
|
||||
return "NEGATIVE"
|
||||
}
|
||||
return "POSITIVE"
|
||||
|
||||
@@ -82,6 +82,7 @@ func (s initialService) GetOne(c *fiber.Ctx, id uint) (*entity.Payment, error) {
|
||||
}
|
||||
|
||||
func (s *initialService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Payment, error) {
|
||||
normalizeOptionalBankId(&req.BankId)
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -124,7 +125,7 @@ func (s *initialService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
|
||||
PaymentDate: time.Now(),
|
||||
PaymentMethod: string(utils.PaymentMethodSaldo),
|
||||
BankId: req.BankId,
|
||||
Direction: directionForInitialType(balanceType),
|
||||
Direction: directionForInitialType(party, balanceType),
|
||||
Nominal: signedNominal(balanceType, req.Nominal),
|
||||
Notes: req.Note,
|
||||
CreatedBy: actorID,
|
||||
@@ -164,6 +165,7 @@ func (s *initialService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
|
||||
}
|
||||
|
||||
func (s initialService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Payment, error) {
|
||||
normalizeOptionalBankId(&req.BankId)
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -186,6 +188,8 @@ func (s initialService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
||||
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
|
||||
var resolvedPartyType string
|
||||
var resolvedPartyId uint
|
||||
if requiresVerification {
|
||||
current, err := s.Repository.GetByID(c.Context(), id, nil)
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
@@ -199,26 +203,25 @@ func (s initialService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Initial not found")
|
||||
}
|
||||
existing = current
|
||||
resolvedPartyType = existing.PartyType
|
||||
resolvedPartyId = existing.PartyId
|
||||
}
|
||||
|
||||
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
|
||||
resolvedPartyType = normalized
|
||||
updateBody["party_type"] = resolvedPartyType
|
||||
}
|
||||
if req.PartyId != nil {
|
||||
partyId = *req.PartyId
|
||||
updateBody["party_id"] = partyId
|
||||
resolvedPartyId = *req.PartyId
|
||||
updateBody["party_id"] = resolvedPartyId
|
||||
}
|
||||
|
||||
if err := s.ensurePartyExists(c.Context(), partyType, partyId); err != nil {
|
||||
if err := s.ensurePartyExists(c.Context(), resolvedPartyType, resolvedPartyId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -238,8 +241,11 @@ func (s initialService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
||||
nominal = *req.Nominal
|
||||
}
|
||||
|
||||
updateBody["direction"] = directionForInitialType(balanceType)
|
||||
updateBody["direction"] = directionForInitialType(resolvedPartyType, balanceType)
|
||||
updateBody["nominal"] = signedNominal(balanceType, nominal)
|
||||
} else if req.PartyType != nil {
|
||||
balanceType := balanceTypeFromPayment(existing)
|
||||
updateBody["direction"] = directionForInitialType(resolvedPartyType, balanceType)
|
||||
}
|
||||
|
||||
if len(updateBody) == 0 {
|
||||
@@ -262,7 +268,7 @@ func isInitialTransaction(transactionType string) bool {
|
||||
}
|
||||
|
||||
func balanceTypeFromPayment(payment *entity.Payment) string {
|
||||
if strings.EqualFold(payment.Direction, "OUT") || payment.Nominal < 0 {
|
||||
if payment.Nominal < 0 {
|
||||
return "NEGATIVE"
|
||||
}
|
||||
return "POSITIVE"
|
||||
@@ -286,11 +292,24 @@ func normalizeInitialBalanceType(balanceType string) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func directionForInitialType(balanceType string) string {
|
||||
if strings.EqualFold(balanceType, "NEGATIVE") {
|
||||
return "OUT"
|
||||
func directionForInitialType(partyType string, balanceType string) string {
|
||||
switch utils.PaymentParty(strings.ToUpper(strings.TrimSpace(partyType))) {
|
||||
case utils.PaymentPartySupplier:
|
||||
if strings.EqualFold(balanceType, "POSITIVE") {
|
||||
return "OUT"
|
||||
}
|
||||
return "IN"
|
||||
case utils.PaymentPartyCustomer:
|
||||
if strings.EqualFold(balanceType, "NEGATIVE") {
|
||||
return "OUT"
|
||||
}
|
||||
return "IN"
|
||||
default:
|
||||
if strings.EqualFold(balanceType, "NEGATIVE") {
|
||||
return "OUT"
|
||||
}
|
||||
return "IN"
|
||||
}
|
||||
return "IN"
|
||||
}
|
||||
|
||||
func signedNominal(balanceType string, nominal float64) float64 {
|
||||
@@ -335,3 +354,12 @@ func (s initialService) ensureBankExists(ctx context.Context, bankId *uint) erro
|
||||
commonSvc.RelationCheck{Name: "Bank", ID: bankId, Exists: s.Repository.BankExists},
|
||||
)
|
||||
}
|
||||
|
||||
func normalizeOptionalBankId(bankId **uint) {
|
||||
if bankId == nil || *bankId == nil {
|
||||
return
|
||||
}
|
||||
if **bankId == 0 {
|
||||
*bankId = nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package validation
|
||||
type Create struct {
|
||||
PartyType string `json:"party_type" validate:"required_strict,max=50"`
|
||||
PartyId uint `json:"party_id" validate:"required_strict,number,gt=0"`
|
||||
BankId *uint `json:"bank_id" validate:"required_strict,number,gt=0"`
|
||||
BankId *uint `json:"bank_id,omitempty" validate:"omitempty,number,gt=0"`
|
||||
ReferenceNumber string `json:"reference_number" validate:"required_strict,max=100"`
|
||||
InitialBalanceType string `json:"initial_balance_type" validate:"required_strict,oneof=NEGATIVE POSITIVE"`
|
||||
Nominal float64 `json:"nominal" validate:"required_strict,gt=0"`
|
||||
|
||||
@@ -110,7 +110,7 @@ func (s *injectionService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
||||
PaymentDate: adjustmentDate,
|
||||
PaymentMethod: string(utils.PaymentMethodSaldo),
|
||||
BankId: req.BankId,
|
||||
Direction: "IN",
|
||||
Direction: directionForInjectionNominal(req.Nominal),
|
||||
Nominal: req.Nominal,
|
||||
Notes: req.Notes,
|
||||
CreatedBy: actorID,
|
||||
@@ -186,6 +186,7 @@ func (s injectionService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
||||
}
|
||||
if req.Nominal != nil {
|
||||
updateBody["nominal"] = *req.Nominal
|
||||
updateBody["direction"] = directionForInjectionNominal(*req.Nominal)
|
||||
}
|
||||
if req.Notes != nil {
|
||||
updateBody["notes"] = *req.Notes
|
||||
@@ -210,6 +211,13 @@ func isInjectionTransaction(transactionType string) bool {
|
||||
return strings.EqualFold(transactionType, string(utils.TransactionTypeInjection))
|
||||
}
|
||||
|
||||
func directionForInjectionNominal(nominal float64) string {
|
||||
if nominal < 0 {
|
||||
return "OUT"
|
||||
}
|
||||
return "IN"
|
||||
}
|
||||
|
||||
func (s injectionService) generateInjectionCode(ctx context.Context) (string, error) {
|
||||
sequence, err := s.Repository.NextPaymentSequence(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -3,14 +3,14 @@ package validation
|
||||
type Create struct {
|
||||
BankId *uint `json:"bank_id" validate:"required_strict,number,gt=0"`
|
||||
AdjustmentDate string `json:"adjustment_date" validate:"required_strict"`
|
||||
Nominal float64 `json:"nominal" validate:"required_strict,gt=0"`
|
||||
Nominal float64 `json:"nominal" validate:"required_strict"`
|
||||
Notes string `json:"notes" validate:"required_strict,max=500"`
|
||||
}
|
||||
|
||||
type Update struct {
|
||||
BankId *uint `json:"bank_id,omitempty" validate:"omitempty,number,gt=0"`
|
||||
AdjustmentDate *string `json:"adjustment_date,omitempty" validate:"omitempty"`
|
||||
Nominal *float64 `json:"nominal,omitempty" validate:"omitempty,gt=0"`
|
||||
Nominal *float64 `json:"nominal,omitempty"`
|
||||
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package controller
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/finance/transactions/dto"
|
||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/transactions/services"
|
||||
@@ -23,10 +24,46 @@ func NewTransactionController(transactionService service.TransactionService) *Tr
|
||||
}
|
||||
|
||||
func (u *TransactionController) GetAll(c *fiber.Ctx) error {
|
||||
parseOptionalUint := func(key string) (*uint, error) {
|
||||
raw := strings.TrimSpace(c.Query(key, ""))
|
||||
if raw == "" {
|
||||
return nil, nil
|
||||
}
|
||||
parsed, err := strconv.ParseUint(raw, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid "+key)
|
||||
}
|
||||
if parsed == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
value := uint(parsed)
|
||||
return &value, nil
|
||||
}
|
||||
|
||||
bankId, err := parseOptionalUint("bank_id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
customerId, err := parseOptionalUint("customer_id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
supplierId, err := parseOptionalUint("supplier_id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query := &validation.Query{
|
||||
Page: c.QueryInt("page", 1),
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
Search: c.Query("search", ""),
|
||||
Page: c.QueryInt("page", 1),
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
Search: c.Query("search", ""),
|
||||
TransactionType: c.Query("transaction_type", ""),
|
||||
BankId: bankId,
|
||||
CustomerId: customerId,
|
||||
SupplierId: supplierId,
|
||||
SortDate: c.Query("sort_date", ""),
|
||||
StartDate: c.Query("start_date", ""),
|
||||
EndDate: c.Query("end_date", ""),
|
||||
}
|
||||
|
||||
if query.Page < 1 || query.Limit < 1 {
|
||||
|
||||
@@ -21,7 +21,7 @@ type TransactionRelationDTO struct {
|
||||
Party Party `json:"party"`
|
||||
PaymentDate time.Time `json:"payment_date"`
|
||||
PaymentMethod string `json:"payment_method"`
|
||||
Bank bankDTO.BankRelationDTO `json:"bank,omitempty"`
|
||||
Bank *bankDTO.BankRelationDTO `json:"bank"`
|
||||
ExpenseAmount float64 `json:"expense_amount"`
|
||||
IncomeAmount float64 `json:"income_amount"`
|
||||
Nominal float64 `json:"nominal"`
|
||||
@@ -37,7 +37,7 @@ type TransactionListDTO struct {
|
||||
Party Party `json:"party"`
|
||||
PaymentDate time.Time `json:"payment_date"`
|
||||
PaymentMethod string `json:"payment_method"`
|
||||
Bank bankDTO.BankRelationDTO `json:"bank"`
|
||||
Bank *bankDTO.BankRelationDTO `json:"bank"`
|
||||
ExpenseAmount float64 `json:"expense_amount"`
|
||||
IncomeAmount float64 `json:"income_amount"`
|
||||
Nominal float64 `json:"nominal"`
|
||||
@@ -151,11 +151,12 @@ func partyFromPayment(e entity.Payment) Party {
|
||||
return party
|
||||
}
|
||||
|
||||
func bankFromPayment(e entity.Payment) bankDTO.BankRelationDTO {
|
||||
func bankFromPayment(e entity.Payment) *bankDTO.BankRelationDTO {
|
||||
if e.BankWarehouse.Id == 0 {
|
||||
return bankDTO.BankRelationDTO{}
|
||||
return nil
|
||||
}
|
||||
return bankDTO.ToBankRelationDTO(e.BankWarehouse)
|
||||
bank := bankDTO.ToBankRelationDTO(e.BankWarehouse)
|
||||
return &bank
|
||||
}
|
||||
|
||||
func userFromPayment(e entity.Payment) userDTO.UserRelationDTO {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
@@ -61,13 +62,19 @@ func (s transactionService) GetAll(c *fiber.Ctx, params *validation.Query) ([]en
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
startDate, endDate, err := parseTransactionDateRange(params.StartDate, params.EndDate)
|
||||
if 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(
|
||||
db = db.Where(
|
||||
`LOWER(payment_code) LIKE ? OR
|
||||
LOWER(COALESCE(reference_number, '')) LIKE ? OR
|
||||
LOWER(COALESCE(transaction_type, '')) LIKE ? OR
|
||||
@@ -75,7 +82,35 @@ func (s transactionService) GetAll(c *fiber.Ctx, params *validation.Query) ([]en
|
||||
like, like, like, like,
|
||||
)
|
||||
}
|
||||
return db.Order("payment_date DESC").Order("created_at DESC")
|
||||
|
||||
if strings.TrimSpace(params.TransactionType) != "" {
|
||||
db = db.Where("transaction_type = ?", strings.ToUpper(strings.TrimSpace(params.TransactionType)))
|
||||
}
|
||||
|
||||
if params.BankId != nil {
|
||||
db = db.Where("bank_id = ?", *params.BankId)
|
||||
}
|
||||
|
||||
if params.CustomerId != nil && params.SupplierId != nil {
|
||||
db = db.Where(
|
||||
"(party_type = ? AND party_id = ?) OR (party_type = ? AND party_id = ?)",
|
||||
string(utils.PaymentPartyCustomer), *params.CustomerId,
|
||||
string(utils.PaymentPartySupplier), *params.SupplierId,
|
||||
)
|
||||
} else if params.CustomerId != nil {
|
||||
db = db.Where("party_type = ? AND party_id = ?", string(utils.PaymentPartyCustomer), *params.CustomerId)
|
||||
} else if params.SupplierId != nil {
|
||||
db = db.Where("party_type = ? AND party_id = ?", string(utils.PaymentPartySupplier), *params.SupplierId)
|
||||
}
|
||||
|
||||
if startDate != nil {
|
||||
db = db.Where("payment_date >= ?", *startDate)
|
||||
}
|
||||
if endDate != nil {
|
||||
db = db.Where("payment_date < ?", *endDate)
|
||||
}
|
||||
|
||||
return applyTransactionSort(db, params.SortDate)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -173,3 +208,47 @@ func (s transactionService) approvalQueryModifier() func(*gorm.DB) *gorm.DB {
|
||||
return db.Preload("ActionUser")
|
||||
}
|
||||
}
|
||||
|
||||
func parseTransactionDateRange(startDate, endDate string) (*time.Time, *time.Time, error) {
|
||||
start := strings.TrimSpace(startDate)
|
||||
end := strings.TrimSpace(endDate)
|
||||
|
||||
var startPtr *time.Time
|
||||
var endPtr *time.Time
|
||||
var endValue *time.Time
|
||||
|
||||
if start != "" {
|
||||
parsed, err := utils.ParseDateString(start)
|
||||
if err != nil {
|
||||
return nil, nil, utils.BadRequest("start_date must use format YYYY-MM-DD")
|
||||
}
|
||||
startPtr = &parsed
|
||||
}
|
||||
|
||||
if end != "" {
|
||||
parsed, err := utils.ParseDateString(end)
|
||||
if err != nil {
|
||||
return nil, nil, utils.BadRequest("end_date must use format YYYY-MM-DD")
|
||||
}
|
||||
endValue = &parsed
|
||||
nextDay := parsed.AddDate(0, 0, 1)
|
||||
endPtr = &nextDay
|
||||
}
|
||||
|
||||
if startPtr != nil && endValue != nil && startPtr.After(*endValue) {
|
||||
return nil, nil, utils.BadRequest("start_date must be earlier than end_date")
|
||||
}
|
||||
|
||||
return startPtr, endPtr, nil
|
||||
}
|
||||
|
||||
func applyTransactionSort(db *gorm.DB, sortDate string) *gorm.DB {
|
||||
switch strings.ToLower(strings.TrimSpace(sortDate)) {
|
||||
case "created_at":
|
||||
return db.Order("created_at DESC").Order("payment_date DESC")
|
||||
case "payment_date":
|
||||
return db.Order("payment_date DESC").Order("created_at DESC")
|
||||
default:
|
||||
return db.Order("payment_date DESC").Order("created_at DESC")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,14 @@ type Update struct {
|
||||
}
|
||||
|
||||
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"`
|
||||
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"`
|
||||
TransactionType string `query:"transaction_type" validate:"omitempty,max=50"`
|
||||
BankId *uint `query:"bank_id" validate:"omitempty,number,gt=0"`
|
||||
CustomerId *uint `query:"customer_id" validate:"omitempty,number,gt=0"`
|
||||
SupplierId *uint `query:"supplier_id" validate:"omitempty,number,gt=0"`
|
||||
SortDate string `query:"sort_date" validate:"omitempty,oneof=created_at payment_date"`
|
||||
StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
|
||||
EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"`
|
||||
}
|
||||
|
||||
@@ -156,6 +156,22 @@ func (s customerService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint
|
||||
updateBody["type"] = typ
|
||||
}
|
||||
|
||||
if req.Address != nil {
|
||||
updateBody["address"] = *req.Address
|
||||
}
|
||||
|
||||
if req.Phone != nil {
|
||||
updateBody["phone"] = *req.Phone
|
||||
}
|
||||
|
||||
if req.Email != nil {
|
||||
updateBody["email"] = *req.Email
|
||||
}
|
||||
|
||||
if req.AccountNumber != nil {
|
||||
updateBody["account_number"] = *req.AccountNumber
|
||||
}
|
||||
|
||||
if len(updateBody) == 0 {
|
||||
return s.GetOne(c, id)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user