Skip to content

[PM-37514] Support Teams 2019 Migration#7864

Draft
sbrown-livefront wants to merge 23 commits into
mainfrom
billing/pm-37514-support-teams-2019-migration
Draft

[PM-37514] Support Teams 2019 Migration#7864
sbrown-livefront wants to merge 23 commits into
mainfrom
billing/pm-37514-support-teams-2019-migration

Conversation

@sbrown-livefront

@sbrown-livefront sbrown-livefront commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

🎟️ Tracking

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

📔 Objective

Adds the Teams 2019 → Teams Current pricing-migration paths (Monthly and Annual) to the deferred price-increase migration engine (epic PM-37496).

"Teams 2019 Packaged" is a flat base covering 5 included seats, plus a per-seat overage for seats beyond 5. Current Teams is pure per-seat.

  • Orgs with < 5 occupied seats are billed for actual occupied seats — unused base headroom disappears.
  • Orgs with ≥ 5 keep their current purchased seat count.

Unlike Teams Starter (a flat bundle), Teams 2019 carries a per-seat overage line, so HasNonSeatBasedPasswordManagerPlan() is false for it — that distinction drives most of the changes below.

Code changes

  • Paths & policy — registers Teams2019AnnualToCurrent / Teams2019MonthlyToCurrent (append-only MigrationPathIds 9, 10; 7–8 reserved for the in-flight Teams Starter paths). Adds SeatCountPolicy { Preserve, ActualUsage } to MigrationPath; Teams 2019 uses ActualUsage, all other paths default to Preserve.
  • Shared plan-migration extensions (PlanMigrationExtensions) — the packaged-source rule and the seat formula are factored into two extensions so the scheduler, the renewal quote, and renewal activation can't drift:
    • IsPackagedMigrationSource(plan, policy) — a source is Packaged when it's a flat bundle (HasNonSeatBasedPasswordManagerPlan()) or its path is ActualUsage. Replaces the same expression previously duplicated across PriceIncreaseScheduler, UpcomingInvoiceHandler, and SubscriptionUpdatedHandler.
    • ResolveMigratedSeatCount(plan, occupied, purchased) — the single billed-seat formula: Max(1, occupied) for a flat bundle, otherwise occupied < base ? occupied : purchased. Guards its inputs and throws ArgumentException for a Scalable source (BaseSeats == 0), which must preserve its line-item quantity rather than resolve from usage.
  • Price mapping (OrganizationPlanMigrationPriceMapper) — a guarded Packaged → Scalable case maps the flat base price (StripePlanId) to the target per-seat price; the not-null guard keeps a null == null misfire off Scalable sources.
  • Phase 2 schedule (PriceIncreaseScheduler) — for a Packaged source, the base line and the seat-overage line (both map to the same target seat price) collapse into a single seat line billed at ResolveMigratedSeatCount. Storage and other items pass through unchanged.
  • Renewal email (UpcomingInvoiceHandler) — reuses BusinessPlanRenewal2020MigrationMail; the quoted seat count comes from the same ResolveMigratedSeatCount, so it matches what Phase 2 bills (occupied for < 5, purchased for ≥ 5).
  • Renewal activation (SubscriptionUpdatedHandler) — source detection uses IsPackagedMigrationSource and selects the base price for packaged sources (so a sub-5 Teams 2019 org, which has no overage line, is still recognized when the schedule advances). The organization.Seats reconciliation (PM-39562, [PM-39562] fix: Reconcile org seats to billed quantity on packaged-plan migration #7869) also fires for Teams 2019 — reconciling Seats to the billed quantity.

Notes for reviewers

  • MaxAutoscaleSeats is preserved by construction — ChangePlan doesn't touch it and the reconciliation writes only Seats.
  • ResolveMigratedSeatCount guard — Scalable sources (Preserve policy) never reach the extension via the call-site gate today; the ArgumentException is a backstop so a future caller or a misconfigured ActualUsage path fails loudly instead of silently billing the wrong quantity.

📸 Screenshots

Incoming!

@sbrown-livefront sbrown-livefront self-assigned this Jun 24, 2026
@sbrown-livefront sbrown-livefront changed the title Billing/pm 37514 support teams 2019 migration [PM-37514] Support Teams 2019 Migration Jun 24, 2026
@sbrown-livefront sbrown-livefront added the t:feature Change Type - Feature Development label Jun 24, 2026
@codecov

codecov Bot commented Jun 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.52941% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 61.23%. Comparing base (f5ec38e) to head (a6b4375).

Files with missing lines Patch % Lines
src/Core/Billing/Pricing/PriceIncreaseScheduler.cs 94.73% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7864      +/-   ##
==========================================
+ Coverage   61.21%   61.23%   +0.01%     
==========================================
  Files        2219     2220       +1     
  Lines       98052    98102      +50     
  Branches     8846     8852       +6     
==========================================
+ Hits        60020    60068      +48     
- Misses      35917    35918       +1     
- Partials     2115     2116       +1     

☔ View full report in Codecov by Harness.
📢 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.

@sbrown-livefront sbrown-livefront added the ai-review Request a Claude code review label Jun 26, 2026
@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

🤖 Bitwarden Claude Code Review

Overall Assessment: APPROVE

Reviewed the addition of the Teams 2019 → Teams Current pricing-migration paths (Monthly and Annual) to the deferred price-increase engine. The change factors the Packaged-source detection and billed-seat formula into shared PlanMigrationExtensions (IsPackagedMigrationSource, ResolveMigratedSeatCount), wires the new SeatCountPolicy.ActualUsage through the scheduler, renewal email, and renewal-activation handler, and collapses the Teams 2019 base + seat-overage lines onto a single per-seat target line. The three billing call sites resolve seats through the same extension, so the quoted, scheduled, and reconciled quantities stay consistent by construction.

Code Review Details

No blocking findings.

Verified during review:

  • The MigrationPath.SeatCountPolicy parameter defaults to Preserve, so the new positional argument is non-breaking for existing paths.
  • Sub-5 Teams 2019 orgs (base line only, no overage line) are still detected via the base price and collapse to one seat line — covered by the new scheduler and handler tests.
  • Storage and non-seat lines pass through unchanged (PreservesStorage test).
  • The ResolveMigratedSeatCount guard throws for Scalable sources (BaseSeats == 0), preventing a misconfigured ActualUsage path from silently billing the wrong quantity.
  • The previously flagged test-clock wait and truncated comment are both resolved in the current head.

Comment thread src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs Outdated
@sbrown-livefront sbrown-livefront force-pushed the billing/pm-37514-support-teams-2019-migration branch from e8b968f to 4868430 Compare June 26, 2026 21:58
Comment thread src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs Outdated
…r.cs

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
@sonarqubecloud

Copy link
Copy Markdown

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 t:feature Change Type - Feature Development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant