From 378d633ea49967af3f447ffb1e61e001b8c82fc8 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 19 Jan 2026 09:27:37 +0700 Subject: [PATCH] feat[BE]: Enhance payment allocation logic to support FIFO consumption for sales transactions --- .../repports/services/repport.service.go | 78 ++++++++++++++++--- 1 file changed, 67 insertions(+), 11 deletions(-) diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index 2e07e212..48f9e205 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -464,9 +464,13 @@ func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.C if err != nil { return nil, 0, err } - result = append(result, item) + + if len(item.Rows) > 0 { + result = append(result, item) + } } + totalCustomers = int64(len(result)) return result, totalCustomers, nil } @@ -503,14 +507,8 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID row.Status = status if status == "LUNAS" { - if previousBalance >= tx.TotalPrice { - days := 0 - row.AgingDay = &days - } else if paymentDate != nil { + if paymentDate != nil { days := int(paymentDate.Sub(tx.TransDate).Hours() / 24) - if days < 0 { - days = 0 - } row.AgingDay = &days } else { days := 0 @@ -518,9 +516,6 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID } } else { days := int(time.Since(tx.TransDate).Hours() / 24) - if days < 0 { - days = 0 - } row.AgingDay = &days } } else if tx.TransactionType == "PAYMENT" { @@ -586,6 +581,67 @@ func (s *repportService) determineSalesStatusAndPaymentDate(transactions []reppo // 4. BELUM LUNAS: no payment at all if previousBalance >= currentSales.TotalPrice { + // Cari payment yang digunakan untuk melunasi sales ini dengan FIFO + // Track payment allocations that are consumed by previous sales + type paymentAllocation struct { + date time.Time + amount float64 + consumed float64 + } + allocations := []paymentAllocation{} + runningBalance := 0.0 + + // Process all transactions before current sales to build allocation map + for i := 0; i < currentIndex; i++ { + if transactions[i].TransactionType == "PAYMENT" { + allocations = append(allocations, paymentAllocation{ + date: transactions[i].TransDate, + amount: transactions[i].PaymentAmount, + consumed: 0, + }) + runningBalance += transactions[i].PaymentAmount + } else if transactions[i].TransactionType == "SALES" { + salesAmount := transactions[i].TotalPrice + remainingToConsume := salesAmount + + // Consume from oldest allocations first (FIFO) + for j := range allocations { + if remainingToConsume <= 0 { + break + } + available := allocations[j].amount - allocations[j].consumed + if available > 0 { + consume := available + if consume > remainingToConsume { + consume = remainingToConsume + } + allocations[j].consumed += consume + remainingToConsume -= consume + } + } + runningBalance -= salesAmount + } + } + + // Now find which allocation covers the current sales + amountNeeded := currentSales.TotalPrice + for _, alloc := range allocations { + available := alloc.amount - alloc.consumed + if available > 0 { + if amountNeeded <= available { + // This allocation fully covers the sales + return "LUNAS", &alloc.date + } else { + // This allocation partially covers, continue to next + amountNeeded -= available + } + } + } + + // If we get here, use the oldest allocation + if len(allocations) > 0 { + return "LUNAS", &allocations[0].date + } return "LUNAS", nil }