mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +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,
|
Name: e.Name,
|
||||||
Type: e.Type,
|
Type: e.Type,
|
||||||
AccountNumber: e.AccountNumber,
|
AccountNumber: e.AccountNumber,
|
||||||
|
Balance: e.Balance,
|
||||||
Pic: pic,
|
Pic: pic,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,35 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
|
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 {
|
type CustomerPaymentReportRow struct {
|
||||||
TransactionType string `json:"transaction_type"`
|
TransactionType string `json:"transaction_type"`
|
||||||
TransactionID int64 `json:"transaction_id"`
|
TransactionID int64 `json:"transaction_id"`
|
||||||
TransDate time.Time `json:"trans_date"`
|
TransDate time.Time `json:"trans_date"`
|
||||||
DeliveryDate *time.Time `json:"delivery_date"`
|
DeliveryDate *time.Time `json:"delivery_date"`
|
||||||
Reference string `json:"reference"`
|
Reference string `json:"reference"`
|
||||||
VehicleNumbers string `json:"vehicle_numbers"`
|
|
||||||
Qty float64 `json:"qty"`
|
Qty float64 `json:"qty"`
|
||||||
Weight float64 `json:"weight"`
|
Weight float64 `json:"weight"`
|
||||||
AverageWeight float64 `json:"average_weight"`
|
AverageWeight float64 `json:"average_weight"`
|
||||||
Price float64 `json:"price"`
|
Price float64 `json:"price"`
|
||||||
CreditNote float64 `json:"credit_note"`
|
CreditNote float64 `json:"credit_note"`
|
||||||
FinalPrice float64 `json:"final_price"`
|
FinalPrice float64 `json:"final_price"`
|
||||||
PPN float64 `json:"ppn"`
|
PPN float64 `json:"ppn"`
|
||||||
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"`
|
VehicleNumbers []string `json:"vehicle_numbers"`
|
||||||
SalesPerson string `json:"sales_person"`
|
PickupInfo []string `json:"pickup_info"`
|
||||||
|
SalesPerson string `json:"sales_person"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CustomerPaymentReportSummary struct {
|
type CustomerPaymentReportSummary struct {
|
||||||
@@ -51,3 +54,70 @@ type CustomerPaymentReportItem struct {
|
|||||||
type CustomerPaymentReportResponse struct {
|
type CustomerPaymentReportResponse struct {
|
||||||
Data []CustomerPaymentReportItem `json:"data"`
|
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,
|
m.so_date::DATE AS trans_date,
|
||||||
MAX(mdp.delivery_date)::DATE AS delivery_date,
|
MAX(mdp.delivery_date)::DATE AS delivery_date,
|
||||||
m.so_number AS reference,
|
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(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,
|
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
|
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 warehouses w ON w.id = pw.warehouse_id").
|
||||||
Joins("LEFT JOIN users u ON u.id = m.sales_person_id").
|
Joins("LEFT JOIN users u ON u.id = m.sales_person_id").
|
||||||
Where("m.deleted_at IS NULL").
|
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 {
|
if customerID != nil {
|
||||||
salesQuery = salesQuery.Where("c.id = ?", *customerID)
|
salesQuery = salesQuery.Where("c.id = ?", *customerID)
|
||||||
|
|||||||
@@ -359,7 +359,7 @@ func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.C
|
|||||||
// Process each customer
|
// Process each customer
|
||||||
var result []dto.CustomerPaymentReportItem
|
var result []dto.CustomerPaymentReportItem
|
||||||
for _, customerID := range customerIDs {
|
for _, customerID := range customerIDs {
|
||||||
item, err := s.processCustomerPayment(ctx.Context(), customerID)
|
item, err := s.processCustomerPayment(ctx.Context(), customerID, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
@@ -369,7 +369,7 @@ func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.C
|
|||||||
return result, totalCustomers, nil
|
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{}
|
customer := entity.Customer{}
|
||||||
if err := s.DB.WithContext(ctx).
|
if err := s.DB.WithContext(ctx).
|
||||||
Where("id = ?", customerID).
|
Where("id = ?", customerID).
|
||||||
@@ -392,28 +392,11 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID
|
|||||||
runningBalance := initialBalance
|
runningBalance := initialBalance
|
||||||
|
|
||||||
for i, tx := range transactions {
|
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
|
previousBalance := runningBalance
|
||||||
|
|
||||||
|
row := dto.ToCustomerPaymentReportRow(tx)
|
||||||
|
|
||||||
if tx.TransactionType == "SALES" {
|
if tx.TransactionType == "SALES" {
|
||||||
runningBalance -= tx.TotalPrice
|
runningBalance -= tx.TotalPrice
|
||||||
status, paymentDate := s.determineSalesStatusAndPaymentDate(transactions, i, previousBalance, runningBalance)
|
status, paymentDate := s.determineSalesStatusAndPaymentDate(transactions, i, previousBalance, runningBalance)
|
||||||
@@ -452,51 +435,58 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID
|
|||||||
rows = append(rows, row)
|
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{
|
var startDate, endDate *time.Time
|
||||||
Id: customer.Id,
|
if params.StartDate != "" {
|
||||||
Name: customer.Name,
|
parsed, err := time.ParseInLocation("2006-01-02", params.StartDate, location)
|
||||||
Type: customer.Type,
|
if err != nil {
|
||||||
AccountNumber: customer.AccountNumber,
|
return dto.CustomerPaymentReportItem{}, err
|
||||||
Balance: customer.Balance,
|
}
|
||||||
|
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{
|
return dto.CustomerPaymentReportItem{
|
||||||
Customer: customerDTO,
|
Customer: customerDTO.ToCustomerRelationDTO(customer),
|
||||||
InitialBalance: initialBalance,
|
InitialBalance: initialBalance,
|
||||||
Rows: rows,
|
Rows: rows,
|
||||||
Summary: summary,
|
Summary: summary,
|
||||||
}, nil
|
}, 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) {
|
func (s *repportService) determineSalesStatusAndPaymentDate(transactions []repportRepo.CustomerPaymentTransaction, currentIndex int, previousBalance, currentBalance float64) (string, *time.Time) {
|
||||||
currentSales := transactions[currentIndex]
|
currentSales := transactions[currentIndex]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user