Yimaru-BackEnd/internal/repository/payments.go
Yared Yemane fbad083ca4 Add admin payments list API with filters and fix /admin route conflict.
Expose GET /api/v1/admin/payments for filtered gateway transaction listing, constrain /admin/:id to integers so /admin/payments is not mistaken for an admin id, and grant payments.list_all to ADMIN.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 05:50:46 -07:00

313 lines
9.7 KiB
Go

package repository
import (
dbgen "Yimaru-Backend/gen/db"
"Yimaru-Backend/internal/domain"
"context"
"github.com/jackc/pgx/v5/pgtype"
)
func (s *Store) CreatePayment(ctx context.Context, input domain.CreatePaymentInput) (*domain.Payment, error) {
payment, err := s.queries.CreatePayment(ctx, dbgen.CreatePaymentParams{
UserID: input.UserID,
PlanID: int64PtrToPgInt8(input.PlanID),
SubscriptionID: pgtype.Int8{Valid: false},
SessionID: pgtype.Text{Valid: false},
TransactionID: pgtype.Text{Valid: false},
Nonce: input.Nonce,
Amount: toPgNumeric(input.Amount),
Currency: input.Currency,
PaymentMethod: toPgText(input.PaymentMethod),
Column10: string(domain.PaymentStatusPending), // status with COALESCE
PaymentUrl: pgtype.Text{Valid: false},
ExpiresAt: toPgTimestamptzPtr(input.ExpiresAt),
})
if err != nil {
return nil, err
}
return paymentToDomain(payment), nil
}
func (s *Store) GetPaymentByID(ctx context.Context, id int64) (*domain.Payment, error) {
payment, err := s.queries.GetPaymentByID(ctx, id)
if err != nil {
return nil, err
}
return paymentToDomain(payment), nil
}
func (s *Store) GetPaymentBySessionID(ctx context.Context, sessionID string) (*domain.Payment, error) {
payment, err := s.queries.GetPaymentBySessionID(ctx, toPgText(&sessionID))
if err != nil {
return nil, err
}
return paymentToDomain(payment), nil
}
func (s *Store) GetPaymentByNonce(ctx context.Context, nonce string) (*domain.Payment, error) {
payment, err := s.queries.GetPaymentByNonce(ctx, nonce)
if err != nil {
return nil, err
}
return paymentToDomain(payment), nil
}
func (s *Store) GetPaymentByTransactionID(ctx context.Context, transactionID string) (*domain.Payment, error) {
payment, err := s.queries.GetPaymentByTransactionID(ctx, toPgText(&transactionID))
if err != nil {
return nil, err
}
return paymentToDomain(payment), nil
}
func (s *Store) GetPaymentsByUserID(ctx context.Context, userID int64, limit, offset int32) ([]domain.Payment, error) {
payments, err := s.queries.GetPaymentsByUserID(ctx, dbgen.GetPaymentsByUserIDParams{
UserID: userID,
Limit: pgtype.Int4{Int32: limit, Valid: true},
Offset: pgtype.Int4{Int32: offset, Valid: true},
})
if err != nil {
return nil, err
}
result := make([]domain.Payment, len(payments))
for i, p := range payments {
result[i] = domain.Payment{
ID: p.ID,
UserID: p.UserID,
PlanID: int8PtrToInt64Ptr(p.PlanID),
SubscriptionID: int8PtrToInt64Ptr(p.SubscriptionID),
SessionID: fromPgTextPtr(p.SessionID),
TransactionID: fromPgTextPtr(p.TransactionID),
Nonce: p.Nonce,
Amount: fromPgNumeric(p.Amount),
Currency: p.Currency,
PaymentMethod: fromPgTextPtr(p.PaymentMethod),
Status: p.Status,
PaymentURL: fromPgTextPtr(p.PaymentUrl),
PaidAt: timePtr(p.PaidAt),
ExpiresAt: timePtr(p.ExpiresAt),
CreatedAt: p.CreatedAt.Time,
UpdatedAt: timePtr(p.UpdatedAt),
PlanName: fromPgTextPtr(p.PlanName),
}
}
return result, nil
}
func (s *Store) GetPendingPaymentsByUserID(ctx context.Context, userID int64) ([]domain.Payment, error) {
payments, err := s.queries.GetPendingPaymentsByUserID(ctx, userID)
if err != nil {
return nil, err
}
result := make([]domain.Payment, len(payments))
for i, p := range payments {
result[i] = *paymentToDomain(p)
}
return result, nil
}
func (s *Store) UpdatePaymentStatus(ctx context.Context, id int64, status string) error {
return s.queries.UpdatePaymentStatus(ctx, dbgen.UpdatePaymentStatusParams{
Status: status,
ID: id,
})
}
func (s *Store) UpdatePaymentStatusBySessionID(ctx context.Context, sessionID, status, transactionID, paymentMethod string) error {
return s.queries.UpdatePaymentStatusBySessionID(ctx, dbgen.UpdatePaymentStatusBySessionIDParams{
Status: status,
TransactionID: transactionID,
PaymentMethod: paymentMethod,
SessionID: sessionID,
})
}
func (s *Store) UpdatePaymentStatusByNonce(ctx context.Context, nonce, status, transactionID, paymentMethod string) error {
return s.queries.UpdatePaymentStatusByNonce(ctx, dbgen.UpdatePaymentStatusByNonceParams{
Status: status,
TransactionID: transactionID,
PaymentMethod: paymentMethod,
Nonce: nonce,
})
}
func (s *Store) UpdatePaymentSessionID(ctx context.Context, id int64, sessionID, paymentURL string) error {
return s.queries.UpdatePaymentSessionID(ctx, dbgen.UpdatePaymentSessionIDParams{
SessionID: toPgText(&sessionID),
PaymentUrl: toPgText(&paymentURL),
ID: id,
})
}
func (s *Store) LinkPaymentToSubscription(ctx context.Context, paymentID, subscriptionID int64) error {
return s.queries.LinkPaymentToSubscription(ctx, dbgen.LinkPaymentToSubscriptionParams{
SubscriptionID: pgtype.Int8{Int64: subscriptionID, Valid: true},
ID: paymentID,
})
}
func (s *Store) GetExpiredPendingPayments(ctx context.Context) ([]domain.Payment, error) {
payments, err := s.queries.GetExpiredPendingPayments(ctx)
if err != nil {
return nil, err
}
result := make([]domain.Payment, len(payments))
for i, p := range payments {
result[i] = *paymentToDomain(p)
}
return result, nil
}
func (s *Store) ExpirePayment(ctx context.Context, id int64) error {
return s.queries.ExpirePayment(ctx, id)
}
func (s *Store) ListPaymentsAdmin(ctx context.Context, filter domain.PaymentListFilter) (domain.PaymentListPage, error) {
params := buildPaymentsAdminFilterParams(filter)
total, err := s.queries.CountPaymentsAdmin(ctx, dbgen.CountPaymentsAdminParams(params))
if err != nil {
return domain.PaymentListPage{}, err
}
rows, err := s.queries.ListPaymentsAdmin(ctx, dbgen.ListPaymentsAdminParams{
PaymentID: int64PtrToPgInt8(filter.PaymentID),
UserID: params.UserID,
PlanID: params.PlanID,
SubscriptionID: params.SubscriptionID,
Status: params.Status,
PaymentMethod: params.PaymentMethod,
Currency: params.Currency,
PlanCategory: params.PlanCategory,
CreatedFrom: params.CreatedFrom,
CreatedTo: params.CreatedTo,
PaidFrom: params.PaidFrom,
PaidTo: params.PaidTo,
MinAmount: params.MinAmount,
MaxAmount: params.MaxAmount,
Reference: params.Reference,
Limit: filter.Limit,
Offset: filter.Offset,
})
if err != nil {
return domain.PaymentListPage{}, err
}
items := make([]domain.Payment, len(rows))
for i, row := range rows {
items[i] = paymentAdminRowToDomain(row)
}
return domain.PaymentListPage{
Items: items,
Total: total,
Limit: filter.Limit,
Offset: filter.Offset,
}, nil
}
func buildPaymentsAdminFilterParams(filter domain.PaymentListFilter) dbgen.CountPaymentsAdminParams {
return dbgen.CountPaymentsAdminParams{
PaymentID: int64PtrToPgInt8(filter.PaymentID),
UserID: int64PtrToPgInt8(filter.UserID),
PlanID: int64PtrToPgInt8(filter.PlanID),
SubscriptionID: int64PtrToPgInt8(filter.SubscriptionID),
Status: toPgText(filter.Status),
PaymentMethod: toPgText(filter.PaymentMethod),
Currency: toPgText(filter.Currency),
PlanCategory: toPgText(filter.PlanCategory),
CreatedFrom: toPgTimestamptzPtr(filter.CreatedFrom),
CreatedTo: toPgTimestamptzPtr(filter.CreatedTo),
PaidFrom: toPgTimestamptzPtr(filter.PaidFrom),
PaidTo: toPgTimestamptzPtr(filter.PaidTo),
MinAmount: toPgNumericPtr(filter.MinAmount),
MaxAmount: toPgNumericPtr(filter.MaxAmount),
Reference: toPgText(filter.Reference),
}
}
func paymentAdminRowToDomain(row dbgen.ListPaymentsAdminRow) domain.Payment {
return domain.Payment{
ID: row.ID,
UserID: row.UserID,
PlanID: int8PtrToInt64Ptr(row.PlanID),
SubscriptionID: int8PtrToInt64Ptr(row.SubscriptionID),
SessionID: fromPgTextPtr(row.SessionID),
TransactionID: fromPgTextPtr(row.TransactionID),
Nonce: row.Nonce,
Amount: fromPgNumeric(row.Amount),
Currency: row.Currency,
PaymentMethod: fromPgTextPtr(row.PaymentMethod),
Status: row.Status,
PaymentURL: fromPgTextPtr(row.PaymentUrl),
PaidAt: timePtr(row.PaidAt),
ExpiresAt: timePtr(row.ExpiresAt),
CreatedAt: row.CreatedAt.Time,
UpdatedAt: timePtr(row.UpdatedAt),
PlanName: fromPgTextPtr(row.PlanName),
PlanCategory: fromPgTextPtr(row.PlanCategory),
UserEmail: fromPgTextPtr(row.UserEmail),
UserFirstName: fromPgTextPtr(row.UserFirstName),
UserLastName: fromPgTextPtr(row.UserLastName),
}
}
func toPgNumericPtr(val *float64) pgtype.Numeric {
if val == nil {
return pgtype.Numeric{Valid: false}
}
return toPgNumeric(*val)
}
// Helper functions
func paymentToDomain(p dbgen.Payment) *domain.Payment {
return &domain.Payment{
ID: p.ID,
UserID: p.UserID,
PlanID: int8PtrToInt64Ptr(p.PlanID),
SubscriptionID: int8PtrToInt64Ptr(p.SubscriptionID),
SessionID: fromPgTextPtr(p.SessionID),
TransactionID: fromPgTextPtr(p.TransactionID),
Nonce: p.Nonce,
Amount: fromPgNumeric(p.Amount),
Currency: p.Currency,
PaymentMethod: fromPgTextPtr(p.PaymentMethod),
Status: p.Status,
PaymentURL: fromPgTextPtr(p.PaymentUrl),
PaidAt: timePtr(p.PaidAt),
ExpiresAt: timePtr(p.ExpiresAt),
CreatedAt: p.CreatedAt.Time,
UpdatedAt: timePtr(p.UpdatedAt),
}
}
func int64PtrToPgInt8(val *int64) pgtype.Int8 {
if val == nil {
return pgtype.Int8{Valid: false}
}
return pgtype.Int8{Int64: *val, Valid: true}
}
func int8PtrToInt64Ptr(val pgtype.Int8) *int64 {
if !val.Valid {
return nil
}
return &val.Int64
}
func fromPgTextPtr(t pgtype.Text) *string {
if !t.Valid {
return nil
}
return &t.String
}
func strPtr(s string) *string {
return &s
}