Skip to content

feat(taxcode): prevent deletion of subscription-referenced tax codes#4758

Open
borbelyr-kong wants to merge 1 commit into
feat/taxcode-addon-delete-guardfrom
feat/taxcode-subscription-delete-guard
Open

feat(taxcode): prevent deletion of subscription-referenced tax codes#4758
borbelyr-kong wants to merge 1 commit into
feat/taxcode-addon-delete-guardfrom
feat/taxcode-subscription-delete-guard

Conversation

@borbelyr-kong

@borbelyr-kong borbelyr-kong commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

What

Deleting a tax code that is still referenced by a subscription's rate cards is now blocked with
a typed TaxCodeReferencedByRateCard error, mirroring the existing plan and add-on delete
guards.

Why

A tax code referenced by a live, scheduled, canceled, or inactive subscription could previously
be deleted, leaving those subscriptions pointing at a non-existent tax code.

How

  • New SubscriptionHook (openmeter/taxcode/service/hooks/subscriptionhook.go) registered
    on the tax code service as a PreDelete hook. It lists subscriptions filtered by tax-code
    reference (all statuses), expands each to a view, and returns a typed referenced-by error per
    matching rate card.
  • ListSubscriptionsInput.TaxCodes filter (list.go + subscriptionrepo.go) filters
    subscriptions by the tax code IDs on their rate-card items. The repo filter mirrors the
    subscription view's soft-delete gate (deleted_at IS NULL OR deleted_at > now), so items
    soft-deleted in the past don't produce spurious matches.
  • Wiring (app/common/taxcode.go, cmd/server/wire*.go) constructs the hook after both the
    subscription and tax code services exist, avoiding a construction cycle.
  • Tests cover referenced/unreferenced deletion, scheduled subscriptions

Summary by CodeRabbit

  • New Features

    • Added subscription filtering by tax code.
    • Prevented deletion of tax codes referenced by active or scheduled subscription rate cards.
    • Tax codes can still be deleted when references belong only to soft-deleted items.
  • Bug Fixes

    • Improved protection against removing tax codes that subscriptions depend on.

Greptile Summary

This PR prevents deleting tax codes referenced by subscription rate cards. The main changes are:

  • Adds a subscription tax-code filter backed by phase and item predicates.
  • Adds a pre-delete hook that checks subscription views for references.
  • Registers the hook through the server dependency graph.
  • Adds database-backed tests for subscription lifecycle cases.

Confidence Score: 4/5

Concurrent subscription writes can bypass the new deletion guard.

  • The normal reference and lifecycle paths are covered.
  • The check and delete are separate database actions, so a reference can be added between them unless writes are serialized.

openmeter/taxcode/service/hooks/subscriptionhook.go

Important Files Changed

Filename Overview
openmeter/taxcode/service/hooks/subscriptionhook.go Adds the subscription pre-delete scan, but the check can race with concurrent subscription reference writes.
openmeter/subscription/repo/subscriptionrepo.go Adds an Ent predicate that filters subscriptions through visible phase items by tax-code ID.
openmeter/subscription/list.go Adds and validates the tax-code filter on subscription list input.
app/common/taxcode.go Constructs and registers the new subscription hook after both services exist.
cmd/server/wire.go Adds the subscription hook provider and application field.
cmd/server/wire_gen.go Updates generated initialization to construct and retain the subscription hook.
openmeter/taxcode/service/hooks/subscriptionhook_test.go Covers referenced, unreferenced, scheduled, and previously soft-deleted subscription items.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Caller
    participant TaxCode as TaxCodeService
    participant Hook as SubscriptionHook
    participant Subs as SubscriptionService

    Caller->>TaxCode: DeleteTaxCode
    TaxCode->>Hook: PreDelete
    Hook->>Subs: List by namespace, status, and tax code
    Subs-->>Hook: Matching subscriptions
    loop Each match
        Hook->>Subs: GetView
        Subs-->>Hook: Phases and rate cards
    end
    alt Reference found
        Hook-->>TaxCode: TaxCodeReferencedByRateCard
        TaxCode-->>Caller: Deletion blocked
    else No reference found
        Hook-->>TaxCode: nil
        TaxCode->>TaxCode: Delete tax code
        TaxCode-->>Caller: Success
    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 Caller
    participant TaxCode as TaxCodeService
    participant Hook as SubscriptionHook
    participant Subs as SubscriptionService

    Caller->>TaxCode: DeleteTaxCode
    TaxCode->>Hook: PreDelete
    Hook->>Subs: List by namespace, status, and tax code
    Subs-->>Hook: Matching subscriptions
    loop Each match
        Hook->>Subs: GetView
        Subs-->>Hook: Phases and rate cards
    end
    alt Reference found
        Hook-->>TaxCode: TaxCodeReferencedByRateCard
        TaxCode-->>Caller: Deletion blocked
    else No reference found
        Hook-->>TaxCode: nil
        TaxCode->>TaxCode: Delete tax code
        TaxCode-->>Caller: Success
    end
Loading

Fix All in Claude Code Fix All in Codex

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
openmeter/taxcode/service/hooks/subscriptionhook.go:51-69
**Reference Check Races With Writes**

The guard only scans subscriptions before the tax-code delete. A concurrent subscription creation or rate-card update can add this tax-code ID after `List` completes but before deletion, so both operations can commit and leave a subscription referencing a deleted tax code unless the database transaction locks or otherwise serializes those writes.

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

Greptile also left 1 inline comment on this PR.

Context used:

  • Context used - CLAUDE.md (source)
  • Context used - AGENTS.md (source)

@borbelyr-kong
borbelyr-kong requested a review from a team as a code owner July 21, 2026 09:44
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds tax-code filtering to subscription listing and introduces a registered pre-delete hook that blocks tax code deletion when active or scheduled subscription rate cards reference the tax code.

Changes

Tax Code Subscription Protection

Layer / File(s) Summary
Subscription tax-code filtering
openmeter/subscription/list.go, openmeter/subscription/repo/subscriptionrepo.go
Subscription listing accepts and validates TaxCodes, then filters through phase items while excluding items hidden by soft deletion.
Tax code subscription hook
openmeter/taxcode/service/hooks/subscriptionhook.go, openmeter/taxcode/service/hooks/subscriptionhook_test.go
The pre-delete hook scans matching subscription views and returns typed reference errors. Tests cover running, scheduled, unreferenced, and soft-deleted item cases.
Application hook wiring
app/common/taxcode.go, cmd/server/wire.go, cmd/server/wire_gen.go
The hook is constructed, registered on the tax code service, and included in application initialization with cleanup on construction failure.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant TaxCodeService
  participant TaxCodeSubscriptionHook
  participant SubscriptionService
  participant SubscriptionRepository
  TaxCodeService->>TaxCodeSubscriptionHook: PreDelete tax code
  TaxCodeSubscriptionHook->>SubscriptionService: List by tax code
  SubscriptionService->>SubscriptionRepository: Query matching phase items
  SubscriptionRepository-->>SubscriptionService: Matching subscriptions
  TaxCodeSubscriptionHook->>SubscriptionService: Fetch subscription views
  SubscriptionService-->>TaxCodeSubscriptionHook: Rate-card references
  TaxCodeSubscriptionHook-->>TaxCodeService: Reference error or nil
Loading

Suggested labels: release-note/feature, area/product-catalog

Suggested reviewers: galexihu, turip

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: blocking deletion of tax codes referenced by subscriptions.
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-subscription-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.

Comment on lines +51 to +69
affected, err := e.subscriptionService.List(ctx, subscription.ListSubscriptionsInput{
Namespaces: []string{tc.Namespace},
Status: []subscription.SubscriptionStatus{
subscription.SubscriptionStatusActive,
subscription.SubscriptionStatusCanceled,
subscription.SubscriptionStatusInactive,
subscription.SubscriptionStatusScheduled,
},
TaxCodes: &filter.FilterString{
In: &[]string{
tc.ID,
},
},
Page: pagination.Page{
PageSize: 5,
PageNumber: 1,
},
})
if err != 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.

P1 Reference Check Races With Writes

The guard only scans subscriptions before the tax-code delete. A concurrent subscription creation or rate-card update can add this tax-code ID after List completes but before deletion, so both operations can commit and leave a subscription referencing a deleted tax code unless the database transaction locks or otherwise serializes those writes.

Prompt To Fix With AI
This is a comment left during a code review.
Path: openmeter/taxcode/service/hooks/subscriptionhook.go
Line: 51-69

Comment:
**Reference Check Races With Writes**

The guard only scans subscriptions before the tax-code delete. A concurrent subscription creation or rate-card update can add this tax-code ID after `List` completes but before deletion, so both operations can commit and leave a subscription referencing a deleted tax code unless the database transaction locks or otherwise serializes those writes.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Codex

@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 (3)
openmeter/taxcode/service/hooks/subscriptionhook.go (2)

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

Consider extracting the reference-scanning loop into a named helper.

The phase → item-by-key → item nested loop that checks each rate card's tax code reference is meaningful domain logic; pulling it into a small helper (e.g. collectReferencingRateCardErrors(view, taxCodeID)) would make PreDelete easier to read at a glance and let the reference-detection logic be unit-tested without going through the DB-backed integration suite.

As per path instructions: "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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openmeter/taxcode/service/hooks/subscriptionhook.go` around lines 89 - 101,
The nested phase → item-by-key → item scanning logic in PreDelete should be
extracted into a named helper such as collectReferencingRateCardErrors. Move the
tax-code matching and taxcode.NewTaxCodeReferencedByRateCardError construction
into that helper, have it accept the view and tax code ID, and update PreDelete
to append the helper’s results.

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

Align Validate() with the repo's validation-error pattern.

Every other Validate() error in this PR (e.g. ListSubscriptionsInput.Validate()) collects failures into errs and returns models.NewNillableGenericValidationError(errors.Join(errs...)). This one returns a bare fmt.Errorf instead, which is inconsistent and skips the field-context wrapping convention.

♻️ Proposed fix
 func (e SubscriptionHookConfig) Validate() error {
-	if e.SubscriptionService == nil {
-		return fmt.Errorf("subscription service is required")
-	}
-
-	return nil
+	var errs []error
+
+	if e.SubscriptionService == nil {
+		errs = append(errs, errors.New("subscription service is required"))
+	}
+
+	return models.NewNillableGenericValidationError(errors.Join(errs...))
 }

As per coding guidelines: "For Validate() error methods, collect all validation failures and return models.NewNillableGenericValidationError(errors.Join(errs...)), preserving field context with wrapped errors."

🤖 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/subscriptionhook.go` around lines 24 - 30,
Update SubscriptionHookConfig.Validate to collect validation failures in an errs
slice, wrap the missing SubscriptionService error with its field context, and
return models.NewNillableGenericValidationError(errors.Join(errs...)) instead of
a bare fmt.Errorf; preserve nil for valid configurations.

Source: Coding guidelines

openmeter/taxcode/service/hooks/subscriptionhook_test.go (1)

54-75: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Nice, thorough coverage of the reference-detection edge cases. One small nit: the DB-deps setup, hook registration, and org-defaults provisioning block is duplicated verbatim across all three top-level test functions. Pulling it into a shared setupSubscriptionHookTest(t) helper would trim the repetition.

Also applies to: 139-163, 203-239

🤖 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/subscriptionhook_test.go` around lines 54 -
75, Extract the repeated DB dependency setup, subscription hook registration,
and organization-default tax-code provisioning from the three top-level tests
into a shared setupSubscriptionHookTest(t) helper. Have the helper return the
initialized dependencies and namespace needed by each test, while preserving
existing cleanup and clock-reset behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@openmeter/taxcode/service/hooks/subscriptionhook_test.go`:
- Around line 54-75: Extract the repeated DB dependency setup, subscription hook
registration, and organization-default tax-code provisioning from the three
top-level tests into a shared setupSubscriptionHookTest(t) helper. Have the
helper return the initialized dependencies and namespace needed by each test,
while preserving existing cleanup and clock-reset behavior.

In `@openmeter/taxcode/service/hooks/subscriptionhook.go`:
- Around line 89-101: The nested phase → item-by-key → item scanning logic in
PreDelete should be extracted into a named helper such as
collectReferencingRateCardErrors. Move the tax-code matching and
taxcode.NewTaxCodeReferencedByRateCardError construction into that helper, have
it accept the view and tax code ID, and update PreDelete to append the helper’s
results.
- Around line 24-30: Update SubscriptionHookConfig.Validate to collect
validation failures in an errs slice, wrap the missing SubscriptionService error
with its field context, and return
models.NewNillableGenericValidationError(errors.Join(errs...)) instead of a bare
fmt.Errorf; preserve nil for valid configurations.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ec63fcde-78eb-4a15-90aa-16f2c7511aa7

📥 Commits

Reviewing files that changed from the base of the PR and between 8974cdf and 002b9b7.

📒 Files selected for processing (7)
  • app/common/taxcode.go
  • cmd/server/wire.go
  • cmd/server/wire_gen.go
  • openmeter/subscription/list.go
  • openmeter/subscription/repo/subscriptionrepo.go
  • openmeter/taxcode/service/hooks/subscriptionhook.go
  • openmeter/taxcode/service/hooks/subscriptionhook_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.

1 participant