Skip to content

Commit

Permalink
Make results paged, but allow access to further pages
Browse files Browse the repository at this point in the history
  • Loading branch information
leighmcculloch committed Feb 19, 2018
1 parent 81e6ad0 commit 063e147
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 97 deletions.
13 changes: 10 additions & 3 deletions search.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ type SearchQuery struct {
Fields []interface{}
}

type SearchResults struct {
type searchResults struct {
XMLName string `xml:"search-results"`
PageSize string `xml:"page-size"`
Ids struct {
PageSize int `xml:"page-size"`
IDs struct {
Item []string `xml:"item"`
} `xml:"ids"`
}
Expand Down Expand Up @@ -112,3 +112,10 @@ func (s *SearchQuery) AddMultiField(field string) *MultiField {
s.Fields = append(s.Fields, f)
return f
}

func (s *SearchQuery) shallowCopy() *SearchQuery {
return &SearchQuery{
XMLName: s.XMLName,
Fields: s.Fields[:len(s.Fields):len(s.Fields)],
}
}
19 changes: 7 additions & 12 deletions transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,19 +213,14 @@ func (r TransactionOptionsPaypalRequest) MarshalXML(e *xml.Encoder, start xml.St
}

type TransactionSearchResult struct {
XMLName string `xml:"search-results"`
// CurrentPageNumber is not in the XML response but added manually for backward compatability.
CurrentPageNumber int
// PageSize indicates the page size for subsequent calls to get transaction detail. There can be more items in the Ids struct than indicated by PageSize.
PageSize int `xml:"page-size"`
// TotalItems is not in the XML response but added manually for backward compatability
TotalItems int
// Transactions is not in the XML response, but added in manually.
Transactions []*Transaction
// Per https://developers.braintreepayments.com/reference/general/searching/search-results/java only 20,000 ids can be returned.
Ids struct {
Item []string `xml:"item"`
} `xml:"ids"`
TotalIDs []string

CurrentPageNumber int
PageSize int
Transactions []*Transaction

searchQuery *SearchQuery
}

type RiskData struct {
Expand Down
126 changes: 66 additions & 60 deletions transaction_gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,93 +153,99 @@ func (g *TransactionGateway) Find(ctx context.Context, id string) (*Transaction,
return nil, &invalidResponseError{resp}
}

// Search finds all transactions matching the search query.
// Per https://developers.braintreepayments.com/reference/general/searching/search-results a max of 20,000 results can be returned.
// Search finds transactions matching the search query, returning the first
// page of results. Use SearchNext to get subsequent pages.
func (g *TransactionGateway) Search(ctx context.Context, query *SearchQuery) (*TransactionSearchResult, error) {
// Get the ids of all transactions that match the search criteria.
resp, err := g.execute(ctx, "POST", "transactions/advanced_search_ids", query)
if err != nil {
return nil, err
}
v := new(TransactionSearchResult)
err = xml.Unmarshal(resp.Body, v)
searchResult, err := g.fetchTransactionIDs(ctx, query)
if err != nil {
return nil, err
}

// Get transaction details and add them to the TransactionSearchResult. Only v.PageSize transactions can be fetched at one time.
totalItems := len(v.Ids.Item)
txs, err := fetchTransactions(ctx, g, query, v.Ids.Item, totalItems, v.PageSize)
if err != nil {
return nil, err
pageSize := searchResult.PageSize
ids := searchResult.IDs.Item

endOffset := pageSize
if endOffset > len(ids) {
endOffset = len(ids)
}
v.Transactions = txs

// Add data for backward compatibility. CurrnetPageNumber is pretty meaningless though.
v.CurrentPageNumber = 1
v.TotalItems = totalItems
firstPageQuery := query.shallowCopy()
firstPageQuery.AddMultiField("ids").Items = ids[:endOffset]
firstPageTransactions, err := g.fetchTransactions(ctx, firstPageQuery)

firstPageResult := &TransactionSearchResult{
TotalItems: len(ids),
TotalIDs: ids,
CurrentPageNumber: 1,
PageSize: pageSize,
Transactions: firstPageTransactions,
searchQuery: query,
}

return v, err
return firstPageResult, err
}

func fetchTransactions(ctx context.Context, g executer, query *SearchQuery, txIds []string, totalItems int, pageSize int) ([]*Transaction, error) {
txs := make([]*Transaction, 0)
ids := query.AddMultiField("ids")
for i := 0; i < totalItems; i += pageSize {
ids.Items = txIds[i:min(i+pageSize, totalItems)]
res, fetchError := fetchTransactionPage(ctx, g, query)
if fetchError != nil {
return nil, fetchError
}
for _, tx := range res.Transactions {
txs = append(txs, tx)
}
// SearchNext finds the next page of transactions matching the search query.
// Use Search to start a search and get the first page of results.
func (g *TransactionGateway) SearchNext(ctx context.Context, result *TransactionSearchResult) (*TransactionSearchResult, error) {
startOffset := result.CurrentPageNumber * result.PageSize
endOffset := startOffset + result.PageSize
if endOffset > len(result.TotalIDs) {
endOffset = len(result.TotalIDs)
}
if len(txs) != totalItems {
return nil, fmt.Errorf("Unexpected number of transactions. Got %v, want %v.", len(txs), totalItems)
if startOffset >= endOffset {
return nil, nil
}
return txs, nil
}

type testOperationPerformedInProductionError struct {
error
}
nextPageQuery := result.searchQuery.shallowCopy()
nextPageQuery.AddMultiField("ids").Items = result.TotalIDs[startOffset:endOffset]
nextPageTransactions, err := g.fetchTransactions(ctx, nextPageQuery)

func (e *testOperationPerformedInProductionError) Error() string {
return fmt.Sprint("Operation not allowed in production environment")
nextPageResult := &TransactionSearchResult{
TotalItems: result.TotalItems,
TotalIDs: result.TotalIDs,
CurrentPageNumber: result.CurrentPageNumber + 1,
PageSize: result.PageSize,
Transactions: nextPageTransactions,
searchQuery: result.searchQuery,
}

return nextPageResult, err
}

// FetchTransactionPage returns a page of transactions including details for a given query.
func fetchTransactionPage(ctx context.Context, g executer, query *SearchQuery) (*transactionDetailSearchResult, error) {
resp, err := g.execute(ctx, "POST", "transactions/advanced_search", query)
func (g *TransactionGateway) fetchTransactionIDs(ctx context.Context, query *SearchQuery) (*searchResults, error) {
resp, err := g.execute(ctx, "POST", "transactions/advanced_search_ids", query)
if err != nil {
return nil, err
}
v := new(transactionDetailSearchResult)
err = xml.Unmarshal(resp.Body, v)
var v searchResults
err = xml.Unmarshal(resp.Body, &v)
if err != nil {
return nil, err
}
return v, err
return &v, err
}

func min(a, b int) int {
if a <= b {
return a
func (g *TransactionGateway) fetchTransactions(ctx context.Context, query *SearchQuery) ([]*Transaction, error) {
resp, err := g.execute(ctx, "POST", "transactions/advanced_search", query)
if err != nil {
return nil, err
}
return b
var v struct {
XMLName string `xml:"credit-card-transactions"`
Transactions []*Transaction `xml:"transaction"`
}
err = xml.Unmarshal(resp.Body, &v)
if err != nil {
return nil, err
}
return v.Transactions, err
}

// TransactionDetailSearchResult is used to fetch a page of transactions with all details.
type transactionDetailSearchResult struct {
XMLName string `xml:"credit-card-transactions"`
CurrentPageNumber int `xml:"current-page-number"`
PageSize int `xml:"page-size"`
TotalItems int `xml:"total-items"`
Transactions []*Transaction `xml:"transaction"`
type testOperationPerformedInProductionError struct {
error
}

// Executer provides an execute method.
type executer interface {
execute(ctx context.Context, method, path string, xmlObj interface{}) (*Response, error)
func (e *testOperationPerformedInProductionError) Error() string {
return fmt.Sprint("Operation not allowed in production environment")
}
65 changes: 43 additions & 22 deletions transaction_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,48 +132,70 @@ func TestTransactionSearchPagination(t *testing.T) {
ctx := context.Background()

txg := testGateway.Transaction()
createTx := func(amount *Decimal, customerName string) error {
_, err := txg.Create(ctx, &TransactionRequest{

const transactionCount = 51
transactionIDs := map[string]bool{}
prefix := "PaginationTest-" + testhelpers.RandomString()
for i := 0; i < transactionCount; i++ {
unique := testhelpers.RandomString()
tx, err := txg.Create(ctx, &TransactionRequest{
Type: "sale",
Amount: amount,
Amount: randomAmount(),
Customer: &Customer{
FirstName: customerName,
FirstName: prefix + unique,
},
CreditCard: &CreditCard{
Number: testCreditCards["visa"].Number,
ExpirationDate: "05/14",
},
})
return err
}

const pageSize = 50
prefix := "PaginationTest-" + testhelpers.RandomString()
for i := 0; i < pageSize+1; i++ {
unique := testhelpers.RandomString()
if err := createTx(randomAmount(), prefix+unique); err != nil {
if err != nil {
t.Fatal(err)
}
transactionIDs[tx.Id] = true
}

t.Logf("transactionIDs = %v", transactionIDs)

query := new(SearchQuery)
f := query.AddTextField("customer-first-name")
f.StartsWith = prefix
query.AddTextField("customer-first-name").StartsWith = prefix

result, err := txg.Search(ctx, query)
results, err := txg.Search(ctx, query)
if err != nil {
t.Fatal(err)
}

if result.TotalItems != pageSize+1 {
t.Fatalf("result.TotalItems = %v, want %v", result.TotalItems, pageSize+1)
t.Logf("results.TotalItems = %v", results.TotalItems)
t.Logf("results.TotalIDs = %v", results.TotalIDs)
t.Logf("results.PageSize = %v", results.PageSize)

if results.TotalItems != transactionCount {
t.Fatalf("results.TotalItems = %v, want %v", results.TotalItems, transactionCount)
}

for i := 0; i < pageSize+1; i++ {
tx := result.Transactions[i]
if firstName := tx.Customer.FirstName; !strings.HasPrefix(firstName, prefix) {
t.Fatalf("tx.Customer.FirstName = %q, want prefix of %q", firstName, prefix)
for {
for _, tx := range results.Transactions {
if firstName := tx.Customer.FirstName; !strings.HasPrefix(firstName, prefix) {
t.Fatalf("tx.Customer.FirstName = %q, want prefix of %q", firstName, prefix)
}
if transactionIDs[tx.Id] {
delete(transactionIDs, tx.Id)
} else {
t.Fatalf("tx.Id = %q, not expected", tx.Id)
}
}

results, err = txg.SearchNext(ctx, results)
if err != nil {
t.Fatal(err)
}
if results == nil {
break
}
}

if len(transactionIDs) > 0 {
t.Fatalf("transactions not returned = %v", transactionIDs)
}
}

Expand Down Expand Up @@ -221,7 +243,6 @@ func TestTransactionSearchTime(t *testing.T) {
}

if result.TotalItems != 1 {
t.Log(result.TotalItems)
t.Fatal(result.Transactions)
}

Expand Down

0 comments on commit 063e147

Please sign in to comment.