diff --git a/internal/modules/repports/controllers/repport.controller.go b/internal/modules/repports/controllers/repport.controller.go index d9701305..577c1b1b 100644 --- a/internal/modules/repports/controllers/repport.controller.go +++ b/internal/modules/repports/controllers/repport.controller.go @@ -242,6 +242,60 @@ func (c *RepportController) GetHppPerKandang(ctx *fiber.Ctx) error { return ctx.Status(fiber.StatusOK).JSON(resp) } +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 + } + } + + 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", ""), + } + + // Validate pagination + if customerID == nil && (query.Page < 1 || query.Limit < 1) { + return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") + } + + result, totalResults, err := c.RepportService.GetCustomerPayment(ctx, query) + if err != nil { + return err + } + + // If single customer mode, return without pagination + if customerID != nil { + return ctx.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get customer payment report successfully", + Data: result, + }) + } + + // Multiple customers mode with pagination + return ctx.Status(fiber.StatusOK). + JSON(response.SuccessWithPaginate[dto.CustomerPaymentReportItem]{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get customer payment report successfully", + Meta: response.Meta{ + Page: query.Page, + Limit: query.Limit, + TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), + TotalResults: totalResults, + }, + Data: result, + }) +} + func (c *RepportController) GetProductionResult(ctx *fiber.Ctx) error { idParam := ctx.Params("idProjectFlockKandang") if idParam == "" { @@ -283,36 +337,6 @@ func (c *RepportController) GetProductionResult(ctx *fiber.Ctx) error { }) } -func (c *RepportController) GetCustomerPayment(ctx *fiber.Ctx) error { - page := ctx.QueryInt("page", 1) - limit := ctx.QueryInt("limit", 10) - - if page < 1 || limit < 1 { - return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") - } - - - - - - data := []dto.CustomerPaymentReportItem{} - totalResults := int64(0) - - return ctx.Status(fiber.StatusOK). - JSON(response.SuccessWithPaginate[dto.CustomerPaymentReportItem]{ - Code: fiber.StatusOK, - Status: "success", - Message: "Get customer payment report successfully", - Meta: response.Meta{ - Page: page, - Limit: limit, - TotalPages: int64(math.Ceil(float64(totalResults) / float64(limit))), - TotalResults: totalResults, - }, - Data: data, - }) -} - func parseCommaSeparatedInt64s(raw string) ([]int64, error) { raw = strings.TrimSpace(raw) if raw == "" { diff --git a/internal/modules/repports/dto/repportCustomerPayment.dto.go b/internal/modules/repports/dto/repportCustomerPayment.dto.go index e0938b51..2f200379 100644 --- a/internal/modules/repports/dto/repportCustomerPayment.dto.go +++ b/internal/modules/repports/dto/repportCustomerPayment.dto.go @@ -2,42 +2,33 @@ package dto import ( "time" + + customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto" ) -// CustomerPaymentReportCustomer represents customer information in the report -type CustomerPaymentReportCustomer struct { - ID uint `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - AccountNumber string `json:"account_number"` - Balance float64 `json:"balance"` - Address string `json:"address"` -} - -// CustomerPaymentReportRow represents each transaction row type CustomerPaymentReportRow struct { - ID uint `json:"id"` - DoDate time.Time `json:"do_date"` - RealizationDate time.Time `json:"realization_date"` - AgingDay int `json:"aging_day"` - Reference string `json:"reference"` - VehiclePlate []string `json:"vehicle_plate"` - 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"` - Total float64 `json:"total"` - Payment float64 `json:"payment"` - AccountsReceivable float64 `json:"accounts_receivable"` - Notes string `json:"notes"` - PickupInfo string `json:"pickup_info"` - SalesMarketing string `json:"sales_marketing"` + 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"` } -// CustomerPaymentReportSummary represents summary calculations per customer type CustomerPaymentReportSummary struct { TotalQty float64 `json:"total_qty"` TotalWeight float64 `json:"total_weight"` @@ -50,14 +41,13 @@ type CustomerPaymentReportSummary struct { TotalAccountsReceivable float64 `json:"total_accounts_receivable"` } -// CustomerPaymentReportItem represents data grouped by customer type CustomerPaymentReportItem struct { - Customer CustomerPaymentReportCustomer `json:"customer"` - Rows []CustomerPaymentReportRow `json:"rows"` - Summary CustomerPaymentReportSummary `json:"summary"` + Customer customerDTO.CustomerRelationDTO `json:"customer"` + InitialBalance float64 `json:"initial_balance"` + Rows []CustomerPaymentReportRow `json:"rows"` + Summary CustomerPaymentReportSummary `json:"summary"` } -// CustomerPaymentReportResponse represents the complete response type CustomerPaymentReportResponse struct { Data []CustomerPaymentReportItem `json:"data"` } diff --git a/internal/modules/repports/module.go b/internal/modules/repports/module.go index 61f37d4d..b0432316 100644 --- a/internal/modules/repports/module.go +++ b/internal/modules/repports/module.go @@ -34,10 +34,11 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate * debtSupplierRepository := repportRepo.NewDebtSupplierRepository(db) hppPerKandangRepository := repportRepo.NewHppPerKandangRepository(db) productionResultRepository := repportRepo.NewProductionResultRepository(db) + customerPaymentRepository := repportRepo.NewCustomerPaymentRepository(db) userRepository := rUser.NewUserRepository(db) approvalSvc := approvalService.NewApprovalService(approvalRepository) - repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, chickinRepository, recordingRepository, approvalSvc, purchaseSupplierRepository, debtSupplierRepository, hppPerKandangRepository, productionResultRepository) + repportService := sRepport.NewRepportService(db, validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, chickinRepository, recordingRepository, approvalSvc, purchaseSupplierRepository, debtSupplierRepository, hppPerKandangRepository, productionResultRepository, customerPaymentRepository) userService := sUser.NewUserService(userRepository, validate) RepportRoutes(router, userService, repportService) diff --git a/internal/modules/repports/repositories/customer_payment.repository.go b/internal/modules/repports/repositories/customer_payment.repository.go new file mode 100644 index 00000000..1d4ffd28 --- /dev/null +++ b/internal/modules/repports/repositories/customer_payment.repository.go @@ -0,0 +1,205 @@ +package repositories + +import ( + "context" + "time" + + "gorm.io/gorm" +) + +type CustomerPaymentTransaction struct { + TransactionType string `gorm:"column:transaction_type"` + TransactionID int64 `gorm:"column:transaction_id"` + CustomerID int64 `gorm:"column:customer_id"` + TransDate time.Time `gorm:"column:trans_date"` + DeliveryDate *time.Time `gorm:"column:delivery_date"` + Reference string `gorm:"column:reference"` + VehicleNumbers string `gorm:"column:vehicle_numbers"` + Qty float64 `gorm:"column:qty"` + 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"` + SalesPerson string `gorm:"column:sales_person"` +} + +type CustomerPaymentRepository interface { + GetCustomerPaymentTransactions(ctx context.Context, customerID *uint) ([]CustomerPaymentTransaction, error) + GetInitialBalanceByCustomer(ctx context.Context, customerID uint) (float64, error) + GetCustomerIDsWithTransactions(ctx context.Context, limit, offset int) ([]uint, int64, error) +} + +type customerPaymentRepositoryImpl struct { + db *gorm.DB +} + +func NewCustomerPaymentRepository(db *gorm.DB) CustomerPaymentRepository { + return &customerPaymentRepositoryImpl{db: db} +} + +func (r *customerPaymentRepositoryImpl) GetCustomerPaymentTransactions(ctx context.Context, customerID *uint) ([]CustomerPaymentTransaction, error) { + // Build SALES subquery + salesQuery := r.db.WithContext(ctx). + Table("marketings m"). + Select(` + 'SALES' AS transaction_type, + m.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 AND 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, + 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 + `). + 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"). + Where("m.deleted_at IS 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(` + 'PAYMENT' AS transaction_type, + p.id::BIGINT AS transaction_id, + c.id::BIGINT AS customer_id, + p.payment_date::DATE AS trans_date, + NULL AS delivery_date, + COALESCE(p.reference_number, p.payment_code) AS reference, + '-' AS vehicle_numbers, + 0::NUMERIC(15,3) AS qty, + 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, + '-' AS sales_person + `). + Joins("INNER JOIN customers c ON c.id = p.party_id"). + Where("p.party_type = ?", "CUSTOMER"). + Where("p.direction = ?", "IN"). + Where("p.transaction_type = ?", "PENJUALAN"). + Where("p.deleted_at IS NULL"). + Where("c.deleted_at IS NULL") + + if customerID != nil { + 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", + salesQuery, + paymentQuery, + ). + Scan(&results). + Error + + if err != nil { + return nil, err + } + + return results, nil +} + +func (r *customerPaymentRepositoryImpl) GetInitialBalanceByCustomer(ctx context.Context, customerID uint) (float64, error) { + var result struct { + Nominal float64 + } + + err := r.db.WithContext(ctx). + Table("payments"). + Select("COALESCE(SUM(nominal), 0) as nominal"). + Where("party_type = ?", "CUSTOMER"). + Where("party_id = ?", customerID). + Where("transaction_type = ?", "SALDO_AWAL"). + Where("deleted_at IS NULL"). + Scan(&result). + Error + + if err != nil { + return 0, err + } + + return result.Nominal, nil +} + +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 " + + "INNER JOIN customers c ON c.id = m.customer_id " + + "WHERE 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 " + + "WHERE p.party_type = 'CUSTOMER' AND p.direction = 'IN' " + + "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 "+ + "INNER JOIN customers c ON c.id = m.customer_id "+ + "WHERE 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 "+ + "WHERE p.party_type = 'CUSTOMER' AND p.direction = 'IN' "+ + "AND p.transaction_type = 'PENJUALAN' AND p.deleted_at IS NULL AND c.deleted_at IS NULL"+ + ") as customer_ids"). + Select("customer_id"). + Order("customer_id ASC"). + Limit(limit). + Offset(offset). + Pluck("customer_id", &customerIDs). + Error + + if err != nil { + return nil, 0, err + } + + return customerIDs, total, nil +} diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index a36d4d21..89acca09 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -19,6 +19,7 @@ import ( expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" 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" 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" @@ -40,12 +41,13 @@ type RepportService interface { GetDebtSupplier(ctx *fiber.Ctx, params *validation.DebtSupplierQuery) ([]dto.DebtSupplierDTO, int64, error) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error) GetProductionResult(ctx *fiber.Ctx, params *validation.ProductionResultQuery) ([]dto.ProductionResultDTO, int64, error) - GetCustomerPayment(ctx *fiber.Ctx) (int, error) + GetCustomerPayment(ctx *fiber.Ctx, params *validation.CustomerPaymentQuery) ([]dto.CustomerPaymentReportItem, int64, error) } type repportService struct { Log *logrus.Logger Validate *validator.Validate + DB *gorm.DB ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository MarketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository PurchaseRepo purchaseRepo.PurchaseRepository @@ -56,6 +58,7 @@ type repportService struct { DebtSupplierRepo repportRepo.DebtSupplierRepository HppPerKandangRepo repportRepo.HppPerKandangRepository ProductionResultRepo repportRepo.ProductionResultRepository + CustomerPaymentRepo repportRepo.CustomerPaymentRepository } type HppCostAggregate struct { @@ -68,6 +71,7 @@ type HppCostAggregate struct { } func NewRepportService( + db *gorm.DB, validate *validator.Validate, expenseRealizationRepo expenseRepo.ExpenseRealizationRepository, marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository, @@ -79,10 +83,12 @@ func NewRepportService( debtSupplierRepo repportRepo.DebtSupplierRepository, hppPerKandangRepo repportRepo.HppPerKandangRepository, productionResultRepo repportRepo.ProductionResultRepository, + customerPaymentRepo repportRepo.CustomerPaymentRepository, ) RepportService { return &repportService{ Log: utils.Log, Validate: validate, + DB: db, ExpenseRealizationRepo: expenseRealizationRepo, MarketingDeliveryRepo: marketingDeliveryRepo, PurchaseRepo: purchaseRepo, @@ -93,6 +99,7 @@ func NewRepportService( DebtSupplierRepo: debtSupplierRepo, HppPerKandangRepo: hppPerKandangRepo, ProductionResultRepo: productionResultRepo, + CustomerPaymentRepo: customerPaymentRepo, } } @@ -308,6 +315,163 @@ func (s *repportService) GetProductionResult(ctx *fiber.Ctx, params *validation. return weeklyResults, totalWeeks, nil } +func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.CustomerPaymentQuery) ([]dto.CustomerPaymentReportItem, int64, error) { + if err := s.Validate.Struct(params); err != nil { + return nil, 0, err + } + + // Determine customer IDs to process + var customerIDs []uint + var totalCustomers int64 + + if params.CustomerID != nil { + // Single customer mode + customerIDs = []uint{*params.CustomerID} + totalCustomers = 1 + } else { + // Multiple customers mode with pagination + page := params.Page + limit := params.Limit + if page < 1 { + page = 1 + } + if limit < 1 { + limit = 10 + } + + offset := (page - 1) * limit + + var err error + customerIDs, totalCustomers, err = s.CustomerPaymentRepo.GetCustomerIDsWithTransactions(ctx.Context(), limit, offset) + if err != nil { + return nil, 0, err + } + + if len(customerIDs) == 0 { + return []dto.CustomerPaymentReportItem{}, 0, nil + } + } + + // Process each customer + var result []dto.CustomerPaymentReportItem + for _, customerID := range customerIDs { + item, err := s.processCustomerPayment(ctx.Context(), customerID) + if err != nil { + return nil, 0, err + } + result = append(result, item) + } + + return result, totalCustomers, nil +} + +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). + First(&customer).Error; err != nil { + 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 { + 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, + } + + // Calculate running balance + if tx.TransactionType == "SALES" { + runningBalance -= tx.TotalPrice + // Status will be calculated later (requires looking ahead) + row.Status = "" + row.AgingDay = 0 // Will be calculated later + } else if tx.TransactionType == "PAYMENT" { + runningBalance += tx.PaymentAmount + row.Status = "" + row.AgingDay = 0 + } + + 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, + Type: customer.Type, + AccountNumber: customer.AccountNumber, + Balance: customer.Balance, + } + + return dto.CustomerPaymentReportItem{ + Customer: customerDTO, + 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 + } + } + + // Final AR = initial balance + total sales - total payment + summary.TotalAccountsReceivable = initialBalance + summary.TotalGrandAmount - summary.TotalPayment + + return summary +} + func mapRecordingToProductionResultDTO(record entity.Recording) dto.ProductionResultDTO { result := dto.ProductionResultDTO{ CreatedAt: record.CreatedAt, @@ -1374,11 +1538,6 @@ func (s *repportService) parseHppPerKandangQuery(ctx *fiber.Ctx) (*validation.Hp return params, filters, nil } -func (s *repportService) GetCustomerPayment(c *fiber.Ctx) (int, error) { - - return 0, nil -} - func parseCommaSeparatedInt64s(raw string) ([]int64, error) { raw = strings.TrimSpace(raw) if raw == "" { diff --git a/internal/modules/repports/validations/repport.validation.go b/internal/modules/repports/validations/repport.validation.go index 5dde8f51..c79dd90d 100644 --- a/internal/modules/repports/validations/repport.validation.go +++ b/internal/modules/repports/validations/repport.validation.go @@ -71,3 +71,11 @@ type ProductionResultQuery struct { Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"` ProjectFlockKandangID uint `query:"-" validate:"required,gt=0"` } + +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"` +}