feat: add Phase 2 migration, make urls command, fix seed script

- Create loyalty_003 migration: company-based architecture (adds
  company_id to all loyalty tables, creates company_loyalty_settings,
  renames vendor_id to enrolled_at_vendor_id on cards)
- Move platform migration back to alembic/versions (not loyalty-specific)
- Add version_locations to alembic.ini for module migration discovery
- Add make urls/urls-dev/urls-prod commands (scripts/show_urls.py)
- Fix seed_demo.py: import all module models to resolve SQLAlchemy
  string relationships, fix multiple admin query, set platform_id
  on vendor content pages
- Fix loyalty test fixtures to match Phase 2 model columns

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-06 20:56:07 +01:00
parent 994c6419f0
commit 922616c9e3
7 changed files with 861 additions and 29 deletions

View File

@@ -1,7 +1,7 @@
# Wizamart Multi-Tenant E-Commerce Platform Makefile
# Cross-platform compatible (Windows & Linux)
.PHONY: install install-dev install-docs install-all dev test test-coverage lint format check docker-build docker-up docker-down clean help tailwind-install tailwind-dev tailwind-build tailwind-watch arch-check arch-check-file arch-check-object test-db-up test-db-down test-db-reset test-db-status celery-worker celery-beat celery-dev flower celery-status celery-purge
.PHONY: install install-dev install-docs install-all dev test test-coverage lint format check docker-build docker-up docker-down clean help tailwind-install tailwind-dev tailwind-build tailwind-watch arch-check arch-check-file arch-check-object test-db-up test-db-down test-db-reset test-db-status celery-worker celery-beat celery-dev flower celery-status celery-purge urls
# Detect OS
ifeq ($(OS),Windows_NT)
@@ -477,6 +477,15 @@ verify-setup:
@echo "Running setup verification..."
@$(PYTHON) scripts/verify_setup.py
urls:
@$(PYTHON) scripts/show_urls.py
urls-dev:
@$(PYTHON) scripts/show_urls.py --dev
urls-prod:
@$(PYTHON) scripts/show_urls.py --prod
check-env:
@echo "Checking Python environment..."
@echo "Detected OS: $(DETECTED_OS)"
@@ -572,6 +581,9 @@ help:
@echo " docker-down - Stop Docker containers"
@echo ""
@echo "=== UTILITIES ==="
@echo " urls - Show all platform/vendor/storefront URLs"
@echo " urls-dev - Show development URLs only"
@echo " urls-prod - Show production URLs only"
@echo " clean - Clean build artifacts"
@echo " check-env - Check Python environment and OS"
@echo ""

View File

@@ -2,7 +2,8 @@
[alembic]
script_location = alembic
prepend_sys_path = .
version_path_separator = os
version_path_separator = space
version_locations = alembic/versions app/modules/loyalty/migrations/versions
# This will be overridden by alembic\env.py using settings.database_url
sqlalchemy.url =
# for PROD: sqlalchemy.url = postgresql://username:password@localhost:5432/ecommerce_db

View File

@@ -0,0 +1,560 @@
"""Phase 2: migrate loyalty module to company-based architecture
Revision ID: loyalty_003_phase2
Revises: 0fb5d6d6ff97
Create Date: 2026-02-06 20:30:00.000000
Phase 2 changes:
- loyalty_programs: vendor_id -> company_id (one program per company)
- loyalty_cards: add company_id, rename vendor_id -> enrolled_at_vendor_id
- loyalty_transactions: add company_id, add related_transaction_id, vendor_id nullable
- staff_pins: add company_id
- NEW TABLE: company_loyalty_settings
- NEW COLUMNS on loyalty_programs: points_expiration_days, welcome_bonus_points,
minimum_redemption_points, minimum_purchase_cents, tier_config
- NEW COLUMN on loyalty_cards: last_activity_at
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "loyalty_003_phase2"
down_revision: Union[str, None] = "0fb5d6d6ff97"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# =========================================================================
# 1. Create company_loyalty_settings table
# =========================================================================
op.create_table(
"company_loyalty_settings",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("company_id", sa.Integer(), nullable=False),
sa.Column(
"staff_pin_policy",
sa.String(length=20),
nullable=False,
server_default="required",
),
sa.Column(
"staff_pin_lockout_attempts",
sa.Integer(),
nullable=False,
server_default="5",
),
sa.Column(
"staff_pin_lockout_minutes",
sa.Integer(),
nullable=False,
server_default="30",
),
sa.Column(
"allow_self_enrollment",
sa.Boolean(),
nullable=False,
server_default=sa.text("true"),
),
sa.Column(
"allow_void_transactions",
sa.Boolean(),
nullable=False,
server_default=sa.text("true"),
),
sa.Column(
"allow_cross_location_redemption",
sa.Boolean(),
nullable=False,
server_default=sa.text("true"),
),
sa.Column(
"require_order_reference",
sa.Boolean(),
nullable=False,
server_default=sa.text("false"),
),
sa.Column(
"log_ip_addresses",
sa.Boolean(),
nullable=False,
server_default=sa.text("true"),
),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.Column("updated_at", sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(
["company_id"], ["companies.id"], ondelete="CASCADE"
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
op.f("ix_company_loyalty_settings_id"),
"company_loyalty_settings",
["id"],
unique=False,
)
op.create_index(
op.f("ix_company_loyalty_settings_company_id"),
"company_loyalty_settings",
["company_id"],
unique=True,
)
# =========================================================================
# 2. Modify loyalty_programs: vendor_id -> company_id + new columns
# =========================================================================
# Add company_id (nullable first for data migration)
op.add_column(
"loyalty_programs", sa.Column("company_id", sa.Integer(), nullable=True)
)
# Migrate existing data: derive company_id from vendor_id
op.execute(
"""
UPDATE loyalty_programs lp
SET company_id = v.company_id
FROM vendors v
WHERE v.id = lp.vendor_id
"""
)
# Make company_id non-nullable
op.alter_column("loyalty_programs", "company_id", nullable=False)
# Add FK and indexes
op.create_foreign_key(
"fk_loyalty_programs_company_id",
"loyalty_programs",
"companies",
["company_id"],
["id"],
ondelete="CASCADE",
)
op.create_index(
op.f("ix_loyalty_programs_company_id"),
"loyalty_programs",
["company_id"],
unique=True,
)
op.create_index(
"idx_loyalty_program_company_active",
"loyalty_programs",
["company_id", "is_active"],
)
# Add new Phase 2 columns
op.add_column(
"loyalty_programs",
sa.Column("points_expiration_days", sa.Integer(), nullable=True),
)
op.add_column(
"loyalty_programs",
sa.Column(
"welcome_bonus_points",
sa.Integer(),
nullable=False,
server_default="0",
),
)
op.add_column(
"loyalty_programs",
sa.Column(
"minimum_redemption_points",
sa.Integer(),
nullable=False,
server_default="100",
),
)
op.add_column(
"loyalty_programs",
sa.Column(
"minimum_purchase_cents",
sa.Integer(),
nullable=False,
server_default="0",
),
)
op.add_column(
"loyalty_programs",
sa.Column("tier_config", sa.JSON(), nullable=True),
)
# Drop old vendor_id column and indexes
op.drop_index("idx_loyalty_program_vendor_active", table_name="loyalty_programs")
op.drop_index(
op.f("ix_loyalty_programs_vendor_id"), table_name="loyalty_programs"
)
op.drop_constraint(
"loyalty_programs_vendor_id_fkey", "loyalty_programs", type_="foreignkey"
)
op.drop_column("loyalty_programs", "vendor_id")
# =========================================================================
# 3. Modify loyalty_cards: add company_id, rename vendor_id
# =========================================================================
# Add company_id
op.add_column(
"loyalty_cards", sa.Column("company_id", sa.Integer(), nullable=True)
)
# Migrate data
op.execute(
"""
UPDATE loyalty_cards lc
SET company_id = v.company_id
FROM vendors v
WHERE v.id = lc.vendor_id
"""
)
op.alter_column("loyalty_cards", "company_id", nullable=False)
op.create_foreign_key(
"fk_loyalty_cards_company_id",
"loyalty_cards",
"companies",
["company_id"],
["id"],
ondelete="CASCADE",
)
op.create_index(
op.f("ix_loyalty_cards_company_id"),
"loyalty_cards",
["company_id"],
unique=False,
)
op.create_index(
"idx_loyalty_card_company_active",
"loyalty_cards",
["company_id", "is_active"],
)
op.create_index(
"idx_loyalty_card_company_customer",
"loyalty_cards",
["company_id", "customer_id"],
unique=True,
)
# Rename vendor_id -> enrolled_at_vendor_id, make nullable, change FK
op.drop_index("idx_loyalty_card_vendor_active", table_name="loyalty_cards")
op.drop_index(op.f("ix_loyalty_cards_vendor_id"), table_name="loyalty_cards")
op.drop_constraint(
"loyalty_cards_vendor_id_fkey", "loyalty_cards", type_="foreignkey"
)
op.alter_column(
"loyalty_cards",
"vendor_id",
new_column_name="enrolled_at_vendor_id",
nullable=True,
)
op.create_foreign_key(
"fk_loyalty_cards_enrolled_vendor",
"loyalty_cards",
"vendors",
["enrolled_at_vendor_id"],
["id"],
ondelete="SET NULL",
)
op.create_index(
op.f("ix_loyalty_cards_enrolled_at_vendor_id"),
"loyalty_cards",
["enrolled_at_vendor_id"],
unique=False,
)
# Add last_activity_at
op.add_column(
"loyalty_cards",
sa.Column("last_activity_at", sa.DateTime(timezone=True), nullable=True),
)
# =========================================================================
# 4. Modify loyalty_transactions: add company_id, related_transaction_id
# =========================================================================
# Add company_id
op.add_column(
"loyalty_transactions",
sa.Column("company_id", sa.Integer(), nullable=True),
)
# Migrate data (from card's company)
op.execute(
"""
UPDATE loyalty_transactions lt
SET company_id = lc.company_id
FROM loyalty_cards lc
WHERE lc.id = lt.card_id
"""
)
op.alter_column("loyalty_transactions", "company_id", nullable=False)
op.create_foreign_key(
"fk_loyalty_transactions_company_id",
"loyalty_transactions",
"companies",
["company_id"],
["id"],
ondelete="CASCADE",
)
op.create_index(
op.f("ix_loyalty_transactions_company_id"),
"loyalty_transactions",
["company_id"],
unique=False,
)
op.create_index(
"idx_loyalty_tx_company_date",
"loyalty_transactions",
["company_id", "transaction_at"],
)
op.create_index(
"idx_loyalty_tx_company_vendor",
"loyalty_transactions",
["company_id", "vendor_id"],
)
# Make vendor_id nullable and change FK to SET NULL
op.drop_constraint(
"loyalty_transactions_vendor_id_fkey",
"loyalty_transactions",
type_="foreignkey",
)
op.alter_column("loyalty_transactions", "vendor_id", nullable=True)
op.create_foreign_key(
"fk_loyalty_transactions_vendor_id",
"loyalty_transactions",
"vendors",
["vendor_id"],
["id"],
ondelete="SET NULL",
)
# Add related_transaction_id (for void linkage)
op.add_column(
"loyalty_transactions",
sa.Column("related_transaction_id", sa.Integer(), nullable=True),
)
op.create_foreign_key(
"fk_loyalty_tx_related",
"loyalty_transactions",
"loyalty_transactions",
["related_transaction_id"],
["id"],
ondelete="SET NULL",
)
op.create_index(
op.f("ix_loyalty_transactions_related_transaction_id"),
"loyalty_transactions",
["related_transaction_id"],
unique=False,
)
# =========================================================================
# 5. Modify staff_pins: add company_id
# =========================================================================
op.add_column(
"staff_pins", sa.Column("company_id", sa.Integer(), nullable=True)
)
# Migrate data (from vendor's company)
op.execute(
"""
UPDATE staff_pins sp
SET company_id = v.company_id
FROM vendors v
WHERE v.id = sp.vendor_id
"""
)
op.alter_column("staff_pins", "company_id", nullable=False)
op.create_foreign_key(
"fk_staff_pins_company_id",
"staff_pins",
"companies",
["company_id"],
["id"],
ondelete="CASCADE",
)
op.create_index(
op.f("ix_staff_pins_company_id"),
"staff_pins",
["company_id"],
unique=False,
)
op.create_index(
"idx_staff_pin_company_active",
"staff_pins",
["company_id", "is_active"],
)
def downgrade() -> None:
# =========================================================================
# 5. Revert staff_pins
# =========================================================================
op.drop_index("idx_staff_pin_company_active", table_name="staff_pins")
op.drop_index(op.f("ix_staff_pins_company_id"), table_name="staff_pins")
op.drop_constraint("fk_staff_pins_company_id", "staff_pins", type_="foreignkey")
op.drop_column("staff_pins", "company_id")
# =========================================================================
# 4. Revert loyalty_transactions
# =========================================================================
op.drop_index(
op.f("ix_loyalty_transactions_related_transaction_id"),
table_name="loyalty_transactions",
)
op.drop_constraint(
"fk_loyalty_tx_related", "loyalty_transactions", type_="foreignkey"
)
op.drop_column("loyalty_transactions", "related_transaction_id")
op.drop_constraint(
"fk_loyalty_transactions_vendor_id",
"loyalty_transactions",
type_="foreignkey",
)
op.alter_column("loyalty_transactions", "vendor_id", nullable=False)
op.create_foreign_key(
"loyalty_transactions_vendor_id_fkey",
"loyalty_transactions",
"vendors",
["vendor_id"],
["id"],
ondelete="CASCADE",
)
op.drop_index(
"idx_loyalty_tx_company_vendor", table_name="loyalty_transactions"
)
op.drop_index(
"idx_loyalty_tx_company_date", table_name="loyalty_transactions"
)
op.drop_index(
op.f("ix_loyalty_transactions_company_id"),
table_name="loyalty_transactions",
)
op.drop_constraint(
"fk_loyalty_transactions_company_id",
"loyalty_transactions",
type_="foreignkey",
)
op.drop_column("loyalty_transactions", "company_id")
# =========================================================================
# 3. Revert loyalty_cards
# =========================================================================
op.drop_column("loyalty_cards", "last_activity_at")
op.drop_index(
op.f("ix_loyalty_cards_enrolled_at_vendor_id"), table_name="loyalty_cards"
)
op.drop_constraint(
"fk_loyalty_cards_enrolled_vendor", "loyalty_cards", type_="foreignkey"
)
op.alter_column(
"loyalty_cards",
"enrolled_at_vendor_id",
new_column_name="vendor_id",
nullable=False,
)
op.create_foreign_key(
"loyalty_cards_vendor_id_fkey",
"loyalty_cards",
"vendors",
["vendor_id"],
["id"],
ondelete="CASCADE",
)
op.create_index(
op.f("ix_loyalty_cards_vendor_id"),
"loyalty_cards",
["vendor_id"],
unique=False,
)
op.create_index(
"idx_loyalty_card_vendor_active",
"loyalty_cards",
["vendor_id", "is_active"],
)
op.drop_index(
"idx_loyalty_card_company_customer", table_name="loyalty_cards"
)
op.drop_index(
"idx_loyalty_card_company_active", table_name="loyalty_cards"
)
op.drop_index(
op.f("ix_loyalty_cards_company_id"), table_name="loyalty_cards"
)
op.drop_constraint(
"fk_loyalty_cards_company_id", "loyalty_cards", type_="foreignkey"
)
op.drop_column("loyalty_cards", "company_id")
# =========================================================================
# 2. Revert loyalty_programs
# =========================================================================
op.add_column(
"loyalty_programs",
sa.Column("vendor_id", sa.Integer(), nullable=True),
)
# Note: data migration back not possible if company had multiple vendors
op.create_foreign_key(
"loyalty_programs_vendor_id_fkey",
"loyalty_programs",
"vendors",
["vendor_id"],
["id"],
ondelete="CASCADE",
)
op.create_index(
op.f("ix_loyalty_programs_vendor_id"),
"loyalty_programs",
["vendor_id"],
unique=True,
)
op.create_index(
"idx_loyalty_program_vendor_active",
"loyalty_programs",
["vendor_id", "is_active"],
)
op.drop_column("loyalty_programs", "tier_config")
op.drop_column("loyalty_programs", "minimum_purchase_cents")
op.drop_column("loyalty_programs", "minimum_redemption_points")
op.drop_column("loyalty_programs", "welcome_bonus_points")
op.drop_column("loyalty_programs", "points_expiration_days")
op.drop_index(
"idx_loyalty_program_company_active", table_name="loyalty_programs"
)
op.drop_index(
op.f("ix_loyalty_programs_company_id"), table_name="loyalty_programs"
)
op.drop_constraint(
"fk_loyalty_programs_company_id", "loyalty_programs", type_="foreignkey"
)
op.drop_column("loyalty_programs", "company_id")
# =========================================================================
# 1. Drop company_loyalty_settings table
# =========================================================================
op.drop_index(
op.f("ix_company_loyalty_settings_company_id"),
table_name="company_loyalty_settings",
)
op.drop_index(
op.f("ix_company_loyalty_settings_id"),
table_name="company_loyalty_settings",
)
op.drop_table("company_loyalty_settings")

View File

@@ -51,21 +51,37 @@ from app.core.config import settings
from app.core.database import SessionLocal
from app.core.environment import get_environment, is_production
from middleware.auth import AuthManager
from app.modules.cms.models import ContentPage
from app.modules.tenancy.models import PlatformAlert
from app.modules.tenancy.models import Company
# =============================================================================
# MODEL IMPORTS
# =============================================================================
# ALL models must be imported before any ORM query so SQLAlchemy can resolve
# cross-module string relationships (e.g. Vendor→VendorEmailTemplate,
# Platform→SubscriptionTier, Product→Inventory).
# Core modules
from app.modules.tenancy.models import Company, PlatformAlert, User, Role, Vendor, VendorUser, VendorDomain
from app.modules.cms.models import ContentPage, VendorTheme
from app.modules.catalog.models import Product
from app.modules.customers.models.customer import Customer, CustomerAddress
from app.modules.orders.models import Order, OrderItem
from app.modules.marketplace.models import (
MarketplaceImportJob,
MarketplaceProduct,
MarketplaceProductTranslation,
)
from app.modules.orders.models import Order, OrderItem
from app.modules.catalog.models import Product
from app.modules.tenancy.models import User
from app.modules.tenancy.models import Role, Vendor, VendorUser
from app.modules.tenancy.models import VendorDomain
from app.modules.cms.models import VendorTheme
# Optional modules — import to register models with SQLAlchemy
for _mod in [
"app.modules.inventory.models",
"app.modules.cart.models",
"app.modules.billing.models",
"app.modules.messaging.models",
"app.modules.loyalty.models",
]:
try:
__import__(_mod)
except ImportError:
pass
SEED_MODE = os.getenv("SEED_MODE", "normal") # normal, minimal, reset
FORCE_RESET = os.getenv("FORCE_RESET", "false").lower() in ("true", "1", "yes")
@@ -394,7 +410,7 @@ def check_environment():
def check_admin_exists(db: Session) -> bool:
"""Check if admin user exists."""
admin = db.execute(select(User).where(User.role == "admin")).scalar_one_or_none()
admin = db.execute(select(User).where(User.role == "admin").limit(1)).scalar_one_or_none()
if not admin:
print_error("No admin user found!")
@@ -799,6 +815,14 @@ def create_demo_vendor_content_pages(db: Session, vendors: list[Vendor]) -> int:
"""
created_count = 0
# Get the OMS platform ID (vendors are registered on OMS)
from app.modules.tenancy.models import Platform
oms_platform = db.execute(
select(Platform).where(Platform.code == "oms")
).scalar_one_or_none()
default_platform_id = oms_platform.id if oms_platform else 1
for vendor in vendors:
vendor_pages = VENDOR_CONTENT_PAGES.get(vendor.vendor_code, [])
@@ -819,6 +843,7 @@ def create_demo_vendor_content_pages(db: Session, vendors: list[Vendor]) -> int:
# Create vendor content page override
page = ContentPage(
platform_id=default_platform_id,
vendor_id=vendor.id,
slug=page_data["slug"],
title=page_data["title"],

247
scripts/show_urls.py Normal file
View File

@@ -0,0 +1,247 @@
#!/usr/bin/env python3
"""
Show all platform, admin, vendor, and storefront URLs.
Queries the database for platforms, vendors, and custom domains,
then prints all accessible URLs for both development and production.
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
"""
import argparse
import sys
from sqlalchemy import text
from app.core.config import settings
from app.core.database import SessionLocal
DEV_BASE = "http://localhost:9999"
SEPARATOR = "" * 72
def get_platforms(db):
"""Get all platforms."""
return db.execute(
text(
"SELECT id, code, name, domain, path_prefix, is_active "
"FROM platforms ORDER BY code"
)
).fetchall()
def get_vendors(db):
"""Get all vendors with company info."""
return db.execute(
text(
"SELECT v.id, v.vendor_code, v.name, v.subdomain, v.is_active, "
" c.name AS company_name "
"FROM vendors v "
"LEFT JOIN companies c ON c.id = v.company_id "
"ORDER BY c.name, v.name"
)
).fetchall()
def get_vendor_domains(db):
"""Get all custom vendor domains."""
return db.execute(
text(
"SELECT vd.vendor_id, vd.domain, vd.is_primary, vd.is_active, "
" vd.is_verified, v.vendor_code "
"FROM vendor_domains vd "
"JOIN vendors v ON v.id = vd.vendor_id "
"ORDER BY vd.vendor_id, vd.is_primary DESC"
)
).fetchall()
def status_badge(is_active):
return "active" if is_active else "INACTIVE"
def print_dev_urls(platforms, vendors, vendor_domains):
"""Print all development URLs."""
print()
print("DEVELOPMENT URLS")
print(f"Base: {DEV_BASE}")
print(SEPARATOR)
# Admin
print()
print(" ADMIN PANEL")
print(f" Login: {DEV_BASE}/admin/login")
print(f" Dashboard: {DEV_BASE}/admin/")
print(f" API: {DEV_BASE}/api/v1/admin/")
print(f" API Docs: {DEV_BASE}/docs")
# Platforms
print()
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}/")
# Vendors
print()
print(" VENDOR DASHBOARDS")
domains_by_vendor = {}
for vd in vendor_domains:
domains_by_vendor.setdefault(vd.vendor_id, []).append(vd)
current_company = None
for v in vendors:
if v.company_name != current_company:
current_company = v.company_name
print(f" [{current_company or 'No Company'}]")
tag = f" [{status_badge(v.is_active)}]" if not v.is_active else ""
code = v.vendor_code
print(f" {v.name} ({code}){tag}")
print(f" Dashboard: {DEV_BASE}/vendor/{code}/")
print(f" API: {DEV_BASE}/api/v1/vendor/{code}/")
# Storefronts
print()
print(" STOREFRONTS")
current_company = None
for v in vendors:
if v.company_name != current_company:
current_company = v.company_name
print(f" [{current_company or 'No Company'}]")
tag = f" [{status_badge(v.is_active)}]" if not v.is_active else ""
code = v.vendor_code
print(f" {v.name} ({code}){tag}")
print(f" Shop: {DEV_BASE}/vendors/{code}/storefront/")
print(f" API: {DEV_BASE}/api/v1/storefront/{code}/")
def print_prod_urls(platforms, vendors, vendor_domains):
"""Print all production URLs."""
platform_domain = settings.platform_domain
print()
print("PRODUCTION URLS")
print(f"Platform domain: {platform_domain}")
print(SEPARATOR)
# Admin
print()
print(" ADMIN PANEL")
print(f" Login: https://admin.{platform_domain}/admin/login")
print(f" Dashboard: https://admin.{platform_domain}/admin/")
print(f" API: https://admin.{platform_domain}/api/v1/admin/")
# Platforms
print()
print(" PLATFORMS")
for p in platforms:
tag = f" [{status_badge(p.is_active)}]" if not p.is_active else ""
if p.domain:
print(f" {p.name} ({p.code}){tag}")
print(f" Home: https://{p.domain}/")
elif p.code == "main":
print(f" {p.name}{tag}")
print(f" Home: https://{platform_domain}/")
else:
print(f" {p.name} ({p.code}){tag}")
print(f" Home: https://{p.code}.{platform_domain}/")
# Group domains by vendor
domains_by_vendor = {}
for vd in vendor_domains:
domains_by_vendor.setdefault(vd.vendor_id, []).append(vd)
# Vendors
print()
print(" VENDOR DASHBOARDS")
current_company = None
for v in vendors:
if v.company_name != current_company:
current_company = v.company_name
print(f" [{current_company or 'No Company'}]")
tag = f" [{status_badge(v.is_active)}]" if not v.is_active else ""
print(f" {v.name} ({v.vendor_code}){tag}")
print(f" Dashboard: https://{v.subdomain}.{platform_domain}/vendor/{v.vendor_code}/")
# Storefronts
print()
print(" STOREFRONTS")
current_company = None
for v in vendors:
if v.company_name != current_company:
current_company = v.company_name
print(f" [{current_company or 'No Company'}]")
tag = f" [{status_badge(v.is_active)}]" if not v.is_active else ""
print(f" {v.name} ({v.vendor_code}){tag}")
# Subdomain URL
print(f" Subdomain: https://{v.subdomain}.{platform_domain}/")
# Custom domains
vd_list = domains_by_vendor.get(v.id, [])
for vd in vd_list:
d_flags = []
if vd.is_primary:
d_flags.append("primary")
if not vd.is_active:
d_flags.append("INACTIVE")
if not vd.is_verified:
d_flags.append("unverified")
suffix = f" ({', '.join(d_flags)})" if d_flags else ""
print(f" Custom: https://{vd.domain}/{suffix}")
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")
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)
db = SessionLocal()
try:
platforms = get_platforms(db)
vendors = get_vendors(db)
vendor_domains = get_vendor_domains(db)
except Exception as e:
print(f"Error querying database: {e}", file=sys.stderr)
sys.exit(1)
finally:
db.close()
print()
print("=" * 72)
print(" WIZAMART PLATFORM - ALL URLS")
print(f" {len(platforms)} platform(s), {len(vendors)} vendor(s), {len(vendor_domains)} custom domain(s)")
print("=" * 72)
if show_dev:
print_dev_urls(platforms, vendors, vendor_domains)
if show_prod:
print_prod_urls(platforms, vendors, vendor_domains)
print()
if __name__ == "__main__":
main()

View File

@@ -7,7 +7,6 @@ Provides fixtures for:
- Loyalty cards
- Transactions
- Staff PINs
- Authentication tokens for loyalty tests
"""
import uuid
@@ -23,9 +22,6 @@ from app.modules.loyalty.models import (
)
from app.modules.loyalty.models.loyalty_program import LoyaltyType
from app.modules.loyalty.models.loyalty_transaction import TransactionType
from app.modules.tenancy.models import Company, Vendor, VendorUser, VendorUserType
from app.modules.customers.models import Customer
from middleware.auth import AuthManager
@pytest.fixture
@@ -60,7 +56,6 @@ def test_loyalty_program(db, test_company):
@pytest.fixture
def test_loyalty_program_no_expiration(db, test_company):
"""Create a test loyalty program without point expiration."""
# Use different company to avoid unique constraint
from app.modules.tenancy.models import Company
unique_id = str(uuid.uuid4())[:8]
@@ -101,13 +96,9 @@ def test_loyalty_card(db, test_loyalty_program, test_customer, test_vendor):
customer_id=test_customer.id,
enrolled_at_vendor_id=test_vendor.id,
card_number=f"CARD-{unique_id}",
customer_email=test_customer.email,
customer_phone=test_customer.phone,
customer_name=f"{test_customer.first_name} {test_customer.last_name}",
points_balance=100,
stamps_balance=0,
total_points_earned=150,
total_points_redeemed=50,
points_redeemed=50,
is_active=True,
last_activity_at=datetime.now(UTC),
)
@@ -127,10 +118,7 @@ def test_loyalty_card_inactive(db, test_loyalty_program, test_vendor):
customer_id=None,
enrolled_at_vendor_id=test_vendor.id,
card_number=f"INACTIVE-{unique_id}",
customer_email=f"inactive{unique_id}@test.com",
customer_name="Inactive Customer",
points_balance=500,
stamps_balance=0,
total_points_earned=500,
is_active=True,
# Last activity was 400 days ago (beyond 365-day expiration)
@@ -166,16 +154,15 @@ def test_loyalty_transaction(db, test_loyalty_card, test_vendor):
@pytest.fixture
def test_staff_pin(db, test_loyalty_program, test_vendor):
"""Create a test staff PIN."""
from app.modules.loyalty.services.pin_service import pin_service
unique_id = str(uuid.uuid4())[:8]
pin = StaffPin(
program_id=test_loyalty_program.id,
company_id=test_loyalty_program.company_id,
vendor_id=test_vendor.id,
staff_name=f"Test Staff {unique_id}",
pin_hash=pin_service._hash_pin("1234"),
name=f"Test Staff {unique_id}",
is_active=True,
)
pin.set_pin("1234")
db.add(pin)
db.commit()
db.refresh(pin)