Preserve typed hosted trial issuance conflicts

Keep hosted trial request-verification and checkout pages aligned with the canonical issuance conflict type so duplicate recovery-email usage is surfaced distinctly from organization reuse.
This commit is contained in:
rcourtman
2026-03-25 11:25:08 +00:00
parent 6f429a098d
commit b204ed5d86
3 changed files with 48 additions and 4 deletions
@@ -264,7 +264,9 @@ still use a secure hosted Stripe-backed session internally, but the customer
copy must present the flow as starting a trial for the originating Pulse
instance, not as a generic purchase funnel. Recovery-contact fields such as
work email and optional company name must remain clearly secondary to the
instance-bound entitlement handoff.
instance-bound entitlement handoff. Hosted form-stage issuance conflicts must
also preserve the canonical reason shape: duplicate recovery-email usage must
not be flattened into an organization-level message.
That same hosted owner also applies after Stripe returns to
`/trial-signup/complete`: customer-facing completion failures must stay inside
owned trial UX rather than dropping raw control-plane error strings, and they
+13 -2
View File
@@ -664,7 +664,7 @@ func (h *TrialSignupHandlers) HandleRequestVerification(w http.ResponseWriter, r
return
}
if conflict != nil {
data.ErrorMessage = "This organization has already used a Pulse Pro trial. Upgrade the existing account or contact support if you need help."
data.ErrorMessage = trialSignupIssuanceConflictMessage(conflict)
h.renderTrialSignupPage(w, r, http.StatusConflict, data)
return
}
@@ -842,7 +842,7 @@ func (h *TrialSignupHandlers) HandleCheckout(w http.ResponseWriter, r *http.Requ
return
}
if conflict != nil {
data.ErrorMessage = "This organization has already used a Pulse Pro trial. Upgrade the existing account or contact support if you need help."
data.ErrorMessage = trialSignupIssuanceConflictMessage(conflict)
h.renderTrialSignupPage(w, r, http.StatusConflict, data)
return
}
@@ -1428,6 +1428,17 @@ func trialSignupFailureDataForPage(cfg *CPConfig, data trialSignupPageData, kind
return page
}
func trialSignupIssuanceConflictMessage(conflict *TrialSignupIssuanceConflict) string {
switch {
case conflict == nil:
return "This Pulse Pro trial request conflicts with a previous trial issuance. Upgrade the existing account or contact support if you need help."
case conflict.Kind == trialSignupConflictEmail:
return "This recovery email has already used a Pulse Pro trial. Upgrade the existing account or contact support if you need help."
default:
return "This organization has already used a Pulse Pro trial. Upgrade the existing account or contact support if you need help."
}
}
func buildCPURL(baseURL, path string, query url.Values) string {
base := strings.TrimSpace(baseURL)
if base == "" {
+32 -1
View File
@@ -186,7 +186,7 @@ func TestTrialSignupHandleRequestVerificationRejectsEmailThatAlreadyUsedTrial(t
if rec.Code != http.StatusConflict {
t.Fatalf("status=%d, want %d body=%q", rec.Code, http.StatusConflict, rec.Body.String())
}
if !strings.Contains(rec.Body.String(), "organization has already used a Pulse Pro trial") {
if !strings.Contains(rec.Body.String(), "recovery email has already used a Pulse Pro trial") {
t.Fatalf("expected duplicate trial message, got %q", rec.Body.String())
}
}
@@ -409,6 +409,37 @@ func TestTrialSignupHandleCheckoutCreatesFreshSessionWhenRecordAlreadyHasSession
}
}
func TestTrialSignupHandleCheckoutRejectsEmailThatAlreadyUsedTrial(t *testing.T) {
h, store, sender := newTrialSignupTestHandler(t)
rawToken := requestTrialVerification(t, h, sender)
verifiedToken := verifyTrialRequest(t, h, rawToken)
requestID := parseVerifiedTokenRequestID(t, h, verifiedToken)
if err := store.MarkTrialIssued(requestID, h.now().UTC()); err != nil {
t.Fatalf("MarkTrialIssued: %v", err)
}
form := url.Values{
"org_id": {"default"},
"return_url": {"https://pulse.example.com/auth/trial-activate"},
"instance_token": {"tsi_test"},
"name": {"Test User"},
"email": {"owner@example.com"},
"company": {"Pulse Labs"},
}
req := httptest.NewRequest(http.MethodPost, "/api/trial-signup/checkout", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rec := httptest.NewRecorder()
h.HandleCheckout(rec, req)
if rec.Code != http.StatusConflict {
t.Fatalf("status=%d, want %d body=%q", rec.Code, http.StatusConflict, rec.Body.String())
}
if !strings.Contains(rec.Body.String(), "recovery email has already used a Pulse Pro trial") {
t.Fatalf("expected duplicate email trial message, got %q", rec.Body.String())
}
}
func TestTrialSignupHandleCompleteRedirectsWithActivationToken(t *testing.T) {
pub, priv, err := ed25519.GenerateKey(nil)
if err != nil {