Skip to content

refactor: add generic feature resolver with ratecard support#4353

Merged
chrisgacsal merged 2 commits into
mainfrom
refactor/rc-feature-resolve
May 16, 2026
Merged

refactor: add generic feature resolver with ratecard support#4353
chrisgacsal merged 2 commits into
mainfrom
refactor/rc-feature-resolve

Conversation

@chrisgacsal
Copy link
Copy Markdown
Collaborator

@chrisgacsal chrisgacsal commented May 13, 2026

Overview

  • add generic FeatureResolver which can be used to find Features by their id, key attributes.
  • add helper for resolving Feature references in-place for Ratecards

Note for the Revier

This PR relies on #4352.

Summary by CodeRabbit

  • Bug Fixes

    • More specific validation codes and clearer error responses for missing or mismatched rate-card features and publish validation.
  • Refactor

    • Centralized, namespace-aware feature resolution across plan and add-on workflows.
    • Batch resolution for rate-card features to ensure consistent mapping and conflict-aware validation.
  • Tests

    • New and updated integration/unit tests for namespaced resolution, batch feature resolution, and rate-card validation.

Review Change Stack

@chrisgacsal chrisgacsal self-assigned this May 13, 2026
@chrisgacsal chrisgacsal requested a review from a team as a code owner May 13, 2026 05:47
@chrisgacsal chrisgacsal added the release-note/feature Release note: Exciting New Features label May 13, 2026
@chrisgacsal chrisgacsal marked this pull request as draft May 13, 2026 05:47
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 13, 2026

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Introduces a namespaced, batch-capable NamespacedFeatureResolver; centralizes rate-card feature resolution with ResolveFeaturesForRateCards; updates Plan and Addon services to accept the resolver; rewires binaries and test infra to provide it; and adjusts tests and RateCard APIs.

Changes

Feature Resolution Refactoring

Layer / File(s) Summary
Feature Resolver Contract and Implementation
openmeter/productcatalog/featureresolver.go, openmeter/productcatalog/featureresolver/namespaced.go, openmeter/productcatalog/featureresolver/namespaced_test.go
NamespacedFeatureResolver adds BatchResolve. New concrete resolver implements Resolve, WithNamespace, and BatchResolve with paginated feature listing and tests.
Rate Card Feature API and Centralized Resolution
openmeter/productcatalog/ratecard.go, openmeter/productcatalog/featureresolver/ratecard.go, openmeter/productcatalog/featureresolver/ratecard_test.go
RateCard adds GetFeatureID, GetFeatureKey, and SetFeature. ResolveFeaturesForRateCards batch-resolves and reconciles rate-card features, detecting mismatches and not-found cases, updating rate cards and returning aggregated validation errors.
Plan Service Refactor
openmeter/productcatalog/plan/service/service.go, openmeter/productcatalog/plan/service/plan.go, openmeter/productcatalog/plan/service/service_test.go, openmeter/productcatalog/plan/adapter/adapter_test.go
Plan service now consumes NamespacedFeatureResolver instead of a feature connector; local resolve helper removed; Create/Update call ResolveFeaturesForRateCards per phase; Publish validates with resolver namespace; tests updated.
Addon Service Refactor
openmeter/productcatalog/addon/service/service.go, openmeter/productcatalog/addon/service/addon.go, openmeter/productcatalog/addon/service/service_test.go
Addon service config now uses FeatureResolver; feature resolution delegated to ResolveFeaturesForRateCards; publish validation uses s.featureResolver.WithNamespace(...); tests adjusted.
Service Wiring Provider
app/common/productcatalog.go
Adds exported Wire provider alias NewNamespaceFeatureResolver and updates NewPlanService/NewAddonService to accept the resolver and pass it as FeatureResolver in configs.
Wire Generation Updates
cmd/billing-worker/wire_gen.go, cmd/jobs/internal/wire_gen.go, cmd/server/wire_gen.go
Generated wiring constructs namespacedFeatureResolver from the feature connector and wires it into plan/addon constructors across binaries.
Test Infrastructure and Utilities
openmeter/productcatalog/testutils/env.go, openmeter/subscription/testutils/service.go, openmeter/subscription/addon/extend_test.go, test/billing/subscription_suite.go, test/customer/testenv.go
Test envs construct a namespaced resolver and pass it into plan/addon services; test helpers implement new RateCard methods to satisfy interface changes.
E2E Test Update
e2e/addons_v3_test.go
E2E assertion updated to assert structured validation code rate_card_feature_not_found instead of matching raw error strings.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

area/product-catalog, area/subscriptions

Suggested reviewers

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

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.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 'refactor: add generic feature resolver with ratecard support' accurately summarizes the main change: introducing a generic feature resolver with ratecard resolution capabilities.
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 refactor/rc-feature-resolve

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

🧹 Nitpick comments (4)
openmeter/productcatalog/featureresolver/ratecard_test.go (1)

154-159: ⚡ Quick win

Please add negative cases for missing and conflicting feature references.

Happy path is solid, but this resolver path also needs assertions for: missing key, missing ID, and ID/key mismatch (expecting validation errors, no panic).

As per coding guidelines: **/*_test.go: Make sure the tests are comprehensive and cover the changes.

🤖 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/productcatalog/featureresolver/ratecard_test.go` around lines 154 -
159, Add unit tests covering negative cases for ResolveFeaturesForRateCards:
create separate subtests that call ResolveFeaturesForRateCards(t.Context(),
resolver, namespace, &ratecards) with (1) a ratecard whose feature reference
lacks Key, (2) a ratecard whose feature reference lacks ID, and (3) a ratecard
whose feature reference contains a mismatched ID and Key; in each subtest assert
that ResolveFeaturesForRateCards returns an error (use require.Error or
assert.Error) and does not panic, and optionally validate the error message/type
to ensure it's a validation error; reuse the existing variables (features,
ratecards, resolver, namespace) but modify the specific ratecard entries before
calling ResolveFeaturesForRateCards so tests are focused and isolated.
openmeter/productcatalog/featureresolver/namespaced_test.go (1)

126-132: ⚡ Quick win

Batch test should verify feature identity, not only presence.

Right now it only checks nil/non-nil, so a wrong feature pointer/value can slip through undetected. Please also assert that each found entry maps to the expected feature ID/key.

As per coding guidelines: **/*_test.go: Make sure the tests are comprehensive and cover the changes.

🤖 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/productcatalog/featureresolver/namespaced_test.go` around lines 126
- 132, The test currently only checks presence in the resolved map; extend the
test cases to include the expected identity (e.g., add fields like expectedID or
expectedFeature to each entry) and, inside the for _, test := range tests loop,
when test.expectedToFind is true assert that resolved[test.idOrKey] is the exact
expected feature (either by comparing the feature ID/key field, e.g.
resolved[test.idOrKey].ID == test.expectedID, or by pointer equality to
test.expectedFeature) so the map entry maps to the correct feature, not just a
non-nil value.
openmeter/productcatalog/plan/service/service_test.go (1)

283-316: ⚡ Quick win

Please add one failing feature-resolution case here.

These edits cover the happy paths for ID-only and key-only references, but they still don't lock in what CreatePlan/UpdatePlan should return when a phase points at a nonexistent feature. One negative case would make the new resolver wiring much harder to regress silently.

As per coding guidelines, **/*_test.go: Make sure the tests are comprehensive and cover the changes. Keep a strong focus on unit tests and in-code integration tests.

Also applies to: 433-435

🤖 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/productcatalog/plan/service/service_test.go` around lines 283 -
316, Add a negative test inside the existing t.Run("NewPhase") block that
asserts CreatePlan/UpdatePlan returns an error when a phase references a
nonexistent feature: create a phase whose RateCard or FeatureKey points to a
fake/nonexistent feature key (e.g., "missing-feature"), call the same service
method used by the other subtests (the service.CreatePlan or service.UpdatePlan
helper used in this file), and assert the call fails with a validation or
not-found error (and does not succeed or silently drop the reference). Reference
the test helper names and the CreatePlan/UpdatePlan service methods used
elsewhere in service_test.go so the failing case mirrors the existing happy-path
setup but expects an error.
openmeter/productcatalog/addon/service/service_test.go (1)

97-99: ⚡ Quick win

Please add a missing-feature regression test too.

The happy-path coverage looks good, but this refactor also changes how CreateAddon/UpdateAddon surface feature lookup failures. A single case with an unknown FeatureID or FeatureKey would give this new resolver path a much safer guardrail.

As per coding guidelines, **/*_test.go: Make sure the tests are comprehensive and cover the changes. Keep a strong focus on unit tests and in-code integration tests.

Also applies to: 183-206

🤖 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/productcatalog/addon/service/service_test.go` around lines 97 - 99,
Add a regression unit test that exercises the failure path when a referenced
feature cannot be found: create tests for CreateAddon and UpdateAddon that pass
an unknown FeatureID and an unknown FeatureKey (mirror the existing happy-path
case that uses FeatureKey: nil and FeatureID: lo.ToPtr(...)) and assert the
service returns the expected not-found/error result instead of succeeding;
locate the test cases near the existing cases in service_test.go (where
NewEntitlementTemplateFrom(BooleanEntitlementTemplate{}) is used) and add
assertions verifying the resolver/feature-lookup path in CreateAddon and
UpdateAddon returns an error for missing features.
🤖 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 `@openmeter/productcatalog/featureresolver/namespaced.go`:
- Around line 135-142: The loop stores the address of the loop variable f into
the result map, causing all entries to reference the same memory; change the
loop to take a fresh variable per iteration (e.g., create a local copy like ff
:= f inside the for _, f := range features loop) and store &ff into result for
both result[f.ID] and result[f.Key] (or use an index-based loop and take the
address of features[i]) so each map entry points to its own feature instance.
- Around line 93-130: In BatchResolve (namespacedFeatureResolver.BatchResolve)
avoid paginating ListFeatures when idsOrKeys is empty by short-circuiting early:
if len(idsOrKeys) == 0 return an empty map[string]*feature.Feature and nil
error; this prevents a full namespace scan on DB-backed ListFeatures and keeps
the existing behavior when idsOrKeys is non-empty where you continue to call
r.service.ListFeatures with pagination.

In `@openmeter/productcatalog/featureresolver/ratecard.go`:
- Around line 63-68: The code dereferences potentially nil entries when
comparing IDs/keys (variables feat, f and the map features), causing a panic;
update the logic around the features[key] lookup so you first retrieve the entry
into a temp (feat := features[key]) and check for nil before accessing feat.ID
or feat.Key and likewise guard f for nil before using f.ID/f.Key; if feat is nil
set feat = f only after ensuring f != nil, and perform the key conflict check
only when both feat and f are non-nil—on conflict append the validation error to
errs as before.

---

Nitpick comments:
In `@openmeter/productcatalog/addon/service/service_test.go`:
- Around line 97-99: Add a regression unit test that exercises the failure path
when a referenced feature cannot be found: create tests for CreateAddon and
UpdateAddon that pass an unknown FeatureID and an unknown FeatureKey (mirror the
existing happy-path case that uses FeatureKey: nil and FeatureID: lo.ToPtr(...))
and assert the service returns the expected not-found/error result instead of
succeeding; locate the test cases near the existing cases in service_test.go
(where NewEntitlementTemplateFrom(BooleanEntitlementTemplate{}) is used) and add
assertions verifying the resolver/feature-lookup path in CreateAddon and
UpdateAddon returns an error for missing features.

In `@openmeter/productcatalog/featureresolver/namespaced_test.go`:
- Around line 126-132: The test currently only checks presence in the resolved
map; extend the test cases to include the expected identity (e.g., add fields
like expectedID or expectedFeature to each entry) and, inside the for _, test :=
range tests loop, when test.expectedToFind is true assert that
resolved[test.idOrKey] is the exact expected feature (either by comparing the
feature ID/key field, e.g. resolved[test.idOrKey].ID == test.expectedID, or by
pointer equality to test.expectedFeature) so the map entry maps to the correct
feature, not just a non-nil value.

In `@openmeter/productcatalog/featureresolver/ratecard_test.go`:
- Around line 154-159: Add unit tests covering negative cases for
ResolveFeaturesForRateCards: create separate subtests that call
ResolveFeaturesForRateCards(t.Context(), resolver, namespace, &ratecards) with
(1) a ratecard whose feature reference lacks Key, (2) a ratecard whose feature
reference lacks ID, and (3) a ratecard whose feature reference contains a
mismatched ID and Key; in each subtest assert that ResolveFeaturesForRateCards
returns an error (use require.Error or assert.Error) and does not panic, and
optionally validate the error message/type to ensure it's a validation error;
reuse the existing variables (features, ratecards, resolver, namespace) but
modify the specific ratecard entries before calling ResolveFeaturesForRateCards
so tests are focused and isolated.

In `@openmeter/productcatalog/plan/service/service_test.go`:
- Around line 283-316: Add a negative test inside the existing t.Run("NewPhase")
block that asserts CreatePlan/UpdatePlan returns an error when a phase
references a nonexistent feature: create a phase whose RateCard or FeatureKey
points to a fake/nonexistent feature key (e.g., "missing-feature"), call the
same service method used by the other subtests (the service.CreatePlan or
service.UpdatePlan helper used in this file), and assert the call fails with a
validation or not-found error (and does not succeed or silently drop the
reference). Reference the test helper names and the CreatePlan/UpdatePlan
service methods used elsewhere in service_test.go so the failing case mirrors
the existing happy-path setup but expects an error.
🪄 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: 394c93f3-f43b-44df-ac2d-ddf3b0759606

📥 Commits

Reviewing files that changed from the base of the PR and between 47d4dc2 and 19cdcd3.

📒 Files selected for processing (23)
  • app/common/productcatalog.go
  • cmd/billing-worker/wire_gen.go
  • cmd/jobs/internal/wire_gen.go
  • cmd/server/wire_gen.go
  • e2e/addons_v3_test.go
  • openmeter/productcatalog/addon/service/addon.go
  • openmeter/productcatalog/addon/service/service.go
  • openmeter/productcatalog/addon/service/service_test.go
  • openmeter/productcatalog/featureresolver.go
  • openmeter/productcatalog/featureresolver/namespaced.go
  • openmeter/productcatalog/featureresolver/namespaced_test.go
  • openmeter/productcatalog/featureresolver/ratecard.go
  • openmeter/productcatalog/featureresolver/ratecard_test.go
  • openmeter/productcatalog/plan/adapter/adapter_test.go
  • openmeter/productcatalog/plan/service/plan.go
  • openmeter/productcatalog/plan/service/service.go
  • openmeter/productcatalog/plan/service/service_test.go
  • openmeter/productcatalog/ratecard.go
  • openmeter/productcatalog/testutils/env.go
  • openmeter/subscription/addon/extend_test.go
  • openmeter/subscription/testutils/service.go
  • test/billing/subscription_suite.go
  • test/customer/testenv.go
💤 Files with no reviewable changes (1)
  • openmeter/productcatalog/plan/adapter/adapter_test.go

Comment thread openmeter/productcatalog/featureresolver/namespaced.go Outdated
Comment thread openmeter/productcatalog/featureresolver/namespaced.go Outdated
Comment thread openmeter/productcatalog/featureresolver/ratecard.go Outdated
@chrisgacsal chrisgacsal force-pushed the refactor/rc-feature-resolve branch from 19cdcd3 to a01c54b Compare May 13, 2026 06:59
@chrisgacsal chrisgacsal force-pushed the refactor/rc-feature-resolve branch from a01c54b to 3c960e0 Compare May 13, 2026 08:06
@chrisgacsal chrisgacsal force-pushed the refactor/rc-feature-resolve branch from 3c960e0 to c17fa46 Compare May 13, 2026 08:21
@chrisgacsal chrisgacsal marked this pull request as ready for review May 13, 2026 08:29
turip
turip previously approved these changes May 13, 2026
Copy link
Copy Markdown
Member

@turip turip left a comment

Choose a reason for hiding this comment

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

LGFM

Comment thread openmeter/productcatalog/featureresolver/namespaced.go Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@openmeter/productcatalog/featureresolver/namespaced.go`:
- Around line 60-85: The Resolve function currently returns a single matched
feature when both id and key are provided but only one resolves; update the
logic in Resolve (the loop over featureIDOrKey, the f variable and features map)
so that if both an id and a key were supplied the method requires both to
resolve to the same feature: if one input resolves and the other does not,
return a NewGenericNotFoundError (or a clear error indicating the missing
reference); if both resolve but to different features return the existing
NewGenericConflictError as before; also add a unit test exercising the “both
provided but only one resolves” case (and reference ResolveFeaturesForRateCards
behavior) to document the intended behavior.
🪄 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: 23466414-9fac-46f7-8b66-fb68655789d3

📥 Commits

Reviewing files that changed from the base of the PR and between 19cdcd3 and c17fa46.

📒 Files selected for processing (23)
  • app/common/productcatalog.go
  • cmd/billing-worker/wire_gen.go
  • cmd/jobs/internal/wire_gen.go
  • cmd/server/wire_gen.go
  • e2e/addons_v3_test.go
  • openmeter/productcatalog/addon/service/addon.go
  • openmeter/productcatalog/addon/service/service.go
  • openmeter/productcatalog/addon/service/service_test.go
  • openmeter/productcatalog/featureresolver.go
  • openmeter/productcatalog/featureresolver/namespaced.go
  • openmeter/productcatalog/featureresolver/namespaced_test.go
  • openmeter/productcatalog/featureresolver/ratecard.go
  • openmeter/productcatalog/featureresolver/ratecard_test.go
  • openmeter/productcatalog/plan/adapter/adapter_test.go
  • openmeter/productcatalog/plan/service/plan.go
  • openmeter/productcatalog/plan/service/service.go
  • openmeter/productcatalog/plan/service/service_test.go
  • openmeter/productcatalog/ratecard.go
  • openmeter/productcatalog/testutils/env.go
  • openmeter/subscription/addon/extend_test.go
  • openmeter/subscription/testutils/service.go
  • test/billing/subscription_suite.go
  • test/customer/testenv.go
💤 Files with no reviewable changes (1)
  • openmeter/productcatalog/plan/adapter/adapter_test.go
✅ Files skipped from review due to trivial changes (4)
  • cmd/jobs/internal/wire_gen.go
  • openmeter/productcatalog/addon/service/service_test.go
  • cmd/billing-worker/wire_gen.go
  • cmd/server/wire_gen.go
🚧 Files skipped from review as they are similar to previous changes (14)
  • e2e/addons_v3_test.go
  • test/customer/testenv.go
  • openmeter/productcatalog/testutils/env.go
  • openmeter/subscription/testutils/service.go
  • test/billing/subscription_suite.go
  • openmeter/productcatalog/plan/service/plan.go
  • openmeter/productcatalog/addon/service/service.go
  • app/common/productcatalog.go
  • openmeter/productcatalog/featureresolver/ratecard_test.go
  • openmeter/productcatalog/ratecard.go
  • openmeter/subscription/addon/extend_test.go
  • openmeter/productcatalog/featureresolver/ratecard.go
  • openmeter/productcatalog/addon/service/addon.go
  • openmeter/productcatalog/featureresolver/namespaced_test.go

Comment thread openmeter/productcatalog/featureresolver/namespaced.go Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
openmeter/productcatalog/featureresolver/namespaced.go (1)

60-76: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Guard unresolved entries before dereferencing ff.

Because BatchResolve seeds every requested id/key with a nil value, ok on Line 61 only means the key was requested, not that it resolved. If the id resolves but the key does not, ff is nil on the second iteration and Line 68 panics on ff.ID.

At minimum, skip nil matches before comparing IDs. If both refs are supposed to be authoritative, this is also the right spot to turn the missing side into a not-found error instead of silently succeeding.

💡 Minimal fix
 	for _, v := range featureIDOrKey {
-		if ff, ok := features[v]; ok {
-			if f == nil {
-				f = ff
-
-				continue
-			}
-
-			if f.ID != ff.ID {
-				return nil, models.NewGenericConflictError(
-					fmt.Errorf("id and key reference different features: %v+",
-						lo.MapToSlice(features, func(key string, v *feature.Feature) string {
-							return fmt.Sprintf("%s => %s", key, v.ID)
-						}),
-					),
-				)
-			}
+		ff, ok := features[v]
+		if !ok || ff == nil {
+			continue
+		}
+
+		if f == nil {
+			f = ff
+
+			continue
+		}
+
+		if f.ID != ff.ID {
+			return nil, models.NewGenericConflictError(
+				fmt.Errorf("id and key reference different features: %v+",
+					lo.MapToSlice(features, func(key string, v *feature.Feature) string {
+						return fmt.Sprintf("%s => %s", key, v.ID)
+					}),
+				),
+			)
 		}
 	}
#!/bin/bash
set -euo pipefail

echo "Resolve path:"
sed -n '38,85p' openmeter/productcatalog/featureresolver/namespaced.go

echo
echo "BatchResolve map initialization:"
sed -n '112,124p' openmeter/productcatalog/featureresolver/namespaced.go

echo
echo "Expected check:"
echo "- Resolve should not treat 'ok == true' as 'feature found' when BatchResolve pre-seeds missing identifiers with nil values."
echo "- A nil guard is needed before any use of ff.ID."

Also applies to: 112-124

🤖 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/productcatalog/featureresolver/namespaced.go` around lines 60 - 76,
The loop over featureIDOrKey uses the features map entries returned by
BatchResolve but treats ok==true as "found" even though entries can be nil;
guard against nil ff before dereferencing (skip nil ff when setting/compare, or
return a not-found error if both id and key were expected to resolve). Update
the loop in namespaced.go (the block that iterates featureIDOrKey and references
ff and f) to: 1) check if ff == nil and either continue (if missing side is
acceptable) or return a not-found error, and 2) only compare f.ID and ff.ID when
both f and ff are non-nil (preserve the existing models.NewGenericConflictError
path but use lo.MapToSlice safely by skipping nil values). Ensure analogous
nil-guards are applied to the same logic that appears around lines initializing
BatchResolve results.
🤖 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.

Duplicate comments:
In `@openmeter/productcatalog/featureresolver/namespaced.go`:
- Around line 60-76: The loop over featureIDOrKey uses the features map entries
returned by BatchResolve but treats ok==true as "found" even though entries can
be nil; guard against nil ff before dereferencing (skip nil ff when
setting/compare, or return a not-found error if both id and key were expected to
resolve). Update the loop in namespaced.go (the block that iterates
featureIDOrKey and references ff and f) to: 1) check if ff == nil and either
continue (if missing side is acceptable) or return a not-found error, and 2)
only compare f.ID and ff.ID when both f and ff are non-nil (preserve the
existing models.NewGenericConflictError path but use lo.MapToSlice safely by
skipping nil values). Ensure analogous nil-guards are applied to the same logic
that appears around lines initializing BatchResolve results.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3dbc5f6d-3659-46e9-b827-ed3ab6c0e80e

📥 Commits

Reviewing files that changed from the base of the PR and between c17fa46 and 8a797e1.

📒 Files selected for processing (23)
  • app/common/productcatalog.go
  • cmd/billing-worker/wire_gen.go
  • cmd/jobs/internal/wire_gen.go
  • cmd/server/wire_gen.go
  • e2e/addons_v3_test.go
  • openmeter/productcatalog/addon/service/addon.go
  • openmeter/productcatalog/addon/service/service.go
  • openmeter/productcatalog/addon/service/service_test.go
  • openmeter/productcatalog/featureresolver.go
  • openmeter/productcatalog/featureresolver/namespaced.go
  • openmeter/productcatalog/featureresolver/namespaced_test.go
  • openmeter/productcatalog/featureresolver/ratecard.go
  • openmeter/productcatalog/featureresolver/ratecard_test.go
  • openmeter/productcatalog/plan/adapter/adapter_test.go
  • openmeter/productcatalog/plan/service/plan.go
  • openmeter/productcatalog/plan/service/service.go
  • openmeter/productcatalog/plan/service/service_test.go
  • openmeter/productcatalog/ratecard.go
  • openmeter/productcatalog/testutils/env.go
  • openmeter/subscription/addon/extend_test.go
  • openmeter/subscription/testutils/service.go
  • test/billing/subscription_suite.go
  • test/customer/testenv.go
💤 Files with no reviewable changes (1)
  • openmeter/productcatalog/plan/adapter/adapter_test.go
✅ Files skipped from review due to trivial changes (2)
  • cmd/billing-worker/wire_gen.go
  • cmd/server/wire_gen.go
🚧 Files skipped from review as they are similar to previous changes (18)
  • openmeter/productcatalog/testutils/env.go
  • openmeter/subscription/testutils/service.go
  • cmd/jobs/internal/wire_gen.go
  • e2e/addons_v3_test.go
  • test/customer/testenv.go
  • openmeter/productcatalog/plan/service/service_test.go
  • openmeter/productcatalog/addon/service/service_test.go
  • openmeter/subscription/addon/extend_test.go
  • openmeter/productcatalog/featureresolver/namespaced_test.go
  • test/billing/subscription_suite.go
  • openmeter/productcatalog/addon/service/service.go
  • openmeter/productcatalog/featureresolver/ratecard_test.go
  • openmeter/productcatalog/plan/service/service.go
  • openmeter/productcatalog/featureresolver.go
  • openmeter/productcatalog/featureresolver/ratecard.go
  • openmeter/productcatalog/plan/service/plan.go
  • openmeter/productcatalog/addon/service/addon.go
  • app/common/productcatalog.go

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
openmeter/productcatalog/featureresolver/namespaced.go (1)

96-98: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Inconsistent empty-input contract for BatchResolve.

When idsOrKeys is empty you return (nil, nil), but the non-empty path always returns a pre-populated map (never nil). Callers iterating the result with range are fine, but anyone doing a length check or comparing to a non-nil empty map gets surprised. Returning an empty (non-nil) map keeps the contract uniform and matches what the past review here suggested.

♻️ Tiny tweak
-	if len(idsOrKeys) == 0 {
-		return nil, nil
-	}
+	if len(idsOrKeys) == 0 {
+		return map[string]*feature.Feature{}, nil
+	}
🤖 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/productcatalog/featureresolver/namespaced.go` around lines 96 - 98,
BatchResolve currently returns (nil, nil) when idsOrKeys is empty which is
inconsistent with the non-empty path that returns a non-nil map; change the
empty-input branch to return an allocated empty map (same map type returned by
BatchResolve) and nil error instead of nil, nil so callers always receive a
non-nil map; locate the empty check around idsOrKeys in BatchResolve and replace
the nil return with a new empty map instance.
🧹 Nitpick comments (1)
openmeter/productcatalog/featureresolver/ratecard_test.go (1)

205-231: ⚡ Quick win

Consider adding a "partial resolution" case.

The current scenarios cover both-resolve-and-match, both-missing, and both-resolve-but-mismatch. There's no case for "one of FeatureID/FeatureKey is set to a real value while the other points to a non-existent feature". That path exercises the corner in namespacedFeatureResolver.Resolve where one map entry is nil (the spot flagged in the other file for a potential NPE). Adding it here would lock down the intended behavior and catch regressions cheaply.

🤖 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/productcatalog/featureresolver/ratecard_test.go` around lines 205 -
231, Add a new test case in ratecard_test.go that covers the "partial
resolution" scenario: create a RateCards entry where one identifier is valid and
resolves to a real feature (e.g., FeatureKey set to features[0].Key) while the
other identifier points to a non-existent feature (e.g., FeatureID set to a fake
ID or vice versa), and assert the resolver returns
productcatalog.ErrRateCardFeatureMismatch; this will exercise
namespacedFeatureResolver.Resolve's path where one map entry can be nil and
prevent a potential NPE by verifying the intended mismatch behavior.
🤖 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 `@openmeter/productcatalog/featureresolver/namespaced.go`:
- Around line 58-78: The loop that compares resolved features (variables f and
ff over featureIDOrKey / features) can dereference a nil ff; before comparing
IDs check for nil: if ff == nil then decide semantics—either skip nil entries
when matching the same feature (continue) or, if both id and key were supplied
but only one resolved, return models.NewGenericNotFoundError (stricter
behavior). Concretely, inside the loop in BatchResolve/Resolve update the logic
to first if ff == nil { if both inputs provided return
NewGenericNotFoundError(...) else continue } else if f == nil { f = ff } else if
f.ID != ff.ID { return models.NewGenericConflictError(...) } to avoid
nil-pointer deref.

In `@openmeter/productcatalog/featureresolver/ratecard_test.go`:
- Line 72: The test case name contains a typo: change the map entry value
"sucess" to the correct "success" in the test case declaration within
ratecard_test.go (the test case name field referenced as name: "sucess"); update
any assertions or subtest t.Run calls that rely on that exact string if present
so they match the corrected "success".

---

Duplicate comments:
In `@openmeter/productcatalog/featureresolver/namespaced.go`:
- Around line 96-98: BatchResolve currently returns (nil, nil) when idsOrKeys is
empty which is inconsistent with the non-empty path that returns a non-nil map;
change the empty-input branch to return an allocated empty map (same map type
returned by BatchResolve) and nil error instead of nil, nil so callers always
receive a non-nil map; locate the empty check around idsOrKeys in BatchResolve
and replace the nil return with a new empty map instance.

---

Nitpick comments:
In `@openmeter/productcatalog/featureresolver/ratecard_test.go`:
- Around line 205-231: Add a new test case in ratecard_test.go that covers the
"partial resolution" scenario: create a RateCards entry where one identifier is
valid and resolves to a real feature (e.g., FeatureKey set to features[0].Key)
while the other identifier points to a non-existent feature (e.g., FeatureID set
to a fake ID or vice versa), and assert the resolver returns
productcatalog.ErrRateCardFeatureMismatch; this will exercise
namespacedFeatureResolver.Resolve's path where one map entry can be nil and
prevent a potential NPE by verifying the intended mismatch behavior.
🪄 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: 3264eac7-5e3a-4931-be51-ffe4fcf6c3b6

📥 Commits

Reviewing files that changed from the base of the PR and between 8a797e1 and 1ef4c89.

📒 Files selected for processing (23)
  • app/common/productcatalog.go
  • cmd/billing-worker/wire_gen.go
  • cmd/jobs/internal/wire_gen.go
  • cmd/server/wire_gen.go
  • e2e/addons_v3_test.go
  • openmeter/productcatalog/addon/service/addon.go
  • openmeter/productcatalog/addon/service/service.go
  • openmeter/productcatalog/addon/service/service_test.go
  • openmeter/productcatalog/featureresolver.go
  • openmeter/productcatalog/featureresolver/namespaced.go
  • openmeter/productcatalog/featureresolver/namespaced_test.go
  • openmeter/productcatalog/featureresolver/ratecard.go
  • openmeter/productcatalog/featureresolver/ratecard_test.go
  • openmeter/productcatalog/plan/adapter/adapter_test.go
  • openmeter/productcatalog/plan/service/plan.go
  • openmeter/productcatalog/plan/service/service.go
  • openmeter/productcatalog/plan/service/service_test.go
  • openmeter/productcatalog/ratecard.go
  • openmeter/productcatalog/testutils/env.go
  • openmeter/subscription/addon/extend_test.go
  • openmeter/subscription/testutils/service.go
  • test/billing/subscription_suite.go
  • test/customer/testenv.go
💤 Files with no reviewable changes (1)
  • openmeter/productcatalog/plan/adapter/adapter_test.go
✅ Files skipped from review due to trivial changes (1)
  • cmd/jobs/internal/wire_gen.go
🚧 Files skipped from review as they are similar to previous changes (18)
  • openmeter/subscription/testutils/service.go
  • openmeter/subscription/addon/extend_test.go
  • openmeter/productcatalog/testutils/env.go
  • test/customer/testenv.go
  • e2e/addons_v3_test.go
  • test/billing/subscription_suite.go
  • openmeter/productcatalog/addon/service/service_test.go
  • openmeter/productcatalog/featureresolver/ratecard.go
  • cmd/billing-worker/wire_gen.go
  • app/common/productcatalog.go
  • openmeter/productcatalog/featureresolver/namespaced_test.go
  • cmd/server/wire_gen.go
  • openmeter/productcatalog/addon/service/addon.go
  • openmeter/productcatalog/plan/service/service.go
  • openmeter/productcatalog/plan/service/service_test.go
  • openmeter/productcatalog/addon/service/service.go
  • openmeter/productcatalog/featureresolver.go
  • openmeter/productcatalog/plan/service/plan.go

Comment thread openmeter/productcatalog/featureresolver/namespaced.go Outdated
Comment thread openmeter/productcatalog/featureresolver/ratecard_test.go Outdated
@chrisgacsal chrisgacsal force-pushed the refactor/rc-feature-resolve branch from 1ef4c89 to 2bf3ae2 Compare May 13, 2026 10:37
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
openmeter/productcatalog/featureresolver/namespaced.go (1)

64-75: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Treat an unresolved key as not found before checking for conflicts.

Right now, if the ID resolves first and the key lookup misses, f stays populated from the ID path, so Line 73 reports a conflict instead of a missing key. That makes this branch asymmetric with the missing-ID case and returns a misleading error type.

💡 Minimal fix
 	if fKey != "" {
-		if f == nil {
-			f = features[fKey]
-		}
-
-		if f == nil {
-			errs = append(errs, models.NewGenericNotFoundError(fmt.Errorf("feature [feature.key=%s]", *key)))
-		}
-
-		if f != nil && f.Key != fKey {
+		ff := features[fKey]
+		if ff == nil {
+			errs = append(errs, models.NewGenericNotFoundError(fmt.Errorf("feature [feature.key=%s]", fKey)))
+		} else if f == nil {
+			f = ff
+		} else if f.ID != ff.ID {
 			errs = append(errs, models.NewGenericConflictError(fmt.Errorf("feature [feature.id=%s feature.key=%s]", fID, fKey)))
 		}
 	}
🤖 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/productcatalog/featureresolver/namespaced.go` around lines 64 - 75,
When handling the fKey branch in the namespaced resolver, ensure a missing key
is treated as "not found" before doing any conflict checks: look up
features[fKey] into a temporary (e.g., found := features[fKey]); if found == nil
append models.NewGenericNotFoundError(fmt.Errorf("feature [feature.key=%s]",
*key)) and set f = nil (or return), otherwise set f = found; only perform the
models.NewGenericConflictError check if f is non-nil and its Key differs from
fKey (use fID and fKey as existing identifiers).
🧹 Nitpick comments (1)
openmeter/productcatalog/featureresolver/namespaced.go (1)

89-109: ⚡ Quick win

Normalize idsOrKeys before calling ListFeatures.

Resolve can feed this with "" for the unused side, and batch callers can pass duplicates. The result map collapses those anyway, but the DB query and page size are still built from the larger raw slice.

♻️ Small cleanup
 func (r *namespacedFeatureResolver) BatchResolve(ctx context.Context, idsOrKeys ...string) (map[string]*feature.Feature, error) {
@@
-	if len(idsOrKeys) == 0 {
+	normalized := make([]string, 0, len(idsOrKeys))
+	seen := make(map[string]struct{}, len(idsOrKeys))
+	for _, v := range idsOrKeys {
+		if v == "" {
+			continue
+		}
+		if _, ok := seen[v]; ok {
+			continue
+		}
+		seen[v] = struct{}{}
+		normalized = append(normalized, v)
+	}
+
+	if len(normalized) == 0 {
 		return nil, nil
 	}
 
 	features, err := pagination.CollectAll(ctx, pagination.NewPaginator(func(ctx context.Context, page pagination.Page) (pagination.Result[feature.Feature], error) {
 		return r.service.ListFeatures(ctx, feature.ListFeaturesParams{
-			IDsOrKeys:       idsOrKeys,
+			IDsOrKeys:       normalized,
 			Namespace:       r.namespace,
 			IncludeArchived: false,
 			Page:            page,
 		})
-	}), min(len(idsOrKeys), 100))
+	}), min(len(normalized), 100))
@@
-	result := lo.SliceToMap(idsOrKeys, func(item string) (string, *feature.Feature) {
+	result := lo.SliceToMap(normalized, func(item string) (string, *feature.Feature) {

As per coding guidelines, "Performance should be a priority in critical code paths. Anything related to event ingestion, message processing, database operations (regardless of database) should be vetted for potential performance bottlenecks."

🤖 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/productcatalog/featureresolver/namespaced.go` around lines 89 -
109, The BatchResolve implementation should normalize the incoming idsOrKeys
slice before using it in the DB query and pagination: inside
namespacedFeatureResolver.BatchResolve, remove empty strings and deduplicate
values from the idsOrKeys parameter (so Resolve's "" placeholders are dropped
and duplicates collapsed) and then pass that normalized slice to
pagination.NewPaginator / r.service.ListFeatures and use
min(len(normalizedSlice), 100) for the page size; update any references to
idsOrKeys in this function to use the normalized slice when constructing
feature.ListFeaturesParams and when computing the paginator size.
🤖 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.

Duplicate comments:
In `@openmeter/productcatalog/featureresolver/namespaced.go`:
- Around line 64-75: When handling the fKey branch in the namespaced resolver,
ensure a missing key is treated as "not found" before doing any conflict checks:
look up features[fKey] into a temporary (e.g., found := features[fKey]); if
found == nil append models.NewGenericNotFoundError(fmt.Errorf("feature
[feature.key=%s]", *key)) and set f = nil (or return), otherwise set f = found;
only perform the models.NewGenericConflictError check if f is non-nil and its
Key differs from fKey (use fID and fKey as existing identifiers).

---

Nitpick comments:
In `@openmeter/productcatalog/featureresolver/namespaced.go`:
- Around line 89-109: The BatchResolve implementation should normalize the
incoming idsOrKeys slice before using it in the DB query and pagination: inside
namespacedFeatureResolver.BatchResolve, remove empty strings and deduplicate
values from the idsOrKeys parameter (so Resolve's "" placeholders are dropped
and duplicates collapsed) and then pass that normalized slice to
pagination.NewPaginator / r.service.ListFeatures and use
min(len(normalizedSlice), 100) for the page size; update any references to
idsOrKeys in this function to use the normalized slice when constructing
feature.ListFeaturesParams and when computing the paginator size.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a8b9e00e-e7d1-4758-9b82-e499090e53f8

📥 Commits

Reviewing files that changed from the base of the PR and between 1ef4c89 and 2bf3ae2.

📒 Files selected for processing (23)
  • app/common/productcatalog.go
  • cmd/billing-worker/wire_gen.go
  • cmd/jobs/internal/wire_gen.go
  • cmd/server/wire_gen.go
  • e2e/addons_v3_test.go
  • openmeter/productcatalog/addon/service/addon.go
  • openmeter/productcatalog/addon/service/service.go
  • openmeter/productcatalog/addon/service/service_test.go
  • openmeter/productcatalog/featureresolver.go
  • openmeter/productcatalog/featureresolver/namespaced.go
  • openmeter/productcatalog/featureresolver/namespaced_test.go
  • openmeter/productcatalog/featureresolver/ratecard.go
  • openmeter/productcatalog/featureresolver/ratecard_test.go
  • openmeter/productcatalog/plan/adapter/adapter_test.go
  • openmeter/productcatalog/plan/service/plan.go
  • openmeter/productcatalog/plan/service/service.go
  • openmeter/productcatalog/plan/service/service_test.go
  • openmeter/productcatalog/ratecard.go
  • openmeter/productcatalog/testutils/env.go
  • openmeter/subscription/addon/extend_test.go
  • openmeter/subscription/testutils/service.go
  • test/billing/subscription_suite.go
  • test/customer/testenv.go
💤 Files with no reviewable changes (1)
  • openmeter/productcatalog/plan/adapter/adapter_test.go
✅ Files skipped from review due to trivial changes (2)
  • cmd/billing-worker/wire_gen.go
  • cmd/server/wire_gen.go
🚧 Files skipped from review as they are similar to previous changes (18)
  • e2e/addons_v3_test.go
  • openmeter/productcatalog/addon/service/service_test.go
  • openmeter/subscription/addon/extend_test.go
  • test/billing/subscription_suite.go
  • test/customer/testenv.go
  • openmeter/productcatalog/plan/service/service.go
  • openmeter/productcatalog/addon/service/service.go
  • openmeter/productcatalog/featureresolver/namespaced_test.go
  • openmeter/productcatalog/ratecard.go
  • openmeter/productcatalog/plan/service/plan.go
  • openmeter/productcatalog/plan/service/service_test.go
  • openmeter/productcatalog/addon/service/addon.go
  • openmeter/productcatalog/testutils/env.go
  • cmd/jobs/internal/wire_gen.go
  • openmeter/subscription/testutils/service.go
  • openmeter/productcatalog/featureresolver/ratecard_test.go
  • openmeter/productcatalog/featureresolver/ratecard.go
  • app/common/productcatalog.go

@chrisgacsal chrisgacsal marked this pull request as draft May 13, 2026 13:58
@chrisgacsal chrisgacsal marked this pull request as draft May 13, 2026 13:58
@chrisgacsal chrisgacsal force-pushed the refactor/rc-feature-resolve branch from 2bf3ae2 to 4663be0 Compare May 13, 2026 19:41
@chrisgacsal chrisgacsal marked this pull request as ready for review May 13, 2026 19:50
tothandras
tothandras previously approved these changes May 13, 2026
turip
turip previously approved these changes May 13, 2026
@chrisgacsal chrisgacsal dismissed stale reviews from turip and tothandras via cbe3887 May 13, 2026 21:22
@chrisgacsal chrisgacsal force-pushed the refactor/rc-feature-resolve branch from 4663be0 to cbe3887 Compare May 13, 2026 21:22
@chrisgacsal chrisgacsal merged commit c3b565e into main May 16, 2026
26 checks passed
@chrisgacsal chrisgacsal deleted the refactor/rc-feature-resolve branch May 16, 2026 05:33
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.

3 participants