diff --git a/app/api/v1/platform/signup.py b/app/api/v1/platform/signup.py index 9cbb4edb..b7960f65 100644 --- a/app/api/v1/platform/signup.py +++ b/app/api/v1/platform/signup.py @@ -40,6 +40,7 @@ class SignupStartRequest(BaseModel): tier_code: str is_annual: bool = False platform_code: str + language: str = "fr" class SignupStartResponse(BaseModel): @@ -64,12 +65,14 @@ class CreateAccountRequest(BaseModel): class CreateAccountResponse(BaseModel): - """Response from account creation.""" + """Response from account creation (includes auto-created store).""" session_id: str user_id: int merchant_id: int stripe_customer_id: str + store_id: int + store_code: str class CreateStoreRequest(BaseModel): @@ -137,6 +140,7 @@ async def start_signup(request: SignupStartRequest) -> SignupStartResponse: tier_code=request.tier_code, is_annual=request.is_annual, platform_code=request.platform_code, + language=request.language, ) return SignupStartResponse( @@ -175,6 +179,8 @@ async def create_account( user_id=result.user_id, merchant_id=result.merchant_id, stripe_customer_id=result.stripe_customer_id, + store_id=result.store_id, + store_code=result.store_code, ) diff --git a/app/modules/base.py b/app/modules/base.py index 3d148de7..b86c80ec 100644 --- a/app/modules/base.py +++ b/app/modules/base.py @@ -49,6 +49,7 @@ if TYPE_CHECKING: from app.modules.contracts.cms import MediaUsageProviderProtocol from app.modules.contracts.features import FeatureProviderProtocol from app.modules.contracts.metrics import MetricsProviderProtocol + from app.modules.contracts.onboarding import OnboardingProviderProtocol from app.modules.contracts.widgets import DashboardWidgetProviderProtocol from app.modules.enums import FrontendType @@ -486,6 +487,29 @@ class ModuleDefinition: # to report where media is being used. media_usage_provider: "Callable[[], MediaUsageProviderProtocol] | None" = None + # ========================================================================= + # Onboarding Provider (Module-Driven Post-Signup Onboarding) + # ========================================================================= + # Callable that returns an OnboardingProviderProtocol implementation. + # Modules declare onboarding steps (what needs to be configured after signup) + # and provide completion checks. The core module's OnboardingAggregator + # discovers and aggregates all providers into a dashboard checklist banner. + # + # Example: + # def _get_onboarding_provider(): + # from app.modules.marketplace.services.marketplace_onboarding import ( + # marketplace_onboarding_provider, + # ) + # return marketplace_onboarding_provider + # + # marketplace_module = ModuleDefinition( + # code="marketplace", + # onboarding_provider=_get_onboarding_provider, + # ) + # + # The provider will be discovered by core's OnboardingAggregator service. + onboarding_provider: "Callable[[], OnboardingProviderProtocol] | None" = None + # ========================================================================= # Menu Item Methods (Legacy - uses menu_items dict of IDs) # ========================================================================= @@ -955,6 +979,24 @@ class ModuleDefinition: return None return self.media_usage_provider() + # ========================================================================= + # Onboarding Provider Methods + # ========================================================================= + + def has_onboarding_provider(self) -> bool: + """Check if this module has an onboarding provider.""" + return self.onboarding_provider is not None + + def get_onboarding_provider_instance(self) -> "OnboardingProviderProtocol | None": + """Get the onboarding provider instance for this module. + + Returns: + OnboardingProviderProtocol instance, or None + """ + if self.onboarding_provider is None: + return None + return self.onboarding_provider() + # ========================================================================= # Magic Methods # ========================================================================= diff --git a/app/modules/billing/routes/pages/platform.py b/app/modules/billing/routes/pages/platform.py index cdb157d4..44dae00a 100644 --- a/app/modules/billing/routes/pages/platform.py +++ b/app/modules/billing/routes/pages/platform.py @@ -114,14 +114,8 @@ async def signup_page( context["is_annual"] = annual context["tiers"] = _get_tiers_data(db, platform_id=platform.id) - # Route to platform-specific signup template - if platform.code == "loyalty": - template_name = "billing/platform/signup-loyalty.html" - else: - template_name = "billing/platform/signup.html" - return templates.TemplateResponse( - template_name, + "billing/platform/signup.html", context, ) diff --git a/app/modules/billing/services/signup_service.py b/app/modules/billing/services/signup_service.py index 63fa7560..03984fee 100644 --- a/app/modules/billing/services/signup_service.py +++ b/app/modules/billing/services/signup_service.py @@ -84,11 +84,13 @@ class SignupSessionData: @dataclass class AccountCreationResult: - """Result of account creation.""" + """Result of account creation (includes auto-created store).""" user_id: int merchant_id: int stripe_customer_id: str + store_id: int + store_code: str @dataclass @@ -131,6 +133,7 @@ class SignupService: tier_code: str, is_annual: bool, platform_code: str, + language: str = "fr", ) -> str: """ Create a new signup session. @@ -139,6 +142,7 @@ class SignupService: tier_code: The subscription tier code is_annual: Whether annual billing is selected platform_code: Platform code (e.g., 'loyalty', 'oms') + language: User's browsing language (from lang cookie) Returns: The session ID @@ -171,6 +175,7 @@ class SignupService: "tier_code": tier.value, "is_annual": is_annual, "platform_code": platform_code, + "language": language, "created_at": now, "updated_at": now, } @@ -304,10 +309,10 @@ class SignupService: phone: str | None = None, ) -> AccountCreationResult: """ - Create user and merchant accounts. + Create user, merchant, store, and subscription in a single atomic step. - Creates User + Merchant + Stripe Customer. Store creation is a - separate step (create_store) so each platform can customize it. + Creates User + Merchant + Store + Stripe Customer + MerchantSubscription. + Store name defaults to merchant_name, language from signup session. Args: db: Database session @@ -320,7 +325,7 @@ class SignupService: phone: Optional phone number Returns: - AccountCreationResult with user and merchant IDs + AccountCreationResult with user, merchant, and store IDs Raises: ResourceNotFoundException: If session not found @@ -338,7 +343,7 @@ class SignupService: username = self.generate_unique_username(db, email) # Create User - from app.modules.tenancy.models import Merchant, User + from app.modules.tenancy.models import Merchant, Store, User user = User( email=email, @@ -362,8 +367,7 @@ class SignupService: db.add(merchant) db.flush() - # Create Stripe Customer (linked to merchant, not store) - # We use a temporary store-like object for Stripe metadata + # Create Stripe Customer stripe_customer_id = stripe_service.create_customer_for_merchant( merchant=merchant, email=email, @@ -375,7 +379,38 @@ class SignupService: }, ) - db.commit() # SVC-006 - Atomic account creation needs commit + # Create Store (name = merchant_name, language from browsing session) + store_code = self.generate_unique_store_code(db, merchant_name) + subdomain = self.generate_unique_subdomain(db, merchant_name) + language = session.get("language", "fr") + + store = Store( + merchant_id=merchant.id, + store_code=store_code, + subdomain=subdomain, + name=merchant_name, + contact_email=email, + is_active=True, + ) + if language: + store.default_language = language + db.add(store) + db.flush() + + # Resolve platform and create subscription + platform_id = self._resolve_platform_id(db, session) + + subscription = sub_service.create_merchant_subscription( + db=db, + merchant_id=merchant.id, + platform_id=platform_id, + tier_code=session.get("tier_code", "essential"), + trial_days=settings.stripe_trial_days, + is_annual=session.get("is_annual", False), + ) + subscription.stripe_customer_id = stripe_customer_id + + db.commit() # SVC-006 - Atomic account + store creation # Update session self.update_session(session_id, { @@ -384,18 +419,24 @@ class SignupService: "merchant_name": merchant_name, "email": email, "stripe_customer_id": stripe_customer_id, + "store_id": store.id, + "store_code": store_code, + "platform_id": platform_id, "step": "account_created", }) logger.info( - f"Created account for {email}: " - f"user_id={user.id}, merchant_id={merchant.id}" + f"Created account + store for {email}: " + f"user_id={user.id}, merchant_id={merchant.id}, " + f"store_code={store_code}" ) return AccountCreationResult( user_id=user.id, merchant_id=merchant.id, stripe_customer_id=stripe_customer_id, + store_id=store.id, + store_code=store_code, ) # ========================================================================= @@ -512,7 +553,7 @@ class SignupService: Raises: ResourceNotFoundException: If session not found - ValidationException: If store not created yet + ValidationException: If account not created yet """ session = self.get_session_or_raise(session_id) @@ -523,12 +564,6 @@ class SignupService: field="session_id", ) - if not session.get("store_id"): - raise ValidationException( - message="Store not created. Please complete the store step first.", - field="session_id", - ) - # Create SetupIntent setup_intent = stripe_service.create_setup_intent( customer_id=stripe_customer_id, @@ -775,21 +810,11 @@ class SignupService: self, db: Session, session: dict, store_code: str ) -> str: """ - Determine redirect URL after signup based on platform. + Determine redirect URL after signup. - Marketplace platforms → onboarding wizard. - Other platforms (loyalty, etc.) → dashboard. + Always redirects to the store dashboard. Platform-specific onboarding + is handled by the dashboard's onboarding banner (module-driven). """ - from app.modules.service import module_service - - platform_id = session.get("platform_id") - if platform_id: - try: - if module_service.is_module_enabled(db, platform_id, "marketplace"): - return f"/store/{store_code}/onboarding" - except Exception: - pass # If check fails, default to dashboard - return f"/store/{store_code}/dashboard" diff --git a/app/modules/billing/templates/billing/platform/signup-loyalty.html b/app/modules/billing/templates/billing/platform/signup-loyalty.html deleted file mode 100644 index 204a3687..00000000 --- a/app/modules/billing/templates/billing/platform/signup-loyalty.html +++ /dev/null @@ -1,520 +0,0 @@ -{# app/templates/platform/signup-loyalty.html #} -{# Loyalty Platform Signup Wizard — 4 steps: Plan → Account → Store → Payment #} -{% extends "platform/base.html" %} - -{% block title %}Start Your Free Trial - RewardFlow{% endblock %} - -{% block extra_head %} -{# Stripe.js for payment #} - -{% endblock %} - -{% block content %} -
-
- - {# Progress Steps #} -
-
- -
-
- - {# Form Card #} -
- - {# =============================================================== - STEP 1: SELECT PLAN - =============================================================== #} -
-

Choose Your Plan

- - {# Billing Toggle #} -
- Monthly - - - Annual Save 17% - -
- - {# Tier Options #} -
- {% for tier in tiers %} - {% if not tier.is_enterprise %} - - {% endif %} - {% endfor %} -
- - {# Free Trial Note #} -
-

- {{ trial_days }}-day free trial. - We'll collect your payment info, but you won't be charged until the trial ends. -

-
- - -
- - {# =============================================================== - STEP 2: CREATE ACCOUNT - =============================================================== #} -
-

Create Your Account

-

- * Required fields -

- -
-
-
- - -
-
- - -
-
- -
- - -
- -
- - -
- -
- - -

Minimum 8 characters

-
- - -
- -
- - -
-
- - {# =============================================================== - STEP 3: SET UP STORE - =============================================================== #} -
-

Set Up Your Store

-

Your store is where your loyalty programs live. You can change these settings later.

- -
-
- - -

Defaults to your business name if left empty

-
- -
- - -
- - -
- -
- - -
-
- - {# =============================================================== - STEP 4: PAYMENT - =============================================================== #} -
-

Add Payment Method

-

You won't be charged until your {{ trial_days }}-day trial ends.

- - {# Stripe Card Element #} -
-
- -
- - -
-
- -
-
-
-{% endblock %} - -{% block extra_scripts %} - -{% endblock %} diff --git a/app/modules/billing/templates/billing/platform/signup.html b/app/modules/billing/templates/billing/platform/signup.html index 80a7004c..fb764574 100644 --- a/app/modules/billing/templates/billing/platform/signup.html +++ b/app/modules/billing/templates/billing/platform/signup.html @@ -1,5 +1,5 @@ {# app/templates/platform/signup.html #} -{# Multi-step Signup Wizard #} +{# 3-Step Signup Wizard: Plan → Account → Payment #} {% extends "platform/base.html" %} {% block title %}Start Your Free Trial - Orion{% endblock %} @@ -16,8 +16,8 @@ {# Progress Steps #}
-