feat[BE]: update customer payment report to support multiple customer IDs and nullable aging days

This commit is contained in:
aguhh18
2026-01-14 14:06:34 +07:00
parent f6e872c0aa
commit 7daa509cd0
5 changed files with 109 additions and 38 deletions
@@ -243,25 +243,30 @@ func (c *RepportController) GetHppPerKandang(ctx *fiber.Ctx) error {
} }
func (c *RepportController) GetCustomerPayment(ctx *fiber.Ctx) error { func (c *RepportController) GetCustomerPayment(ctx *fiber.Ctx) error {
var customerID *uint var customerIDs []uint
if customerIDStr := ctx.Query("customer_id"); customerIDStr != "" { if customerIDsStr := ctx.Query("customer_ids"); customerIDsStr != "" {
if id, err := strconv.ParseUint(customerIDStr, 10, 32); err == nil { ids := strings.Split(customerIDsStr, ",")
cid := uint(id) for _, idStr := range ids {
customerID = &cid idStr = strings.TrimSpace(idStr)
if idStr != "" {
if id, err := strconv.ParseUint(idStr, 10, 32); err == nil {
customerIDs = append(customerIDs, uint(id))
}
}
} }
} }
query := &validation.CustomerPaymentQuery{ query := &validation.CustomerPaymentQuery{
Page: ctx.QueryInt("page", 1), Page: ctx.QueryInt("page", 1),
Limit: ctx.QueryInt("limit", 10), Limit: ctx.QueryInt("limit", 10),
CustomerID: customerID, CustomerIDs: customerIDs,
StartDate: ctx.Query("start_date", ""), StartDate: ctx.Query("start_date", ""),
EndDate: ctx.Query("end_date", ""), EndDate: ctx.Query("end_date", ""),
} }
// Validate pagination // Validate pagination
if customerID == nil && (query.Page < 1 || query.Limit < 1) { if len(customerIDs) == 0 && (query.Page < 1 || query.Limit < 1) {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0 when customer_ids is not provided")
} }
result, totalResults, err := c.RepportService.GetCustomerPayment(ctx, query) result, totalResults, err := c.RepportService.GetCustomerPayment(ctx, query)
@@ -269,8 +274,8 @@ func (c *RepportController) GetCustomerPayment(ctx *fiber.Ctx) error {
return err return err
} }
// If single customer mode, return without pagination // If single customer mode (only 1 customer ID), return without pagination
if customerID != nil { if len(customerIDs) == 1 {
return ctx.Status(fiber.StatusOK). return ctx.Status(fiber.StatusOK).
JSON(response.Success{ JSON(response.Success{
Code: fiber.StatusOK, Code: fiber.StatusOK,
@@ -23,7 +23,7 @@ type CustomerPaymentReportRow struct {
TotalPrice float64 `json:"total_price"` TotalPrice float64 `json:"total_price"`
PaymentAmount float64 `json:"payment_amount"` PaymentAmount float64 `json:"payment_amount"`
AccountsReceivable float64 `json:"accounts_receivable"` AccountsReceivable float64 `json:"accounts_receivable"`
AgingDay int `json:"aging_day"` AgingDay *int `json:"aging_day"`
Status string `json:"status"` Status string `json:"status"`
PickupInfo string `json:"pickup_info"` PickupInfo string `json:"pickup_info"`
SalesPerson string `json:"sales_person"` SalesPerson string `json:"sales_person"`
@@ -2,6 +2,7 @@ package repositories
import ( import (
"context" "context"
"time" "time"
"gorm.io/gorm" "gorm.io/gorm"
@@ -324,10 +324,14 @@ func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.C
var customerIDs []uint var customerIDs []uint
var totalCustomers int64 var totalCustomers int64
if params.CustomerID != nil { if len(params.CustomerIDs) > 0 {
// Single customer mode // Specific customer IDs mode (no pagination)
customerIDs = []uint{*params.CustomerID} customerIDs = params.CustomerIDs
totalCustomers = 1 totalCustomers = int64(len(customerIDs))
if len(customerIDs) == 0 {
return []dto.CustomerPaymentReportItem{}, 0, nil
}
} else { } else {
// Multiple customers mode with pagination // Multiple customers mode with pagination
page := params.Page page := params.Page
@@ -366,7 +370,6 @@ func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.C
} }
func (s *repportService) processCustomerPayment(ctx context.Context, customerID uint) (dto.CustomerPaymentReportItem, error) { func (s *repportService) processCustomerPayment(ctx context.Context, customerID uint) (dto.CustomerPaymentReportItem, error) {
// Get customer info
customer := entity.Customer{} customer := entity.Customer{}
if err := s.DB.WithContext(ctx). if err := s.DB.WithContext(ctx).
Where("id = ?", customerID). Where("id = ?", customerID).
@@ -374,24 +377,21 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID
return dto.CustomerPaymentReportItem{}, err return dto.CustomerPaymentReportItem{}, err
} }
// Get initial balance
initialBalance, err := s.CustomerPaymentRepo.GetInitialBalanceByCustomer(ctx, customerID) initialBalance, err := s.CustomerPaymentRepo.GetInitialBalanceByCustomer(ctx, customerID)
if err != nil { if err != nil {
return dto.CustomerPaymentReportItem{}, err return dto.CustomerPaymentReportItem{}, err
} }
// Get transactions for this customer
cid := customerID cid := customerID
transactions, err := s.CustomerPaymentRepo.GetCustomerPaymentTransactions(ctx, &cid) transactions, err := s.CustomerPaymentRepo.GetCustomerPaymentTransactions(ctx, &cid)
if err != nil { if err != nil {
return dto.CustomerPaymentReportItem{}, err return dto.CustomerPaymentReportItem{}, err
} }
// Process transactions and calculate running balance
rows := make([]dto.CustomerPaymentReportRow, 0, len(transactions)) rows := make([]dto.CustomerPaymentReportRow, 0, len(transactions))
runningBalance := initialBalance runningBalance := initialBalance
for _, tx := range transactions { for i, tx := range transactions {
row := dto.CustomerPaymentReportRow{ row := dto.CustomerPaymentReportRow{
TransactionType: tx.TransactionType, TransactionType: tx.TransactionType,
TransactionID: tx.TransactionID, TransactionID: tx.TransactionID,
@@ -412,26 +412,48 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID
SalesPerson: tx.SalesPerson, SalesPerson: tx.SalesPerson,
} }
// Calculate running balance previousBalance := runningBalance
if tx.TransactionType == "SALES" { if tx.TransactionType == "SALES" {
runningBalance -= tx.TotalPrice runningBalance -= tx.TotalPrice
// Status will be calculated later (requires looking ahead) status, paymentDate := s.determineSalesStatusAndPaymentDate(transactions, i, previousBalance, runningBalance)
row.Status = "" row.Status = status
row.AgingDay = 0 // Will be calculated later
if status == "LUNAS" {
if previousBalance >= tx.TotalPrice {
days := 0
row.AgingDay = &days
} else if paymentDate != nil {
// Aging = payment_date - trans_date (SO date)
days := int(paymentDate.Sub(tx.TransDate).Hours() / 24)
if days < 0 {
days = 0
}
row.AgingDay = &days
} else {
days := 0
row.AgingDay = &days
}
} else {
// Aging = current_date - trans_date (SO date)
days := int(time.Since(tx.TransDate).Hours() / 24)
if days < 0 {
days = 0
}
row.AgingDay = &days
}
} else if tx.TransactionType == "PAYMENT" { } else if tx.TransactionType == "PAYMENT" {
runningBalance += tx.PaymentAmount runningBalance += tx.PaymentAmount
row.Status = "" row.Status = ""
row.AgingDay = 0 row.AgingDay = nil
} }
row.AccountsReceivable = runningBalance row.AccountsReceivable = runningBalance
rows = append(rows, row) rows = append(rows, row)
} }
// Calculate summary
summary := s.calculateSummary(rows, initialBalance) summary := s.calculateSummary(rows, initialBalance)
// Build customer DTO
customerDTO := customerDTO.CustomerRelationDTO{ customerDTO := customerDTO.CustomerRelationDTO{
Id: customer.Id, Id: customer.Id,
Name: customer.Name, Name: customer.Name,
@@ -466,12 +488,55 @@ func (s *repportService) calculateSummary(rows []dto.CustomerPaymentReportRow, i
} }
} }
// Final AR = initial balance + total sales - total payment // Formula: Total AR = Initial Balance - Total Sales + Total Payment
summary.TotalAccountsReceivable = initialBalance + summary.TotalGrandAmount - summary.TotalPayment // - Initial balance: positive (customer deposit)
// - Sales: reduces balance (customer debt)
// - Payment: increases balance (customer pays)
summary.TotalAccountsReceivable = initialBalance - summary.TotalGrandAmount + summary.TotalPayment
return summary return summary
} }
func (s *repportService) determineSalesStatusAndPaymentDate(transactions []repportRepo.CustomerPaymentTransaction, currentIndex int, previousBalance, currentBalance float64) (string, *time.Time) {
currentSales := transactions[currentIndex]
// Status Logic:
// 1. LUNAS: previousBalance >= salesAmount (paid from deposit)
// 2. LUNAS: future payments make AR >= 0 (eventually paid)
// 3. DIBAYAR SEBAGIAN: has payment but not enough
// 4. BELUM LUNAS: no payment at all
if previousBalance >= currentSales.TotalPrice {
return "LUNAS", nil
}
hasPartialPaymentFromBalance := previousBalance > 0 && previousBalance < currentSales.TotalPrice
futureBalance := currentBalance
hasPayment := false
var paymentDateThatMadeItLunas *time.Time
for i := currentIndex + 1; i < len(transactions); i++ {
if transactions[i].TransactionType == "PAYMENT" {
futureBalance += transactions[i].PaymentAmount
hasPayment = true
if futureBalance >= 0 {
paymentDateThatMadeItLunas = &transactions[i].TransDate
return "LUNAS", paymentDateThatMadeItLunas
}
} else if transactions[i].TransactionType == "SALES" {
futureBalance -= transactions[i].TotalPrice
}
}
if hasPayment || hasPartialPaymentFromBalance {
return "DIBAYAR SEBAGIAN", nil
}
return "BELUM LUNAS", nil
}
func mapRecordingToProductionResultDTO(record entity.Recording) dto.ProductionResultDTO { func mapRecordingToProductionResultDTO(record entity.Recording) dto.ProductionResultDTO {
result := dto.ProductionResultDTO{ result := dto.ProductionResultDTO{
CreatedAt: record.CreatedAt, CreatedAt: record.CreatedAt,
@@ -73,9 +73,9 @@ type ProductionResultQuery struct {
} }
type CustomerPaymentQuery struct { type CustomerPaymentQuery struct {
Page int `query:"page" validate:"omitempty,min=1,gt=0"` Page int `query:"page" validate:"omitempty,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"` Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"`
CustomerID *uint `query:"customer_id" validate:"omitempty,gt=0"` CustomerIDs []uint `query:"customer_ids" validate:"omitempty,dive,gt=0"`
StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"` StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"` EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"`
} }