feat(subscriptions): migrate subscription management to merchant level and seed tiers
Move subscription create/edit from store detail (broken endpoint) to merchant detail page with proper modal UI. Seed 4 subscription tiers (Essential, Professional, Business, Enterprise) in init_production.py. Also includes cross-module dependency declarations, store domain platform_id migration, platform context middleware, CMS route fixes, and migration backups. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,9 +9,11 @@ Usage:
|
||||
python scripts/show_urls.py # Show all URLs
|
||||
python scripts/show_urls.py --dev # Development URLs only
|
||||
python scripts/show_urls.py --prod # Production URLs only
|
||||
python scripts/show_urls.py --check # Check dev URLs with curl
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from sqlalchemy import text
|
||||
@@ -60,11 +62,107 @@ def get_store_domains(db):
|
||||
).fetchall()
|
||||
|
||||
|
||||
def get_store_platform_map(db):
|
||||
"""
|
||||
Get store-to-platform mapping.
|
||||
|
||||
Returns dict: store_id -> list of platform codes.
|
||||
Uses store_platforms junction table.
|
||||
"""
|
||||
rows = db.execute(
|
||||
text(
|
||||
"SELECT sp.store_id, p.code AS platform_code "
|
||||
"FROM store_platforms sp "
|
||||
"JOIN platforms p ON p.id = sp.platform_id "
|
||||
"WHERE sp.is_active = true "
|
||||
"ORDER BY sp.store_id, sp.is_primary DESC"
|
||||
)
|
||||
).fetchall()
|
||||
|
||||
mapping = {}
|
||||
for r in rows:
|
||||
mapping.setdefault(r.store_id, []).append(r.platform_code)
|
||||
return mapping
|
||||
|
||||
|
||||
def status_badge(is_active):
|
||||
return "active" if is_active else "INACTIVE"
|
||||
|
||||
|
||||
def print_dev_urls(platforms, stores, store_domains):
|
||||
def _store_dev_dashboard_url(store_code, platform_code=None):
|
||||
"""Build store dashboard URL for dev mode."""
|
||||
if platform_code and platform_code != "main":
|
||||
return f"{DEV_BASE}/platforms/{platform_code}/store/{store_code}/"
|
||||
return f"{DEV_BASE}/store/{store_code}/"
|
||||
|
||||
|
||||
def _store_dev_login_url(store_code, platform_code=None):
|
||||
"""Build store login URL for dev mode."""
|
||||
if platform_code and platform_code != "main":
|
||||
return f"{DEV_BASE}/platforms/{platform_code}/store/{store_code}/login"
|
||||
return f"{DEV_BASE}/store/{store_code}/login"
|
||||
|
||||
|
||||
def collect_dev_urls(platforms, stores, store_domains, store_platform_map):
|
||||
"""
|
||||
Collect all dev URLs with labels and expected status codes.
|
||||
|
||||
Returns list of (label, url, expected_codes) tuples.
|
||||
"""
|
||||
urls = []
|
||||
|
||||
# Admin
|
||||
urls.append(("Admin Login", f"{DEV_BASE}/admin/login", [200]))
|
||||
urls.append(("Admin Dashboard", f"{DEV_BASE}/admin/", [200, 302]))
|
||||
urls.append(("API Docs", f"{DEV_BASE}/docs", [200]))
|
||||
urls.append(("Health", f"{DEV_BASE}/health", [200]))
|
||||
|
||||
# Platforms
|
||||
for p in platforms:
|
||||
if not p.is_active:
|
||||
continue
|
||||
if p.code == "main":
|
||||
urls.append((f"Platform: {p.name}", f"{DEV_BASE}/", [200]))
|
||||
else:
|
||||
urls.append((f"Platform: {p.name}", f"{DEV_BASE}/platforms/{p.code}/", [200]))
|
||||
|
||||
# Store dashboards (redirect to login when unauthenticated)
|
||||
for v in stores:
|
||||
if not v.is_active:
|
||||
continue
|
||||
platform_codes = store_platform_map.get(v.id, [])
|
||||
if platform_codes:
|
||||
for pc in platform_codes:
|
||||
url = _store_dev_login_url(v.store_code, pc)
|
||||
urls.append((f"Store Login: {v.name} ({pc})", url, [200]))
|
||||
else:
|
||||
url = _store_dev_login_url(v.store_code)
|
||||
urls.append((f"Store Login: {v.name}", url, [200]))
|
||||
|
||||
# Storefronts
|
||||
for v in stores:
|
||||
if not v.is_active:
|
||||
continue
|
||||
urls.append((
|
||||
f"Storefront: {v.name}",
|
||||
f"{DEV_BASE}/stores/{v.store_code}/storefront/",
|
||||
[200, 302],
|
||||
))
|
||||
|
||||
# Store info API (public, no auth needed)
|
||||
for v in stores:
|
||||
if not v.is_active:
|
||||
continue
|
||||
urls.append((
|
||||
f"Store API: {v.name}",
|
||||
f"{DEV_BASE}/api/v1/store/info/{v.store_code}",
|
||||
[200],
|
||||
))
|
||||
|
||||
return urls
|
||||
|
||||
|
||||
def print_dev_urls(platforms, stores, store_domains, store_platform_map):
|
||||
"""Print all development URLs."""
|
||||
print()
|
||||
print("DEVELOPMENT URLS")
|
||||
@@ -84,24 +182,16 @@ def print_dev_urls(platforms, stores, store_domains):
|
||||
print(" PLATFORMS")
|
||||
for p in platforms:
|
||||
tag = f" [{status_badge(p.is_active)}]" if not p.is_active else ""
|
||||
prefix = p.path_prefix or ""
|
||||
if p.code == "main":
|
||||
print(f" {p.name}{tag}")
|
||||
print(f" Home: {DEV_BASE}/")
|
||||
else:
|
||||
print(f" {p.name} ({p.code}){tag}")
|
||||
if prefix:
|
||||
print(f" Home: {DEV_BASE}/platforms/{p.code}/")
|
||||
else:
|
||||
print(f" Home: {DEV_BASE}/platforms/{p.code}/")
|
||||
print(f" Home: {DEV_BASE}/platforms/{p.code}/")
|
||||
|
||||
# Stores
|
||||
print()
|
||||
print(" STORE DASHBOARDS")
|
||||
domains_by_store = {}
|
||||
for vd in store_domains:
|
||||
domains_by_store.setdefault(vd.store_id, []).append(vd)
|
||||
|
||||
current_merchant = None
|
||||
for v in stores:
|
||||
if v.merchant_name != current_merchant:
|
||||
@@ -110,9 +200,19 @@ def print_dev_urls(platforms, stores, store_domains):
|
||||
|
||||
tag = f" [{status_badge(v.is_active)}]" if not v.is_active else ""
|
||||
code = v.store_code
|
||||
|
||||
# Get platform(s) for this store
|
||||
platform_codes = store_platform_map.get(v.id, [])
|
||||
|
||||
print(f" {v.name} ({code}){tag}")
|
||||
print(f" Dashboard: {DEV_BASE}/store/{code}/")
|
||||
print(f" API: {DEV_BASE}/api/v1/store/{code}/")
|
||||
if platform_codes:
|
||||
for pc in platform_codes:
|
||||
print(f" Login: {_store_dev_login_url(code, pc)}")
|
||||
print(f" Dashboard: {_store_dev_dashboard_url(code, pc)}")
|
||||
else:
|
||||
print(f" Login: {_store_dev_login_url(code)}")
|
||||
print(f" Dashboard: {_store_dev_dashboard_url(code)}")
|
||||
print(f" (!) No platform assigned - use /platforms/{{code}}/store/{code}/ for platform context")
|
||||
|
||||
# Storefronts
|
||||
print()
|
||||
@@ -208,20 +308,81 @@ def print_prod_urls(platforms, stores, store_domains):
|
||||
print(f" Custom: https://{vd.domain}/{suffix}")
|
||||
|
||||
|
||||
def check_dev_urls(platforms, stores, store_domains, store_platform_map):
|
||||
"""Curl all dev URLs and report reachability."""
|
||||
urls = collect_dev_urls(platforms, stores, store_domains, store_platform_map)
|
||||
|
||||
print()
|
||||
print("URL HEALTH CHECK")
|
||||
print(f"Base: {DEV_BASE}")
|
||||
print(SEPARATOR)
|
||||
print()
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
errors = []
|
||||
|
||||
for label, url, expected_codes in urls:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", "-L", "--max-time", "5", url],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
status = int(result.stdout.strip())
|
||||
|
||||
if status in expected_codes:
|
||||
print(f" PASS {status} {label}")
|
||||
print(f" {url}")
|
||||
passed += 1
|
||||
else:
|
||||
expected_str = "/".join(str(c) for c in expected_codes)
|
||||
print(f" FAIL {status} {label} (expected {expected_str})")
|
||||
print(f" {url}")
|
||||
failed += 1
|
||||
errors.append((label, url, status, expected_codes))
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError, ValueError) as e:
|
||||
print(f" ERR --- {label} ({e})")
|
||||
print(f" {url}")
|
||||
failed += 1
|
||||
errors.append((label, url, 0, expected_codes))
|
||||
|
||||
# Summary
|
||||
print()
|
||||
print(SEPARATOR)
|
||||
total = passed + failed
|
||||
print(f" Results: {passed}/{total} passed", end="")
|
||||
if failed:
|
||||
print(f", {failed} failed")
|
||||
else:
|
||||
print()
|
||||
|
||||
if errors:
|
||||
print()
|
||||
print(" Failed URLs:")
|
||||
for label, url, status, expected in errors:
|
||||
expected_str = "/".join(str(c) for c in expected)
|
||||
print(f" [{status or 'ERR'}] {url} (expected {expected_str})")
|
||||
|
||||
print()
|
||||
return failed == 0
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Show all platform URLs")
|
||||
parser.add_argument("--dev", action="store_true", help="Development URLs only")
|
||||
parser.add_argument("--prod", action="store_true", help="Production URLs only")
|
||||
parser.add_argument("--check", action="store_true", help="Check dev URLs with curl")
|
||||
args = parser.parse_args()
|
||||
|
||||
show_dev = args.dev or (not args.dev and not args.prod)
|
||||
show_prod = args.prod or (not args.dev and not args.prod)
|
||||
show_dev = args.dev or (not args.dev and not args.prod and not args.check)
|
||||
show_prod = args.prod or (not args.dev and not args.prod and not args.check)
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
platforms = get_platforms(db)
|
||||
stores = get_stores(db)
|
||||
store_domains = get_store_domains(db)
|
||||
store_platform_map = get_store_platform_map(db)
|
||||
except Exception as e:
|
||||
print(f"Error querying database: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -234,8 +395,12 @@ def main():
|
||||
print(f" {len(platforms)} platform(s), {len(stores)} store(s), {len(store_domains)} custom domain(s)")
|
||||
print("=" * 72)
|
||||
|
||||
if args.check:
|
||||
success = check_dev_urls(platforms, stores, store_domains, store_platform_map)
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
if show_dev:
|
||||
print_dev_urls(platforms, stores, store_domains)
|
||||
print_dev_urls(platforms, stores, store_domains, store_platform_map)
|
||||
|
||||
if show_prod:
|
||||
print_prod_urls(platforms, stores, store_domains)
|
||||
|
||||
Reference in New Issue
Block a user