Skip to content

feat(taxcode): prevent deletion of plan-referenced tax codes and validate tax codes on plan publish#4557

Draft
borbelyr-kong wants to merge 3 commits into
fix/tax-code-deletefrom
feat/taxcode-plan-publish-validation
Draft

feat(taxcode): prevent deletion of plan-referenced tax codes and validate tax codes on plan publish#4557
borbelyr-kong wants to merge 3 commits into
fix/tax-code-deletefrom
feat/taxcode-plan-publish-validation

Conversation

@borbelyr-kong

@borbelyr-kong borbelyr-kong commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai

Summary by CodeRabbit

  • New Features

    • Plan publishing now validates referenced tax codes and surfaces missing or deleted tax-code issues clearly.
    • Rate cards can now report their linked tax-code reference for validation.
  • Bug Fixes

    • Publishing a plan now fails when a referenced tax code no longer exists or has been deleted.
    • Validation errors now point to the specific phase or rate card involved.
  • Tests

    • Added coverage for tax-code resolution, missing references, and publish-time validation behavior.

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ae2692fd-0f59-485f-840f-5990d361e147

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/taxcode-plan-publish-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.

@borbelyr-kong

Copy link
Copy Markdown
Contributor Author

@greptileai review this draft

@greptile-apps

greptile-apps Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds two complementary safeguards around tax code lifecycle: (1) a namespace-scoped filter was missing from the PreDelete hook in planvalidator.go, which could have caused plans in unrelated namespaces to block tax code deletion; and (2) a new TaxCodeResolver interface and implementation validate that every rate card's tax code is present and not soft-deleted before a plan is allowed to publish.

  • Bug fix: PreDelete now passes Namespaces: []string{tc.Namespace} to ListPlans, restricting the reference check to the correct namespace.
  • New validation: ValidatePlanWithTaxCodes + ValidateRateCardsWithTaxCodes run at publish time, returning ErrRateCardTaxCodeNotFound (HTTP 400) for each rate card whose tax code is missing or deleted.
  • Wiring: TaxCodeResolver is injected into planservice.Config, with nil-guard, and threaded through all three wire entry points and test helpers.

Confidence Score: 4/5

Safe to merge after addressing the empty-id error-classification edge case; the core delete-prevention and publish-time validation logic is correct.

The namespace fix in the PreDelete hook and the publish-time tax code validation are implemented correctly and consistently follow the existing FeatureResolver pattern. Two small gaps worth noting: the concrete resolver doesn't guard against an empty id string, causing a system-level validation error to surface instead of a tidy not-found response; and the unit-test stub ignores the id argument, leaving the lookup-key path untested. Neither blocks the core flows.

openmeter/productcatalog/taxcoderesolver/resolver.go — missing empty-id guard; openmeter/productcatalog/taxcoderesolver_test.go — stub resolver does not assert on the id passed to it.

Important Files Changed

Filename Overview
openmeter/taxcode/service/hooks/planvalidator.go Adds Namespaces filter to PreDelete hook's ListPlans call — this was the key bug fix preventing cross-namespace false positives when checking for plan references.
openmeter/productcatalog/taxcoderesolver/resolver.go New concrete TaxCodeResolver/NamespacedTaxCodeResolver implementation; validates namespace but not id, so an empty-string TaxCodeID will surface a system-level validation error rather than a user-friendly not-found error.
openmeter/productcatalog/ratecard.go Adds ValidateRateCardsWithTaxCodes; tc == nil branch is unreachable with the concrete resolver (which always returns non-nil on success), but is correct defensive code for the interface.
openmeter/productcatalog/plan.go Adds ValidatePlanWithTaxCodes wrapping per-phase rate card validation with correct field-path prefixing.
openmeter/productcatalog/plan/service/plan.go PublishPlan now validates all rate card tax codes via ValidatePlanWithTaxCodes; placement is consistent with existing feature validation pattern.
openmeter/productcatalog/taxcoderesolver_test.go Unit tests for ValidateRateCardsWithTaxCodes; stub resolver ignores the id argument, so the tests cannot verify the correct TaxCodeID is being looked up from the rate card.
openmeter/productcatalog/errors.go Adds ErrRateCardTaxCodeNotFound validation issue with correct HTTP 400 status and field path taxConfig.
openmeter/productcatalog/taxcoderesolver.go Clean interface definitions for TaxCodeResolver and NamespacedTaxCodeResolver, mirroring the existing FeatureResolver pattern.
openmeter/productcatalog/plan/service/service.go Adds TaxCodeResolver to Config with nil-guard, consistent with existing FeatureResolver pattern.
app/common/productcatalog.go Wires NewTaxCodeResolver into the Plan wire.Set and threads it through NewPlanService correctly.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant TaxCodeService
    participant PlanValidatorHook
    participant PlanService
    participant TaxCodeResolver

    Note over Client,TaxCodeResolver: Tax code deletion flow (PreDelete hook)
    Client->>TaxCodeService: DeleteTaxCode(id)
    TaxCodeService->>PlanValidatorHook: PreDelete(tc)
    PlanValidatorHook->>PlanService: "ListPlans(namespaces=[tc.Namespace], status=[active,archived,draft,scheduled], taxCodes=[tc.ID])"
    PlanService-->>PlanValidatorHook: matched plans (up to 5)
    alt plans reference this tax code
        PlanValidatorHook-->>TaxCodeService: error: ErrTaxCodeReferencedByPlan
        TaxCodeService-->>Client: 409 Conflict
    else no plans reference it
        PlanValidatorHook-->>TaxCodeService: nil (allow delete)
        TaxCodeService-->>Client: 200 OK (deleted)
    end

    Note over Client,TaxCodeResolver: Plan publish flow (tax code validation)
    Client->>PlanService: PublishPlan(namespace, planID)
    PlanService->>PlanService: ValidatePlanWithTaxCodes(plan)
    loop each phase
        loop each rate card with TaxConfig
            PlanService->>TaxCodeResolver: WithNamespace(ns).ResolveTaxCode(taxCodeID)
            TaxCodeResolver->>TaxCodeService: GetTaxCode(namespace, id)
            alt tax code exists and not deleted
                TaxCodeService-->>PlanService: TaxCode
            else not found or deleted
                TaxCodeService-->>PlanService: "TaxCodeNotFoundError or DeletedAt != nil"
                PlanService-->>Client: 400 ErrRateCardTaxCodeNotFound
            end
        end
    end
    PlanService-->>Client: Plan published
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Client
    participant TaxCodeService
    participant PlanValidatorHook
    participant PlanService
    participant TaxCodeResolver

    Note over Client,TaxCodeResolver: Tax code deletion flow (PreDelete hook)
    Client->>TaxCodeService: DeleteTaxCode(id)
    TaxCodeService->>PlanValidatorHook: PreDelete(tc)
    PlanValidatorHook->>PlanService: "ListPlans(namespaces=[tc.Namespace], status=[active,archived,draft,scheduled], taxCodes=[tc.ID])"
    PlanService-->>PlanValidatorHook: matched plans (up to 5)
    alt plans reference this tax code
        PlanValidatorHook-->>TaxCodeService: error: ErrTaxCodeReferencedByPlan
        TaxCodeService-->>Client: 409 Conflict
    else no plans reference it
        PlanValidatorHook-->>TaxCodeService: nil (allow delete)
        TaxCodeService-->>Client: 200 OK (deleted)
    end

    Note over Client,TaxCodeResolver: Plan publish flow (tax code validation)
    Client->>PlanService: PublishPlan(namespace, planID)
    PlanService->>PlanService: ValidatePlanWithTaxCodes(plan)
    loop each phase
        loop each rate card with TaxConfig
            PlanService->>TaxCodeResolver: WithNamespace(ns).ResolveTaxCode(taxCodeID)
            TaxCodeResolver->>TaxCodeService: GetTaxCode(namespace, id)
            alt tax code exists and not deleted
                TaxCodeService-->>PlanService: TaxCode
            else not found or deleted
                TaxCodeService-->>PlanService: "TaxCodeNotFoundError or DeletedAt != nil"
                PlanService-->>Client: 400 ErrRateCardTaxCodeNotFound
            end
        end
    end
    PlanService-->>Client: Plan published
Loading

Fix All in Claude Code

Reviews (1): Last reviewed commit: "feat(taxcode): prevent deletion of plan-..." | Re-trigger Greptile

Comment on lines +23 to +27
func (s stubTaxCodeResolver) Namespace() string { return s.namespace }

func (s stubTaxCodeResolver) ResolveTaxCode(_ context.Context, _ string) (*taxcode.TaxCode, error) {
return s.result, s.err
}

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.

P2 Stub resolver ignores the id argument — test can't verify correct lookup key

stubTaxCodeResolver.ResolveTaxCode discards the id parameter and always returns the preset result/err. This means the tests cannot catch a bug where the wrong field (e.g. rc.TaxConfig.TaxCodeID vs. some other field) is passed to the resolver. Adding an expectedID string field to the stub and asserting on it (or using require.Equal(t, taxCodeID, capturedID)) would make the tests meaningful for the lookup-key path.

Fix in Claude Code

Comment on lines +43 to +46
func (r *resolver) ResolveTaxCode(ctx context.Context, namespace string, id string) (*taxcode.TaxCode, error) {
if namespace == "" {
return nil, errors.New("namespace is not set")
}

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.

P2 Empty id produces a misleading system error instead of a user-friendly not-found error

When TaxCodeID is a non-nil pointer to an empty string, GetTaxCodeInput.Validate() returns ErrResourceIDEmpty — a validation issue that is neither IsGenericNotFoundError nor IsTaxCodeNotFoundError. The caller in ValidateRateCardsWithTaxCodes therefore falls into the generic "failed to resolve tax code for ratecard: ..." branch rather than surfacing ErrRateCardTaxCodeNotFound. An early guard here keeps the error classification consistent.

Suggested change
func (r *resolver) ResolveTaxCode(ctx context.Context, namespace string, id string) (*taxcode.TaxCode, error) {
if namespace == "" {
return nil, errors.New("namespace is not set")
}
func (r *resolver) ResolveTaxCode(ctx context.Context, namespace string, id string) (*taxcode.TaxCode, error) {
if namespace == "" {
return nil, errors.New("namespace is not set")
}
if id == "" {
return nil, errors.New("id is not set")
}

Fix in Claude Code

Comment on lines +970 to +972
if tc == nil || tc.DeletedAt != nil {
errs = append(errs, models.ErrorWithFieldPrefix(rateCardFieldSelector, ErrRateCardTaxCodeNotFound))
}

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.

P2 tc == nil branch is unreachable with the concrete resolver

resolver.ResolveTaxCode either returns an error or &tc (a non-nil pointer). A (nil, nil) return is impossible from the production implementation, so tc == nil can never be true here. The DeletedAt check alone is sufficient. Since the NamespacedTaxCodeResolver interface doesn't prohibit (nil, nil) from other implementations, keeping the guard is defensible — but a comment explaining the intent would help future readers.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
openmeter/productcatalog/taxcoderesolver_test.go (1)

84-85: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use t.Context() instead of context.Background() in this test.

Line 84 should use the test-scoped context so cancellation/deadline stays tied to the test harness.

🔧 Proposed change
-	ctx := context.Background()
+	ctx := t.Context()

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

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

In `@openmeter/productcatalog/taxcoderesolver_test.go` around lines 84 - 85,
Replace the `context.Background()` call with `t.Context()` in the ctx variable
initialization to ensure the context is properly tied to the test harness for
correct cancellation and deadline management. The ctx variable assignment should
use t.Context() instead of context.Background() to align with the coding
guidelines for Go tests.

Source: Coding guidelines

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

Nitpick comments:
In `@openmeter/productcatalog/taxcoderesolver_test.go`:
- Around line 84-85: Replace the `context.Background()` call with `t.Context()`
in the ctx variable initialization to ensure the context is properly tied to the
test harness for correct cancellation and deadline management. The ctx variable
assignment should use t.Context() instead of context.Background() to align with
the coding guidelines for Go tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0ec3f2f6-65e0-432f-97b6-52cd84a58039

📥 Commits

Reviewing files that changed from the base of the PR and between a46a043 and 8a31078.

📒 Files selected for processing (17)
  • app/common/productcatalog.go
  • cmd/billing-worker/wire_gen.go
  • cmd/jobs/internal/wire_gen.go
  • cmd/server/wire_gen.go
  • openmeter/productcatalog/errors.go
  • openmeter/productcatalog/plan.go
  • openmeter/productcatalog/plan/service/plan.go
  • openmeter/productcatalog/plan/service/service.go
  • openmeter/productcatalog/ratecard.go
  • openmeter/productcatalog/taxcoderesolver.go
  • openmeter/productcatalog/taxcoderesolver/resolver.go
  • openmeter/productcatalog/taxcoderesolver_test.go
  • openmeter/productcatalog/testutils/env.go
  • openmeter/subscription/testutils/service.go
  • openmeter/taxcode/service/hooks/planvalidator.go
  • test/billing/subscription_suite.go
  • test/customer/testenv.go

@borbelyr-kong borbelyr-kong force-pushed the feat/taxcode-plan-publish-validation branch 2 times, most recently from 7a906df to 2f9c5ab Compare June 24, 2026 12:28
@borbelyr-kong borbelyr-kong force-pushed the feat/taxcode-plan-publish-validation branch from 2f9c5ab to 3a58a03 Compare June 24, 2026 13:10

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

892-893: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Small nudge: derive from t.Context().

You can build the cancelable context off the test context so cancellation stays tied to the harness (and t.Context() is already canceled at cleanup, so this stays nicely scoped):

♻️ Suggested tweak
-	ctx, cancel := context.WithCancel(context.Background())
+	ctx, cancel := context.WithCancel(t.Context())
 	defer cancel()

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

🤖 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/taxcode_test.go` around lines 892 -
893, The test context setup is using context.Background() directly instead of
the test harness context. Update the cancelable context creation in
taxcode_test.go to derive from t.Context() so it stays scoped to the test
lifecycle, keeping the existing cancel behavior while tying cancellation to the
test cleanup.

Source: Coding guidelines

openmeter/productcatalog/taxcoderesolver_test.go (1)

84-84: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Tiny one: prefer t.Context() here.

Since t is right there, t.Context() keeps cancellation tied to the test harness. Quick swap:

♻️ Suggested tweak
-	ctx := context.Background()
+	ctx := t.Context()

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

🤖 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/taxcoderesolver_test.go` at line 84, The test setup
currently creates a standalone context with context.Background() instead of
using the test-scoped context. Update the test in taxcoderesolver_test.go to use
t.Context() wherever the local testing.T is available, so cancellation and
deadlines stay tied to the test harness; this should be applied at the ctx
initialization near the TaxCodeResolver test setup.

Source: Coding guidelines

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

Nitpick comments:
In `@openmeter/productcatalog/plan/service/taxcode_test.go`:
- Around line 892-893: The test context setup is using context.Background()
directly instead of the test harness context. Update the cancelable context
creation in taxcode_test.go to derive from t.Context() so it stays scoped to the
test lifecycle, keeping the existing cancel behavior while tying cancellation to
the test cleanup.

In `@openmeter/productcatalog/taxcoderesolver_test.go`:
- Line 84: The test setup currently creates a standalone context with
context.Background() instead of using the test-scoped context. Update the test
in taxcoderesolver_test.go to use t.Context() wherever the local testing.T is
available, so cancellation and deadlines stay tied to the test harness; this
should be applied at the ctx initialization near the TaxCodeResolver test setup.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2eb85a87-7d87-4680-bbb4-0807a71c8fa2

📥 Commits

Reviewing files that changed from the base of the PR and between 8a31078 and 3a58a03.

📒 Files selected for processing (17)
  • app/common/productcatalog.go
  • cmd/billing-worker/wire_gen.go
  • cmd/jobs/internal/wire_gen.go
  • cmd/server/wire_gen.go
  • openmeter/productcatalog/errors.go
  • openmeter/productcatalog/plan.go
  • openmeter/productcatalog/plan/service/plan.go
  • openmeter/productcatalog/plan/service/service.go
  • openmeter/productcatalog/plan/service/taxcode_test.go
  • openmeter/productcatalog/ratecard.go
  • openmeter/productcatalog/taxcoderesolver.go
  • openmeter/productcatalog/taxcoderesolver/resolver.go
  • openmeter/productcatalog/taxcoderesolver_test.go
  • openmeter/productcatalog/testutils/env.go
  • openmeter/subscription/testutils/service.go
  • test/billing/subscription_suite.go
  • test/customer/testenv.go
✅ Files skipped from review due to trivial changes (2)
  • cmd/billing-worker/wire_gen.go
  • cmd/jobs/internal/wire_gen.go
🚧 Files skipped from review as they are similar to previous changes (11)
  • openmeter/productcatalog/taxcoderesolver.go
  • test/billing/subscription_suite.go
  • openmeter/productcatalog/errors.go
  • openmeter/productcatalog/plan/service/service.go
  • cmd/server/wire_gen.go
  • openmeter/productcatalog/plan.go
  • openmeter/productcatalog/testutils/env.go
  • test/customer/testenv.go
  • openmeter/productcatalog/ratecard.go
  • openmeter/productcatalog/plan/service/plan.go
  • app/common/productcatalog.go

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant