mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
feat[BE]: update customer payment report to support multiple customer IDs and nullable aging days
This commit is contained in:
@@ -243,25 +243,30 @@ func (c *RepportController) GetHppPerKandang(ctx *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *RepportController) GetCustomerPayment(ctx *fiber.Ctx) error {
|
func (c *RepportController) GetCustomerPayment(ctx *fiber.Ctx) error {
|
||||||
var customerID *uint
|
var customerIDs []uint
|
||||||
if customerIDStr := ctx.Query("customer_id"); customerIDStr != "" {
|
if customerIDsStr := ctx.Query("customer_ids"); customerIDsStr != "" {
|
||||||
if id, err := strconv.ParseUint(customerIDStr, 10, 32); err == nil {
|
ids := strings.Split(customerIDsStr, ",")
|
||||||
cid := uint(id)
|
for _, idStr := range ids {
|
||||||
customerID = &cid
|
idStr = strings.TrimSpace(idStr)
|
||||||
|
if idStr != "" {
|
||||||
|
if id, err := strconv.ParseUint(idStr, 10, 32); err == nil {
|
||||||
|
customerIDs = append(customerIDs, uint(id))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
query := &validation.CustomerPaymentQuery{
|
query := &validation.CustomerPaymentQuery{
|
||||||
Page: ctx.QueryInt("page", 1),
|
Page: ctx.QueryInt("page", 1),
|
||||||
Limit: ctx.QueryInt("limit", 10),
|
Limit: ctx.QueryInt("limit", 10),
|
||||||
CustomerID: customerID,
|
CustomerIDs: customerIDs,
|
||||||
StartDate: ctx.Query("start_date", ""),
|
StartDate: ctx.Query("start_date", ""),
|
||||||
EndDate: ctx.Query("end_date", ""),
|
EndDate: ctx.Query("end_date", ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate pagination
|
// Validate pagination
|
||||||
if customerID == nil && (query.Page < 1 || query.Limit < 1) {
|
if len(customerIDs) == 0 && (query.Page < 1 || query.Limit < 1) {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0 when customer_ids is not provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
result, totalResults, err := c.RepportService.GetCustomerPayment(ctx, query)
|
result, totalResults, err := c.RepportService.GetCustomerPayment(ctx, query)
|
||||||
@@ -269,8 +274,8 @@ func (c *RepportController) GetCustomerPayment(ctx *fiber.Ctx) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If single customer mode, return without pagination
|
// If single customer mode (only 1 customer ID), return without pagination
|
||||||
if customerID != nil {
|
if len(customerIDs) == 1 {
|
||||||
return ctx.Status(fiber.StatusOK).
|
return ctx.Status(fiber.StatusOK).
|
||||||
JSON(response.Success{
|
JSON(response.Success{
|
||||||
Code: fiber.StatusOK,
|
Code: fiber.StatusOK,
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ type CustomerPaymentReportRow struct {
|
|||||||
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"`
|
PickupInfo string `json:"pickup_info"`
|
||||||
SalesPerson string `json:"sales_person"`
|
SalesPerson string `json:"sales_person"`
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package repositories
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|||||||
@@ -324,10 +324,14 @@ func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.C
|
|||||||
var customerIDs []uint
|
var customerIDs []uint
|
||||||
var totalCustomers int64
|
var totalCustomers int64
|
||||||
|
|
||||||
if params.CustomerID != nil {
|
if len(params.CustomerIDs) > 0 {
|
||||||
// Single customer mode
|
// Specific customer IDs mode (no pagination)
|
||||||
customerIDs = []uint{*params.CustomerID}
|
customerIDs = params.CustomerIDs
|
||||||
totalCustomers = 1
|
totalCustomers = int64(len(customerIDs))
|
||||||
|
|
||||||
|
if len(customerIDs) == 0 {
|
||||||
|
return []dto.CustomerPaymentReportItem{}, 0, nil
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Multiple customers mode with pagination
|
// Multiple customers mode with pagination
|
||||||
page := params.Page
|
page := params.Page
|
||||||
@@ -366,7 +370,6 @@ func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.C
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *repportService) processCustomerPayment(ctx context.Context, customerID uint) (dto.CustomerPaymentReportItem, error) {
|
func (s *repportService) processCustomerPayment(ctx context.Context, customerID uint) (dto.CustomerPaymentReportItem, error) {
|
||||||
// Get customer info
|
|
||||||
customer := entity.Customer{}
|
customer := entity.Customer{}
|
||||||
if err := s.DB.WithContext(ctx).
|
if err := s.DB.WithContext(ctx).
|
||||||
Where("id = ?", customerID).
|
Where("id = ?", customerID).
|
||||||
@@ -374,24 +377,21 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID
|
|||||||
return dto.CustomerPaymentReportItem{}, err
|
return dto.CustomerPaymentReportItem{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get initial balance
|
|
||||||
initialBalance, err := s.CustomerPaymentRepo.GetInitialBalanceByCustomer(ctx, customerID)
|
initialBalance, err := s.CustomerPaymentRepo.GetInitialBalanceByCustomer(ctx, customerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return dto.CustomerPaymentReportItem{}, err
|
return dto.CustomerPaymentReportItem{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get transactions for this customer
|
|
||||||
cid := customerID
|
cid := customerID
|
||||||
transactions, err := s.CustomerPaymentRepo.GetCustomerPaymentTransactions(ctx, &cid)
|
transactions, err := s.CustomerPaymentRepo.GetCustomerPaymentTransactions(ctx, &cid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return dto.CustomerPaymentReportItem{}, err
|
return dto.CustomerPaymentReportItem{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process transactions and calculate running balance
|
|
||||||
rows := make([]dto.CustomerPaymentReportRow, 0, len(transactions))
|
rows := make([]dto.CustomerPaymentReportRow, 0, len(transactions))
|
||||||
runningBalance := initialBalance
|
runningBalance := initialBalance
|
||||||
|
|
||||||
for _, tx := range transactions {
|
for i, tx := range transactions {
|
||||||
row := dto.CustomerPaymentReportRow{
|
row := dto.CustomerPaymentReportRow{
|
||||||
TransactionType: tx.TransactionType,
|
TransactionType: tx.TransactionType,
|
||||||
TransactionID: tx.TransactionID,
|
TransactionID: tx.TransactionID,
|
||||||
@@ -412,26 +412,48 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID
|
|||||||
SalesPerson: tx.SalesPerson,
|
SalesPerson: tx.SalesPerson,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate running balance
|
previousBalance := runningBalance
|
||||||
|
|
||||||
if tx.TransactionType == "SALES" {
|
if tx.TransactionType == "SALES" {
|
||||||
runningBalance -= tx.TotalPrice
|
runningBalance -= tx.TotalPrice
|
||||||
// Status will be calculated later (requires looking ahead)
|
status, paymentDate := s.determineSalesStatusAndPaymentDate(transactions, i, previousBalance, runningBalance)
|
||||||
row.Status = ""
|
row.Status = status
|
||||||
row.AgingDay = 0 // Will be calculated later
|
|
||||||
|
if status == "LUNAS" {
|
||||||
|
if previousBalance >= tx.TotalPrice {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
row.AgingDay = &days
|
||||||
|
} else {
|
||||||
|
days := 0
|
||||||
|
row.AgingDay = &days
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Aging = current_date - trans_date (SO date)
|
||||||
|
days := int(time.Since(tx.TransDate).Hours() / 24)
|
||||||
|
if days < 0 {
|
||||||
|
days = 0
|
||||||
|
}
|
||||||
|
row.AgingDay = &days
|
||||||
|
}
|
||||||
} else if tx.TransactionType == "PAYMENT" {
|
} else if tx.TransactionType == "PAYMENT" {
|
||||||
runningBalance += tx.PaymentAmount
|
runningBalance += tx.PaymentAmount
|
||||||
row.Status = ""
|
row.Status = ""
|
||||||
row.AgingDay = 0
|
row.AgingDay = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
row.AccountsReceivable = runningBalance
|
row.AccountsReceivable = runningBalance
|
||||||
rows = append(rows, row)
|
rows = append(rows, row)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate summary
|
|
||||||
summary := s.calculateSummary(rows, initialBalance)
|
summary := s.calculateSummary(rows, initialBalance)
|
||||||
|
|
||||||
// Build customer DTO
|
|
||||||
customerDTO := customerDTO.CustomerRelationDTO{
|
customerDTO := customerDTO.CustomerRelationDTO{
|
||||||
Id: customer.Id,
|
Id: customer.Id,
|
||||||
Name: customer.Name,
|
Name: customer.Name,
|
||||||
@@ -466,12 +488,55 @@ func (s *repportService) calculateSummary(rows []dto.CustomerPaymentReportRow, i
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Final AR = initial balance + total sales - total payment
|
// Formula: Total AR = Initial Balance - Total Sales + Total Payment
|
||||||
summary.TotalAccountsReceivable = initialBalance + summary.TotalGrandAmount - summary.TotalPayment
|
// - Initial balance: positive (customer deposit)
|
||||||
|
// - Sales: reduces balance (customer debt)
|
||||||
|
// - Payment: increases balance (customer pays)
|
||||||
|
summary.TotalAccountsReceivable = initialBalance - summary.TotalGrandAmount + summary.TotalPayment
|
||||||
|
|
||||||
return summary
|
return summary
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *repportService) determineSalesStatusAndPaymentDate(transactions []repportRepo.CustomerPaymentTransaction, currentIndex int, previousBalance, currentBalance float64) (string, *time.Time) {
|
||||||
|
currentSales := transactions[currentIndex]
|
||||||
|
|
||||||
|
// Status Logic:
|
||||||
|
// 1. LUNAS: previousBalance >= salesAmount (paid from deposit)
|
||||||
|
// 2. LUNAS: future payments make AR >= 0 (eventually paid)
|
||||||
|
// 3. DIBAYAR SEBAGIAN: has payment but not enough
|
||||||
|
// 4. BELUM LUNAS: no payment at all
|
||||||
|
|
||||||
|
if previousBalance >= currentSales.TotalPrice {
|
||||||
|
return "LUNAS", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
hasPartialPaymentFromBalance := previousBalance > 0 && previousBalance < currentSales.TotalPrice
|
||||||
|
|
||||||
|
futureBalance := currentBalance
|
||||||
|
hasPayment := false
|
||||||
|
var paymentDateThatMadeItLunas *time.Time
|
||||||
|
|
||||||
|
for i := currentIndex + 1; i < len(transactions); i++ {
|
||||||
|
if transactions[i].TransactionType == "PAYMENT" {
|
||||||
|
futureBalance += transactions[i].PaymentAmount
|
||||||
|
hasPayment = true
|
||||||
|
|
||||||
|
if futureBalance >= 0 {
|
||||||
|
paymentDateThatMadeItLunas = &transactions[i].TransDate
|
||||||
|
return "LUNAS", paymentDateThatMadeItLunas
|
||||||
|
}
|
||||||
|
} else if transactions[i].TransactionType == "SALES" {
|
||||||
|
futureBalance -= transactions[i].TotalPrice
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasPayment || hasPartialPaymentFromBalance {
|
||||||
|
return "DIBAYAR SEBAGIAN", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "BELUM LUNAS", nil
|
||||||
|
}
|
||||||
|
|
||||||
func mapRecordingToProductionResultDTO(record entity.Recording) dto.ProductionResultDTO {
|
func mapRecordingToProductionResultDTO(record entity.Recording) dto.ProductionResultDTO {
|
||||||
result := dto.ProductionResultDTO{
|
result := dto.ProductionResultDTO{
|
||||||
CreatedAt: record.CreatedAt,
|
CreatedAt: record.CreatedAt,
|
||||||
|
|||||||
@@ -73,9 +73,9 @@ type ProductionResultQuery struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CustomerPaymentQuery struct {
|
type CustomerPaymentQuery struct {
|
||||||
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
|
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
|
||||||
Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"`
|
Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"`
|
||||||
CustomerID *uint `query:"customer_id" validate:"omitempty,gt=0"`
|
CustomerIDs []uint `query:"customer_ids" validate:"omitempty,dive,gt=0"`
|
||||||
StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
|
StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
|
||||||
EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"`
|
EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"`
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user