Skip to content

feat: feature filters - #4536

Merged
GAlexIHU merged 5 commits into
mainfrom
feat/complete-feature-filters
Jun 23, 2026
Merged

feat: feature filters#4536
GAlexIHU merged 5 commits into
mainfrom
feat/complete-feature-filters

Conversation

@GAlexIHU

@GAlexIHU GAlexIHU commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Overview

  • Implements FeatureFilters for Credit balances and Transaction listing

Summary by CodeRabbit

Release Notes

  • New Features

    • Added optional feature_key filtering for customer credit balance and credit transaction listing.
    • Added feature-aware MatchFeature routing across ledger transactions, expired breakage, and customer balance calculations.
    • Added feature-filter support for funded credit activity queries.
  • Improvements

    • Updated exists/nexists filter parsing to accept explicit boolean values (including empty values) consistently.
  • Bug Fixes

    • Corrected/clarified exists=false behavior to return only unrestricted results where applicable.
  • Other

    • Credit grant creation now validates feature filters instead of rejecting them.

@GAlexIHU
GAlexIHU requested a review from a team as a code owner June 17, 2026 16:52
@GAlexIHU GAlexIHU added the release-note/feature Release note: Exciting New Features label Jun 17, 2026
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fb65ac8a-116b-4c49-9d70-52e1e7a69dbd

📥 Commits

Reviewing files that changed from the base of the PR and between 2a92f27 and fdc4743.

⛔ Files ignored due to path filters (1)
  • api/v3/openapi.yaml is excluded by !**/openapi.yaml
📒 Files selected for processing (46)
  • api/spec/packages/aip-client-javascript/src/models/schemas.ts
  • api/spec/packages/aip-client-javascript/src/models/types.ts
  • api/spec/packages/aip/src/customers/credits/operations.tsp
  • api/v3/api.gen.go
  • api/v3/filters/parse.go
  • api/v3/filters/parse_test.go
  • api/v3/handlers/customers/credits/feature_filter.go
  • api/v3/handlers/customers/credits/feature_filter_test.go
  • api/v3/handlers/customers/credits/get_balance.go
  • api/v3/handlers/customers/credits/list_transactions.go
  • api/v3/test/filters_test.go
  • openmeter/billing/charges/creditpurchase/adapter/funded_credit_activity.go
  • openmeter/billing/charges/creditpurchase/adapter/funded_credit_activity_test.go
  • openmeter/billing/charges/creditpurchase/charge_test.go
  • openmeter/billing/charges/creditpurchase/featurefilters.go
  • openmeter/billing/charges/creditpurchase/funded_credit_activity.go
  • openmeter/billing/creditgrant/errors.go
  • openmeter/billing/creditgrant/service.go
  • openmeter/ledger/account/adapter/subaccount.go
  • openmeter/ledger/breakage/adapter/query.go
  • openmeter/ledger/breakage/adapter/query_test.go
  • openmeter/ledger/breakage/adapter/record.go
  • openmeter/ledger/breakage/adapter/record_test.go
  • openmeter/ledger/breakage/breakage_impacts.go
  • openmeter/ledger/breakage/service.go
  • openmeter/ledger/breakage/types.go
  • openmeter/ledger/customerbalance/expired_loader.go
  • openmeter/ledger/customerbalance/expired_loader_test.go
  • openmeter/ledger/customerbalance/facade.go
  • openmeter/ledger/customerbalance/feature_filter.go
  • openmeter/ledger/customerbalance/funded_loader.go
  • openmeter/ledger/customerbalance/ledger_loader.go
  • openmeter/ledger/customerbalance/loaders.go
  • openmeter/ledger/customerbalance/noop.go
  • openmeter/ledger/customerbalance/service.go
  • openmeter/ledger/customerbalance/service_test.go
  • openmeter/ledger/customerbalance/testenv_test.go
  • openmeter/ledger/customerbalance/transactions.go
  • openmeter/ledger/historical/adapter/ledger.go
  • openmeter/ledger/historical/adapter/ledger_test.go
  • openmeter/ledger/historical/adapter/sumentries_query.go
  • openmeter/ledger/primitives.go
  • openmeter/ledger/routing.go
  • openmeter/ledger/routing_test.go
  • openmeter/ledger/validations_test.go
  • test/credits/creditgrant_test.go
💤 Files with no reviewable changes (2)
  • openmeter/billing/creditgrant/errors.go
  • openmeter/billing/creditgrant/service.go
🚧 Files skipped from review as they are similar to previous changes (44)
  • openmeter/ledger/customerbalance/noop.go
  • openmeter/ledger/breakage/adapter/record.go
  • openmeter/billing/charges/creditpurchase/featurefilters.go
  • openmeter/ledger/account/adapter/subaccount.go
  • openmeter/ledger/breakage/adapter/record_test.go
  • openmeter/ledger/breakage/service.go
  • openmeter/ledger/historical/adapter/sumentries_query.go
  • openmeter/billing/charges/creditpurchase/charge_test.go
  • openmeter/billing/charges/creditpurchase/funded_credit_activity.go
  • openmeter/ledger/customerbalance/funded_loader.go
  • openmeter/ledger/customerbalance/expired_loader.go
  • api/spec/packages/aip/src/customers/credits/operations.tsp
  • openmeter/ledger/routing_test.go
  • openmeter/ledger/validations_test.go
  • openmeter/ledger/customerbalance/ledger_loader.go
  • api/spec/packages/aip-client-javascript/src/models/types.ts
  • api/v3/handlers/customers/credits/feature_filter.go
  • api/v3/handlers/customers/credits/get_balance.go
  • openmeter/ledger/breakage/types.go
  • openmeter/ledger/breakage/adapter/query_test.go
  • api/v3/test/filters_test.go
  • openmeter/ledger/breakage/breakage_impacts.go
  • openmeter/ledger/historical/adapter/ledger_test.go
  • api/v3/handlers/customers/credits/feature_filter_test.go
  • api/spec/packages/aip-client-javascript/src/models/schemas.ts
  • api/v3/handlers/customers/credits/list_transactions.go
  • openmeter/ledger/customerbalance/transactions.go
  • openmeter/ledger/customerbalance/loaders.go
  • openmeter/billing/charges/creditpurchase/adapter/funded_credit_activity_test.go
  • api/v3/api.gen.go
  • openmeter/ledger/primitives.go
  • openmeter/billing/charges/creditpurchase/adapter/funded_credit_activity.go
  • openmeter/ledger/historical/adapter/ledger.go
  • test/credits/creditgrant_test.go
  • openmeter/ledger/customerbalance/feature_filter.go
  • openmeter/ledger/customerbalance/testenv_test.go
  • openmeter/ledger/routing.go
  • openmeter/ledger/breakage/adapter/query.go
  • api/v3/filters/parse_test.go
  • api/v3/filters/parse.go
  • openmeter/ledger/customerbalance/service_test.go
  • openmeter/ledger/customerbalance/service.go
  • openmeter/ledger/customerbalance/expired_loader_test.go
  • openmeter/ledger/customerbalance/facade.go

📝 Walkthrough

Walkthrough

Adds an optional feature_key filter to the GetCreditBalance and ListCreditTransactions API endpoints. The change propagates through API spec, handler parsing, a new tri-state FeatureFilter option in the customerbalance service, and all backing database adapters (historical ledger, breakage, credit-purchase). Also fixes exists/nexists filter parsing to respect the provided boolean value instead of ignoring it.

Changes

Feature-key filtering for credit balance and transactions

Layer / File(s) Summary
API spec and generated types
api/spec/packages/aip/src/customers/credits/operations.tsp, api/v3/api.gen.go, api/spec/packages/aip-client-javascript/src/models/...
TypeSpec, generated Go structs, JS schemas, and types gain optional feature_key on both credit filter shapes; Swagger payload regenerated.
exists/nexists value-based parsing fix
api/v3/filters/parse.go, api/v3/filters/parse_test.go, api/v3/test/filters_test.go
parseFilterString now reads the actual value for exists/nexists via a new parseOptionalBool helper; tests expanded for the full true/false/empty/invalid matrix.
FeatureFilters domain model and credit-grant unlock
openmeter/billing/charges/creditpurchase/featurefilters.go, openmeter/billing/charges/creditpurchase/charge_test.go, openmeter/billing/creditgrant/service.go, test/credits/creditgrant_test.go
ValidateAsFeatureFilter enforces the single-feature API contract; the temporary "unsupported" block in creditgrant service replaced by proper validation; integration test flipped to expect success.
RouteFilter.MatchFeature and ledger primitives
openmeter/ledger/primitives.go, openmeter/ledger/routing.go, openmeter/ledger/routing_test.go, openmeter/ledger/validations_test.go
RouteFilter gains MatchFeature; Route.Matches and RouteFilter.Normalize handle it; ListTransactionsInput gets a Route field with validation; ValidateFeatures exported; route-filter validation tests added.
customerbalance tri-state filter helpers and GetBalanceServiceInput
openmeter/ledger/customerbalance/feature_filter.go, openmeter/ledger/customerbalance/service.go, openmeter/ledger/customerbalance/noop.go
New feature_filter.go provides AllFeatureFilter, NewFeatureFilter, NewUnrestrictedFeatureFilter, ValidateFeatureFilter, and helper functions for tri-state semantics; GetBalanceServiceInput replaces positional GetBalance parameters and adds FeatureFilter plus route-building methods.
customerbalance service, transactions, and loaders
openmeter/ledger/customerbalance/service.go, openmeter/ledger/customerbalance/transactions.go, openmeter/ledger/customerbalance/facade.go, openmeter/ledger/customerbalance/loaders.go, openmeter/ledger/customerbalance/funded_loader.go, openmeter/ledger/customerbalance/ledger_loader.go, openmeter/ledger/customerbalance/expired_loader.go
Service and facade refactored to use GetBalanceServiceInput with feature filtering; transactions carry FeatureFilter through; all loader inputs receive and propagate feature filters; funded and expired loaders handle feature-aware activity and cursor resolution.
Historical ledger adapter
openmeter/ledger/historical/adapter/ledger.go, openmeter/ledger/historical/adapter/sumentries_query.go, openmeter/ledger/historical/adapter/ledger_test.go
ListTransactions and SumEntries build route-aware Postgres predicates including MatchFeature containment queries using pq.StringArray; supporting SQL expression helpers added; tests verify SQL generation and result filtering.
Breakage adapter expired-record route filtering
openmeter/ledger/breakage/types.go, openmeter/ledger/breakage/breakage_impacts.go, openmeter/ledger/breakage/service.go, openmeter/ledger/breakage/adapter/query.go, openmeter/ledger/breakage/adapter/record.go, openmeter/ledger/breakage/adapter/query_test.go, openmeter/ledger/breakage/adapter/record_test.go
Route field added to expired-record inputs; ValidateExpiredRouteFilter enforces mutual exclusivity; expiredRecordRoutePredicate builds an IN-subquery with array predicates; tests verify filtering by feature and currency.
CreditPurchase and sub-account adapter
openmeter/billing/charges/creditpurchase/funded_credit_activity.go, openmeter/billing/charges/creditpurchase/adapter/funded_credit_activity.go, openmeter/ledger/account/adapter/subaccount.go, openmeter/billing/charges/creditpurchase/adapter/funded_credit_activity_test.go
ListFundedCreditActivitiesInput gains FeatureFilter; adapter builds Ent predicate with pq.StringArray containment; sub-account adapter gains matchFeature helper; tests cover filtering by specific feature sets.
API handler feature_key parsing
api/v3/handlers/customers/credits/feature_filter.go, api/v3/handlers/customers/credits/get_balance.go, api/v3/handlers/customers/credits/list_transactions.go, api/v3/handlers/customers/credits/feature_filter_test.go
New feature_filter.go converts api.StringFieldFilter to creditpurchase.FeatureFilters; both credit handlers parse Filter.FeatureKey and propagate it to service inputs with 400 errors on invalid input.
Integration and service tests
openmeter/ledger/customerbalance/service_test.go, openmeter/ledger/customerbalance/expired_loader_test.go, openmeter/ledger/customerbalance/testenv_test.go
Service tests cover GetBalanceServiceInput validation, route selection, and feature-filtered balance/pending-impact scenarios; expired loader tests cover feature-filtered expired breakage and full transaction flows; test helpers extended to accept feature keys.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Handler
    participant Facade
    participant Service
    participant HistoricalLedger
    participant BreakageSvc
    participant CreditPurchaseSvc

    Client->>Handler: GET /credits/balance?filter[feature_key][eq]=api-calls
    Handler->>Handler: fromAPICustomerCreditFeatureFilter → FeatureFilter
    Handler->>Facade: GetBalances(GetBalancesInput{FeatureFilter})
    Facade->>Service: GetBalance(GetBalanceServiceInput{FeatureFilter})
    Service->>Service: featureFilterRoute → RouteFilter{MatchFeature}
    Service->>HistoricalLedger: GetBalance(bookedRoute)
    Service->>HistoricalLedger: GetBalance(advanceRoute)
    Service->>Service: getChargePendingBalanceImpacts(featureFilter)
    Service-->>Facade: ledger.Balance
    Facade-->>Handler: balance response
    Handler-->>Client: 200 OK

    Client->>Handler: GET /credits/transactions?filter[feature_key][eq]=api-calls
    Handler->>Facade: ListCreditTransactions(ListCreditTransactionsInput{FeatureFilter})
    Facade->>CreditPurchaseSvc: ListFundedCreditActivities(FeatureFilter)
    Facade->>HistoricalLedger: ListTransactions(Route{MatchFeature})
    Facade->>BreakageSvc: ListExpiredBreakageImpacts(Route{MatchFeature})
    Facade-->>Handler: merged transactions
    Handler-->>Client: 200 OK
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • openmeterio/openmeter#4052: Both PRs modify the v3 customer credit balance handler to add/interpret feature-based query filtering through the endpoint and service layer.
  • openmeterio/openmeter#4117: Both PRs touch the API v3 filtering plumbing—specifically the api/v3/filters parser that defines how deep-object StringFieldFilter operators (including exists/nexists) are decoded and represented.
  • openmeterio/openmeter#4481: This PR wires the new API filter[feature_key] semantics into credit handlers by converting it into creditpurchase.FeatureFilters, which relies on the ledger credit FeatureFilters propagation added in that PR.

Suggested labels

area/billing

Suggested reviewers

  • turip
  • tothandras
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: feature filters' directly and clearly describes the main change: implementing feature filters for credit balances and transaction listing.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/complete-feature-filters

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.

@greptile-apps

greptile-apps Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR implements optional feature_key filtering for credit balance queries and transaction listing. The feature filter is represented as a tri-state option (absent = all, Some(nil) = unrestricted only, Some(feature) = unrestricted + that feature), consistently applied across the API, service, ledger routing, breakage, and funded-credit-activity layers.

  • Adds MatchFeature to RouteFilter propagated through all DB adapters using PostgreSQL @> array-containment queries.
  • Removes the temporary "feature filters unsupported" guard from credit grant creation.
  • Extends exists/nexists filter parsing to accept explicit boolean values while preserving backwards-compatible empty-value behaviour.

Confidence Score: 5/5

Safe to merge; the feature filter logic is correctly implemented and consistently threaded across all layers with good test coverage.

The tri-state filter semantics are consistently applied from the API handler through service, routing, breakage, and funded-credit adapters. SQL predicates use correct @> array-containment. The two flagged items are a latent silent-match-all edge case unreachable through current validation, and an O(n) DB-call pattern in the funded loader that could affect high-traffic customers but does not produce incorrect results.

openmeter/ledger/customerbalance/funded_loader.go warrants a second look on the per-activity DB lookup before this hits high-volume customers.

Important Files Changed

Filename Overview
openmeter/ledger/customerbalance/funded_loader.go Adds FeatureFilter propagation and balanceCursorForFundedActivity; introduces O(n) DB calls per loaded page.
openmeter/billing/charges/creditpurchase/adapter/funded_credit_activity.go Adds @> array-contains predicate for feature filtering; silent nil return for Some([]) after normalization is a latent footgun.
openmeter/ledger/customerbalance/feature_filter.go New tri-state feature filter helpers; well-documented and correct.
openmeter/ledger/customerbalance/service.go Refactors GetBalance to GetBalanceServiceInput; correctly threads FeatureFilter through all balance computation paths.
openmeter/ledger/primitives.go Adds MatchFeature to RouteFilter and Route to ListTransactionsInput with mutual-exclusion validation.
openmeter/ledger/routing.go Extends Route.Matches for MatchFeature with unrestricted-or-contains semantics; consistent with DB @> queries.
openmeter/ledger/historical/adapter/ledger.go Adds MatchFeature SQL support; correctly conditionalizes the route JOIN on predicate presence.
openmeter/ledger/breakage/adapter/query.go New expiredRecordRouteQuery implementing MatchFeature predicates for breakage records.
api/v3/filters/parse.go Backwards-compatible extension of exists/nexists to accept explicit booleans; well-tested.
api/v3/handlers/customers/credits/feature_filter.go Translates StringFieldFilter to tri-state FeatureFilter; rejects unsupported operators cleanly.
openmeter/billing/creditgrant/service.go Removes the temporary feature-filters-unsupported guard, unblocking the feature.

Reviews (5): Last reviewed commit: "chore: review comments" | Re-trigger Greptile

Comment thread openmeter/ledger/customerbalance/feature_filter.go

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

🧹 Nitpick comments (6)
api/spec/packages/aip/src/customers/credits/operations.tsp (1)

155-160: ⚡ Quick win

Clarify the exact supported feature_key operators in the API docs.

Nice addition overall. One small contract-clarity tweak: these fields use Common.StringFieldFilter, but downstream handling only supports eq, oeq, and exists=false (while exists=true/other operators are rejected). Calling that out in the doc comments here would make the API behavior much clearer for SDK/users.

Suggested doc tweak
   /**
    * Filter credit balance by feature key. Omit to return the total portfolio value.
-   * Use `exists=false` to return only unrestricted balance.
+   * Supported operators: `eq`, `oeq`, and `exists=false`.
+   * Use `exists=false` to return only unrestricted balance.
    */
   feature_key?: Common.StringFieldFilter;
   /**
    * Filter credit transactions by feature key. Omit to return all credit
-   * transactions. Use `exists=false` to return only unrestricted credit
+   * transactions. Supported operators: `eq`, `oeq`, and `exists=false`.
+   * Use `exists=false` to return only unrestricted credit
    * transactions.
    */
   feature_key?: Common.StringFieldFilter;

As per coding guidelines, the declared API should be accurate and easy to understand for users.

Also applies to: 207-213

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/spec/packages/aip/src/customers/credits/operations.tsp` around lines 155
- 160, Update the JSDoc comments for the `feature_key` parameter field to
clarify which operators are actually supported by the downstream implementation.
Add a note specifying that only `eq`, `oeq`, and `exists=false` operators are
supported for this `Common.StringFieldFilter`, while other operators like
`exists=true` are rejected. Apply this same clarification to the similar field
at line 207-213 to ensure API documentation accurately reflects the actual
supported behavior.

Source: Coding guidelines

test/credits/creditgrant_test.go (1)

190-193: ⚡ Quick win

Use test-scoped context in this updated test.

Small tweak: switch to s.T().Context() here so cancellation/lifecycle stays tied to the test harness.

Suggested patch
 func (s *CreditGrantTestSuite) TestCreateFeatureFilteredGrant() {
-	ctx := context.Background()
+	ctx := s.T().Context()
 	ns := s.GetUniqueNamespace("creditgrant-service-feature-filters")
 	s.ProvisionDefaultTaxCodes(ctx, ns)

As per coding guidelines: "Use t.Context() in Go tests when a testing.T or testing.TB is available instead of introducing context.Background() to keep cancellation tied to the test harness."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/credits/creditgrant_test.go` around lines 190 - 193, In the
TestCreateFeatureFilteredGrant method, replace the context initialization from
context.Background() to s.T().Context() so that the test's cancellation and
lifecycle are properly tied to the test harness. Change the line that declares
ctx to use s.T().Context() instead of context.Background().

Source: Coding guidelines

openmeter/billing/charges/creditpurchase/adapter/funded_credit_activity.go (1)

136-155: 💤 Low value

Accessing features[0] relies on upstream validation ensuring exactly one feature.

The code accesses features[0] directly after normalization. This is safe because ValidateAsFeatureFilter() enforces exactly one feature per the upstream contract, but adding a defensive length check would make this more resilient to future changes in validation logic.

That said, validation happens in ListFundedCreditActivitiesInput.Validate() before this code runs, so this should be fine in practice.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openmeter/billing/charges/creditpurchase/adapter/funded_credit_activity.go`
around lines 136 - 155, In the fundedCreditActivityFeatureFilterPredicate
function, add a defensive length check on the features slice before accessing
features[0]. After normalizing features, verify that the slice has exactly one
element before attempting to access features[0] in the predicate; if the length
check fails, return an appropriate default predicate (such as nil or
dbchargecreditpurchase.FeatureFiltersIsNil()) to prevent potential index out of
bounds errors.
openmeter/ledger/historical/adapter/ledger.go (3)

609-636: ⚖️ Poor tradeoff

Consider extracting shared Postgres array predicate types to a common package.

These types (postgresArrayRouteOperator, postgresQualifiedColumn, postgresArrayRouteExpression) are duplicated in openmeter/ledger/breakage/adapter/query.go. Extracting them to a shared internal package would reduce duplication and make future maintenance easier.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openmeter/ledger/historical/adapter/ledger.go` around lines 609 - 636, The
types postgresArrayRouteOperator, postgresQualifiedColumn, and
postgresArrayRouteExpression along with their methods (Ident and Predicate) are
currently defined in openmeter/ledger/historical/adapter/ledger.go but are
duplicated in openmeter/ledger/breakage/adapter/query.go. Create a new shared
internal package (e.g., openmeter/internal/postgres or similar) and move these
type definitions and their associated methods to this common location. Then
update both the historical and breakage adapter files to import and use these
types from the new shared package instead of maintaining duplicate definitions.

420-429: 💤 Low value

Potential duplicate currency predicate when both currency and route.Currency are set.

If both the currency parameter and route.Currency are populated, this function will append two separate currency equality predicates. While they'd both match the same rows if identical, it adds redundant conditions to the query. Consider checking if they're the same value before appending:

🔧 Suggested fix
 func listTransactionsRoutePredicates(currency *currencyx.Code, route ledger.RouteFilter) []predicate.LedgerSubAccountRoute {
 	routePredicates := make([]predicate.LedgerSubAccountRoute, 0, 3)

 	if currency != nil {
 		routePredicates = append(routePredicates, ledgersubaccountroutedb.Currency(string(*currency)))
 	}

-	if route.Currency != "" {
+	if route.Currency != "" && (currency == nil || route.Currency != *currency) {
 		routePredicates = append(routePredicates, ledgersubaccountroutedb.Currency(string(route.Currency)))
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openmeter/ledger/historical/adapter/ledger.go` around lines 420 - 429, The
listTransactionsRoutePredicates function appends duplicate currency predicates
when both the currency parameter and route.Currency field are populated,
creating redundant query conditions. Remove the second currency predicate append
that checks route.Currency, and instead consolidate the logic so that only one
currency predicate is added. Either prioritize the currency parameter if it's
not nil, or use route.Currency as a fallback when currency is nil, ensuring only
a single ledgersubaccountroutedb.Currency predicate is appended to the
routePredicates slice.

564-573: 💤 Low value

Same potential duplicate currency predicate issue in the scoped route selector.

This helper has the same pattern where both currency and route.Currency could add separate predicates for the same filter. Keep it consistent with whatever approach you take in listTransactionsRoutePredicates.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openmeter/ledger/historical/adapter/ledger.go` around lines 564 - 573, The
scopedRouteSelectorPredicates function has duplicate currency predicate logic
where both the currency parameter check and the route.Currency check
independently add predicates for the same FieldCurrency field. Refactor the
function to consolidate these two conditions into a single logic block that adds
only one currency predicate, prioritizing the appropriate source (either
currency parameter or route.Currency field) to match the approach used in
listTransactionsRoutePredicates. Remove the separate if blocks for currency and
route.Currency, and replace them with unified logic that prevents duplicate
predicates.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@api/v3/handlers/customers/credits/feature_filter_test.go`:
- Around line 15-61: The TestFromAPICustomerCreditFeatureFilter table test is
missing coverage for unsupported operators that are handled by
unsupportedCustomerCreditFeatureKeyOperator. Add six new test cases to the tests
slice to cover the neq, ocontains, gt, gte, lt, and lte operators in the
api.StringFieldFilter. Each test case should set the corresponding field on the
StringFieldFilter to an appropriate test value and expect wantErr to be true,
ensuring that all unsupported operator branches are tested and
regression-protected.

In `@openmeter/ledger/breakage/adapter/record_test.go`:
- Around line 39-87: Add a new test case to the tests slice that covers the
scenario where MatchFeature is set but Route.Currency is left empty or unset,
allowing the currency value to come from the top-level context. Create this test
case similar to the existing "feature match route includes unrestricted and
containing features" test case but ensure Route.Currency is not populated in the
ledger.RouteFilter struct, then define the appropriate want slice with the
expected matching records based on feature matching behavior.

In `@openmeter/ledger/customerbalance/service_test.go`:
- Around line 87-95: The test case "after and as of both set" is using an empty
ledger.TransactionCursor{} on line 92, which causes an earlier validation error
and masks the mutual-exclusion check between After and AsOf that the test is
meant to verify. Replace the empty ledger.TransactionCursor{} with a properly
initialized valid cursor (e.g., one that would normally pass cursor validation)
so the test can isolate and validate the specific rule that After and AsOf
cannot both be set simultaneously.

---

Nitpick comments:
In `@api/spec/packages/aip/src/customers/credits/operations.tsp`:
- Around line 155-160: Update the JSDoc comments for the `feature_key` parameter
field to clarify which operators are actually supported by the downstream
implementation. Add a note specifying that only `eq`, `oeq`, and `exists=false`
operators are supported for this `Common.StringFieldFilter`, while other
operators like `exists=true` are rejected. Apply this same clarification to the
similar field at line 207-213 to ensure API documentation accurately reflects
the actual supported behavior.

In `@openmeter/billing/charges/creditpurchase/adapter/funded_credit_activity.go`:
- Around line 136-155: In the fundedCreditActivityFeatureFilterPredicate
function, add a defensive length check on the features slice before accessing
features[0]. After normalizing features, verify that the slice has exactly one
element before attempting to access features[0] in the predicate; if the length
check fails, return an appropriate default predicate (such as nil or
dbchargecreditpurchase.FeatureFiltersIsNil()) to prevent potential index out of
bounds errors.

In `@openmeter/ledger/historical/adapter/ledger.go`:
- Around line 609-636: The types postgresArrayRouteOperator,
postgresQualifiedColumn, and postgresArrayRouteExpression along with their
methods (Ident and Predicate) are currently defined in
openmeter/ledger/historical/adapter/ledger.go but are duplicated in
openmeter/ledger/breakage/adapter/query.go. Create a new shared internal package
(e.g., openmeter/internal/postgres or similar) and move these type definitions
and their associated methods to this common location. Then update both the
historical and breakage adapter files to import and use these types from the new
shared package instead of maintaining duplicate definitions.
- Around line 420-429: The listTransactionsRoutePredicates function appends
duplicate currency predicates when both the currency parameter and
route.Currency field are populated, creating redundant query conditions. Remove
the second currency predicate append that checks route.Currency, and instead
consolidate the logic so that only one currency predicate is added. Either
prioritize the currency parameter if it's not nil, or use route.Currency as a
fallback when currency is nil, ensuring only a single
ledgersubaccountroutedb.Currency predicate is appended to the routePredicates
slice.
- Around line 564-573: The scopedRouteSelectorPredicates function has duplicate
currency predicate logic where both the currency parameter check and the
route.Currency check independently add predicates for the same FieldCurrency
field. Refactor the function to consolidate these two conditions into a single
logic block that adds only one currency predicate, prioritizing the appropriate
source (either currency parameter or route.Currency field) to match the approach
used in listTransactionsRoutePredicates. Remove the separate if blocks for
currency and route.Currency, and replace them with unified logic that prevents
duplicate predicates.

In `@test/credits/creditgrant_test.go`:
- Around line 190-193: In the TestCreateFeatureFilteredGrant method, replace the
context initialization from context.Background() to s.T().Context() so that the
test's cancellation and lifecycle are properly tied to the test harness. Change
the line that declares ctx to use s.T().Context() instead of
context.Background().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 167a43ca-5865-4e80-8c3d-d7d8f0cbb189

📥 Commits

Reviewing files that changed from the base of the PR and between 2ab8cd0 and 8ae6eea.

⛔ Files ignored due to path filters (1)
  • api/v3/openapi.yaml is excluded by !**/openapi.yaml
📒 Files selected for processing (45)
  • api/spec/packages/aip-client-javascript/src/models/schemas.ts
  • api/spec/packages/aip-client-javascript/src/models/types.ts
  • api/spec/packages/aip/src/customers/credits/operations.tsp
  • api/v3/api.gen.go
  • api/v3/filters/parse.go
  • api/v3/filters/parse_test.go
  • api/v3/handlers/customers/credits/feature_filter.go
  • api/v3/handlers/customers/credits/feature_filter_test.go
  • api/v3/handlers/customers/credits/get_balance.go
  • api/v3/handlers/customers/credits/list_transactions.go
  • api/v3/test/filters_test.go
  • openmeter/billing/charges/creditpurchase/adapter/funded_credit_activity.go
  • openmeter/billing/charges/creditpurchase/adapter/funded_credit_activity_test.go
  • openmeter/billing/charges/creditpurchase/charge_test.go
  • openmeter/billing/charges/creditpurchase/featurefilters.go
  • openmeter/billing/charges/creditpurchase/funded_credit_activity.go
  • openmeter/billing/creditgrant/errors.go
  • openmeter/billing/creditgrant/service.go
  • openmeter/ledger/account/adapter/subaccount.go
  • openmeter/ledger/breakage/adapter/query.go
  • openmeter/ledger/breakage/adapter/query_test.go
  • openmeter/ledger/breakage/adapter/record.go
  • openmeter/ledger/breakage/adapter/record_test.go
  • openmeter/ledger/breakage/breakage_impacts.go
  • openmeter/ledger/breakage/service.go
  • openmeter/ledger/breakage/types.go
  • openmeter/ledger/customerbalance/expired_loader.go
  • openmeter/ledger/customerbalance/expired_loader_test.go
  • openmeter/ledger/customerbalance/facade.go
  • openmeter/ledger/customerbalance/feature_filter.go
  • openmeter/ledger/customerbalance/funded_loader.go
  • openmeter/ledger/customerbalance/ledger_loader.go
  • openmeter/ledger/customerbalance/loaders.go
  • openmeter/ledger/customerbalance/noop.go
  • openmeter/ledger/customerbalance/service.go
  • openmeter/ledger/customerbalance/service_test.go
  • openmeter/ledger/customerbalance/testenv_test.go
  • openmeter/ledger/customerbalance/transactions.go
  • openmeter/ledger/historical/adapter/ledger.go
  • openmeter/ledger/historical/adapter/ledger_test.go
  • openmeter/ledger/historical/adapter/sumentries_query.go
  • openmeter/ledger/primitives.go
  • openmeter/ledger/routing.go
  • openmeter/ledger/routing_test.go
  • test/credits/creditgrant_test.go
💤 Files with no reviewable changes (2)
  • openmeter/billing/creditgrant/service.go
  • openmeter/billing/creditgrant/errors.go

Comment thread api/v3/handlers/customers/credits/feature_filter_test.go
Comment thread openmeter/ledger/breakage/adapter/record_test.go
Comment thread openmeter/ledger/customerbalance/service_test.go
@GAlexIHU
GAlexIHU force-pushed the feat/complete-feature-filters branch from 8ae6eea to 6cea320 Compare June 17, 2026 17:16

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (4)
openmeter/ledger/breakage/adapter/query.go (2)

85-97: ⚡ Quick win

Consider adding a comment explaining MatchFeature semantics.

The OR logic here implements "match unrestricted routes (NULL features) or routes containing this single feature key." While correct, the business intent isn't immediately clear from the code. A brief comment would help future readers understand why we OR with NULL.

📝 Suggested comment
 if q.Route.MatchFeature != "" {
+  // MatchFeature matches unrestricted routes (NULL features) or routes containing the specified feature.
   predicates = append(predicates, sql.Or(
     sql.IsNull(routeColumn(ledgersubaccountroutedb.FieldFeatures)),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openmeter/ledger/breakage/adapter/query.go` around lines 85 - 97, Add a
comment above the if statement that checks q.Route.MatchFeature to explain the
OR logic semantics. The comment should clarify that the condition matches both
unrestricted routes (where FieldFeatures is NULL) and routes that contain the
specified MatchFeature value. This will help future readers understand why we OR
the IsNull check with the postgresArrayRouteExpression containing the feature
key lookup.

68-83: ⚡ Quick win

Consider documenting the array equality sorting contract.

The exact-match filter relies on Postgres array equality (=), which is order-sensitive. The code correctly sorts features here, but this creates an implicit contract that stored route features must also be canonically sorted. A brief comment noting this dependency would help future maintainers understand why the sorting step is essential.

📝 Suggested comment
 if q.Route.Features.IsPresent() {
   features, _ := q.Route.Features.Get()
+  // Sort features for canonical ordering required by Postgres array equality.
+  // Stored route features must also be sorted for exact-match semantics.
   features = ledger.SortedFeatures(features)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openmeter/ledger/breakage/adapter/query.go` around lines 68 - 83, The code
sorts features using ledger.SortedFeatures() before constructing a
postgresArrayRouteExpression with postgresArrayRouteOperatorEqual, but this
critical ordering dependency is not documented. Add a comment above the sorting
step explaining that Postgres array equality is order-sensitive and therefore
this sorting creates an implicit contract requiring both the query features and
stored route features to be canonically sorted in the same order. This will help
future maintainers understand why the sorting is essential for correctness.
openmeter/ledger/customerbalance/funded_loader.go (2)

95-95: 💤 Low value

Error message could include match criteria.

Consider including the currency and amount in this error message to make debugging easier when this condition occurs.

💬 Suggested improvement
-	return nil, fmt.Errorf("funded credit transaction group %s has no matching customer FBO transaction", activity.TransactionGroupID)
+	return nil, fmt.Errorf("funded credit transaction group %s has no matching customer FBO transaction with currency %s and amount %s", activity.TransactionGroupID, activity.Currency, activity.Amount)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openmeter/ledger/customerbalance/funded_loader.go` at line 95, The error
message in the funded_loader.go file that returns an error about "funded credit
transaction group has no matching customer FBO transaction" only includes the
transaction group ID. Enhance this error message by adding the currency and
amount information to the fmt.Errorf call to provide more context for debugging.
Include the relevant currency and amount values from the activity object or its
associated transaction data in the error message string.

66-96: ⚖️ Poor tradeoff

Nice cursor resolution logic, with a minor optimization opportunity.

The helper correctly handles the three scenarios (empty TransactionGroupID, matching transaction found, no match). The scanning logic that continues on non-FBO transactions (lines 85-87) is exactly right.

One thing to note: this creates an N+1 query pattern—one GetTransactionGroup call per funded activity. Since the Load method is paginated (line 24), you're typically making 20–50 queries per page, which is probably fine for most use cases. If you ever see performance issues with large page sizes, batching the transaction group lookups could be a nice optimization, but it's definitely not blocking anything now.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openmeter/ledger/customerbalance/funded_loader.go` around lines 66 - 96, The
balanceCursorForFundedActivity method calls GetTransactionGroup once per funded
activity, creating an N+1 query pattern. To optimize this, consider batching the
transaction group lookups when processing multiple funded activities in the Load
method. Instead of calling GetTransactionGroup individually for each activity
through balanceCursorForFundedActivity, collect all the unique
TransactionGroupIDs first, fetch them in a single batch operation (if the Ledger
service supports batch retrieval), and then match the results to the activities.
This optimization becomes important for larger page sizes and can be implemented
when performance becomes a concern.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@openmeter/ledger/breakage/adapter/query.go`:
- Around line 85-97: Add a comment above the if statement that checks
q.Route.MatchFeature to explain the OR logic semantics. The comment should
clarify that the condition matches both unrestricted routes (where FieldFeatures
is NULL) and routes that contain the specified MatchFeature value. This will
help future readers understand why we OR the IsNull check with the
postgresArrayRouteExpression containing the feature key lookup.
- Around line 68-83: The code sorts features using ledger.SortedFeatures()
before constructing a postgresArrayRouteExpression with
postgresArrayRouteOperatorEqual, but this critical ordering dependency is not
documented. Add a comment above the sorting step explaining that Postgres array
equality is order-sensitive and therefore this sorting creates an implicit
contract requiring both the query features and stored route features to be
canonically sorted in the same order. This will help future maintainers
understand why the sorting is essential for correctness.

In `@openmeter/ledger/customerbalance/funded_loader.go`:
- Line 95: The error message in the funded_loader.go file that returns an error
about "funded credit transaction group has no matching customer FBO transaction"
only includes the transaction group ID. Enhance this error message by adding the
currency and amount information to the fmt.Errorf call to provide more context
for debugging. Include the relevant currency and amount values from the activity
object or its associated transaction data in the error message string.
- Around line 66-96: The balanceCursorForFundedActivity method calls
GetTransactionGroup once per funded activity, creating an N+1 query pattern. To
optimize this, consider batching the transaction group lookups when processing
multiple funded activities in the Load method. Instead of calling
GetTransactionGroup individually for each activity through
balanceCursorForFundedActivity, collect all the unique TransactionGroupIDs
first, fetch them in a single batch operation (if the Ledger service supports
batch retrieval), and then match the results to the activities. This
optimization becomes important for larger page sizes and can be implemented when
performance becomes a concern.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7565c255-b58a-4a66-8d5a-fa98d04545ab

📥 Commits

Reviewing files that changed from the base of the PR and between 8ae6eea and 6cea320.

⛔ Files ignored due to path filters (1)
  • api/v3/openapi.yaml is excluded by !**/openapi.yaml
📒 Files selected for processing (46)
  • api/spec/packages/aip-client-javascript/src/models/schemas.ts
  • api/spec/packages/aip-client-javascript/src/models/types.ts
  • api/spec/packages/aip/src/customers/credits/operations.tsp
  • api/v3/api.gen.go
  • api/v3/filters/parse.go
  • api/v3/filters/parse_test.go
  • api/v3/handlers/customers/credits/feature_filter.go
  • api/v3/handlers/customers/credits/feature_filter_test.go
  • api/v3/handlers/customers/credits/get_balance.go
  • api/v3/handlers/customers/credits/list_transactions.go
  • api/v3/test/filters_test.go
  • openmeter/billing/charges/creditpurchase/adapter/funded_credit_activity.go
  • openmeter/billing/charges/creditpurchase/adapter/funded_credit_activity_test.go
  • openmeter/billing/charges/creditpurchase/charge_test.go
  • openmeter/billing/charges/creditpurchase/featurefilters.go
  • openmeter/billing/charges/creditpurchase/funded_credit_activity.go
  • openmeter/billing/creditgrant/errors.go
  • openmeter/billing/creditgrant/service.go
  • openmeter/ledger/account/adapter/subaccount.go
  • openmeter/ledger/breakage/adapter/query.go
  • openmeter/ledger/breakage/adapter/query_test.go
  • openmeter/ledger/breakage/adapter/record.go
  • openmeter/ledger/breakage/adapter/record_test.go
  • openmeter/ledger/breakage/breakage_impacts.go
  • openmeter/ledger/breakage/service.go
  • openmeter/ledger/breakage/types.go
  • openmeter/ledger/customerbalance/expired_loader.go
  • openmeter/ledger/customerbalance/expired_loader_test.go
  • openmeter/ledger/customerbalance/facade.go
  • openmeter/ledger/customerbalance/feature_filter.go
  • openmeter/ledger/customerbalance/funded_loader.go
  • openmeter/ledger/customerbalance/ledger_loader.go
  • openmeter/ledger/customerbalance/loaders.go
  • openmeter/ledger/customerbalance/noop.go
  • openmeter/ledger/customerbalance/service.go
  • openmeter/ledger/customerbalance/service_test.go
  • openmeter/ledger/customerbalance/testenv_test.go
  • openmeter/ledger/customerbalance/transactions.go
  • openmeter/ledger/historical/adapter/ledger.go
  • openmeter/ledger/historical/adapter/ledger_test.go
  • openmeter/ledger/historical/adapter/sumentries_query.go
  • openmeter/ledger/primitives.go
  • openmeter/ledger/routing.go
  • openmeter/ledger/routing_test.go
  • openmeter/ledger/validations_test.go
  • test/credits/creditgrant_test.go
💤 Files with no reviewable changes (3)
  • openmeter/billing/creditgrant/service.go
  • openmeter/billing/creditgrant/errors.go
  • api/v3/api.gen.go
🚧 Files skipped from review as they are similar to previous changes (39)
  • openmeter/ledger/breakage/adapter/record.go
  • openmeter/ledger/customerbalance/ledger_loader.go
  • openmeter/ledger/historical/adapter/sumentries_query.go
  • openmeter/ledger/breakage/service.go
  • openmeter/ledger/breakage/types.go
  • api/v3/filters/parse_test.go
  • api/v3/handlers/customers/credits/get_balance.go
  • api/spec/packages/aip/src/customers/credits/operations.tsp
  • openmeter/ledger/account/adapter/subaccount.go
  • openmeter/ledger/routing_test.go
  • api/spec/packages/aip-client-javascript/src/models/schemas.ts
  • openmeter/ledger/breakage/adapter/query_test.go
  • openmeter/ledger/customerbalance/noop.go
  • api/v3/handlers/customers/credits/list_transactions.go
  • openmeter/ledger/customerbalance/expired_loader.go
  • api/v3/filters/parse.go
  • openmeter/ledger/breakage/breakage_impacts.go
  • api/v3/handlers/customers/credits/feature_filter_test.go
  • api/v3/test/filters_test.go
  • openmeter/billing/charges/creditpurchase/funded_credit_activity.go
  • openmeter/billing/charges/creditpurchase/charge_test.go
  • openmeter/billing/charges/creditpurchase/featurefilters.go
  • openmeter/ledger/primitives.go
  • openmeter/ledger/routing.go
  • openmeter/billing/charges/creditpurchase/adapter/funded_credit_activity.go
  • openmeter/ledger/customerbalance/loaders.go
  • openmeter/ledger/customerbalance/feature_filter.go
  • api/spec/packages/aip-client-javascript/src/models/types.ts
  • openmeter/billing/charges/creditpurchase/adapter/funded_credit_activity_test.go
  • openmeter/ledger/customerbalance/facade.go
  • openmeter/ledger/breakage/adapter/record_test.go
  • api/v3/handlers/customers/credits/feature_filter.go
  • openmeter/ledger/customerbalance/expired_loader_test.go
  • openmeter/ledger/customerbalance/service.go
  • openmeter/ledger/customerbalance/testenv_test.go
  • openmeter/ledger/customerbalance/service_test.go
  • test/credits/creditgrant_test.go
  • openmeter/ledger/historical/adapter/ledger_test.go
  • openmeter/ledger/historical/adapter/ledger.go

@GAlexIHU
GAlexIHU force-pushed the feat/complete-feature-filters branch from 6cea320 to d4a411a Compare June 17, 2026 17:34
turip
turip previously approved these changes Jun 19, 2026
// ValidateAsFeatureFilter validates the singular customer-facing filter form.
// Credit routes may be restricted to multiple features, but a spendability
// query can only ask for one feature at a time.
func (f FeatureFilters) ValidateAsFeatureFilter() error {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

ValidateAsSingularSpendabilityFilter might be a better name

@GAlexIHU
GAlexIHU force-pushed the feat/complete-feature-filters branch from 2a92f27 to fdc4743 Compare June 22, 2026 11:34
@GAlexIHU
GAlexIHU merged commit ffb2523 into main Jun 23, 2026
27 of 28 checks passed
@GAlexIHU
GAlexIHU deleted the feat/complete-feature-filters branch June 23, 2026 10:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-note/feature Release note: Exciting New Features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants