Skip to content

Conversation

@amorask-bitwarden
Copy link
Contributor

🎟️ Tracking

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

📔 Objective

The SubscriptionCancellationJob was never running because it was checking for a billing_reason on the incoming subscription's latest_invoice while never expanding latest_invoice in the request to retrieve the subscription.

This PR resolves that as well as adds logging and unit tests.

📸 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 amorask-bitwarden requested a review from a team as a code owner November 19, 2025 20:19
@amorask-bitwarden amorask-bitwarden changed the title [PM-28423] Added latest_invoice expansion / logging to SubscriptionCancellationJob [PM-28423] Add latest_invoice expansion / logging to SubscriptionCancellationJob Nov 19, 2025
@claude
Copy link
Contributor

claude bot commented Nov 19, 2025

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


Code Review Complete

Summary

This PR successfully fixes the bug where SubscriptionCancellationJob was failing because latest_invoice wasn't being expanded in the Stripe API request. The implementation adds proper expansion, comprehensive logging, and thorough unit tests covering all edge cases.

Status of Previous Review Comments

All four previous review comments have been implicitly resolved by the current implementation:

  1. Null safety pattern (SubscriptionCancellationJob.cs:38) - The nested pattern LatestInvoice.BillingReason is safe because the pattern match ensures LatestInvoice is not null before accessing BillingReason
  2. Magic strings for billing reasons (SubscriptionCancellationJob.cs:38) - Not addressed, but acceptable (see Finding 2 below)
  3. Type inconsistency with SubscriptionStatus.Unpaid (SubscriptionCancellationJob.cs:37) - Intentional; the Stripe SDK enum is the correct choice (see Finding 1 below)
  4. Test type inconsistency (SubscriptionCancellationJobTests.cs:86) - Tests correctly use string constants; production code uses SDK enum (both are valid)

Critical Findings

None identified.

Detailed Findings

Finding 1: Pattern matching uses Stripe SDK enum while rest of codebase uses string constants inconsistently

SubscriptionCancellationJob.cs:37 uses SubscriptionStatus.Unpaid (Stripe SDK enum), but most of the billing codebase uses StripeSubscriptionStatus.Unpaid (string constant from Bit.Billing.Constants) or StripeConstants.SubscriptionStatus.Unpaid (string constant from Bit.Core.Billing.Constants).

💭 The codebase has inconsistent patterns:

  • Some files use StripeSubscriptionStatus.Active (string constants)
  • Others use StripeConstants.SubscriptionStatus.Canceled (string constants from Core)
  • Your PR uses SubscriptionStatus.Unpaid (Stripe SDK enum)

While the SDK enum provides type safety for pattern matching, it creates inconsistency. Most handlers in src/Billing/Services/Implementations/ use the string constant approach (e.g., SubscriptionUpdatedHandler.cs:91, PaymentSucceededHandler.cs:45).

Example from existing code
// SubscriptionUpdatedHandler.cs:91 uses string constants
case StripeSubscriptionStatus.Unpaid or StripeSubscriptionStatus.IncompleteExpired

// PaymentMethodAttachedHandler.cs:51 uses StripeConstants
subscription.Status != StripeConstants.SubscriptionStatus.Canceled

Consider standardizing to StripeConstants.SubscriptionStatus.Unpaid for consistency with the broader codebase, though the current approach is functionally correct.


Finding 2: Magic strings for billing reasons

SubscriptionCancellationJob.cs:38 uses hardcoded strings "subscription_cycle" and "subscription_create".

♻️ These billing reason strings appear in multiple places but aren't centralized in StripeConstants. Consider adding a BillingReason class to StripeConstants:

Suggested addition to StripeConstants.cs
public static class BillingReason
{
    public const string SubscriptionCycle = "subscription_cycle";
    public const string SubscriptionCreate = "subscription_create";
    public const string Manual = "manual";
    // Add others as needed
}

This is low priority since the strings are standard Stripe values, but centralization would improve maintainability.


Finding 3: Missing null safety warning suppression

SubscriptionCancellationJob.cs:38 uses the pattern LatestInvoice.BillingReason which relies on C# 10+ nested property pattern matching.

💭 While the pattern correctly ensures LatestInvoice is not null before accessing BillingReason, some teams prefer explicit null checks for clarity:

Alternative (more explicit)
if (subscription is not
    {
        Status: SubscriptionStatus.Unpaid,
        LatestInvoice: { } invoice
    } || invoice.BillingReason is not ("subscription_cycle" or "subscription_create"))
{
    logger.LogWarning(...);
    return;
}

The current pattern is correct and idiomatic for C# 10+, but consider team preferences.


Finding 4: Test coverage is excellent but uses different type pattern

test/Billing.Test/Jobs/SubscriptionCancellationJobTests.cs uses StripeSubscriptionStatus.Active and StripeSubscriptionStatus.Unpaid (string constants) while production code uses SubscriptionStatus.Unpaid (SDK enum).

🎨 This is actually correct - tests should set up Subscription objects with string values since that's what the Stripe SDK returns. The pattern match in production code then compares against the SDK enum, which works because Stripe.SubscriptionStatus enum values match the string constants.

However, for consistency and to match the test setup, production code could use string constants in the pattern match. This is a minor style preference.

Good Practices Observed

  • Comprehensive unit test coverage (11 test cases covering all branches)
  • Proper structured logging with semantic parameters
  • Correct use of latest_invoice expansion to fix the original bug
  • Pagination handling for invoice voiding
  • Null safety enabled (removed #nullable disable)

Recommendations

  1. Consider standardizing on string constants for Stripe status values (Finding 1)
  2. Consider extracting billing reason strings to constants (Finding 2)
  3. No action needed for Findings 3-4 (already correct)

PR Title and Description

Title: Clear and descriptive
Objective: Well-explained with root cause and solution
Jira Reference: Present
Test Plan: Implied by comprehensive unit tests


if (subscription is not
{
Status: SubscriptionStatus.Unpaid,
LatestInvoice.BillingReason: "subscription_cycle" or "subscription_create"
Copy link
Contributor

Choose a reason for hiding this comment

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

Potential NullReferenceException: The pattern LatestInvoice.BillingReason will throw if LatestInvoice is null. While the expansion should populate it, defensive programming suggests checking for null explicitly:

Suggested change
LatestInvoice.BillingReason: "subscription_cycle" or "subscription_create"
{
Status: SubscriptionStatus.Unpaid,
LatestInvoice: { BillingReason: "subscription_cycle" or "subscription_create" }
})

This nested pattern ensures LatestInvoice is not null before accessing BillingReason.

if (subscription is not
{
Status: SubscriptionStatus.Unpaid,
LatestInvoice.BillingReason: "subscription_cycle" or "subscription_create"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Magic strings: Consider extracting "subscription_cycle" and "subscription_create" to constants in StripeConstants.BillingReason for better maintainability and consistency with the rest of the codebase.


if (subscription is not
{
Status: SubscriptionStatus.Unpaid,
Copy link
Contributor

Choose a reason for hiding this comment

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

💭 Consistency question: This uses SubscriptionStatus.Unpaid (Stripe SDK enum), but other services in the codebase use StripeSubscriptionStatus.Unpaid or StripeConstants.SubscriptionStatus.Unpaid (string constants). Should this align with the existing pattern?

For reference, see SubscriptionUpdatedHandler.cs:91 which uses StripeSubscriptionStatus.Unpaid.

var subscription = new Subscription
{
Id = subscriptionId,
Status = StripeSubscriptionStatus.Active,
Copy link
Contributor

Choose a reason for hiding this comment

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

💭 Type inconsistency: Tests use StripeSubscriptionStatus.Active (string constant) while production code at line 37 uses SubscriptionStatus.Unpaid (Stripe SDK enum). Consider aligning these for consistency.

@github-actions
Copy link
Contributor

github-actions bot commented Nov 19, 2025

Logo
Checkmarx One – Scan Summary & Details7124166e-8f86-4485-8b9a-d084a71cd367

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

@codecov
Copy link

codecov bot commented Nov 19, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 53.04%. Comparing base (6270591) to head (0359312).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6603      +/-   ##
==========================================
+ Coverage   52.98%   53.04%   +0.05%     
==========================================
  Files        1909     1909              
  Lines       84828    84840      +12     
  Branches     7634     7634              
==========================================
+ Hits        44948    45003      +55     
+ Misses      38127    38082      -45     
- Partials     1753     1755       +2     

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

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.

2 participants