diff --git a/app/modules/prospecting/routes/api/admin_campaigns.py b/app/modules/prospecting/routes/api/admin_campaigns.py
index 6540faf7..1f312177 100644
--- a/app/modules/prospecting/routes/api/admin_campaigns.py
+++ b/app/modules/prospecting/routes/api/admin_campaigns.py
@@ -99,7 +99,7 @@ def send_campaign(
db,
template_id=data.template_id,
prospect_ids=data.prospect_ids,
- sent_by_user_id=current_admin.user_id,
+ sent_by_user_id=current_admin.id,
)
db.commit()
return [CampaignSendResponse.model_validate(s) for s in sends]
diff --git a/app/modules/prospecting/routes/api/admin_interactions.py b/app/modules/prospecting/routes/api/admin_interactions.py
index ca8c7902..124e062d 100644
--- a/app/modules/prospecting/routes/api/admin_interactions.py
+++ b/app/modules/prospecting/routes/api/admin_interactions.py
@@ -48,7 +48,7 @@ def create_interaction(
interaction = interaction_service.create(
db,
prospect_id=prospect_id,
- user_id=current_admin.user_id,
+ user_id=current_admin.id,
data=data.model_dump(exclude_none=True),
)
db.commit()
diff --git a/app/modules/prospecting/routes/api/admin_prospects.py b/app/modules/prospecting/routes/api/admin_prospects.py
index ae649d94..2fd76d36 100644
--- a/app/modules/prospecting/routes/api/admin_prospects.py
+++ b/app/modules/prospecting/routes/api/admin_prospects.py
@@ -80,7 +80,7 @@ def create_prospect(
prospect = prospect_service.create(
db,
data.model_dump(exclude_none=True),
- captured_by_user_id=current_admin.user_id,
+ captured_by_user_id=current_admin.id,
)
db.commit()
return _to_response(prospect)
diff --git a/app/modules/prospecting/schemas/score.py b/app/modules/prospecting/schemas/score.py
index a184206f..6e16d8cf 100644
--- a/app/modules/prospecting/schemas/score.py
+++ b/app/modules/prospecting/schemas/score.py
@@ -1,9 +1,10 @@
# app/modules/prospecting/schemas/score.py
"""Pydantic schemas for opportunity scoring."""
+import json
from datetime import datetime
-from pydantic import BaseModel
+from pydantic import BaseModel, field_validator
class ProspectScoreResponse(BaseModel):
@@ -23,5 +24,19 @@ class ProspectScoreResponse(BaseModel):
created_at: datetime
updated_at: datetime
+ @field_validator("reason_flags", mode="before")
+ @classmethod
+ def parse_reason_flags(cls, v):
+ if isinstance(v, str):
+ return json.loads(v)
+ return v
+
+ @field_validator("score_breakdown", mode="before")
+ @classmethod
+ def parse_score_breakdown(cls, v):
+ if isinstance(v, str):
+ return json.loads(v)
+ return v
+
class Config:
from_attributes = True
diff --git a/app/modules/prospecting/templates/prospecting/admin/prospect-detail.html b/app/modules/prospecting/templates/prospecting/admin/prospect-detail.html
index 44282293..01895892 100644
--- a/app/modules/prospecting/templates/prospecting/admin/prospect-detail.html
+++ b/app/modules/prospecting/templates/prospecting/admin/prospect-detail.html
@@ -11,7 +11,8 @@
{{ loading_state('Loading prospect...') }}
{{ error_state('Error loading prospect') }}
-
+
+
@@ -215,6 +216,7 @@
No campaigns sent yet
+
{% call modal('interactionModal', 'Log Interaction', show_var='showInteractionModal', size='md', show_footer=false) %}
diff --git a/app/modules/tenancy/services/store_team_service.py b/app/modules/tenancy/services/store_team_service.py
index 0fb2ea4d..3f0cbc4b 100644
--- a/app/modules/tenancy/services/store_team_service.py
+++ b/app/modules/tenancy/services/store_team_service.py
@@ -980,9 +980,13 @@ class StoreTeamService:
role_name: str,
):
"""Send team invitation email."""
+ from app.core.config import settings as app_settings
from app.modules.messaging.services.email_service import EmailService
- acceptance_link = f"/store/invitation/accept?token={token}"
+ domain = app_settings.main_domain.rstrip("/")
+ scheme = "http" if "localhost" in domain else "https"
+ base_url = f"{scheme}://{domain}" if "://" not in domain else domain
+ acceptance_link = f"{base_url}/store/invitation/accept?token={token}"
email_service = EmailService(db)
email_service.send_template(
diff --git a/docs/proposals/hosting-site-creation-fix.md b/docs/proposals/hosting-site-creation-fix.md
new file mode 100644
index 00000000..3f9b30ce
--- /dev/null
+++ b/docs/proposals/hosting-site-creation-fix.md
@@ -0,0 +1,73 @@
+# Hosting Site Creation — Require Merchant or Prospect
+
+## Problem
+
+`hosted_site_service.create()` (lines 118-131) auto-creates a fake "HostWizard System" `Merchant` with no `owner_user_id` as a placeholder to satisfy the Store's `merchant_id` FK. This is wrong on two levels:
+
+- Crashes because `Merchant.owner_user_id` is `nullable=False`
+- Conceptually wrong — POC sites should belong to a real entity
+
+## Current Flow
+
+```
+create() → invents a system merchant (broken)
+ → creates Store under it
+ → creates HostedSite
+
+create_from_prospect() → reads prospect data
+ → calls create() (same broken path)
+
+accept_proposal() → creates real merchant
+ → reassigns store to it
+```
+
+## Proposed Design: Require merchant_id OR prospect_id
+
+### Schema (`HostedSiteCreate`)
+
+- Add `merchant_id: int | None` and `prospect_id: int | None`
+- Add `model_validator`: at least one must be provided
+- Remove `prospect_id` from the separate URL path endpoint — unify into one create endpoint
+
+### Service (`create`)
+
+- **If `merchant_id` provided**: validate merchant exists, create Store under it directly
+- **If `prospect_id` provided** (no merchant): auto-create a merchant from prospect data using `merchant_service.create_merchant_with_owner()` (same as `accept_proposal` does today), then create Store under it
+- **If both provided**: use the existing merchant, link the prospect
+- Remove the system merchant creation entirely (lines 118-131)
+
+### `create_from_prospect` method
+
+Can be removed — its logic merges into `create` via the `prospect_id` field.
+
+### `accept_proposal`
+
+Simplify — merchant already exists at this point, so it only needs to handle subscription creation and prospect status update. The store reassignment is no longer needed.
+
+## Files to Change
+
+| File | Change |
+|---|---|
+| `hosting/schemas/hosted_site.py` | Add `merchant_id`, `prospect_id` fields + validator |
+| `hosting/services/hosted_site_service.py` | Rewrite `create()` to use provided merchant or create from prospect. Remove `create_from_prospect()`. Simplify `accept_proposal()`. |
+| `hosting/routes/api/admin_sites.py` | Remove `/from-prospect/{prospect_id}` endpoint. The main `POST /sites` handles both paths now. |
+| `hosting/templates/hosting/admin/site-new.html` | Add merchant selector (dropdown/autocomplete) and prospect selector. Validate one is chosen. |
+| `hosting/tests/conftest.py` | Update `hosted_site` fixture to pass `merchant_id` |
+| `hosting/tests/unit/test_hosted_site_service.py` | Update all `create` calls, remove `TestHostedSiteFromProspect` class (merge into main tests), add validation tests for the "at least one required" rule |
+
+## Template Changes Needed
+
+The form currently has a business name + contact fields + a prospect ID input at the bottom. It needs:
+
+1. A **merchant selector** (autocomplete dropdown searching existing merchants)
+2. A **prospect selector** (autocomplete dropdown searching existing prospects)
+3. Validation: at least one must be selected
+4. When prospect is selected: auto-fill business_name, contact fields from prospect data
+5. When merchant is selected: auto-fill business_name from merchant name
+
+## What Gets Deleted
+
+- The entire "HostWizard System" merchant pattern (lines 118-131 in service)
+- The `POST /sites/from-prospect/{prospect_id}` endpoint
+- The `create_from_prospect()` service method
+- The prospect ID number input in the template (replaced by proper selector)
diff --git a/static/shared/js/dev-toolbar.js b/static/shared/js/dev-toolbar.js
index 7224794e..f498cf45 100644
--- a/static/shared/js/dev-toolbar.js
+++ b/static/shared/js/dev-toolbar.js
@@ -223,6 +223,30 @@
};
});
+ // Capture uncaught errors and unhandled rejections
+ window.addEventListener('error', function (e) {
+ var msg = e.message || 'Unknown error';
+ if (e.filename) msg += ' at ' + e.filename.split('/').pop() + ':' + e.lineno;
+ consoleLogs.push({
+ timestamp: Date.now(),
+ level: 'error',
+ args: [msg],
+ });
+ if (consoleLogs.length > MAX_CONSOLE_LOGS) consoleLogs.shift();
+ refreshIfActive('console');
+ });
+
+ window.addEventListener('unhandledrejection', function (e) {
+ var msg = e.reason ? (e.reason.message || String(e.reason)) : 'Unhandled promise rejection';
+ consoleLogs.push({
+ timestamp: Date.now(),
+ level: 'error',
+ args: [msg],
+ });
+ if (consoleLogs.length > MAX_CONSOLE_LOGS) consoleLogs.shift();
+ refreshIfActive('console');
+ });
+
// ── Refresh helper ──
function refreshIfActive(tab) {
if (isExpanded && activeTab === tab && contentEl) {