Version : 1.0.0
Date : 2026-06-13
Scope : Complete customer onboarding flow from signup to first GBP post + admin notification
Target : 80% completion rate from signup to first GBP post published
Source Spec : docs/business_flow_map.md Section 3
Test Plan : docs/test-plan.md Section 3
1. Mock Setup & External Service Stubs#
1.1 Google OAuth Mock#
// src/__tests__/mocks/google-oauth.ts
import { http, HttpResponse } from "msw";
export const googleOAuthHandlers = [
// OAuth token exchange
http.post("https://oauth2.googleapis.com/token", async () => {
return HttpResponse.json({
access_token: "mock_access_token_12345",
refresh_token: "mock_refresh_token_67890",
expires_in: 3600,
token_type: "Bearer",
scope: "https://www.googleapis.com/auth/business.manage",
});
}),
// Google People API
http.get("https://people.googleapis.com/v1/people/me", () => {
return HttpResponse.json({
resourceName: "people/mock123",
names: [{ displayName: "Dr. Smith" }],
emailAddresses: [{ value: "dr.smith@example.com" }],
});
}),
// GBP Business Information API
http.get("https://mybusinessbusinessinformation.googleapis.com/v1/accounts/-/locations", () => {
return HttpResponse.json({
locations: [
{
name: "locations/123456789",
locationName: "Dr. Smith Dental Clinic",
primaryPhone: "+919876543210",
address: {
regionCode: "IN",
locality: "Kochi",
administrativeArea: "Kerala",
postalCode: "682001",
addressLines: ["123 Main Road"],
},
primaryCategory: { displayName: "Dentist" },
websiteUri: "https://dr-smith-dental.rankflow.in",
},
],
});
}),
// GBP Accounts API
http.get("https://mybusinessaccountmanagement.googleapis.com/v1/accounts", () => {
return HttpResponse.json({
accounts: [{ name: "accounts/123456789", accountName: "Dr. Smith Dental Clinic" }],
});
}),
// GBP Posts API
http.post("https://mybusiness.googleapis.com/v4/:name/localPosts", () => {
return HttpResponse.json({ name: "localPosts/abc123", searchUrl: "https://g.page/dr-smith-dental/post/abc123" });
}),
// OAuth access denied simulation
http.get("/api/webhooks/google/oauth-callback", ({ request }) => {
const url = new URL(request.url);
if (url.searchParams.get("error") === "access_denied") {
return HttpResponse.json({ error: "access_denied" }, { status: 400 });
}
return HttpResponse.json({ code: "mock_auth_code_123" });
}),
];
1.2 Stripe / Razorpay Mock#
// src/__tests__/mocks/payment-provider.ts
import { http, HttpResponse } from "msw";
export const paymentProviderHandlers = [
// Stripe — create customer
http.post("https://api.stripe.com/v1/customers", () => {
return HttpResponse.json({
id: "cus_mock_12345",
email: "dr.smith@example.com",
created: Math.floor(Date.now() / 1000),
});
}),
// Stripe — create subscription
http.post("https://api.stripe.com/v1/subscriptions", () => {
return HttpResponse.json({
id: "sub_mock_67890",
status: "trialing",
trial_end: Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60,
customer: "cus_mock_12345",
});
}),
// Stripe — webhook events
http.post("/api/webhooks/stripe", async ({ request }) => {
const payload = await request.json();
return HttpResponse.json({ received: true, id: payload.id });
}),
// Razorpay — create customer
http.post("https://api.razorpay.com/v1/customers", () => {
return HttpResponse.json({
id: "cust_mock_12345",
email: "dr.smith@example.com",
contact: "+919876543210",
});
}),
// Razorpay — create subscription
http.post("https://api.razorpay.com/v1/subscriptions", () => {
return HttpResponse.json({
id: "sub_mock_67890",
status: "active",
current_start: Math.floor(Date.now() / 1000),
current_end: Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60,
customer_id: "cust_mock_12345",
});
}),
// Razorpay — webhook events
http.post("/api/webhooks/razorpay", async ({ request }) => {
const payload = await request.json();
return HttpResponse.json({ received: true, event: payload.event });
}),
];
1.3 Composio / Zernio Mock#
// src/__tests__/mocks/composio.ts
import { http, HttpResponse } from "msw";
export const composioHandlers = [
// Composio — initiate connection
http.post("https://backend.composio.dev/api/v1/connectedAccounts", () => {
return HttpResponse.json({
id: "conn_mock_instagram_123",
integrationId: "instagram",
status: "ACTIVE",
appName: "instagram",
});
}),
http.post("https://backend.composio.dev/api/v1/connectedAccounts", () => {
return HttpResponse.json({
id: "conn_mock_facebook_456",
integrationId: "facebook",
status: "ACTIVE",
appName: "facebook",
});
}),
// Zernio — schedule post
http.post("https://api.zernio.com/v1/posts", () => {
return HttpResponse.json({
id: "post_mock_789",
status: "scheduled",
platform: "instagram",
scheduledAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
});
}),
];
1.4 Resend Email Mock#
// src/__tests__/mocks/resend.ts
import { http, HttpResponse } from "msw";
export const resendHandlers = [
http.post("https://api.resend.com/emails", () => {
return HttpResponse.json({
id: "email_mock_12345",
from: "onboarding@rankflow.in",
to: "dr.smith@example.com",
created_at: new Date().toISOString(),
});
}),
// Resend webhook delivery tracking
http.post("/api/webhooks/resend", async ({ request }) => {
const payload = await request.json();
return HttpResponse.json({
received: true,
type: payload.type,
email_id: payload.data?.email_id,
});
}),
];
1.5 LLM API Mock (Claude / GPT / Llama)#
// src/__tests__/mocks/llm-router.ts
import { http, HttpResponse } from "msw";
export const llmRouterHandlers = [
// Claude Sonnet — directory profile content
http.post("https://api.anthropic.com/v1/messages", () => {
return HttpResponse.json({
id: "msg_mock_claude_123",
content: [
{
type: "text",
text: JSON.stringify({
hero: {
h1: "Best Dentist in Kochi — Dr. Smith Dental Clinic",
subtitle: "Advanced dental care with 15+ years of experience. Book your appointment today.",
cta: "Book Appointment",
},
about: "Dr. Smith leads a team of dental specialists providing comprehensive oral care in Kochi.",
services: [
{ name: "Teeth Cleaning", description: "Professional dental cleaning for healthy gums" },
{ name: "Root Canal Treatment", description: "Pain-free root canal therapy using latest techniques" },
{ name: "Dental Implants", description: "Permanent tooth replacement with natural-looking results" },
],
faq: [
{ q: "What are your clinic hours?", a: "We are open Monday-Saturday, 9 AM to 7 PM." },
{ q: "Do you accept insurance?", a: "Yes, we accept all major dental insurance plans." },
],
schema: {
"@context": "https://schema.org",
"@type": "Dentist",
name: "Dr. Smith Dental Clinic",
address: {
"@type": "PostalAddress",
addressLocality: "Kochi",
addressRegion: "Kerala",
postalCode: "682001",
addressCountry: "IN",
},
telephone: "+919876543210",
},
}),
},
],
model: "claude-sonnet-4-20250514",
usage: { input_tokens: 1200, output_tokens: 800 },
});
}),
// Claude Haiku — FAQ / meta / bulk
http.post("https://api.anthropic.com/v1/messages", () => {
return HttpResponse.json({
id: "msg_mock_haiku_456",
content: [{ type: "text", text: "Best Dentist in Kochi | Dr. Smith Dental Clinic — Book Now" }],
model: "claude-haiku-3-20240307",
usage: { input_tokens: 400, output_tokens: 60 },
});
}),
// OpenAI GPT — fallback / social / citation descriptions
http.post("https://api.openai.com/v1/chat/completions", () => {
return HttpResponse.json({
id: "chatcmpl_mock_gpt_789",
choices: [
{
message: {
role: "assistant",
content: JSON.stringify({
description: "Dr. Smith Dental Clinic offers comprehensive dental services in Kochi, Kerala. With over 15 years of experience, we specialize in cosmetic dentistry, implants, and root canal treatments. Visit us for personalized, pain-free dental care.",
keywords: ["dentist kochi", "dental clinic kerala", "root canal treatment"],
}),
},
},
],
model: "gpt-4o-mini",
usage: { prompt_tokens: 500, completion_tokens: 150 },
});
}),
// Llama 3 — citation descriptions (bulk)
http.post("https://api.together.xyz/v1/completions", () => {
return HttpResponse.json({
id: "together_mock_llama_123",
choices: [
{
text: JSON.stringify({
directoryId: "justdial",
title: "Dr. Smith Dental Clinic — Best Dentist in Kochi",
description: "Looking for a trusted dentist in Kochi? Dr. Smith Dental Clinic offers advanced dental care including implants, root canals, and cosmetic dentistry. 15+ years experience. Book today!",
keywords: ["dentist kochi", "dental clinic", "root canal"],
}),
},
],
model: "meta-llama/Llama-3-70b",
usage: { prompt_tokens: 300, completion_tokens: 120 },
});
}),
];
1.6 Playwright Test Environment Setup#
// e2e/setup/onboarding.setup.ts
import { test as setup } from "@playwright/test";
import { prisma } from "@/lib/db";
import { createMockPractice } from "@/__tests__/factories/practice";
import { createMockUser } from "@/__tests__/factories/user";
import { createMockLocation } from "@/__tests__/factories/location";
import { seedCitationDirectories } from "@/__tests__/seeders/citation-directories";
setup("seed onboarding test environment", async () => {
// Clean test database
await prisma.$transaction([
prisma.consentLog.deleteMany({}),
prisma.citation.deleteMany({}),
prisma.socialAccount.deleteMany({}),
prisma.gbpLocation.deleteMany({}),
prisma.gbpAccount.deleteMany({}),
prisma.directoryProfileSection.deleteMany({}),
prisma.contentPiece.deleteMany({}),
prisma.scheduledPost.deleteMany({}),
prisma.location.deleteMany({}),
prisma.clientProfile.deleteMany({}),
prisma.user.deleteMany({}),
prisma.citationDirectory.deleteMany({}),
]);
// Seed 30 citation directories
await seedCitationDirectories();
// Verify seed
const dirCount = await prisma.citationDirectory.count();
if (dirCount !== 30) throw new Error(`Expected 30 directories, got ${dirCount}`);
});
2. Step 1: Account Creation & Plan Selection#
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
signupValidation
Email: dr.smith@example.com, Password: Password123
Valid Zod schema
src/__tests__/unit/auth.validation.test.ts
signupDuplicateEmail
Existing email
409 Conflict with message
src/__tests__/unit/auth.validation.test.ts
passwordStrength
Password: weak
Zod error: min 8 chars, 1 uppercase, 1 number
src/__tests__/unit/auth.validation.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
signupCreatesUser
Clean DB
POST /api/auth/signup
User + ClientProfile created with role: CLIENT, status: TRIAL
src/__tests__/integration/auth.router.test.ts
signupCapturesUtm
UTM params in URL
POST /api/auth/signup
ClientProfile.utmSource stored
src/__tests__/integration/auth.router.test.ts
signupRedirects
Valid payload
POST /api/auth/signup
302 redirect to /onboarding?step=1
src/__tests__/integration/auth.router.test.ts
E2E Tests#
Flow
Steps
Expected End State
File
signupFlow
1. Visit /signup 2. Select "Standard" plan 3. Fill email, password, practice name 4. Click "Create Account"
Redirected to /onboarding?step=1 within 3s; User + ClientProfile records exist
e2e/onboarding/step-01-signup.spec.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : PostgreSQL running, Prisma schema migrated, User and ClientProfile tables empty
Required env vars : NEXTAUTH_SECRET, DATABASE_URL, NEXT_PUBLIC_APP_URL=http://localhost:3000
Required external mocks : None (pure internal auth)
Required test data : Factory createMockUser() + createMockClientProfile()
Verification Commands#
# Run tests for Step 1
pnpm test:unit -- src/__tests__/unit/auth.validation.test.ts
pnpm test:integration -- src/__tests__/integration/auth.router.test.ts
pnpm test:e2e -- e2e/onboarding/step-01-signup.spec.ts
Playwright Test Code Outline#
// e2e/onboarding/step-01-signup.spec.ts
import { test, expect } from "@playwright/test";
import { prisma } from "@/lib/db";
test("Step 1: Account creation and plan selection", async ({ page }) => {
await page.goto("/signup?utm_source=google_ads&utm_medium=cpc&utm_campaign=dental_kochi");
// Select plan
await page.click('[data-testid="plan-standard"]');
await page.click('[data-testid="plan-confirm"]');
// Fill signup form
await page.fill('[data-testid="email-input"]', "dr.smith@example.com");
await page.fill('[data-testid="password-input"]', "Password123");
await page.fill('[data-testid="confirm-password-input"]', "Password123");
await page.fill('[data-testid="practice-name-input"]', "Dr. Smith Dental Clinic");
// Submit
await page.click('[data-testid="create-account-button"]');
// Assert redirect
await expect(page).toHaveURL(/\/onboarding\?step=1/, { timeout: 5000 });
// Assert DB state
const user = await prisma.user.findFirst({ where: { email: "dr.smith@example.com" } });
expect(user).not.toBeNull();
expect(user?.role).toBe("CLIENT");
const profile = await prisma.clientProfile.findFirst({ where: { userId: user!.id } });
expect(profile).not.toBeNull();
expect(profile?.status).toBe("TRIAL");
expect(profile?.plan).toBe("STANDARD");
expect(profile?.onboardingComplete).toBe(false);
expect(profile?.utmSource).toBe("google_ads");
});
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
businessProfileValidation
Full valid form payload
Zod schema passes
src/__tests__/unit/practice.validation.test.ts
phoneFormatValidation
Phone: 9876543210
Zod error: must include +91
src/__tests__/unit/practice.validation.test.ts
pinCodeValidation
PIN: 12345
Zod error: must be 6 digits
src/__tests__/unit/practice.validation.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
practiceUpdate
Authenticated user with ClientProfile
tRPC practice.update
Location record created with isPrimary: true; Practice updated
src/__tests__/integration/practice.router.test.ts
autoSaveDraft
Partial form data
tRPC practice.saveDraft
Draft saved to DB with partial fields
src/__tests__/integration/practice.router.test.ts
E2E Tests#
Flow
Steps
Expected End State
File
businessProfileFlow
1. Wizard step 1: Business info 2. Step 2: Location & hours 3. Step 3: Services & media 4. Submit
Location record with full NAP; Practice updated; directorySlug slug generated
e2e/onboarding/step-02-business-profile.spec.ts
Success Criteria (Binary)#
All required fields populated and validated (name, category, address, city, state, PIN, phone, email, hours, services, USPs, doctor name)
Location record created with isPrimary: true and full NAP data
Practice record updated with businessName, category, type
DirectorySlug slug auto-generated from business name (e.g., dr-smith-dental-clinic)
Invalid phone format (+91 + 10 digits) returns inline error
Invalid 6-digit PIN code returns inline error
Image > 2MB returns client-side error before upload
Form abandonment > 10 min triggers auto-save draft email
Agent Context (Pre-conditions)#
Required DB state : User + ClientProfile from Step 1; Location and Practice tables empty
Required env vars : DATABASE_URL, UPLOAD_MAX_SIZE=2097152 (2MB)
Required external mocks : None
Required test data : Factory createMockLocation(), createMockPractice() with full NAP
Verification Commands#
pnpm test:unit -- src/__tests__/unit/practice.validation.test.ts
pnpm test:integration -- src/__tests__/integration/practice.router.test.ts
pnpm test:e2e -- e2e/onboarding/step-02-business-profile.spec.ts
Playwright Test Code Outline#
// e2e/onboarding/step-02-business-profile.spec.ts
import { test, expect } from "@playwright/test";
import { prisma } from "@/lib/db";
import { createMockUserAndProfile } from "@/__tests__/helpers/auth";
test("Step 2: Business profile capture form", async ({ page }) => {
const { user, profile } = await createMockUserAndProfile({ plan: "STANDARD" });
await page.goto("/onboarding?step=1");
// Step 1: Business Info
await page.fill('[data-testid="business-name"]', "Dr. Smith Dental Clinic");
await page.selectOption('[data-testid="business-category"]', "DENTIST");
await page.fill('[data-testid="doctor-name"]', "Dr. John Smith");
await page.fill('[data-testid="years-experience"]', "15");
await page.fill('[data-testid="usps"]', "Advanced dental implants, painless root canals, cosmetic dentistry");
await page.click('[data-testid="next-step-2"]');
// Step 2: Location & Hours
await page.fill('[data-testid="primary-address"]', "123 Main Road, Ernakulam");
await page.selectOption('[data-testid="city"]', "Kochi");
await page.fill('[data-testid="pin-code"]', "682001");
await page.fill('[data-testid="primary-phone"]', "+919876543210");
await page.fill('[data-testid="business-email"]', "contact@drsmithdental.com");
await page.fill('[data-testid="hours-monday-open"]', "09:00");
await page.fill('[data-testid="hours-monday-close"]', "19:00");
await page.click('[data-testid="next-step-3"]');
// Step 3: Services & Media
await page.fill('[data-testid="service-1"]', "Teeth Cleaning");
await page.fill('[data-testid="service-2"]', "Root Canal Treatment");
await page.fill('[data-testid="service-3"]', "Dental Implants");
await page.click('[data-testid="submit-profile"]');
// Assert completion
await expect(page).toHaveURL(/\/onboarding\?step=consent/, { timeout: 5000 });
// Assert DB state
const location = await prisma.location.findFirst({ where: { practiceId: profile.id } });
expect(location).not.toBeNull();
expect(location?.isPrimary).toBe(true);
expect(location?.phone).toBe("+919876543210");
expect(location?.city).toBe("Kochi");
const practice = await prisma.practice.findUnique({ where: { id: profile.id } });
expect(practice?.directorySlug).toMatch(/dr-smith-dental-clinic/);
});
4. Step 3: Consent & Terms Acceptance#
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
consentSchemaValidation
All required consents checked
Zod passes
src/__tests__/unit/consent.validation.test.ts
consentMissingRequired
Terms unchecked
Zod error on termsOfService
src/__tests__/unit/consent.validation.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
consentLogCreation
Authenticated user with Practice
POST /api/consent
6 ConsentLog records created with ipAddress, timestamp, userAgent
src/__tests__/integration/consent.router.test.ts
statusProgression
Profile with status: TRIAL
POST /api/consent
ClientProfile.status updated to ONBOARDING_IN_PROGRESS
src/__tests__/integration/consent.router.test.ts
E2E Tests#
Flow
Steps
Expected End State
File
consentFlow
1. Check all required boxes 2. Leave marketing unchecked 3. Click "I Agree & Continue"
6 ConsentLog records; status: ONBOARDING_IN_PROGRESS; redirect to payment step
e2e/onboarding/step-03-consent.spec.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : User + ClientProfile + Practice + Location from Steps 1–2; ConsentLog table empty
Required env vars : DATABASE_URL, DPDPA_COMPLIANCE_MODE=true
Required external mocks : None
Required test data : Practice with status: TRIAL, onboardingComplete: false
Verification Commands#
pnpm test:unit -- src/__tests__/unit/consent.validation.test.ts
pnpm test:integration -- src/__tests__/integration/consent.router.test.ts
pnpm test:e2e -- e2e/onboarding/step-03-consent.spec.ts
Playwright Test Code Outline#
// e2e/onboarding/step-03-consent.spec.ts
import { test, expect } from "@playwright/test";
import { prisma } from "@/lib/db";
import { createMockUserWithProfileAndLocation } from "@/__tests__/helpers/auth";
test("Step 3: Consent and terms acceptance", async ({ page }) => {
const { profile } = await createMockUserWithProfileAndLocation();
await page.goto("/onboarding?step=consent");
// Check all required consents
await page.check('[data-testid="consent-terms"]');
await page.check('[data-testid="consent-privacy"]');
await page.check('[data-testid="consent-gbp"]');
await page.check('[data-testid="consent-data-processing"]');
await page.check('[data-testid="consent-citations"]');
// Leave marketing unchecked
await page.click('[data-testid="agree-and-continue"]');
// Assert redirect
await expect(page).toHaveURL(/\/onboarding\?step=payment/, { timeout: 3000 });
// Assert consent logs
const consentLogs = await prisma.consentLog.findMany({
where: { practiceId: profile.id },
});
expect(consentLogs).toHaveLength(5);
expect(consentLogs.every((l) => l.ipAddress !== null && l.timestamp !== null)).toBe(true);
// Assert status progression
const updatedProfile = await prisma.clientProfile.findUnique({ where: { id: profile.id } });
expect(updatedProfile?.status).toBe("ONBOARDING_IN_PROGRESS");
});
5. Step 4: Payment Method Setup#
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
paymentMethodSchema
Stripe token tok_visa
Valid object with provider: stripe
src/__tests__/unit/payment.validation.test.ts
trialSubscriptionMath
Now = 2026-06-13
trial_end = 2026-06-20
src/__tests__/unit/payment.validation.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
stripeTrialSubscription
Stripe mock server running
POST /api/billing/subscribe
Subscription record with provider: stripe, status: trialing, trialEnd = +7 days
src/__tests__/integration/billing.router.test.ts
razorpayTrialSubscription
Razorpay mock server running
POST /api/billing/subscribe
Subscription record with provider: razorpay, status: trialing
src/__tests__/integration/billing.router.test.ts
webhookHandler
Stripe invoice.paid event
POST /api/webhooks/stripe
Invoice record created; webhook responded 200
src/__tests__/integration/webhooks.test.ts
E2E Tests#
Flow
Steps
Expected End State
File
paymentFlowStripe
1. Enter card details 2. Complete 3DS mock 3. Confirm
Subscription trialing; confirmation email sent; redirect to /onboarding?step=5
e2e/onboarding/step-04-payment.spec.ts
paymentFlowRazorpay
1. Select UPI 2. Complete mock OTP 3. Confirm
Subscription trialing; paymentMethodId stored in Billing table
e2e/onboarding/step-04-payment.spec.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : User + ClientProfile + Practice + Location + ConsentLog from Steps 1–3; Subscription and Billing tables empty
Required env vars : STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, RAZORPAY_KEY_ID, RAZORPAY_KEY_SECRET, RAZORPAY_WEBHOOK_SECRET, RESEND_API_KEY
Required external mocks : Stripe API mock server, Razorpay API mock server, Resend API mock server
Required test data : createMockStripeCustomer(), createMockRazorpayCustomer()
Verification Commands#
pnpm test:unit -- src/__tests__/unit/payment.validation.test.ts
pnpm test:integration -- src/__tests__/integration/billing.router.test.ts
pnpm test:integration -- src/__tests__/integration/webhooks.test.ts
pnpm test:e2e -- e2e/onboarding/step-04-payment.spec.ts
Playwright Test Code Outline#
// e2e/onboarding/step-04-payment.spec.ts
import { test, expect } from "@playwright/test";
import { prisma } from "@/lib/db";
import { createMockUserWithFullProfile } from "@/__tests__/helpers/auth";
test("Step 4: Payment method setup (Stripe)", async ({ page }) => {
const { profile } = await createMockUserWithFullProfile();
await page.goto("/onboarding?step=payment");
// Fill Stripe test card
const stripeFrame = page.frameLocator('[data-testid="stripe-card-element"] iframe');
await stripeFrame.locator('[placeholder="Card number"]').fill("4242 4242 4242 4242");
await stripeFrame.locator('[placeholder="MM / YY"]').fill("12/30");
await stripeFrame.locator('[placeholder="CVC"]').fill("123");
await page.click('[data-testid="start-trial-button"]');
// Assert redirect
await expect(page).toHaveURL(/\/onboarding\?step=5/, { timeout: 10000 });
// Assert DB state
const billing = await prisma.billing.findFirst({ where: { practiceId: profile.id } });
expect(billing).not.toBeNull();
expect(billing?.subscriptionId).toMatch(/^sub_/);
expect(billing?.provider).toBe("stripe");
const subscription = await prisma.subscription.findFirst({ where: { practiceId: profile.id } });
expect(subscription?.status).toBe("trialing");
const trialEnd = subscription?.trialEnd;
const sevenDaysFromNow = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
expect(trialEnd?.getDate()).toBe(sevenDaysFromNow.getDate());
});
6. Step 5: Directory Profile Creation#
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
profileUrlGeneration
Dr. Smith Dental Clinic, city Kochi
/clinics/kochi/dr-smith-dental-clinic
src/__tests__/unit/directory-profile.test.ts
slugCollision
Existing dr-smith-dental-clinic in Kochi
dr-smith-dental-clinic-2
src/__tests__/unit/directory-profile.test.ts
slugInvalidChars
Dr. Smith & Co. (Kochi)
dr-smith-and-co-kochi
src/__tests__/unit/directory-profile.test.ts
citySlugMapping
City Kochi
kochi
src/__tests__/unit/directory-profile.test.ts
directoryProfileRecord
Practice with NAP
DirectoryProfile created with status: DRAFT
src/__tests__/unit/directory-profile.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
directoryProfileCreation
Practice with no DirectoryProfile
tRPC directoryProfile.create
DirectoryProfile record created with slug, citySlug, status: DRAFT, profileUrl
src/__tests__/integration/routers/directory-profile.test.ts
profileUrlResolution
DirectoryProfile with citySlug: kochi, slug: dr-smith
GET /clinics/kochi/dr-smith
Returns 200 with placeholder page
src/__tests__/integration/routers/directory-profile.test.ts
E2E Tests#
Flow
Steps
Expected End State
File
profileCreationFlow
Payment step completes → system auto-creates profile
DirectoryProfile with status: DRAFT; profileUrl resolvable; preview placeholder visible
e2e/onboarding/step-05-profile-creation.spec.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : Practice with businessName and Location.city but no DirectoryProfile; DirectoryProfile table empty
Required env vars : NEXT_PUBLIC_APP_URL=https://rankflow.in
Required external mocks : None
Required test data : createMockPractice({ directoryProfile: null }), createMockDirectoryProfile()
Verification Commands#
pnpm test:unit -- src/__tests__/unit/directory-profile.test.ts
pnpm test:integration -- src/__tests__/integration/routers/directory-profile.test.ts
pnpm test:e2e -- e2e/onboarding/step-05-profile-creation.spec.ts
Playwright Test Code Outline#
// e2e/onboarding/step-05-profile-creation.spec.ts
import { test, expect } from "@playwright/test";
import { prisma } from "@/lib/db";
import { createMockUserWithFullProfile } from "@/__tests__/helpers/auth";
test("Step 5: Directory profile creation", async ({ page }) => {
const { profile } = await createMockUserWithFullProfile();
// Simulate post-payment state
await prisma.practice.update({
where: { id: profile.id },
data: { businessName: "Dr. Smith Dental Clinic", city: "Kochi" },
});
await page.goto("/onboarding?step=5");
// Trigger directory profile creation
await page.click('[data-testid="confirm-profile-creation"]');
// Assert DB state
const directoryProfile = await prisma.directoryProfile.findFirst({ where: { practiceId: profile.id } });
expect(directoryProfile).not.toBeNull();
expect(directoryProfile?.slug).toMatch(/^dr-smith-dental-clinic/);
expect(directoryProfile?.citySlug).toBe("kochi");
expect(directoryProfile?.status).toBe("DRAFT");
expect(directoryProfile?.profileUrl).toBe("/clinics/kochi/dr-smith-dental-clinic");
// Assert preview link
const previewLink = page.locator('[data-testid="profile-preview-link"]');
await expect(previewLink).toHaveAttribute("href", "https://rankflow.in/clinics/kochi/dr-smith-dental-clinic");
});
7. Step 6: Doctor Approval Gate#
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
complianceCheck
Bio with "guaranteed cure"
checkCompliance() returns false; flagged with FORBIDDEN_WORDS hit
src/__tests__/unit/medical-compliance.test.ts
complianceCheckPass
Bio with "15+ years experience"
checkCompliance() returns true
src/__tests__/unit/medical-compliance.test.ts
approvalNotificationRender
Practice + DirectoryProfile content
Email HTML contains Approve/Edit/Reject buttons + preview link
src/__tests__/unit/email-templates.test.ts
autoApproveTimer
approvalSentAt = now - 47h
DirectoryProfile.status transitions to PUBLISHED at 48h
src/__tests__/unit/approval-gate.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
contentCompliance
AI-generated bio, services, FAQs
Call checkCompliance() on each
All content passes; violations rejected and regenerated
src/__tests__/integration/approval-gate.test.ts
doctorApprovalEmail
Resend mock; DirectoryProfile with status: PENDING_REVIEW
Inngest send-approval-email step
Resend API called with preview link; EmailLog created with type: PROFILE_APPROVAL_REQUEST
src/__tests__/integration/approval-gate.test.ts
approvalGateApprove
Doctor clicks "Approve" in email
POST /api/directory-profile/approve
DirectoryProfile.status → PUBLISHED; ISR revalidation triggered; admin notified
src/__tests__/integration/approval-gate.test.ts
approvalGateEdit
Doctor clicks "Edit & Approve"
Opens inline editor; saves changes; clicks approve
DirectoryProfile content updated; status → PUBLISHED; audit log records edit
src/__tests__/integration/approval-gate.test.ts
approvalGateReject
Doctor clicks "Reject"
DirectoryProfile.status → REJECTED; content regeneration queued; admin notified
src/__tests__/integration/approval-gate.test.ts
approvalGateAutoApprove
No doctor response for 48h
Inngest cron or timer
DirectoryProfile.status → PUBLISHED if compliance score > 90%
src/__tests__/integration/approval-gate.test.ts
E2E Tests#
Flow
Steps
Expected End State
File
approvalGateFlow
AI content generated → doctor receives approval email → doctor approves
DirectoryProfile.status: PUBLISHED; profile live; admin notified
e2e/onboarding/step-06-approval-gate.spec.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : Practice + Location with full NAP; DirectoryProfile with status: PENDING_REVIEW and generated content; EmailLog table empty
Required env vars : ANTHROPIC_API_KEY, RESEND_API_KEY, RESEND_FROM_EMAIL=onboarding@rankflow.in, AUTO_APPROVE_HOURS=48
Required external mocks : Claude API mock (for regeneration), Resend API mock, Google OAuth mock (optional)
Required test data : createMockDirectoryProfile({ status: "PENDING_REVIEW" }) with generated content
Verification Commands#
pnpm test:unit -- src/__tests__/unit/medical-compliance.test.ts
pnpm test:unit -- src/__tests__/unit/email-templates.test.ts
pnpm test:unit -- src/__tests__/unit/approval-gate.test.ts
pnpm test:integration -- src/__tests__/integration/approval-gate.test.ts
pnpm test:e2e -- e2e/onboarding/step-06-approval-gate.spec.ts
Playwright Test Code Outline#
// e2e/onboarding/step-06-approval-gate.spec.ts
import { test, expect } from "@playwright/test";
import { prisma } from "@/lib/db";
import { triggerInngestEvent } from "@/__tests__/helpers/inngest";
import { createMockUserWithFullProfile } from "@/__tests__/helpers/auth";
test("Step 6: Doctor approval gate", async () => {
const { profile } = await createMockUserWithFullProfile();
await prisma.directoryProfile.create({
data: {
practiceId: profile.id,
slug: "dr-smith-dental-clinic",
citySlug: "kochi",
status: "PENDING_REVIEW",
bio: "Dr. Smith leads a team of dental specialists in Kochi with 15+ years of experience.",
services: JSON.stringify(["Teeth Cleaning", "Root Canal", "Dental Implants"]),
faqs: JSON.stringify([{ q: "Hours?", a: "Mon-Sat 9-7" }]),
},
});
// Trigger approval email
await triggerInngestEvent("onboarding-pipeline.send-approval-email", { practiceId: profile.id });
// Assert EmailLog
const emailLog = await prisma.emailLog.findFirst({
where: { practiceId: profile.id, type: "PROFILE_APPROVAL_REQUEST" },
});
expect(emailLog).not.toBeNull();
expect(emailLog?.status).toBe("DELIVERED");
expect(emailLog?.html).toContain("Approve Profile");
expect(emailLog?.html).toContain("rankflow.in/clinics/kochi/dr-smith-dental-clinic");
// Assert compliance
const directoryProfile = await prisma.directoryProfile.findFirst({ where: { practiceId: profile.id } });
expect(directoryProfile?.status).toBe("PENDING_REVIEW");
});
8. Step 7: Profile Publish#
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
isrRevalidation
revalidatePath("/clinics/kochi/dr-smith")
Next.js ISR cache purged; revalidation tag set
src/__tests__/unit/profile-publish.test.ts
schemaInjectorRender
Valid JSON-LD schema
<script type="application/ld+json"> in HTML
src/__tests__/unit/profile-publish.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
profilePublish
DirectoryProfile with status: PENDING_REVIEW or APPROVED
tRPC directoryProfile.publish
DirectoryProfile.status → PUBLISHED; ISR revalidation triggered; HTTP 200 at profile URL
src/__tests__/integration/profile-publish.test.ts
isrRevalidation
Published profile
Call directoryProfile.revalidate
Next.js ISR cache purged for /clinics/{citySlug}/{slug}; stale-while-revalidate enabled
src/__tests__/integration/profile-publish.test.ts
profileUnpublish
DirectoryProfile with status: PUBLISHED
tRPC directoryProfile.unpublish
DirectoryProfile.status → PAUSED; profile returns 404 or "Coming Soon" page
src/__tests__/integration/profile-publish.test.ts
E2E Tests#
Flow
Steps
Expected End State
File
publishFlow
Doctor approves → admin publishes → ISR revalidates
Directory profile live at /clinics/kochi/dr-smith; HTTP 200; LCP < 2.5s; schema present
e2e/onboarding/step-07-profile-publish.spec.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : DirectoryProfile with status: PENDING_REVIEW or APPROVED and generated content; DirectoryProfile table ready
Required env vars : NEXT_PUBLIC_APP_URL, CLOUDFLARE_API_TOKEN, ISR_REVALIDATE_SECRET
Required external mocks : Cloudflare CDN API mock (optional), S3 mock
Required test data : createMockDirectoryProfile({ status: "APPROVED" }) with bio, services, FAQs
Verification Commands#
pnpm test:unit -- src/__tests__/unit/profile-publish.test.ts
pnpm test:integration -- src/__tests__/integration/profile-publish.test.ts
pnpm test:e2e -- e2e/onboarding/step-07-profile-publish.spec.ts
Playwright Test Code Outline#
// e2e/onboarding/step-07-profile-publish.spec.ts
import { test, expect } from "@playwright/test";
import { prisma } from "@/lib/db";
import { triggerInngestEvent } from "@/__tests__/helpers/inngest";
import { createMockUserWithFullProfile } from "@/__tests__/helpers/auth";
test("Step 7: Profile publish", async ({ page }) => {
const { profile } = await createMockUserWithFullProfile();
await prisma.directoryProfile.create({
data: {
practiceId: profile.id,
slug: "dr-smith-dental-clinic",
citySlug: "kochi",
status: "APPROVED",
bio: "Dr. Smith leads a team of dental specialists in Kochi with 15+ years of experience.",
services: JSON.stringify(["Teeth Cleaning", "Root Canal", "Dental Implants"]),
faqs: JSON.stringify([{ q: "Hours?", a: "Mon-Sat 9-7" }]),
},
});
// Trigger publish
await triggerInngestEvent("onboarding-pipeline.publish-profile", { practiceId: profile.id });
// Assert live page
await page.goto("https://rankflow.in/clinics/kochi/dr-smith-dental-clinic");
await expect(page).toHaveURL("https://rankflow.in/clinics/kochi/dr-smith-dental-clinic");
expect(page.status()).resolves.toBe(200);
// Assert schema present
const schemaScript = page.locator('script[type="application/ld+json"]');
await expect(schemaScript).toHaveCount(1);
const schemaText = await schemaScript.textContent();
expect(() => JSON.parse(schemaText!)).not.toThrow();
// Assert related clinics section
const relatedClinics = page.locator('[data-testid="related-clinics"]');
await expect(relatedClinics).toBeVisible();
// Assert DB state
const directoryProfile = await prisma.directoryProfile.findFirst({ where: { practiceId: profile.id } });
expect(directoryProfile?.status).toBe("PUBLISHED");
expect(directoryProfile?.publishedAt).toBeInstanceOf(Date);
});
9. Step 8: GBP OAuth Connection Request Email#
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
oauthUrlGeneration
practiceId: "prf_abc123"
Base64-encoded state param; correct scopes
src/__tests__/unit/gbp-oauth.test.ts
emailTemplateRender
Practice + OAuth URL
HTML email with button, screenshots, support link
src/__tests__/unit/email-templates.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
gbpOAuthEmail
Resend mock; Practice with directory profile live
Inngest notify-gbp-auth step
Resend API called with correct to, subject, html
src/__tests__/integration/gbp-oauth.test.ts
emailDeliveryTracking
Resend mock returns emailId
Webhook callback
EmailLog record created with status: DELIVERED
src/__tests__/integration/email-tracking.test.ts
E2E Tests#
Flow
Steps
Expected End State
File
gbpOAuthEmailFlow
Inngest step triggers after directory profile deploy
Resend API receives email with OAuth link; EmailLog record created
e2e/onboarding/step-08-gbp-oauth-email.spec.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : Practice with status: PUBLISHED, directorySlug assigned; GbpAccount table empty; EmailLog table empty
Required env vars : GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_OAUTH_REDIRECT_URI, RESEND_API_KEY, RESEND_FROM_EMAIL=onboarding@rankflow.in
Required external mocks : Resend API mock; Google OAuth mock (optional for email step)
Required test data : createMockPractice() with directorySlug and status: PUBLISHED
Verification Commands#
pnpm test:unit -- src/__tests__/unit/gbp-oauth.test.ts
pnpm test:unit -- src/__tests__/unit/email-templates.test.ts
pnpm test:integration -- src/__tests__/integration/gbp-oauth.test.ts
pnpm test:integration -- src/__tests__/integration/email-tracking.test.ts
pnpm test:e2e -- e2e/onboarding/step-08-gbp-oauth-email.spec.ts
Playwright Test Code Outline#
// e2e/onboarding/step-08-gbp-oauth-email.spec.ts
import { test, expect } from "@playwright/test";
import { prisma } from "@/lib/db";
import { triggerInngestEvent } from "@/__tests__/helpers/inngest";
import { createMockUserWithFullProfile } from "@/__tests__/helpers/auth";
test("Step 8: GBP OAuth connection request email", async () => {
const { profile } = await createMockUserWithFullProfile();
await prisma.practice.update({
where: { id: profile.id },
data: { directorySlug: "dr-smith-dental-clinic", status: PUBLISHED },
});
await triggerInngestEvent("onboarding-pipeline.notify-gbp-auth", { practiceId: profile.id });
// Assert EmailLog
const emailLog = await prisma.emailLog.findFirst({
where: { practiceId: profile.id, type: "GBP_OAUTH_REQUEST" },
});
expect(emailLog).not.toBeNull();
expect(emailLog?.to).toBe("dr.smith@example.com");
expect(emailLog?.status).toBe("DELIVERED");
// Assert email content contains OAuth link
expect(emailLog?.html).toContain("Connect Google Business Profile");
expect(emailLog?.html).toMatch(/accounts\.google\.com\/o\/oauth2\/v2\/auth/);
expect(emailLog?.html).toContain("business.manage");
});
10. Step 9: GBP OAuth Flow & Token Storage#
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
tokenEncryption
access_token: "secret123"
AES-256-GCM encrypted string with IV + auth tag
src/__tests__/unit/crypto.test.ts
stateParamDecode
Base64 practiceId payload
Decoded practiceId matches original
src/__tests__/unit/gbp-oauth.test.ts
tokenExpiryCalculation
expires_in: 3600
tokenExpiresAt = now + 3600s
src/__tests__/unit/gbp-oauth.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
oauthCallbackHandler
Google OAuth mock; valid code + state
GET /api/webhooks/google/oauth-callback
GbpAccount created with encrypted tokens; redirect to /dashboard/gbp?connected=true
src/__tests__/integration/gbp-oauth.test.ts
accessDeniedHandler
error=access_denied in callback
GET /api/webhooks/google/oauth-callback
Email sent with "You declined access" + fresh OAuth link
src/__tests__/integration/gbp-oauth.test.ts
E2E Tests#
Flow
Steps
Expected End State
File
gbpOAuthFlow
1. Click OAuth link from email 2. Sign in to Google 3. Click "Allow" 4. Redirect to dashboard
GbpAccount with isActive: true; tokens encrypted; redirect with ?connected=true
e2e/onboarding/step-09-gbp-oauth-flow.spec.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : Practice + ClientProfile; GbpAccount table empty; OAuth email sent
Required env vars : GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_OAUTH_REDIRECT_URI, ENCRYPTION_KEY (32-byte base64)
Required external mocks : Google OAuth token endpoint mock, Google People API mock
Required test data : createMockGbpAccount() factory
Verification Commands#
pnpm test:unit -- src/__tests__/unit/crypto.test.ts
pnpm test:unit -- src/__tests__/unit/gbp-oauth.test.ts
pnpm test:integration -- src/__tests__/integration/gbp-oauth.test.ts
pnpm test:e2e -- e2e/onboarding/step-09-gbp-oauth-flow.spec.ts
Playwright Test Code Outline#
// e2e/onboarding/step-09-gbp-oauth-flow.spec.ts
import { test, expect } from "@playwright/test";
import { prisma } from "@/lib/db";
import { createMockUserWithFullProfile } from "@/__tests__/helpers/auth";
test("Step 9: GBP OAuth flow and token storage", async ({ page }) => {
const { profile } = await createMockUserWithFullProfile();
await prisma.practice.update({
where: { id: profile.id },
data: { directorySlug: "dr-smith-dental-clinic", status: PUBLISHED },
});
// Simulate OAuth callback
const state = Buffer.from(JSON.stringify({ practiceId: profile.id })).toString("base64");
await page.goto(`/api/webhooks/google/oauth-callback?code=mock_auth_code&state=${state}`);
// Assert redirect to dashboard
await expect(page).toHaveURL("/dashboard/gbp?connected=true", { timeout: 5000 });
// Assert GbpAccount created
const gbpAccount = await prisma.gbpAccount.findFirst({ where: { practiceId: profile.id } });
expect(gbpAccount).not.toBeNull();
expect(gbpAccount?.isActive).toBe(true);
expect(gbpAccount?.scope).toContain("business.manage");
expect(gbpAccount?.accessToken).not.toBe("mock_access_token_12345"); // encrypted
expect(gbpAccount?.refreshToken).not.toBe("mock_refresh_token_67890"); // encrypted
expect(gbpAccount?.tokenExpiresAt).toBeInstanceOf(Date);
});
11. Step 10: GBP Location Sync#
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
locationMapping
GBP API location + local Location
GbpLocation linked to Location via practiceId
src/__tests__/unit/gbp-sync.test.ts
categoryMapping
category: DENTIST
GBP primaryCategory.displayName: "Dentist"
src/__tests__/unit/gbp-sync.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
gbpLocationSync
GbpAccount with valid tokens; GBP API mock with 1 location
Inngest gbp/sync-locations
GbpLocation record created with gbpLocationId, mapped to Location
src/__tests__/integration/gbp-sync.test.ts
businessInfoSync
Practice with updated hours
Inngest gbp/sync-locations
GBP API patch called with updated description, hours, phone, website
src/__tests__/integration/gbp-sync.test.ts
E2E Tests#
Flow
Steps
Expected End State
File
gbpSyncFlow
OAuth complete → Inngest triggers sync
GbpLocation records synced; dashboard shows "Connected — X locations"
e2e/onboarding/step-10-gbp-sync.spec.ts
Success Criteria (Binary)#
All GBP locations synced to local GbpLocation records with gbpLocationId, name, address, phone, hours, category
GbpLocation records linked to Location records via practiceId
Business info updated on GBP API: description, hours, category, phone, directory profile URL
Logo and photos uploaded to GBP if provided during onboarding
Customer sees GBP status as "Connected — X locations" in /dashboard/gbp
GBP API rate limit (HTTP 429) triggers exponential backoff retry (5min, 15min, 1h)
Location data mismatch (address/phone differs) flags for admin review and emails customer
Photo upload failure skips photos and queues for manual upload
Category not supported falls back to closest supported category and flags for manual correction
Agent Context (Pre-conditions)#
Required DB state : GbpAccount with encrypted tokens; Location with full NAP; GbpLocation table empty
Required env vars : GOOGLE_API_KEY, GBP_API_RATE_LIMIT=100/min
Required external mocks : GBP Business Information API mock (accounts.locations.list, locations.patch), GBP Media API mock
Required test data : createMockGbpLocation(), createMockLocation()
Verification Commands#
pnpm test:unit -- src/__tests__/unit/gbp-sync.test.ts
pnpm test:integration -- src/__tests__/integration/gbp-sync.test.ts
pnpm test:e2e -- e2e/onboarding/step-10-gbp-sync.spec.ts
Playwright Test Code Outline#
// e2e/onboarding/step-10-gbp-sync.spec.ts
import { test, expect } from "@playwright/test";
import { prisma } from "@/lib/db";
import { triggerInngestEvent } from "@/__tests__/helpers/inngest";
import { createMockUserWithFullProfile } from "@/__tests__/helpers/auth";
test("Step 10: GBP location sync", async () => {
const { profile } = await createMockUserWithFullProfile();
await prisma.gbpAccount.create({
data: {
practiceId: profile.id,
accountEmail: "dr.smith@example.com",
accessToken: "encrypted_access_token",
refreshToken: "encrypted_refresh_token",
tokenExpiresAt: new Date(Date.now() + 3600 * 1000),
scope: ["business.manage"],
isActive: true,
},
});
await triggerInngestEvent("gbp/sync-locations", { practiceId: profile.id });
// Assert GbpLocation records
const gbpLocations = await prisma.gbpLocation.findMany({ where: { practiceId: profile.id } });
expect(gbpLocations.length).toBeGreaterThanOrEqual(1);
expect(gbpLocations[0]?.gbpLocationId).toMatch(/^locations\//);
// Assert linked to Location
const location = await prisma.location.findFirst({ where: { practiceId: profile.id } });
expect(gbpLocations[0]?.practiceId).toBe(location?.practiceId);
});
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
composioUrlGeneration
platform: instagram, practiceId: prf_abc
OAuth URL with practiceId in state param
src/__tests__/unit/social-oauth.test.ts
platformSelectionLogic
category: DENTIST
Instagram + Facebook prioritized; LinkedIn + Twitter optional
src/__tests__/unit/social-oauth.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
socialConnectEmail
Resend mock; Practice with GBP synced
Inngest social connection step
Resend API called with platform buttons; EmailLog created
src/__tests__/integration/social-connect.test.ts
composioOAuthCallback
Composio mock returns connectionId
GET /api/webhooks/composio/oauth-callback
SocialAccount created with composioConnectionId, isActive: true
src/__tests__/integration/social-connect.test.ts
E2E Tests#
Flow
Steps
Expected End State
File
socialConnectFlow
1. Receive email 2. Click Instagram button 3. Authorize via Composio 4. Redirect to dashboard
SocialAccount with platform: INSTAGRAM, isActive: true; dashboard shows connected
e2e/onboarding/step-11-social-connect.spec.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : Practice with GBP synced; SocialAccount table empty; EmailLog table empty
Required env vars : COMPOSIO_API_KEY, COMPOSIO_CLIENT_ID, ZERNIO_API_KEY
Required external mocks : Composio OAuth mock, Zernio API mock, Resend API mock
Required test data : createMockSocialAccount() factory
Verification Commands#
pnpm test:unit -- src/__tests__/unit/social-oauth.test.ts
pnpm test:integration -- src/__tests__/integration/social-connect.test.ts
pnpm test:e2e -- e2e/onboarding/step-11-social-connect.spec.ts
Playwright Test Code Outline#
// e2e/onboarding/step-11-social-connect.spec.ts
import { test, expect } from "@playwright/test";
import { prisma } from "@/lib/db";
import { createMockUserWithFullProfile } from "@/__tests__/helpers/auth";
test("Step 11: Social media connection prompts", async ({ page }) => {
const { profile } = await createMockUserWithFullProfile();
await prisma.gbpAccount.create({
data: {
practiceId: profile.id,
accountEmail: "dr.smith@example.com",
accessToken: "encrypted",
refreshToken: "encrypted",
tokenExpiresAt: new Date(Date.now() + 3600 * 1000),
scope: ["business.manage"],
isActive: true,
},
});
await page.goto("/dashboard/social");
// Click Instagram connect
await page.click('[data-testid="connect-instagram"]');
// Simulate Composio OAuth callback
const state = Buffer.from(JSON.stringify({ practiceId: profile.id, platform: "INSTAGRAM" })).toString("base64");
await page.goto(`/api/webhooks/composio/oauth-callback?connectionId=conn_mock_instagram&state=${state}`);
// Assert redirect
await expect(page).toHaveURL("/dashboard/social?connected=instagram", { timeout: 5000 });
// Assert SocialAccount
const socialAccount = await prisma.socialAccount.findFirst({
where: { practiceId: profile.id, platform: "INSTAGRAM" },
});
expect(socialAccount).not.toBeNull();
expect(socialAccount?.isActive).toBe(true);
expect(socialAccount?.composioConnectionId).toBe("conn_mock_instagram");
});
13. Step 12: Citation Directory Description Generation#
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
descriptionUniqueness
30 generated descriptions
> 80% uniqueness score; no duplicates > 70% similarity
src/__tests__/unit/citation-description.test.ts
lengthValidation
Description: 50 chars
Rejected (min 100 chars); regenerated
src/__tests__/unit/citation-description.test.ts
medicalComplianceFilter
Description with "guaranteed cure"
Rejected; flagged; regenerated
src/__tests__/unit/citation-description.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
descriptionGeneration
30 CitationDirectory seeded; Practice with NAP
Inngest citation-builder step 3
30 description objects in memory, each with directoryId, title, description, keywords
src/__tests__/integration/citation-builder.test.ts
llmRouterBulk
task: "citation_description", 30 directories
Call ai.generate() batch
Llama 3 70B selected; cost < Rs 5; all descriptions returned in < 45s
src/__tests__/integration/ai-router.test.ts
E2E Tests#
Flow
Steps
Expected End State
File
citationDescriptionFlow
Inngest triggers after directory profile deploy
30 unique descriptions in Citation records or workflow memory
e2e/onboarding/step-12-citation-descriptions.spec.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : 30 CitationDirectory records seeded; Practice + Location with full NAP; Citation table empty
Required env vars : TOGETHER_API_KEY, ANTHROPIC_API_KEY, LLM_ROUTER_TIMEOUT=15000
Required external mocks : Together.xyz Llama API mock, Claude Haiku fallback mock
Required test data : MOCK_CITATION_DIRECTORIES (30 directories), createMockNAP()
Verification Commands#
pnpm test:unit -- src/__tests__/unit/citation-description.test.ts
pnpm test:integration -- src/__tests__/integration/citation-builder.test.ts
pnpm test:integration -- src/__tests__/integration/ai-router.test.ts
pnpm test:e2e -- e2e/onboarding/step-12-citation-descriptions.spec.ts
Playwright Test Code Outline#
// e2e/onboarding/step-12-citation-descriptions.spec.ts
import { test, expect } from "@playwright/test";
import { prisma } from "@/lib/db";
import { triggerInngestEvent } from "@/__tests__/helpers/inngest";
import { createMockUserWithFullProfile } from "@/__tests__/helpers/auth";
import { seedCitationDirectories } from "@/__tests__/seeders/citation-directories";
test("Step 12: Citation description generation", async () => {
const { profile } = await createMockUserWithFullProfile();
await seedCitationDirectories();
await triggerInngestEvent("citation-builder.generate-descriptions", { practiceId: profile.id });
// Assert descriptions generated in Citation records or workflow state
const citations = await prisma.citation.findMany({ where: { practiceId: profile.id } });
expect(citations.length).toBe(30);
// Uniqueness check
const descriptions = citations.map((c) => c.description);
const uniqueDescriptions = new Set(descriptions);
expect(uniqueDescriptions.size / descriptions.length).toBeGreaterThanOrEqual(0.8);
// Compliance check
const hasBanned = descriptions.some((d) => /guaranteed cure|100% success|miracle/i.test(d));
expect(hasBanned).toBe(false);
// Length check
const allValidLength = descriptions.every((d) => d.length >= 100 && d.length <= 500);
expect(allValidLength).toBe(true);
});
14. Step 13: Citation Submission to 30 Directories#
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
apiSubmitter
directory: practo, valid NAP
HTTP POST to Practo API; success pattern matched
src/__tests__/unit/citation-submitter.test.ts
formSubmitter
directory: justdial, valid NAP
Puppeteer form fill; screenshot captured; confirmation detected
src/__tests__/unit/citation-submitter.test.ts
ownedBlogSubmitter
directory: kerala-health
Direct DB insert; ISR re-render triggered
src/__tests__/unit/citation-submitter.test.ts
idempotencyCheck
Duplicate submission attempt
Skipped; existing Citation record returned
src/__tests__/unit/citation-submitter.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
batchSubmission
30 descriptions + NAP + 30 directories
Inngest citation-builder step 4
≥ 24 Citation records with status: SUBMITTED; all have directoryUrl or screenshotUrl
src/__tests__/integration/citation-builder.test.ts
concurrencyLimit
30 directories queued
Submission engine runs
Max 3 concurrent submissions; no rate limit breaches
src/__tests__/integration/citation-builder.test.ts
E2E Tests#
Flow
Steps
Expected End State
File
citationSubmissionFlow
Inngest triggers submission after description generation
≥ 24 Citation SUBMITTED; screenshots stored; NAP consistent
e2e/onboarding/step-13-citation-submit.spec.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : 30 CitationDirectory records; 30 descriptions in Citation or workflow memory; Practice + Location with NAP
Required env vars : HYPERBROWSER_API_KEY, S3_CITATIONS_BUCKET, CAPTCHA_SOLVER_KEY
Required external mocks : Practo API mock, Justdial form mock (Hyperbrowser), S3 mock
Required test data : MOCK_CITATION_DIRECTORIES, createMockCitation()
Verification Commands#
pnpm test:unit -- src/__tests__/unit/citation-submitter.test.ts
pnpm test:integration -- src/__tests__/integration/citation-builder.test.ts
pnpm test:e2e -- e2e/onboarding/step-13-citation-submit.spec.ts
Playwright Test Code Outline#
// e2e/onboarding/step-13-citation-submit.spec.ts
import { test, expect } from "@playwright/test";
import { prisma } from "@/lib/db";
import { triggerInngestEvent } from "@/__tests__/helpers/inngest";
import { createMockUserWithFullProfile } from "@/__tests__/helpers/auth";
import { seedCitationDirectories } from "@/__tests__/seeders/citation-directories";
test("Step 13: Citation submission to 30 directories", async () => {
const { profile } = await createMockUserWithFullProfile();
await seedCitationDirectories();
// Pre-seed citations with descriptions
const directories = await prisma.citationDirectory.findMany();
await prisma.citation.createMany({
data: directories.map((dir, i) => ({
practiceId: profile.id,
locationId: profile.id, // simplified
directoryName: dir.name,
status: "PENDING",
description: `Unique description for ${dir.name} — Dr. Smith Dental Clinic in Kochi. ${i}`,
submittedAt: null,
})),
});
await triggerInngestEvent("citation-builder.submit", { practiceId: profile.id });
// Assert submission results
const citations = await prisma.citation.findMany({ where: { practiceId: profile.id } });
const submitted = citations.filter((c) => c.status === "SUBMITTED");
expect(submitted.length).toBeGreaterThanOrEqual(24); // 80% success rate
// Assert proof
const withProof = submitted.filter((c) => c.directoryUrl || c.screenshotUrl);
expect(withProof.length).toBe(submitted.length);
// Assert NAP consistency
const napSnapshot = submitted[0]?.napSnapshot;
expect(napSnapshot).toBeDefined();
expect(JSON.parse(napSnapshot as string).name).toBe("Dr. Smith Dental Clinic");
});
15. Step 14: NAP Verification Scheduling#
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
sleepDuration
step.sleep("7d")
sleepUntil = now + 7 days
src/__tests__/unit/inngest-schedule.test.ts
cronExpression
0 2 1 * *
Next run = 1st of next month at 2 AM IST
src/__tests__/unit/inngest-schedule.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
napVerifySchedule
Citations with status: SUBMITTED
Inngest citation-builder step 6
NapCheckProcessor BullMQ job scheduled with 7-day delay; recurring cron set
src/__tests__/integration/citation-builder.test.ts
E2E Tests#
Flow
Steps
Expected End State
File
napScheduleFlow
All submissions complete → Inngest sleeps
Citation records have status: SUBMITTED and submittedAt populated; verification event scheduled
e2e/onboarding/step-14-nap-verify-schedule.spec.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : ≥ 24 Citation records with status: SUBMITTED; NapCheck queue configured
Required env vars : INNGEST_EVENT_KEY, BULLMQ_REDIS_URL, NAP_VERIFY_DELAY_DAYS=7
Required external mocks : None (internal scheduling)
Required test data : createMockCitation({ status: "SUBMITTED", submittedAt: new Date() })
Verification Commands#
pnpm test:unit -- src/__tests__/unit/inngest-schedule.test.ts
pnpm test:integration -- src/__tests__/integration/citation-builder.test.ts
pnpm test:e2e -- e2e/onboarding/step-14-nap-verify-schedule.spec.ts
Playwright Test Code Outline#
// e2e/onboarding/step-14-nap-verify-schedule.spec.ts
import { test, expect } from "@playwright/test";
import { prisma } from "@/lib/db";
import { triggerInngestEvent } from "@/__tests__/helpers/inngest";
import { createMockUserWithFullProfile } from "@/__tests__/helpers/auth";
test("Step 14: NAP verification scheduling", async () => {
const { profile } = await createMockUserWithFullProfile();
// Seed submitted citations
await prisma.citation.createMany({
data: Array.from({ length: 30 }, (_, i) => ({
practiceId: profile.id,
locationId: profile.id,
directoryName: `directory-${i}`,
status: "SUBMITTED",
submittedAt: new Date(),
description: `Description ${i}`,
})),
});
await triggerInngestEvent("citation-builder.schedule-verify", { practiceId: profile.id });
// Assert all citations have submittedAt
const citations = await prisma.citation.findMany({ where: { practiceId: profile.id } });
expect(citations.every((c) => c.status === "SUBMITTED" && c.submittedAt !== null)).toBe(true);
// Assert Inngest scheduled event (via Inngest test helper)
const scheduledEvents = await getInngestScheduledEvents("skill/14-citation-verify-nap");
expect(scheduledEvents.length).toBeGreaterThanOrEqual(1);
expect(scheduledEvents[0]?.payload.practiceId).toBe(profile.id);
});
16. Step 15: First GBP Post Generation & Approval Queue#
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
gbpPostLength
Content: 1600 chars
Truncated to 1500 chars; flag for review
src/__tests__/unit/gbp-post.test.ts
medicalComplianceGbp
Post with "guaranteed results"
Rejected; regenerated; alert if 2x failure
src/__tests__/unit/gbp-post.test.ts
autoPublishTimer
autoPublishAt = now + 24h
ContentPiece.status transitions to PUBLISHED at expiry
src/__tests__/unit/gbp-post.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
firstGbpPost
GbpAccount connected; Practice with NAP
Inngest create-first-post step
ContentPiece created with type: GBP_POST, status: PENDING_REVIEW, 24h timer
src/__tests__/integration/gbp-post.test.ts
approvalQueue
ContentPiece in PENDING_REVIEW
Customer approves via dashboard
ContentPiece.status → APPROVED; GbpPost queued for publish
src/__tests__/integration/gbp-post.test.ts
E2E Tests#
Flow
Steps
Expected End State
File
firstGbpPostFlow
GBP OAuth complete → Inngest generates post
Post in /dashboard/content with 24h countdown; email sent; passes compliance
e2e/onboarding/step-15-first-gbp-post.spec.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : GbpAccount with isActive: true; Practice with NAP; ContentPiece table empty
Required env vars : ANTHROPIC_API_KEY, GBP_POST_MAX_LENGTH=1500, AUTO_PUBLISH_HOURS=24
Required external mocks : Claude Haiku mock for GBP post generation
Required test data : createMockContentPiece(), createMockGbpPost()
Verification Commands#
pnpm test:unit -- src/__tests__/unit/gbp-post.test.ts
pnpm test:integration -- src/__tests__/integration/gbp-post.test.ts
pnpm test:e2e -- e2e/onboarding/step-15-first-gbp-post.spec.ts
Playwright Test Code Outline#
// e2e/onboarding/step-15-first-gbp-post.spec.ts
import { test, expect } from "@playwright/test";
import { prisma } from "@/lib/db";
import { triggerInngestEvent } from "@/__tests__/helpers/inngest";
import { createMockUserWithFullProfile } from "@/__tests__/helpers/auth";
test("Step 15: First GBP post generation and approval queue", async () => {
const { profile } = await createMockUserWithFullProfile();
await prisma.gbpAccount.create({
data: {
practiceId: profile.id,
accountEmail: "dr.smith@example.com",
accessToken: "encrypted",
refreshToken: "encrypted",
tokenExpiresAt: new Date(Date.now() + 3600 * 1000),
scope: ["business.manage"],
isActive: true,
},
});
await triggerInngestEvent("onboarding-pipeline.create-first-post", { practiceId: profile.id });
// Assert ContentPiece created
const contentPiece = await prisma.contentPiece.findFirst({
where: { practiceId: profile.id, type: "GBP_POST" },
});
expect(contentPiece).not.toBeNull();
expect(contentPiece?.status).toBe("PENDING_REVIEW");
expect(contentPiece?.content.length).toBeLessThanOrEqual(1500);
expect(contentPiece?.autoPublishAt).toBeInstanceOf(Date);
// Assert compliance
expect(/guaranteed|miracle|100% success/i.test(contentPiece?.content || "")).toBe(false);
// Assert email sent
const emailLog = await prisma.emailLog.findFirst({
where: { practiceId: profile.id, type: "CONTENT_APPROVAL_REMINDER" },
});
expect(emailLog).not.toBeNull();
});
17. Step 16: First Social Posts Generation & Approval Queue#
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
instagramCaptionLength
Caption: 200 words
Hashtag set (10–15 tags); character count < 2200
src/__tests__/unit/social-post.test.ts
facebookPostFormat
Longer post (150–200 words)
Contains directory profile link; CTA button
src/__tests__/unit/social-post.test.ts
platformCharacterLimit
Twitter post: 300 chars
Truncated to 280 chars
src/__tests__/unit/social-post.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
firstSocialPosts
SocialAccount for Instagram + Facebook connected
AI Router generates posts
2 ContentPiece records: type: SOCIAL_POST, status: PENDING_REVIEW
src/__tests__/integration/social-post.test.ts
crossPlatformAdaptation
Same core topic
Generate for 2 platforms
Instagram has hashtags; Facebook has link; content differs per platform
src/__tests__/integration/social-post.test.ts
E2E Tests#
Flow
Steps
Expected End State
File
firstSocialPostsFlow
Social connected → Inngest generates posts
1 post per connected platform in approval queue; platform-appropriate format
e2e/onboarding/step-16-first-social-posts.spec.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : SocialAccount records with isActive: true for ≥ 1 platform; ContentPiece table empty
Required env vars : OPENAI_API_KEY, DALLE_API_KEY (optional), SOCIAL_POST_MAX_LENGTH_IG=2200, SOCIAL_POST_MAX_LENGTH_TWITTER=280
Required external mocks : GPT-4o-mini mock for social captions; DALL-E mock (optional)
Required test data : createMockSocialPost(), createMockContentPiece()
Verification Commands#
pnpm test:unit -- src/__tests__/unit/social-post.test.ts
pnpm test:integration -- src/__tests__/integration/social-post.test.ts
pnpm test:e2e -- e2e/onboarding/step-16-first-social-posts.spec.ts
Playwright Test Code Outline#
// e2e/onboarding/step-16-first-social-posts.spec.ts
import { test, expect } from "@playwright/test";
import { prisma } from "@/lib/db";
import { triggerInngestEvent } from "@/__tests__/helpers/inngest";
import { createMockUserWithFullProfile } from "@/__tests__/helpers/auth";
test("Step 16: First social posts generation and approval queue", async () => {
const { profile } = await createMockUserWithFullProfile();
await prisma.socialAccount.createMany({
data: [
{ practiceId: profile.id, platform: "INSTAGRAM", composioConnectionId: "conn_instagram", isActive: true, accountName: "drsmithdental" },
{ practiceId: profile.id, platform: "FACEBOOK", composioConnectionId: "conn_facebook", isActive: true, accountName: "Dr. Smith Dental Clinic" },
],
});
await triggerInngestEvent("onboarding-pipeline.create-social-posts", { practiceId: profile.id });
// Assert ContentPieces created
const socialPosts = await prisma.contentPiece.findMany({
where: { practiceId: profile.id, type: "SOCIAL_POST" },
});
expect(socialPosts.length).toBe(2);
const igPost = socialPosts.find((p) => p.platform === "INSTAGRAM");
const fbPost = socialPosts.find((p) => p.platform === "FACEBOOK");
expect(igPost).toBeDefined();
expect(fbPost).toBeDefined();
expect(igPost?.status).toBe("PENDING_REVIEW");
expect(fbPost?.status).toBe("PENDING_REVIEW");
// Assert platform appropriateness
expect(igPost?.content).toMatch(/#[A-Za-z]+/); // hashtags
expect(fbPost?.content).toMatch(/https?:\/\//); // link
});
18. Step 17: Welcome Email with Dashboard Link#
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
emailTemplateValidation
Practice with directory profile, citations, GBP, social
HTML contains all 6 sections with correct links
src/__tests__/unit/email-templates.test.ts
urlValidation
directorySlug: dr-smith-dental-clinic
All deep links return 200 before email send
src/__tests__/unit/email-templates.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
welcomeEmailSend
Resend mock; onboarding data complete
Inngest final step → Resend API
EmailLog with type: WELCOME, status: DELIVERED; all links UTM-tagged
src/__tests__/integration/email-delivery.test.ts
emailOpenTracking
Resend webhook opened event
POST /api/webhooks/resend
EmailLog.openedAt populated
src/__tests__/integration/email-tracking.test.ts
E2E Tests#
Flow
Steps
Expected End State
File
welcomeEmailFlow
Onboarding completion triggers email
Email delivered within 5 min; all deep links functional and UTM-tagged; open tracked
e2e/onboarding/step-17-welcome-email.spec.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : Practice with status: PUBLISHED; Citation records; GbpAccount (connected or not); SocialAccount (connected or not); EmailLog table ready
Required env vars : RESEND_API_KEY, RESEND_FROM_EMAIL=onboarding@rankflow.in, NEXT_PUBLIC_APP_URL
Required external mocks : Resend API mock; Resend webhook mock
Required test data : createMockEmailLog()
Verification Commands#
pnpm test:unit -- src/__tests__/unit/email-templates.test.ts
pnpm test:integration -- src/__tests__/integration/email-delivery.test.ts
pnpm test:integration -- src/__tests__/integration/email-tracking.test.ts
pnpm test:e2e -- e2e/onboarding/step-17-welcome-email.spec.ts
Playwright Test Code Outline#
// e2e/onboarding/step-17-welcome-email.spec.ts
import { test, expect } from "@playwright/test";
import { prisma } from "@/lib/db";
import { triggerInngestEvent } from "@/__tests__/helpers/inngest";
import { createMockUserWithFullProfile } from "@/__tests__/helpers/auth";
test("Step 17: Welcome email with dashboard link", async () => {
const { profile } = await createMockUserWithFullProfile();
await prisma.practice.update({
where: { id: profile.id },
data: { directorySlug: "dr-smith-dental-clinic", status: PUBLISHED },
});
await prisma.citation.createMany({
data: Array.from({ length: 24 }, (_, i) => ({
practiceId: profile.id,
locationId: profile.id,
directoryName: `dir-${i}`,
status: "SUBMITTED",
submittedAt: new Date(),
description: `Desc ${i}`,
})),
});
await triggerInngestEvent("onboarding-pipeline.send-welcome-email", { practiceId: profile.id });
const emailLog = await prisma.emailLog.findFirst({
where: { practiceId: profile.id, type: "WELCOME" },
});
expect(emailLog).not.toBeNull();
expect(emailLog?.status).toBe("DELIVERED");
expect(emailLog?.html).toContain("Your RankFlow AI Dashboard is Ready");
expect(emailLog?.html).toContain("rankflow.in/clinics/kochi/dr-smith-dental-clinic");
expect(emailLog?.html).toContain("/dashboard/citations");
expect(emailLog?.html).toContain("/dashboard/content");
expect(emailLog?.html).toContain("utm_source=welcome_email");
});
19. Step 18: Welcome Report Generation#
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
reportDataGathering
practiceId with all onboarding data
All 7 sections populated; no null crashes
src/__tests__/unit/report-generator.test.ts
pdfGeneration
HTML report template
Playwright renders A4 PDF; < 30s
src/__tests__/unit/report-generator.test.ts
s3Upload
PDF buffer
URL returned; reports/welcome-{practiceId}-{date}.pdf
src/__tests__/unit/report-generator.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
welcomeReport
Onboarding data complete; Playwright + S3 mocks
Inngest send-welcome-report
Report record created; PDF uploaded to S3; attached to email
src/__tests__/integration/reporting.test.ts
incompleteDataFallback
Some Citation records null
Report generation
"Pending" displayed for incomplete sections; no crash
src/__tests__/integration/reporting.test.ts
E2E Tests#
Flow
Steps
Expected End State
File
welcomeReportFlow
Inngest triggers report generation
PDF in S3; attached to welcome email; accurate status data
e2e/onboarding/step-18-welcome-report.spec.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : All onboarding data from Steps 1–17; Report table empty; S3 bucket configured
Required env vars : S3_REPORTS_BUCKET, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, PLAYWRIGHT_PDF_TIMEOUT=30000
Required external mocks : S3 mock (MinIO or localstack); Playwright PDF generation mock
Required test data : createMockReport()
Verification Commands#
pnpm test:unit -- src/__tests__/unit/report-generator.test.ts
pnpm test:integration -- src/__tests__/integration/reporting.test.ts
pnpm test:e2e -- e2e/onboarding/step-18-welcome-report.spec.ts
Playwright Test Code Outline#
// e2e/onboarding/step-18-welcome-report.spec.ts
import { test, expect } from "@playwright/test";
import { prisma } from "@/lib/db";
import { triggerInngestEvent } from "@/__tests__/helpers/inngest";
import { createMockUserWithFullProfile } from "@/__tests__/helpers/auth";
test("Step 18: Welcome report generation", async () => {
const { profile } = await createMockUserWithFullProfile();
// Seed complete onboarding data
await prisma.practice.update({
where: { id: profile.id },
data: { directorySlug: "dr-smith-dental-clinic", status: PUBLISHED },
});
await prisma.citation.createMany({
data: Array.from({ length: 24 }, (_, i) => ({
practiceId: profile.id,
locationId: profile.id,
directoryName: `dir-${i}`,
status: "SUBMITTED",
submittedAt: new Date(),
description: `Desc ${i}`,
})),
});
await prisma.gbpAccount.create({
data: {
practiceId: profile.id,
accountEmail: "dr.smith@example.com",
accessToken: "encrypted",
refreshToken: "encrypted",
tokenExpiresAt: new Date(Date.now() + 3600 * 1000),
scope: ["business.manage"],
isActive: true,
},
});
await triggerInngestEvent("onboarding-pipeline.send-welcome-report", { practiceId: profile.id });
const report = await prisma.report.findFirst({
where: { practiceId: profile.id, type: "WELCOME" },
});
expect(report).not.toBeNull();
expect(report?.pdfUrl).toMatch(/reports\/welcome-/);
expect(report?.pdfUrl).toContain(profile.id);
});
20. Step 19: Onboarding Completion Notification#
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
statusUpdate
onboardingComplete: false → true
ClientProfile.status → ACTIVE; onboardedAt set
src/__tests__/unit/onboarding-completion.test.ts
cronExpressionValidation
GBP posts: 0 9 * * 1,3,5
Cron parses correctly; next run is Mon/Wed/Fri 9 AM IST
src/__tests__/unit/onboarding-completion.test.ts
jobIdempotency
Same practiceId scheduled twice
Second schedule detects duplicate; no duplicate jobs
src/__tests__/unit/onboarding-completion.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
onboardingComplete
Inngest workflow returns { practice_id, status: "onboarded" }
Prisma update + BullMQ scheduling
ClientProfile.onboardingComplete: true; 6 recurring jobs in BullMQ
src/__tests__/integration/onboarding-completion.test.ts
recurringJobSchedule
ClientProfile with status: ACTIVE
BullMQ job creation
Jobs: gbp-post, social-post, review-monitor, nap-check, monthly-report, profile-refresh
src/__tests__/integration/onboarding-completion.test.ts
E2E Tests#
Flow
Steps
Expected End State
File
onboardingCompleteFlow
All steps complete → Inngest finalizes
onboardingComplete: true; jobs scheduled; completion email sent; dashboard functional
e2e/onboarding/step-19-onboarding-complete.spec.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : All onboarding data from Steps 1–18; ClientProfile with onboardingComplete: false; BullMQ queues configured
Required env vars : BULLMQ_REDIS_URL, TZ=Asia/Kolkata
Required external mocks : None (internal scheduling)
Required test data : createMockClientProfile({ onboardingComplete: false })
Verification Commands#
pnpm test:unit -- src/__tests__/unit/onboarding-completion.test.ts
pnpm test:integration -- src/__tests__/integration/onboarding-completion.test.ts
pnpm test:e2e -- e2e/onboarding/step-19-onboarding-complete.spec.ts
Playwright Test Code Outline#
// e2e/onboarding/step-19-onboarding-complete.spec.ts
import { test, expect } from "@playwright/test";
import { prisma } from "@/lib/db";
import { triggerInngestEvent } from "@/__tests__/helpers/inngest";
import { createMockUserWithFullProfile } from "@/__tests__/helpers/auth";
import { getBullMQJobs } from "@/__tests__/helpers/bullmq";
test("Step 19: Onboarding completion notification", async () => {
const { profile } = await createMockUserWithFullProfile();
// Seed all onboarding artifacts
await prisma.practice.update({
where: { id: profile.id },
data: { directorySlug: "dr-smith-dental-clinic", status: PUBLISHED },
});
await prisma.citation.createMany({
data: Array.from({ length: 24 }, (_, i) => ({
practiceId: profile.id,
locationId: profile.id,
directoryName: `dir-${i}`,
status: "SUBMITTED",
submittedAt: new Date(),
description: `Desc ${i}`,
})),
});
await prisma.gbpAccount.create({
data: {
practiceId: profile.id,
accountEmail: "dr.smith@example.com",
accessToken: "encrypted",
refreshToken: "encrypted",
tokenExpiresAt: new Date(Date.now() + 3600 * 1000),
scope: ["business.manage"],
isActive: true,
},
});
await triggerInngestEvent("onboarding-pipeline.complete", { practiceId: profile.id });
// Assert profile updated
const updatedProfile = await prisma.clientProfile.findUnique({ where: { id: profile.id } });
expect(updatedProfile?.onboardingComplete).toBe(true);
expect(updatedProfile?.status).toBe("ACTIVE");
expect(updatedProfile?.onboardedAt).toBeInstanceOf(Date);
// Assert recurring jobs scheduled
const jobs = await getBullMQJobs(profile.id);
const jobNames = jobs.map((j) => j.name);
expect(jobNames).toContain("gbp-post");
expect(jobNames).toContain("social-post");
expect(jobNames).toContain("review-monitor");
expect(jobNames).toContain("nap-check");
expect(jobNames).toContain("monthly-report");
expect(jobNames).toContain("profile-refresh");
});
21. Step 20: Admin Notification of New Client#
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
adminEmailTemplate
Practice with full onboarding data
HTML contains all 8 sections: client summary, status, GBP, social, citations, directory profile, action items, deep link
src/__tests__/unit/email-templates.test.ts
actionItemFlagging
2 citations failed, GBP pending
Action items section lists both; prioritized
src/__tests__/unit/email-templates.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
adminNotification
Resend mock; onboarding complete
Inngest final step → Resend API
EmailLog with to: admin@rankflow.in, type: ADMIN_NEW_CLIENT; action items listed
src/__tests__/integration/email-delivery.test.ts
slackFallback
Resend bounce webhook
Slack webhook POST
Slack #alerts receives fallback notification
src/__tests__/integration/slack-webhook.test.ts
E2E Tests#
Flow
Steps
Expected End State
File
adminNotificationFlow
Onboarding complete triggers admin email
Admin email delivered within 2 min; all actionable items flagged; deep link functional
e2e/onboarding/step-20-admin-notification.spec.ts
Success Criteria (Binary)#
Admin email delivered within 2 minutes of onboarding completion to admin@rankflow.in and founder@rankflow.in
Email subject: "New Client Onboarded: {BusinessName} ({Plan})"
Email contains 8 sections: client summary (name, city, category, plan, trial end), onboarding status, GBP connection status, social connections, citation success rate, directory profile URL, action items, deep link to /admin/clients/{id}
All actionable items clearly flagged with priority: "GBP OAuth pending — call client?", "2 citations failed — manual submission needed"
Deep link to /admin/clients/{id} functional and returns 200 for admin user
Admin email bounce triggers Slack webhook #alerts as backup
Missing action items detected by logic → full client data dump included for manual review
Agent Context (Pre-conditions)#
Required DB state : All onboarding data from Steps 1–19; EmailLog table; admin user with role: ADMIN
Required env vars : RESEND_API_KEY, ADMIN_EMAIL=admin@rankflow.in, FOUNDER_EMAIL=founder@rankflow.in, SLACK_WEBHOOK_URL
Required external mocks : Resend API mock; Slack webhook mock
Required test data : createMockAdminUser(), createMockEmailLog()
Verification Commands#
pnpm test:unit -- src/__tests__/unit/email-templates.test.ts
pnpm test:integration -- src/__tests__/integration/email-delivery.test.ts
pnpm test:integration -- src/__tests__/integration/slack-webhook.test.ts
pnpm test:e2e -- e2e/onboarding/step-20-admin-notification.spec.ts
Playwright Test Code Outline#
// e2e/onboarding/step-20-admin-notification.spec.ts
import { test, expect } from "@playwright/test";
import { prisma } from "@/lib/db";
import { triggerInngestEvent } from "@/__tests__/helpers/inngest";
import { createMockUserWithFullProfile } from "@/__tests__/helpers/auth";
test("Step 20: Admin notification of new client", async () => {
const { profile } = await createMockUserWithFullProfile();
// Complete onboarding
await prisma.practice.update({
where: { id: profile.id },
data: { directorySlug: "dr-smith-dental-clinic", status: PUBLISHED },
});
await prisma.citation.createMany({
data: [
...Array.from({ length: 24 }, (_, i) => ({
practiceId: profile.id,
locationId: profile.id,
directoryName: `dir-${i}`,
status: "SUBMITTED",
submittedAt: new Date(),
description: `Desc ${i}`,
})),
...Array.from({ length: 6 }, (_, i) => ({
practiceId: profile.id,
locationId: profile.id,
directoryName: `failed-dir-${i}`,
status: "FAILED",
submittedAt: null,
description: `Failed ${i}`,
})),
],
});
await triggerInngestEvent("onboarding-pipeline.notify-admin", { practiceId: profile.id });
const emailLog = await prisma.emailLog.findFirst({
where: { type: "ADMIN_NEW_CLIENT", to: { contains: "admin@rankflow.in" } },
});
expect(emailLog).not.toBeNull();
expect(emailLog?.status).toBe("DELIVERED");
expect(emailLog?.html).toContain("Dr. Smith Dental Clinic");
expect(emailLog?.html).toContain("STANDARD");
expect(emailLog?.html).toContain("24/30"); // citation success rate
expect(emailLog?.html).toContain("/admin/clients/");
expect(emailLog?.html).toContain("GBP OAuth pending");
expect(emailLog?.html).toContain("6 citations failed");
});
22. E2E: Full Onboarding Flow (Signup → First Post)#
This section defines the complete end-to-end test that exercises all 20 steps in sequence, measuring the critical metric: 80% completion rate from signup to first GBP post published .
Test Plan & Verification#
E2E Tests#
Flow
Steps
Expected End State
File
fullOnboardingHappyPath
1. Signup 2. Business profile 3. Consent 4. Payment 5. DirectorySlug 6. LP content 7. LP deploy 8. GBP OAuth email 9. GBP OAuth 10. GBP sync 11. Social connect 12. Citations desc 13. Citations submit 14. NAP schedule 15. First GBP post 16. First social posts 17. Welcome email 18. Welcome report 19. Onboarding complete 20. Admin notification
All 20 steps complete; onboardingComplete: true; ≥ 24 citations submitted; dashboard functional; 80% completion rate target met
e2e/onboarding/full-onboarding.spec.ts
fullOnboardingPartialSocial
Same as above but customer skips social connect
Onboarding completes; social posts skipped; all other steps succeed
e2e/onboarding/full-onboarding-partial.spec.ts
fullOnboardingGbpDeclined
Customer declines GBP OAuth initially
Onboarding pauses at Step 9; reminder emails sent; admin alerted; can resume later
e2e/onboarding/full-onboarding-gbp-declined.spec.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : Clean PostgreSQL database with all tables migrated; 30 CitationDirectory records seeded
Required env vars : All production env vars loaded from .env.test; mock mode enabled for external APIs
Required external mocks : Google OAuth, Stripe/Razorpay, Composio, Resend, Claude/GPT/Llama, GBP API, Cloudflare CDN, S3 — all mocked via msw
Required test data : Full mock factories for User, ClientProfile, Practice, Location, GbpAccount, GbpLocation, SocialAccount, Citation, DirectoryProfileSection, ContentPiece, ScheduledPost, Report, EmailLog, ConsentLog, Subscription, Billing
Required tooling : Playwright browser context, Inngest test client, BullMQ test client, MSW server, Prisma test client
Verification Commands#
# Run the full onboarding E2E test suite
pnpm test:e2e -- e2e/onboarding/full-onboarding.spec.ts
# Run with headed browser for debugging
pnpm test:e2e -- e2e/onboarding/full-onboarding.spec.ts --headed
# Run specific scenario
pnpm test:e2e -- e2e/onboarding/full-onboarding-partial.spec.ts
pnpm test:e2e -- e2e/onboarding/full-onboarding-gbp-declined.spec.ts
Playwright Test Code Outline#
// e2e/onboarding/full-onboarding.spec.ts
import { test, expect } from "@playwright/test";
import { prisma } from "@/lib/db";
import { seedCitationDirectories } from "@/__tests__/seeders/citation-directories";
import { setupMockServer } from "@/__tests__/mocks/server";
test.describe("Full Onboarding Flow", () => {
test.beforeAll(async () => {
await setupMockServer();
await seedCitationDirectories();
});
test.beforeEach(async () => {
// Clean DB before each run
await prisma.$transaction([
prisma.consentLog.deleteMany(),
prisma.citation.deleteMany(),
prisma.socialAccount.deleteMany(),
prisma.gbpLocation.deleteMany(),
prisma.gbpAccount.deleteMany(),
prisma.directoryProfileSection.deleteMany(),
prisma.contentPiece.deleteMany(),
prisma.scheduledPost.deleteMany(),
prisma.location.deleteMany(),
prisma.clientProfile.deleteMany(),
prisma.user.deleteMany(),
]);
});
test("Complete onboarding from signup to first GBP post", async ({ page }) => {
const startTime = Date.now();
// Step 1: Signup
await page.goto("/signup");
await page.click('[data-testid="plan-standard"]');
await page.fill('[data-testid="email-input"]', "dr.smith@example.com");
await page.fill('[data-testid="password-input"]', "Password123");
await page.fill('[data-testid="confirm-password-input"]', "Password123");
await page.fill('[data-testid="practice-name-input"]', "Dr. Smith Dental Clinic");
await page.click('[data-testid="create-account-button"]');
await expect(page).toHaveURL(/\/onboarding\?step=1/, { timeout: 5000 });
// Step 2: Business Profile
await page.fill('[data-testid="business-name"]', "Dr. Smith Dental Clinic");
await page.selectOption('[data-testid="business-category"]', "DENTIST");
await page.fill('[data-testid="doctor-name"]', "Dr. John Smith");
await page.fill('[data-testid="usps"]', "Advanced dental implants, painless root canals");
await page.click('[data-testid="next-step-2"]');
await page.fill('[data-testid="primary-address"]', "123 Main Road, Ernakulam");
await page.selectOption('[data-testid="city"]', "Kochi");
await page.fill('[data-testid="pin-code"]', "682001");
await page.fill('[data-testid="primary-phone"]', "+919876543210");
await page.fill('[data-testid="business-email"]', "contact@drsmithdental.com");
await page.fill('[data-testid="hours-monday-open"]', "09:00");
await page.fill('[data-testid="hours-monday-close"]', "19:00");
await page.click('[data-testid="next-step-3"]');
await page.fill('[data-testid="service-1"]', "Teeth Cleaning");
await page.fill('[data-testid="service-2"]', "Root Canal Treatment");
await page.fill('[data-testid="service-3"]', "Dental Implants");
await page.click('[data-testid="submit-profile"]');
await expect(page).toHaveURL(/\/onboarding\?step=consent/, { timeout: 5000 });
// Step 3: Consent
await page.check('[data-testid="consent-terms"]');
await page.check('[data-testid="consent-privacy"]');
await page.check('[data-testid="consent-gbp"]');
await page.check('[data-testid="consent-data-processing"]');
await page.check('[data-testid="consent-citations"]');
await page.click('[data-testid="agree-and-continue"]');
await expect(page).toHaveURL(/\/onboarding\?step=payment/, { timeout: 3000 });
// Step 4: Payment (Stripe)
const stripeFrame = page.frameLocator('[data-testid="stripe-card-element"] iframe');
await stripeFrame.locator('[placeholder="Card number"]').fill("4242 4242 4242 4242");
await stripeFrame.locator('[placeholder="MM / YY"]').fill("12/30");
await stripeFrame.locator('[placeholder="CVC"]').fill("123");
await page.click('[data-testid="start-trial-button"]');
await expect(page).toHaveURL(/\/onboarding\?step=5/, { timeout: 10000 });
// Steps 5-7: DirectorySlug, Content, Deploy (system automated)
// In test environment, Inngest steps are triggered and mocked
await page.waitForTimeout(3000); // Allow system automation
// Step 8-9: GBP OAuth
await page.goto("/dashboard/gbp");
await page.click('[data-testid="connect-gbp-button"]');
// Simulate OAuth completion
const state = Buffer.from(JSON.stringify({ practiceId: "auto" })).toString("base64");
await page.goto(`/api/webhooks/google/oauth-callback?code=mock_code&state=${state}`);
await expect(page).toHaveURL("/dashboard/gbp?connected=true", { timeout: 5000 });
// Step 10: GBP Sync (system automated)
await page.waitForTimeout(2000);
// Step 11: Social Connect
await page.goto("/dashboard/social");
await page.click('[data-testid="connect-instagram"]');
const socialState = Buffer.from(JSON.stringify({ practiceId: "auto", platform: "INSTAGRAM" })).toString("base64");
await page.goto(`/api/webhooks/composio/oauth-callback?connectionId=conn_instagram&state=${socialState}`);
await expect(page).toHaveURL("/dashboard/social?connected=instagram", { timeout: 5000 });
// Steps 12-14: Citations (system automated)
await page.waitForTimeout(3000);
// Steps 15-16: First Posts (system automated)
await page.waitForTimeout(2000);
// Step 17-20: Final notifications (system automated)
await page.waitForTimeout(2000);
// Final assertions
const user = await prisma.user.findFirst({ where: { email: "dr.smith@example.com" } });
const profile = await prisma.clientProfile.findFirst({ where: { userId: user!.id } });
expect(profile?.onboardingComplete).toBe(true);
expect(profile?.status).toBe("ACTIVE");
const citations = await prisma.citation.findMany({ where: { practiceId: profile!.id } });
const submittedCount = citations.filter((c) => c.status === "SUBMITTED").length;
expect(submittedCount).toBeGreaterThanOrEqual(24); // 80% success
const gbpAccount = await prisma.gbpAccount.findFirst({ where: { practiceId: profile!.id } });
expect(gbpAccount?.isActive).toBe(true);
const contentPieces = await prisma.contentPiece.findMany({ where: { practiceId: profile!.id } });
expect(contentPieces.length).toBeGreaterThanOrEqual(1); // At least first GBP post
const emailLogs = await prisma.emailLog.findMany({ where: { practiceId: profile!.id } });
expect(emailLogs.length).toBeGreaterThanOrEqual(4); // Welcome, GBP OAuth, Content, Admin
// Performance assertion
const duration = Date.now() - startTime;
expect(duration).toBeLessThan(25 * 60 * 1000); // < 25 minutes
});
});
23. Success Criteria (Binary)#
Overall Onboarding E2E Success Criteria#
24. Agent Context & Verification Commands#
Agent Context (Pre-conditions for Full Suite)#
- **Required DB state**: PostgreSQL 15+ with Prisma schema fully migrated; all tables empty except 30 `CitationDirectory` records seeded
- **Required env vars** (`.env.test`):
- `DATABASE_URL=postgresql://test:test@localhost:5432/rankflow_test`
- `NEXTAUTH_SECRET=test-secret-12345678901234567890123456789012`
- `NEXT_PUBLIC_APP_URL=http://localhost:3000`
- `STRIPE_SECRET_KEY=sk_test_mock`
- `STRIPE_WEBHOOK_SECRET=whsec_mock`
- `RAZORPAY_KEY_ID=rzp_test_mock`
- `RAZORPAY_KEY_SECRET=mock_secret`
- `RAZORPAY_WEBHOOK_SECRET=mock_webhook_secret`
- `GOOGLE_CLIENT_ID=mock_google_client_id`
- `GOOGLE_CLIENT_SECRET=mock_google_client_secret`
- `GOOGLE_OAUTH_REDIRECT_URI=http://localhost:3000/api/webhooks/google/oauth-callback`
- `COMPOSIO_API_KEY=mock_composio_key`
- `ZERNIO_API_KEY=mock_zernio_key`
- `RESEND_API_KEY=re_mock_1234567890`
- `RESEND_FROM_EMAIL=onboarding@rankflow.in`
- `ANTHROPIC_API_KEY=sk-ant-mock`
- `OPENAI_API_KEY=sk-openai-mock`
- `TOGETHER_API_KEY=mock_together_key`
- `HYPERBROWSER_API_KEY=mock_hyperbrowser_key`
- `S3_REPORTS_BUCKET=rankflow-test-reports`
- `S3_CITATIONS_BUCKET=rankflow-test-citations`
- `AWS_ACCESS_KEY_ID=test`
- `AWS_SECRET_ACCESS_KEY=test`
- `AWS_REGION=ap-south-1`
- `CLOUDFLARE_API_TOKEN=mock_cf_token`
- `WILDCARD_DNS_DOMAIN=rankflow.in`
- `ENCRYPTION_KEY=aGVsbG8td29ybGQtaGVsbG8td29ybGQtaGVsbG8=` (32-byte base64)
- `BULLMQ_REDIS_URL=redis://localhost:6379/1`
- `INNGEST_EVENT_KEY=test-key`
- `TZ=Asia/Kolkata`
- `DPDPA_COMPLIANCE_MODE=true`
- `UPLOAD_MAX_SIZE=2097152`
- `LLM_ROUTER_TIMEOUT=30000`
- `GBP_POST_MAX_LENGTH=1500`
- `AUTO_PUBLISH_HOURS=24`
- `SOCIAL_POST_MAX_LENGTH_IG=2200`
- `SOCIAL_POST_MAX_LENGTH_TWITTER=280`
- `NAP_VERIFY_DELAY_DAYS=7`
- `PLAYWRIGHT_PDF_TIMEOUT=30000`
- `ADMIN_EMAIL=admin@rankflow.in`
- `FOUNDER_EMAIL=founder@rankflow.in`
- `SLACK_WEBHOOK_URL=https://hooks.slack.com/services/mock`
- **Required external mocks**: All external services mocked via `msw` in `src/__tests__/mocks/server.ts`
- **Required test data**: All factories in `src/__tests__/factories/`; all seeders in `src/__tests__/seeders/`
- **Required test database**: Isolated test database `rankflow_test`; never touches production data
- **Required CI config**: GitHub Actions workflow with PostgreSQL + Redis services; Playwright browsers installed
Verification Commands#
# ─────────────────────────────────────────────
# Setup & Environment Verification
# ─────────────────────────────────────────────
# 1. Verify test database is running and accessible
pnpm prisma migrate reset --force --skip-seed
# 2. Seed citation directories
pnpm tsx src/__tests__/seeders/citation-directories.ts
# 3. Verify mock server starts
pnpm test:unit -- src/__tests__/mocks/server.test.ts
# ─────────────────────────────────────────────
# Step-by-Step Verification (Individual Steps)
# ─────────────────────────────────────────────
# Step 1: Account creation
pnpm test:unit -- src/__tests__/unit/auth.validation.test.ts
pnpm test:integration -- src/__tests__/integration/auth.router.test.ts
pnpm test:e2e -- e2e/onboarding/step-01-signup.spec.ts
# Step 2: Business profile
pnpm test:unit -- src/__tests__/unit/practice.validation.test.ts
pnpm test:integration -- src/__tests__/integration/practice.router.test.ts
pnpm test:e2e -- e2e/onboarding/step-02-business-profile.spec.ts
# Step 3: Consent
pnpm test:unit -- src/__tests__/unit/consent.validation.test.ts
pnpm test:integration -- src/__tests__/integration/consent.router.test.ts
pnpm test:e2e -- e2e/onboarding/step-03-consent.spec.ts
# Step 4: Payment
pnpm test:unit -- src/__tests__/unit/payment.validation.test.ts
pnpm test:integration -- src/__tests__/integration/billing.router.test.ts
pnpm test:integration -- src/__tests__/integration/webhooks.test.ts
pnpm test:e2e -- e2e/onboarding/step-04-payment.spec.ts
# Step 5: DirectorySlug
pnpm test:unit -- src/__tests__/unit/directorySlug.test.ts
pnpm test:integration -- src/__tests__/integration/practice.router.test.ts
pnpm test:e2e -- e2e/onboarding/step-05-profile-creation.spec.ts
# Step 6: LP Content Generation
pnpm test:unit -- src/__tests__/unit/medical-compliance.test.ts
pnpm test:unit -- src/__tests__/unit/schema.validation.test.ts
pnpm test:integration -- src/__tests__/integration/profile-content-generation.test.ts
pnpm test:integration -- src/__tests__/integration/ai-router.test.ts
pnpm test:e2e -- e2e/onboarding/step-06-approval-gate.spec.ts
# Step 7: LP Deployment
pnpm test:unit -- src/__tests__/unit/profile-publish.test.ts
pnpm test:integration -- src/__tests__/integration/profile-publish.test.ts
pnpm test:e2e -- e2e/onboarding/step-07-profile-publish.spec.ts
# Step 8: GBP OAuth Email
pnpm test:unit -- src/__tests__/unit/gbp-oauth.test.ts
pnpm test:unit -- src/__tests__/unit/email-templates.test.ts
pnpm test:integration -- src/__tests__/integration/gbp-oauth.test.ts
pnpm test:integration -- src/__tests__/integration/email-tracking.test.ts
pnpm test:e2e -- e2e/onboarding/step-08-gbp-oauth-email.spec.ts
# Step 9: GBP OAuth Flow
pnpm test:unit -- src/__tests__/unit/crypto.test.ts
pnpm test:unit -- src/__tests__/unit/gbp-oauth.test.ts
pnpm test:integration -- src/__tests__/integration/gbp-oauth.test.ts
pnpm test:e2e -- e2e/onboarding/step-09-gbp-oauth-flow.spec.ts
# Step 10: GBP Location Sync
pnpm test:unit -- src/__tests__/unit/gbp-sync.test.ts
pnpm test:integration -- src/__tests__/integration/gbp-sync.test.ts
pnpm test:e2e -- e2e/onboarding/step-10-gbp-sync.spec.ts
# Step 11: Social Connect
pnpm test:unit -- src/__tests__/unit/social-oauth.test.ts
pnpm test:integration -- src/__tests__/integration/social-connect.test.ts
pnpm test:e2e -- e2e/onboarding/step-11-social-connect.spec.ts
# Step 12: Citation Descriptions
pnpm test:unit -- src/__tests__/unit/citation-description.test.ts
pnpm test:integration -- src/__tests__/integration/citation-builder.test.ts
pnpm test:integration -- src/__tests__/integration/ai-router.test.ts
pnpm test:e2e -- e2e/onboarding/step-12-citation-descriptions.spec.ts
# Step 13: Citation Submission
pnpm test:unit -- src/__tests__/unit/citation-submitter.test.ts
pnpm test:integration -- src/__tests__/integration/citation-builder.test.ts
pnpm test:e2e -- e2e/onboarding/step-13-citation-submit.spec.ts
# Step 14: NAP Verify Schedule
pnpm test:unit -- src/__tests__/unit/inngest-schedule.test.ts
pnpm test:integration -- src/__tests__/integration/citation-builder.test.ts
pnpm test:e2e -- e2e/onboarding/step-14-nap-verify-schedule.spec.ts
# Step 15: First GBP Post
pnpm test:unit -- src/__tests__/unit/gbp-post.test.ts
pnpm test:integration -- src/__tests__/integration/gbp-post.test.ts
pnpm test:e2e -- e2e/onboarding/step-15-first-gbp-post.spec.ts
# Step 16: First Social Posts
pnpm test:unit -- src/__tests__/unit/social-post.test.ts
pnpm test:integration -- src/__tests__/integration/social-post.test.ts
pnpm test:e2e -- e2e/onboarding/step-16-first-social-posts.spec.ts
# Step 17: Welcome Email
pnpm test:unit -- src/__tests__/unit/email-templates.test.ts
pnpm test:integration -- src/__tests__/integration/email-delivery.test.ts
pnpm test:integration -- src/__tests__/integration/email-tracking.test.ts
pnpm test:e2e -- e2e/onboarding/step-17-welcome-email.spec.ts
# Step 18: Welcome Report
pnpm test:unit -- src/__tests__/unit/report-generator.test.ts
pnpm test:integration -- src/__tests__/integration/reporting.test.ts
pnpm test:e2e -- e2e/onboarding/step-18-welcome-report.spec.ts
# Step 19: Onboarding Complete
pnpm test:unit -- src/__tests__/unit/onboarding-completion.test.ts
pnpm test:integration -- src/__tests__/integration/onboarding-completion.test.ts
pnpm test:e2e -- e2e/onboarding/step-19-onboarding-complete.spec.ts
# Step 20: Admin Notification
pnpm test:unit -- src/__tests__/unit/email-templates.test.ts
pnpm test:integration -- src/__tests__/integration/email-delivery.test.ts
pnpm test:integration -- src/__tests__/integration/slack-webhook.test.ts
pnpm test:e2e -- e2e/onboarding/step-20-admin-notification.spec.ts
# ─────────────────────────────────────────────
# Full E2E Flow Verification
# ─────────────────────────────────────────────
# Run all onboarding tests sequentially
pnpm test:e2e -- e2e/onboarding/
# Run full onboarding happy path (target: 80% completion rate)
pnpm test:e2e -- e2e/onboarding/full-onboarding.spec.ts
# Run with tracing for debugging failures
pnpm test:e2e -- e2e/onboarding/full-onboarding.spec.ts --trace on
# Run in CI mode (headless, parallel where safe)
pnpm test:e2e -- e2e/onboarding/ --workers=2
# ─────────────────────────────────────────────
# Combined Verification (All Tiers)
# ─────────────────────────────────────────────
# Run all unit tests
pnpm test:unit
# Run all integration tests
pnpm test:integration
# Run all E2E tests
pnpm test:e2e
# Full test suite (CI pipeline)
pnpm test
Verification Report Template#
After running the full suite, produce a VERIFY.md in docs/test-specs/VERIFY-onboarding-e2e.md:
# Onboarding E2E Verification Report
**Date**: 2026-06-13
**Test Suite**: `TEST-onboarding-e2e.md`
**Runner**: Playwright + Vitest + MSW
**Environment**: `.env.test` with all external services mocked
## Results Summary
| Tier | Total | Passed | Failed | Skipped | Coverage |
|------|-------|--------|--------|---------|----------|
| Unit | 0 | 0 | 0 | 0 | 0% |
| Integration | 0 | 0 | 0 | 0 | 0% |
| E2E | 0 | 0 | 0 | 0 | 0% |
## Success Criteria Checklist
- [ ] SC-01: Account creation within 3s
- [ ] SC-02: Location with full NAP
- [ ] SC-03: 5 consent logs with IP
- [ ] SC-04: Subscription trialing
- [ ] SC-05: DirectorySlug resolvable
- [ ] SC-06: 8+ profile sections + valid schema
- [ ] SC-07: Directory profile live, LCP < 2.5s
- [ ] SC-08: GBP OAuth email delivered
- [ ] SC-09: Encrypted tokens stored
- [ ] SC-10: GBP locations synced
- [ ] SC-11: Social account connected
- [ ] SC-12: 30 unique descriptions
- [ ] SC-13: 24+ citations submitted
- [ ] SC-14: NAP verify scheduled
- [ ] SC-15: First GBP post queued
- [ ] SC-16: First social posts queued
- [ ] SC-17: Welcome email delivered
- [ ] SC-18: Welcome report PDF generated
- [ ] SC-19: Onboarding complete, jobs scheduled
- [ ] SC-20: Admin notification sent
- [ ] SC-21: 80% completion rate achieved
- [ ] SC-22: Total time < 25 minutes
- [ ] SC-23: Zero unencrypted tokens
- [ ] SC-24: All emails tracked
- [ ] SC-25: All failure paths handled
## Failure Analysis
| Step | Failure | Root Cause | Fix Required | Status |
|------|---------|-----------|-------------|--------|
| — | — | — | — | — |
## Sign-off
- [ ] E2E Test Specialist: ___________
- [ ] QA Lead: ___________
- [ ] Engineering Lead: ___________
End of Onboarding E2E Test Specification — RankFlow AI v1.0.0