feat[BE]: enhance customer payment report with vehicle numbers and pickup info, add date filtering

This commit is contained in:
aguhh18
2026-01-14 15:15:29 +07:00
parent 7daa509cd0
commit 804ff45dbd
4 changed files with 141 additions and 78 deletions
@@ -52,6 +52,7 @@ func ToCustomerRelationDTO(e entity.Customer) CustomerRelationDTO {
Name: e.Name,
Type: e.Type,
AccountNumber: e.AccountNumber,
Balance: e.Balance,
Pic: pic,
}
}
@@ -1,32 +1,35 @@
package dto
import (
"strings"
"time"
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
repportRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/repositories"
)
type CustomerPaymentReportRow struct {
TransactionType string `json:"transaction_type"`
TransactionID int64 `json:"transaction_id"`
TransDate time.Time `json:"trans_date"`
DeliveryDate *time.Time `json:"delivery_date"`
Reference string `json:"reference"`
VehicleNumbers string `json:"vehicle_numbers"`
Qty float64 `json:"qty"`
Weight float64 `json:"weight"`
AverageWeight float64 `json:"average_weight"`
Price float64 `json:"price"`
CreditNote float64 `json:"credit_note"`
FinalPrice float64 `json:"final_price"`
PPN float64 `json:"ppn"`
TotalPrice float64 `json:"total_price"`
PaymentAmount float64 `json:"payment_amount"`
AccountsReceivable float64 `json:"accounts_receivable"`
AgingDay *int `json:"aging_day"`
Status string `json:"status"`
PickupInfo string `json:"pickup_info"`
SalesPerson string `json:"sales_person"`
TransactionType string `json:"transaction_type"`
TransactionID int64 `json:"transaction_id"`
TransDate time.Time `json:"trans_date"`
DeliveryDate *time.Time `json:"delivery_date"`
Reference string `json:"reference"`
Qty float64 `json:"qty"`
Weight float64 `json:"weight"`
AverageWeight float64 `json:"average_weight"`
Price float64 `json:"price"`
CreditNote float64 `json:"credit_note"`
FinalPrice float64 `json:"final_price"`
PPN float64 `json:"ppn"`
TotalPrice float64 `json:"total_price"`
PaymentAmount float64 `json:"payment_amount"`
AccountsReceivable float64 `json:"accounts_receivable"`
AgingDay *int `json:"aging_day"`
Status string `json:"status"`
VehicleNumbers []string `json:"vehicle_numbers"`
PickupInfo []string `json:"pickup_info"`
SalesPerson string `json:"sales_person"`
}
type CustomerPaymentReportSummary struct {
@@ -51,3 +54,70 @@ type CustomerPaymentReportItem struct {
type CustomerPaymentReportResponse struct {
Data []CustomerPaymentReportItem `json:"data"`
}
func ToCustomerPaymentReportRow(tx repportRepo.CustomerPaymentTransaction) CustomerPaymentReportRow {
return CustomerPaymentReportRow{
TransactionType: tx.TransactionType,
TransactionID: tx.TransactionID,
TransDate: tx.TransDate,
DeliveryDate: tx.DeliveryDate,
Reference: tx.Reference,
Qty: tx.Qty,
Weight: tx.Weight,
AverageWeight: tx.AverageWeight,
Price: tx.Price,
CreditNote: tx.CreditNote,
FinalPrice: tx.FinalPrice,
PPN: tx.PPN,
TotalPrice: tx.TotalPrice,
PaymentAmount: tx.PaymentAmount,
VehicleNumbers: parseStringSlice(tx.VehicleNumbers),
PickupInfo: parseStringSlice(tx.PickupInfo),
SalesPerson: tx.SalesPerson,
}
}
func parseStringSlice(str string) []string {
str = strings.TrimSpace(str)
if str == "" || str == "-" {
return []string{}
}
parts := strings.Split(str, ",")
result := make([]string, 0, len(parts))
for _, part := range parts {
part = strings.TrimSpace(part)
if part != "" {
result = append(result, part)
}
}
return result
}
func CalculateCustomerPaymentSummary(rows []CustomerPaymentReportRow, initialBalance float64) CustomerPaymentReportSummary {
summary := CustomerPaymentReportSummary{}
for _, row := range rows {
summary.TotalQty += row.Qty
summary.TotalWeight += row.Weight
summary.TotalCreditNote += row.CreditNote
summary.TotalPPN += row.PPN
if row.TransactionType == "SALES" {
summary.TotalInitialAmount += row.TotalPrice
summary.TotalFinalAmount += row.FinalPrice
summary.TotalGrandAmount += row.TotalPrice
} else if row.TransactionType == "PAYMENT" {
summary.TotalPayment += row.PaymentAmount
}
}
// Formula: Total AR = Initial Balance - Total Sales + Total Payment
// - Initial balance: positive (customer deposit)
// - Sales: reduces balance (customer debt)
// - Payment: increases balance (customer pays)
summary.TotalAccountsReceivable = initialBalance - summary.TotalGrandAmount + summary.TotalPayment
return summary
}
@@ -54,7 +54,8 @@ func (r *customerPaymentRepositoryImpl) GetCustomerPaymentTransactions(ctx conte
m.so_date::DATE AS trans_date,
MAX(mdp.delivery_date)::DATE AS delivery_date,
m.so_number AS reference,
COALESCE(STRING_AGG(DISTINCT mdp.vehicle_number, ', ') FILTER (WHERE mdp.vehicle_number IS NOT NULL AND mdp.vehicle_number != ''), '') AS vehicle_numbers,
COALESCE(STRING_AGG(DISTINCT mdp.vehicle_number, ', ') FILTER (WHERE mdp.vehicle_number IS NOT NULL), '') AS vehicle_numbers,
COALESCE(SUM(COALESCE(mp.qty, 0)), 0)::NUMERIC(15,3) AS qty,
COALESCE(SUM(COALESCE(mdp.total_weight, mp.total_weight, 0)), 0)::NUMERIC(15,3) AS weight,
CASE WHEN COALESCE(SUM(COALESCE(mp.qty, 0)), 0) > 0
@@ -77,7 +78,8 @@ func (r *customerPaymentRepositoryImpl) GetCustomerPaymentTransactions(ctx conte
Joins("LEFT JOIN warehouses w ON w.id = pw.warehouse_id").
Joins("LEFT JOIN users u ON u.id = m.sales_person_id").
Where("m.deleted_at IS NULL").
Where("c.deleted_at IS NULL")
Where("c.deleted_at IS NULL").
Where("mdp.delivery_date IS NOT NULL")
if customerID != nil {
salesQuery = salesQuery.Where("c.id = ?", *customerID)
@@ -359,7 +359,7 @@ func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.C
// Process each customer
var result []dto.CustomerPaymentReportItem
for _, customerID := range customerIDs {
item, err := s.processCustomerPayment(ctx.Context(), customerID)
item, err := s.processCustomerPayment(ctx.Context(), customerID, params)
if err != nil {
return nil, 0, err
}
@@ -369,7 +369,7 @@ func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.C
return result, totalCustomers, nil
}
func (s *repportService) processCustomerPayment(ctx context.Context, customerID uint) (dto.CustomerPaymentReportItem, error) {
func (s *repportService) processCustomerPayment(ctx context.Context, customerID uint, params *validation.CustomerPaymentQuery) (dto.CustomerPaymentReportItem, error) {
customer := entity.Customer{}
if err := s.DB.WithContext(ctx).
Where("id = ?", customerID).
@@ -392,28 +392,11 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID
runningBalance := initialBalance
for i, tx := range transactions {
row := dto.CustomerPaymentReportRow{
TransactionType: tx.TransactionType,
TransactionID: tx.TransactionID,
TransDate: tx.TransDate,
DeliveryDate: tx.DeliveryDate,
Reference: tx.Reference,
VehicleNumbers: tx.VehicleNumbers,
Qty: tx.Qty,
Weight: tx.Weight,
AverageWeight: tx.AverageWeight,
Price: tx.Price,
CreditNote: tx.CreditNote,
FinalPrice: tx.FinalPrice,
PPN: tx.PPN,
TotalPrice: tx.TotalPrice,
PaymentAmount: tx.PaymentAmount,
PickupInfo: tx.PickupInfo,
SalesPerson: tx.SalesPerson,
}
previousBalance := runningBalance
row := dto.ToCustomerPaymentReportRow(tx)
if tx.TransactionType == "SALES" {
runningBalance -= tx.TotalPrice
status, paymentDate := s.determineSalesStatusAndPaymentDate(transactions, i, previousBalance, runningBalance)
@@ -452,51 +435,58 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID
rows = append(rows, row)
}
summary := s.calculateSummary(rows, initialBalance)
if params.StartDate != "" || params.EndDate != "" {
filteredRows := make([]dto.CustomerPaymentReportRow, 0, len(rows))
location, err := time.LoadLocation("Asia/Jakarta")
if err != nil {
return dto.CustomerPaymentReportItem{}, err
}
customerDTO := customerDTO.CustomerRelationDTO{
Id: customer.Id,
Name: customer.Name,
Type: customer.Type,
AccountNumber: customer.AccountNumber,
Balance: customer.Balance,
var startDate, endDate *time.Time
if params.StartDate != "" {
parsed, err := time.ParseInLocation("2006-01-02", params.StartDate, location)
if err != nil {
return dto.CustomerPaymentReportItem{}, err
}
startDate = &parsed
}
if params.EndDate != "" {
parsed, err := time.ParseInLocation("2006-01-02", params.EndDate, location)
if err != nil {
return dto.CustomerPaymentReportItem{}, err
}
// End date should be inclusive, so set to end of day
endOfDay := time.Date(parsed.Year(), parsed.Month(), parsed.Day(), 23, 59, 59, 999999999, location)
endDate = &endOfDay
}
for _, row := range rows {
transDate := row.TransDate.In(location)
// Check if transaction date is within range
if startDate != nil && transDate.Before(*startDate) {
continue
}
if endDate != nil && transDate.After(*endDate) {
continue
}
filteredRows = append(filteredRows, row)
}
rows = filteredRows
}
summary := dto.CalculateCustomerPaymentSummary(rows, initialBalance)
return dto.CustomerPaymentReportItem{
Customer: customerDTO,
Customer: customerDTO.ToCustomerRelationDTO(customer),
InitialBalance: initialBalance,
Rows: rows,
Summary: summary,
}, nil
}
func (s *repportService) calculateSummary(rows []dto.CustomerPaymentReportRow, initialBalance float64) dto.CustomerPaymentReportSummary {
summary := dto.CustomerPaymentReportSummary{}
for _, row := range rows {
summary.TotalQty += row.Qty
summary.TotalWeight += row.Weight
summary.TotalCreditNote += row.CreditNote
summary.TotalPPN += row.PPN
if row.TransactionType == "SALES" {
summary.TotalInitialAmount += row.TotalPrice
summary.TotalFinalAmount += row.FinalPrice
summary.TotalGrandAmount += row.TotalPrice
} else if row.TransactionType == "PAYMENT" {
summary.TotalPayment += row.PaymentAmount
}
}
// Formula: Total AR = Initial Balance - Total Sales + Total Payment
// - Initial balance: positive (customer deposit)
// - Sales: reduces balance (customer debt)
// - Payment: increases balance (customer pays)
summary.TotalAccountsReceivable = initialBalance - summary.TotalGrandAmount + summary.TotalPayment
return summary
}
func (s *repportService) determineSalesStatusAndPaymentDate(transactions []repportRepo.CustomerPaymentTransaction, currentIndex int, previousBalance, currentBalance float64) (string, *time.Time) {
currentSales := transactions[currentIndex]