Skip to content

[PM-33301] Add Functionality for Upgrading Using PayPal#7183

Open
sbrown-livefront wants to merge 5 commits intomainfrom
billing/pm-33301/upgrade-using-paypal-account
Open

[PM-33301] Add Functionality for Upgrading Using PayPal#7183
sbrown-livefront wants to merge 5 commits intomainfrom
billing/pm-33301/upgrade-using-paypal-account

Conversation

@sbrown-livefront
Copy link
Collaborator

🎟️ Tracking

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

📔 Objective

This PR adds required functionality to be able to use Paypal as a payment method for upgrading.

It also pull out some of the upgrade command functionality into helper methods to improve readability of the main function.

📸 Screenshots

Upgrading with Existing PayPal Payment Method

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

Upgrading with New Payment Method Changing from PayPal

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

@sbrown-livefront sbrown-livefront requested a review from a team as a code owner March 9, 2026 17:50
@github-actions
Copy link
Contributor

github-actions bot commented Mar 9, 2026

Logo
Checkmarx One – Scan Summary & Details0a8efb4b-8aa5-4d1c-9b62-a68623cf1a67


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

@codecov
Copy link

codecov bot commented Mar 9, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 57.38%. Comparing base (49ec89d) to head (1a15b7f).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #7183   +/-   ##
=======================================
  Coverage   57.38%   57.38%           
=======================================
  Files        2031     2031           
  Lines       89282    89285    +3     
  Branches     7936     7943    +7     
=======================================
+ Hits        51237    51239    +2     
- Misses      36204    36205    +1     
  Partials     1841     1841           

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

@sonarqubecloud
Copy link

sonarqubecloud bot commented Mar 9, 2026

@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 2m 34s —— View job


Review: PM-33301 Add Functionality for Upgrading Using PayPal

Overview

This PR adds PayPal (Braintree) payment support for the Premium-to-Organization upgrade flow, and refactors the upgrade command into smaller helper methods. The refactoring is clean and improves readability. Two new tests cover the PayPal and non-PayPal paths. The overall PayPal integration pattern (setting default_incomplete payment behavior, disabling auto-advance on the invoice, then paying through Braintree) is consistent with the existing CreatePremiumCloudHostedSubscriptionCommand.

Findings

Severity File Description
❌ Critical UpgradePremiumToOrganizationCommand.cs (line 362) new UserId(organizationId) should be new OrganizationId(organizationId) -- the subscription is being transferred to an Organization, so the Braintree transaction metadata will incorrectly record this as a user payment instead of an organization payment
❌ Critical UpgradePremiumToOrganizationCommandTests.cs (lines 1404-1406) Test asserts id.IsT0 (UserId variant) instead of id.IsT1 (OrganizationId variant), masking the production bug

Summary

One bug found: the PayInvoiceUsingPayPalAsync method wraps the organization GUID in a UserId instead of an OrganizationId. This causes the Braintree transaction custom fields to use user_id as the key instead of organization_id, creating data inconsistency that would affect payment reconciliation and support tooling. The corresponding test has the same error, so it passes despite the bug. Both need to be fixed together.


return organization.Id;
});
await braintreeService.PayInvoice(new UserId(organizationId), invoice);
Copy link
Contributor

Choose a reason for hiding this comment

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

CRITICAL: Wrong subscriber type passed to Braintree -- organization payment recorded as user payment

This passes a UserId wrapping the organization's GUID. Since the subscription is being transferred from a User to an Organization (the metadata is updated to set OrganizationId and clear UserId on lines 271-272), the Braintree transaction should be associated with the organization, not a user.

Inside BraintreeService.PayInvoice, the SubscriberId variant determines the Braintree transaction custom field key:

  • UserId produces "user_id"
  • OrganizationId produces "organization_id"

Using UserId here means the Braintree transaction metadata will contain user_id:{organizationGuid} instead of organization_id:{organizationGuid}, creating a data inconsistency that will affect payment reconciliation, support lookups, and any downstream systems that key off these Braintree custom fields.

Suggested change
await braintreeService.PayInvoice(new UserId(organizationId), invoice);
await braintreeService.PayInvoice(new OrganizationId(organizationId), invoice);

Copy link
Contributor

Choose a reason for hiding this comment

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

❌ Confirming above ^

Comment on lines +1404 to +1406
await _braintreeService.Received(1).PayInvoice(
Arg.Is<SubscriberId>(id => id.IsT0 && id.AsT0.Value == organizationId),
mockInvoice);
Copy link
Contributor

Choose a reason for hiding this comment

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

CRITICAL: Test asserts the wrong subscriber type, masking the production bug

id.IsT0 checks for the UserId variant (index 0 in the OneOf<UserId, OrganizationId, ProviderId>). Since the upgrade creates an Organization and the Braintree payment is on behalf of that organization, this should assert IsT1 (the OrganizationId variant) and use AsT1.

This test currently passes because the production code has the same bug (new UserId(organizationId) instead of new OrganizationId(organizationId)), so the assertion matches the buggy behavior.

Suggested change
await _braintreeService.Received(1).PayInvoice(
Arg.Is<SubscriberId>(id => id.IsT0 && id.AsT0.Value == organizationId),
mockInvoice);
await _braintreeService.Received(1).PayInvoice(
Arg.Is<SubscriberId>(id => id.IsT1 && id.AsT1.Value == organizationId),
mockInvoice);

Copy link
Contributor

@cyprain-okeke cyprain-okeke left a comment

Choose a reason for hiding this comment

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

Nice work!


return organization.Id;
});
await braintreeService.PayInvoice(new UserId(organizationId), invoice);
Copy link
Contributor

Choose a reason for hiding this comment

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

❌ Confirming above ^

return organization.Id;
});

private async Task<(Subscription currentSubscription, List<PremiumPlan> premiumPlans, SubscriptionItem? passwordManagerItem)> GetPremiumPlanAndSubscriptionDetailsAsync(User user)
Copy link
Contributor

Choose a reason for hiding this comment

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

⛏️ The utility of this method is a bit unclear to me. I think 3 straight-forward lines turned into tuple destructuring adds some misdirection.

},
TaxExempt = billingAddress.Country != CountryAbbreviations.UnitedStates ? TaxExempt.Reverse : TaxExempt.None
});

Copy link
Contributor

Choose a reason for hiding this comment

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

⛏️ Extra line

// Create the Organization entity
var organization = new Organization
// if the target plan is non-seat-based, set seats to the base seats of the target plan, otherwise set to 1
var initialSeats = isNonSeatBasedPmPlan ? targetPlan.PasswordManager.BaseSeats : 1;
Copy link
Contributor

Choose a reason for hiding this comment

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

❌ This is incorrect at the database-level. While we may only want to purchase 1 Families subscription, a Families organization gets 6 seats.

MaxStorageGb = targetPlan.PasswordManager.BaseStorageGb,
UsePolicies = targetPlan.HasPolicies,
UseMyItems = targetPlan.HasPolicies, // TODO: use the plan property when added (PM-32366)
UseMyItems = targetPlan.HasMyItems,
Copy link
Contributor

Choose a reason for hiding this comment

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

✅ Thank you for fixing this.

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