Skip to content

Commit

Permalink
SDP-1391 Export Payments with Filtering (#493)
Browse files Browse the repository at this point in the history
  • Loading branch information
marwen-abid authored Dec 13, 2024
1 parent fe53476 commit 38b91ca
Show file tree
Hide file tree
Showing 9 changed files with 248 additions and 26 deletions.
15 changes: 9 additions & 6 deletions internal/data/payments.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ func (p *PaymentModel) Count(ctx context.Context, queryParams *QueryParams, sqlE
JOIN receiver_wallets rw on rw.receiver_id = p.receiver_id AND rw.wallet_id = w.id
`

query, params := newPaymentQuery(baseQuery, queryParams, false, sqlExec)
query, params := newPaymentQuery(baseQuery, queryParams, sqlExec, QueryTypeCount)

err := sqlExec.GetContext(ctx, &count, query, params...)
if err != nil {
Expand All @@ -262,10 +262,10 @@ func (p *PaymentModel) Count(ctx context.Context, queryParams *QueryParams, sqlE
}

// GetAll returns all PAYMENTS matching the given query parameters.
func (p *PaymentModel) GetAll(ctx context.Context, queryParams *QueryParams, sqlExec db.SQLExecuter) ([]Payment, error) {
func (p *PaymentModel) GetAll(ctx context.Context, queryParams *QueryParams, sqlExec db.SQLExecuter, queryType QueryType) ([]Payment, error) {
payments := []Payment{}

query, params := newPaymentQuery(basePaymentQuery, queryParams, true, sqlExec)
query, params := newPaymentQuery(basePaymentQuery, queryParams, sqlExec, queryType)

err := sqlExec.SelectContext(ctx, &payments, query, params...)
if err != nil {
Expand Down Expand Up @@ -620,7 +620,7 @@ func (p *PaymentModel) GetByIDs(ctx context.Context, sqlExec db.SQLExecuter, pay
}

// newPaymentQuery generates the full query and parameters for a payment search query
func newPaymentQuery(baseQuery string, queryParams *QueryParams, paginated bool, sqlExec db.SQLExecuter) (string, []interface{}) {
func newPaymentQuery(baseQuery string, queryParams *QueryParams, sqlExec db.SQLExecuter, queryType QueryType) (string, []interface{}) {
qb := NewQueryBuilder(baseQuery)
if queryParams.Filters[FilterKeyStatus] != nil {
if statusSlice, ok := queryParams.Filters[FilterKeyStatus].([]PaymentStatus); ok {
Expand All @@ -640,10 +640,13 @@ func newPaymentQuery(baseQuery string, queryParams *QueryParams, paginated bool,
if queryParams.Filters[FilterKeyCreatedAtBefore] != nil {
qb.AddCondition("p.created_at <= ?", queryParams.Filters[FilterKeyCreatedAtBefore])
}
if paginated {
qb.AddSorting(queryParams.SortBy, queryParams.SortOrder, "p")
if queryType == QueryTypeSelectPaginated {
qb.AddPagination(queryParams.Page, queryParams.PageLimit)
}

if queryType == QueryTypeSelectAll || queryType == QueryTypeSelectPaginated {
qb.AddSorting(queryParams.SortBy, queryParams.SortOrder, "p")
}
query, params := qb.Build()
return sqlExec.Rebind(query), params
}
Expand Down
36 changes: 19 additions & 17 deletions internal/data/payments_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ func Test_PaymentModelGetAll(t *testing.T) {
paymentModel := PaymentModel{dbConnectionPool: dbConnectionPool}

t.Run("returns empty list when no payments exist", func(t *testing.T) {
payments, errPayment := paymentModel.GetAll(ctx, &QueryParams{}, dbConnectionPool)
payments, errPayment := paymentModel.GetAll(ctx, &QueryParams{}, dbConnectionPool, QueryTypeSelectPaginated)
require.NoError(t, errPayment)
assert.Equal(t, 0, len(payments))
})
Expand Down Expand Up @@ -302,7 +302,7 @@ func Test_PaymentModelGetAll(t *testing.T) {

t.Run("returns payments successfully", func(t *testing.T) {
params := QueryParams{SortBy: DefaultPaymentSortField, SortOrder: DefaultPaymentSortOrder}
actualPayments, err := paymentModel.GetAll(ctx, &params, dbConnectionPool)
actualPayments, err := paymentModel.GetAll(ctx, &params, dbConnectionPool, QueryTypeSelectPaginated)
require.NoError(t, err)
assert.Equal(t, 2, len(actualPayments))
assert.Equal(t, []Payment{*expectedPayment2, *expectedPayment1}, actualPayments)
Expand All @@ -315,7 +315,7 @@ func Test_PaymentModelGetAll(t *testing.T) {
Page: 1,
PageLimit: 1,
}
actualPayments, err := paymentModel.GetAll(ctx, &params, dbConnectionPool)
actualPayments, err := paymentModel.GetAll(ctx, &params, dbConnectionPool, QueryTypeSelectPaginated)
require.NoError(t, err)
assert.Equal(t, 1, len(actualPayments))
assert.Equal(t, []Payment{*expectedPayment2}, actualPayments)
Expand All @@ -328,22 +328,24 @@ func Test_PaymentModelGetAll(t *testing.T) {
SortBy: DefaultPaymentSortField,
SortOrder: DefaultPaymentSortOrder,
}
actualPayments, err := paymentModel.GetAll(ctx, &params, dbConnectionPool)
actualPayments, err := paymentModel.GetAll(ctx, &params, dbConnectionPool, QueryTypeSelectPaginated)
require.NoError(t, err)
assert.Equal(t, 1, len(actualPayments))
assert.Equal(t, []Payment{*expectedPayment1}, actualPayments)
})

t.Run("returns payments successfully with created at order", func(t *testing.T) {
actualPayments, err := paymentModel.GetAll(ctx, &QueryParams{SortBy: SortFieldCreatedAt, SortOrder: SortOrderASC}, dbConnectionPool)
params := &QueryParams{SortBy: SortFieldCreatedAt, SortOrder: SortOrderASC}
actualPayments, err := paymentModel.GetAll(ctx, params, dbConnectionPool, QueryTypeSelectPaginated)

require.NoError(t, err)
assert.Equal(t, 2, len(actualPayments))
assert.Equal(t, []Payment{*expectedPayment1, *expectedPayment2}, actualPayments)
})

t.Run("returns payments successfully with updated at order", func(t *testing.T) {
actualPayments, err := paymentModel.GetAll(ctx, &QueryParams{SortBy: SortFieldUpdatedAt, SortOrder: SortOrderASC}, dbConnectionPool)
params := &QueryParams{SortBy: SortFieldUpdatedAt, SortOrder: SortOrderASC}
actualPayments, err := paymentModel.GetAll(ctx, params, dbConnectionPool, QueryTypeSelectPaginated)

require.NoError(t, err)
assert.Equal(t, 2, len(actualPayments))
Expand All @@ -354,7 +356,7 @@ func Test_PaymentModelGetAll(t *testing.T) {
filters := map[FilterKey]interface{}{
FilterKeyStatus: PendingPaymentStatus,
}
actualPayments, err := paymentModel.GetAll(ctx, &QueryParams{Filters: filters}, dbConnectionPool)
actualPayments, err := paymentModel.GetAll(ctx, &QueryParams{Filters: filters}, dbConnectionPool, QueryTypeSelectPaginated)
require.NoError(t, err)
assert.Equal(t, 1, len(actualPayments))
assert.Equal(t, []Payment{*expectedPayment2}, actualPayments)
Expand All @@ -372,7 +374,7 @@ func Test_PaymentModelGetAll(t *testing.T) {
SortBy: DefaultPaymentSortField,
SortOrder: DefaultPaymentSortOrder,
}
actualPayments, err := paymentModel.GetAll(ctx, &queryParams, dbConnectionPool)
actualPayments, err := paymentModel.GetAll(ctx, &queryParams, dbConnectionPool, QueryTypeSelectPaginated)
require.NoError(t, err)
assert.Equal(t, 2, len(actualPayments))
assert.Equal(t, []Payment{*expectedPayment2, *expectedPayment1}, actualPayments)
Expand Down Expand Up @@ -435,7 +437,7 @@ func Test_PaymentModelGetAll(t *testing.T) {
},
SortBy: DefaultPaymentSortField,
SortOrder: DefaultPaymentSortOrder,
}, dbConnectionPool)
}, dbConnectionPool, QueryTypeSelectPaginated)
require.NoError(t, err)

assert.Len(t, payments, 2)
Expand Down Expand Up @@ -606,15 +608,15 @@ func Test_PaymentNewPaymentQuery(t *testing.T) {
name string
baseQuery string
queryParams QueryParams
paginated bool
queryType QueryType
expectedQuery string
expectedParams []interface{}
}{
{
name: "build payment query without params and pagination",
baseQuery: "SELECT * FROM payments p",
queryParams: QueryParams{},
paginated: false,
queryType: QueryTypeSelectAll,
expectedQuery: "SELECT * FROM payments p",
expectedParams: []interface{}{},
},
Expand All @@ -626,7 +628,7 @@ func Test_PaymentNewPaymentQuery(t *testing.T) {
FilterKeyStatus: "draft",
},
},
paginated: false,
queryType: QueryTypeSelectAll,
expectedQuery: "SELECT * FROM payments p WHERE 1=1 AND p.status = $1",
expectedParams: []interface{}{"draft"},
},
Expand All @@ -638,7 +640,7 @@ func Test_PaymentNewPaymentQuery(t *testing.T) {
FilterKeyReceiverID: "receiver_id",
},
},
paginated: false,
queryType: QueryTypeSelectAll,
expectedQuery: "SELECT * FROM payments p WHERE 1=1 AND p.receiver_id = $1",
expectedParams: []interface{}{"receiver_id"},
},
Expand All @@ -651,7 +653,7 @@ func Test_PaymentNewPaymentQuery(t *testing.T) {
FilterKeyCreatedAtBefore: "00-01-31",
},
},
paginated: false,
queryType: QueryTypeSelectAll,
expectedQuery: "SELECT * FROM payments p WHERE 1=1 AND p.created_at >= $1 AND p.created_at <= $2",
expectedParams: []interface{}{"00-01-01", "00-01-31"},
},
Expand All @@ -664,7 +666,7 @@ func Test_PaymentNewPaymentQuery(t *testing.T) {
SortBy: "created_at",
SortOrder: "ASC",
},
paginated: true,
queryType: QueryTypeSelectPaginated,
expectedQuery: "SELECT * FROM payments p ORDER BY p.created_at ASC LIMIT $1 OFFSET $2",
expectedParams: []interface{}{20, 0},
},
Expand All @@ -683,15 +685,15 @@ func Test_PaymentNewPaymentQuery(t *testing.T) {
FilterKeyCreatedAtBefore: "00-01-31",
},
},
paginated: true,
queryType: QueryTypeSelectPaginated,
expectedQuery: "SELECT * FROM payments p WHERE 1=1 AND p.status = $1 AND p.receiver_id = $2 AND p.created_at >= $3 AND p.created_at <= $4 ORDER BY p.created_at ASC LIMIT $5 OFFSET $6",
expectedParams: []interface{}{"draft", "receiver_id", "00-01-01", "00-01-31", 20, 0},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
query, params := newPaymentQuery(tc.baseQuery, &tc.queryParams, tc.paginated, dbConnectionPool)
query, params := newPaymentQuery(tc.baseQuery, &tc.queryParams, dbConnectionPool, tc.queryType)

assert.Equal(t, tc.expectedQuery, query)
assert.Equal(t, tc.expectedParams, params)
Expand Down
2 changes: 1 addition & 1 deletion internal/serve/httphandler/assets_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1456,7 +1456,7 @@ func Test_AssetHandler_submitChangeTrustTransaction_makeSurePreconditionsAreSetA
timeSinceCreation := time.Since(creationTime)

expectedAdjustedMaxTime := expectedMaxTime.Add(timeSinceCreation)
require.WithinDuration(t, expectedAdjustedMaxTime, actualMaxTime, 5*time.Second)
require.WithinDuration(t, expectedAdjustedMaxTime, actualMaxTime, 10*time.Second)
}
}

Expand Down
72 changes: 72 additions & 0 deletions internal/serve/httphandler/export_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,75 @@ func (e ExportHandler) ExportDisbursements(rw http.ResponseWriter, r *http.Reque
return
}
}

type PaymentCSV struct {
ID string
Amount string
StellarTransactionID string
Status data.PaymentStatus
DisbursementID string `csv:"Disbursement.ID"`
Asset data.Asset
Wallet data.Wallet
ReceiverID string `csv:"Receiver.ID"`
ReceiverWalletAddress string `csv:"ReceiverWallet.Address"`
ReceiverWalletStatus data.ReceiversWalletStatus `csv:"ReceiverWallet.Status"`
CreatedAt time.Time
UpdatedAt time.Time
ExternalPaymentID string
CircleTransferRequestID *string
}

func (e ExportHandler) ExportPayments(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()

validator := validators.NewPaymentQueryValidator()
queryParams := validator.ParseParametersFromRequest(r)

if validator.HasErrors() {
httperror.BadRequest("Request invalid", nil, validator.Errors).Render(rw)
return
}

queryParams.Filters = validator.ValidateAndGetPaymentFilters(queryParams.Filters)
if validator.HasErrors() {
httperror.BadRequest("Request invalid", nil, validator.Errors).Render(rw)
return
}

payments, err := e.Models.Payment.GetAll(ctx, queryParams, e.Models.DBConnectionPool, data.QueryTypeSelectAll)
if err != nil {
httperror.InternalError(ctx, "Failed to get payments", err, nil).Render(rw)
return
}

// Convert payments to PaymentCSV
paymentCSVs := make([]*PaymentCSV, 0, len(payments))
for _, payment := range payments {
paymentCSV := &PaymentCSV{
ID: payment.ID,
Amount: payment.Amount,
StellarTransactionID: payment.StellarTransactionID,
Status: payment.Status,
DisbursementID: payment.Disbursement.ID,
Asset: payment.Asset,
Wallet: payment.ReceiverWallet.Wallet,
ReceiverID: payment.ReceiverWallet.Receiver.ID,
ReceiverWalletAddress: payment.ReceiverWallet.StellarAddress,
ReceiverWalletStatus: payment.ReceiverWallet.Status,
CreatedAt: payment.CreatedAt,
UpdatedAt: payment.UpdatedAt,
ExternalPaymentID: payment.ExternalPaymentID,
CircleTransferRequestID: payment.CircleTransferRequestID,
}
paymentCSVs = append(paymentCSVs, paymentCSV)
}

fileName := fmt.Sprintf("payments_%s.csv", time.Now().Format("2006-01-02-15-04-05"))
rw.Header().Set("Content-Type", "text/csv")
rw.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", fileName))

if err := gocsv.Marshal(paymentCSVs, rw); err != nil {
httperror.InternalError(ctx, "Failed to write CSV", err, nil).Render(rw)
return
}
}
Loading

0 comments on commit 38b91ca

Please sign in to comment.