* [PM-37068] feat: Add business plan cohort branch to UpcomingInvoiceHandler
Wires the caller side of the Teams/Enterprise 2020 → current price
migration. AlignOrganizationSubscriptionConcernsAsync becomes a
ProductTier dispatcher; the new business branch loads the cohort
assignment, runs eligibility guards (assignment unscheduled, cohort
active, org PlanType matches cohort source), and invokes PM-37064's
ScheduleBusinessPriceIncrease. The renewal email is a placeholder until
PM-37070 lands.
Also expands subscriptions.data.customer on the customer load so
PM-37064 can preserve customer-level discounts into Phase 2.
Incidentally strips pre-existing #region markers in
UpcomingInvoiceHandlerTests.cs per the no-regions rule.
* Apply review feedback: gate FF in handler, split MigrationPath checks, silence scheduler churn-only warning
* Add cohort metadata to subscription on migration schedule
---------
Co-authored-by: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com>
* feat(billing): inject feature service into billing warning queries
* test(billing): add provider tax warning tests for automatic tax flag
* test(billing): add organization tax warning tests for automatic tax flag
* feat(billing): modify provider tax id warning based on automatic tax feature flag
* feat(billing): modify organization tax id warning based on automatic tax feature flag
* refactor(billing): clean up unused usings and file encoding
* test(billing): add tax id verification warnings for providers
* test(billing): add tax id verification warnings for organizations
* [PM-37064] feat: Add ScheduleBusinessPriceIncrease scheduler entry point
Extends PriceIncreaseScheduler (PM-32645) with a parallel entry point for
Teams/Enterprise 2020 → current plan migrations under epic PM-35215. The
business path consumes a cohort directly, preserves existing discounts,
optionally appends the cohort's proactive coupon, and stamps ScheduledDate
on the assignment row on success.
Existing Schedule renames to SchedulePersonalPriceIncrease across four
call sites; ResolvePhase2Async becomes private. Shared concerns extract
into ActiveScheduleExistsAsync and CreateAndConfigureScheduleAsync helpers.
ScheduleBusinessPriceIncrease has zero production callers — the
UpcomingInvoiceHandler dispatcher branch lands in a follow-up. The new
PM35215_BusinessPlanPriceMigration feature flag defaults off.
* Address PR review: prevent orphan schedules and fix Release flag gate
* feat(billing): add feature flag for automatic tax enforcement
* refactor(billing): remove unused SubscriptionUpdateOptionsExtensions
* refactor(billing): inject IFeatureService into billing services and commands
* feat(billing): conditionalize customer tax exemption logic with feature flag
* feat(billing): conditionally enable Stripe automatic tax in OrganizationBillingService
* test(billing): add unit tests for Stripe automatic tax feature flag
* fix(billing): Run dotnet format
* test(Premium): use class-level IFeatureService mock in UpgradePremiumToOrganizationCommandTests
* refactor(billing): consolidate customer return conditions for automatic tax
* refactor(billing): broaden postal code validation for organization creation
* refactor(billing): remove PM37597 feature flag for automatic tax logic
* Revert "refactor(billing): broaden postal code validation for organization creation"
This reverts commit cddbda838c.
* fix(test): remove outdated test
* Fix null warnings and add more descriptive log in the case or non-successful HTTP calls.
* Add RelayPushRegistration tests
* Skip Push Registration when the client is not a mobile client
* Formatting
* Pin New Dependency
* Assert more in mobile test and use actual mobile device type
* [PM-36949] Add OrganizationPlanMigrationCohort schema and Core domain types
Add the foundation for cohort-based plan migrations:
- Two tables: OrganizationPlanMigrationCohort and OrganizationPlanMigrationCohortAssignment
- Two views and nine stored procedures (four CRUD on cohort, four CRUD plus
ReadByOrganizationId on assignment)
- Single Migrator script for MSSQL deployment
- Core entities, MigrationPath value object and its registry, and bare repository
interfaces under Bit.Core.Billing.Organizations.PlanMigration
The cohort table holds the human-managed metadata (name, discount coupons,
MigrationPathId byte) and the assignment table records each organization's
position in the migration lifecycle (scheduled, migrated, churn-mitigated).
Both Update SPs follow the accept-but-don't-assign pattern: immutable columns
(OrganizationId, CohortId, CreatedAt) are parameters but not SET clauses.
* [PM-36949] Add Dapper repositories for plan migration cohort tables
OrganizationPlanMigrationCohortRepository inherits the base Repository<T, TId>
CRUD methods unchanged. OrganizationPlanMigrationCohortAssignmentRepository
also relies on the base for CRUD and adds GetByOrganizationIdAsync which
returns at most one row (the UNIQUE constraint on OrganizationId at the
database layer guarantees this).
* [PM-36949] Add EF Core configurations, repositories, and provider migrations for plan migration cohort tables
- EF models wrap the Core entities; the assignment model exposes nav properties
for Organization and Cohort so the FK + cascade-delete is inferred by EF.
- EntityTypeConfiguration classes pin ID generation to application code
(ValueGeneratedNever) and declare the UNIQUE indexes plus the composite
(CohortId, ScheduledAt, MigratedAt) index.
- Repositories follow the OrganizationInstallationRepository template; the
assignment repo adds GetByOrganizationIdAsync to mirror the SP exposed on
the MSSQL side.
- DatabaseContext gets two DbSet properties; auto-discovery picks up the
configuration classes.
- Generated migrations for MySQL, Postgres, and SQLite create matching schemas;
EF truncates FK and index names on providers with 64-char identifier limits,
which is consistent with the rest of the codebase.
* [PM-36949] Wire up DI and add tests for plan migration cohort repositories
Register both Dapper and EF Core repositories in their respective service
collection extensions, following the existing AddSingleton convention in
these files.
Add tests:
- MigrationPathIdsSnapshotTests guards the immortal byte IDs that downstream
code pins on. The class- and method-level comments document why these
values can never be renumbered.
- MigrationPathTests covers the FromId round-trip and the null-on-unknown
behavior the registry promises to callers.
- OrganizationPlanMigrationCohortRepositoryTests exercises CRUD, the UNIQUE
Name constraint, and verifies that ReplaceAsync ignores CreatedAt
mutations (per the accept-but-don't-assign Update SP).
- OrganizationPlanMigrationCohortAssignmentRepositoryTests exercises CRUD,
GetByOrganizationIdAsync, the UNIQUE OrganizationId constraint,
cascade-delete from both Organization and Cohort, and verifies that
ReplaceAsync ignores OrganizationId, CohortId, and CreatedAt mutations.
* [PM-36949] Use PlanType and MigrationPathId enums on MigrationPath
Replace the byte Id with a byte-backed MigrationPathId enum and replace
the string FromPlan/ToPlan fields with PlanType. Persistence is
unchanged -- EF normalises enum-backed properties to their underlying
type in the model snapshot, and Dapper handles enum-to-byte parameter
mapping automatically.
* [PM-36949] Add *.lscache to .gitignore
* [PM-36949] fix: Override ReplaceAsync on EF cohort repositories for immutability parity
The Dapper _Update SPs accept-but-don't-assign certain columns (CreatedAt
on cohort; OrganizationId, CohortId, and CreatedAt on assignment), but
the base EF Repository<T,TEntity,Guid>.ReplaceAsync uses SetValues which
writes every scalar. Override on both repos and mark the immutable
properties as IsModified = false so MySQL/Postgres/Sqlite match MSSQL
behavior. Mirrors the existing DeviceRepository.ReplaceAsync pattern.
* [PM-36949] fix: Bound cohort string columns and widen Name to 255 chars
Add [MaxLength] attributes to the three cohort string properties so the
EF providers (MySQL/Postgres/Sqlite) enforce the same limits as MSSQL,
where the columns were already NVARCHAR-capped. Widen Name from 64 to
255 chars across MSSQL DDL, both _Create/_Update SP signatures, the
Migrator script, the entity, and all three regenerated EF migrations.
Coupon codes stay at 64 (Stripe IDs are short).
* [PM-36949] test: Lock FromPlan and ToPlan per MigrationPath
The snapshot test class doc says "byte N means a specific FromPlan ->
ToPlan transition forever", but only the byte value was being asserted.
A silent refactor of the registry's PlanType references would not have
been caught. Add per-path FromPlan/ToPlan assertions to close the gap.
* [PM-36949] chore: Apply dotnet format
* [PM-36949] test: Use LaxDateTimeComparer for round-tripped DateTimes
Postgres timestamp and MySQL datetime(6) store microsecond precision (6
fractional digits), but .NET DateTime is 100ns ticks (7 digits). Exact
Assert.Equal fails by a single tick on round-trip. Switch the three
DateTime comparisons in ReplaceAsync_UpdatesMutableColumns_AndIgnoresImmutableOnes
to LaxDateTimeComparer.Default -- the same 2ms-tolerance comparer used
by SendRepositoryTests and InstallationRepositoryTests for the same
precision issue.
* [PM-36949] refactor: Rename DateTime columns to Date suffix per naming convention
Addresses PR review feedback. Also tightens DATETIME2(7) / NVARCHAR(N) spacing
per the SQL style guide, drops *.lscache from .gitignore (handled by #7648),
and regenerates EF migrations for MySQL, Postgres, and SQLite.
* Apply dotnet format to regenerated migrations
* [PM-36949] chore: Align NULL/NOT NULL columns in cohort tables
* PM-37621 - Fix Device.LastActivityDate surfacing legacy NULL rows as DateTime.UtcNow
Dapper's deserializer skips the property setter when a nullable column is
DBNull, leaving the property at its CLR default. The field initializer
`public DateTime? LastActivityDate { get; internal set; } = DateTime.UtcNow`
poisoned that default, so rows whose LastActivityDate column was NULL (e.g.
devices created before the column existed) read back as the current time.
Drop the initializer, relax `internal set` to `set`, and stamp
LastActivityDate explicitly at the two creation call sites
(DeviceValidator.GetDeviceFromRequest and DeviceRequestModel.ToDevice). Adds
an integration regression test that creates a device with an explicit null
LastActivityDate and asserts the read path surfaces null. Augments
DeviceValidatorTests.GetDeviceFromRequest_RawDeviceInfoValid_ReturnsDevice
to lock in the creation-time stamp.
* PM-37621 - DeviceSeeder - creation should set LastActivityDate
Add *.lscache, *.dmp, and *.dump to .gitignore following the pattern
established in dotnet/runtime#126718 and tracked in
microsoft/vscode-dotnettools#2952.
*.lscache files are generated by the C# Dev Kit language server and
appear as untracked noise in any developer's working tree. *.dmp and
*.dump files are .NET memory dump artifacts that can be large and may
contain sensitive in-process data.
* PM-37166 - Add ClientVersion to Device entity and repository contract
* PM-37166 - Add ClientVersion SQL schema and refactor bump stored procedures
* PM-37166 - Implement combined bump in repositories and add EF migrations
EF snapshot regeneration also absorbs Collection / CollectionGroup /
CollectionUser namespace moves (Bit.Infrastructure.EntityFramework.Models
-> Bit.Infrastructure.EntityFramework.AdminConsole.Models) that were left
un-regenerated by PR #7523 (PM-35489). Namespace-only, no SQL impact;
flagged with the AC team for awareness.
* PM-37166 - Replace DeviceLastActivityCacheService with DeviceDataCacheService
* PM-37166 - Replace BumpDeviceLastActivityDateCommand with BumpDeviceDataCommand
* PM-37166 - Pass ClientVersion through identity request validators
* PM-37166 - Align migration script with SQL style guide
Refresh Device_ReadBy* sprocs after DeviceView change so their cached
schema picks up ClientVersion, swap retired-sproc drops to
DROP PROCEDURE IF EXISTS, and tighten the ALTER TABLE indent in step 1.
* PM-37166 - Rename BumpData to UpdateLastActivity across device write pathway
The "BumpData" naming was vague — "data" named a category, not the thing
being written. Rename to "UpdateLastActivity" everywhere: SP, repositories,
command, cache, validators, tests. "Last activity" names the event of the
device's most recent appearance; LastActivityDate (when) and ClientVersion
(what was running) are facts we observed about that event. ClientVersion is
treated as a property of the activity event rather than an independent value,
so future last-observed properties (last IP, OS, etc.) slot in without renaming.
The SQL layer uses Update* per architect guidance on bitwarden/server#7302;
the Bump* SPs in this codebase are legacy and not being extended. The
extensibility note lives on IUpdateDeviceLastActivityCommand with short
pointers from the SP, repo, and cache.
Cache key prefix changes from device:data: to device:last-activity: — safe
because the cache is only a write-suppression optimization (SP guards ensure
correctness) and entries TTL out within 24h.
Migration renamed to 2026-05-14 to reflect the rewrite.
* PM-37166 - Add UTF-8 BOM to device last activity cache files
Aligns file encoding with the repo's .editorconfig (charset = utf-8-bom for .cs) so dotnet format --verify-no-changes passes.
* PM-37166 - Compare LastActivityDate at second precision in device creation test
GetManyByUserIdWithDeviceAuth_ReturnsLastActivityDate_ForNewDeviceAsync was
flaking on SqlServer: Dapper binds DateTime params as legacy `datetime`
(~3.33ms granularity), so the entity initializer's UtcNow can be rounded a
few ms earlier than the in-memory `beforeCreation` capture, making a strict
>= comparison occasionally false. Truncate both sides to the second to
absorb that drift while still rejecting stale or defaulted values.
* PM-37166 - Rename Device.ClientVersion EF migration
Renames migration class/files from AddDeviceClientVersionRefactorDeviceDataBump
to AddDeviceClientVersion to drop stale "Bump" terminology and the misleading
"RefactorDeviceData" prefix. The EF migration only adds the ClientVersion
column; the BumpData -> UpdateLastActivity SP refactor lives in MSSQL .sql
files and has no EF representation.
* PM-37166 - Document null-is-no-op semantics for ClientVersion on IUpdateDeviceLastActivityCommand
Tighten the interface-level summary and add a <param> note clarifying that
a null clientVersion is treated as "no opinion" and will not clear an
existing stored value.
* PM-37166 - Regenerate Device.ClientVersion EF migration on post-#7634 baseline
PR #7634 merged AddLastApiKeyRotationDateToUserTable into main while this
branch was open. The prior AddDeviceClientVersion migration's frozen model
snapshot (its .Designer.cs) was generated before that PR landed, so it did
not include User.LastApiKeyRotationDate. Applying migrations incrementally
against that stale snapshot would produce an inconsistent model graph.
Regenerated AddDeviceClientVersion on top of the merged-from-main baseline
so the new .Designer.cs files include both columns. The migration body
itself still only adds Device.ClientVersion; the top-level
DatabaseContextModelSnapshot.cs files were already correct from git's
three-way merge.
New timestamps (20260514192xxx) come after the User migration
(20260514011xxx), preserving migration order.
* PM-37166 - util/Migrator/DbScripts/2026-05-14_00_AddDeviceClientVersionAndUpdateLastActivitySp.sql - fix wrong comment
* PM-37166 - Bump Device.ClientVersion column width from 20 to 43
43 is the upper bound of Version.ToString() for any input parseable by
Version.TryParse — four Int32 components (Int32.MaxValue = 10 digits)
joined by 3 dots. Sizing to the type's mathematical max prevents
SQL Server error 8152 on malformed/hostile Bitwarden-Client-Version
headers without paying the cost of normalization at the call sites.
Real Bitwarden CalVer (YYYY.M.B) remains well within bounds at ~9 chars.
- Device.cs [MaxLength] + entity doc comment
- SSDT table + 4 stored procedures
- Cloud migration ALTER TABLE + SP parameters
- EF migrations regenerated for MySQL / Postgres / SQLite
* PM-37166 - Defer dropping old single-column UpdateLastActivityDate SPs to follow-up
Server and DB deploys are decoupled, so dropping the old SPs in the same migration that
introduces the new combined ones would break server rollback. Per discussion on PR #7632:
- Remove DROP PROCEDURE statements from the migration; replace with a note explaining the deferral.
- Restore the old Device_UpdateLastActivityDate{ById,ByIdentifierUserId}.sql files in src/Sql/dbo
so the SSDT source-of-truth stays aligned with deployed schema (EDD).
A follow-up ticket will drop the old SPs and delete the .sql files together once we're
confident no deployed server version still calls them.
* PM-37166 - Pass @LastActivityDate into Device_UpdateLastActivity SPs
Bitwarden convention is to compute timestamps in the application layer
and pass them as DATETIME2(7) params, not call GETUTCDATE() inside SPs.
Dapper repo now computes DateTime.UtcNow locally (matching the EF repo
and UserRepository.cs precedent) and passes LastActivityDate through.
* PM-37165 - Add LastApiKeyRotationDate column to User
Adds a nullable DATETIME2(7) LastApiKeyRotationDate column on the User
table alongside the other Last*Date audit columns. Covers the MSSQL
table, view, User_Create / User_Update stored procedures (new optional
parameter, EDD-safe with default NULL), the SSDT source-of-truth, and
EF migrations for MySql, Postgres, and Sqlite.
Repository round-trip integration tests verify that CreateAsync
defaults the column to NULL and ReplaceAsync persists it across all
four providers.
* PM-37165 - Add RotateUserApiKeyCommand under Auth/UserFeatures
Extracts user API key rotation out of UserService into a new CQS
command at src/Core/Auth/UserFeatures/UserApiKey/, mirroring the
existing decomposition pattern for other Auth user features. The
command generates a new 30-char ApiKey, bumps RevisionDate, sets
LastApiKeyRotationDate, and persists via IUserRepository.ReplaceAsync.
Adds the PM37165_RotateUserApiKeyCommand feature flag so the new path
can be rolled out behind a flag in a follow-up commit. Registers the
command via AddUserApiKeyCommands inside AddUserServices.
Unit tests verify the command assigns a fresh key, updates both
RevisionDate and LastApiKeyRotationDate to the same recent UTC value,
and calls ReplaceAsync exactly once.
* PM-37165 - Flag-gate rotate-api-key endpoint to new command
Wires AccountsController.RotateApiKey to dispatch between
IRotateUserApiKeyCommand (flag on) and the legacy
UserService.RotateApiKeyAsync (flag off) based on
PM37165_RotateUserApiKeyCommand. Both paths preserve the existing
auth and secret-verification guards, which run before the flag
branch.
Marks IUserService.RotateApiKeyAsync and its implementation [Obsolete]
pointing callers at IRotateUserApiKeyCommand, with TODOs tying their
removal to the flag cleanup. The body of the legacy method is
deliberately unchanged so it does NOT write LastApiKeyRotationDate
while the flag is off; that genuinely gates the new behavior so the
ramp is observable and reversible. The single remaining call site
(the controller fallback) is wrapped in #pragma warning disable
CS0618 so the attribute continues to flag any new callers.
Tests:
- AccountsControllerTests: dispatch tests for both flag states; the
auth and bad-secret guard tests are parameterized over flag state.
Pre-existing typo in two tests that called _sut.ApiKey() instead of
_sut.RotateApiKey() is fixed.
- UserServiceTests: regression test locks in the legacy non-write
behavior so it cannot drift before the flag is removed.
- AccountsControllerTest (integration): three endpoint tests cover
flag-off (LastApiKeyRotationDate stays NULL), flag-on (column is
populated), and bad-secret over both flag states (no rotation
occurs).
Each flag-state-specific test carries a TODO breadcrumb describing
the exact rename or deletion when the flag is cleaned up.
* PM-37165 - Tweak comment
* PM-37165 - Move LastApiKeyRotationDate to end of User schema
Append the new column to the end of User.sql, UserView.sql, the
matching CREATE OR ALTER VIEW in the migrator script, and the User
entity so SSDT mirrors what ALTER TABLE ADD produces in production.
* expose the ExemptFromBillingAutomation to client
* Fix the failing database
* Rename the file to resolve chronological order error
* Renamed the migration file name
* fix the failing database
* Suppress warnings for exempt orgs at the query level
Gate InactiveSubscription and ResellerRenewal warnings inside
GetOrganizationWarningsQuery on Organization.ExemptFromBillingAutomation
instead of plumbing the field through the profile response, view models,
EF queries, and SQL views.
* Add the ExemptFromBillingAutomation property
---------
Co-authored-by: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com>
* Refactor admin reset password email templates and update email service message
- Updated Handlebars and MJML templates for the admin reset password email to improve clarity and user instructions.
- Enhanced the text version to provide step-by-step recovery instructions.
- Modified the email service to reflect the organization name in the email subject line.
* Update admin reset password email template to adjust font size for clarity
* Adjust font size in admin reset password email template for improved readability
* Enhance admin reset password email by adding WebVaultUrl and SiteName to the model for improved context and user experience.
* Refactor admin reset password email templates for clarity and user guidance
- Updated text and Handlebars templates to improve messaging around account recovery steps.
- Adjusted links to direct users appropriately based on password reset status.
* Update admin reset password email templates to reflect changes in recovery link logic
- Modified Handlebars and MJML templates to adjust the recovery link based on the two-factor reset status.
- Ensured consistency in messaging across HTML and text versions of the email.
* Update HandlebarsMailService to use email as UserName for account recovery
This produces inherently flaky tests because it works by
fully migrating a database, and then re-running a specified
migration (the data migration) out of order. This means that
subsequent changes to the db schema will cause the migration
tests to fail, because the data migration will be re-run after
the schema has changed. Needs to be fixed before use.
* Remove BW-GHAPP tokens from repository-management workflow
- Remove all Azure Key Vault and BW-GHAPP token generation
- Use github.token instead of app token
- Use github-actions[bot] email instead of actions@github.com
- Create PR with version bump instead of pushing directly to main
- Update permissions (remove id-token, add pull-requests for bump_version)
- No GPG signing to remove (wasn't present)
* Fix
template injection security issue
* Remove BW-GHAPP tokens from repository-management workflow
- Remove all Azure Key Vault and BW-GHAPP token generation
- Use github.token instead of app token
- Use github-actions[bot] email instead of actions@github.com
- Create PR with version bump instead of pushing directly to main
- Update permissions (remove id-token, add pull-requests for bump_version)
- Add 'version update' label to automated PRs
- Fix template injection security issue
- Remove all Azure Key Vault and BW-GHAPP token generation
- Use github.token instead of app token
- Use github-actions[bot] email instead of actions@github.com
- Create PR with version bump instead of pushing directly to main
- Update permissions (remove id-token, add pull-requests for bump_version)
- No GPG signing to remove (wasn't present)