[PM-38388] Support sales-assisted trials#7881
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #7881 +/- ##
==========================================
+ Coverage 61.21% 61.38% +0.17%
==========================================
Files 2219 2243 +24
Lines 98059 98788 +729
Branches 8848 8937 +89
==========================================
+ Hits 60025 60641 +616
- Misses 35917 36010 +93
- Partials 2117 2137 +20 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
🤖 Bitwarden Claude Code ReviewOverall Assessment: APPROVE Reviewed the latest commit ( Code Review Details
|
|
SalesAssistedRegistrationTokenableFactory computed ExpirationDate by passing lifetimeDays into the tokenable's own constructor, rather than setting it directly — violating .claude/rules/auth-tokenables.md's "factory MUST set ExpirationDate from a GlobalSettings value" and diverging from the OrgUserInviteTokenableFactory exemplar it was modeled on. That structure also made the mint-time computation untestable without a tolerance window: SalesAssistedRegistrationTokenableTests carried a 10-second _timeTolerance to paper over two independent, unsynchronized DateTime.UtcNow reads (one in the test, one in the constructor). - Factory now sets ExpirationDate directly via an injected TimeProvider, matching OrgUserInviteTokenableFactory's shape; tests freeze time and assert exact values instead of tolerating a window. - Both SalesAssistedRegistrationTokenable constructors are now internal, including the [JsonConstructor] one — previously public, so nothing but a comment stopped other assemblies from bypassing the factory. Verified System.Text.Json can still deserialize via a non-public [JsonConstructor] ctor (round-trip test still passes). - Deleted a test covering a constructor responsibility that no longer exists; fixed two tests whose Valid/IsExpired assertions implicitly relied on the constructor's removed default ExpirationDate. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
connerbw
left a comment
There was a problem hiding this comment.
Approved for billing with a nit
| public const string DebugDisableSelfHostPremiumCheck = "debug-disable-self-host-premium-check"; | ||
| public const string PM35215_BusinessPlanPriceMigration = "pm-35215-business-plan-price-migration"; | ||
| public const string PM37597_AlwaysEnableStripeAutomaticTax = "pm-37597-always-enable-stripe-automatic-tax"; | ||
| public const string PM35092AuthSalesAssistedTrials = "pm-35092-auth-sales-assisted-trials"; |
There was a problem hiding this comment.
⛏️ This one is landing in the billing FF section. Could you move this up to auth's on line 166?



🎟️ Tracking
PM-38388
📔 Objective
What changed
Registration path (
- Added `SalesAssistedRegistrationTokenable` — a signed, expiring token carrying the prospect's email and optional name. Lifetime is configured via `globalSettings.salesAssistedRegistrationTokenLifetimeDays`. - Added `ISalesAssistedRegistrationTokenableFactory` — the sole mint path; guards the internal lifetime-setting constructor per the tokenable factory convention. - Added `RegisterUserViaSalesAssistedToken` to `RegisterUserCommand`. Unlike the standard path, this method intentionally omits `ValidateOpenRegistrationAllowed()` — the token itself is the authorization.Identity)Admin portal (
- New `SalesAssistedTrialController` (Auth area) with a form to configure and send invitations. Placed in Auth rather than Billing because the feature is fundamentally about bypassing the registration gate, not about billing or trial setup. - Nav link added to the Tools dropdown, gated behind the `pm-35092-auth-sales-assisted-trials` feature flag and the `Org_InitiateSalesAssistedTrial` permission. The flag keeps the entry hidden in Admin until the corresponding client work ships. - Form validation moved server-side: removed `type="email"` and `min`/`max` from inputs; added `[EmailAddress]` to the model so all fields validate consistently via ASP.NET model binding.Admin)Invitation email (
- New MJML-based HTML email using the `mj-bw-ac-hero` component, product-aware feature bullets, and a "We're here for you!" support footer. - `SalesAssistedTrialInvitationEmailView` exposes computed properties (`HeroTitle`, `ProductName`, `SpotImageUrl`, `Features`) so the template stays logic-free. - `ExpiryDays` sourced from `globalSettings.SalesAssistedRegistrationTokenLifetimeDays`, distinct from the trial length.Core)
- Validates product tier, trial length range, and that the email is not already registered before minting the token. - Delegates mail send to `IMailer` (not the deprecated `IMailService`).SendSalesAssistedTrialInvitationCommand(Core.Billing)Tests
SendSalesAssistedTrialInvitationCommand— covers happy path, existing user rejection, trial length range validation,ExpiryDayssourced from settings (not trial length), and sender email forwarding.RegisterUserCommand— covers token validation, expiry, and email binding.AccountsControllerTests: valid token withdisableUserRegistration=truesucceeds and creates the user; undecodable token returns 400; token bound to a different email returns 400.IdentityApplicationFactory.RegisterNewIdentityFactoryUserAsyncnow explicitly nullsSalesAssistedTokenbefore the finish call — AutoFixture previously populated it with a random value that shadowed the email verification token and caused unrelated integration tests to fail.Notes for reviewers
Autharea even thoughSendSalesAssistedTrialInvitationCommandis inCore.Billing. Auth area = registration bypass concern; Billing = email and trial lifecycle concern. A comment in the controller explains this.disableUserRegistrationbypass is intentional and load-bearing. The token is the authorization mechanism.pm-35092-auth-sales-assisted-trials) must be enabled before the Admin nav link is visible📸 Screenshots
Happy Path
Admin fills out the supporting form and submits to onboard a new user and start their trial.
pm-38388__happy-path.mov
User Exists
The intended user already exists in the environment; Admin is denied use of this feature with a descriptive error message.
From here, they can use existing pathways tailor-made for trial assistance. _Why?_ This flow is primarily concerned with onboarding users to environments where open registration is disallowed/disabled. This flow should be prioritized to solve that problem; initiating a trial for an existing user would constitute an off-label use of this feature.pm-38388__user-exists.mov
Form Validation
Exercising various paths to submit the form.
pm-38388__form-validation.mov