Skip to content

Commit 1fdea93

Browse files
committed
add tx filtering by amount
1 parent f690471 commit 1fdea93

File tree

5 files changed

+92
-6
lines changed

5 files changed

+92
-6
lines changed

internal/api/v2/controllers_transactions_list_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,20 @@ func TestTransactionsList(t *testing.T) {
146146
},
147147
},
148148
},
149+
{
150+
name: "using amount",
151+
body: `{"$lt": {"amount[EUR]": 20}}`,
152+
expectQuery: storagecommon.InitialPaginatedQuery[any]{
153+
PageSize: bunpaginate.QueryDefaultPageSize,
154+
Column: "id",
155+
Order: pointer.For(bunpaginate.Order(bunpaginate.OrderDesc)),
156+
Options: storagecommon.ResourceQuery[any]{
157+
PIT: &now,
158+
Builder: query.Lt("EUR", 20),
159+
Expand: make([]string, 0),
160+
},
161+
},
162+
},
149163
{
150164
name: "using empty cursor",
151165
queryParams: url.Values{

internal/storage/ledger/resource_transactions.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ func (h transactionsResourceHandler) Schema() common.EntitySchema {
2525
"reference": common.NewStringField(),
2626
"inserted_at": common.NewDateField().Paginated(),
2727
"updated_at": common.NewDateField().Paginated(),
28+
"amount": common.NewNumericMapField(),
2829
},
2930
}
3031
}
@@ -101,6 +102,20 @@ func (h transactionsResourceHandler) ResolveFilter(_ common.ResourceQuery[any],
101102
return filterAccountAddressOnTransactions(value.(string), true, false), nil, nil
102103
case property == "destination":
103104
return filterAccountAddressOnTransactions(value.(string), false, true), nil, nil
105+
case amountRegex.Match([]byte(property)):
106+
asset := amountRegex.FindStringSubmatch(property)[1]
107+
108+
selectAmount := h.store.db.NewSelect().
109+
ModelTableExpr(fmt.Sprintf("%[1]s, jsonb_array_elements(%[1]s.postings::jsonb) posting", h.store.GetPrefixedRelationName("transactions"))).
110+
Where("id = dataset.id").
111+
Where("ledger = ?", h.store.ledger.Name).
112+
Where("posting->>'asset' = ?", asset).
113+
ColumnExpr("sum((posting->'amount')::numeric) amount")
114+
115+
return h.store.db.NewSelect().
116+
TableExpr("(?) amount", selectAmount).
117+
ColumnExpr(fmt.Sprintf("amount %s ?", common.ConvertOperatorToSQL(operator)), value).
118+
String(), nil, nil
104119
case common.MetadataRegex.Match([]byte(property)):
105120
match := common.MetadataRegex.FindAllStringSubmatch(property, 3)
106121

internal/storage/ledger/transactions.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"math/big"
1010
"slices"
1111
"strings"
12+
"regexp"
1213

1314
"github.com/formancehq/ledger/internal/tracing"
1415

@@ -28,6 +29,10 @@ import (
2829
"github.com/uptrace/bun"
2930
)
3031

32+
var (
33+
amountRegex = regexp.MustCompile(`amount\[(.*)]`)
34+
)
35+
3136
func (store *Store) CommitTransaction(ctx context.Context, tx *ledger.Transaction, accountMetadata map[string]metadata.Metadata) error {
3237
if accountMetadata == nil {
3338
accountMetadata = make(map[string]metadata.Metadata)

internal/storage/ledger/transactions_test.go

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,15 @@ func TestTransactionsList(t *testing.T) {
730730
err = store.CommitTransaction(ctx, &tx5, nil)
731731
require.NoError(t, err)
732732

733+
tx6 := ledger.NewTransaction().
734+
WithPostings(
735+
ledger.NewPosting("world", "users:charlie", "EUR", big.NewInt(30)),
736+
ledger.NewPosting("users:charlie", "world", "EUR", big.NewInt(30)),
737+
).
738+
WithTimestamp(now)
739+
err = store.CommitTransaction(ctx, &tx6, nil)
740+
require.NoError(t, err)
741+
733742
type testCase struct {
734743
name string
735744
query common.InitialPaginatedQuery[any]
@@ -740,7 +749,7 @@ func TestTransactionsList(t *testing.T) {
740749
{
741750
name: "nominal",
742751
query: common.InitialPaginatedQuery[any]{},
743-
expected: []ledger.Transaction{tx5, tx4, tx3, tx2, tx1},
752+
expected: []ledger.Transaction{tx6, tx5, tx4, tx3, tx2, tx1},
744753
},
745754
{
746755
name: "address filter",
@@ -767,7 +776,7 @@ func TestTransactionsList(t *testing.T) {
767776
Builder: query.Match("account", "users:"),
768777
},
769778
},
770-
expected: []ledger.Transaction{tx5, tx4, tx3},
779+
expected: []ledger.Transaction{tx6, tx5, tx4, tx3},
771780
},
772781
{
773782
name: "address filter using segment and unbounded segment list",
@@ -776,7 +785,7 @@ func TestTransactionsList(t *testing.T) {
776785
Builder: query.Match("account", "users:..."),
777786
},
778787
},
779-
expected: []ledger.Transaction{tx5, tx4, tx3},
788+
expected: []ledger.Transaction{tx6, tx5, tx4, tx3},
780789
},
781790
{
782791
name: "filter using metadata",
@@ -846,7 +855,7 @@ func TestTransactionsList(t *testing.T) {
846855
Builder: query.Not(query.Exists("metadata", "category")),
847856
},
848857
},
849-
expected: []ledger.Transaction{tx5, tx4},
858+
expected: []ledger.Transaction{tx6, tx5, tx4},
850859
},
851860
{
852861
name: "filter using timestamp",
@@ -855,7 +864,16 @@ func TestTransactionsList(t *testing.T) {
855864
Builder: query.Match("timestamp", tx5.Timestamp.Format(time.RFC3339Nano)),
856865
},
857866
},
858-
expected: []ledger.Transaction{tx5, tx4},
867+
expected: []ledger.Transaction{tx6, tx5, tx4},
868+
},
869+
{
870+
name: "filter using amount",
871+
query: common.InitialPaginatedQuery[any]{
872+
Options: common.ResourceQuery[any]{
873+
Builder: query.Gt("amount[EUR]", 50),
874+
},
875+
},
876+
expected: []ledger.Transaction{tx6, tx1},
859877
},
860878
}
861879

test/e2e/api_transactions_list_test.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,7 @@ var _ = Context("Ledger transactions list API tests", func() {
399399
}))
400400
})
401401
})
402-
When("listing transactions using filter on a single match", func() {
402+
When("listing transactions using filter on multiple matches", func() {
403403
var (
404404
err error
405405
response *operations.V2ListTransactionsResponse
@@ -444,6 +444,40 @@ var _ = Context("Ledger transactions list API tests", func() {
444444
}))
445445
})
446446
})
447+
When("listing transactions using filter on amount", func() {
448+
var (
449+
err error
450+
response *operations.V2ListTransactionsResponse
451+
now = time.Now().Round(time.Second).UTC()
452+
)
453+
JustBeforeEach(func(specContext SpecContext) {
454+
response, err = Wait(specContext, DeferClient(testServer)).Ledger.V2.ListTransactions(
455+
ctx,
456+
operations.V2ListTransactionsRequest{
457+
RequestBody: map[string]interface{}{
458+
"$gt": map[string]any{
459+
"amount[EUR]": 10,
460+
},
461+
},
462+
Ledger: "default",
463+
PageSize: pointer.For(pageSize),
464+
Pit: &now,
465+
},
466+
)
467+
Expect(err).To(BeNil())
468+
})
469+
_ = response
470+
It("Should be ok", func() {
471+
Expect(response.V2TransactionsCursorResponse.Cursor.Next).NotTo(BeNil())
472+
cursor := &common.ColumnPaginatedQuery[any]{}
473+
Expect(bunpaginate.UnmarshalCursor(*response.V2TransactionsCursorResponse.Cursor.Next, cursor)).To(BeNil())
474+
Expect(cursor.PageSize).To(Equal(uint64(10)))
475+
Expect(cursor.Options).To(Equal(common.ResourceQuery[any]{
476+
Builder: query.Gt("amount[EUR]", float64(10.0)),
477+
PIT: pointer.For(libtime.New(now)),
478+
}))
479+
})
480+
})
447481
When("listing transactions using invalid filter", func() {
448482
var (
449483
err error

0 commit comments

Comments
 (0)