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 }