From 458c8e0a9128952909fdc7caa9811d695507f514 Mon Sep 17 00:00:00 2001 From: "Hafizh A. Y" Date: Sat, 24 Jan 2026 13:35:13 +0700 Subject: [PATCH] fix(BE): edit customer, finance: bank optional, nominal minus, and filter --- ...3_alter_payments_bank_id_nullable.down.sql | 6 ++ ...853_alter_payments_bank_id_nullable.up.sql | 6 ++ .../finance/initials/dto/initial.dto.go | 11 +-- .../initials/services/initial.service.go | 58 +++++++++---- .../validations/initial.validation.go | 2 +- .../injections/services/injection.service.go | 10 ++- .../validations/injection.validation.go | 4 +- .../controllers/transaction.controller.go | 43 +++++++++- .../transactions/dto/transaction.dto.go | 11 +-- .../services/transaction.service.go | 83 ++++++++++++++++++- .../validations/transaction.validation.go | 13 ++- .../customers/services/customer.service.go | 16 ++++ 12 files changed, 226 insertions(+), 37 deletions(-) create mode 100644 internal/database/migrations/20260124050853_alter_payments_bank_id_nullable.down.sql create mode 100644 internal/database/migrations/20260124050853_alter_payments_bank_id_nullable.up.sql diff --git a/internal/database/migrations/20260124050853_alter_payments_bank_id_nullable.down.sql b/internal/database/migrations/20260124050853_alter_payments_bank_id_nullable.down.sql new file mode 100644 index 00000000..1b1c7f6f --- /dev/null +++ b/internal/database/migrations/20260124050853_alter_payments_bank_id_nullable.down.sql @@ -0,0 +1,6 @@ +BEGIN; + +ALTER TABLE payments + ALTER COLUMN bank_id SET NOT NULL; + +COMMIT; diff --git a/internal/database/migrations/20260124050853_alter_payments_bank_id_nullable.up.sql b/internal/database/migrations/20260124050853_alter_payments_bank_id_nullable.up.sql new file mode 100644 index 00000000..b95dcbf0 --- /dev/null +++ b/internal/database/migrations/20260124050853_alter_payments_bank_id_nullable.up.sql @@ -0,0 +1,6 @@ +BEGIN; + +ALTER TABLE payments + ALTER COLUMN bank_id DROP NOT NULL; + +COMMIT; diff --git a/internal/modules/finance/initials/dto/initial.dto.go b/internal/modules/finance/initials/dto/initial.dto.go index 1311024f..454422fa 100644 --- a/internal/modules/finance/initials/dto/initial.dto.go +++ b/internal/modules/finance/initials/dto/initial.dto.go @@ -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" diff --git a/internal/modules/finance/initials/services/initial.service.go b/internal/modules/finance/initials/services/initial.service.go index e06e99dd..14ab1f54 100644 --- a/internal/modules/finance/initials/services/initial.service.go +++ b/internal/modules/finance/initials/services/initial.service.go @@ -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 + } +} diff --git a/internal/modules/finance/initials/validations/initial.validation.go b/internal/modules/finance/initials/validations/initial.validation.go index 27df2eea..d3e1df54 100644 --- a/internal/modules/finance/initials/validations/initial.validation.go +++ b/internal/modules/finance/initials/validations/initial.validation.go @@ -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"` diff --git a/internal/modules/finance/injections/services/injection.service.go b/internal/modules/finance/injections/services/injection.service.go index 8cb80e1c..99b0e488 100644 --- a/internal/modules/finance/injections/services/injection.service.go +++ b/internal/modules/finance/injections/services/injection.service.go @@ -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 { diff --git a/internal/modules/finance/injections/validations/injection.validation.go b/internal/modules/finance/injections/validations/injection.validation.go index b5b75087..744256a1 100644 --- a/internal/modules/finance/injections/validations/injection.validation.go +++ b/internal/modules/finance/injections/validations/injection.validation.go @@ -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"` } diff --git a/internal/modules/finance/transactions/controllers/transaction.controller.go b/internal/modules/finance/transactions/controllers/transaction.controller.go index fa3e1369..5c25cbcd 100644 --- a/internal/modules/finance/transactions/controllers/transaction.controller.go +++ b/internal/modules/finance/transactions/controllers/transaction.controller.go @@ -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 { diff --git a/internal/modules/finance/transactions/dto/transaction.dto.go b/internal/modules/finance/transactions/dto/transaction.dto.go index 07703fce..89b63ecf 100644 --- a/internal/modules/finance/transactions/dto/transaction.dto.go +++ b/internal/modules/finance/transactions/dto/transaction.dto.go @@ -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 { diff --git a/internal/modules/finance/transactions/services/transaction.service.go b/internal/modules/finance/transactions/services/transaction.service.go index f7398d43..f422320f 100644 --- a/internal/modules/finance/transactions/services/transaction.service.go +++ b/internal/modules/finance/transactions/services/transaction.service.go @@ -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") + } +} diff --git a/internal/modules/finance/transactions/validations/transaction.validation.go b/internal/modules/finance/transactions/validations/transaction.validation.go index 7d16d3ee..f367dda1 100644 --- a/internal/modules/finance/transactions/validations/transaction.validation.go +++ b/internal/modules/finance/transactions/validations/transaction.validation.go @@ -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"` } diff --git a/internal/modules/master/customers/services/customer.service.go b/internal/modules/master/customers/services/customer.service.go index fe4cb41e..6156dc8c 100644 --- a/internal/modules/master/customers/services/customer.service.go +++ b/internal/modules/master/customers/services/customer.service.go @@ -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) }