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:
14
Makefile
14
Makefile
@@ -1,7 +1,7 @@
|
|||||||
# Wizamart Multi-Tenant E-Commerce Platform Makefile
|
# Wizamart Multi-Tenant E-Commerce Platform Makefile
|
||||||
# Cross-platform compatible (Windows & Linux)
|
# 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
|
# Detect OS
|
||||||
ifeq ($(OS),Windows_NT)
|
ifeq ($(OS),Windows_NT)
|
||||||
@@ -477,6 +477,15 @@ verify-setup:
|
|||||||
@echo "Running setup verification..."
|
@echo "Running setup verification..."
|
||||||
@$(PYTHON) scripts/verify_setup.py
|
@$(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:
|
check-env:
|
||||||
@echo "Checking Python environment..."
|
@echo "Checking Python environment..."
|
||||||
@echo "Detected OS: $(DETECTED_OS)"
|
@echo "Detected OS: $(DETECTED_OS)"
|
||||||
@@ -572,6 +581,9 @@ help:
|
|||||||
@echo " docker-down - Stop Docker containers"
|
@echo " docker-down - Stop Docker containers"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "=== UTILITIES ==="
|
@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 " clean - Clean build artifacts"
|
||||||
@echo " check-env - Check Python environment and OS"
|
@echo " check-env - Check Python environment and OS"
|
||||||
@echo ""
|
@echo ""
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
[alembic]
|
[alembic]
|
||||||
script_location = alembic
|
script_location = alembic
|
||||||
prepend_sys_path = .
|
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
|
# This will be overridden by alembic\env.py using settings.database_url
|
||||||
sqlalchemy.url =
|
sqlalchemy.url =
|
||||||
# for PROD: sqlalchemy.url = postgresql://username:password@localhost:5432/ecommerce_db
|
# for PROD: sqlalchemy.url = postgresql://username:password@localhost:5432/ecommerce_db
|
||||||
|
|||||||
@@ -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")
|
||||||
@@ -51,21 +51,37 @@ from app.core.config import settings
|
|||||||
from app.core.database import SessionLocal
|
from app.core.database import SessionLocal
|
||||||
from app.core.environment import get_environment, is_production
|
from app.core.environment import get_environment, is_production
|
||||||
from middleware.auth import AuthManager
|
from middleware.auth import AuthManager
|
||||||
from app.modules.cms.models import ContentPage
|
# =============================================================================
|
||||||
from app.modules.tenancy.models import PlatformAlert
|
# MODEL IMPORTS
|
||||||
from app.modules.tenancy.models import Company
|
# =============================================================================
|
||||||
|
# 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.customers.models.customer import Customer, CustomerAddress
|
||||||
|
from app.modules.orders.models import Order, OrderItem
|
||||||
from app.modules.marketplace.models import (
|
from app.modules.marketplace.models import (
|
||||||
MarketplaceImportJob,
|
MarketplaceImportJob,
|
||||||
MarketplaceProduct,
|
MarketplaceProduct,
|
||||||
MarketplaceProductTranslation,
|
MarketplaceProductTranslation,
|
||||||
)
|
)
|
||||||
from app.modules.orders.models import Order, OrderItem
|
|
||||||
from app.modules.catalog.models import Product
|
# Optional modules — import to register models with SQLAlchemy
|
||||||
from app.modules.tenancy.models import User
|
for _mod in [
|
||||||
from app.modules.tenancy.models import Role, Vendor, VendorUser
|
"app.modules.inventory.models",
|
||||||
from app.modules.tenancy.models import VendorDomain
|
"app.modules.cart.models",
|
||||||
from app.modules.cms.models import VendorTheme
|
"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
|
SEED_MODE = os.getenv("SEED_MODE", "normal") # normal, minimal, reset
|
||||||
FORCE_RESET = os.getenv("FORCE_RESET", "false").lower() in ("true", "1", "yes")
|
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:
|
def check_admin_exists(db: Session) -> bool:
|
||||||
"""Check if admin user exists."""
|
"""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:
|
if not admin:
|
||||||
print_error("No admin user found!")
|
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
|
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:
|
for vendor in vendors:
|
||||||
vendor_pages = VENDOR_CONTENT_PAGES.get(vendor.vendor_code, [])
|
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
|
# Create vendor content page override
|
||||||
page = ContentPage(
|
page = ContentPage(
|
||||||
|
platform_id=default_platform_id,
|
||||||
vendor_id=vendor.id,
|
vendor_id=vendor.id,
|
||||||
slug=page_data["slug"],
|
slug=page_data["slug"],
|
||||||
title=page_data["title"],
|
title=page_data["title"],
|
||||||
|
|||||||
247
scripts/show_urls.py
Normal file
247
scripts/show_urls.py
Normal 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()
|
||||||
21
tests/fixtures/loyalty_fixtures.py
vendored
21
tests/fixtures/loyalty_fixtures.py
vendored
@@ -7,7 +7,6 @@ Provides fixtures for:
|
|||||||
- Loyalty cards
|
- Loyalty cards
|
||||||
- Transactions
|
- Transactions
|
||||||
- Staff PINs
|
- Staff PINs
|
||||||
- Authentication tokens for loyalty tests
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import uuid
|
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_program import LoyaltyType
|
||||||
from app.modules.loyalty.models.loyalty_transaction import TransactionType
|
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
|
@pytest.fixture
|
||||||
@@ -60,7 +56,6 @@ def test_loyalty_program(db, test_company):
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def test_loyalty_program_no_expiration(db, test_company):
|
def test_loyalty_program_no_expiration(db, test_company):
|
||||||
"""Create a test loyalty program without point expiration."""
|
"""Create a test loyalty program without point expiration."""
|
||||||
# Use different company to avoid unique constraint
|
|
||||||
from app.modules.tenancy.models import Company
|
from app.modules.tenancy.models import Company
|
||||||
|
|
||||||
unique_id = str(uuid.uuid4())[:8]
|
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,
|
customer_id=test_customer.id,
|
||||||
enrolled_at_vendor_id=test_vendor.id,
|
enrolled_at_vendor_id=test_vendor.id,
|
||||||
card_number=f"CARD-{unique_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,
|
points_balance=100,
|
||||||
stamps_balance=0,
|
|
||||||
total_points_earned=150,
|
total_points_earned=150,
|
||||||
total_points_redeemed=50,
|
points_redeemed=50,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
last_activity_at=datetime.now(UTC),
|
last_activity_at=datetime.now(UTC),
|
||||||
)
|
)
|
||||||
@@ -127,10 +118,7 @@ def test_loyalty_card_inactive(db, test_loyalty_program, test_vendor):
|
|||||||
customer_id=None,
|
customer_id=None,
|
||||||
enrolled_at_vendor_id=test_vendor.id,
|
enrolled_at_vendor_id=test_vendor.id,
|
||||||
card_number=f"INACTIVE-{unique_id}",
|
card_number=f"INACTIVE-{unique_id}",
|
||||||
customer_email=f"inactive{unique_id}@test.com",
|
|
||||||
customer_name="Inactive Customer",
|
|
||||||
points_balance=500,
|
points_balance=500,
|
||||||
stamps_balance=0,
|
|
||||||
total_points_earned=500,
|
total_points_earned=500,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
# Last activity was 400 days ago (beyond 365-day expiration)
|
# 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
|
@pytest.fixture
|
||||||
def test_staff_pin(db, test_loyalty_program, test_vendor):
|
def test_staff_pin(db, test_loyalty_program, test_vendor):
|
||||||
"""Create a test staff PIN."""
|
"""Create a test staff PIN."""
|
||||||
from app.modules.loyalty.services.pin_service import pin_service
|
|
||||||
|
|
||||||
unique_id = str(uuid.uuid4())[:8]
|
unique_id = str(uuid.uuid4())[:8]
|
||||||
pin = StaffPin(
|
pin = StaffPin(
|
||||||
program_id=test_loyalty_program.id,
|
program_id=test_loyalty_program.id,
|
||||||
|
company_id=test_loyalty_program.company_id,
|
||||||
vendor_id=test_vendor.id,
|
vendor_id=test_vendor.id,
|
||||||
staff_name=f"Test Staff {unique_id}",
|
name=f"Test Staff {unique_id}",
|
||||||
pin_hash=pin_service._hash_pin("1234"),
|
|
||||||
is_active=True,
|
is_active=True,
|
||||||
)
|
)
|
||||||
|
pin.set_pin("1234")
|
||||||
db.add(pin)
|
db.add(pin)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(pin)
|
db.refresh(pin)
|
||||||
|
|||||||
Reference in New Issue
Block a user