Self-Signup — End-to-End Test Scenario
Overview
End-to-end test covering the full self-signup wizard — from landing on /signup through Razorpay payment to a fully provisioned tenant account ready for login.
Tested via: MCP Playwright browser (real browser, not CLI specs)
Portals: Dashboard (port 3000), API (port 3003)
Date verified: 2026-04-10 (full browser run — UPI success@razorpay, Professional plan, ₹58,998.82)
Prerequisites
- All Docker containers healthy (
docker ps) - All dev servers running (see startup procedure)
- Scheduler worker running (
pnpm --filter @leadmetrics/server-scheduler dev) - Razorpay test keys configured in
apps/api/.env:RAZORPAY_KEY_ID=rzp_test_... RAZORPAY_KEY_SECRET=... RAZORPAY_WEBHOOK_SECRET=... - At least one Plan + Offering seeded in the database (run
pnpm --filter api db:seed) DASHBOARD_URLset inapps/api/.env(used in session-resume emails)
Actors
| Actor | Credentials | Portal |
|---|---|---|
| New customer (self-signup) | (created during the test) | Dashboard (3000) |
| Super admin (verification only) | superadmin@leadmetrics.ai / admin | Manage (3001) |
Session Storage Keys (reference)
| Key | Set by | Content |
|---|---|---|
signup_step1 | S1 on submit | { country, title, firstName, lastName, email, mobile, password } |
signup_session_token | S1 on submit (from API) | JWT — passed as Bearer in all subsequent step calls |
signup_step2 | S2 on submit | { companyName, companyWebsite, industry } |
signup_step3 | S3 on submit | { state, hasGst, gstNumber, sezStatus, panNumber } |
Scenario A — Full Happy Path (India, Professional Plan, UPI)
Step 1 — Contact Details (/signup)
- Navigate to
http://localhost:3000/signup - Select Country: India
- Fill contact fields:
- Title: Mr.
- First Name: Moble
- Last Name: Joseph
- Email:
moble+signup-test-{timestamp}@leadmetrics.ai - Mobile: 9447884435
- Password: Password123
- Click Continue →
API call: POST /auth/v1/register/start
Expected:
RegistrationSessionrow created in DB withcurrentStage: "step_1"signup_session_tokenandsignup_step1written tosessionStorage- Navigated to
/signup/company - Session-link email queued (template:
signup-session-link) - Internal sales alert queued for scheduler (1 hr delay —
signup.alert.internal)
Step 2 — Company Details (/signup/company)
- Fill company fields:
- Company Name: Leadmetrics Test Pvt Ltd
- Industry: Technology (select from dropdown)
- Company Website: (leave blank — optional)
- Click Continue →
API call: POST /auth/v1/register/step/2 (with session token)
Expected:
RegistrationSession.currentStageupdated to"step_2"signup_step2written tosessionStorage- Country is India → navigated to
/signup/location - Early abandonment reminder queued for scheduler (1 hr —
signup.reminder.early)
Step 3 — Location & Tax (/signup/location)
India-only step. Country shown read-only as “India” (from Step 1).
- Select State: Kerala
- Click Yes, I have GST button
- GST Number field appears — enter:
32AABCU9603R1ZX - SEZ Status dropdown: No (default)
- PAN Number:
AABCU9603R(optional — can leave blank) - Click Continue →
API call: POST /auth/v1/register/step/3 (with session token)
Expected:
RegistrationSession.currentStageupdated to"step_3"signup_step3written tosessionStoragewith{ state: "Kerala", hasGst: true, gstNumber: "32AABCU9603R1ZX", sezStatus: "no" }- Tax label below GST toggle updates to show: “GST Treatment: Registered Business — 18% CGST/SGST (intra-state)”
- Navigated to
/signup/plan
Step 4 — Plan & Payment (/signup/plan)
- Wait for plan cards to load — Professional plan card should appear (₹49,999/month + 18% tax)
- Click Professional plan card (should be pre-selected — violet border + checkmark badge)
- Fill billing address:
- Billing Name: Leadmetrics Test Pvt Ltd (pre-filled from Step 2)
- Billing Email:
moble+signup-test-{timestamp}@leadmetrics.ai(pre-filled from Step 1) - Street Address: 42 MG Road, Infopark
- City: Kochi
- Postal / ZIP Code: 682030
- State: Kerala (pre-filled from Step 3)
- Country: India (read-only)
- Click Complete Sign-Up & Pay →
API call: POST /auth/v1/register/complete (with session token + plan + billing)
Expected:
- Razorpay Checkout modal opens with amount ₹58,998.82 (₹49,999 × 1.18)
- Modal header shows “Leadmetrics”
- Mobile pre-filled as +91 94478 84435
Step 5 — Razorpay UPI Payment
- In the Razorpay modal, click UPI in the payment method list (left rail)
- Scroll to “Pay with UPI ID / Number” section
- Enter UPI ID:
success@razorpay - Click Verify and Pay
- Modal shows “Confirming Payment — This will only take a few seconds.”
- Modal transitions to “Payment Successful”
Expected on Razorpay success screen:
- Heading: “Payment Successful”
- “You will be redirected in 4 seconds”
- Amount: ₹58,998.82
- Method: UPI
- Payment ID:
pay_…(Razorpay test ID)
Step 6 — Payment Verification & Account Creation
After the Razorpay modal auto-redirects:
Page shows: “Confirming payment… Please wait while we verify your payment.”
API call (client-side): POST /auth/v1/register/payment/verify
Expected (server-side actions):
- Razorpay signature verified
Tenantrecord created withstatus: onboarding,createdVia: "self-signup",signupSessionIdUserrecord created and linked to tenantTenantMemberrow created with roleownerSubscriptionrecord created (Professional plan)- Welcome email queued
RegistrationSession.currentStageset to"complete"
Final redirect: http://localhost:3000/login?registered=1
Step 7 — Login with New Account
Page shows: Login form with green banner “Account created! Sign in to get started.”
- Enter the email used in Step 1
- Enter password: Password123
- Click Sign in
Expected: Redirected to /dashboard showing the GettingStartedView (onboarding wizard — “AI agents are working on your account…”).
Step 8 — Verify Tenant in Manage Portal (optional but recommended)
Portal: Manage → /tenants
- Log in as
superadmin@leadmetrics.ai / admin - Search for the new tenant by company name
- Click to open the tenant detail page
Expected:
- Status: onboarding
- Plan: Professional
createdVia: self-signup- Subscriptions tab: one active Professional subscription
- Knowledge Base: 4 default datasets present
Scenario B — Non-India Customer (Step 3 Skipped)
For customers outside India, Step 3 (Location & Tax) is auto-completed server-side and the wizard skips directly from Step 2 to Step 4.
Steps
- Complete Step 1 with Country: United Arab Emirates (AE)
- Complete Step 2 with company details
- Expected: After submitting Step 2, page navigates directly to
/signup/plan—/signup/locationis skipped - Complete Step 4 — plan cards should show AED pricing if configured, else fallback to the default region
API behaviour: POST /auth/v1/register/step/2 detects non-India country and auto-creates the step 3 record server-side (autoSkipped: true in stepHistory).
Scenario C — Abandoned Signup (Reminder Emails)
Tests the scheduler’s abandonment handling. Requires scheduler worker to be running.
Setup
- Complete Step 1 only (do not proceed to Step 2)
- Note the
signup_session_tokenfromsessionStorage
What the scheduler does
After Step 1, two BullMQ jobs are enqueued with delays:
| Job | Delay | Template | Handler |
|---|---|---|---|
signup.reminder.early | 1 hour | signup-reminder-early | handleSignupReminderEmail |
signup.reminder.late | 24 hours | signup-reminder-late | handleSignupReminderEmail |
signup.alert.internal | 1 hour (once per session) | signup-alert-internal | handleSignupAlertEmail |
Reminder email behaviour
handleSignupReminderEmailfirst checksRegistrationSession.currentStage- If
currentStageisstep_4or"complete"— email is skipped (user finished or paid) - Otherwise — email sent with
resumeUrl = {DASHBOARD_URL}/signup?session={token} dedupeKeyprevents duplicate sends:signup-reminder-early__{sessionId}
Internal alert behaviour
handleSignupAlertEmailsends toSIGNUP_ALERT_EMAIL(e.g. sales@leadmetrics.ai)- Includes: name, email, mobile, company, step number, timestamp
- Priority: high
- Does NOT query the DB — fire-and-forget
dedupeKey=signup-alert-internal__{email}(one alert per email address)
Testing without waiting 1 hour
To test immediately, reduce the job delay in the scheduler and trigger manually, or call the task handler directly in a unit test (see apps/servers/scheduler/src/__tests__/signup-emails.test.ts).
Scenario D — Session Resume (Returning Visitor)
A user who starts signup but closes the browser can resume via the session link email.
Flow
- User completes Step 1 → receives session-link email with URL:
http://localhost:3000/signup?session={token} - User opens the link (new browser session, empty
sessionStorage) /signuppage detects?session=query param- API call:
GET /auth/v1/register/session(Bearer: token) - If
data.expiredis true → immediately redirects to/signup/expired - Otherwise → Step 1 form is pre-filled with: Country, First Name, Last Name, Email, Mobile, Title
signup_session_tokenwritten to sessionStorage;signup_session_restored = "1"flag set- Password field is left empty — user must re-enter it
- User types password and clicks Continue →
- Step 1 re-submits (
POST /auth/v1/register/start), new session token is created - Wizard proceeds from
/signup/company(Step 2) as normal
Expected:
- URL stays at
/signup?session=…while pre-filling - Form fields populated (name, email, mobile, country visible)
- Password field empty
- No automatic redirect to a later step — user always returns to Step 1
Note on currentStage: The frontend does NOT use the server’s currentStage to skip steps on resume. Regardless of which step the user abandoned, they are always brought back to Step 1 with pre-filled contact details. They re-enter their password and the wizard continues from Step 2 onward.
Expired token: If the API returns { expired: true } (which happens for valid-format tokens older than 30 days), the page calls router.replace("/signup/expired"). Malformed or invalid tokens are silently swallowed by the .catch() handler — the page shows a blank Step 1 form.
Scenario E — Expired Session Page (/signup/expired)
- Navigate directly to
http://localhost:3000/signup/expired
Expected:
- Heading: “Your Signup Link Has Expired”
- Body: explains the 30-day inactivity limit
- “Start New Signup” link →
href="/signup"(starts fresh) - “Contact Sales” link visible
Scenario F — Duplicate Email
A second signup attempt using an already-registered email.
- Navigate to
/signup - Enter an email that is already registered (e.g.
moble+signup-a-1@leadmetrics.aifrom a prior Scenario A run) - Fill remaining Step 1 fields and click Continue →
API call: POST /auth/v1/register/start
Expected:
- API returns
409 Conflictimmediately at Step 1 - User stays on
/signup— does not navigate to/signup/company - Inline error shown under the email field: “An account with this email already exists.”
Note: A second 409 guard also exists at register/complete (line 757 of auth.ts) to catch race conditions, but in practice the error is always surfaced at Step 1.
Scenario G — Payment Failure / Retry
- Complete Steps 1–4 (billing address filled, new unused email)
- Click Complete Sign-Up & Pay →
- In the Razorpay modal, enter UPI ID:
failure@razorpay→ click Verify and Pay - Razorpay shows “Payment could not be completed” failure screen
- Click the back arrow on the failure screen → returns to UPI options
- Click Close Checkout → Razorpay “Are you sure you want to exit?” overlay appears
- Click Yes, exit
Expected:
- Page URL stays
/signup/plan - Plan form is replaced by a “Payment Incomplete” screen
- Heading: “Payment Incomplete” (⚠️ icon)
- Body: “Your account has been created. Complete your payment to unlock all features.”
- Three action buttons:
- Retry Payment — re-opens Razorpay to attempt payment again
- Pay via Bank Transfer —
mailto:support@leadmetrics.ai?subject=Bank+Transfer+Payment - Contact Sales: +91 8590 468 816 —
tel:+918590468816
- Footer link: “You can also log in now and complete payment from your dashboard.”
Note: The Tenant and User records ARE created at register/complete (before Razorpay opens). Payment failure does not roll back account creation — the account exists but is in an unpaid state. Clicking “Retry Payment” re-opens the Razorpay modal for the same order.
Razorpay Test Credentials
| Method | Value | Behaviour |
|---|---|---|
| UPI ID | success@razorpay | Instant success, no OTP — use for all E2E tests |
| UPI ID | failure@razorpay | Simulates payment failure |
| Card (no 3DS) | 5267 3181 8797 5449 / exp 02/26 / CVV 123 | Mastercard — success without OTP popup |
| Card (3DS, OTP) | 4208 3000 9609 2278 / OTP 1234 | Visa — OTP prompt within Razorpay frame |
| Avoid | 4111 1111 1111 1111 | External bank 3DS popup — Playwright cannot interact with it |
Sample Test Data
Leadmetrics Test Account (verified 2026-04-10)
- Email: moble+test1@leadmetrics.ai
- Company: Leadmetrics Pvt Ltd
- Location: Kochi, Kerala — GST registered
- Plan: Professional (₹49,999/month)
- Amount charged: ₹58,998.82 (incl. 18% GST)
- Payment ID: pay_Sbp1J63X04LoAK (Razorpay test mode)
- Payment method: UPI — success@razorpay
- Post-payment URL:
/login?registered=1
Known Issues / Gotchas
| Issue | Detail |
|---|---|
| Razorpay modal is in an iframe | All Playwright interactions inside the modal must use f1e*-prefixed refs (iframe element refs). Normal page.click() selectors won’t reach the modal. Use browser_snapshot to get current refs. |
Card 4111 1111 1111 1111 triggers external 3DS popup | Razorpay routes this card through a bank-hosted OTP page in a separate tab. Playwright cannot switch to it in the MCP session. Always use success@razorpay UPI instead. |
| Razorpay modal has a “confirm close” overlay | Clicking the close (✕) button shows a “Are you sure?” overlay. Clicking ✕ again on the overlay is needed to fully dismiss it. |
planId is optional in register/complete | The Fastify JSON schema does NOT have planId in required. The handler auto-selects the first available plan if none is provided. Do not re-add it to required — it would cause a 400 before the auth check runs. |
| Plan cards only load if API server is running | The plan step fetches GET /api/auth/v1/plans on mount. If the API server is down, the submit button stays disabled. |
| Session token not persisted to localStorage | signup_session_token lives only in sessionStorage. Closing the browser tab and opening a new one clears it. The session-link email is the recovery path. |
billingPostal + billingState reach the API | Both fields are sent to register/complete and stored on the RegistrationSession. They are included in the billing address object used for invoice generation. |
| Scheduler worker is separate from the API worker | Abandonment emails are sent by apps/servers/scheduler, not the BullMQ API worker. Start it with pnpm --filter @leadmetrics/server-scheduler dev. |
| BullMQ dedupeKey blocks same-day re-runs | The dedupeKey on reminder emails means re-triggering the scheduler on the same day won’t re-send. Clear the Redis key or use a different sessionId to test. See feedback memory: feedback_bullmq_dedupe_rerun.md. |
| Step 3 auto-skipped for non-India but session must be valid | For non-India customers, register/step/2 marks step 3 as autoSkipped: true in stepHistory. The frontend skips /signup/location but still reads signup_step3 from sessionStorage at Step 4. Step 3 data defaults to empty strings — this is fine. |
title (Mr./Mrs.) is collected in Step 1 UI but not yet persisted to DB | It is stored in signup_step1 in sessionStorage and sent to register/start, but the API currently ignores it. The User table does not have a title column. This is a known medium-priority gap. |