Skip to content

feat(taxcode): prevent deletion of add-on-referenced tax codes#4561

Open
borbelyr-kong wants to merge 4 commits into
fix/tax-code-deletefrom
feat/taxcode-addon-delete-guard
Open

feat(taxcode): prevent deletion of add-on-referenced tax codes#4561
borbelyr-kong wants to merge 4 commits into
fix/tax-code-deletefrom
feat/taxcode-addon-delete-guard

Conversation

@borbelyr-kong

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

Copy link
Copy Markdown
Contributor

@coderabbitai

Summary by CodeRabbit

  • New Features
    • Add-ons can now be filtered by tax code in listings.
    • Add-ons are validated against tax code references before publishing.
    • Tax codes now have deletion protection when they’re still used by add-ons (via rate cards).
  • Bug Fixes
    • Deletion protection now reports clearer “referenced by rate cards” validation details (instead of plans).
    • Multiple related reference issues can be returned together.
  • Tests
    • Added coverage for tax-code filtering and add-on–tax-code deletion protection.

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR threads a TaxCodeResolver through addon service construction, adds addon publish/list validation against tax codes, and introduces tax-code deletion hooks that block removal when referenced by add-ons or plans. Server wiring and test environments are updated to construct and pass the new dependencies.

Changes

Tax code resolver + add-on protections

Layer / File(s) Summary
Addon service resolver wiring
app/common/productcatalog.go, openmeter/productcatalog/addon/service/service.go, cmd/billing-worker/wire_gen.go, cmd/jobs/internal/wire_gen.go, openmeter/productcatalog/testutils/env.go, openmeter/subscription/testutils/service.go, test/billing/subscription_suite.go, test/customer/testenv.go
NewAddonService and addon service config gain a required TaxCodeResolver, and the generated/runtime wiring paths now pass it through.
Addon publish and list tax code checks
openmeter/productcatalog/addon.go, openmeter/productcatalog/addon/service.go, openmeter/productcatalog/addon/service/addon.go, openmeter/productcatalog/addon/adapter/addon.go, openmeter/productcatalog/addon/adapter/adapter_test.go, openmeter/productcatalog/addon/service/taxcode_test.go
Addons are validated against rate-card tax codes during publish, ListAddons accepts a TaxCodes filter, and the adapter/tests cover filtering by tax code.
Tax code deletion errors and hooks
openmeter/taxcode/errors.go, openmeter/taxcode/service/hooks/addonhook.go, openmeter/taxcode/service/hooks/planhook.go, openmeter/taxcode/service/hooks/addonhook_test.go, openmeter/taxcode/service/hooks/planhook_test.go
Tax-code deletion errors now point at rate-card references, and the add-on/plan pre-delete hooks collect matching rate-card errors before blocking deletion.
Tax code addon hook wiring
app/common/taxcode.go, cmd/server/wire.go, cmd/server/wire_gen.go
A new add-on hook constructor is wired into server startup and exposed on the application struct.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested labels: release-note/bug-fix

Suggested reviewers: turip, chrisgacsal

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: blocking deletion of tax codes referenced by add-ons.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ 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/taxcode-addon-delete-guard

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 deletion protection for tax codes referenced by add-ons, mirroring the existing protection for plan-referenced tax codes. It also adds tax-code validation to the PublishAddon flow, a TaxCodes filter to ListAddons, and renames the error code from tax_code_referenced_by_plan to the more generic tax_code_referenced_by_rate_card.

  • A new AddonHook is registered on the tax-code service; its PreDelete queries live add-ons (all statuses including Invalid) filtered by tax-code ID and blocks deletion if any rate card still holds the reference.
  • PublishAddon now validates that every rate card's referenced tax code still exists, catching the case where a tax code was soft-deleted after the draft was created.
  • Both plan and addon hooks share the renamed error ErrCodeTaxCodeReferencedByRateCard; the old ErrCodeTaxCodeReferencedByPlan symbol is fully removed from the codebase.

Confidence Score: 5/5

Safe to merge; the deletion protection is correctly layered, all four addon statuses are covered, and the publish-time validation catches dangling references.

The new addon hook is structurally symmetric with the existing plan hook and the two-stage DB-then-in-memory check is sound. Wire-up is correct in all three binaries. The only items worth a second look are the page-size cap on error reporting and confirming whether the error code rename from tax_code_referenced_by_plan could affect already-shipped SDK consumers.

openmeter/taxcode/errors.go — confirm the old error code was never shipped to a stable/public release before the rename lands.

Important Files Changed

Filename Overview
openmeter/taxcode/service/hooks/addonhook.go New pre-delete hook for add-ons; covers all four addon statuses, uses DB-level TaxCodes filter then confirms in-memory, includes defensive fallback. PageSize capped at 5.
openmeter/taxcode/service/hooks/planhook.go Refactored to emit one error per rate card key (instead of one combined plan-IDs error); renames the error to ErrCodeTaxCodeReferencedByRateCard. PageSize still 5.
openmeter/taxcode/errors.go Error renamed from tax_code_referenced_by_plan → tax_code_referenced_by_rate_card; old symbol fully removed — breaking API contract change for any client that matched the old error code string.
openmeter/productcatalog/addon/service/addon.go Adds ValidateAddonWithTaxCodes call in PublishAddon, consistent with how feature validation works; catches dangling tax-code references at publish time.
openmeter/productcatalog/addon/adapter/addon.go Adds TaxCodes filter to ListAddons query using HasRatecardsWith predicate, correctly excluding soft-deleted rate cards.
app/common/taxcode.go Wires up NewTaxCodeAddonServiceHook; hook is registered only in server (not billing-worker/jobs), which is appropriate since tax-code deletion is API-only.
cmd/server/wire_gen.go taxCodeResolver creation moved earlier to satisfy addonService dependency; TaxCodeAddonHook added to Application struct and initialization sequence.
openmeter/productcatalog/addon/service/taxcode_test.go New test TestAddonPublishRejectsDeletedTaxCode validates the publish-time tax-code check by deliberately bypassing the pre-delete hook to create a dangling reference.
openmeter/taxcode/service/hooks/addonhook_test.go Integration tests for the addon pre-delete hook covering both blocking and non-blocking deletion paths.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant TaxCodeService
    participant AddonHook
    participant PlanHook
    participant AddonService
    participant PlanService

    Client->>TaxCodeService: DeleteTaxCode(id)
    TaxCodeService->>AddonHook: PreDelete(taxCode)
    AddonHook->>AddonService: "ListAddons(TaxCodes=[id], Status=[All], PageSize=5)"
    AddonService-->>AddonHook: affectedAddons (up to 5)
    AddonHook->>AddonHook: verify rate card references in-memory
    alt any add-on references tax code
        AddonHook-->>TaxCodeService: ErrTaxCodeReferencedByRateCard
        TaxCodeService-->>Client: 409 Conflict
    else no references
        AddonHook-->>TaxCodeService: nil
        TaxCodeService->>PlanHook: PreDelete(taxCode)
        PlanHook->>PlanService: "ListPlans(TaxCodes=[id], Status=[All], PageSize=5)"
        PlanService-->>PlanHook: affectedPlans (up to 5)
        PlanHook->>PlanHook: verify rate card references in-memory
        alt any plan references tax code
            PlanHook-->>TaxCodeService: ErrTaxCodeReferencedByRateCard
            TaxCodeService-->>Client: 409 Conflict
        else no references
            PlanHook-->>TaxCodeService: nil
            TaxCodeService->>TaxCodeService: soft-delete tax code
            TaxCodeService-->>Client: 200 OK
        end
    end

    note over Client,PlanService: PublishAddon flow
    Client->>AddonService: PublishAddon(addonID)
    AddonService->>AddonService: ValidateAddonWithTaxCodes
    alt referenced tax code was deleted
        AddonService-->>Client: ValidationIssue (ErrCodeRateCardTaxCodeNotFound)
    else tax code still exists
        AddonService->>AddonService: archive previous active version
        AddonService-->>Client: published Addon
    end
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 AddonHook
    participant PlanHook
    participant AddonService
    participant PlanService

    Client->>TaxCodeService: DeleteTaxCode(id)
    TaxCodeService->>AddonHook: PreDelete(taxCode)
    AddonHook->>AddonService: "ListAddons(TaxCodes=[id], Status=[All], PageSize=5)"
    AddonService-->>AddonHook: affectedAddons (up to 5)
    AddonHook->>AddonHook: verify rate card references in-memory
    alt any add-on references tax code
        AddonHook-->>TaxCodeService: ErrTaxCodeReferencedByRateCard
        TaxCodeService-->>Client: 409 Conflict
    else no references
        AddonHook-->>TaxCodeService: nil
        TaxCodeService->>PlanHook: PreDelete(taxCode)
        PlanHook->>PlanService: "ListPlans(TaxCodes=[id], Status=[All], PageSize=5)"
        PlanService-->>PlanHook: affectedPlans (up to 5)
        PlanHook->>PlanHook: verify rate card references in-memory
        alt any plan references tax code
            PlanHook-->>TaxCodeService: ErrTaxCodeReferencedByRateCard
            TaxCodeService-->>Client: 409 Conflict
        else no references
            PlanHook-->>TaxCodeService: nil
            TaxCodeService->>TaxCodeService: soft-delete tax code
            TaxCodeService-->>Client: 200 OK
        end
    end

    note over Client,PlanService: PublishAddon flow
    Client->>AddonService: PublishAddon(addonID)
    AddonService->>AddonService: ValidateAddonWithTaxCodes
    alt referenced tax code was deleted
        AddonService-->>Client: ValidationIssue (ErrCodeRateCardTaxCodeNotFound)
    else tax code still exists
        AddonService->>AddonService: archive previous active version
        AddonService-->>Client: published Addon
    end
Loading

Reviews (4): Last reviewed commit: "fix(taxcode): merge plan/addon delete-gu..." | Re-trigger Greptile

Comment thread openmeter/taxcode/service/hooks/addonhook.go
@borbelyr-kong borbelyr-kong force-pushed the feat/taxcode-plan-publish-validation branch from 8a31078 to 7a906df Compare June 23, 2026 21:01
@borbelyr-kong borbelyr-kong force-pushed the feat/taxcode-addon-delete-guard branch from e850ada to 5946f52 Compare June 23, 2026 21:01
@borbelyr-kong borbelyr-kong force-pushed the feat/taxcode-plan-publish-validation branch 2 times, most recently from 2f9c5ab to 3a58a03 Compare June 24, 2026 13:10
@borbelyr-kong borbelyr-kong force-pushed the feat/taxcode-plan-publish-validation branch from f5bc13d to d50dfbb Compare July 3, 2026 09:47
@borbelyr-kong borbelyr-kong force-pushed the feat/taxcode-addon-delete-guard branch from 16973cc to b1f0aff Compare July 3, 2026 10:43
Base automatically changed from feat/taxcode-plan-publish-validation to fix/tax-code-delete July 3, 2026 11:59
@borbelyr-kong borbelyr-kong marked this pull request as ready for review July 3, 2026 11:59
@borbelyr-kong borbelyr-kong requested a review from a team as a code owner July 3, 2026 11:59
Comment thread openmeter/taxcode/errors.go Outdated
return errors.As(err, &vi) && vi.Code() == ErrCodeTaxCodeReferencedByPlan
}

const ErrCodeTaxCodeReferencedByAddon models.ErrorCode = "tax_code_referenced_by_addon"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please merge ErrTaxCodeReferencedByPlan and ErrTaxCodeReferencedByAddon in to ErrCodeTaxCodeReferencedByRateCard to use the same error in plans and addons.

@borbelyr-kong borbelyr-kong force-pushed the feat/taxcode-addon-delete-guard branch from b1f0aff to 8e5e07a Compare July 3, 2026 12:16

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
openmeter/productcatalog/addon/adapter/addon.go (1)

53-60: 🧹 Nitpick | 🔵 Trivial

Filter logic looks solid — restricting to non-deleted ratecards before matching on tax_code_id is exactly the right behavior for the deletion-protection use case.

One thing worth a quick check: since this is now used by the tax-code delete pre-hook (via PreDelete in addonhook.go), make sure addon_rate_card.tax_code_id is indexed so this HasRatecardsWith subquery doesn't do a full table scan as the ratecard table grows.

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

In `@openmeter/productcatalog/addon/adapter/addon.go` around lines 53 - 60, The
new tax-code filter in addon adapter is correct, but the `HasRatecardsWith` path
used by `PreDelete` in `addonhook.go` may become slow without an index on
`addon_rate_card.tax_code_id`. Add or verify a database index for the
`tax_code_id` field on the addon rate card model/schema so the subquery in the
addon adapter can use it efficiently as the table grows.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@openmeter/taxcode/service/hooks/addonhook.go`:
- Around line 26-32: Update AddonHookConfig.Validate to follow the shared
validation pattern instead of returning immediately on the first missing field.
Replace the fail-fast check on AddonService with collecting issues in a var errs
[]error, add any validation failures to that slice, and return
models.NewNillableGenericValidationError(errors.Join(errs...)) from Validate.
Keep the method name AddonHookConfig.Validate as the entry point so future
fields can be added without changing the validation pattern.

---

Nitpick comments:
In `@openmeter/productcatalog/addon/adapter/addon.go`:
- Around line 53-60: The new tax-code filter in addon adapter is correct, but
the `HasRatecardsWith` path used by `PreDelete` in `addonhook.go` may become
slow without an index on `addon_rate_card.tax_code_id`. Add or verify a database
index for the `tax_code_id` field on the addon rate card model/schema so the
subquery in the addon adapter can use it efficiently as the table grows.
🪄 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: 3eb05cb6-6eb5-44f4-80c8-3c54bf0be033

📥 Commits

Reviewing files that changed from the base of the PR and between e193df7 and 8e5e07a.

📒 Files selected for processing (20)
  • app/common/productcatalog.go
  • app/common/taxcode.go
  • cmd/billing-worker/wire_gen.go
  • cmd/jobs/internal/wire_gen.go
  • cmd/server/wire.go
  • cmd/server/wire_gen.go
  • openmeter/productcatalog/addon.go
  • openmeter/productcatalog/addon/adapter/adapter_test.go
  • openmeter/productcatalog/addon/adapter/addon.go
  • openmeter/productcatalog/addon/service.go
  • openmeter/productcatalog/addon/service/addon.go
  • openmeter/productcatalog/addon/service/service.go
  • openmeter/productcatalog/addon/service/taxcode_test.go
  • openmeter/productcatalog/testutils/env.go
  • openmeter/subscription/testutils/service.go
  • openmeter/taxcode/errors.go
  • openmeter/taxcode/service/hooks/addonhook.go
  • openmeter/taxcode/service/hooks/addonhook_test.go
  • test/billing/subscription_suite.go
  • test/customer/testenv.go

Comment on lines +26 to +32
func (e AddonHookConfig) Validate() error {
if e.AddonService == nil {
return fmt.Errorf("addon service is required")
}

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Validate() fails fast instead of collecting errors.

As per coding guidelines, "For Go Validate() error methods, collect all validation issues into var errs []error and return models.NewNillableGenericValidationError(errors.Join(errs...)) instead of failing fast." This config only has one field today, but that will silently drift once more fields get added.

♻️ Proposed fix
+import "errors"
+
 func (e AddonHookConfig) Validate() error {
-	if e.AddonService == nil {
-		return fmt.Errorf("addon service is required")
-	}
-
-	return nil
+	var errs []error
+
+	if e.AddonService == nil {
+		errs = append(errs, fmt.Errorf("addon service is required"))
+	}
+
+	return models.NewNillableGenericValidationError(errors.Join(errs...))
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (e AddonHookConfig) Validate() error {
if e.AddonService == nil {
return fmt.Errorf("addon service is required")
}
return nil
}
func (e AddonHookConfig) Validate() error {
var errs []error
if e.AddonService == nil {
errs = append(errs, fmt.Errorf("addon service is required"))
}
return models.NewNillableGenericValidationError(errors.Join(errs...))
}
🤖 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/taxcode/service/hooks/addonhook.go` around lines 26 - 32, Update
AddonHookConfig.Validate to follow the shared validation pattern instead of
returning immediately on the first missing field. Replace the fail-fast check on
AddonService with collecting issues in a var errs []error, add any validation
failures to that slice, and return
models.NewNillableGenericValidationError(errors.Join(errs...)) from Validate.
Keep the method name AddonHookConfig.Validate as the entry point so future
fields can be added without changing the validation pattern.

Source: Coding guidelines

@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/taxcode/service/hooks/addonhook.go (1)

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

Rate-card error-collection loop is duplicated with planhook.go.

This block (walk rate cards, check TaxCodeReference(), append taxcode.NewTaxCodeReferencedByRateCardError, guard against zero-match anomaly) is essentially the same logic as planhook.go's PreDelete (Lines 77-92 there), just iterating one level shallower (no Phases). Extracting a small shared helper that takes []productcatalog.RateCard + tax code ID and returns []error would remove this duplication across both hooks.

♻️ Example shared helper
func collectRateCardTaxCodeErrors(taxCodeID string, rateCards []productcatalog.RateCard) []error {
	var errs []error
	for _, rateCard := range rateCards {
		ref := rateCard.AsMeta().TaxCodeReference()
		if ref == nil || *ref != taxCodeID {
			continue
		}
		errs = append(errs, taxcode.NewTaxCodeReferencedByRateCardError(taxCodeID, rateCard.Key()))
	}
	return errs
}
🤖 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/taxcode/service/hooks/addonhook.go` around lines 74 - 91, The
rate-card tax code reference check in addonhook.go duplicates the same PreDelete
logic from planhook.go, so extract the shared loop into a small helper that
collects errors from a slice of rate cards and re-use it in both hooks. Keep the
helper focused on TaxCodeReference(), appending
taxcode.NewTaxCodeReferencedByRateCardError, and preserving the zero-match
anomaly handling currently done around affectedAddons.Items and errs.
🤖 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/taxcode/service/hooks/addonhook.go`:
- Around line 74-91: The rate-card tax code reference check in addonhook.go
duplicates the same PreDelete logic from planhook.go, so extract the shared loop
into a small helper that collects errors from a slice of rate cards and re-use
it in both hooks. Keep the helper focused on TaxCodeReference(), appending
taxcode.NewTaxCodeReferencedByRateCardError, and preserving the zero-match
anomaly handling currently done around affectedAddons.Items and errs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 33e234d3-78c4-40f5-8b4f-5f0d21f67860

📥 Commits

Reviewing files that changed from the base of the PR and between 8e5e07a and b7f198d.

📒 Files selected for processing (5)
  • openmeter/taxcode/errors.go
  • openmeter/taxcode/service/hooks/addonhook.go
  • openmeter/taxcode/service/hooks/addonhook_test.go
  • openmeter/taxcode/service/hooks/planhook.go
  • openmeter/taxcode/service/hooks/planhook_test.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.

2 participants