Skip to content

fix(productcatalog): rate card validation#4268

Merged
chrisgacsal merged 6 commits into
mainfrom
fix/rate-card-validation
May 4, 2026
Merged

fix(productcatalog): rate card validation#4268
chrisgacsal merged 6 commits into
mainfrom
fix/rate-card-validation

Conversation

@tothandras
Copy link
Copy Markdown
Contributor

@tothandras tothandras commented May 1, 2026

Summary by CodeRabbit

  • Bug Fixes

    • Usage-based (non-flat) rate cards now require a feature key and reject references to non-metered features with clear validation errors.
    • Flat-fee rate cards remain valid without a feature key.
    • Mapping/backfill now populates missing rate-card feature keys from related feature data to ensure consistent validation.
  • Tests

    • Updated unit and end-to-end tests, fixtures, and e2e meter config to cover feature-bound rate cards, metered-feature validation, and addon/plan compatibility.

@tothandras tothandras requested a review from a team as a code owner May 1, 2026 20:44
@tothandras tothandras added the release-note/bug-fix Release note: Bug Fixes label May 1, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 1, 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 two validation errors and enforces that non-flat (usage-based) rate cards must supply a FeatureKey and reference a metered feature. Updates validation, feature-resolution checks, mapping/backfill, e2e helpers/config, and multiple tests/fixtures to create or bind features for usage-based rate cards.

Changes

Usage-based rate-card validation & wiring

Layer / File(s) Summary
Error declarations
openmeter/productcatalog/errors.go
Adds two exported errors: rate_card_usage_based_price_requires_feature_key and rate_card_usage_based_price_requires_metered_feature (critical, HTTP 400), each carrying featureKey.
Validation logic
openmeter/productcatalog/ratecard.go
RateCardMeta.Validate now requires FeatureKey (or FeatureID) when Price is non-flat (usage-based); missing feature triggers the new feature-key error.
Resolve-time enforcement
openmeter/productcatalog/.../addon.go, openmeter/productcatalog/plan/service/plan.go
resolveFeatures now fails for usage-based rate cards if the resolved feature is not metered (MeterID == nil), returning the new metered-feature validation error in all resolution branches.
Mapper backfill
openmeter/productcatalog/plan/adapter/mapping.go, openmeter/subscription/repo/mapping.go
Backfill RateCardMeta.FeatureKey when missing by using loaded Feature edge or item.Key for non-flat prices to ensure metadata presence during mapping.
E2E helpers & config
e2e/v3helpers_test.go, e2e/addons_v3_test.go, e2e/config.yaml
Add addon_meter to e2e config; introduce helper to create metered features and change tests to create features at runtime, passing their IDs/keys into unit/graduated rate-card fixtures.
Tests & fixtures updated
openmeter/productcatalog/*_test.go, openmeter/subscription/*_test.go, openmeter/productcatalog/plan/adapter/adapter_test.go, e2e/*, ...
Many tests updated to provision or bind features (IDs/keys) for usage-based rate cards; flat-price tests retain permissive behavior when FeatureKey is absent.
Client usage in tests
e2e/e2e_test.go
ListFeatures call in a test is now filtered by meter slug to scope results.

Sequence Diagram(s)

sequenceDiagram
    participant Test as Test Runner
    participant API as ProductCatalog API
    participant DB as Datastore
    participant FeatureSvc as Feature store (via API/v1)

    Test->>FeatureSvc: Create metered Feature (meter slug)
    FeatureSvc-->>Test: Return FeatureID, FeatureKey
    Test->>API: Submit RateCard (usage-based) with FeatureKey/FeatureID
    API->>DB: Persist RateCard + meta
    API->>DB: Load related Feature (resolveFeatures)
    DB-->>API: Return Feature (includes MeterID)
    alt Feature.MeterID != nil
        API-->>Test: Accept RateCard
    else Feature.MeterID == nil
        API-->>Test: Return validation error (requires metered feature)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

area/product-catalog, area/subscriptions

Suggested reviewers

  • mark-vass-konghq
  • borbelyr-kong
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.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 'fix(productcatalog): rate card validation' directly and accurately summarizes the main change—adding validation for rate card feature key requirements in the product catalog.
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 fix/rate-card-validation

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.

chrisgacsal
chrisgacsal previously approved these changes May 1, 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.

🧹 Nitpick comments (2)
e2e/addons_v3_test.go (2)

288-325: ⚡ Quick win

Add one negative case for the new validation.

This subtest only proves the metered happy path. A quick failure case for a missing or non-metered feature would make the new rule much harder to regress.

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

In `@e2e/addons_v3_test.go` around lines 288 - 325, Add a negative subtest to
TestV3AddonInstanceTypePriceCompatibility that exercises the new validation by
providing an incompatible combination (e.g., instanceType =
apiv3.AddonInstanceTypeSingle with a flat rate card, or
apiv3.AddonInstanceTypeMultiple with a unit rate card that references a
non-metered/missing feature) and expect http.StatusBadRequest; add the case to
the cases slice using the existing helper functions (validFlatRateCard,
validUnitRateCard, createTestFeature as needed) and update the assertions after
c.CreateAddon(body) to check tc.expectedStatus and assert addon is nil when the
status is not http.StatusCreated (and non-nil when it is).

155-185: ⚡ Quick win

Assert the feature refs on the round-trip too.

This test now pays the setup cost to create metered features, but it never checks that Feature.Id survives create/publish/get. A regression that accepts the request and drops the reference would still pass here.

♻️ Tiny follow-up
 gotUnit, ok := byKey[unit.Key]
 require.True(t, ok, "unit rate card missing on round-trip")
+require.NotNil(t, gotUnit.Feature)
+assert.Equal(t, unitFeatureID, gotUnit.Feature.Id)
 unitPrice, err := gotUnit.Price.AsBillingPriceUnit()
 require.NoError(t, err, "unit price should decode as BillingPriceUnit")
@@
 gotGraduated, ok := byKey[graduated.Key]
 require.True(t, ok, "graduated rate card missing on round-trip")
+require.NotNil(t, gotGraduated.Feature)
+assert.Equal(t, graduatedFeatureID, gotGraduated.Feature.Id)
 gradPrice, err := gotGraduated.Price.AsBillingPriceGraduated()
 require.NoError(t, err, "graduated price should decode as BillingPriceGraduated")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/addons_v3_test.go` around lines 155 - 185, The test currently verifies
RateCards round-trip but doesn't assert that the metered feature references
survive create/publish/get; update the assertions after retrieving got (from
c.GetAddon(addon.Id)) to verify each returned rate card still references the
original feature IDs: for gotFlat, gotUnit, and gotGraduated assert that the
rate card's feature reference field (e.g. Feature.Id or FeatureRef.Id on
apiv3.BillingRateCard) equals the original feature id variables used when
creating the rate cards (the variables named flat, unit, graduated or their
associated feature ids), so any regression that drops the Feature.Id will fail
the test. Ensure you use the exact field name on apiv3.BillingRateCard (Feature
or FeatureRef) present in the codebase when adding these assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@e2e/addons_v3_test.go`:
- Around line 288-325: Add a negative subtest to
TestV3AddonInstanceTypePriceCompatibility that exercises the new validation by
providing an incompatible combination (e.g., instanceType =
apiv3.AddonInstanceTypeSingle with a flat rate card, or
apiv3.AddonInstanceTypeMultiple with a unit rate card that references a
non-metered/missing feature) and expect http.StatusBadRequest; add the case to
the cases slice using the existing helper functions (validFlatRateCard,
validUnitRateCard, createTestFeature as needed) and update the assertions after
c.CreateAddon(body) to check tc.expectedStatus and assert addon is nil when the
status is not http.StatusCreated (and non-nil when it is).
- Around line 155-185: The test currently verifies RateCards round-trip but
doesn't assert that the metered feature references survive create/publish/get;
update the assertions after retrieving got (from c.GetAddon(addon.Id)) to verify
each returned rate card still references the original feature IDs: for gotFlat,
gotUnit, and gotGraduated assert that the rate card's feature reference field
(e.g. Feature.Id or FeatureRef.Id on apiv3.BillingRateCard) equals the original
feature id variables used when creating the rate cards (the variables named
flat, unit, graduated or their associated feature ids), so any regression that
drops the Feature.Id will fail the test. Ensure you use the exact field name on
apiv3.BillingRateCard (Feature or FeatureRef) present in the codebase when
adding these assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 868fb748-f743-4a73-83a0-e7b427c46993

📥 Commits

Reviewing files that changed from the base of the PR and between 6d3d5a7 and 8155d05.

📒 Files selected for processing (9)
  • e2e/addons_v3_test.go
  • e2e/config.yaml
  • e2e/v3helpers_test.go
  • openmeter/productcatalog/addon/service/addon.go
  • openmeter/productcatalog/errors.go
  • openmeter/productcatalog/plan/adapter/adapter_test.go
  • openmeter/productcatalog/plan/service/plan.go
  • openmeter/subscription/addon/diff/restore_test.go
  • openmeter/subscription/workflow/service/addon_test.go
✅ Files skipped from review due to trivial changes (1)
  • e2e/config.yaml

chrisgacsal
chrisgacsal previously approved these changes May 2, 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
openmeter/subscription/repo/mapping.go (1)

131-137: 💤 Low value

Tiny optional: consider a local copy instead of &item.Key

featureKey = &item.Key is GC-safe since item is heap-allocated and the GC will keep it alive. That said, a local copy makes it clearer that the returned struct doesn't implicitly hold a reference to the DB entity:

✨ Optional tweak
-	if featureKey == nil && item.Price != nil && item.Price.Type() != productcatalog.FlatPriceType {
-		featureKey = &item.Key
-	}
+	if featureKey == nil && item.Price != nil && item.Price.Type() != productcatalog.FlatPriceType {
+		key := item.Key
+		featureKey = &key
+	}

Totally fine to leave as-is though — this is purely a readability/intent thing.

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

In `@openmeter/subscription/repo/mapping.go` around lines 131 - 137, The code
currently assigns featureKey = &item.Key which works but can ambiguously tie the
returned pointer to the DB entity; instead create a local copy of item.Key and
assign the pointer to that local (e.g. copyKey := item.Key; featureKey =
&copyKey) when setting featureKey inside the conditional that checks item.Price
and productcatalog.FlatPriceType (referencing featureKey, item.Key, item.Price,
and productcatalog.FlatPriceType).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@e2e/e2e_test.go`:
- Around line 1563-1565: Replace context.Background() with t.Context() in the
ListFeaturesWithResponse call (the call to ListFeaturesWithResponse) so the
request is tied to the test lifecycle; also in the "Slow: Reset testing" subtest
update the Get/List call that currently passes nil params to instead pass a
filtered params object (e.g., MeterSlug with the meterSlug slice used above) and
assert on the filtered results rather than require.Len(t, features, 1) so the
test is deterministic even when other features exist.

---

Nitpick comments:
In `@openmeter/subscription/repo/mapping.go`:
- Around line 131-137: The code currently assigns featureKey = &item.Key which
works but can ambiguously tie the returned pointer to the DB entity; instead
create a local copy of item.Key and assign the pointer to that local (e.g.
copyKey := item.Key; featureKey = &copyKey) when setting featureKey inside the
conditional that checks item.Price and productcatalog.FlatPriceType (referencing
featureKey, item.Key, item.Price, and productcatalog.FlatPriceType).
🪄 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: a91e78bd-f635-435e-836f-0d59f1ffb45a

📥 Commits

Reviewing files that changed from the base of the PR and between c343d77 and 0cf17fe.

📒 Files selected for processing (3)
  • e2e/e2e_test.go
  • openmeter/productcatalog/plan/adapter/mapping.go
  • openmeter/subscription/repo/mapping.go

Comment thread e2e/e2e_test.go Outdated
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: András Tóth <4157749+tothandras@users.noreply.github.com>
@chrisgacsal chrisgacsal merged commit 1ac96d9 into main May 4, 2026
29 checks passed
@chrisgacsal chrisgacsal deleted the fix/rate-card-validation branch May 4, 2026 08:02
@chrisgacsal chrisgacsal added the release-note/ignore Ignore this change when generating release notes label May 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-note/bug-fix Release note: Bug Fixes 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