# app/modules/billing/tests/unit/test_signup_service.py """Unit tests for SignupService (simplified 3-step signup).""" import uuid from unittest.mock import patch import pytest from app.exceptions import ( ConflictException, ResourceNotFoundException, ValidationException, ) from app.modules.billing.models import TierCode from app.modules.billing.services.signup_service import SignupService, _signup_sessions @pytest.fixture(autouse=True) def _clear_sessions(): """Clear in-memory signup sessions before each test.""" _signup_sessions.clear() yield _signup_sessions.clear() @pytest.mark.unit @pytest.mark.billing class TestSignupServiceSession: """Tests for SignupService session management.""" def setup_method(self): self.service = SignupService() def test_create_session_stores_language(self): """create_session stores the user's browsing language.""" session_id = self.service.create_session( tier_code=TierCode.ESSENTIAL.value, is_annual=False, platform_code="loyalty", language="de", ) session = self.service.get_session(session_id) assert session is not None assert session["language"] == "de" def test_create_session_default_language_fr(self): """create_session defaults to French when no language provided.""" session_id = self.service.create_session( tier_code=TierCode.ESSENTIAL.value, is_annual=False, platform_code="oms", ) session = self.service.get_session(session_id) assert session["language"] == "fr" def test_create_session_stores_platform_code(self): """create_session stores the platform code.""" session_id = self.service.create_session( tier_code=TierCode.PROFESSIONAL.value, is_annual=True, platform_code="loyalty", language="en", ) session = self.service.get_session(session_id) assert session["platform_code"] == "loyalty" assert session["is_annual"] is True assert session["tier_code"] == TierCode.PROFESSIONAL.value def test_create_session_raises_without_platform_code(self): """create_session raises ValidationException when platform_code is empty.""" with pytest.raises(ValidationException): self.service.create_session( tier_code=TierCode.ESSENTIAL.value, is_annual=False, platform_code="", ) def test_get_session_or_raise_missing(self): """get_session_or_raise raises ResourceNotFoundException for invalid session.""" with pytest.raises(ResourceNotFoundException): self.service.get_session_or_raise("nonexistent_session_id") def test_delete_session(self): """delete_session removes the session from storage.""" session_id = self.service.create_session( tier_code=TierCode.ESSENTIAL.value, is_annual=False, platform_code="oms", ) assert self.service.get_session(session_id) is not None self.service.delete_session(session_id) assert self.service.get_session(session_id) is None @pytest.mark.unit @pytest.mark.billing class TestSignupServiceAccountCreation: """Tests for SignupService.create_account (merged store creation).""" def setup_method(self): self.service = SignupService() def test_create_account_creates_store(self, db): """create_account creates User + Merchant + Store atomically.""" from app.modules.billing.models import SubscriptionTier from app.modules.tenancy.models import Platform # Create platform platform = Platform( code=f"test_{uuid.uuid4().hex[:8]}", name="Test Platform", is_active=True, ) db.add(platform) db.commit() # Create a tier for the platform tier = SubscriptionTier( code=TierCode.ESSENTIAL.value, name="Essential", platform_id=platform.id, price_monthly_cents=0, display_order=1, is_active=True, is_public=True, ) db.add(tier) db.commit() # Create session session_id = self.service.create_session( tier_code=TierCode.ESSENTIAL.value, is_annual=False, platform_code=platform.code, language="de", ) # Mock Stripe with patch( "app.modules.billing.services.signup_service.stripe_service" ) as mock_stripe: mock_stripe.create_customer_for_merchant.return_value = "cus_test123" result = self.service.create_account( db=db, session_id=session_id, email=f"test_{uuid.uuid4().hex[:8]}@example.com", password="securepass123", # noqa: SEC-001 # noqa: SEC-001 first_name="John", last_name="Doe", merchant_name="John's Shop", ) # Verify result includes store info assert result.user_id is not None assert result.merchant_id is not None assert result.store_id is not None assert result.store_code is not None assert result.stripe_customer_id == "cus_test123" # Verify store was created with correct language from app.modules.tenancy.models import Store store = db.query(Store).filter(Store.id == result.store_id).first() assert store is not None assert store.name == "John's Shop" assert store.default_language == "de" def test_create_account_uses_session_language(self, db): """create_account sets store default_language from the signup session.""" from app.modules.billing.models import SubscriptionTier from app.modules.tenancy.models import Platform platform = Platform( code=f"test_{uuid.uuid4().hex[:8]}", name="Test Platform", is_active=True, ) db.add(platform) db.commit() tier = SubscriptionTier( code=TierCode.ESSENTIAL.value, name="Essential", platform_id=platform.id, price_monthly_cents=0, display_order=1, is_active=True, is_public=True, ) db.add(tier) db.commit() session_id = self.service.create_session( tier_code=TierCode.ESSENTIAL.value, is_annual=False, platform_code=platform.code, language="en", ) with patch( "app.modules.billing.services.signup_service.stripe_service" ) as mock_stripe: mock_stripe.create_customer_for_merchant.return_value = "cus_test456" result = self.service.create_account( db=db, session_id=session_id, email=f"test_{uuid.uuid4().hex[:8]}@example.com", password="securepass123", # noqa: SEC-001 first_name="Jane", last_name="Smith", merchant_name="Jane's Bakery", ) from app.modules.tenancy.models import Store store = db.query(Store).filter(Store.id == result.store_id).first() assert store.default_language == "en" def test_create_account_rejects_duplicate_email(self, db): """create_account raises ConflictException for existing email.""" from app.modules.billing.models import SubscriptionTier from app.modules.tenancy.models import Platform platform = Platform( code=f"test_{uuid.uuid4().hex[:8]}", name="Test Platform", is_active=True, ) db.add(platform) db.commit() tier = SubscriptionTier( code=TierCode.ESSENTIAL.value, name="Essential", platform_id=platform.id, price_monthly_cents=0, display_order=1, is_active=True, is_public=True, ) db.add(tier) db.commit() email = f"dup_{uuid.uuid4().hex[:8]}@example.com" # Create first account session_id1 = self.service.create_session( tier_code=TierCode.ESSENTIAL.value, is_annual=False, platform_code=platform.code, ) with patch( "app.modules.billing.services.signup_service.stripe_service" ) as mock_stripe: mock_stripe.create_customer_for_merchant.return_value = "cus_first" self.service.create_account( db=db, session_id=session_id1, email=email, password="securepass123", # noqa: SEC-001 first_name="First", last_name="User", merchant_name="First Shop", ) # Try to create second account with same email session_id2 = self.service.create_session( tier_code=TierCode.ESSENTIAL.value, is_annual=False, platform_code=platform.code, ) with pytest.raises(ConflictException): self.service.create_account( db=db, session_id=session_id2, email=email, password="securepass123", # noqa: SEC-001 first_name="Second", last_name="User", merchant_name="Second Shop", ) def test_create_account_updates_session(self, db): """create_account updates session with user/merchant/store IDs.""" from app.modules.billing.models import SubscriptionTier from app.modules.tenancy.models import Platform platform = Platform( code=f"test_{uuid.uuid4().hex[:8]}", name="Test Platform", is_active=True, ) db.add(platform) db.commit() tier = SubscriptionTier( code=TierCode.ESSENTIAL.value, name="Essential", platform_id=platform.id, price_monthly_cents=0, display_order=1, is_active=True, is_public=True, ) db.add(tier) db.commit() session_id = self.service.create_session( tier_code=TierCode.ESSENTIAL.value, is_annual=False, platform_code=platform.code, ) with patch( "app.modules.billing.services.signup_service.stripe_service" ) as mock_stripe: mock_stripe.create_customer_for_merchant.return_value = "cus_test789" result = self.service.create_account( db=db, session_id=session_id, email=f"test_{uuid.uuid4().hex[:8]}@example.com", password="securepass123", # noqa: SEC-001 first_name="Test", last_name="User", merchant_name="Test Shop", ) session = self.service.get_session(session_id) assert session["user_id"] == result.user_id assert session["merchant_id"] == result.merchant_id assert session["store_id"] == result.store_id assert session["store_code"] == result.store_code assert session["stripe_customer_id"] == "cus_test789" assert session["step"] == "account_created" @pytest.mark.unit @pytest.mark.billing class TestSignupServicePostRedirect: """Tests for SignupService._get_post_signup_redirect.""" def setup_method(self): self.service = SignupService() def test_always_redirects_to_dashboard(self, db): """Post-signup redirect always goes to store dashboard.""" session = {"platform_code": "loyalty"} url = self.service._get_post_signup_redirect(db, session, "MY_STORE") assert url == "/store/MY_STORE/dashboard" def test_redirect_for_oms_platform(self, db): """OMS platform also redirects to dashboard (not onboarding wizard).""" session = {"platform_code": "oms"} url = self.service._get_post_signup_redirect(db, session, "OMS_STORE") assert url == "/store/OMS_STORE/dashboard"