Skip to content

fix(tax-code): fix tax code delete#4555

Open
borbelyr-kong wants to merge 9 commits into
mainfrom
fix/tax-code-delete
Open

fix(tax-code): fix tax code delete#4555
borbelyr-kong wants to merge 9 commits into
mainfrom
fix/tax-code-delete

Conversation

@borbelyr-kong

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

Copy link
Copy Markdown
Contributor

@coderabbitai @greptileai review this draft

Summary by CodeRabbit

  • New Features
    • Added filtering for plan results by tax code IDs.
    • Added plan publish validation to ensure referenced tax codes exist (including rejecting deleted references).
    • Added rate-card validation for missing/invalid tax code references (returns a validation error).
  • Bug Fixes
    • Prevent deleting a tax code when it’s referenced by existing plans; now returns 409 Conflict with related plan IDs.
    • Tax-code retrieval no longer returns soft-deleted tax codes.
    • Organization default tax-code upsert now rejects soft-deleted/inactive tax codes.
  • Tests
    • Expanded integration-style coverage for tax-code deletion prevention and plan-by-tax-code filtering.

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds tax-code resolver and hook wiring, validates plan tax-code references, filters plans by tax code, removes the delete gate, and rejects soft-deleted tax codes in organization-default updates.

Changes

Tax code deletion guard, resolver, and plan validation

Layer / File(s) Summary
Resolver and validation contracts
openmeter/taxcode/service.go, openmeter/productcatalog/errors.go, openmeter/productcatalog/ratecard.go, openmeter/productcatalog/plan.go, openmeter/productcatalog/plan/service.go, openmeter/productcatalog/taxcoderesolver.go
Adds the tax-code resolver interfaces, the rate-card tax-code accessor and validator, the plan validation wrapper, the plan service resolver dependency, and the tax-code-referenced-by-plan validation error.
Tax code resolver implementation and wiring
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, app/common/productcatalog.go, cmd/billing-worker/wire_gen.go, cmd/jobs/internal/wire_gen.go, cmd/server/wire.go, cmd/server/wire_gen.go
Adds the resolver implementation and wires it into plan service construction in application and test setups, with unit and integration coverage for resolver behavior.
Tax code lifecycle hooks
openmeter/taxcode/service.go, openmeter/taxcode/service/service.go, openmeter/taxcode/service/taxcode.go, openmeter/taxcode/service/hooks/planhook.go, openmeter/taxcode/service/hooks/planhook_test.go, openmeter/taxcode/errors.go, openmeter/taxcode/adapter/taxcode.go, openmeter/productcatalog/tax_test.go, openmeter/server/server_test.go, app/common/taxcode.go
Adds the tax-code hook registry, registers hooks on the service, implements create/update/delete callbacks, and adds the plan hook that blocks deletion when plans reference a tax code.
Plan listing by tax code
openmeter/productcatalog/plan/service.go, openmeter/productcatalog/plan/adapter/plan.go, openmeter/productcatalog/plan/adapter/adapter_test.go
Adds TaxCodes filtering to plan listing and verifies the adapter returns only plans whose rate cards match the requested tax code IDs.
Org defaults and delete authorization
openmeter/taxcode/service/organizationdefaulttaxcodes.go, openmeter/taxcode/service/organizationdefaulttaxcodes_test.go, api/v3/handlers/taxcodes/delete.go
Rejects soft-deleted tax codes in organization-default upserts and removes the delete feature-flag gate from the tax-code delete handler.

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

Possibly related PRs

Suggested labels: release-note/bug-fix, area/product-catalog

Suggested reviewers: turip

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.75% 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 is related to the main change and clearly points to tax code deletion behavior.
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 fix/tax-code-delete

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 borbelyr-kong changed the title Fix/tax code delete fix(tax-code): fix tax code delete Jun 23, 2026
@borbelyr-kong borbelyr-kong added the release-note/misc Miscellaneous changes label Jun 23, 2026
@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 enables tax code deletion by adding a PreDelete hook that blocks the operation with HTTP 409 when any active plan references the tax code, and adds filtering of plans by tax code to ListPlans. It also hardens UpsertOrganizationDefaultTaxCodes against soft-deleted tax codes and wires up a new TaxCodeResolver used during PublishPlan to reject plans that reference deleted tax codes.

  • Delete guard: a new planHook.PreDelete hook queries plans referencing the tax code and returns a TaxCodeReferencedByPlan 409 error if any are found, giving callers a sample of the referencing plan IDs.
  • Publish-time validation: ValidatePlanWithTaxCodes is now called during PublishPlan; the resolver detects soft-deleted tax codes (via a DeletedAt != nil guard) and rejects the plan with ErrRateCardTaxCodeNotFound.
  • Adapter change: GetTaxCode in the adapter dropped the DeletedAtIsNil() filter so that service-layer code can detect soft-deleted records explicitly; callers that must not see deleted records now perform their own IsDeleted() check.

Confidence Score: 4/5

The delete guard and publish-time validation work correctly, but the adapter's GetTaxCode now returns soft-deleted records to all callers including the public GET handler, which lacks its own deleted-at guard.

The core deletion-block and publish-validation flows are correct and well-tested. The removal of DeletedAtIsNil() from the adapter's GetTaxCode is intentionally noted in code comments as a design trade-off, but the public-facing GetTaxCode service method still passes through without filtering, so GET /taxcodes/{id} on a deleted tax code returns the record with deleted_at populated instead of a 404. This regression was already flagged in a prior review pass and remains unaddressed.

openmeter/taxcode/adapter/taxcode.go and openmeter/taxcode/service/taxcode.go — the public GetTaxCode path is missing a deleted-at guard after the DeletedAtIsNil() filter was removed from the adapter

Important Files Changed

Filename Overview
openmeter/taxcode/adapter/taxcode.go Removed DeletedAtIsNil() from GetTaxCode to support detecting soft-deleted records; public API callers now receive deleted tax codes unless each call site adds its own guard
openmeter/taxcode/service/taxcode.go Wires hooks into Create/Update/Delete; GetTaxCode service method passes through to adapter without a deleted-at guard, so soft-deleted records are visible to all callers including the public API handler
openmeter/taxcode/service/hooks/planhook.go New pre-delete hook that blocks tax code deletion when active plans reference it; correctly scoped to namespace; uses a hard-coded page size of 5, so the 409 error body includes only a sample of referencing plan IDs
openmeter/taxcode/service/organizationdefaulttaxcodes.go Added requireActiveTaxCode helper that rejects soft-deleted tax codes from being designated as org defaults; logic is correct
openmeter/productcatalog/plan/adapter/plan.go Added TaxCodes filter to ListPlans; correctly scopes to non-deleted rate cards via ratecarddb.DeletedAtIsNil()
openmeter/productcatalog/ratecard.go New ValidateRateCardsWithTaxCodes function guards against empty, missing, and soft-deleted tax code references at publish time
openmeter/productcatalog/taxcoderesolver/resolver.go New resolver delegates to service.GetTaxCode, which now returns soft-deleted records; the deleted-at check is performed by the caller in ValidateRateCardsWithTaxCodes, so validation is correct but the design is indirect
openmeter/productcatalog/plan/service/plan.go Wires ValidatePlanWithTaxCodes into PublishPlan; correct placement after feature validation
api/v3/handlers/taxcodes/delete.go Removed the deleteTaxCodeEnabled = false guard, enabling the delete endpoint; error encoding is correctly delegated to the generic encoder which handles 409
openmeter/taxcode/errors.go New ErrTaxCodeReferencedByPlan error with correct 409 status code and typed IsTaxCodeReferencedByPlanError predicate

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant API as DELETE /taxcodes/{id}
    participant Svc as taxcode.Service
    participant Hook as planHook.PreDelete
    participant PlanSvc as plan.Service
    participant Adapter as taxcode.Adapter

    API->>Svc: DeleteTaxCode(input)
    Svc->>Adapter: GetTaxCode(id) [no DeletedAtIsNil filter]
    Adapter-->>Svc: existing (or soft-deleted)
    Svc->>Svc: existing.IsDeleted()? → return nil (idempotent)
    Svc->>Svc: IsManagedBySystem? / IsOrgDefault? checks
    Svc->>Hook: PreDelete(existing)
    Hook->>PlanSvc: "ListPlans(namespace, allStatuses, TaxCodes=[id], page=1/5)"
    PlanSvc-->>Hook: affectedPlans
    alt "len(affectedPlans.Items) > 0"
        Hook-->>Svc: ErrTaxCodeReferencedByPlan (409)
        Svc-->>API: 409 Conflict + plan_ids[]
    else no plans found
        Hook-->>Svc: nil
        Svc->>Adapter: DeleteTaxCode(id)
        Svc->>Adapter: GetTaxCode(id) [re-fetch deleted record]
        Adapter-->>Svc: deleted (with DeletedAt set)
        Svc->>Hook: PostDelete(deleted)
        Svc-->>API: 204 No Content
    end

    note over API,Adapter: PublishPlan also validates via ValidatePlanWithTaxCodes → TaxCodeResolver.Resolve → GetTaxCode → check DeletedAt != nil
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 API as DELETE /taxcodes/{id}
    participant Svc as taxcode.Service
    participant Hook as planHook.PreDelete
    participant PlanSvc as plan.Service
    participant Adapter as taxcode.Adapter

    API->>Svc: DeleteTaxCode(input)
    Svc->>Adapter: GetTaxCode(id) [no DeletedAtIsNil filter]
    Adapter-->>Svc: existing (or soft-deleted)
    Svc->>Svc: existing.IsDeleted()? → return nil (idempotent)
    Svc->>Svc: IsManagedBySystem? / IsOrgDefault? checks
    Svc->>Hook: PreDelete(existing)
    Hook->>PlanSvc: "ListPlans(namespace, allStatuses, TaxCodes=[id], page=1/5)"
    PlanSvc-->>Hook: affectedPlans
    alt "len(affectedPlans.Items) > 0"
        Hook-->>Svc: ErrTaxCodeReferencedByPlan (409)
        Svc-->>API: 409 Conflict + plan_ids[]
    else no plans found
        Hook-->>Svc: nil
        Svc->>Adapter: DeleteTaxCode(id)
        Svc->>Adapter: GetTaxCode(id) [re-fetch deleted record]
        Adapter-->>Svc: deleted (with DeletedAt set)
        Svc->>Hook: PostDelete(deleted)
        Svc-->>API: 204 No Content
    end

    note over API,Adapter: PublishPlan also validates via ValidatePlanWithTaxCodes → TaxCodeResolver.Resolve → GetTaxCode → check DeletedAt != nil
Loading

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

Comment thread openmeter/taxcode/service/taxcode.go
Comment thread openmeter/taxcode/service/hooks/planhook.go

@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: 2

🧹 Nitpick comments (2)
openmeter/taxcode/service/hooks/planvalidator.go (1)

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

Align Validate() with the repo’s validation aggregation convention.

This method returns on first error. In this codebase, Validate() error is expected to aggregate errors and return models.NewNillableGenericValidationError(errors.Join(errs...)).

♻️ Suggested refactor
 import (
 	"context"
+	"errors"
 	"fmt"
@@
 func (e PlanValidatorHookConfig) Validate() error {
+	var errs []error
+
 	if e.PlanService == nil {
-		return fmt.Errorf("plan service is required")
+		errs = append(errs, fmt.Errorf("plan service is required"))
 	}
 
-	return nil
+	return models.NewNillableGenericValidationError(errors.Join(errs...))
 }

As per coding guidelines For Go Validate() error methods, prefer collecting all validation issues into var errs []error and returning 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/planvalidator.go` around lines 26 - 32, The
Validate() method in PlanValidatorHookConfig currently returns immediately on
the first error instead of aggregating all validation errors as per the codebase
convention. Refactor the method to collect validation errors in a slice (var
errs []error) rather than returning early. When the PlanService field is nil,
append the error to the errs slice and continue with any other validations.
Finally, return models.NewNillableGenericValidationError(errors.Join(errs...))
to aggregate and return all collected errors together.

Source: Coding guidelines

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

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

Nice coverage on the core flow—please add the two missing error-path tests.

Would be great to add subtests for:

  1. NewPlanValidatorHook with PlanService: nil
  2. PreDelete when ListPlans returns an error

That closes coverage for all new branches in this file.

As per path instructions **/*_test.go: Make sure the tests are comprehensive and cover the changes.

🤖 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/planvalidator_test.go` around lines 61 - 115,
The TestPlanValidatorHook_PreDelete test function is missing coverage for error
paths. Add two new subtests: one that calls NewPlanValidatorHook with a nil
PlanService value in the PlanValidatorHookConfig to verify it properly handles
and returns an error, and another subtest that uses a stubPlanService configured
to return an error from ListPlans (by adding an error field to the stub) and
verifies that PreDelete properly propagates that error. These error-path tests
will close the coverage gaps for all branches in the hook implementation.

Source: Path instructions

🤖 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/adapter/taxcode.go`:
- Around line 113-116: The GetTaxCode adapter query (lines 113-116) is missing
the DeletedAtIsNil() filter that all other adapter queries like
GetTaxCodeByAppMapping and ListTaxCodes include, allowing it to unexpectedly
return soft-deleted records. Fix this by adding an IncludeDeleted field to the
GetTaxCodeInput struct, then conditionally apply the DeletedAtIsNil() filter in
the GetTaxCode adapter method based on this field (defaulting to false),
ensuring callers can explicitly opt-in to retrieving soft-deleted records.
Alternatively, add DeletedAtIsNil() unconditionally to the GetTaxCode query and
refactor DeleteTaxCode.PostDelete to use a separate internal query method that
explicitly handles soft-deleted records to maintain consistency across all
adapter queries.

In `@openmeter/taxcode/service/hooks/planvalidator.go`:
- Around line 54-59: The pre-delete integrity check for tax codes uses a
hard-coded Status slice that excludes PlanStatusInvalid and will regress when
new plan statuses are introduced. Instead of explicitly listing specific
statuses in the Status field, modify the query to include all possible plan
statuses or query without status filtering (or filter only for deleted plans if
applicable) so that the integrity check catches references in plans with any
status, including invalid and future statuses.

---

Nitpick comments:
In `@openmeter/taxcode/service/hooks/planvalidator_test.go`:
- Around line 61-115: The TestPlanValidatorHook_PreDelete test function is
missing coverage for error paths. Add two new subtests: one that calls
NewPlanValidatorHook with a nil PlanService value in the PlanValidatorHookConfig
to verify it properly handles and returns an error, and another subtest that
uses a stubPlanService configured to return an error from ListPlans (by adding
an error field to the stub) and verifies that PreDelete properly propagates that
error. These error-path tests will close the coverage gaps for all branches in
the hook implementation.

In `@openmeter/taxcode/service/hooks/planvalidator.go`:
- Around line 26-32: The Validate() method in PlanValidatorHookConfig currently
returns immediately on the first error instead of aggregating all validation
errors as per the codebase convention. Refactor the method to collect validation
errors in a slice (var errs []error) rather than returning early. When the
PlanService field is nil, append the error to the errs slice and continue with
any other validations. Finally, return
models.NewNillableGenericValidationError(errors.Join(errs...)) to aggregate and
return all collected errors together.
🪄 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: e988c293-4668-4ac6-b727-1f60ca7ec354

📥 Commits

Reviewing files that changed from the base of the PR and between 277f8d3 and d99b721.

📒 Files selected for processing (16)
  • api/v3/handlers/taxcodes/delete.go
  • app/common/taxcode.go
  • cmd/server/wire.go
  • cmd/server/wire_gen.go
  • openmeter/productcatalog/plan/adapter/adapter_test.go
  • openmeter/productcatalog/plan/adapter/plan.go
  • openmeter/productcatalog/plan/service.go
  • openmeter/productcatalog/tax_test.go
  • openmeter/server/server_test.go
  • openmeter/taxcode/adapter/taxcode.go
  • openmeter/taxcode/errors.go
  • openmeter/taxcode/service.go
  • openmeter/taxcode/service/hooks/planvalidator.go
  • openmeter/taxcode/service/hooks/planvalidator_test.go
  • openmeter/taxcode/service/service.go
  • openmeter/taxcode/service/taxcode.go
💤 Files with no reviewable changes (1)
  • api/v3/handlers/taxcodes/delete.go

Comment on lines +113 to +116
Where(
taxcodedb.Namespace(input.Namespace),
taxcodedb.ID(input.ID),
)

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.

🎯 Functional Correctness | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect TaxCode service methods, especially GetTaxCode and delete flow
ast-grep outline openmeter/taxcode/service/taxcode.go --view expanded

# 2) Find all GetTaxCode call sites in taxcode service + v3 taxcode handlers
rg -n --type=go '\bGetTaxCode\s*\(' openmeter/taxcode openmeter/api/v3/handlers/taxcodes

# 3) Check whether deleted-row guards still exist in service/handler paths
rg -n --type=go 'DeletedAt|IncludeDeleted|IsDeleted|NotFound' openmeter/taxcode openmeter/api/v3/handlers/taxcodes

Repository: openmeterio/openmeter

Length of output: 2389


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Get the full GetTaxCode adapter implementation (lines 106-130)
sed -n '106,130p' openmeter/taxcode/adapter/taxcode.go

# 2) Get the DeleteTaxCode service implementation, especially the refetch at line 182
sed -n '149,195p' openmeter/taxcode/service/taxcode.go

# 3) Check UpdateTaxCode service method for soft-delete guards
sed -n '34,67p' openmeter/taxcode/service/taxcode.go

# 4) Look for any NotFound or deletion-aware logic in the service
rg -n --type=go 'ErrNotFound|NotFound|soft.*delete' openmeter/taxcode/service/ -A 2

Repository: openmeterio/openmeter

Length of output: 6314


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Look at adapter.DeleteTaxCode implementation
sed -n '127,150p' openmeter/taxcode/adapter/taxcode.go

# 2) Check ListTaxCodes for soft-delete filtering
sed -n '60,90p' openmeter/taxcode/adapter/taxcode.go

# 3) Search for test coverage on GetTaxCode post-deletion
rg -n --type=go 'GetTaxCode.*after.*[dD]elete|[dD]elete.*GetTaxCode' openmeter/taxcode -A 5

# 4) Check if taxcodedb schema has DeletedAt and how it's used
rg -n --type=go 'DeletedAt|WithDeletedAt' openmeter/taxcode/adapter

Repository: openmeterio/openmeter

Length of output: 3311


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check if GetTaxCodeInput has any IncludeDeleted parameter
rg -n --type=go 'type GetTaxCodeInput' openmeter/taxcode -A 10

# Verify the complete DeleteTaxCode adapter to see the soft-delete operation
sed -n '200,220p' openmeter/taxcode/adapter/taxcode.go

# Check if there are any tests specifically for getting a deleted tax code
rg -n --type=go 'Delete.*GetTaxCode|GetTaxCode.*[Dd]elete' openmeter/taxcode --type=go -B 2 -A 8

Repository: openmeterio/openmeter

Length of output: 1235


Add an IncludeDeleted parameter to GetTaxCodeInput or refactor DeleteTaxCode.PostDelete to avoid exposing soft-deleted records.

The GetTaxCode adapter (lines 113–116) queries only by namespace+ID and lacks the DeletedAtIsNil() filter that all other adapter queries use (e.g., GetTaxCodeByAppMapping, UpdateTaxCode, ListTaxCodes). This creates an asymmetry: GetTaxCode can return soft-deleted records, but callers have no way to opt into this behavior since GetTaxCodeInput has no IncludeDeleted field.

The refetch in DeleteTaxCode.PostDelete (service line 182) relies on this unprotected access, but regular reads—like UpdateTaxCode calling GetTaxCode at service line 40—could also unexpectedly receive deleted records. Either add explicit opt-in via IncludeDeleted or refactor the delete hook pattern to preserve consistency with the rest of the adapter.

🤖 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/adapter/taxcode.go` around lines 113 - 116, The GetTaxCode
adapter query (lines 113-116) is missing the DeletedAtIsNil() filter that all
other adapter queries like GetTaxCodeByAppMapping and ListTaxCodes include,
allowing it to unexpectedly return soft-deleted records. Fix this by adding an
IncludeDeleted field to the GetTaxCodeInput struct, then conditionally apply the
DeletedAtIsNil() filter in the GetTaxCode adapter method based on this field
(defaulting to false), ensuring callers can explicitly opt-in to retrieving
soft-deleted records. Alternatively, add DeletedAtIsNil() unconditionally to the
GetTaxCode query and refactor DeleteTaxCode.PostDelete to use a separate
internal query method that explicitly handles soft-deleted records to maintain
consistency across all adapter queries.

Comment thread openmeter/taxcode/service/hooks/planhook.go
@borbelyr-kong borbelyr-kong marked this pull request as ready for review June 23, 2026 21:02
@borbelyr-kong borbelyr-kong requested a review from a team as a code owner June 23, 2026 21:02

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

♻️ Duplicate comments (1)
openmeter/taxcode/service/hooks/planhook.go (1)

52-82: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

The status filtering gap flagged in the previous review is still present.

The previous review correctly identified that the hard-coded status list excludes PlanStatusInvalid, which means a tax code can be deleted even when invalid plans reference it. Additionally, this explicit list will need manual updates when new plan statuses are introduced.

🤖 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/planhook.go` around lines 52 - 82, In the
PreDelete method of the planHook struct, the Status filter is using a hard-coded
list that excludes PlanStatusInvalid and will require manual updates when new
statuses are added. Either add PlanStatusInvalid to the Status filter list to
include invalid plans in the reference check, or remove the Status filter
entirely if the intent is to verify that no plans of any status reference the
tax code being deleted. This ensures all referenced plans are detected
regardless of their status.
🤖 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.

Duplicate comments:
In `@openmeter/taxcode/service/hooks/planhook.go`:
- Around line 52-82: In the PreDelete method of the planHook struct, the Status
filter is using a hard-coded list that excludes PlanStatusInvalid and will
require manual updates when new statuses are added. Either add PlanStatusInvalid
to the Status filter list to include invalid plans in the reference check, or
remove the Status filter entirely if the intent is to verify that no plans of
any status reference the tax code being deleted. This ensures all referenced
plans are detected regardless of their status.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9b801067-be76-44bb-94a3-abeb7bee6e98

📥 Commits

Reviewing files that changed from the base of the PR and between d99b721 and ec4da11.

📒 Files selected for processing (7)
  • app/common/taxcode.go
  • cmd/server/wire.go
  • cmd/server/wire_gen.go
  • openmeter/taxcode/service/hooks/planhook.go
  • openmeter/taxcode/service/hooks/planhook_test.go
  • openmeter/taxcode/service/organizationdefaulttaxcodes.go
  • openmeter/taxcode/service/organizationdefaulttaxcodes_test.go
✅ Files skipped from review due to trivial changes (2)
  • openmeter/taxcode/service/hooks/planhook_test.go
  • cmd/server/wire_gen.go

@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/planhook_test.go (1)

48-137: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Nice, this reads really cleanly! The given/when/then comments make the two scenarios super easy to follow, and reusing the real pctestutils environment gives you genuine end-to-end confidence in the hook path. 👍

One small thought for later: both subtests share the same ns and run sequentially, so they're correctly relying on no t.Parallel(). If anyone adds parallelism down the road, the shared namespace (and the plan created in the first subtest) would leak across cases. A tiny // note: subtests share ns; do not run in parallel would save a future reader a head-scratch.

🤖 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/planhook_test.go` around lines 48 - 137, The
shared namespace setup in TestPlanHookPreDelete means both subtests depend on
the same state and must not be parallelized; add a brief note near the shared ns
setup or test body making that constraint explicit. Use the
TestPlanHookPreDelete subtests and the shared ns variable as the reference point
so future changes don’t introduce t.Parallel() or other parallel execution that
would leak plan/tax code state between cases.
🤖 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/planhook_test.go`:
- Around line 48-137: The shared namespace setup in TestPlanHookPreDelete means
both subtests depend on the same state and must not be parallelized; add a brief
note near the shared ns setup or test body making that constraint explicit. Use
the TestPlanHookPreDelete subtests and the shared ns variable as the reference
point so future changes don’t introduce t.Parallel() or other parallel execution
that would leak plan/tax code state between cases.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4b811fdd-7359-453d-9583-eeb5833ae377

📥 Commits

Reviewing files that changed from the base of the PR and between ec4da11 and 805f044.

📒 Files selected for processing (4)
  • openmeter/taxcode/service/hooks/planhook.go
  • openmeter/taxcode/service/hooks/planhook_test.go
  • openmeter/taxcode/service/organizationdefaulttaxcodes.go
  • openmeter/taxcode/service/taxcode.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • openmeter/taxcode/service/organizationdefaulttaxcodes.go
  • openmeter/taxcode/service/hooks/planhook.go

@borbelyr-kong borbelyr-kong force-pushed the fix/tax-code-delete branch from 55d7233 to 181cff6 Compare July 3, 2026 09:33
@borbelyr-kong borbelyr-kong force-pushed the fix/tax-code-delete branch from 181cff6 to 96f7d97 Compare July 3, 2026 09:34
chrisgacsal
chrisgacsal previously approved these changes Jul 3, 2026

@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/taxcoderesolver/resolver.go (1)

43-56: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Minor: guard empty id for consistency with the namespace check.

Resolve explicitly guards against an empty namespace but lets an empty id fall through to r.service.GetTaxCode. The only current caller already filters empty tax code IDs upstream, so this isn't broken today, but since TaxCodeResolver is a public interface, it'd be safer/more self-documenting to reject empty id the same way as empty namespace.

🛡️ Optional guard
 func (r *resolver) Resolve(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")
+	}
🤖 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/resolver.go` around lines 43 - 56,
`resolver.Resolve` currently checks `namespace` but allows an empty `id` to
reach `r.service.GetTaxCode`, which is inconsistent for a public resolver API.
Add the same early validation for `id` near the existing `namespace` guard in
`Resolve`, returning an error before building `taxcode.GetTaxCodeInput` when
`id` is empty, so the method explicitly rejects invalid input for both fields.
openmeter/productcatalog/taxcoderesolver_test.go (1)

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

Nice, solid coverage — consider adding a case for the generic-error branch too.

All the not-found/deleted/skip paths are covered here. One gap: there's no test for a resolver returning a plain (non-not-found) error, which the validator wraps differently ("failed to resolve tax code for ratecard: %w" instead of ErrCodeRateCardTaxCodeNotFound). Might be worth a quick subtest to lock in that distinction.

✅ Suggested additional subtest
t.Run("generic resolver error is wrapped, not classified as not-found", func(t *testing.T) {
    taxCodeResolver := stubTaxCodeResolver{
        namespace: "test",
        err:       errors.New("boom"),
    }

    cards := RateCards{makeFlatFeeRateCardWithTaxCodeID("rc-1", taxCodeID)}
    err := ValidateRateCardsWithTaxCodes(ctx, taxCodeResolver)(cards)
    require.Error(t, err)

    var vi models.ValidationIssue
    require.False(t, errors.As(err, &vi), "expected a plain wrapped error, not a ValidationIssue")
})
🤖 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 83 - 169, Add
a new subtest in TestValidateRateCardsWithTaxCodes to cover the generic
resolver-error path: when stubTaxCodeResolver returns a plain error (not a
taxcode.NewTaxCodeNotFoundError), ValidateRateCardsWithTaxCodes should return
the wrapped error from the validator and not a models.ValidationIssue. Use the
existing test helpers like stubTaxCodeResolver and
makeFlatFeeRateCardWithTaxCodeID, and assert that errors.As does not classify
the result as ErrCodeRateCardTaxCodeNotFound.

Source: Path instructions

🤖 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 83-169: Add a new subtest in TestValidateRateCardsWithTaxCodes to
cover the generic resolver-error path: when stubTaxCodeResolver returns a plain
error (not a taxcode.NewTaxCodeNotFoundError), ValidateRateCardsWithTaxCodes
should return the wrapped error from the validator and not a
models.ValidationIssue. Use the existing test helpers like stubTaxCodeResolver
and makeFlatFeeRateCardWithTaxCodeID, and assert that errors.As does not
classify the result as ErrCodeRateCardTaxCodeNotFound.

In `@openmeter/productcatalog/taxcoderesolver/resolver.go`:
- Around line 43-56: `resolver.Resolve` currently checks `namespace` but allows
an empty `id` to reach `r.service.GetTaxCode`, which is inconsistent for a
public resolver API. Add the same early validation for `id` near the existing
`namespace` guard in `Resolve`, returning an error before building
`taxcode.GetTaxCodeInput` when `id` is empty, so the method explicitly rejects
invalid input for both fields.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 31b15981-1879-4711-b73e-d2bf797cf169

📥 Commits

Reviewing files that changed from the base of the PR and between 96f7d97 and e193df7.

📒 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/jobs/internal/wire_gen.go
  • cmd/server/wire_gen.go

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

Labels

release-note/misc Miscellaneous changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants