feat[BE]: refine customer payment report structure by removing unused fields and enhancing query logic for better performance

This commit is contained in:
aguhh18
2026-01-14 20:00:44 +07:00
parent 804ff45dbd
commit aeb5433346
4 changed files with 40 additions and 68 deletions
@@ -19,9 +19,7 @@ type CustomerPaymentReportRow struct {
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"`
@@ -35,10 +33,7 @@ type CustomerPaymentReportRow struct {
type CustomerPaymentReportSummary struct {
TotalQty float64 `json:"total_qty"`
TotalWeight float64 `json:"total_weight"`
TotalInitialAmount float64 `json:"total_initial_amount"`
TotalCreditNote float64 `json:"total_credit_note"`
TotalFinalAmount float64 `json:"total_final_amount"`
TotalPPN float64 `json:"total_ppn"`
TotalGrandAmount float64 `json:"total_grand_amount"`
TotalPayment float64 `json:"total_payment"`
TotalAccountsReceivable float64 `json:"total_accounts_receivable"`
@@ -66,9 +61,7 @@ func ToCustomerPaymentReportRow(tx repportRepo.CustomerPaymentTransaction) Custo
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),
@@ -101,11 +94,8 @@ func CalculateCustomerPaymentSummary(rows []CustomerPaymentReportRow, initialBal
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" {
@@ -113,10 +103,7 @@ func CalculateCustomerPaymentSummary(rows []CustomerPaymentReportRow, initialBal
}
}
// Formula: Total AR = Initial Balance - Total Sales + Total Payment
// - Initial balance: positive (customer deposit)
// - Sales: reduces balance (customer debt)
// - Payment: increases balance (customer pays)
// Total AR = Initial Balance - Total Sales + Total Payment
summary.TotalAccountsReceivable = initialBalance - summary.TotalGrandAmount + summary.TotalPayment
return summary
+3 -1
View File
@@ -12,6 +12,7 @@ import (
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
customerRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
chickinRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
purchaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
@@ -35,10 +36,11 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
hppPerKandangRepository := repportRepo.NewHppPerKandangRepository(db)
productionResultRepository := repportRepo.NewProductionResultRepository(db)
customerPaymentRepository := repportRepo.NewCustomerPaymentRepository(db)
customerRepository := customerRepo.NewCustomerRepository(db)
userRepository := rUser.NewUserRepository(db)
approvalSvc := approvalService.NewApprovalService(approvalRepository)
repportService := sRepport.NewRepportService(db, validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, chickinRepository, recordingRepository, approvalSvc, purchaseSupplierRepository, debtSupplierRepository, hppPerKandangRepository, productionResultRepository, customerPaymentRepository)
repportService := sRepport.NewRepportService(db, validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, chickinRepository, recordingRepository, approvalSvc, purchaseSupplierRepository, debtSupplierRepository, hppPerKandangRepository, productionResultRepository, customerPaymentRepository, customerRepository)
userService := sUser.NewUserService(userRepository, validate)
RepportRoutes(router, userService, repportService)
@@ -20,9 +20,7 @@ type CustomerPaymentTransaction struct {
Weight float64 `gorm:"column:weight"`
AverageWeight float64 `gorm:"column:average_weight"`
Price float64 `gorm:"column:price"`
CreditNote float64 `gorm:"column:credit_note"`
FinalPrice float64 `gorm:"column:final_price"`
PPN float64 `gorm:"column:ppn"`
TotalPrice float64 `gorm:"column:total_price"`
PaymentAmount float64 `gorm:"column:payment_amount"`
PickupInfo string `gorm:"column:pickup_info"`
@@ -44,50 +42,41 @@ func NewCustomerPaymentRepository(db *gorm.DB) CustomerPaymentRepository {
}
func (r *customerPaymentRepositoryImpl) GetCustomerPaymentTransactions(ctx context.Context, customerID *uint) ([]CustomerPaymentTransaction, error) {
// Build SALES subquery
salesQuery := r.db.WithContext(ctx).
Table("marketings m").
Table("marketing_delivery_products mdp").
Select(`
'SALES' AS transaction_type,
m.id::BIGINT AS transaction_id,
mdp.id::BIGINT AS transaction_id,
c.id::BIGINT AS customer_id,
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), '') AS vehicle_numbers,
mdp.delivery_date::DATE AS delivery_date,
m.so_number || '-' || TO_CHAR(mdp.delivery_date, 'YYYYMMDD') || '-' || CAST(pw.warehouse_id AS VARCHAR) AS reference,
COALESCE(mdp.vehicle_number, '') 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
THEN (COALESCE(SUM(COALESCE(mdp.total_weight, mp.total_weight, 0)), 0) / COALESCE(SUM(COALESCE(mp.qty, 0)), 0))::NUMERIC(15,3)
ELSE 0::NUMERIC(15,3)
END AS average_weight,
COALESCE(AVG(COALESCE(mdp.unit_price, mp.unit_price, 0)), 0)::NUMERIC(15,3) AS price,
0::NUMERIC(15,3) AS credit_note,
COALESCE(SUM(COALESCE(mdp.total_price, mp.total_price)), 0)::NUMERIC(15,3) AS final_price,
0::NUMERIC(15,3) AS ppn,
COALESCE(SUM(COALESCE(mdp.total_price, mp.total_price)), 0)::NUMERIC(15,3) AS total_price,
COALESCE(mdp.usage_qty, 0)::NUMERIC(15,3) AS qty,
COALESCE(mdp.total_weight, 0)::NUMERIC(15,3) AS weight,
COALESCE(mdp.avg_weight, 0)::NUMERIC(15,3) AS average_weight,
COALESCE(mdp.unit_price, 0)::NUMERIC(15,3) AS price,
COALESCE(mdp.total_price, 0)::NUMERIC(15,3) AS final_price,
COALESCE(mdp.total_price, 0)::NUMERIC(15,3) AS total_price,
0::NUMERIC(15,3) AS payment_amount,
COALESCE(STRING_AGG(DISTINCT w.name, ', ') FILTER (WHERE w.name IS NOT NULL), '') AS pickup_info,
MAX(u.name) AS sales_person
w.name AS pickup_info,
u.name AS sales_person
`).
Joins("INNER JOIN marketing_products mp ON mp.id = mdp.marketing_product_id").
Joins("INNER JOIN marketings m ON m.id = mp.marketing_id").
Joins("INNER JOIN customers c ON c.id = m.customer_id").
Joins("LEFT JOIN marketing_products mp ON mp.marketing_id = m.id").
Joins("LEFT JOIN marketing_delivery_products mdp ON mdp.marketing_product_id = mp.id").
Joins("LEFT JOIN product_warehouses pw ON pw.id = mp.product_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("INNER JOIN product_warehouses pw ON pw.id = mdp.product_warehouse_id").
Joins("INNER JOIN warehouses w ON w.id = pw.warehouse_id").
Joins("INNER JOIN users u ON u.id = m.sales_person_id").
Where("mdp.delivery_date IS NOT NULL").
Where("m.deleted_at IS NULL").
Where("c.deleted_at IS NULL").
Where("mdp.delivery_date IS NOT NULL")
Where("c.deleted_at IS NULL")
if customerID != nil {
salesQuery = salesQuery.Where("c.id = ?", *customerID)
}
salesQuery = salesQuery.Group("m.id, c.id, m.so_date, m.so_number")
// Build PAYMENT subquery
paymentQuery := r.db.WithContext(ctx).
Table("payments p").
Select(`
@@ -102,9 +91,7 @@ func (r *customerPaymentRepositoryImpl) GetCustomerPaymentTransactions(ctx conte
0::NUMERIC(15,3) AS weight,
0::NUMERIC(15,3) AS average_weight,
0::NUMERIC(15,3) AS price,
0::NUMERIC(15,3) AS credit_note,
0::NUMERIC(15,3) AS final_price,
0::NUMERIC(15,3) AS ppn,
0::NUMERIC(15,3) AS total_price,
p.nominal::NUMERIC(15,3) AS payment_amount,
'-' AS pickup_info,
@@ -121,7 +108,6 @@ func (r *customerPaymentRepositoryImpl) GetCustomerPaymentTransactions(ctx conte
paymentQuery = paymentQuery.Where("c.id = ?", *customerID)
}
// Combine with UNION ALL and execute
var results []CustomerPaymentTransaction
err := r.db.WithContext(ctx).
Raw("? UNION ALL ? ORDER BY customer_id, trans_date, transaction_type DESC, transaction_id",
@@ -161,12 +147,13 @@ func (r *customerPaymentRepositoryImpl) GetInitialBalanceByCustomer(ctx context.
}
func (r *customerPaymentRepositoryImpl) GetCustomerIDsWithTransactions(ctx context.Context, limit, offset int) ([]uint, int64, error) {
// Subquery to get all distinct customer IDs with transactions
subQuery := r.db.WithContext(ctx).
Table("(" +
"SELECT DISTINCT c.id as customer_id FROM marketings m " +
"SELECT DISTINCT c.id as customer_id FROM marketing_delivery_products mdp " +
"INNER JOIN marketing_products mp ON mp.id = mdp.marketing_product_id " +
"INNER JOIN marketings m ON m.id = mp.marketing_id " +
"INNER JOIN customers c ON c.id = m.customer_id " +
"WHERE m.deleted_at IS NULL AND c.deleted_at IS NULL " +
"WHERE mdp.delivery_date IS NOT NULL AND m.deleted_at IS NULL AND c.deleted_at IS NULL " +
"UNION " +
"SELECT DISTINCT c.id as customer_id FROM payments p " +
"INNER JOIN customers c ON c.id = p.party_id " +
@@ -174,19 +161,19 @@ func (r *customerPaymentRepositoryImpl) GetCustomerIDsWithTransactions(ctx conte
"AND p.transaction_type = 'PENJUALAN' AND p.deleted_at IS NULL AND c.deleted_at IS NULL" +
") as customer_ids")
// Count total customers
var total int64
if err := subQuery.Count(&total).Error; err != nil {
return nil, 0, err
}
// Get paginated customer IDs
var customerIDs []uint
err := r.db.WithContext(ctx).
Table("("+
"SELECT DISTINCT c.id as customer_id FROM marketings m "+
"SELECT DISTINCT c.id as customer_id FROM marketing_delivery_products mdp "+
"INNER JOIN marketing_products mp ON mp.id = mdp.marketing_product_id "+
"INNER JOIN marketings m ON m.id = mp.marketing_id "+
"INNER JOIN customers c ON c.id = m.customer_id "+
"WHERE m.deleted_at IS NULL AND c.deleted_at IS NULL "+
"WHERE mdp.delivery_date IS NOT NULL AND m.deleted_at IS NULL AND c.deleted_at IS NULL "+
"UNION "+
"SELECT DISTINCT c.id as customer_id FROM payments p "+
"INNER JOIN customers c ON c.id = p.party_id "+
@@ -20,6 +20,7 @@ import (
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto"
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
customerRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
chickinRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
@@ -59,6 +60,7 @@ type repportService struct {
HppPerKandangRepo repportRepo.HppPerKandangRepository
ProductionResultRepo repportRepo.ProductionResultRepository
CustomerPaymentRepo repportRepo.CustomerPaymentRepository
CustomerRepo customerRepo.CustomerRepository
}
type HppCostAggregate struct {
@@ -84,6 +86,7 @@ func NewRepportService(
hppPerKandangRepo repportRepo.HppPerKandangRepository,
productionResultRepo repportRepo.ProductionResultRepository,
customerPaymentRepo repportRepo.CustomerPaymentRepository,
customerRepo customerRepo.CustomerRepository,
) RepportService {
return &repportService{
Log: utils.Log,
@@ -100,6 +103,7 @@ func NewRepportService(
HppPerKandangRepo: hppPerKandangRepo,
ProductionResultRepo: productionResultRepo,
CustomerPaymentRepo: customerPaymentRepo,
CustomerRepo: customerRepo,
}
}
@@ -356,7 +360,6 @@ 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, params)
@@ -370,10 +373,9 @@ func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.C
}
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).
First(&customer).Error; err != nil {
customer, err := s.CustomerRepo.GetByID(ctx, customerID, nil)
if err != nil {
return dto.CustomerPaymentReportItem{}, err
}
@@ -407,7 +409,6 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID
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
@@ -418,7 +419,6 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID
row.AgingDay = &days
}
} else {
// Aging = current_date - trans_date (SO date)
days := int(time.Since(tx.TransDate).Hours() / 24)
if days < 0 {
days = 0
@@ -455,22 +455,18 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID
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)
}
@@ -480,7 +476,7 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID
summary := dto.CalculateCustomerPaymentSummary(rows, initialBalance)
return dto.CustomerPaymentReportItem{
Customer: customerDTO.ToCustomerRelationDTO(customer),
Customer: customerDTO.ToCustomerRelationDTO(*customer),
InitialBalance: initialBalance,
Rows: rows,
Summary: summary,