Skip to content

feat(plans): add key, name, status, currency filters and sort option#4221

Merged
borosr merged 5 commits intomainfrom
feat/productcatalog-plans-filter
Apr 27, 2026
Merged

feat(plans): add key, name, status, currency filters and sort option#4221
borosr merged 5 commits intomainfrom
feat/productcatalog-plans-filter

Conversation

@borosr
Copy link
Copy Markdown
Collaborator

@borosr borosr commented Apr 24, 2026

Overview

Extend the GET /api/v3/openmeter/plans endpoint with a filter[name], filter[key], filter[status] and filter[currency] query params, also implement sort query param.

Notes for review

Testing guide:

  1. Create plan(s)
curl --request POST \
  --url http://localhost:8888/api/v3/openmeter/plans \
  --header 'Content-Type: application/json' \
  --data '{
  "key": "smokestarter",
  "name": "Starter Plan",
  "currency": "HUF",
  "billing_cadence": "P1M",
  "description": "Entry-level USD plan for smoke testing",
  "phases": [
    {
      "key": "default",
      "name": "Default Phase",
      "rate_cards": []
    }
  ]
}'
  1. List the plans (sort)
curl --request GET \
  --url 'http://localhost:8888/api/v3/openmeter/plans?sort=key%20asc'
  1. List the plans (name and key)
curl --request GET \
  --url 'http://localhost:8888/api/v3/openmeter/plans?filter%5Bname%5D%5Bcontains%5D=Hello&filter%5Bkey%5D%5Beq%5D=meterid1'

The response should look like this:

{
	"data": [
		{
			"billing_cadence": "P1M",
			"created_at": "2026-04-24T09:30:43.186863+02:00",
			"currency": "USD",
			"description": "Entry-level USD plan for smoke testing",
			"id": "01KPZ6BTBJP4TAE4PH29D9KDW7",
			"key": "smokestarter",
			"name": "Starter Plan",
			"phases": [
				{
					"key": "default",
					"name": "Default Phase",
					"rate_cards": []
				}
			],
			"pro_rating_enabled": true,
			"status": "draft",
			"updated_at": "2026-04-24T09:30:43.186865+02:00",
			"validation_errors": [
				{
					"code": "plan_phase_has_no_rate_cards",
					"field": "$.phases[?(@.key=='default')].rateCards",
					"message": "plan phase must have at least one rate card"
				}
			],
			"version": 1
		},
		{
			"billing_cadence": "P1M",
			"created_at": "2026-04-24T09:32:04.998109+02:00",
			"currency": "EUR",
			"description": "Entry-level USD plan for smoke testing",
			"id": "01KPZ6EA860P45GT8R1P7BFCAF",
			"key": "smokestarter2",
			"name": "Starter Plan 2",
			"phases": [
				{
					"key": "default",
					"name": "Default Phase",
					"rate_cards": []
				}
			],
			"pro_rating_enabled": true,
			"status": "draft",
			"updated_at": "2026-04-24T09:32:04.998111+02:00",
			"validation_errors": [
				{
					"code": "plan_phase_has_no_rate_cards",
					"field": "$.phases[?(@.key=='default')].rateCards",
					"message": "plan phase must have at least one rate card"
				}
			],
			"version": 1
		},
		{
			"billing_cadence": "P1M",
			"created_at": "2026-04-24T09:32:25.521558+02:00",
			"currency": "HUF",
			"description": "Entry-level USD plan for smoke testing",
			"id": "01KPZ6EY9H7SBDDH4JY4QRX7EZ",
			"key": "smokestarter3",
			"name": "Starter Plan 3",
			"phases": [
				{
					"key": "default",
					"name": "Default Phase",
					"rate_cards": []
				}
			],
			"pro_rating_enabled": true,
			"status": "draft",
			"updated_at": "2026-04-24T09:32:25.52156+02:00",
			"validation_errors": [
				{
					"code": "plan_phase_has_no_rate_cards",
					"field": "$.phases[?(@.key=='default')].rateCards",
					"message": "plan phase must have at least one rate card"
				}
			],
			"version": 1
		}
	],
	"meta": {
		"page": {
			"size": 20,
			"number": 1,
			"total": 3
		}
	}
}

Summary by CodeRabbit

  • New Features

    • Plans list endpoint supports optional sorting and deep-object filtering by key, name, status, and currency.
  • Bug Fixes

    • Corrected ordering by plan key and improved validation to return field-scoped 400 errors for malformed sort/filter inputs.
  • Documentation

    • Updated API filter operator matrix to include a new ULID filter type and reflect accepted operators.
  • Tests

    • Added tests validating list filtering across key, name, and currency combinations.

@borosr borosr requested a review from a team as a code owner April 24, 2026 09:26
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 24, 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 client-controlled sorting and deepObject string filters for listPlans across spec, generated bindings, HTTP handler, adapter, and service layers; introduces ListPlansParamsFilter (key, name, status, currency) and documents a new FilterULID operator in API filters docs.

Changes

Cohort / File(s) Summary
API Docs & Spec
.agents/skills/api-filters/SKILL.md, api/spec/packages/aip/src/productcatalog/operations.tsp
Documented new FilterULID operator and operators matrix; added optional sort and deep filter params to listPlans and introduced ListPlansParamsFilter model (key, name, status, currency).
Generated API Bindings
api/v3/api.gen.go
Added ListPlansParamsFilter type; extended ListPlansParams with Sort and Filter; updated handler binding to parse/validate sort and deep-object filter query params and return format errors.
HTTP Handler
api/v3/handlers/plans/list.go, api/v3/handlers/plans/convert.go
Parses and validates filter[...] fields (key, name, currency, status) and sort, maps them into request object, converts status filters, and returns field-scoped BadRequest errors on parse failures.
Data Adapter
openmeter/productcatalog/plan/adapter/plan.go
Applies parsed Key, Name, Currency predicates via filter.ApplyToQuery; fixes ordering to sort by plan key (not version).
Service Layer
openmeter/productcatalog/plan/service.go
Adds Key, Name, Currency filter fields to ListPlansInput; adds OrderBy.Values() and OrderBy.Validate(); ListPlansInput.Validate() now validates filters and ordering and aggregates errors.
Tests
openmeter/productcatalog/plan/service/service_test.go
New test TestListPlansFilters verifying filtering across operators (Eq, Ne, Contains, In) for key, name, currency, and combined filter scenarios.

Sequence Diagram

sequenceDiagram
    participant Client as Client
    participant Handler as API Handler
    participant Service as Plan Service
    participant Adapter as Data Adapter
    participant DB as Database

    Client->>Handler: GET /openmeter/plans?sort=...&filter[key]=...
    Handler->>Handler: Parse sort & deepObject filter params
    Handler->>Handler: Validate formats (field-scoped errors on failure)
    Handler->>Service: ListPlans(ListPlansInput{Sort, Filter...})
    Service->>Service: Validate filters & ordering
    Service->>Adapter: buildQuery(filters, ordering)
    Adapter->>Adapter: Apply filter predicates (Key, Name, Currency, Status)
    Adapter->>Adapter: Apply order by clause
    Adapter->>DB: Execute query
    DB-->>Adapter: Results
    Adapter-->>Service: Plans
    Service-->>Handler: Plans
    Handler-->>Client: 200 OK (sorted & filtered results)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

release-note/feature

Suggested reviewers

  • gergely-kurucz-konghq
  • tothandras
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 clearly and specifically summarizes the main changes: adding filter parameters (key, name, status, currency) and a sort option to the plans endpoint.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/productcatalog-plans-filter

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: 4

🧹 Nitpick comments (3)
openmeter/productcatalog/plan/service/service_test.go (1)

826-1057: Great filter coverage — consider rounding out with sort and status cases too.

The matrix here is really nice: four plans with intentionally overlapping substrings (er appearing in both alpha-starter and gamma-enterprise 👌), and the combined AND cases give good confidence that filters compose correctly.

That said, the PR title mentions key, name, status, currency filters and sort — and this test covers key/name/currency filtering, but not:

  • sort (especially the OrderByKey bug fix on the adapter side — a regression test would lock that in so we don't silently slip back to version-ordering again).
  • filter[status] — the params.Status branch in ListPlans has non-trivial effective_from/effective_to logic across Active / Draft / Scheduled / Archived / Invalid.

Since all four fixture plans are drafts here, a small follow-up publishing one or two of them and then exercising status + sort would nicely close the loop.

As per coding guidelines: "Make sure the tests are comprehensive and cover the changes."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/productcatalog/plan/service/service_test.go` around lines 826 -
1057, Add test cases to TestListPlansFilters that exercise sorting and status
filtering: extend the fixture by publishing one or two of the draft plans (use
the same env.Plan.* helpers that created plans, e.g. call the publish method on
the created plan keys) then add tcs entries that set plan.ListPlansInput.Sort to
OrderByKey (and reverse) to assert stable key ordering and a regression for the
adapter's OrderByKey behavior, and add tcs that set the params.Status (the
status field on plan.ListPlansInput / params.Status branch used in ListPlans) to
Active/Draft/Scheduled/Archived to verify effective_from/effective_to semantics;
run each case through env.Plan.ListPlans and assert expected key lists.
api/v3/handlers/plans/list.go (1)

95-104: sort field isn't validated at the handler, only downstream.

Works fine in practice because ListPlansInput.Validate() rejects unknown OrderBy values — just flagging that the 400 returned for a bogus sort field will surface as a generic validation error rather than a sort-scoped InvalidParameter like the filter errors above. If you want consistent error shape for clients, validate plan.OrderBy(sort.Field) here and wrap it as a sort-field InvalidParameters entry. Totally optional.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/v3/handlers/plans/list.go` around lines 95 - 104, The handler currently
parses params.Sort via request.ParseSortBy but doesn't validate that the
resulting plan.OrderBy value is a known enum, deferring that to
ListPlansInput.Validate; update the sort handling in the handler (the block
using params.Sort, request.ParseSortBy, and setting req.OrderBy) to validate
plan.OrderBy(sort.Field) immediately and, on invalid value, return
apierrors.NewBadRequestError with apierrors.InvalidParameters containing a
single entry with Field: "sort", Reason set to the validation error message, and
Source: apierrors.InvalidParamSourceQuery so clients get a sort-scoped
InvalidParameter instead of a generic downstream validation error. Ensure
req.OrderBy only gets set after this validation succeeds.
openmeter/productcatalog/plan/service.go (1)

38-54: Tiny nit: Values() allocates on every call.

Not a hot path, but OrderBy.Values() rebuilds the slice each time Validate() runs. A package-level var orderByValues = []OrderBy{...} (or a map[OrderBy]struct{} for O(1) lookup) would be a touch cleaner, and Values() can just return it. Totally optional.

♻️ Tiny refactor sketch
-func (f OrderBy) Values() []OrderBy {
-    return []OrderBy{
-        OrderByID,
-        OrderByKey,
-        OrderByVersion,
-        OrderByCreatedAt,
-        OrderByUpdatedAt,
-    }
-}
+var orderByValues = []OrderBy{
+    OrderByID,
+    OrderByKey,
+    OrderByVersion,
+    OrderByCreatedAt,
+    OrderByUpdatedAt,
+}
+
+func (f OrderBy) Values() []OrderBy { return orderByValues }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/productcatalog/plan/service.go` around lines 38 - 54, The Values()
method currently constructs a new slice every call; create a package-level
variable (e.g., orderByValues := []OrderBy{OrderByID, OrderByKey,
OrderByVersion, OrderByCreatedAt, OrderByUpdatedAt}) or a lookup map (e.g.,
orderByLookup := map[OrderBy]struct{}{}) and change OrderBy.Values() to return
that variable (or remove Values and reference the lookup directly), then update
OrderBy.Validate() to check membership against the package-level slice with
slices.Contains(orderByValues, f) or against the map with a simple presence
check to avoid repeated allocations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/spec/packages/aip/src/productcatalog/operations.tsp`:
- Around line 46-55: The JSDoc for the sort query is missing the `id` attribute;
update the comment block above the `sort?: Common.SortQuery` declaration to
include `id` in the supported sort attributes so the spec matches
OrderBy.Values() in openmeter/productcatalog/plan/service.go (or alternatively
remove `id` from OrderBy.Values() if you prefer implementation-first parity) —
locate the comment near the `@query(#{ name: "sort" }) sort?: Common.SortQuery`
declaration and add `- 'id'` to the list.
- Around line 27-28: The status field is currently typed as
Common.StringFieldFilter but only exact equality operators are supported in the
handler (see the plans list handler logic that checks eq/oeq), so change the
type to the closed/strict filter to avoid implying unsupported operators:
replace status?: Common.StringFieldFilter with status?:
Common.StringFieldFilterExact (or the equivalent exact-only type used elsewhere,
e.g., charges), and update any validation or docs to only accept eq/oeq so the
type matches the actual handler behavior in the list handler.

In `@api/v3/api.gen.go`:
- Around line 5303-5310: The comment for the Sort field in the API spec repeats
the `key` sort attribute; update the source API spec (the comment/description
for the plans endpoint that documents supported sort attributes) to remove the
duplicate `key` entry, then regenerate the generated file so the docblock above
the Sort field (Sort *SortQuery `form:"sort,omitempty" json:"sort,omitempty"`)
no longer lists `key` twice.

In `@openmeter/productcatalog/plan/service.go`:
- Around line 101-108: Update the field comments for Key, Name, and Currency to
accurately reflect all operators that FilterString supports or explicitly state
which subset the plan adapter honors: mention
gt/gte/lt/lte/contains/ocontains/eq/neq/oeq/$exists (or list the exact supported
subset), and ensure the wording matches how the plan adapter and list.go status
filter handle these operators so callers aren't misled about silently dropped
ops; locate the Key/Name/Currency fields and adjust their docstrings
accordingly.

---

Nitpick comments:
In `@api/v3/handlers/plans/list.go`:
- Around line 95-104: The handler currently parses params.Sort via
request.ParseSortBy but doesn't validate that the resulting plan.OrderBy value
is a known enum, deferring that to ListPlansInput.Validate; update the sort
handling in the handler (the block using params.Sort, request.ParseSortBy, and
setting req.OrderBy) to validate plan.OrderBy(sort.Field) immediately and, on
invalid value, return apierrors.NewBadRequestError with
apierrors.InvalidParameters containing a single entry with Field: "sort", Reason
set to the validation error message, and Source:
apierrors.InvalidParamSourceQuery so clients get a sort-scoped InvalidParameter
instead of a generic downstream validation error. Ensure req.OrderBy only gets
set after this validation succeeds.

In `@openmeter/productcatalog/plan/service.go`:
- Around line 38-54: The Values() method currently constructs a new slice every
call; create a package-level variable (e.g., orderByValues :=
[]OrderBy{OrderByID, OrderByKey, OrderByVersion, OrderByCreatedAt,
OrderByUpdatedAt}) or a lookup map (e.g., orderByLookup :=
map[OrderBy]struct{}{}) and change OrderBy.Values() to return that variable (or
remove Values and reference the lookup directly), then update OrderBy.Validate()
to check membership against the package-level slice with
slices.Contains(orderByValues, f) or against the map with a simple presence
check to avoid repeated allocations.

In `@openmeter/productcatalog/plan/service/service_test.go`:
- Around line 826-1057: Add test cases to TestListPlansFilters that exercise
sorting and status filtering: extend the fixture by publishing one or two of the
draft plans (use the same env.Plan.* helpers that created plans, e.g. call the
publish method on the created plan keys) then add tcs entries that set
plan.ListPlansInput.Sort to OrderByKey (and reverse) to assert stable key
ordering and a regression for the adapter's OrderByKey behavior, and add tcs
that set the params.Status (the status field on plan.ListPlansInput /
params.Status branch used in ListPlans) to Active/Draft/Scheduled/Archived to
verify effective_from/effective_to semantics; run each case through
env.Plan.ListPlans and assert expected key lists.
🪄 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: 1dc96f57-04c4-4b28-9456-f62711b18a15

📥 Commits

Reviewing files that changed from the base of the PR and between 7210983 and 3a68ac6.

⛔ Files ignored due to path filters (1)
  • api/v3/openapi.yaml is excluded by !**/openapi.yaml
📒 Files selected for processing (7)
  • .agents/skills/api-filters/SKILL.md
  • api/spec/packages/aip/src/productcatalog/operations.tsp
  • api/v3/api.gen.go
  • api/v3/handlers/plans/list.go
  • openmeter/productcatalog/plan/adapter/plan.go
  • openmeter/productcatalog/plan/service.go
  • openmeter/productcatalog/plan/service/service_test.go

Comment thread api/spec/packages/aip/src/productcatalog/operations.tsp Outdated
Comment thread api/spec/packages/aip/src/productcatalog/operations.tsp
Comment thread api/v3/api.gen.go
Comment thread openmeter/productcatalog/plan/service.go
@borosr borosr added the release-note/ignore Ignore this change when generating release notes label Apr 24, 2026
@borosr borosr force-pushed the feat/productcatalog-plans-filter branch from 3379e81 to a168896 Compare April 24, 2026 10:14
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

🧹 Nitpick comments (2)
openmeter/productcatalog/plan/service/service_test.go (1)

829-830: Two tiny nits on an otherwise great table test.

  • Line 829: context.WithCancel(t.Context()) is effectively a no-op wrapper — t.Context() is already cancelled on test cleanup. You can just use ctx := t.Context() and drop the defer cancel().
  • Line 958: the case name advertises "case-insensitive" but every fixture uses the exact casing Plan, so that property isn't actually asserted. Either rename (e.g. "name contains Plan (3 of 4 names)") or add a case like Contains: lo.ToPtr("plan") to genuinely exercise case-insensitivity.

As per coding guidelines, *_test.go: "prefer t.Context() when a testing.T or testing.TB is available instead of context.Background()".

Also applies to: 958-965

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/productcatalog/plan/service/service_test.go` around lines 829 -
830, Replace the unnecessary context.WithCancel wrapper in the test setup by
using ctx := t.Context() and remove the defer cancel() (the current context is
already cancelled on test cleanup); then update the table test case whose name
advertises "case-insensitive" so it actually asserts that behavior—either rename
the case to accurately describe the fixtures (e.g., "name contains Plan (3 of 4
names)") or change the case's filter to use a lowercased Contains value (e.g.,
set Contains: lo.ToPtr("plan")) so the test for case-insensitivity in the table
entry is exercised; look for t.Context(), context.WithCancel(...) and the table
case named "case-insensitive" to locate the spots to edit.
openmeter/productcatalog/plan/service.go (1)

96-108: Heads-up on Currency vs Currencies coexistence.

Currency *filter.FilterString now sits right next to the existing Currencies []string, and both get applied independently in the adapter (OR group + AND ApplyToQuery). It's correct, but the naming is easy to trip on — internal callers reaching for "filter by currency" may pick the wrong one and get subtly different semantics. Consider a short doc note clarifying that Currency is the new request-driven filter and Currencies is the legacy OR-group input, or (follow-up) collapsing the two down the line.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/productcatalog/plan/service.go` around lines 96 - 108, Add a short
clarifying comment above the Currencies and Currency fields to distinguish their
semantics: state that Currencies []string is the legacy OR-group filter (matches
any of the listed currencies) while Currency *filter.FilterString is the newer
request-driven filter with full operators (eq/neq/contains/oeq) and is applied
separately via ApplyToQuery; mention which one takes precedence or that both are
combined (AND) in the adapter so callers know to pick the right field (reference
the Currencies and Currency struct fields and the ApplyToQuery usage in the
adapter/service).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/v3/handlers/plans/convert.go`:
- Around line 781-805: Tighten operator validation in fromAPIStatusFilter by
whitelisting only Eq and Oeq: if any of the other operator fields on
api.StringFieldFilter (Neq, Contains, Ocontains, Exists, Gt, Gte, Lt, Lte, etc.)
are set return apierrors.NewBadRequestError with an InvalidParameters entry for
"filter[status]" and a message that only eq and oeq are supported; additionally
validate each status string (both *f.Eq and each entry in f.Oeq) before casting
to productcatalog.PlanStatus by checking it exists in the set returned by
productcatalog.PlanStatus.Values() and return a 400 InvalidParameters error for
unknown values so invalid statuses like "bogus" are rejected instead of
propagated.

---

Nitpick comments:
In `@openmeter/productcatalog/plan/service.go`:
- Around line 96-108: Add a short clarifying comment above the Currencies and
Currency fields to distinguish their semantics: state that Currencies []string
is the legacy OR-group filter (matches any of the listed currencies) while
Currency *filter.FilterString is the newer request-driven filter with full
operators (eq/neq/contains/oeq) and is applied separately via ApplyToQuery;
mention which one takes precedence or that both are combined (AND) in the
adapter so callers know to pick the right field (reference the Currencies and
Currency struct fields and the ApplyToQuery usage in the adapter/service).

In `@openmeter/productcatalog/plan/service/service_test.go`:
- Around line 829-830: Replace the unnecessary context.WithCancel wrapper in the
test setup by using ctx := t.Context() and remove the defer cancel() (the
current context is already cancelled on test cleanup); then update the table
test case whose name advertises "case-insensitive" so it actually asserts that
behavior—either rename the case to accurately describe the fixtures (e.g., "name
contains Plan (3 of 4 names)") or change the case's filter to use a lowercased
Contains value (e.g., set Contains: lo.ToPtr("plan")) so the test for
case-insensitivity in the table entry is exercised; look for t.Context(),
context.WithCancel(...) and the table case named "case-insensitive" to locate
the spots to edit.
🪄 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: 31c53dbd-1401-4524-8867-ae6a1ee32cd0

📥 Commits

Reviewing files that changed from the base of the PR and between 3379e81 and a168896.

⛔ Files ignored due to path filters (1)
  • api/v3/openapi.yaml is excluded by !**/openapi.yaml
📒 Files selected for processing (8)
  • .agents/skills/api-filters/SKILL.md
  • api/spec/packages/aip/src/productcatalog/operations.tsp
  • api/v3/api.gen.go
  • api/v3/handlers/plans/convert.go
  • api/v3/handlers/plans/list.go
  • openmeter/productcatalog/plan/adapter/plan.go
  • openmeter/productcatalog/plan/service.go
  • openmeter/productcatalog/plan/service/service_test.go
✅ Files skipped from review due to trivial changes (1)
  • .agents/skills/api-filters/SKILL.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • api/spec/packages/aip/src/productcatalog/operations.tsp
  • api/v3/api.gen.go
  • api/v3/handlers/plans/list.go

Comment thread api/v3/handlers/plans/convert.go Outdated
tothandras
tothandras previously approved these changes Apr 24, 2026
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 (2)
api/v3/handlers/plans/convert.go (1)

781-805: ⚠️ Potential issue | 🟠 Major

Status values still aren't validated — bogus values silently return an empty page.

Narrowing the parameter to *api.StringFieldFilterExact nicely tightened the operator surface (nice cleanup, the whitelist part of the earlier concern is basically gone). But the value-level check from that earlier comment is still open: productcatalog.PlanStatus(*f.Eq) / PlanStatus(v) happily accept any string. Downstream in adapter/plan.go none of the slices.Contains(params.Status, …) branches match, so predicates stays empty and plandb.Or() ends up excluding all rows — a caller sending filter[status][eq]=bogus gets a silent empty result instead of a 400.

Worth validating against productcatalog.PlanStatus("").Values() and returning a BadRequestError for unknown values so the failure is visible.

🛠️ Suggested nudge
+	valid := productcatalog.PlanStatus("").Values()
+	appendStatus := func(s string, out []productcatalog.PlanStatus) ([]productcatalog.PlanStatus, error) {
+		ps := productcatalog.PlanStatus(s)
+		if !slices.Contains(valid, ps) {
+			err := fmt.Errorf("unknown plan status %q", s)
+			return nil, apierrors.NewBadRequestError(ctx, err, apierrors.InvalidParameters{
+				{Field: "filter[status]", Reason: err.Error(), Source: apierrors.InvalidParamSourceQuery},
+			})
+		}
+		return append(out, ps), nil
+	}
+
 	var statuses []productcatalog.PlanStatus
 	if f.Eq != nil {
-		statuses = append(statuses, productcatalog.PlanStatus(*f.Eq))
+		var err error
+		if statuses, err = appendStatus(*f.Eq, statuses); err != nil {
+			return nil, err
+		}
 	}
 	for _, v := range f.Oeq {
-		statuses = append(statuses, productcatalog.PlanStatus(v))
+		var err error
+		if statuses, err = appendStatus(v, statuses); err != nil {
+			return nil, err
+		}
 	}

Requires adding slices to imports (and confirming PlanStatus.Values() returns []PlanStatus).

#!/bin/bash
# Confirm PlanStatus.Values() exists and its return type, plus check the adapter behavior on empty predicates.
ast-grep --pattern 'func ($_ PlanStatus) Values() $_ {
  $$$
}'
rg -nP '\bPlanStatus\b' --type=go -g '!**/*_test.go' -C2 | rg -n 'Values\(\)' -C2
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/v3/handlers/plans/convert.go` around lines 781 - 805, The
fromAPIStatusFilter function currently casts arbitrary strings into
productcatalog.PlanStatus which lets invalid status values silently yield an
empty result; change it to validate each input against the canonical set
returned by productcatalog.PlanStatus("").Values() (or the method on PlanStatus)
using slices.Contains (add slices to imports) and if any value is unknown return
apierrors.NewBadRequestError with an InvalidParameters entry for
"filter[status]" describing the invalid value; only append validated PlanStatus
values to statuses and proceed as before.
openmeter/productcatalog/plan/service.go (1)

101-108: ⚠️ Potential issue | 🟡 Minor

Docstrings still undersell what these filters accept.

Flagged before but still standing: *filter.FilterString carries more than eq/neq/contains/oeq (there's gt/gte/lt/lte/in/exists in pkg/filter/filter.go). Since those operators are honored by the adapter via filter.ApplyToQuery, the current comments will mislead readers into thinking the others are dropped. Either list the full set or state "as supported by pkg/filter.FilterString" and let the type be the source of truth.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/productcatalog/plan/service.go` around lines 101 - 108, The
docstrings for the Key, Name, and Currency fields under the plan filter
underspecify supported operators; update their comments to either enumerate all
operators from pkg/filter.FilterString
(eq/neq/contains/oeq/gt/gte/lt/lte/in/exists) or replace the current partial
list with a short phrase like "operators supported by pkg/filter.FilterString"
to accurately reflect the behavior used by filter.ApplyToQuery and avoid
misleading readers.
🧹 Nitpick comments (3)
openmeter/productcatalog/plan/service/service_test.go (1)

826-1057: Solid coverage for the new filter surface

Good mix of per-field and combined-AND cases, and ElementsMatch keeps things order-agnostic — exactly the right call. A couple of totally optional thoughts:

  • context.WithCancel(t.Context()) at line 829 is a little belt-and-suspenders since t.Context() is already cancelled at test cleanup. You can drop the wrapper (and the defer cancel()) unless you need early cancellation mid-test.
  • Would be a small win to also cover Status and the new sort path here, given they're part of the same PR — happy to help scaffold those if useful.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/productcatalog/plan/service/service_test.go` around lines 826 -
1057, In TestListPlansFilters remove the redundant context.WithCancel wrapper:
stop creating the cancelable ctx and defer cancel(); use t.Context() directly
when calling env.Plan.ListPlans and other APIs (replace ctx with t.Context()),
and delete the variables ctx and cancel and their defer to avoid
double-cancellation; this change targets the TestListPlansFilters function where
ctx, cancel are declared and used.
api/v3/handlers/plans/list.go (1)

58-99: Filter/sort plumbing reads nicely 👌

The per-field 400 mapping with the right Field: label is exactly what clients need for invalid_parameters. Two tiny, optional thoughts:

  • The four filter[...] blocks are structurally identical — if this grows, a small helper like parseFilterTo(ctx, params.Filter.Key, "filter[key]", &req.Key, filters.FromAPIFilterString) would tighten things up. Totally a nice-to-have.
  • Unknown sort field values currently fall through to plan.OrderBy(sort.Field) and only get rejected inside the service's OrderBy.Validate(). Still yields a 400 via the generic validation encoder, but the InvalidParameters won't be scoped to "sort" the way the filter errors are. If you want symmetry, a quick if err := req.OrderBy.Validate(); err != nil right here lets you return the same shape as the filter branches.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/v3/handlers/plans/list.go` around lines 58 - 99, The sort handling should
validate the parsed OrderBy immediately so invalid sort fields return a 400 with
an InvalidParameters entry scoped to "sort"; after calling request.ParseSortBy
and setting req.OrderBy = plan.OrderBy(sort.Field) do an if err :=
req.OrderBy.Validate(); err != nil { return ListPlansRequest{},
apierrors.NewBadRequestError(ctx, err, apierrors.InvalidParameters{{Field:
"sort", Reason: err.Error(), Source: apierrors.InvalidParamSourceQuery}}) } to
mirror the filter branches. Optionally, reduce repetition in the filter block by
extracting a small helper (e.g., parseFilterTo(ctx, params.Filter.Key,
"filter[key]", &req.Key, filters.FromAPIFilterString)) that accepts the API
value, the parameter label, a pointer to the target field on ListPlansRequest,
and the appropriate parser function like filters.FromAPIFilterString or
filters.FromAPIFilterStringExact.
openmeter/productcatalog/plan/service.go (1)

38-54: Tiny stylistic thought on OrderBy.Values().

Totally optional — the receiver f is unused, so this could be a package-level var OrderByValues = []OrderBy{...} (or a top-level func OrderByValues()). That said, the method form matches the PlanStatus.Values() pattern elsewhere in the codebase, so keeping it consistent is a perfectly reasonable call. Feel free to leave as-is.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/productcatalog/plan/service.go` around lines 38 - 54, The receiver
on OrderBy.Values() is unused; refactor to a package-level slice (e.g., var
OrderByValues = []OrderBy{OrderByID, OrderByKey, OrderByVersion,
OrderByCreatedAt, OrderByUpdatedAt}) or a top-level function OrderByValues() and
update OrderBy.Validate() to call that new symbol instead of f.Values();
alternatively keep the method to match PlanStatus.Values() if you prefer
consistency—ensure OrderBy.Validate() uses the chosen top-level symbol
(OrderByValues or OrderByValues()) and remove the unused receiver from Values()
if you convert it to a package-level construct.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@api/v3/handlers/plans/convert.go`:
- Around line 781-805: The fromAPIStatusFilter function currently casts
arbitrary strings into productcatalog.PlanStatus which lets invalid status
values silently yield an empty result; change it to validate each input against
the canonical set returned by productcatalog.PlanStatus("").Values() (or the
method on PlanStatus) using slices.Contains (add slices to imports) and if any
value is unknown return apierrors.NewBadRequestError with an InvalidParameters
entry for "filter[status]" describing the invalid value; only append validated
PlanStatus values to statuses and proceed as before.

In `@openmeter/productcatalog/plan/service.go`:
- Around line 101-108: The docstrings for the Key, Name, and Currency fields
under the plan filter underspecify supported operators; update their comments to
either enumerate all operators from pkg/filter.FilterString
(eq/neq/contains/oeq/gt/gte/lt/lte/in/exists) or replace the current partial
list with a short phrase like "operators supported by pkg/filter.FilterString"
to accurately reflect the behavior used by filter.ApplyToQuery and avoid
misleading readers.

---

Nitpick comments:
In `@api/v3/handlers/plans/list.go`:
- Around line 58-99: The sort handling should validate the parsed OrderBy
immediately so invalid sort fields return a 400 with an InvalidParameters entry
scoped to "sort"; after calling request.ParseSortBy and setting req.OrderBy =
plan.OrderBy(sort.Field) do an if err := req.OrderBy.Validate(); err != nil {
return ListPlansRequest{}, apierrors.NewBadRequestError(ctx, err,
apierrors.InvalidParameters{{Field: "sort", Reason: err.Error(), Source:
apierrors.InvalidParamSourceQuery}}) } to mirror the filter branches.
Optionally, reduce repetition in the filter block by extracting a small helper
(e.g., parseFilterTo(ctx, params.Filter.Key, "filter[key]", &req.Key,
filters.FromAPIFilterString)) that accepts the API value, the parameter label, a
pointer to the target field on ListPlansRequest, and the appropriate parser
function like filters.FromAPIFilterString or filters.FromAPIFilterStringExact.

In `@openmeter/productcatalog/plan/service.go`:
- Around line 38-54: The receiver on OrderBy.Values() is unused; refactor to a
package-level slice (e.g., var OrderByValues = []OrderBy{OrderByID, OrderByKey,
OrderByVersion, OrderByCreatedAt, OrderByUpdatedAt}) or a top-level function
OrderByValues() and update OrderBy.Validate() to call that new symbol instead of
f.Values(); alternatively keep the method to match PlanStatus.Values() if you
prefer consistency—ensure OrderBy.Validate() uses the chosen top-level symbol
(OrderByValues or OrderByValues()) and remove the unused receiver from Values()
if you convert it to a package-level construct.

In `@openmeter/productcatalog/plan/service/service_test.go`:
- Around line 826-1057: In TestListPlansFilters remove the redundant
context.WithCancel wrapper: stop creating the cancelable ctx and defer cancel();
use t.Context() directly when calling env.Plan.ListPlans and other APIs (replace
ctx with t.Context()), and delete the variables ctx and cancel and their defer
to avoid double-cancellation; this change targets the TestListPlansFilters
function where ctx, cancel are declared and used.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 25743039-1af1-432b-a89e-3bd530e418be

📥 Commits

Reviewing files that changed from the base of the PR and between f47fa13 and acbdb1c.

⛔ Files ignored due to path filters (1)
  • api/v3/openapi.yaml is excluded by !**/openapi.yaml
📒 Files selected for processing (8)
  • .agents/skills/api-filters/SKILL.md
  • api/spec/packages/aip/src/productcatalog/operations.tsp
  • api/v3/api.gen.go
  • api/v3/handlers/plans/convert.go
  • api/v3/handlers/plans/list.go
  • openmeter/productcatalog/plan/adapter/plan.go
  • openmeter/productcatalog/plan/service.go
  • openmeter/productcatalog/plan/service/service_test.go
✅ Files skipped from review due to trivial changes (1)
  • api/v3/api.gen.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/spec/packages/aip/src/productcatalog/operations.tsp

tothandras
tothandras previously approved these changes Apr 24, 2026
@borosr borosr merged commit acf60a9 into main Apr 27, 2026
27 of 28 checks passed
@borosr borosr deleted the feat/productcatalog-plans-filter branch April 27, 2026 08:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-note/ignore Ignore this change when generating release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants