[PM-38388] Support sales-assisted trials - #7881
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #7881 +/- ##
==========================================
+ Coverage 62.05% 62.08% +0.02%
==========================================
Files 2277 2284 +7
Lines 99452 99722 +270
Branches 8983 9015 +32
==========================================
+ Hits 61718 61908 +190
- Misses 35561 35639 +78
- Partials 2173 2175 +2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
🤖 Bitwarden Claude Code ReviewOverall Assessment: APPROVE Reviewed the sales-assisted trials feature end-to-end: the Code Review DetailsNo new findings at or above the confidence threshold. Notes from validation (no action required):
|
|
…n the view model Move the "payment optional requires a trial period" rule into SalesTrialInviteModel via IValidatableObject, the sole cross-field validation convention already used throughout src/Admin (e.g. OrganizationEditModel, CohortFormModel, CreateSubscriptionDiscountModel). This fails fast and renders inline next to the Payment Optional field (new asp-validation-for span) instead of only after the mailer/token layer is reached, surfaced as a generic top-of-form error. The identical check in SendSalesAssistedTrialInvitationCommand is left unchanged as a defense-in-depth safeguard for any future non-Admin-form caller — the same "ViewModel fast-fail + command boundary check" duplication already exists for the EndDate/StartDate rule across SubscriptionDiscount, CreateSubscriptionDiscountModel, and EditSubscriptionDiscountModel.
… trial form Hand-filter ProductTierType.TeamsStarter out of the Product Tier dropdown, mirroring _OrganizationForm.cshtml's existing PlanType.TeamsStarter filter -- the codebase's own precedent for excluding this exact value. There is no more-authoritative, data-driven signal to filter on at this enum's granularity: Plan.Disabled/Available is a per-SKU (PlanType) flag from the Pricing Service, one level more specific than the coarse ProductTierType grouping this form uses. Add a matching Validate() rule as a server-side backstop against a hand-crafted POST bypassing the dropdown.
ike-kottlowski
left a comment
There was a problem hiding this comment.
recent changes are good.
…validator RegisterUserCommand reimplemented token validation inline instead of calling SalesAssistedRegistrationTokenable's static validator, leaving it dead production code and collapsing the Expired/Invalid error distinction into one generic message. Delegate to the static validator, mirroring TryValidateOrgInviteToken's existing idiom in this file, and keep an explicit null-check on the post-validation TryUnprotect call for defense in depth.



🎟️ 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