Skip to content

Commit c93a655

Browse files
authored
feat: add batch account_ids filter to position-keeping and financial-accounting (#1883)
Merging with UNSTABLE - all failures pre-existing in services/identity/dex/. Zero errors from position-keeping or financial-accounting changes.
1 parent 363dd11 commit c93a655

15 files changed

Lines changed: 1114 additions & 680 deletions

File tree

api/proto/meridian/financial_accounting/v1/financial_accounting.pb.go

Lines changed: 206 additions & 191 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/proto/meridian/financial_accounting/v1/financial_accounting.proto

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,19 @@ message ListLedgerPostingsRequest {
440440
meridian.common.v1.TransactionStatus status = 8 [(buf.validate.field).enum = {
441441
defined_only: true
442442
}];
443+
444+
// account_ids filters by multiple account identifiers (optional, max 100).
445+
// When provided, takes precedence over account_id.
446+
repeated string account_ids = 9 [(buf.validate.field).repeated = {
447+
max_items: 100
448+
items: {
449+
string: {
450+
min_len: 1
451+
max_len: 255
452+
pattern: "^[a-zA-Z0-9_-]+$"
453+
}
454+
}
455+
}];
443456
}
444457

445458
// ListLedgerPostingsResponse returns a page of ledger postings.

api/proto/meridian/position_keeping/v1/position_keeping.pb.go

Lines changed: 492 additions & 478 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/proto/meridian/position_keeping/v1/position_keeping.proto

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,7 @@ message BulkImportTransactionsResponse {
453453
// ListFinancialPositionLogsRequest lists financial position logs with filtering.
454454
message ListFinancialPositionLogsRequest {
455455
// account_id filters logs by account (optional).
456+
// Deprecated in favour of account_ids. When account_ids is provided, account_id is ignored.
456457
string account_id = 1 [(buf.validate.field).string.max_len = 255];
457458

458459
// status filters logs by current status (optional).
@@ -463,6 +464,18 @@ message ListFinancialPositionLogsRequest {
463464

464465
// pagination controls the page size and offset.
465466
meridian.common.v1.Pagination pagination = 4;
467+
468+
// account_ids filters logs by multiple accounts (optional, max 100).
469+
// When provided, takes precedence over account_id.
470+
repeated string account_ids = 5 [(buf.validate.field).repeated = {
471+
max_items: 100
472+
items: {
473+
string: {
474+
min_len: 1
475+
max_len: 255
476+
}
477+
}
478+
}];
466479
}
467480

468481
// ListFinancialPositionLogsResponse returns a list of logs.

frontend/src/api/gen/meridian/financial_accounting/v1/financial_accounting_pb.ts

Lines changed: 9 additions & 1 deletion
Large diffs are not rendered by default.

frontend/src/api/gen/meridian/position_keeping/v1/position_keeping_pb.ts

Lines changed: 10 additions & 1 deletion
Large diffs are not rendered by default.

services/financial-accounting/adapters/persistence/repository.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -612,9 +612,12 @@ type ListPostingsParams struct {
612612
// BookingLogID filters by parent booking log (empty for no filter)
613613
BookingLogID *uuid.UUID
614614

615-
// AccountID filters by account identifier (empty for no filter)
615+
// AccountID filters by account identifier (empty for no filter). Ignored when AccountIDs is non-empty.
616616
AccountID string
617617

618+
// AccountIDs filters by multiple account identifiers (empty for no filter). Takes precedence over AccountID.
619+
AccountIDs []string
620+
618621
// PostingDirection filters by DEBIT or CREDIT (empty for no filter)
619622
PostingDirection string
620623

@@ -670,8 +673,10 @@ func (r *LedgerRepository) ListPostings(ctx context.Context, params ListPostings
670673
query = query.Where("financial_booking_log_id = ?", *params.BookingLogID)
671674
}
672675

673-
// Apply account ID filter if provided
674-
if params.AccountID != "" {
676+
// Apply account ID filter - AccountIDs takes precedence over AccountID
677+
if len(params.AccountIDs) > 0 {
678+
query = query.Where("account_id IN ?", params.AccountIDs)
679+
} else if params.AccountID != "" {
675680
query = query.Where("account_id = ?", params.AccountID)
676681
}
677682

services/financial-accounting/adapters/persistence/repository_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -798,3 +798,64 @@ func TestAuditChangedByDefaultsToSystem(t *testing.T) {
798798
require.NotNil(t, outbox.ChangedBy)
799799
assert.Equal(t, "system", *outbox.ChangedBy)
800800
}
801+
802+
func TestListPostings_FilterByAccountIDs(t *testing.T) {
803+
db, ctx, cleanup := setupTestDB(t)
804+
defer cleanup()
805+
806+
repo := NewLedgerRepository(db)
807+
808+
bookingLogID := uuid.New()
809+
bookingLog := &FinancialBookingLogEntity{
810+
ID: bookingLogID,
811+
FinancialAccountType: "DEBIT",
812+
ProductServiceReference: "PROD-001",
813+
BusinessUnitReference: "BU-001",
814+
ChartOfAccountsRules: "{}",
815+
BaseCurrency: "GBP",
816+
Status: "ACTIVE",
817+
IdempotencyKey: "test-key-" + uuid.New().String(),
818+
CreatedAt: time.Now(),
819+
UpdatedAt: time.Now(),
820+
Version: 1,
821+
}
822+
require.NoError(t, db.Create(bookingLog).Error)
823+
824+
gbpInstrument := domain.MustCurrencyToInstrument(domain.CurrencyGBP)
825+
money := domain.NewMoney(decimal.NewFromFloat(100.00), gbpInstrument)
826+
827+
for _, accID := range []string{"ACC-X1", "ACC-X2", "ACC-OTHER"} {
828+
p := &domain.LedgerPosting{
829+
ID: uuid.New(),
830+
FinancialBookingLogID: bookingLogID,
831+
Direction: domain.PostingDirectionDebit,
832+
Amount: money,
833+
AccountID: accID,
834+
ValueDate: time.Now(),
835+
Status: domain.TransactionStatusPending,
836+
CreatedAt: time.Now(),
837+
}
838+
require.NoError(t, repo.SavePosting(ctx, p))
839+
}
840+
841+
// Filter by two accounts
842+
result, err := repo.ListPostings(ctx, ListPostingsParams{
843+
PageSize: 10,
844+
AccountIDs: []string{"ACC-X1", "ACC-X2"},
845+
})
846+
require.NoError(t, err)
847+
require.Len(t, result.Postings, 2)
848+
for _, p := range result.Postings {
849+
assert.Contains(t, []string{"ACC-X1", "ACC-X2"}, p.AccountID)
850+
}
851+
852+
// AccountIDs takes precedence over AccountID
853+
result, err = repo.ListPostings(ctx, ListPostingsParams{
854+
PageSize: 10,
855+
AccountID: "ACC-OTHER",
856+
AccountIDs: []string{"ACC-X1"},
857+
})
858+
require.NoError(t, err)
859+
require.Len(t, result.Postings, 1)
860+
assert.Equal(t, "ACC-X1", result.Postings[0].AccountID)
861+
}

services/financial-accounting/service/grpc_integration_test.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1484,6 +1484,114 @@ func TestUpdateFinancialBookingLog_Integration_NonPostedTransition_SkipsValidati
14841484
assert.Equal(t, commonv1.TransactionStatus_TRANSACTION_STATUS_FAILED, resp.FinancialBookingLog.Status)
14851485
}
14861486

1487+
// TestListLedgerPostings_Integration_FilterByAccountIDs tests filtering by multiple account IDs.
1488+
func TestListLedgerPostings_Integration_FilterByAccountIDs(t *testing.T) {
1489+
ts, _ := setupIntegrationTest(t)
1490+
defer ts.cleanup()
1491+
1492+
ctx, cancel := context.WithTimeout(ts.ctx, 10*time.Second)
1493+
defer cancel()
1494+
1495+
bookingLogID := createTestBookingLog(t, ts.db, ts.ctx)
1496+
gbpInstrument := domain.MustCurrencyToInstrument(domain.CurrencyGBP)
1497+
amount := domain.NewMoney(decimal.NewFromInt(100), gbpInstrument)
1498+
1499+
// Create postings for three different accounts
1500+
for _, accID := range []string{"ACC-A1", "ACC-A2", "ACC-OTHER"} {
1501+
posting := &domain.LedgerPosting{
1502+
ID: uuid.New(),
1503+
FinancialBookingLogID: bookingLogID,
1504+
Direction: domain.PostingDirectionDebit,
1505+
Amount: amount,
1506+
AccountID: accID,
1507+
ValueDate: time.Now(),
1508+
Status: domain.TransactionStatusPending,
1509+
CreatedAt: time.Now(),
1510+
}
1511+
require.NoError(t, ts.repo.SavePosting(ctx, posting))
1512+
}
1513+
1514+
// Filter for two accounts only
1515+
resp, err := ts.grpcClient.ListLedgerPostings(ctx, &financialaccountingv1.ListLedgerPostingsRequest{
1516+
AccountIds: []string{"ACC-A1", "ACC-A2"},
1517+
Pagination: &commonv1.Pagination{PageSize: 10},
1518+
})
1519+
1520+
require.NoError(t, err)
1521+
require.NotNil(t, resp)
1522+
assert.Len(t, resp.LedgerPostings, 2)
1523+
for _, p := range resp.LedgerPostings {
1524+
assert.Contains(t, []string{"ACC-A1", "ACC-A2"}, p.AccountId)
1525+
}
1526+
}
1527+
1528+
// TestListLedgerPostings_Integration_AccountIDs_TakesPrecedence verifies account_ids takes
1529+
// precedence over account_id when both are provided.
1530+
func TestListLedgerPostings_Integration_AccountIDs_TakesPrecedence(t *testing.T) {
1531+
ts, _ := setupIntegrationTest(t)
1532+
defer ts.cleanup()
1533+
1534+
ctx, cancel := context.WithTimeout(ts.ctx, 10*time.Second)
1535+
defer cancel()
1536+
1537+
bookingLogID := createTestBookingLog(t, ts.db, ts.ctx)
1538+
gbpInstrument := domain.MustCurrencyToInstrument(domain.CurrencyGBP)
1539+
amount := domain.NewMoney(decimal.NewFromInt(100), gbpInstrument)
1540+
1541+
for _, accID := range []string{"ACC-B1", "ACC-B2", "ACC-IGNORED"} {
1542+
posting := &domain.LedgerPosting{
1543+
ID: uuid.New(),
1544+
FinancialBookingLogID: bookingLogID,
1545+
Direction: domain.PostingDirectionDebit,
1546+
Amount: amount,
1547+
AccountID: accID,
1548+
ValueDate: time.Now(),
1549+
Status: domain.TransactionStatusPending,
1550+
CreatedAt: time.Now(),
1551+
}
1552+
require.NoError(t, ts.repo.SavePosting(ctx, posting))
1553+
}
1554+
1555+
// account_ids should take precedence over account_id
1556+
resp, err := ts.grpcClient.ListLedgerPostings(ctx, &financialaccountingv1.ListLedgerPostingsRequest{
1557+
AccountId: "ACC-IGNORED",
1558+
AccountIds: []string{"ACC-B1", "ACC-B2"},
1559+
Pagination: &commonv1.Pagination{PageSize: 10},
1560+
})
1561+
1562+
require.NoError(t, err)
1563+
require.NotNil(t, resp)
1564+
assert.Len(t, resp.LedgerPostings, 2)
1565+
for _, p := range resp.LedgerPostings {
1566+
assert.Contains(t, []string{"ACC-B1", "ACC-B2"}, p.AccountId)
1567+
}
1568+
}
1569+
1570+
// TestListLedgerPostings_Integration_AccountIDs_MaxLimitRejected tests that >100 account_ids
1571+
// is rejected with InvalidArgument.
1572+
func TestListLedgerPostings_Integration_AccountIDs_MaxLimitRejected(t *testing.T) {
1573+
ts, _ := setupIntegrationTest(t)
1574+
defer ts.cleanup()
1575+
1576+
ctx, cancel := context.WithTimeout(ts.ctx, 10*time.Second)
1577+
defer cancel()
1578+
1579+
tooMany := make([]string, 101)
1580+
for i := range tooMany {
1581+
tooMany[i] = "ACC-001"
1582+
}
1583+
1584+
_, err := ts.grpcClient.ListLedgerPostings(ctx, &financialaccountingv1.ListLedgerPostingsRequest{
1585+
AccountIds: tooMany,
1586+
Pagination: &commonv1.Pagination{PageSize: 10},
1587+
})
1588+
1589+
require.Error(t, err)
1590+
st, ok := status.FromError(err)
1591+
require.True(t, ok)
1592+
assert.Equal(t, codes.InvalidArgument, st.Code())
1593+
}
1594+
14871595
// TestUpdateFinancialBookingLog_Integration_CancelledTransition_SkipsValidation tests that
14881596
// transitions to CANCELLED skip balance validation.
14891597
func TestUpdateFinancialBookingLog_Integration_CancelledTransition_SkipsValidation(t *testing.T) {

services/financial-accounting/service/grpc_ledger_endpoints.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,13 @@ func (s *FinancialAccountingService) ListLedgerPostings(
157157
params.BookingLogID = &bookingLogID
158158
}
159159

160-
// Apply account ID filter if provided
161-
if req.AccountId != "" {
160+
// Apply account ID filter - account_ids takes precedence over account_id
161+
if len(req.AccountIds) > 0 {
162+
if len(req.AccountIds) > 100 {
163+
return nil, status.Error(codes.InvalidArgument, "account_ids must not exceed 100 items")
164+
}
165+
params.AccountIDs = req.AccountIds
166+
} else if req.AccountId != "" {
162167
params.AccountID = req.AccountId
163168
}
164169

0 commit comments

Comments
 (0)