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:
2026-02-28 23:39:42 +01:00
parent f8a2394da5
commit ef9ea29643
26 changed files with 2055 additions and 699 deletions

View File

@@ -0,0 +1,140 @@
# app/modules/marketplace/tests/unit/test_marketplace_onboarding.py
"""Unit tests for MarketplaceOnboardingProvider."""
import uuid
import pytest
from app.modules.marketplace.services.marketplace_onboarding import (
MarketplaceOnboardingProvider,
)
from app.modules.tenancy.models import Merchant, Store, User
@pytest.fixture
def mp_owner(db):
"""Create a store owner for marketplace onboarding tests."""
from middleware.auth import AuthManager
auth = AuthManager()
user = User(
email=f"mpowner_{uuid.uuid4().hex[:8]}@test.com",
username=f"mpowner_{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 mp_merchant(db, mp_owner):
"""Create a merchant for marketplace onboarding tests."""
merchant = Merchant(
name="MP Onboarding Merchant",
owner_user_id=mp_owner.id,
contact_email=mp_owner.email,
is_active=True,
is_verified=True,
)
db.add(merchant)
db.commit()
db.refresh(merchant)
return merchant
@pytest.fixture
def mp_store(db, mp_merchant):
"""Create a store for marketplace onboarding tests."""
uid = uuid.uuid4().hex[:8]
store = Store(
merchant_id=mp_merchant.id,
store_code=f"MPSTORE_{uid.upper()}",
subdomain=f"mpstore{uid.lower()}",
name="MP Test Store",
is_active=True,
is_verified=True,
)
db.add(store)
db.commit()
db.refresh(store)
return store
@pytest.mark.unit
@pytest.mark.marketplace
class TestMarketplaceOnboardingProvider:
"""Tests for MarketplaceOnboardingProvider."""
def setup_method(self):
self.provider = MarketplaceOnboardingProvider()
def test_category(self):
"""Returns 'marketplace' as the onboarding category."""
assert self.provider.onboarding_category == "marketplace"
def test_get_onboarding_steps_returns_two_steps(self):
"""Returns two steps: connect_api and import_products."""
steps = self.provider.get_onboarding_steps()
assert len(steps) == 2
assert steps[0].key == "marketplace.connect_api"
assert steps[0].order == 200
assert steps[1].key == "marketplace.import_products"
assert steps[1].order == 210
def test_connect_api_incomplete_without_credentials(self, db, mp_store):
"""connect_api step is incomplete when no credentials exist."""
result = self.provider.is_step_completed(
db, mp_store.id, "marketplace.connect_api"
)
assert result is False
def test_connect_api_complete_with_credentials(self, db, mp_store):
"""connect_api step is complete when StoreLetzshopCredentials has api_key."""
from app.modules.marketplace.models.letzshop import StoreLetzshopCredentials
creds = StoreLetzshopCredentials(
store_id=mp_store.id,
api_key_encrypted="encrypted_key_here",
)
db.add(creds)
db.commit()
result = self.provider.is_step_completed(
db, mp_store.id, "marketplace.connect_api"
)
assert result is True
def test_import_products_incomplete_without_products(self, db, mp_store):
"""import_products step is incomplete when store has no products."""
result = self.provider.is_step_completed(
db, mp_store.id, "marketplace.import_products"
)
assert result is False
def test_import_products_complete_with_products(self, db, mp_store):
"""import_products step is complete when store has at least one product."""
from app.modules.catalog.models.product import Product
product = Product(
store_id=mp_store.id,
store_sku=f"SKU-{uuid.uuid4().hex[:8]}",
is_active=True,
)
db.add(product)
db.commit()
result = self.provider.is_step_completed(
db, mp_store.id, "marketplace.import_products"
)
assert result is True
def test_unknown_step_key_returns_false(self, db, mp_store):
"""Returns False for unrecognized step key."""
result = self.provider.is_step_completed(
db, mp_store.id, "marketplace.unknown"
)
assert result is False