Skip to content

feat(billing): add SearchOrgInvoices RQL endpoint#1536

Open
paanSinghCoder wants to merge 16 commits intomainfrom
feat/search-org-invoices-rql
Open

feat(billing): add SearchOrgInvoices RQL endpoint#1536
paanSinghCoder wants to merge 16 commits intomainfrom
feat/search-org-invoices-rql

Conversation

@paanSinghCoder
Copy link
Copy Markdown
Contributor

@paanSinghCoder paanSinghCoder commented Apr 13, 2026

Summary

  • add a new SearchOrgInvoices RPC in FrontierService that supports RQLRequest and pagination response

SearchOrgInvoices RQL Support Matrix

Category Supported Notes
org_id Yes (required) Request is org-scoped; validation enforced.
nonzero_amount_only Yes true applies amount > 0 filter.
query.limit Yes Defaults to 50 when missing/invalid.
query.offset Yes Defaults to 0 when missing/invalid.
query.search Yes Applied to searchable invoice fields.
query.sort Yes Works on supported fields below.
query.filters Yes Works on supported fields below.
query.group_by No Explicitly rejected (group_by is not supported).

Filterable / Sortable Fields

Field Type Filter Sort
id string Yes Yes
provider_id string Yes Yes
customer_id string Yes Yes
state string Yes Yes
currency string Yes Yes
amount number Yes Yes
hosted_url string Yes Yes
due_at datetime Yes Yes
effective_at datetime Yes Yes
created_at datetime Yes Yes
period_start_at datetime Yes Yes
period_end_at datetime Yes Yes

Searchable Fields

Field
id
provider_id
state
currency
hosted_url

Operator Support (practical)

Type Common operators
string eq, neq, like, notlike, in, notin, empty, notempty
number eq, neq, gt, gte, lt, lte
datetime eq, neq, gt, gte, lt, lte

Test plan

  • run make proto and verify no generated-file diffs remain
  • run billing API tests for connect handler and repository paths
  • verify existing ListInvoices consumers still behave unchanged
  • exercise SearchOrgInvoices with filters/sort/search/limit/offset

Introduce a new SearchOrgInvoices endpoint with RQL support while keeping ListInvoices behavior unchanged for existing consumers, and wire backend auth/service/repository plus regenerated frontier protobuf bindings.

Made-with: Cursor
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 13, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
frontier Ready Ready Preview, Comment Apr 16, 2026 8:28am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 13, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an organization-scoped invoice search feature across API handler, service, repository, models, mocks, tests, and authorization; also updates the Makefile PROTON_COMMIT used for proto generation.

Changes

Cohort / File(s) Summary
Build Configuration
Makefile
Updated PROTON_COMMIT value from 3959f2283a9335b800a60eb6fd1cf498c2e2e8aa to a55e97d0e73877e41d5a6ac0237d0a8bdc0fb2ca.
Data Model
billing/invoice/invoice.go
Added exported types SearchOrgInvoice, SearchOrgInvoicesPagination, and SearchOrgInvoicesResult with RQL tags for search fields and pagination wrapper.
Service Layer
billing/invoice/service.go, billing/invoice/service_search_org_invoices_test.go
Extended Repository interface with SearchOrgInvoices; added Service.SearchOrgInvoices (input validation, group_by rejection) and unit tests for validation and delegation.
API Interface & Mocks
internal/api/v1beta1connect/interfaces.go, internal/api/v1beta1connect/mocks/invoice_service.go
Added InvoiceService.SearchOrgInvoices to the interface and corresponding mock method/expectation helpers.
API Handler & Tests
internal/api/v1beta1connect/billing_invoice.go, internal/api/v1beta1connect/billing_invoice_test.go
Added ConnectHandler.SearchOrgInvoices (org→customer resolution, proto→RQL transform/validate, service call, error mapping, response pagination) and comprehensive handler tests covering validation, not-found, mapping, and pagination.
Repository (Postgres)
internal/store/postgres/billing_invoice_repository.go
Implemented BillingInvoiceRepository.SearchOrgInvoices: builds customer-scoped SQL using RQL filters/search/sort/pagination, executes COUNT and paginated data queries, transforms rows to models, and returns SearchOrgInvoicesResult.
Authorization
pkg/server/connect_interceptors/authorization.go
Added authorization validation entry for /raystack.frontier.v1beta1.FrontierService/SearchOrgInvoices enforcing org-scoped UpdatePermission.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • AmanGIT07
  • rohilsurana
  • rsbh

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 29242d54-19b4-4ba1-872f-e89c7f7065a6

📥 Commits

Reviewing files that changed from the base of the PR and between 73227f6 and d42cbf1.

⛔ Files ignored due to path filters (2)
  • proto/v1beta1/frontier.pb.go is excluded by !**/*.pb.go, !proto/**
  • proto/v1beta1/frontierv1beta1connect/frontier.connect.go is excluded by !proto/**
📒 Files selected for processing (8)
  • Makefile
  • billing/invoice/invoice.go
  • billing/invoice/service.go
  • internal/api/v1beta1connect/billing_invoice.go
  • internal/api/v1beta1connect/interfaces.go
  • internal/api/v1beta1connect/mocks/invoice_service.go
  • internal/store/postgres/billing_invoice_repository.go
  • pkg/server/connect_interceptors/authorization.go

Comment thread internal/api/v1beta1connect/billing_invoice.go Outdated
Comment thread internal/store/postgres/billing_invoice_repository.go
@coveralls
Copy link
Copy Markdown

coveralls commented Apr 13, 2026

Coverage Report for CI Build 24500005804

Coverage decreased (-0.01%) to 41.805%

Details

  • Coverage decreased (-0.01%) from the base build.
  • Patch coverage: 82 uncovered changes across 3 files (52 of 134 lines covered, 38.81%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
internal/store/postgres/billing_invoice_repository.go 64 0 0.0%
internal/api/v1beta1connect/billing_invoice.go 55 41 74.55%
pkg/server/connect_interceptors/authorization.go 4 0 0.0%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 37029
Covered Lines: 15480
Line Coverage: 41.81%
Coverage Strength: 11.78 hits per line

💛 - Coveralls

Comment thread billing/invoice/invoice.go
Comment on lines +316 to +324
if customerID == "" {
return nil, 0, errors.New("customer id is required")
}
if rqlQuery == nil {
return nil, 0, fmt.Errorf("%w: query is required", ErrBadInput)
}
if len(rqlQuery.GroupBy) > 0 {
return nil, 0, fmt.Errorf("%w: group_by is not supported", ErrBadInput)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validations can go in proto. (Caution: check if rqlQuery is validated there)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

customerID and GroupBy are not present on proto level thus makes sense to keep here.

Remove the query check.

Comment thread internal/api/v1beta1connect/billing_invoice.go Outdated
Comment thread internal/store/postgres/billing_invoice_repository.go Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
internal/store/postgres/billing_invoice_repository.go (1)

314-333: ⚠️ Potential issue | 🟠 Major

Add a deterministic default order before paginating.

Line 330 paginates withSort even when the request does not specify any sort, so page boundaries are nondeterministic and clients can see duplicates/missing invoices across pages.

Suggested fix
 	withSort, err := frontierutils.AddRQLSortInQuery(withSearch, rqlQuery)
 	if err != nil {
 		return nil, 0, fmt.Errorf("%w: %w", invoice.ErrBadInput, err)
 	}
+
+	if len(rqlQuery.Sort) == 0 {
+		withSort = withSort.OrderAppend(
+			goqu.I(TABLE_BILLING_INVOICES+".created_at").Desc(),
+			goqu.I(TABLE_BILLING_INVOICES+".id").Desc(),
+		)
+	}
 
 	countQuery, countParams, err := withSearch.Select(goqu.COUNT("*")).ToSQL()

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 04d90ada-f210-4e7d-a6e5-293e01496ae8

📥 Commits

Reviewing files that changed from the base of the PR and between d42cbf1 and e8c3ce4.

📒 Files selected for processing (3)
  • billing/invoice/service.go
  • internal/api/v1beta1connect/billing_invoice.go
  • internal/store/postgres/billing_invoice_repository.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/api/v1beta1connect/billing_invoice.go

Comment thread billing/invoice/service.go Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
billing/invoice/service_search_org_invoices_test.go (1)

84-93: Prefer ErrorIs for wrapped validation errors.

For the nil-query and group_by cases, asserting only message substrings is brittle. Add assert.ErrorIs(t, err, ErrBadInput) to validate the contract and keep the message checks as secondary.

Proposed test hardening
 	_, _, err = svc.SearchOrgInvoices(context.Background(), "cust-1", true, nil)
 	assert.Error(t, err)
+	assert.ErrorIs(t, err, ErrBadInput)
 	assert.Contains(t, err.Error(), "query is required")
@@
 	_, _, err = svc.SearchOrgInvoices(context.Background(), "cust-1", true, &rql.Query{
 		GroupBy: []string{"state"},
 	})
 	assert.Error(t, err)
+	assert.ErrorIs(t, err, ErrBadInput)
 	assert.Contains(t, err.Error(), "group_by is not supported")
internal/api/v1beta1connect/billing_invoice_test.go (1)

750-754: Avoid early return before mock expectation checks.

Line 753 returns before AssertExpectations, so error-path cases can skip mock verification.

Suggested restructuring
 			got, err := handler.SearchOrgInvoices(context.Background(), tt.request)
 			if tt.wantCode != 0 {
 				assert.Error(t, err)
 				assert.Equal(t, tt.wantCode, connect.CodeOf(err))
-				return
-			}
-
-			assert.NoError(t, err)
-			if tt.assertResp != nil {
-				tt.assertResp(t, got)
+				assert.Nil(t, got)
+			} else {
+				assert.NoError(t, err)
+				if tt.assertResp != nil {
+					tt.assertResp(t, got)
+				}
 			}
 
 			mockInvoiceService.AssertExpectations(t)
 			mockCustomerService.AssertExpectations(t)

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b292a96f-57e4-4776-9c10-9c5151698a4e

📥 Commits

Reviewing files that changed from the base of the PR and between e8c3ce4 and b8d6070.

📒 Files selected for processing (2)
  • billing/invoice/service_search_org_invoices_test.go
  • internal/api/v1beta1connect/billing_invoice_test.go

Comment thread billing/invoice/service_search_org_invoices_test.go
Comment thread internal/api/v1beta1connect/billing_invoice_test.go
Comment thread internal/api/v1beta1connect/billing_invoice_test.go
Comment thread internal/store/postgres/billing_invoice_repository.go
Comment thread internal/store/postgres/billing_invoice_repository.go Outdated
Comment thread internal/store/postgres/billing_invoice_repository.go Outdated
@AmanGIT07
Copy link
Copy Markdown
Contributor

@paanSinghCoder Please add some sample request and response

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
internal/api/v1beta1connect/billing_invoice.go (1)

104-135: ⚠️ Potential issue | 🟠 Major

Validate and normalize RQL before GetByOrgID early-return paths.

Right now, invalid RQL can return 200 OK for customer.ErrNotFound, and default pagination is skipped in that branch because validation/defaulting happens later.

🛠️ Proposed fix
 func (h *ConnectHandler) SearchOrgInvoices(ctx context.Context, request *connect.Request[frontierv1beta1.SearchOrgInvoicesRequest]) (*connect.Response[frontierv1beta1.SearchOrgInvoicesResponse], error) {
 	errorLogger := NewErrorLogger()
 	queryMsg := request.Msg.GetQuery()
 	if queryMsg == nil {
 		queryMsg = &frontierv1beta1.RQLRequest{}
 	}

+	rqlQuery, err := utils.TransformProtoToRQL(queryMsg, invoice.SearchOrgInvoice{})
+	if err != nil {
+		return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("failed to read rql query: %v", err))
+	}
+	if err = rql.ValidateQuery(rqlQuery, invoice.SearchOrgInvoice{}); err != nil {
+		return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("failed to validate rql query: %v", err))
+	}
+	if rqlQuery.Limit <= 0 {
+		rqlQuery.Limit = utils.DefaultLimit
+	}
+	if rqlQuery.Offset < 0 {
+		rqlQuery.Offset = utils.DefaultOffset
+	}
+
 	cust, err := h.customerService.GetByOrgID(ctx, request.Msg.GetOrgId())
 	if err != nil {
 		if errors.Is(err, customer.ErrNotFound) {
 			return connect.NewResponse(&frontierv1beta1.SearchOrgInvoicesResponse{
 				Invoices: []*frontierv1beta1.Invoice{},
 				Pagination: &frontierv1beta1.RQLQueryPaginationResponse{
-					Offset:     queryMsg.GetOffset(),
-					Limit:      queryMsg.GetLimit(),
+					Offset:     uint32(rqlQuery.Offset),
+					Limit:      uint32(rqlQuery.Limit),
 					TotalCount: 0,
 				},
 			}), nil
 		}
@@
-	rqlQuery, err := utils.TransformProtoToRQL(queryMsg, invoice.SearchOrgInvoice{})
-	if err != nil {
-		return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("failed to read rql query: %v", err))
-	}
-	if err = rql.ValidateQuery(rqlQuery, invoice.SearchOrgInvoice{}); err != nil {
-		return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("failed to validate rql query: %v", err))
-	}

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1302b844-eedb-4f66-b9e1-83a8b3ad8be5

📥 Commits

Reviewing files that changed from the base of the PR and between b8d6070 and 9a40f21.

📒 Files selected for processing (9)
  • billing/invoice/invoice.go
  • billing/invoice/service.go
  • billing/invoice/service_search_org_invoices_test.go
  • internal/api/v1beta1connect/billing_invoice.go
  • internal/api/v1beta1connect/billing_invoice_test.go
  • internal/api/v1beta1connect/interfaces.go
  • internal/api/v1beta1connect/mocks/invoice_service.go
  • internal/store/postgres/billing_invoice_repository.go
  • pkg/server/connect_interceptors/authorization.go
✅ Files skipped from review due to trivial changes (2)
  • billing/invoice/service_search_org_invoices_test.go
  • billing/invoice/invoice.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • pkg/server/connect_interceptors/authorization.go
  • internal/store/postgres/billing_invoice_repository.go
  • internal/api/v1beta1connect/billing_invoice_test.go
  • internal/api/v1beta1connect/interfaces.go
  • billing/invoice/service.go

Comment thread billing/invoice/invoice.go Outdated
@paanSinghCoder
Copy link
Copy Markdown
Contributor Author

Rename SearchOrgInvoices to SearchOrganisationInvoices

Comment thread internal/api/v1beta1connect/billing_invoice_test.go
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants