fix(hosting): require merchant or prospect for site creation

- Schema: add merchant_id/prospect_id with model_validator requiring
  at least one. Remove from-prospect endpoint (unified into POST /sites)
- Service: rewrite create() — if merchant_id use it directly, if
  prospect_id auto-create merchant from prospect data. Remove system
  merchant hack entirely. Extract _create_merchant_from_prospect helper.
- Simplify accept_proposal() — merchant already exists at creation,
  only creates subscription and marks prospect converted
- Tests: update all create calls with merchant_id, replace from-prospect
  tests with prospect_id + validation tests

Closes docs/proposals/hosting-site-creation-fix.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-31 23:14:47 +02:00
parent 2bc03ed97c
commit 59b0d8977a
5 changed files with 112 additions and 124 deletions

View File

@@ -55,6 +55,7 @@ class TestHostedSiteService:
db,
{
"business_name": f"New Business {unique}",
"merchant_id": system_merchant.id,
"contact_email": f"test-{unique}@example.com",
},
)
@@ -72,6 +73,7 @@ class TestHostedSiteService:
db,
{
"business_name": f"Full Business {unique}",
"merchant_id": system_merchant.id,
"contact_name": "Jane Doe",
"contact_email": f"jane-{unique}@example.com",
"contact_phone": "+352 999 888",
@@ -251,75 +253,65 @@ class TestHostedSiteLifecycle:
@pytest.mark.unit
@pytest.mark.hosting
class TestHostedSiteFromProspect:
"""Tests for creating hosted sites from prospects."""
"""Tests for creating hosted sites from prospects via prospect_id."""
def setup_method(self):
self.service = HostedSiteService()
def test_create_from_prospect(self, db, hosting_platform, system_merchant):
"""Test creating a hosted site from a prospect."""
from app.modules.prospecting.models import Prospect
def test_create_with_prospect_id(self, db, hosting_platform):
"""Test creating a hosted site with prospect_id auto-creates merchant."""
from app.modules.prospecting.models import Prospect, ProspectContact
unique = uuid.uuid4().hex[:8]
prospect = Prospect(
channel="digital",
domain_name=f"prospect-{uuid.uuid4().hex[:8]}.lu",
domain_name=f"prospect-{unique}.lu",
business_name="Prospect Business",
status="active",
has_website=True,
)
db.add(prospect)
db.flush()
# Add email contact (needed for merchant creation)
db.add(ProspectContact(
prospect_id=prospect.id,
contact_type="email",
value=f"hello-{unique}@test.lu",
is_primary=True,
))
db.commit()
db.refresh(prospect)
site = self.service.create_from_prospect(db, prospect.id)
site = self.service.create(
db,
{
"business_name": "Prospect Business",
"prospect_id": prospect.id,
"contact_email": f"hello-{unique}@test.lu",
},
)
db.commit()
assert site.id is not None
assert site.prospect_id == prospect.id
assert site.business_name == "Prospect Business"
assert site.store.merchant is not None
def test_create_from_prospect_with_contacts(
self, db, hosting_platform, system_merchant
):
"""Test creating from prospect pre-fills contact info."""
from app.modules.prospecting.models import Prospect, ProspectContact
def test_create_without_merchant_or_prospect_raises(self, db, hosting_platform):
"""Test that creating without merchant_id or prospect_id raises ValueError."""
with pytest.raises(ValueError, match="Either merchant_id or prospect_id"):
self.service.create(
db,
{"business_name": "No Owner"},
)
prospect = Prospect(
channel="digital",
domain_name=f"contacts-{uuid.uuid4().hex[:8]}.lu",
business_name="Contact Business",
status="active",
)
db.add(prospect)
db.flush()
email_contact = ProspectContact(
prospect_id=prospect.id,
contact_type="email",
value="hello@test.lu",
is_primary=True,
)
phone_contact = ProspectContact(
prospect_id=prospect.id,
contact_type="phone",
value="+352 111 222",
is_primary=True,
)
db.add_all([email_contact, phone_contact])
db.commit()
db.refresh(prospect)
site = self.service.create_from_prospect(db, prospect.id)
db.commit()
assert site.contact_email == "hello@test.lu"
assert site.contact_phone == "+352 111 222"
def test_create_from_nonexistent_prospect(
self, db, hosting_platform, system_merchant
):
"""Test creating from non-existent prospect raises exception."""
def test_create_with_nonexistent_prospect_raises(self, db, hosting_platform):
"""Test creating with non-existent prospect raises exception."""
from app.modules.prospecting.exceptions import ProspectNotFoundException
with pytest.raises(ProspectNotFoundException):
self.service.create_from_prospect(db, 99999)
self.service.create(
db,
{"business_name": "Ghost", "prospect_id": 99999},
)