Skip to content

[PM-38388] Support sales-assisted trials - #7881

Merged
enmande merged 34 commits into
mainfrom
auth/pm-38388/support-sales-assisted-trials
Jul 15, 2026
Merged

[PM-38388] Support sales-assisted trials#7881
enmande merged 34 commits into
mainfrom
auth/pm-38388/support-sales-assisted-trials

Conversation

@enmande

@enmande enmande commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

🎟️ Tracking

PM-38388

📔 Objective

What changed

Registration path (Identity) - 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.
Admin portal (Admin) - 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.
Invitation email (Core) - 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.
SendSalesAssistedTrialInvitationCommand (Core.Billing) - 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`).

Tests

  • Unit tests for SendSalesAssistedTrialInvitationCommand — covers happy path, existing user rejection, trial length range validation, ExpiryDays sourced from settings (not trial length), and sender email forwarding.
  • Unit tests for RegisterUserCommand — covers token validation, expiry, and email binding.
  • Integration tests for the registration endpoint — three new cases in AccountsControllerTests: valid token with disableUserRegistration=true succeeds and creates the user; undecodable token returns 400; token bound to a different email returns 400.
  • IdentityApplicationFactory.RegisterNewIdentityFactoryUserAsync now explicitly nulls SalesAssistedToken before 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

  • The controller lives in the Auth area even though SendSalesAssistedTrialInvitationCommand is in Core.Billing. Auth area = registration bypass concern; Billing = email and trial lifecycle concern. A comment in the controller explains this.
  • The disableUserRegistration bypass is intentional and load-bearing. The token is the authorization mechanism.
  • The feature flag (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

Comment thread src/Admin/Auth/Controllers/SalesAssistedTrialController.cs Fixed
@codecov

codecov Bot commented Jun 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 69.45455% with 84 lines in your changes missing coverage. Please review.
✅ Project coverage is 62.08%. Comparing base (c32cd8e) to head (0d97e34).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
...il/Mailer/SalesAssistedTrialInvitationEmailView.cs 21.31% 48 Missing ⚠️
...c/Admin/Auth/Views/SalesAssistedTrial/Index.cshtml 0.00% 15 Missing ⚠️
src/Admin/Utilities/RolePermissionMapping.cs 0.00% 6 Missing ⚠️
src/Admin/Views/Shared/_Layout.cshtml 0.00% 6 Missing ⚠️
...Api/Request/Accounts/RegisterFinishRequestModel.cs 45.45% 5 Missing and 1 partial ⚠️
...egistration/Implementations/RegisterUserCommand.cs 89.28% 2 Missing and 1 partial ⚠️
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.
📢 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.

@enmande enmande added t:feature Change Type - Feature Development ai-review Request a Claude code review labels Jun 29, 2026
@github-actions

github-actions Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

🤖 Bitwarden Claude Code Review

Overall Assessment: APPROVE

Reviewed the sales-assisted trials feature end-to-end: the SalesAssistedRegistrationTokenable and its factory, the RegisterUserViaSalesAssistedToken registration path (including the intentional open-registration bypass), the SendSalesAssistedTrialInvitationCommand, the Admin controller/view/model, DI wiring, and the invitation email. Verified the token authorization is signed, expiring, and email-bound (case-insensitive), that DI is complete in both Identity and Admin startup paths, and that unit plus integration coverage exercises the happy path, expiry, email-binding, and validation failures.

Code Review Details

No new findings at or above the confidence threshold.

Notes from validation (no action required):

  • The ValidateOpenRegistrationAllowed() bypass in RegisterUserViaSalesAssistedToken is intentional and token-authorized — the signed, email-bound, expiring token is the authorization mechanism. Validation correctly delegates to the tokenable's static validator and null-checks the post-validation TryUnprotect.
  • Surfacing ex.Message to the view in SalesAssistedTrialController matches the established Admin-project convention (ToolsController, ProvidersController, OrganizationsController) for this internal, permission-gated tool; the unexpected-exception log path correctly omits the message.
  • AddSingleton (rather than TryAddSingleton) for the tokenizer registrations matches the existing local convention in AddTokenizers.
  • Prior review findings (inline token validation, controller flag gating, constant placement, obsolete SendWelcomeEmailAsync) are all resolved.

Comment thread src/Admin/Auth/Controllers/SalesAssistedTrialController.cs
@sonarqubecloud

Copy link
Copy Markdown

@enmande
enmande marked this pull request as ready for review June 29, 2026 22:31
enmande added 3 commits July 13, 2026 12:26
…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.
@enmande
enmande marked this pull request as ready for review July 13, 2026 19:01
@enmande
enmande requested a review from ike-kottlowski July 13, 2026 19:02
ike-kottlowski
ike-kottlowski previously approved these changes Jul 13, 2026

@ike-kottlowski ike-kottlowski left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

recent changes are good.

connerbw
connerbw previously approved these changes Jul 14, 2026
@enmande
enmande dismissed stale reviews from connerbw and ike-kottlowski via a892297 July 14, 2026 15:35
Comment thread src/Admin/Auth/Controllers/SalesAssistedTrialController.cs Dismissed
…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.
@enmande
enmande requested a review from ike-kottlowski July 14, 2026 17:08
ike-kottlowski
ike-kottlowski previously approved these changes Jul 14, 2026
@enmande
enmande requested a review from connerbw July 14, 2026 17:10
connerbw
connerbw previously approved these changes Jul 14, 2026
@enmande
enmande dismissed stale reviews from connerbw and ike-kottlowski via 0d97e34 July 15, 2026 09:35
@enmande enmande removed the needs-qa label Jul 15, 2026
@enmande
enmande enabled auto-merge (squash) July 15, 2026 13:14
@enmande
enmande merged commit ac18ce9 into main Jul 15, 2026
44 of 46 checks passed
@enmande
enmande deleted the auth/pm-38388/support-sales-assisted-trials branch July 15, 2026 13:27
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.

3 participants