diff --git a/Makefile b/Makefile index d385618c..97c8dab1 100644 --- a/Makefile +++ b/Makefile @@ -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 "" diff --git a/alembic.ini b/alembic.ini index 7693de9a..39759137 100644 --- a/alembic.ini +++ b/alembic.ini @@ -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 diff --git a/app/modules/loyalty/migrations/versions/loyalty_002_add_loyalty_platform.py b/alembic/versions/z5f6g7h8i9j0_add_loyalty_platform.py similarity index 100% rename from app/modules/loyalty/migrations/versions/loyalty_002_add_loyalty_platform.py rename to alembic/versions/z5f6g7h8i9j0_add_loyalty_platform.py diff --git a/app/modules/loyalty/migrations/versions/loyalty_003_phase2_company_based.py b/app/modules/loyalty/migrations/versions/loyalty_003_phase2_company_based.py new file mode 100644 index 00000000..ce61b74f --- /dev/null +++ b/app/modules/loyalty/migrations/versions/loyalty_003_phase2_company_based.py @@ -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") diff --git a/scripts/seed_demo.py b/scripts/seed_demo.py index 4110635c..61bf04ea 100644 --- a/scripts/seed_demo.py +++ b/scripts/seed_demo.py @@ -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"], diff --git a/scripts/show_urls.py b/scripts/show_urls.py new file mode 100644 index 00000000..036abf70 --- /dev/null +++ b/scripts/show_urls.py @@ -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() diff --git a/tests/fixtures/loyalty_fixtures.py b/tests/fixtures/loyalty_fixtures.py index 8ed303e3..ce040a31 100644 --- a/tests/fixtures/loyalty_fixtures.py +++ b/tests/fixtures/loyalty_fixtures.py @@ -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)