From 804ff45dbd1bc6e2ccf4c324853834efc702c31c Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Wed, 14 Jan 2026 15:15:29 +0700 Subject: [PATCH] feat[BE]: enhance customer payment report with vehicle numbers and pickup info, add date filtering --- .../master/customers/dto/customer.dto.go | 1 + .../dto/repportCustomerPayment.dto.go | 110 ++++++++++++++---- .../customer_payment.repository.go | 6 +- .../repports/services/repport.service.go | 102 ++++++++-------- 4 files changed, 141 insertions(+), 78 deletions(-) diff --git a/internal/modules/master/customers/dto/customer.dto.go b/internal/modules/master/customers/dto/customer.dto.go index 444c6768..592f14cd 100644 --- a/internal/modules/master/customers/dto/customer.dto.go +++ b/internal/modules/master/customers/dto/customer.dto.go @@ -52,6 +52,7 @@ func ToCustomerRelationDTO(e entity.Customer) CustomerRelationDTO { Name: e.Name, Type: e.Type, AccountNumber: e.AccountNumber, + Balance: e.Balance, Pic: pic, } } diff --git a/internal/modules/repports/dto/repportCustomerPayment.dto.go b/internal/modules/repports/dto/repportCustomerPayment.dto.go index 439eed42..3f6b7a2d 100644 --- a/internal/modules/repports/dto/repportCustomerPayment.dto.go +++ b/internal/modules/repports/dto/repportCustomerPayment.dto.go @@ -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 +} diff --git a/internal/modules/repports/repositories/customer_payment.repository.go b/internal/modules/repports/repositories/customer_payment.repository.go index 49e9424c..5a39b127 100644 --- a/internal/modules/repports/repositories/customer_payment.repository.go +++ b/internal/modules/repports/repositories/customer_payment.repository.go @@ -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) diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index c6f18002..1dba2114 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -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]