From 6ebb3fe2267537fa74cb31057c927e2f384913bf Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 25 Mar 2026 11:27:10 +0000 Subject: [PATCH] Render hosted trial terminal conflicts as outcome pages Move pre-checkout hosted trial conflicts onto the owned terminal outcome UX so users do not land back in an editable form for non-retryable duplicate-trial states. --- .../v6/internal/subsystems/cloud-paid.md | 3 +- internal/cloudcp/trial_signup_handlers.go | 16 +++++++--- .../cloudcp/trial_signup_handlers_test.go | 30 +++++++++++++------ 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md index 2bef44d81..7367be69b 100644 --- a/docs/release-control/v6/internal/subsystems/cloud-paid.md +++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md @@ -266,7 +266,8 @@ 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. 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. +not be flattened into an organization-level message, and terminal conflicts +must render as owned hosted outcome UX rather than editable inline form state. 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 diff --git a/internal/cloudcp/trial_signup_handlers.go b/internal/cloudcp/trial_signup_handlers.go index fe2f5fa72..f68fac1c7 100644 --- a/internal/cloudcp/trial_signup_handlers.go +++ b/internal/cloudcp/trial_signup_handlers.go @@ -664,8 +664,12 @@ func (h *TrialSignupHandlers) HandleRequestVerification(w http.ResponseWriter, r return } if conflict != nil { - data.ErrorMessage = trialSignupIssuanceConflictMessage(conflict) - h.renderTrialSignupPage(w, r, http.StatusConflict, data) + h.renderTrialSignupFailurePage(w, r, http.StatusConflict, trialSignupFailureDataForPage( + h.cfg, + data, + trialSignupFailureConflict, + trialSignupIssuanceConflictMessage(conflict), + )) return } } @@ -842,8 +846,12 @@ func (h *TrialSignupHandlers) HandleCheckout(w http.ResponseWriter, r *http.Requ return } if conflict != nil { - data.ErrorMessage = trialSignupIssuanceConflictMessage(conflict) - h.renderTrialSignupPage(w, r, http.StatusConflict, data) + h.renderTrialSignupFailurePage(w, r, http.StatusConflict, trialSignupFailureDataForPage( + h.cfg, + data, + trialSignupFailureConflict, + trialSignupIssuanceConflictMessage(conflict), + )) return } record = &TrialSignupRecord{ diff --git a/internal/cloudcp/trial_signup_handlers_test.go b/internal/cloudcp/trial_signup_handlers_test.go index 31a6feece..c36525462 100644 --- a/internal/cloudcp/trial_signup_handlers_test.go +++ b/internal/cloudcp/trial_signup_handlers_test.go @@ -186,9 +186,13 @@ 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(), "recovery email has already used a Pulse Pro trial") { - t.Fatalf("expected duplicate trial message, got %q", rec.Body.String()) - } + assertTrialSignupFailurePageContains(t, rec.Body.String(), + "Trial already used", + "This recovery email has already used a Pulse Pro trial.", + "pulse.example.com", + "This trial request cannot be restarted for the same recovery contact or organization.", + ) + assertTrialSignupFailurePageOmits(t, rec.Body.String(), "Continue To Secure Trial Setup", "