Skip to content

[PM-31645] Implement Swiss Tax Logic#7186

Open
sbrown-livefront wants to merge 10 commits intomainfrom
billing/pm-31645/swiss-tax-logic-update
Open

[PM-31645] Implement Swiss Tax Logic#7186
sbrown-livefront wants to merge 10 commits intomainfrom
billing/pm-31645/swiss-tax-logic-update

Conversation

@sbrown-livefront
Copy link
Collaborator

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-31645

📔 Objective

This pull request refactors tax exemption logic across billing-related services and validation models to centralize country determination . The main change is replacing direct country checks (previously hardcoded for the United States) with calls to the new TaxHelpers utility. Tax exemption status is now consistently determined using TaxHelpers.DetermineTaxExemptStatus, ensuring correct handling for various countries and scenarios.

  • Added Bit.Core.Billing.Tax.Utilities imports throughout affected files to support the new utility methods.
  • Refactored static imports and removed direct references to constants where possible, further decoupling country logic from hardcoded values.

📸 Screenshots

Trigger Billing Warnings for Required TaxId When Updating Organization Billing Address

Screen.Recording.2026-03-06.at.5.12.32.PM.mov

No Billing Warnings for Swiss Organization When Updating Organization Billing Address

Screen.Recording.2026-03-06.at.5.14.29.PM.mov

Update Tax Exempt Status When Updating Organization Billing Address

Screen.Recording.2026-03-06.at.5.21.47.PM.mov

Update Tax Exempt Status When Upgrading Organization

Screen.Recording.2026-03-06.at.5.25.01.PM.mov

Trigger Billing Warnings for Required TaxId When Updating Provider Billing Address

Screen.Recording.2026-03-06.at.5.28.51.PM.mov

Create Client Organization With the Correct Tax Exempt Status (Provider Tax Exempt Status)

Screen.Recording.2026-03-06.at.5.41.18.PM.mov

Upcoming Invoice Handler Retains Manually Set Tax Exempt Status Swiss Organization (Works the Same for Providers)

Screen.Recording.2026-03-09.at.2.31.22.PM.mov

Upcoming Invoice Handler Correctly Sets Tax Exempt Status For International Organization (Works the Same for Providers)

Screen.Recording.2026-03-09.at.2.35.39.PM.mov

@sbrown-livefront sbrown-livefront requested review from a team as code owners March 9, 2026 19:01
@github-actions
Copy link
Contributor

github-actions bot commented Mar 9, 2026

Logo
Checkmarx One – Scan Summary & Details510f655f-7ec5-496b-8a76-9bc6370e18a0


New Issues (2) Checkmarx found the following issues in this Pull Request
# Severity Issue Source File / Package Checkmarx Insight
1 MEDIUM CSRF /src/Api/Billing/Controllers/VNext/AccountBillingVNextController.cs: 123
detailsMethod at line 123 of /src/Api/Billing/Controllers/VNext/AccountBillingVNextController.cs gets a parameter from a user request from request. Thi...
Attack Vector
2 MEDIUM CSRF /src/Api/Auth/Controllers/AccountsController.cs: 452
detailsMethod at line 452 of /src/Api/Auth/Controllers/AccountsController.cs gets a parameter from a user request from model. This parameter value flow...
Attack Vector

@amorask-bitwarden
Copy link
Contributor

@sbrown-livefront You've got some failing tests on here. Wanna take care of those before I review?

@sonarqubecloud
Copy link

sonarqubecloud bot commented Mar 9, 2026

@codecov
Copy link

codecov bot commented Mar 9, 2026

Codecov Report

❌ Patch coverage is 75.00000% with 27 lines in your changes missing coverage. Please review.
✅ Project coverage is 57.51%. Comparing base (49ec89d) to head (394ac8a).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
...Services/Implementations/UpcomingInvoiceHandler.cs 52.94% 16 Missing ⚠️
...lling/Providers/Services/ProviderBillingService.cs 50.00% 1 Missing and 3 partials ⚠️
...ling/Services/Implementations/SubscriberService.cs 60.00% 2 Missing ⚠️
...ling/Providers/Queries/GetProviderWarningsQuery.cs 0.00% 0 Missing and 1 partial ⚠️
...st/Organizations/OrganizationCreateRequestModel.cs 0.00% 1 Missing ⚠️
...Api/Models/Request/Accounts/PremiumRequestModel.cs 0.00% 1 Missing ⚠️
...dels/Request/Accounts/TaxInfoUpdateRequestModel.cs 0.00% 1 Missing ⚠️
...ganizations/Services/OrganizationBillingService.cs 83.33% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7186      +/-   ##
==========================================
+ Coverage   57.38%   57.51%   +0.12%     
==========================================
  Files        2031     2032       +1     
  Lines       89282    89299      +17     
  Branches     7936     7946      +10     
==========================================
+ Hits        51237    51356     +119     
+ Misses      36204    36088     -116     
- Partials     1841     1855      +14     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@theMickster theMickster added the ai-review Request a Claude code review label Mar 10, 2026
@claude
Copy link
Contributor

claude bot commented Mar 10, 2026

Claude finished @sbrown-livefront's task in 4m 41s —— View job


Pull Request Review: PM-31645 Implement Swiss Tax Logic

Summary

This PR centralizes tax exemption logic into a new TaxHelpers utility class, replacing hardcoded US-only country checks with a configurable set of "direct tax countries" (US and Switzerland). The key behavioral change is that Switzerland is now treated as a direct-tax country where tax_exempt defaults to "none", with a special override to preserve manually-set "reverse" status for existing Swiss customers. The refactoring touches 15 source files and adds thorough test coverage across 8 test files.

The centralization approach is well-designed -- the DirectTaxCountries and PreserveReverseChargeCountries sets make it straightforward to add new countries in the future, and the DetermineTaxExemptStatus method encapsulates the decision logic cleanly.

Findings

Severity Finding Location
⚠️ IMPORTANT customer.Address.Country accessed without null-conditional operator, risking NullReferenceException. Commit 5c17069c0 fixed this in OrganizationBillingService.cs but 4 other call sites still have the same issue. SubscriberService.cs:605, StripePaymentService.cs:126, UpcomingInvoiceHandler.cs:165, UpcomingInvoiceHandler.cs:458

Test Coverage

Test coverage is comprehensive. The PR adds unit tests for:

  • TaxHelpers directly (all branches of IsDirectTaxCountry and DetermineTaxExemptStatus)
  • Swiss customer scenarios in UpcomingInvoiceHandler, StripePaymentService, OrganizationBillingService, UpdateBillingAddressCommand, PreviewOrganizationTaxCommand, GetOrganizationWarningsQuery, GetProviderWarningsQuery, and ProviderBillingService
  • US correction scenarios (manual reverse charge on US customers gets corrected to none)
  • Non-direct-tax country scenarios (still receive reverse charge as before)

Codecov reports 75% patch coverage with 27 uncovered lines, which is reasonable for this type of change.

Notes

  • The extra Stripe API call in UpdateBillingAddressCommand.GetDeterminedTaxExemptStatusAsync (fetching the customer before updating) is a necessary trade-off to support the Swiss preserve-reverse-charge logic.
  • The OrganizationBillingService.CreateCustomer path (line 243) now correctly skips setting TaxExempt = Reverse for new Swiss customers, which aligns with the PR objective.


if (isBusinessUseSubscriber)
{
var determinedTaxExemptStatus = TaxHelpers.DetermineTaxExemptStatus(customer.Address.Country, customer.TaxExempt);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ IMPORTANT: NullReferenceException risk -- customer.Address.Country will throw if Address is null.

The original code used pattern matching (case { Address.Country: ... }), which is inherently null-safe. The refactored code extracts customer.Address.Country directly before the switch, reintroducing the NRE risk that commit 5c17069c0 fixed in OrganizationBillingService.cs (where customer.Address?.Country is correctly used).

The same issue exists in 3 other files:

  • src/Core/Billing/Services/Implementations/StripePaymentService.cs:126 -- sub.Customer.Address.Country
  • src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs:165 -- customer.Address.Country
  • src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs:458 -- customer.Address.Country

All four call sites should use the null-conditional operator, consistent with the fix already applied in OrganizationBillingService.cs.

Suggested change
var determinedTaxExemptStatus = TaxHelpers.DetermineTaxExemptStatus(customer.Address.Country, customer.TaxExempt);
var determinedTaxExemptStatus = TaxHelpers.DetermineTaxExemptStatus(customer.Address?.Country, customer.TaxExempt);

Copy link
Contributor

Choose a reason for hiding this comment

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

I know by this point it's a virtual guarantee that the customer will have an Address, but since it's nullable in Stripe, this is probably worth addressing to be safe.

Copy link
Contributor

@BTreston BTreston left a comment

Choose a reason for hiding this comment

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

Approved for AC

Copy link
Contributor

@amorask-bitwarden amorask-bitwarden left a comment

Choose a reason for hiding this comment

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

The logic looks good to me - I think Claude's NRE is easy enough to add, but there's also a couple missed spots:

UpgradePremiumToOrganizationCommand:205
UpdateOrganizationSubscriptionCommand:162 (New)

We're also doing similar, but not exactly the same, operations in a few different places. I don't think we need to fix that here, but might be worth filing a Tech Debt ticket to resolve it if possible.


if (isBusinessUseSubscriber)
{
var determinedTaxExemptStatus = TaxHelpers.DetermineTaxExemptStatus(customer.Address.Country, customer.TaxExempt);
Copy link
Contributor

Choose a reason for hiding this comment

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

I know by this point it's a virtual guarantee that the customer will have an Address, but since it's nullable in Stripe, this is probably worth addressing to be safe.

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

Labels

ai-review Request a Claude code review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants