feat: module-driven onboarding system + simplified 3-step signup
Add OnboardingProviderProtocol so modules declare their own post-signup onboarding steps. The core OnboardingAggregator discovers enabled providers and exposes a dashboard API (GET /dashboard/onboarding). A session-scoped banner on the store dashboard shows a checklist that guides merchants through setup without blocking signup. Signup is simplified from 4 steps to 3 (Plan → Account → Payment): store creation is merged into account creation, store language is captured from the user's browsing language, and platform-specific template branching is removed. Includes 47 unit and integration tests covering all new providers, the aggregator, the API endpoint, and the signup service changes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
215
app/modules/core/tests/integration/test_onboarding_routes.py
Normal file
215
app/modules/core/tests/integration/test_onboarding_routes.py
Normal file
@@ -0,0 +1,215 @@
|
||||
# app/modules/core/tests/integration/test_onboarding_routes.py
|
||||
"""
|
||||
Integration tests for the onboarding API endpoint.
|
||||
|
||||
Tests the store dashboard onboarding endpoint at:
|
||||
GET /api/v1/store/dashboard/onboarding
|
||||
|
||||
Verifies:
|
||||
- Endpoint returns onboarding steps with completion status
|
||||
- Endpoint requires store authentication
|
||||
- Response structure matches expected schema
|
||||
"""
|
||||
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from app.api.deps import get_current_store_api
|
||||
from app.modules.tenancy.models import Merchant, Platform, Store, User
|
||||
from app.modules.tenancy.models.store_platform import StorePlatform
|
||||
from app.modules.tenancy.schemas.auth import UserContext
|
||||
from main import app
|
||||
|
||||
# ============================================================================
|
||||
# Fixtures
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def onb_owner(db):
|
||||
"""Create a store owner user for onboarding tests."""
|
||||
from middleware.auth import AuthManager
|
||||
|
||||
auth = AuthManager()
|
||||
user = User(
|
||||
email=f"onbroute_{uuid.uuid4().hex[:8]}@test.com",
|
||||
username=f"onbroute_{uuid.uuid4().hex[:8]}",
|
||||
hashed_password=auth.hash_password("pass123"),
|
||||
role="merchant_owner",
|
||||
is_active=True,
|
||||
)
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
return user
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def onb_platform(db):
|
||||
"""Create a platform for onboarding tests."""
|
||||
platform = Platform(
|
||||
code=f"onbp_{uuid.uuid4().hex[:8]}",
|
||||
name="Onboarding Test Platform",
|
||||
is_active=True,
|
||||
)
|
||||
db.add(platform)
|
||||
db.commit()
|
||||
db.refresh(platform)
|
||||
return platform
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def onb_merchant(db, onb_owner):
|
||||
"""Create a merchant for onboarding tests."""
|
||||
merchant = Merchant(
|
||||
name="Onboarding Route Test Merchant",
|
||||
owner_user_id=onb_owner.id,
|
||||
contact_email=onb_owner.email,
|
||||
is_active=True,
|
||||
is_verified=True,
|
||||
)
|
||||
db.add(merchant)
|
||||
db.commit()
|
||||
db.refresh(merchant)
|
||||
return merchant
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def onb_store(db, onb_merchant):
|
||||
"""Create a store for onboarding tests."""
|
||||
uid = uuid.uuid4().hex[:8]
|
||||
store = Store(
|
||||
merchant_id=onb_merchant.id,
|
||||
store_code=f"ONBROUTE_{uid.upper()}",
|
||||
subdomain=f"onbroute{uid.lower()}",
|
||||
name="Onboarding Route Test Store",
|
||||
is_active=True,
|
||||
is_verified=True,
|
||||
)
|
||||
db.add(store)
|
||||
db.commit()
|
||||
db.refresh(store)
|
||||
return store
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def onb_store_platform(db, onb_store, onb_platform):
|
||||
"""Link store to platform."""
|
||||
sp = StorePlatform(
|
||||
store_id=onb_store.id,
|
||||
platform_id=onb_platform.id,
|
||||
is_active=True,
|
||||
)
|
||||
db.add(sp)
|
||||
db.commit()
|
||||
db.refresh(sp)
|
||||
return sp
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def onb_auth(onb_owner, onb_store):
|
||||
"""Override auth dependency for store API auth."""
|
||||
user_context = UserContext(
|
||||
id=onb_owner.id,
|
||||
email=onb_owner.email,
|
||||
username=onb_owner.username,
|
||||
role="merchant_owner",
|
||||
is_active=True,
|
||||
token_store_id=onb_store.id,
|
||||
token_store_code=onb_store.store_code,
|
||||
)
|
||||
|
||||
def _override():
|
||||
return user_context
|
||||
|
||||
app.dependency_overrides[get_current_store_api] = _override
|
||||
yield {"Authorization": "Bearer fake-token"}
|
||||
app.dependency_overrides.pop(get_current_store_api, None)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Tests
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.core
|
||||
class TestOnboardingEndpoint:
|
||||
"""Tests for GET /api/v1/store/dashboard/onboarding."""
|
||||
|
||||
def test_returns_onboarding_summary(
|
||||
self, client, db, onb_auth, onb_store, onb_store_platform
|
||||
):
|
||||
"""Endpoint returns onboarding summary with expected structure."""
|
||||
response = client.get(
|
||||
"/api/v1/store/dashboard/onboarding",
|
||||
headers=onb_auth,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
assert "steps" in data
|
||||
assert "total_steps" in data
|
||||
assert "completed_steps" in data
|
||||
assert "progress_percentage" in data
|
||||
assert "all_completed" in data
|
||||
assert isinstance(data["steps"], list)
|
||||
assert isinstance(data["total_steps"], int)
|
||||
assert isinstance(data["completed_steps"], int)
|
||||
|
||||
def test_includes_tenancy_step(
|
||||
self, client, db, onb_auth, onb_store, onb_store_platform
|
||||
):
|
||||
"""Response includes the tenancy customize_store step (always present)."""
|
||||
response = client.get(
|
||||
"/api/v1/store/dashboard/onboarding",
|
||||
headers=onb_auth,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
step_keys = [s["key"] for s in data["steps"]]
|
||||
assert "tenancy.customize_store" in step_keys
|
||||
|
||||
def test_step_data_has_required_fields(
|
||||
self, client, db, onb_auth, onb_store, onb_store_platform
|
||||
):
|
||||
"""Each step has key, title_key, description_key, icon, route, completed."""
|
||||
response = client.get(
|
||||
"/api/v1/store/dashboard/onboarding",
|
||||
headers=onb_auth,
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
for step in data["steps"]:
|
||||
assert "key" in step
|
||||
assert "title_key" in step
|
||||
assert "description_key" in step
|
||||
assert "icon" in step
|
||||
assert "route" in step
|
||||
assert "completed" in step
|
||||
assert isinstance(step["completed"], bool)
|
||||
|
||||
def test_routes_contain_store_code(
|
||||
self, client, db, onb_auth, onb_store, onb_store_platform
|
||||
):
|
||||
"""Step routes have {store_code} replaced with the actual store code."""
|
||||
response = client.get(
|
||||
"/api/v1/store/dashboard/onboarding",
|
||||
headers=onb_auth,
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
for step in data["steps"]:
|
||||
assert "{store_code}" not in step["route"]
|
||||
if "/store/" in step["route"]:
|
||||
assert onb_store.store_code in step["route"]
|
||||
|
||||
def test_requires_auth(self, client):
|
||||
"""Returns 401 without authentication."""
|
||||
app.dependency_overrides.pop(get_current_store_api, None)
|
||||
response = client.get(
|
||||
"/api/v1/store/dashboard/onboarding",
|
||||
)
|
||||
assert response.status_code == 401
|
||||
Reference in New Issue
Block a user