diff --git a/internal/modules/repports/controllers/repport.controller.go b/internal/modules/repports/controllers/repport.controller.go index 577c1b1b..f83f0902 100644 --- a/internal/modules/repports/controllers/repport.controller.go +++ b/internal/modules/repports/controllers/repport.controller.go @@ -243,25 +243,30 @@ func (c *RepportController) GetHppPerKandang(ctx *fiber.Ctx) error { } func (c *RepportController) GetCustomerPayment(ctx *fiber.Ctx) error { - var customerID *uint - if customerIDStr := ctx.Query("customer_id"); customerIDStr != "" { - if id, err := strconv.ParseUint(customerIDStr, 10, 32); err == nil { - cid := uint(id) - customerID = &cid + var customerIDs []uint + if customerIDsStr := ctx.Query("customer_ids"); customerIDsStr != "" { + ids := strings.Split(customerIDsStr, ",") + for _, idStr := range ids { + idStr = strings.TrimSpace(idStr) + if idStr != "" { + if id, err := strconv.ParseUint(idStr, 10, 32); err == nil { + customerIDs = append(customerIDs, uint(id)) + } + } } } query := &validation.CustomerPaymentQuery{ - Page: ctx.QueryInt("page", 1), - Limit: ctx.QueryInt("limit", 10), - CustomerID: customerID, - StartDate: ctx.Query("start_date", ""), - EndDate: ctx.Query("end_date", ""), + Page: ctx.QueryInt("page", 1), + Limit: ctx.QueryInt("limit", 10), + CustomerIDs: customerIDs, + StartDate: ctx.Query("start_date", ""), + EndDate: ctx.Query("end_date", ""), } // Validate pagination - if customerID == nil && (query.Page < 1 || query.Limit < 1) { - return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") + if len(customerIDs) == 0 && (query.Page < 1 || query.Limit < 1) { + 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) @@ -269,8 +274,8 @@ func (c *RepportController) GetCustomerPayment(ctx *fiber.Ctx) error { return err } - // If single customer mode, return without pagination - if customerID != nil { + // If single customer mode (only 1 customer ID), return without pagination + if len(customerIDs) == 1 { return ctx.Status(fiber.StatusOK). JSON(response.Success{ Code: fiber.StatusOK, diff --git a/internal/modules/repports/dto/repportCustomerPayment.dto.go b/internal/modules/repports/dto/repportCustomerPayment.dto.go index 2f200379..439eed42 100644 --- a/internal/modules/repports/dto/repportCustomerPayment.dto.go +++ b/internal/modules/repports/dto/repportCustomerPayment.dto.go @@ -23,7 +23,7 @@ type CustomerPaymentReportRow struct { TotalPrice float64 `json:"total_price"` PaymentAmount float64 `json:"payment_amount"` AccountsReceivable float64 `json:"accounts_receivable"` - AgingDay int `json:"aging_day"` + AgingDay *int `json:"aging_day"` Status string `json:"status"` PickupInfo string `json:"pickup_info"` SalesPerson string `json:"sales_person"` diff --git a/internal/modules/repports/repositories/customer_payment.repository.go b/internal/modules/repports/repositories/customer_payment.repository.go index 1d4ffd28..49e9424c 100644 --- a/internal/modules/repports/repositories/customer_payment.repository.go +++ b/internal/modules/repports/repositories/customer_payment.repository.go @@ -2,6 +2,7 @@ package repositories import ( "context" + "time" "gorm.io/gorm" diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index 89acca09..c6f18002 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -324,10 +324,14 @@ func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.C var customerIDs []uint var totalCustomers int64 - if params.CustomerID != nil { - // Single customer mode - customerIDs = []uint{*params.CustomerID} - totalCustomers = 1 + if len(params.CustomerIDs) > 0 { + // Specific customer IDs mode (no pagination) + customerIDs = params.CustomerIDs + totalCustomers = int64(len(customerIDs)) + + if len(customerIDs) == 0 { + return []dto.CustomerPaymentReportItem{}, 0, nil + } } else { // Multiple customers mode with pagination 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) { - // Get customer info customer := entity.Customer{} if err := s.DB.WithContext(ctx). Where("id = ?", customerID). @@ -374,24 +377,21 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID return dto.CustomerPaymentReportItem{}, err } - // Get initial balance initialBalance, err := s.CustomerPaymentRepo.GetInitialBalanceByCustomer(ctx, customerID) if err != nil { return dto.CustomerPaymentReportItem{}, err } - // Get transactions for this customer cid := customerID transactions, err := s.CustomerPaymentRepo.GetCustomerPaymentTransactions(ctx, &cid) if err != nil { return dto.CustomerPaymentReportItem{}, err } - // Process transactions and calculate running balance rows := make([]dto.CustomerPaymentReportRow, 0, len(transactions)) runningBalance := initialBalance - for _, tx := range transactions { + for i, tx := range transactions { row := dto.CustomerPaymentReportRow{ TransactionType: tx.TransactionType, TransactionID: tx.TransactionID, @@ -412,26 +412,48 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID SalesPerson: tx.SalesPerson, } - // Calculate running balance + previousBalance := runningBalance + if tx.TransactionType == "SALES" { runningBalance -= tx.TotalPrice - // Status will be calculated later (requires looking ahead) - row.Status = "" - row.AgingDay = 0 // Will be calculated later + status, paymentDate := s.determineSalesStatusAndPaymentDate(transactions, i, previousBalance, runningBalance) + row.Status = status + + 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" { runningBalance += tx.PaymentAmount row.Status = "" - row.AgingDay = 0 + row.AgingDay = nil } row.AccountsReceivable = runningBalance rows = append(rows, row) } - // Calculate summary summary := s.calculateSummary(rows, initialBalance) - // Build customer DTO customerDTO := customerDTO.CustomerRelationDTO{ Id: customer.Id, Name: customer.Name, @@ -466,12 +488,55 @@ func (s *repportService) calculateSummary(rows []dto.CustomerPaymentReportRow, i } } - // Final AR = initial balance + total sales - total payment - summary.TotalAccountsReceivable = initialBalance + summary.TotalGrandAmount - summary.TotalPayment + // 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] + + // 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 { result := dto.ProductionResultDTO{ CreatedAt: record.CreatedAt, diff --git a/internal/modules/repports/validations/repport.validation.go b/internal/modules/repports/validations/repport.validation.go index c79dd90d..68bfee90 100644 --- a/internal/modules/repports/validations/repport.validation.go +++ b/internal/modules/repports/validations/repport.validation.go @@ -73,9 +73,9 @@ type ProductionResultQuery struct { } type CustomerPaymentQuery struct { - Page int `query:"page" validate:"omitempty,min=1,gt=0"` - Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"` - CustomerID *uint `query:"customer_id" validate:"omitempty,gt=0"` - StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"` - EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"` + Page int `query:"page" validate:"omitempty,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"` + CustomerIDs []uint `query:"customer_ids" validate:"omitempty,dive,gt=0"` + StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"` + EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"` }