Skip to content

feat(productcatalog): eager load tax_code on plan rate cards and expo…#3945

Merged
borbelyr-kong merged 16 commits intomainfrom
feat/tax-codes-double-write-plan
Mar 24, 2026
Merged

feat(productcatalog): eager load tax_code on plan rate cards and expo…#3945
borbelyr-kong merged 16 commits intomainfrom
feat/tax-codes-double-write-plan

Conversation

@borbelyr-kong
Copy link
Copy Markdown
Contributor

@borbelyr-kong borbelyr-kong commented Mar 14, 2026

Summary by CodeRabbit

  • New Features

    • Tax code resolution added: services now resolve/create tax codes for Stripe-mapped rate cards and persist tax code IDs alongside legacy tax config.
    • Adapters and loaders now include tax-code data and backfill legacy tax config from dedicated DB columns.
  • Tests

    • Extensive tests added for tax-code dual-write, backfill, reuse, update semantics, and addon/plan interactions.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 14, 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

Integrates TaxCode support across product catalog and subscription flows: adds TaxCode service methods, resolves/creates tax codes during plan/addon create/update, persists/backfills tax data on rate cards, wires the TaxCode dependency through app initialization, and adds extensive tests for dual-write and backfill behavior.

Changes

Cohort / File(s) Summary
Service & API surface
openmeter/taxcode/service.go, openmeter/taxcode/repository.go, openmeter/taxcode/service/taxcode.go
Added app-mapping inputs and two service methods (GetTaxCodeByAppMapping, GetOrCreateByAppMapping), validation for inputs, repository interface extension, and transactional creation-with-retry logic.
Adapter & repository
openmeter/taxcode/adapter/taxcode.go, openmeter/taxcode/adapter/mapping.go, openmeter/taxcode/repository.go
New adapter method for JSONB app-mapping lookup, renamed export MapTaxCodeFromEntity, moved validation outside transactions, and repository interface change to support app-mapping queries.
TaxCode model & errors
openmeter/taxcode/taxcode.go, openmeter/taxcode/errors.go
Added GetAppMapping helper on TaxCode and new error constructor / detection for app-mapping not-found scenarios.
Product catalog models
openmeter/productcatalog/tax.go, openmeter/productcatalog/ratecard.go, openmeter/productcatalog/tax_test.go
Added TaxCodeID to TaxConfig with clone/equal/merge support; added TaxCode pointer to RateCardMeta; updated tests for equality/merge/clone.
Backfill helper
openmeter/productcatalog/adapter/taxconfig.go
New BackfillTaxConfig to reconstruct legacy TaxConfig from dedicated DB columns + TaxCode entity (Stripe mapping, behavior, TaxCodeID).
Plan service & wiring
openmeter/productcatalog/plan/service/service.go, openmeter/productcatalog/plan/service/plan.go
Added TaxCode dependency to service config, resolveTaxCodes helper, and wired tax resolution into CreatePlan/UpdatePlan.
Addon service & wiring
openmeter/productcatalog/addon/service/service.go, openmeter/productcatalog/addon/service/addon.go
Added required TaxCode dependency to config, resolveTaxCodes helper, and tax resolution in CreateAddon/UpdateAddon.
Adapters: mapping & eager-loading
openmeter/productcatalog/plan/adapter/mapping.go, openmeter/productcatalog/addon/adapter/mapping.go, openmeter/productcatalog/plan/adapter/phase.go, openmeter/productcatalog/plan/adapter/plan.go, openmeter/productcatalog/addon/adapter/addon.go
Eager-load .WithTaxCode() for rate-cards, map loaded TaxCode via MapTaxCodeFromEntity, call BackfillTaxConfig, and persist dedicated columns (tax_code_id, tax_behavior) when writing rate cards.
Persistence & repo queries
openmeter/subscription/addon/repo/subscriptionaddon.go
Updated subscription-addon query to eager-load addon rate-cards with .WithFeatures().WithTaxCode() so tax code edge is available.
Wiring / application init
app/common/productcatalog.go, cmd/*/wire.go, cmd/*/wire_gen.go (cmd/billing-worker/wire.go, cmd/billing-worker/wire_gen.go, cmd/jobs/internal/wire.go, cmd/jobs/internal/wire_gen.go, cmd/server/wire_gen.go)
Added TaxCode provider to Wire graphs, created TaxCode adapter/service in generated init, and passed taxCodeService into NewPlanService and NewAddonService calls. Minor notification variable rename in generated code.
Tests & test helpers
many test files (openmeter/productcatalog/plan/service/taxcode_test.go, openmeter/productcatalog/addon/service/taxcode_test.go, openmeter/productcatalog/plan/service/service_test.go, openmeter/productcatalog/addon/service/service_test.go, openmeter/productcatalog/testutils/env.go, openmeter/subscription/testutils/service.go, openmeter/subscription/addon/diff/apply_test.go, openmeter/productcatalog/testutils/env.go, test/*, openmeter/server/server_test.go)
Added large integration/unit tests for dual-write and backfill behaviors, updated test envs and mocks to construct and inject a taxcode.Service, and added no-op methods to test NoopTaxCodeService. Many tests assert persisted tax_code_id/tax_behavior and TaxCode entity creation/reuse.
Billing profile conversion
api/v3/handlers/billingprofiles/convert.go
Added // goverter:ignore TaxCodeID directive above ConvertBillingTaxConfigToTaxConfig to ignore TaxCodeID in that conversion mapping.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant PlanService as Plan Service
    participant TaxCodeSvc as Tax Code Service
    participant Adapter as Rate Card Adapter
    participant DB as Database

    Client->>PlanService: CreatePlan(phases with RateCards)
    PlanService->>PlanService: resolveFeatures()
    PlanService->>PlanService: resolveTaxCodes()
    PlanService->>TaxCodeSvc: GetOrCreateByAppMapping(namespace, AppTypeStripe, stripeCode)
    TaxCodeSvc->>DB: Query TaxCode by app_mappings
    alt TaxCode exists
        DB-->>TaxCodeSvc: TaxCode
    else Not found
        TaxCodeSvc->>DB: Create TaxCode with AppMapping
        DB-->>TaxCodeSvc: New TaxCode
    end
    TaxCodeSvc-->>PlanService: TaxCode (with ID)
    PlanService->>Adapter: Persist RateCard (TaxCodeID, TaxBehavior)
    Adapter->>DB: INSERT/UPDATE with dedicated columns
    DB-->>Adapter: Success
    Adapter-->>PlanService: Success
    PlanService-->>Client: Plan with resolved TaxCodeID
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

area/product-catalog

Suggested reviewers

  • tothandras
  • GAlexIHU
🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title describes the main change (eager loading tax codes on plan/addon rate cards), which aligns with a significant portion of the changeset, but it's truncated and incomplete. Complete the title to fully convey the scope—e.g., 'feat(productcatalog): eager load tax_code on plan and addon rate cards' or similar, making it clear what is actually being exposed.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/tax-codes-double-write-plan

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.

@borbelyr-kong borbelyr-kong added the release-note/ignore Ignore this change when generating release notes label Mar 14, 2026
@borbelyr-kong borbelyr-kong force-pushed the feat/tax-codes-v3-add-fk-keys branch from 360fb25 to 47540a1 Compare March 19, 2026 15:43
Base automatically changed from feat/tax-codes-v3-add-fk-keys to main March 19, 2026 16:28
Comment thread openmeter/productcatalog/plan/adapter/mapping.go Outdated
Comment thread openmeter/productcatalog/plan/adapter/mapping.go Outdated
Comment thread openmeter/productcatalog/plan/service/plan.go Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
openmeter/productcatalog/ratecard.go (1)

142-181: ⚠️ Potential issue | 🟡 Minor

Equal() doesn't compare the new TaxCode field.

The Equal() method compares all other fields in RateCardMeta but omits the new TaxCode field. If two rate cards should be considered different when their tax codes differ, this needs updating.

If the intentional behavior is to ignore TaxCode during equality checks (e.g., because it's only for display/eager loading), adding a comment would clarify that decision.

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

In `@openmeter/productcatalog/ratecard.go` around lines 142 - 181, The Equal
method on RateCardMeta omits the new TaxCode field; update RateCardMeta.Equal to
compare TaxCode (use the same pointer-safe comparison style as
FeatureKey/FeatureID, e.g., lo.FromPtr(r.TaxCode) vs lo.FromPtr(v.TaxCode)) and
return false if they differ, or if the intended semantics are to ignore TaxCode
add a clarifying comment above RateCardMeta.Equal explaining why it is excluded;
reference the RateCardMeta.Equal method and the TaxCode field when making the
change.
openmeter/productcatalog/tax.go (1)

88-99: ⚠️ Potential issue | 🟡 Minor

Keep the cloning pattern consistent across all pointer fields.

Good catch on deep-copying TaxCodeID! Though Behavior is also a pointer field, it's assigned directly instead of being deep-copied like Stripe. For consistency and maintainability, all pointer fields should follow the same cloning pattern.

🛠️ Suggested fix
 func (c TaxConfig) Clone() TaxConfig {
-	out := TaxConfig{
-		Behavior: c.Behavior,
-	}
+	out := TaxConfig{}
+
+	if c.Behavior != nil {
+		out.Behavior = lo.ToPtr(*c.Behavior)
+	}
 
 	if c.Stripe != nil {
 		out.Stripe = lo.ToPtr(c.Stripe.Clone())
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/productcatalog/tax.go` around lines 88 - 99, The Clone method for
TaxConfig assigns the pointer field Behavior directly rather than deep-copying
it like Stripe and TaxCodeID; update TaxConfig.Clone to deep-copy Behavior as
well (e.g. in the same pattern used for Stripe/TaxCodeID: check if c.Behavior !=
nil and set out.Behavior to a new pointer holding a cloned or dereferenced value
using lo.ToPtr or the Behavior.Clone() method as appropriate) so all pointer
fields are cloned consistently in TaxConfig.Clone.
openmeter/productcatalog/plan/adapter/mapping.go (1)

339-341: ⚠️ Potential issue | 🟡 Minor

Error variable may be nil in this error message.

The err variable referenced here comes from the ParsePtrOrNil() call on line 300. If that succeeded but we hit this default case, err will be nil, leading to a confusing error message like invalid RateCard type foo: <nil>.

🐛 Suggested fix
 	default:
-		return nil, fmt.Errorf("invalid RateCard type %s: %w", r.Type, err)
+		return nil, fmt.Errorf("invalid RateCard type %s", r.Type)
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/productcatalog/plan/adapter/mapping.go` around lines 339 - 341, The
default error return in the switch uses err (from ParsePtrOrNil) which may be
nil, producing messages like "invalid RateCard type foo: <nil>"; update the
default case in mapping.go to avoid appending a nil error—e.g., check if err !=
nil and return fmt.Errorf("invalid RateCard type %s: %w", r.Type, err) otherwise
return fmt.Errorf("invalid RateCard type %s", r.Type); ensure you reference the
ParsePtrOrNil result and avoid shadowing the err variable.
🧹 Nitpick comments (5)
openmeter/taxcode/service.go (1)

125-171: Consider consolidating duplicate input types.

GetTaxCodeByAppMappingInput and GetOrCreateByAppMappingInput have identical structures and validation logic. You could define a shared base type or use a single input type with a type alias if the semantic distinction matters:

type AppMappingLookupInput struct {
    Namespace string
    AppType   app.AppType
    TaxCode   string
}

type GetTaxCodeByAppMappingInput = AppMappingLookupInput
type GetOrCreateByAppMappingInput = AppMappingLookupInput

That said, keeping them separate is totally fine if you anticipate them diverging in the future.

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

In `@openmeter/taxcode/service.go` around lines 125 - 171, Replace the duplicated
structs and validation by introducing a single shared type (e.g.,
AppMappingLookupInput) containing Namespace, AppType and TaxCode, move the
Validate() implementation to that type (Validate on AppMappingLookupInput) and
then create type aliases GetTaxCodeByAppMappingInput = AppMappingLookupInput and
GetOrCreateByAppMappingInput = AppMappingLookupInput so existing code referring
to GetTaxCodeByAppMappingInput and GetOrCreateByAppMappingInput continues to
work; ensure you remove the duplicate Validate methods for the old names and
keep references to AppType.Validate() and
models.NewNillableGenericValidationError(errors.Join(...)) unchanged.
api/v3/handlers/billingprofiles/convert.go (1)

69-71: FIXME noted — consider tracking this.

The goverter ignore for TaxCodeID makes sense as a temporary measure. Just a friendly nudge to ensure this gets tracked (issue/ticket) so it doesn't get forgotten once the OpenAPI schema is updated.

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

In `@api/v3/handlers/billingprofiles/convert.go` around lines 69 - 71, The
goverter ignore on TaxCodeID in the ConvertBillingTaxConfigToTaxConfig
declaration is a temporary workaround; create and reference a tracking
ticket/issue for removing the "// goverter:ignore TaxCodeID" once the OpenAPI
schema is updated, then update this comment to include the issue number (e.g.,
"TODO: remove goverter:ignore TaxCodeID — tracked in ISSUE-1234") and add a
brief note in the function comment for ConvertBillingTaxConfigToTaxConfig so
reviewers can find and remove the ignore when the schema change lands.
openmeter/productcatalog/plan/service/service_test.go (1)

214-216: Consider adding a nil check for robustness.

The extraction logic assumes TaxConfig and TaxCodeID are populated. While the test setup guarantees this, a quick require.NotNil would make the test more self-documenting and catch regressions early if the tax code resolution ever fails silently.

taxConfig := draftPlanV1.Phases[0].RateCards[0].AsMeta().TaxConfig
require.NotNil(t, taxConfig, "TaxConfig should be populated after plan creation")
require.NotNil(t, taxConfig.TaxCodeID, "TaxCodeID should be resolved during plan creation")
taxCodeID := taxConfig.TaxCodeID
🤖 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 214 -
216, Add explicit nil assertions before extracting TaxCodeID from draftPlanV1 to
make the test robust: retrieve taxConfig via
draftPlanV1.Phases[0].RateCards[0].AsMeta().TaxConfig, call require.NotNil(t,
taxConfig, "TaxConfig should be populated after plan creation") and
require.NotNil(t, taxConfig.TaxCodeID, "TaxCodeID should be resolved during plan
creation"), then assign taxCodeID := taxConfig.TaxCodeID; this ensures TaxConfig
and TaxCodeID are validated before use in the test.
openmeter/productcatalog/plan/service/taxcode_test.go (1)

23-38: Consider adding test coverage for UsageBasedRateCard.

The current tests comprehensively cover FlatFeeRateCard scenarios, but the production code in resolveTaxCodes has different handling for UsageBasedRateCard (BillingCadence is required and non-pointer). Adding a test case with a usage-based rate card + tax config would validate that code path.

Something like:

t.Run("UsageBasedWithStripeCode", func(t *testing.T) {
    ubp := &productcatalog.UsageBasedRateCard{
        RateCardMeta: productcatalog.RateCardMeta{
            Key:  features[0].Key,
            Name: features[0].Name,
            TaxConfig: &productcatalog.TaxConfig{
                Stripe: &productcatalog.StripeTaxConfig{Code: "txcd_ubp_test"},
            },
            // ... price config
        },
        BillingCadence: MonthPeriod, // non-pointer
    }
    // ... test assertions
})

Based on learnings: In productcatalog.UsageBasedRateCard, the BillingCadence field is a non-pointer isodate.Period, while in productcatalog.FlatFeeRateCard, BillingCadence is a pointer type.

Also applies to: 67-337

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

In `@openmeter/productcatalog/plan/service/taxcode_test.go` around lines 23 - 38,
Tests only exercise FlatFeeRateCard but production resolveTaxCodes also handles
UsageBasedRateCard (with non-pointer BillingCadence); add a test covering
UsageBasedRateCard to validate that path. Create a helper similar to
newTestFlatRateCard (e.g., newTestUsageBasedRateCard) that returns a
*productcatalog.UsageBasedRateCard with RateCardMeta including a TaxConfig
(StripeTaxConfig.Code set) and a non-pointer BillingCadence (e.g., MonthPeriod),
then add a t.Run case (e.g., "UsageBasedWithStripeCode") asserting
resolveTaxCodes returns the expected stripe tax code mapping for that feature;
reference resolveTaxCodes, productcatalog.UsageBasedRateCard, RateCardMeta, and
StripeTaxConfig in the new test.
openmeter/productcatalog/plan/service/plan.go (1)

185-240: Consider extracting the rate card rebuild logic to reduce duplication.

The rate card type-switching and rebuild pattern (lines 212-236) duplicates what's already in resolveFeatures (lines 120-142, 154-176). The existing FIXME on line 37 mentions this is "a bit brittle."

This isn't blocking, but consolidating into a shared helper (e.g., rebuildRateCardWithMeta(rc RateCard, meta RateCardMeta) (RateCard, error)) would reduce duplication and make future rate card type additions easier.

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

In `@openmeter/productcatalog/plan/service/plan.go` around lines 185 - 240, The
rate-card rebuild switch in resolveTaxCodes duplicates logic from
resolveFeatures; extract that logic into a shared helper (e.g.,
rebuildRateCardWithMeta(rc productcatalog.RateCard, meta
productcatalog.RateCardMeta) (productcatalog.RateCard, error)) that inspects
rc.Type(), uses rc.GetBillingCadence() when needed, validates required billing
cadence for UsageBasedRateCardType, constructs and returns the appropriate
productcatalog.FlatFeeRateCard or productcatalog.UsageBasedRateCard with the
provided meta, and returns an error for unsupported types or missing cadence;
then call this helper from both resolveTaxCodes and resolveFeatures and keep the
existing rc.Merge(newRC) call and error handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openmeter/productcatalog/ratecard.go`:
- Around line 83-84: The RateCard.Clone() method currently omits the newly added
TaxCode field so cloned RateCard instances lose the eagerly-loaded tax
reference; update the Clone() implementation(s) that copy RateCard data (look
for the Clone method on RateCard and any helper clone functions in the 93-140
region) to also copy the TaxCode pointer (preserving nil vs non-nil) into the
cloned struct, or add a clarifying comment in Clone() that omitting TaxCode is
intentional if it must remain read-only/eager-only.

In `@openmeter/productcatalog/tax_test.go`:
- Around line 156-191: Add a test case that exercises TaxConfig.Clone(): create
a TaxConfig with pointer fields (e.g., TaxCodeID), call Clone() and assert the
cloned value is equal via Equal(), then mutate the clone (change the TaxCodeID)
and assert the original is unchanged and pointers are not the same; place this
alongside the existing Equal()/MergeTaxConfigs() cases so the test suite catches
any shallow-copy regressions in TaxConfig.Clone().

In `@openmeter/taxcode/service/taxcode.go`:
- Around line 58-87: The code in GetOrCreateByAppMapping calls
s.adapter.GetTaxCodeByAppMapping and treats any error as "not found" — change
this so you only proceed to create when the error is an explicit not-found; if
err != nil and !models.IsNotFoundError(err) (or the project’s equivalent
not-found checker) return the error immediately, otherwise continue to build the
key and call s.adapter.CreateTaxCode; keep the existing concurrent-create
conflict handling using models.IsGenericConflictError and the subsequent
GetTaxCodeByAppMapping call.

---

Outside diff comments:
In `@openmeter/productcatalog/plan/adapter/mapping.go`:
- Around line 339-341: The default error return in the switch uses err (from
ParsePtrOrNil) which may be nil, producing messages like "invalid RateCard type
foo: <nil>"; update the default case in mapping.go to avoid appending a nil
error—e.g., check if err != nil and return fmt.Errorf("invalid RateCard type %s:
%w", r.Type, err) otherwise return fmt.Errorf("invalid RateCard type %s",
r.Type); ensure you reference the ParsePtrOrNil result and avoid shadowing the
err variable.

In `@openmeter/productcatalog/ratecard.go`:
- Around line 142-181: The Equal method on RateCardMeta omits the new TaxCode
field; update RateCardMeta.Equal to compare TaxCode (use the same pointer-safe
comparison style as FeatureKey/FeatureID, e.g., lo.FromPtr(r.TaxCode) vs
lo.FromPtr(v.TaxCode)) and return false if they differ, or if the intended
semantics are to ignore TaxCode add a clarifying comment above
RateCardMeta.Equal explaining why it is excluded; reference the
RateCardMeta.Equal method and the TaxCode field when making the change.

In `@openmeter/productcatalog/tax.go`:
- Around line 88-99: The Clone method for TaxConfig assigns the pointer field
Behavior directly rather than deep-copying it like Stripe and TaxCodeID; update
TaxConfig.Clone to deep-copy Behavior as well (e.g. in the same pattern used for
Stripe/TaxCodeID: check if c.Behavior != nil and set out.Behavior to a new
pointer holding a cloned or dereferenced value using lo.ToPtr or the
Behavior.Clone() method as appropriate) so all pointer fields are cloned
consistently in TaxConfig.Clone.

---

Nitpick comments:
In `@api/v3/handlers/billingprofiles/convert.go`:
- Around line 69-71: The goverter ignore on TaxCodeID in the
ConvertBillingTaxConfigToTaxConfig declaration is a temporary workaround; create
and reference a tracking ticket/issue for removing the "// goverter:ignore
TaxCodeID" once the OpenAPI schema is updated, then update this comment to
include the issue number (e.g., "TODO: remove goverter:ignore TaxCodeID —
tracked in ISSUE-1234") and add a brief note in the function comment for
ConvertBillingTaxConfigToTaxConfig so reviewers can find and remove the ignore
when the schema change lands.

In `@openmeter/productcatalog/plan/service/plan.go`:
- Around line 185-240: The rate-card rebuild switch in resolveTaxCodes
duplicates logic from resolveFeatures; extract that logic into a shared helper
(e.g., rebuildRateCardWithMeta(rc productcatalog.RateCard, meta
productcatalog.RateCardMeta) (productcatalog.RateCard, error)) that inspects
rc.Type(), uses rc.GetBillingCadence() when needed, validates required billing
cadence for UsageBasedRateCardType, constructs and returns the appropriate
productcatalog.FlatFeeRateCard or productcatalog.UsageBasedRateCard with the
provided meta, and returns an error for unsupported types or missing cadence;
then call this helper from both resolveTaxCodes and resolveFeatures and keep the
existing rc.Merge(newRC) call and error handling.

In `@openmeter/productcatalog/plan/service/service_test.go`:
- Around line 214-216: Add explicit nil assertions before extracting TaxCodeID
from draftPlanV1 to make the test robust: retrieve taxConfig via
draftPlanV1.Phases[0].RateCards[0].AsMeta().TaxConfig, call require.NotNil(t,
taxConfig, "TaxConfig should be populated after plan creation") and
require.NotNil(t, taxConfig.TaxCodeID, "TaxCodeID should be resolved during plan
creation"), then assign taxCodeID := taxConfig.TaxCodeID; this ensures TaxConfig
and TaxCodeID are validated before use in the test.

In `@openmeter/productcatalog/plan/service/taxcode_test.go`:
- Around line 23-38: Tests only exercise FlatFeeRateCard but production
resolveTaxCodes also handles UsageBasedRateCard (with non-pointer
BillingCadence); add a test covering UsageBasedRateCard to validate that path.
Create a helper similar to newTestFlatRateCard (e.g., newTestUsageBasedRateCard)
that returns a *productcatalog.UsageBasedRateCard with RateCardMeta including a
TaxConfig (StripeTaxConfig.Code set) and a non-pointer BillingCadence (e.g.,
MonthPeriod), then add a t.Run case (e.g., "UsageBasedWithStripeCode") asserting
resolveTaxCodes returns the expected stripe tax code mapping for that feature;
reference resolveTaxCodes, productcatalog.UsageBasedRateCard, RateCardMeta, and
StripeTaxConfig in the new test.

In `@openmeter/taxcode/service.go`:
- Around line 125-171: Replace the duplicated structs and validation by
introducing a single shared type (e.g., AppMappingLookupInput) containing
Namespace, AppType and TaxCode, move the Validate() implementation to that type
(Validate on AppMappingLookupInput) and then create type aliases
GetTaxCodeByAppMappingInput = AppMappingLookupInput and
GetOrCreateByAppMappingInput = AppMappingLookupInput so existing code referring
to GetTaxCodeByAppMappingInput and GetOrCreateByAppMappingInput continues to
work; ensure you remove the duplicate Validate methods for the old names and
keep references to AppType.Validate() and
models.NewNillableGenericValidationError(errors.Join(...)) unchanged.
🪄 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: 76d2fa06-08c7-4b11-96ce-1bad47181fca

📥 Commits

Reviewing files that changed from the base of the PR and between fb0b4a2 and 7133e3c.

📒 Files selected for processing (29)
  • api/v3/handlers/billingprofiles/convert.go
  • app/common/productcatalog.go
  • cmd/billing-worker/wire.go
  • cmd/billing-worker/wire_gen.go
  • cmd/jobs/internal/wire.go
  • cmd/jobs/internal/wire_gen.go
  • cmd/server/wire_gen.go
  • openmeter/productcatalog/plan/adapter/mapping.go
  • openmeter/productcatalog/plan/adapter/phase.go
  • openmeter/productcatalog/plan/adapter/plan.go
  • openmeter/productcatalog/plan/service/plan.go
  • openmeter/productcatalog/plan/service/service.go
  • openmeter/productcatalog/plan/service/service_test.go
  • openmeter/productcatalog/plan/service/taxcode_test.go
  • openmeter/productcatalog/ratecard.go
  • openmeter/productcatalog/tax.go
  • openmeter/productcatalog/tax_test.go
  • openmeter/productcatalog/testutils/env.go
  • openmeter/server/server_test.go
  • openmeter/subscription/addon/diff/apply_test.go
  • openmeter/subscription/testutils/service.go
  • openmeter/taxcode/adapter/mapping.go
  • openmeter/taxcode/adapter/taxcode.go
  • openmeter/taxcode/repository.go
  • openmeter/taxcode/service.go
  • openmeter/taxcode/service/taxcode.go
  • openmeter/taxcode/taxcode.go
  • test/billing/subscription_suite.go
  • test/customer/testenv.go

Comment thread openmeter/productcatalog/ratecard.go
Comment thread openmeter/productcatalog/tax_test.go
Comment thread openmeter/taxcode/service/taxcode.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: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openmeter/productcatalog/adapter/taxconfig.go`:
- Around line 11-39: BackfillTaxConfig must also populate the TaxCodeID when
rebuilding a nil cfg: in BackfillTaxConfig, after creating cfg (cfg =
&productcatalog.TaxConfig{}) and before returning, set cfg.TaxCodeID (if it's
empty/nil) from the provided taxcode object (use tc.GetId() when tc != nil),
similar to how cfg.Behavior and cfg.Stripe are set so the backfill path
preserves the TaxCodeID field consistent with the dual-write path.

In `@openmeter/productcatalog/addon/adapter/mapping.go`:
- Around line 102-115: The FromPlanRateCardRow path never backfills legacy tax
fields from the eagerly-loaded TaxCode like the addon path does; update the
FromPlanRateCardRow handling to call r.Edges.TaxCodeOrErr(), map it via
taxcodeadapter.MapTaxCodeFromEntity, set meta.TaxCode, and then call
productcatalogadapter.BackfillTaxConfig(meta.TaxConfig, r.TaxBehavior,
meta.TaxCode) (same sequence as in the addon branch) so plan-phase rate cards
receive the same backfilled TaxConfig and TaxCode values.

In `@openmeter/productcatalog/addon/service/addon.go`:
- Around line 503-506: In UpdateAddon, move the call to s.resolveTaxCodes(ctx,
params.Namespace, params.RateCards) so it runs after the existing add-on is
loaded and after the draft-only/status validation (i.e., after the code that
enforces update allowed only for draft status), not before; this prevents
running the get-or-create write on failing/unauthorized updates and avoids
creating orphan tax codes when the update is rejected. Ensure resolveTaxCodes
remains using params.Namespace and params.RateCards and only executes once the
status check on the loaded add-on passes.

In `@openmeter/productcatalog/addon/service/taxcode_test.go`:
- Around line 527-533: The test is missing an assertion for the backfilled
TaxCodeID; update the test block that reads tc := backfillRC.AsMeta().TaxConfig
to also require/not-nil and assert the expected TaxCodeID value (e.g.,
require.NotNil or require.Equal on tc.TaxCodeID) so the dedicated-column read
path preserves TaxConfig.TaxCodeID; reference the existing variables backfillRC,
tc, and TaxConfig.TaxCodeID when adding the assertion.
🪄 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: 5a0996d7-7bdd-4ea3-b897-1a83eb8c662c

📥 Commits

Reviewing files that changed from the base of the PR and between 7133e3c and 8451fa5.

📒 Files selected for processing (22)
  • app/common/productcatalog.go
  • cmd/billing-worker/wire_gen.go
  • cmd/jobs/internal/wire_gen.go
  • cmd/server/wire_gen.go
  • openmeter/productcatalog/adapter/taxconfig.go
  • openmeter/productcatalog/addon/adapter/addon.go
  • openmeter/productcatalog/addon/adapter/mapping.go
  • openmeter/productcatalog/addon/service/addon.go
  • openmeter/productcatalog/addon/service/service.go
  • openmeter/productcatalog/addon/service/service_test.go
  • openmeter/productcatalog/addon/service/taxcode_test.go
  • openmeter/productcatalog/plan/adapter/mapping.go
  • openmeter/productcatalog/plan/adapter/plan.go
  • openmeter/productcatalog/plan/service/taxcode_test.go
  • openmeter/productcatalog/ratecard.go
  • openmeter/productcatalog/tax.go
  • openmeter/productcatalog/tax_test.go
  • openmeter/productcatalog/testutils/env.go
  • openmeter/subscription/addon/repo/subscriptionaddon.go
  • openmeter/subscription/testutils/service.go
  • test/billing/subscription_suite.go
  • test/customer/testenv.go
✅ Files skipped from review due to trivial changes (3)
  • cmd/billing-worker/wire_gen.go
  • cmd/jobs/internal/wire_gen.go
  • cmd/server/wire_gen.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • openmeter/productcatalog/plan/adapter/plan.go
  • openmeter/productcatalog/plan/adapter/mapping.go
  • app/common/productcatalog.go
  • openmeter/productcatalog/plan/service/taxcode_test.go

Comment thread openmeter/productcatalog/adapter/taxconfig.go
Comment thread openmeter/productcatalog/addon/adapter/mapping.go
Comment thread openmeter/productcatalog/addon/service/addon.go
Comment thread openmeter/productcatalog/addon/service/taxcode_test.go
@borbelyr-kong borbelyr-kong marked this pull request as ready for review March 23, 2026 15:39
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 (1)
openmeter/productcatalog/addon/service/taxcode_test.go (1)

433-443: Consider asserting TaxCodeID for completeness.

The test verifies Behavior and Stripe are preserved on read-back, but doesn't check TaxCodeID. Since this test covers the full round-trip (create → get), asserting TaxCodeID would ensure the resolution path is fully validated.

🧪 Suggested assertion to add
 tc := getFirstAddonRCTaxConfig(t, fetched)
 require.NotNil(t, tc)
+
+// TaxCodeID should be present
+require.NotNil(t, tc.TaxCodeID, "TaxCodeID must be resolved and preserved")

 // Behavior should be present
 require.NotNil(t, tc.Behavior)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/productcatalog/addon/service/taxcode_test.go` around lines 433 -
443, The test checks Behavior and Stripe on the read-back tax config but omits
asserting the TaxCodeID; update the test after calling getFirstAddonRCTaxConfig
(variable tc) to assert that tc.TaxCodeID is present and equals the expected tax
code identifier used when creating the resource (e.g.,
assert.NotNil(tc.TaxCodeID) and assert.Equal(t, expectedTaxCodeID, tc.TaxCodeID)
where expectedTaxCodeID is the tax code ID variable from the create/setup step).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@openmeter/productcatalog/addon/service/taxcode_test.go`:
- Around line 433-443: The test checks Behavior and Stripe on the read-back tax
config but omits asserting the TaxCodeID; update the test after calling
getFirstAddonRCTaxConfig (variable tc) to assert that tc.TaxCodeID is present
and equals the expected tax code identifier used when creating the resource
(e.g., assert.NotNil(tc.TaxCodeID) and assert.Equal(t, expectedTaxCodeID,
tc.TaxCodeID) where expectedTaxCodeID is the tax code ID variable from the
create/setup step).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5b745e11-2fcc-4347-94b5-4db7707ddf5c

📥 Commits

Reviewing files that changed from the base of the PR and between 8451fa5 and d04acf9.

📒 Files selected for processing (5)
  • openmeter/productcatalog/adapter/taxconfig.go
  • openmeter/productcatalog/addon/service/taxcode_test.go
  • openmeter/productcatalog/plan/service/taxcode_test.go
  • openmeter/productcatalog/testutils/env.go
  • openmeter/subscription/testutils/service.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • openmeter/productcatalog/adapter/taxconfig.go
  • openmeter/productcatalog/plan/service/taxcode_test.go

@borbelyr-kong borbelyr-kong requested a review from turip March 23, 2026 15:57
Comment thread openmeter/productcatalog/addon/adapter/mapping.go
Comment thread openmeter/taxcode/service/taxcode.go Outdated
Comment on lines +62 to +64
if err == nil {
return tc, nil
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
if err == nil {
return tc, nil
}
if err != nil && !errors.Is(err, taxcode.ErrTaxCodeNotFound) {
return taxcode.TaxCode{}, err
}
if err == nil { // If taxcode is returned let's just return it to the caller
return tc, nil
}

It's better as if there is any other error except of the not found we are propagating that downstream.

Also generally speaking in go you almost always follow each call returning an err with the if err != nil (think as those are always coupled).

Comment thread openmeter/taxcode/adapter/taxcode.go Outdated
Comment on lines +126 to +128
if err := input.Validate(); err != nil {
return taxcode.TaxCode{}, err
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: you can do input validation before entutils.TransactingRepo / transaction.Run (it's a bit more efficient)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Changed everywhere in the file to be consistent.


// GetOrCreateByAppMapping looks up a TaxCode by its app mapping. If none exists,
// it creates one with a key derived from the app-specific code.
func (s *service) GetOrCreateByAppMapping(ctx context.Context, input taxcode.GetOrCreateByAppMappingInput) (taxcode.TaxCode, error) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please validate the input before passing downstream in case the implementation changes later.

})
}

func (s *service) GetTaxCodeByAppMapping(ctx context.Context, input taxcode.GetTaxCodeByAppMappingInput) (taxcode.TaxCode, error) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: you can consider validating the input here even if the adapter does that (when the service is a simple passthru it's not that important)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Changed everywhere in the file to be consistent.

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/taxcode/errors.go (1)

60-71: Optional cleanup: extract shared not-found issue builder.

Line [61] and Line [67] repeat the same base chain (ErrTaxCodeNotFound.WithPathString(...)) and only differ by attrs. A tiny helper would keep this easier to extend and reduce drift.

💡 Small refactor sketch
+func newTaxCodeNotFoundIssue(path string) models.ValidationIssue {
+	return ErrTaxCodeNotFound.WithPathString(path)
+}
+
 func NewTaxCodeNotFoundError(id string) error {
-	return ErrTaxCodeNotFound.
-		WithPathString("id").
+	return newTaxCodeNotFoundIssue("id").
 		WithAttr("id", id)
 }
 
 func NewTaxCodeByAppMappingNotFoundError(appType, taxCode string) error {
-	return ErrTaxCodeNotFound.
-		WithPathString("app_mappings").
+	return newTaxCodeNotFoundIssue("app_mappings").
 		WithAttr("app_type", appType).
 		WithAttr("tax_code", taxCode)
 }

As per coding guidelines, "In general when reviewing the Golang code make readability and maintainability a priority, even potentially suggest restructuring the code to improve them."

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

In `@openmeter/taxcode/errors.go` around lines 60 - 71, Extract the repeated base
builder into a small helper (e.g., buildTaxCodeNotFound or
taxCodeNotFoundWithPath) that takes the differing path string and returns
ErrTaxCodeNotFound.WithPathString(path), then use that helper inside
NewTaxCodeNotFoundError and NewTaxCodeByAppMappingNotFoundError to chain
WithAttr calls; reference ErrTaxCodeNotFound, NewTaxCodeNotFoundError and
NewTaxCodeByAppMappingNotFoundError to locate the current duplication and
replace the duplicated WithPathString(...) start of the chain with the helper
call.
openmeter/productcatalog/addon/adapter/mapping.go (1)

102-115: Consider extracting the tax-code hydrate/backfill path into a shared helper.

FromAddonRateCardRow and FromPlanRateCardRow now repeat the same TaxCodeOrErrMapTaxCodeFromEntityBackfillTaxConfig flow. A small helper would keep the addon and plan mappers from drifting the next time tax handling changes.

As per coding guidelines, **/*.go: In general when reviewing the Golang code make readability and maintainability a priority, even potentially suggest restructuring the code to improve them.

Also applies to: 302-315

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

In `@openmeter/productcatalog/addon/adapter/mapping.go` around lines 102 - 115,
Both FromAddonRateCardRow and FromPlanRateCardRow duplicate the TaxCode
hydrate/backfill flow; extract that into a shared helper (e.g.,
hydrateAndBackfillTaxForRateCard) that accepts the rate card row (the object
with Edges.TaxCodeOrErr and TaxBehavior) and a pointer to meta (with
TaxConfig/TaxCode) and performs: call TaxCodeOrErr, map via
taxcodeadapter.MapTaxCodeFromEntity, set meta.TaxCode, then call
productcatalogadapter.BackfillTaxConfig(meta.TaxConfig, r.TaxBehavior,
meta.TaxCode); update both FromAddonRateCardRow and FromPlanRateCardRow to call
this helper and propagate any errors returned.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openmeter/productcatalog/addon/service/taxcode_test.go`:
- Around line 63-449: The tests in TestAddonTaxCodeDualWrite only assert the
returned TaxConfig and TaxCode entity but don't verify the dedicated DB columns;
after each CreateAddon and UpdateAddon call (e.g., within the "Create" and
"Update" subtests such as "StripeCodeOnly", "ChangeStripeCode",
"RemoveTaxConfig", etc.), perform a direct read of the persisted AddonRateCard
row (use the same DB accessor used elsewhere or a helper that queries the
AddonRateCard table for the addon—refer to env.Addon.CreateAddon /
env.Addon.UpdateAddon and the rate card keys) and assert that the dedicated
columns tax_code_id and tax_behavior (or the DB column names used by
asAddonRateCardRow) contain the expected values (nil or the TaxCode ID and
expected behavior enum) to ensure the dedicated columns—not just JSONB—were
written.

---

Nitpick comments:
In `@openmeter/productcatalog/addon/adapter/mapping.go`:
- Around line 102-115: Both FromAddonRateCardRow and FromPlanRateCardRow
duplicate the TaxCode hydrate/backfill flow; extract that into a shared helper
(e.g., hydrateAndBackfillTaxForRateCard) that accepts the rate card row (the
object with Edges.TaxCodeOrErr and TaxBehavior) and a pointer to meta (with
TaxConfig/TaxCode) and performs: call TaxCodeOrErr, map via
taxcodeadapter.MapTaxCodeFromEntity, set meta.TaxCode, then call
productcatalogadapter.BackfillTaxConfig(meta.TaxConfig, r.TaxBehavior,
meta.TaxCode); update both FromAddonRateCardRow and FromPlanRateCardRow to call
this helper and propagate any errors returned.

In `@openmeter/taxcode/errors.go`:
- Around line 60-71: Extract the repeated base builder into a small helper
(e.g., buildTaxCodeNotFound or taxCodeNotFoundWithPath) that takes the differing
path string and returns ErrTaxCodeNotFound.WithPathString(path), then use that
helper inside NewTaxCodeNotFoundError and NewTaxCodeByAppMappingNotFoundError to
chain WithAttr calls; reference ErrTaxCodeNotFound, NewTaxCodeNotFoundError and
NewTaxCodeByAppMappingNotFoundError to locate the current duplication and
replace the duplicated WithPathString(...) start of the chain with the helper
call.
🪄 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: c094852f-7245-476a-a1d3-affa0003b4bc

📥 Commits

Reviewing files that changed from the base of the PR and between d04acf9 and 5b9e864.

📒 Files selected for processing (6)
  • openmeter/productcatalog/addon/adapter/mapping.go
  • openmeter/productcatalog/addon/service/taxcode_test.go
  • openmeter/productcatalog/plan/adapter/mapping.go
  • openmeter/taxcode/adapter/taxcode.go
  • openmeter/taxcode/errors.go
  • openmeter/taxcode/service/taxcode.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • openmeter/taxcode/adapter/taxcode.go
  • openmeter/productcatalog/plan/adapter/mapping.go
  • openmeter/taxcode/service/taxcode.go

Comment thread openmeter/productcatalog/addon/service/taxcode_test.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: 1

♻️ Duplicate comments (1)
openmeter/productcatalog/addon/service/taxcode_test.go (1)

482-495: ⚠️ Potential issue | 🟡 Minor

Please assert TaxCodeID on the GetAddon readback too.

This subtest proves Stripe/Behavior survived the fetch, but it never checks the fetched TaxConfig.TaxCodeID. If the adapter stops wiring that field on reads, this path still stays green because the raw DB assertion is separate from the returned payload.

🧪 Minimal assertion to add
 			tc := getFirstAddonRCTaxConfig(t, fetched)
 			require.NotNil(t, tc)
+			require.NotNil(t, tc.TaxCodeID, "TaxCodeID must be present on readback")
 
 			// Behavior should be present
 			require.NotNil(t, tc.Behavior)
 			assert.Equal(t, productcatalog.ExclusiveTaxBehavior, *tc.Behavior)
@@
 			tcEntity13, err := findAddonTaxCodeByStripeCode(t, ctx, env.TaxCode, namespace, "txcd_13000001")
 			require.NoError(t, err)
+			assert.Equal(t, tcEntity13.ID, *tc.TaxCodeID)
 			assertAddonRCDBCols(t, ctx, env, fetched.ID, features[0].Key, lo.ToPtr(tcEntity13.ID), lo.ToPtr(productcatalog.ExclusiveTaxBehavior))

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."

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

In `@openmeter/productcatalog/addon/service/taxcode_test.go` around lines 482 -
495, The test is missing an assertion that the fetched Addon TaxConfig's
TaxCodeID is populated; after retrieving tc via getFirstAddonRCTaxConfig and
finding tcEntity13 with findAddonTaxCodeByStripeCode, add an assertion that
tc.TaxCodeID is non-nil and equals the ID of tcEntity13 (use the same pointer/ID
comparison style as assertAddonRCDBCols or require/assert helpers used
elsewhere) so the readback verifies TaxCodeID wiring in the returned payload.
🧹 Nitpick comments (1)
openmeter/productcatalog/addon/service/taxcode_test.go (1)

84-112: Might be worth extracting the shared meter/feature bootstrap.

The same env + namespace + meter/feature setup shows up in all three top-level tests. A small helper returning env, namespace, and features would keep each suite focused on the tax behavior it’s actually validating. As per coding guidelines, **/*.go: "In general when reviewing the Golang code make readability and maintainability a priority, even potentially suggest restructuring the code to improve them."

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

In `@openmeter/productcatalog/addon/service/taxcode_test.go` around lines 84 -
112, Extract the repeated test setup into a helper (e.g., setupTestEnvFeatures)
that creates and returns the TestEnv, namespace and []feature.Feature: call
pctestutils.NewTestEnv, defer/cleanup env.Close, call env.DBSchemaMigrate,
create namespace via pctestutils.NewTestNamespace, replace meters with
env.Meter.ReplaceMeters(pctestutils.NewTestMeters(...)), list meters via
env.Meter.ListMeters with meter.ListMetersParams, then create features by
calling env.Feature.CreateFeature for each pctestutils.NewTestFeatureFromMeter
result and return the env, namespace and features slice; update each top-level
test to call this helper and remove the duplicated lines.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openmeter/productcatalog/addon/service/taxcode_test.go`:
- Around line 313-461: Add a new subtest under the existing "Update" suite
(e.g., t.Run("TaxBehaviorOnlyUpdate", ...)) that creates an addon with a
RateCard containing a Stripe TaxConfig (Stripe.Code set, TaxBehavior nil), then
calls env.Addon.UpdateAddon with the same Stripe.Code but sets TaxBehavior to a
value (and another subcase clearing it), and asserts that
getFirstAddonRCTaxConfig returns the same TaxCodeID (unchanged) and same
Stripe.Code, while the tax_behavior column is updated as expected using
assertAddonRCDBCols; use env.Addon.CreateAddon, env.Addon.UpdateAddon,
getFirstAddonRCTaxConfig, findAddonTaxCodeByStripeCode and assertAddonRCDBCols
to verify TaxCode entity still exists and only the tax_behavior column changed.

---

Duplicate comments:
In `@openmeter/productcatalog/addon/service/taxcode_test.go`:
- Around line 482-495: The test is missing an assertion that the fetched Addon
TaxConfig's TaxCodeID is populated; after retrieving tc via
getFirstAddonRCTaxConfig and finding tcEntity13 with
findAddonTaxCodeByStripeCode, add an assertion that tc.TaxCodeID is non-nil and
equals the ID of tcEntity13 (use the same pointer/ID comparison style as
assertAddonRCDBCols or require/assert helpers used elsewhere) so the readback
verifies TaxCodeID wiring in the returned payload.

---

Nitpick comments:
In `@openmeter/productcatalog/addon/service/taxcode_test.go`:
- Around line 84-112: Extract the repeated test setup into a helper (e.g.,
setupTestEnvFeatures) that creates and returns the TestEnv, namespace and
[]feature.Feature: call pctestutils.NewTestEnv, defer/cleanup env.Close, call
env.DBSchemaMigrate, create namespace via pctestutils.NewTestNamespace, replace
meters with env.Meter.ReplaceMeters(pctestutils.NewTestMeters(...)), list meters
via env.Meter.ListMeters with meter.ListMetersParams, then create features by
calling env.Feature.CreateFeature for each pctestutils.NewTestFeatureFromMeter
result and return the env, namespace and features slice; update each top-level
test to call this helper and remove the duplicated lines.
🪄 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: 35f73f31-2b71-42e5-a122-945c955f5ce0

📥 Commits

Reviewing files that changed from the base of the PR and between 5b9e864 and 8d955f6.

📒 Files selected for processing (2)
  • openmeter/productcatalog/addon/service/taxcode_test.go
  • openmeter/productcatalog/plan/service/taxcode_test.go
✅ Files skipped from review due to trivial changes (1)
  • openmeter/productcatalog/plan/service/taxcode_test.go

Comment on lines +313 to +461
t.Run("Update", func(t *testing.T) {
t.Run("AddTaxConfig", func(t *testing.T) {
// Create addon without TaxConfig
input := newTestAddonInput(t, namespace, newTestAddonFlatRateCard(features[0], nil))
input.Key = "addon-update-add-tax"
input.Name = "Update Add Tax"

a, err := env.Addon.CreateAddon(ctx, input)
require.NoError(t, err)

tc := getFirstAddonRCTaxConfig(t, a)
assert.Nil(t, tc)

// Update to add TaxConfig with Stripe code
updatedRateCards := productcatalog.RateCards{
newTestAddonFlatRateCard(features[0], &productcatalog.TaxConfig{
Stripe: &productcatalog.StripeTaxConfig{Code: "txcd_70000001"},
}),
}

updated, err := env.Addon.UpdateAddon(ctx, addon.UpdateAddonInput{
NamespacedID: a.NamespacedID,
RateCards: &updatedRateCards,
})
require.NoError(t, err)

tc = getFirstAddonRCTaxConfig(t, updated)
require.NotNil(t, tc)
require.NotNil(t, tc.Stripe)
assert.Equal(t, "txcd_70000001", tc.Stripe.Code)
require.NotNil(t, tc.TaxCodeID, "TaxCodeID must be populated after update")

tcEntity, err := findAddonTaxCodeByStripeCode(t, ctx, env.TaxCode, namespace, "txcd_70000001")
require.NoError(t, err)
assert.Equal(t, *tc.TaxCodeID, tcEntity.ID)

assertAddonRCDBCols(t, ctx, env, updated.ID, features[0].Key, lo.ToPtr(tcEntity.ID), nil)
})

t.Run("ChangeStripeCode", func(t *testing.T) {
input := newTestAddonInput(t, namespace, newTestAddonFlatRateCard(features[0], &productcatalog.TaxConfig{
Stripe: &productcatalog.StripeTaxConfig{Code: "txcd_80000001"},
}))
input.Key = "addon-update-change-code"
input.Name = "Update Change Code"

a, err := env.Addon.CreateAddon(ctx, input)
require.NoError(t, err)

oldTC := getFirstAddonRCTaxConfig(t, a)
require.NotNil(t, oldTC)
require.NotNil(t, oldTC.TaxCodeID)
oldTaxCodeID := *oldTC.TaxCodeID

// Update to different stripe code
updatedRateCards := productcatalog.RateCards{
newTestAddonFlatRateCard(features[0], &productcatalog.TaxConfig{
Stripe: &productcatalog.StripeTaxConfig{Code: "txcd_90000001"},
}),
}

updated, err := env.Addon.UpdateAddon(ctx, addon.UpdateAddonInput{
NamespacedID: a.NamespacedID,
RateCards: &updatedRateCards,
})
require.NoError(t, err)

newTC := getFirstAddonRCTaxConfig(t, updated)
require.NotNil(t, newTC)
require.NotNil(t, newTC.Stripe)
assert.Equal(t, "txcd_90000001", newTC.Stripe.Code)
require.NotNil(t, newTC.TaxCodeID)

assert.NotEqual(t, oldTaxCodeID, *newTC.TaxCodeID, "new stripe code must create a new TaxCode entity")

// Old TaxCode entity should still exist
_, err = findAddonTaxCodeByStripeCode(t, ctx, env.TaxCode, namespace, "txcd_80000001")
assert.NoError(t, err, "old TaxCode entity should still exist")

newTCEntity, err := findAddonTaxCodeByStripeCode(t, ctx, env.TaxCode, namespace, "txcd_90000001")
require.NoError(t, err)
assertAddonRCDBCols(t, ctx, env, updated.ID, features[0].Key, lo.ToPtr(newTCEntity.ID), nil)
})

t.Run("RemoveTaxConfig", func(t *testing.T) {
input := newTestAddonInput(t, namespace, newTestAddonFlatRateCard(features[0], &productcatalog.TaxConfig{
Stripe: &productcatalog.StripeTaxConfig{Code: "txcd_11000001"},
}))
input.Key = "addon-update-remove-tax"
input.Name = "Update Remove Tax"

a, err := env.Addon.CreateAddon(ctx, input)
require.NoError(t, err)

tc := getFirstAddonRCTaxConfig(t, a)
require.NotNil(t, tc)
require.NotNil(t, tc.TaxCodeID)

// Update to remove TaxConfig
updatedRateCards := productcatalog.RateCards{
newTestAddonFlatRateCard(features[0], nil),
}

updated, err := env.Addon.UpdateAddon(ctx, addon.UpdateAddonInput{
NamespacedID: a.NamespacedID,
RateCards: &updatedRateCards,
})
require.NoError(t, err)

tc = getFirstAddonRCTaxConfig(t, updated)
assert.Nil(t, tc, "TaxConfig should be nil after removal")

// TaxCode entity should still exist (orphaned, not deleted)
_, err = findAddonTaxCodeByStripeCode(t, ctx, env.TaxCode, namespace, "txcd_11000001")
assert.NoError(t, err, "TaxCode entity should not be deleted")

assertAddonRCDBCols(t, ctx, env, updated.ID, features[0].Key, nil, nil)
})

t.Run("MetadataOnlyUpdate", func(t *testing.T) {
input := newTestAddonInput(t, namespace, newTestAddonFlatRateCard(features[0], &productcatalog.TaxConfig{
Stripe: &productcatalog.StripeTaxConfig{Code: "txcd_12000001"},
}))
input.Key = "addon-update-metadata-only"
input.Name = "Update Metadata Only"

a, err := env.Addon.CreateAddon(ctx, input)
require.NoError(t, err)

tc := getFirstAddonRCTaxConfig(t, a)
require.NotNil(t, tc)
require.NotNil(t, tc.TaxCodeID)
originalTaxCodeID := *tc.TaxCodeID

// Update only addon name, no ratecards
updated, err := env.Addon.UpdateAddon(ctx, addon.UpdateAddonInput{
NamespacedID: a.NamespacedID,
Name: lo.ToPtr("Updated Name"),
})
require.NoError(t, err)

tc = getFirstAddonRCTaxConfig(t, updated)
require.NotNil(t, tc)
require.NotNil(t, tc.TaxCodeID)
assert.Equal(t, originalTaxCodeID, *tc.TaxCodeID, "TaxCodeID should be unchanged on metadata-only update")

assertAddonRCDBCols(t, ctx, env, updated.ID, features[0].Key, lo.ToPtr(originalTaxCodeID), nil)
})
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Please add an update case that only changes TaxBehavior.

Right now the update coverage exercises TaxCodeID churn and whole-config removal, but not the case where the Stripe code stays the same and only tax_behavior is added, changed, or cleared. A regression in the dedicated-column update path would still slip through. 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."

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

In `@openmeter/productcatalog/addon/service/taxcode_test.go` around lines 313 -
461, Add a new subtest under the existing "Update" suite (e.g.,
t.Run("TaxBehaviorOnlyUpdate", ...)) that creates an addon with a RateCard
containing a Stripe TaxConfig (Stripe.Code set, TaxBehavior nil), then calls
env.Addon.UpdateAddon with the same Stripe.Code but sets TaxBehavior to a value
(and another subcase clearing it), and asserts that getFirstAddonRCTaxConfig
returns the same TaxCodeID (unchanged) and same Stripe.Code, while the
tax_behavior column is updated as expected using assertAddonRCDBCols; use
env.Addon.CreateAddon, env.Addon.UpdateAddon, getFirstAddonRCTaxConfig,
findAddonTaxCodeByStripeCode and assertAddonRCDBCols to verify TaxCode entity
still exists and only the tax_behavior column changed.

@borbelyr-kong borbelyr-kong requested a review from turip March 24, 2026 14:48
Comment on lines +73 to 76
func IsTaxCodeNotFoundError(err error) bool {
var vi models.ValidationIssue
return errors.As(err, &vi) && vi.Code() == ErrCodeTaxCodeNotFound
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this is equivalent of errors.Is(err, ErrTaxCodeNotFound), but if it works I am fine with this.

@borbelyr-kong borbelyr-kong merged commit a211764 into main Mar 24, 2026
26 checks passed
@borbelyr-kong borbelyr-kong deleted the feat/tax-codes-double-write-plan branch March 24, 2026 15:21
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