Skip to content

Conversation

@amorask-bitwarden
Copy link
Contributor

🎟️ Tracking

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

📔 Objective

📸 Screenshots

⏰ Reminders before review

  • Contributor guidelines followed
  • All formatters and local linters executed and passed
  • Written new unit and / or integration tests where applicable
  • Protected functional changes with optionality (feature flags)
  • Used internationalization (i18n) for all UI strings
  • CI builds passed
  • Communicated to DevOps any deployment requirements
  • Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team

🦮 Reviewer guidelines

  • 👍 (:+1:) or similar for great changes
  • 📝 (:memo:) or ℹ️ (:information_source:) for notes or general info
  • ❓ (:question:) for questions
  • 🤔 (:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
  • 🎨 (:art:) for suggestions / improvements
  • ❌ (:x:) or ⚠️ (:warning:) for more significant problems or concerns needing attention
  • 🌱 (:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt
  • ⛏ (:pick:) for minor or nitpick changes

@amorask-bitwarden
Copy link
Contributor Author

@bitwarden bitwarden deleted a comment from claude bot Nov 24, 2025
@github-actions
Copy link
Contributor

github-actions bot commented Nov 24, 2025

Logo
Checkmarx One – Scan Summary & Details72c977a4-80aa-40d0-bbf6-cfeac6328c30

Great job! No new security vulnerabilities introduced in this pull request

@codecov
Copy link

codecov bot commented Nov 24, 2025

Codecov Report

❌ Patch coverage is 97.77778% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 53.12%. Comparing base (3559759) to head (b4be0d5).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
...Services/Implementations/UpcomingInvoiceHandler.cs 97.67% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6638      +/-   ##
==========================================
+ Coverage   53.10%   53.12%   +0.02%     
==========================================
  Files        1903     1903              
  Lines       84877    84895      +18     
  Branches     7633     7633              
==========================================
+ Hits        45071    45099      +28     
+ Misses      38055    38045      -10     
  Partials     1751     1751              

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

cyprain-okeke
cyprain-okeke previously approved these changes Nov 25, 2025
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.

Looks Great. I just added a unblocking comments

Lines = new StripeList<InvoiceLineItem>
{
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
Data = [new() { Description = "Test Item" }]
Copy link
Contributor

Choose a reason for hiding this comment

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

remove extra line

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What extra line?

Id = passwordManagerItemId,
Price = new Price { Id = families2019Plan.PasswordManager.StripePlanId }
},

Copy link
Contributor

Choose a reason for hiding this comment

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

remove extra line

Lines = new StripeList<InvoiceLineItem>
{
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
Data = [new() { Description = "Test Item" }]
Copy link
Contributor

Choose a reason for hiding this comment

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

remove extra line

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same question here.

Lines = new StripeList<InvoiceLineItem>
{
Data = new List<InvoiceLineItem> { new() { Description = "Test Item" } }
Data = [new() { Description = "Test Item" }]
Copy link
Contributor

Choose a reason for hiding this comment

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

remove extra line

Copy link
Contributor Author

Choose a reason for hiding this comment

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

And here.

@claude
Copy link
Contributor

claude bot commented Nov 25, 2025

Claude finished @amorask-bitwarden's task —— View job


Code Review Complete

Summary of Changes Since Last Review

The PR has been updated with one commit since the previous Claude review:

  • a386350: "Cy's feedback" - Removed 1 empty line from test file

Previous major issues flagged by Claude remain unaddressed:

  1. Hardcoded "15 days" in email template (families-2020-renewal.mjml:17)
  2. Premium users receiving "Families" renewal email (UpcomingInvoiceHandler.cs:567-578)

Critical Issues

Finding 1: Hardcoded renewal timing remains inaccurate

Location: src/Core/MailTemplates/Mjml/emails/Billing/Renewals/families-2020-renewal.mjml:17

The hardcoded "15 days" text does not match when Stripe's invoice.upcoming event fires (typically ~7 days before renewal). The author's response shows they're aware this is a "Marketing copy/design decision," but this creates a factual inaccuracy that could mislead customers.

Suggested alternatives

Option 1 - Pass actual date:

Your Bitwarden Families subscription renews on {{RenewalDate}}. The price is updating to {{MonthlyRenewalPrice}}/month, billed annually.

Option 2 - Generic language:

Your Bitwarden Families subscription is renewing soon. The price is updating to {{MonthlyRenewalPrice}}/month, billed annually.

Finding 2: Premium users receive misleading "Families" email subject

Location: src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs:567-578

The TODO comment acknowledges this is temporary, but premium users currently receive an email with subject "Your Bitwarden Families renewal is updating" which is confusing and unprofessional. This is a customer-facing bug that should be resolved before merge.

Recommended fix

Create a proper premium renewal template OR parameterize the existing template:

public class SubscriptionRenewalMailView : BaseMailView
{
    public required string PlanName { get; set; }
    public required string MonthlyRenewalPrice { get; set; }
}

public class SubscriptionRenewalMail : BaseMail<SubscriptionRenewalMailView>
{
    public required string PlanName { get; set; }
    public override string Subject => $"Your Bitwarden {PlanName} renewal is updating";
}

Finding 3: Missing coverage for premium user email path

Location: test/Billing.Test/Services/UpcomingInvoiceHandlerTests.cs

No tests verify that SendPremiumRenewalEmailAsync (lines 563-579) is called correctly for premium users. Given this reuses the Families template with a TODO comment, comprehensive test coverage is critical.

Required test: HandleAsync_WhenMilestone2Enabled_AndPremiumUser_SendsPremiumRenewalEmail

Minor Issues & Improvements

Finding 4: Hardcoded price ID string literal

Location: src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs:271

The string "personal-org-seat-annually" is hardcoded. For consistency with other price IDs in the codebase, this should be defined in a constants class (likely StripeConstants.Prices).

Finding 5: Currency formatting may not match user's locale

Location: src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs:556, 574

Both renewal email methods format prices using new CultureInfo("en-US"):

MonthlyRenewalPrice = (familiesPlan.PasswordManager.BasePrice / 12).ToString("C", new CultureInfo("en-US"))

💭 This always displays USD formatting ($X.XX) regardless of the customer's actual currency or location. For international customers who may be charged in EUR, GBP, etc., this could be misleading. Consider using the customer's actual currency from Stripe.

Finding 6: Email template has no unsubscribe/preference center link

Location: src/Core/MailTemplates/Mjml/emails/Billing/Renewals/families-2020-renewal.mjml

The email template lacks an unsubscribe or email preference management link. While billing/transactional emails may have different requirements than marketing emails, best practice (and some jurisdictions' laws) recommend providing preference controls even for transactional emails.

Good Practices Observed

  • Comprehensive test coverage for the Families 2019 to 2025 migration path
  • Proper error handling and logging throughout
  • Feature flag gating for gradual rollout
  • ProrationBehavior.None prevents mid-cycle charges

Action Items

Must fix before merge:

  1. ❌ Finding 2: Resolve premium user email subject issue (create proper template or parameterize)
  2. ❌ Finding 3: Add test coverage for premium renewal email

Should fix before merge:
3. ❌ Finding 1: Address hardcoded "15 days" timing inaccuracy
4. ⚠️ Finding 4: Extract hardcoded price ID to constants

Consider for follow-up:
5. 💭 Finding 5: Review currency formatting for international customers
6. 💭 Finding 6: Evaluate adding email preference management link


@amorask-bitwarden amorask-bitwarden merged commit 219993c into main Nov 26, 2025
57 checks passed
@amorask-bitwarden amorask-bitwarden deleted the billing/PM-26461/families-2020-renewal-email branch November 26, 2025 14:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants