diff --git a/internal/modules/repports/dto/repportCustomerPayment.dto.go b/internal/modules/repports/dto/repportCustomerPayment.dto.go index 3f6b7a2d..cdac5029 100644 --- a/internal/modules/repports/dto/repportCustomerPayment.dto.go +++ b/internal/modules/repports/dto/repportCustomerPayment.dto.go @@ -19,9 +19,7 @@ type CustomerPaymentReportRow struct { 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"` @@ -35,10 +33,7 @@ type CustomerPaymentReportRow struct { type CustomerPaymentReportSummary struct { TotalQty float64 `json:"total_qty"` TotalWeight float64 `json:"total_weight"` - TotalInitialAmount float64 `json:"total_initial_amount"` - TotalCreditNote float64 `json:"total_credit_note"` TotalFinalAmount float64 `json:"total_final_amount"` - TotalPPN float64 `json:"total_ppn"` TotalGrandAmount float64 `json:"total_grand_amount"` TotalPayment float64 `json:"total_payment"` TotalAccountsReceivable float64 `json:"total_accounts_receivable"` @@ -66,9 +61,7 @@ func ToCustomerPaymentReportRow(tx repportRepo.CustomerPaymentTransaction) Custo 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), @@ -101,11 +94,8 @@ func CalculateCustomerPaymentSummary(rows []CustomerPaymentReportRow, initialBal 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" { @@ -113,10 +103,7 @@ func CalculateCustomerPaymentSummary(rows []CustomerPaymentReportRow, initialBal } } - // Formula: Total AR = Initial Balance - Total Sales + Total Payment - // - Initial balance: positive (customer deposit) - // - Sales: reduces balance (customer debt) - // - Payment: increases balance (customer pays) + // Total AR = Initial Balance - Total Sales + Total Payment summary.TotalAccountsReceivable = initialBalance - summary.TotalGrandAmount + summary.TotalPayment return summary diff --git a/internal/modules/repports/module.go b/internal/modules/repports/module.go index b0432316..d081306c 100644 --- a/internal/modules/repports/module.go +++ b/internal/modules/repports/module.go @@ -12,6 +12,7 @@ import ( expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" + customerRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories" chickinRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" purchaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories" @@ -35,10 +36,11 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate * hppPerKandangRepository := repportRepo.NewHppPerKandangRepository(db) productionResultRepository := repportRepo.NewProductionResultRepository(db) customerPaymentRepository := repportRepo.NewCustomerPaymentRepository(db) + customerRepository := customerRepo.NewCustomerRepository(db) userRepository := rUser.NewUserRepository(db) approvalSvc := approvalService.NewApprovalService(approvalRepository) - repportService := sRepport.NewRepportService(db, validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, chickinRepository, recordingRepository, approvalSvc, purchaseSupplierRepository, debtSupplierRepository, hppPerKandangRepository, productionResultRepository, customerPaymentRepository) + repportService := sRepport.NewRepportService(db, validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, chickinRepository, recordingRepository, approvalSvc, purchaseSupplierRepository, debtSupplierRepository, hppPerKandangRepository, productionResultRepository, customerPaymentRepository, customerRepository) 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 index 5a39b127..8a5747aa 100644 --- a/internal/modules/repports/repositories/customer_payment.repository.go +++ b/internal/modules/repports/repositories/customer_payment.repository.go @@ -20,9 +20,7 @@ type CustomerPaymentTransaction struct { 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"` @@ -44,50 +42,41 @@ func NewCustomerPaymentRepository(db *gorm.DB) CustomerPaymentRepository { } func (r *customerPaymentRepositoryImpl) GetCustomerPaymentTransactions(ctx context.Context, customerID *uint) ([]CustomerPaymentTransaction, error) { - // Build SALES subquery salesQuery := r.db.WithContext(ctx). - Table("marketings m"). + Table("marketing_delivery_products mdp"). Select(` 'SALES' AS transaction_type, - m.id::BIGINT AS transaction_id, + mdp.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), '') AS vehicle_numbers, + mdp.delivery_date::DATE AS delivery_date, + m.so_number || '-' || TO_CHAR(mdp.delivery_date, 'YYYYMMDD') || '-' || CAST(pw.warehouse_id AS VARCHAR) AS reference, + COALESCE(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, + COALESCE(mdp.usage_qty, 0)::NUMERIC(15,3) AS qty, + COALESCE(mdp.total_weight, 0)::NUMERIC(15,3) AS weight, + COALESCE(mdp.avg_weight, 0)::NUMERIC(15,3) AS average_weight, + COALESCE(mdp.unit_price, 0)::NUMERIC(15,3) AS price, + COALESCE(mdp.total_price, 0)::NUMERIC(15,3) AS final_price, + COALESCE(mdp.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 + w.name AS pickup_info, + u.name AS sales_person `). + Joins("INNER JOIN marketing_products mp ON mp.id = mdp.marketing_product_id"). + Joins("INNER JOIN marketings m ON m.id = mp.marketing_id"). 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"). + Joins("INNER JOIN product_warehouses pw ON pw.id = mdp.product_warehouse_id"). + Joins("INNER JOIN warehouses w ON w.id = pw.warehouse_id"). + Joins("INNER JOIN users u ON u.id = m.sales_person_id"). + Where("mdp.delivery_date IS NOT NULL"). Where("m.deleted_at IS NULL"). - Where("c.deleted_at IS NULL"). - Where("mdp.delivery_date IS NOT 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(` @@ -102,9 +91,7 @@ func (r *customerPaymentRepositoryImpl) GetCustomerPaymentTransactions(ctx conte 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, @@ -121,7 +108,6 @@ func (r *customerPaymentRepositoryImpl) GetCustomerPaymentTransactions(ctx conte 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", @@ -161,12 +147,13 @@ func (r *customerPaymentRepositoryImpl) GetInitialBalanceByCustomer(ctx context. } 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 " + + "SELECT DISTINCT c.id as customer_id FROM marketing_delivery_products mdp " + + "INNER JOIN marketing_products mp ON mp.id = mdp.marketing_product_id " + + "INNER JOIN marketings m ON m.id = mp.marketing_id " + "INNER JOIN customers c ON c.id = m.customer_id " + - "WHERE m.deleted_at IS NULL AND c.deleted_at IS NULL " + + "WHERE mdp.delivery_date IS NOT NULL AND 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 " + @@ -174,19 +161,19 @@ func (r *customerPaymentRepositoryImpl) GetCustomerIDsWithTransactions(ctx conte "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 "+ + "SELECT DISTINCT c.id as customer_id FROM marketing_delivery_products mdp "+ + "INNER JOIN marketing_products mp ON mp.id = mdp.marketing_product_id "+ + "INNER JOIN marketings m ON m.id = mp.marketing_id "+ "INNER JOIN customers c ON c.id = m.customer_id "+ - "WHERE m.deleted_at IS NULL AND c.deleted_at IS NULL "+ + "WHERE mdp.delivery_date IS NOT NULL AND 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 "+ diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index 1dba2114..2c102418 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -20,6 +20,7 @@ import ( 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" + customerRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories" 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" @@ -59,6 +60,7 @@ type repportService struct { HppPerKandangRepo repportRepo.HppPerKandangRepository ProductionResultRepo repportRepo.ProductionResultRepository CustomerPaymentRepo repportRepo.CustomerPaymentRepository + CustomerRepo customerRepo.CustomerRepository } type HppCostAggregate struct { @@ -84,6 +86,7 @@ func NewRepportService( hppPerKandangRepo repportRepo.HppPerKandangRepository, productionResultRepo repportRepo.ProductionResultRepository, customerPaymentRepo repportRepo.CustomerPaymentRepository, + customerRepo customerRepo.CustomerRepository, ) RepportService { return &repportService{ Log: utils.Log, @@ -100,6 +103,7 @@ func NewRepportService( HppPerKandangRepo: hppPerKandangRepo, ProductionResultRepo: productionResultRepo, CustomerPaymentRepo: customerPaymentRepo, + CustomerRepo: customerRepo, } } @@ -356,7 +360,6 @@ 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, params) @@ -370,10 +373,9 @@ func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.C } 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). - First(&customer).Error; err != nil { + + customer, err := s.CustomerRepo.GetByID(ctx, customerID, nil) + if err != nil { return dto.CustomerPaymentReportItem{}, err } @@ -407,7 +409,6 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID 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 @@ -418,7 +419,6 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID row.AgingDay = &days } } else { - // Aging = current_date - trans_date (SO date) days := int(time.Since(tx.TransDate).Hours() / 24) if days < 0 { days = 0 @@ -455,22 +455,18 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID 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) } @@ -480,7 +476,7 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID summary := dto.CalculateCustomerPaymentSummary(rows, initialBalance) return dto.CustomerPaymentReportItem{ - Customer: customerDTO.ToCustomerRelationDTO(customer), + Customer: customerDTO.ToCustomerRelationDTO(*customer), InitialBalance: initialBalance, Rows: rows, Summary: summary,