mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 05:21:57 +00:00
feat[BE]: enhance customer payment report with vehicle numbers and pickup info, add date filtering
This commit is contained in:
@@ -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]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user