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:
@@ -1,424 +0,0 @@
|
||||
"""add loyalty platform
|
||||
|
||||
Revision ID: z5f6g7h8i9j0
|
||||
Revises: z4e5f6a7b8c9
|
||||
Create Date: 2026-01-19 12:00:00.000000
|
||||
|
||||
This migration adds the Loyalty+ platform:
|
||||
1. Inserts loyalty platform record
|
||||
2. Creates platform marketing pages (home, pricing, features, how-it-works)
|
||||
3. Creates vendor default pages (about, rewards-catalog, terms, privacy)
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "z5f6g7h8i9j0"
|
||||
down_revision: Union[str, None] = "z4e5f6a7b8c9"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
|
||||
# =========================================================================
|
||||
# 1. Insert Loyalty platform
|
||||
# =========================================================================
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
INSERT INTO platforms (code, name, description, domain, path_prefix, default_language,
|
||||
supported_languages, is_active, is_public, theme_config, settings,
|
||||
created_at, updated_at)
|
||||
VALUES ('loyalty', 'Loyalty+', 'Customer loyalty program platform for Luxembourg businesses',
|
||||
'loyalty.lu', 'loyalty', 'fr', '["fr", "de", "en"]', true, true,
|
||||
'{"primary_color": "#8B5CF6", "secondary_color": "#A78BFA"}',
|
||||
'{"features": ["points", "rewards", "tiers", "analytics"]}',
|
||||
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
""")
|
||||
)
|
||||
|
||||
# Get the Loyalty platform ID
|
||||
result = conn.execute(sa.text("SELECT id FROM platforms WHERE code = 'loyalty'"))
|
||||
loyalty_platform_id = result.fetchone()[0]
|
||||
|
||||
# =========================================================================
|
||||
# 2. Create platform marketing pages (is_platform_page=True)
|
||||
# =========================================================================
|
||||
platform_pages = [
|
||||
{
|
||||
"slug": "home",
|
||||
"title": "Loyalty+ - Customer Loyalty Platform",
|
||||
"content": """<div class="hero-section">
|
||||
<h1>Build Customer Loyalty That Lasts</h1>
|
||||
<p class="lead">Reward your customers, increase retention, and grow your business with Loyalty+</p>
|
||||
</div>
|
||||
|
||||
<div class="features-grid">
|
||||
<div class="feature">
|
||||
<h3>Points & Rewards</h3>
|
||||
<p>Create custom point systems that incentivize repeat purchases and customer engagement.</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<h3>Member Tiers</h3>
|
||||
<p>Reward your best customers with exclusive benefits and VIP treatment.</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<h3>Real-time Analytics</h3>
|
||||
<p>Track program performance and customer behavior with detailed insights.</p>
|
||||
</div>
|
||||
</div>""",
|
||||
"meta_description": "Loyalty+ is Luxembourg's leading customer loyalty platform. Build lasting relationships with your customers through points, rewards, and personalized experiences.",
|
||||
"show_in_header": False,
|
||||
"show_in_footer": False,
|
||||
"display_order": 0,
|
||||
},
|
||||
{
|
||||
"slug": "pricing",
|
||||
"title": "Pricing - Loyalty+",
|
||||
"content": """<div class="pricing-header">
|
||||
<h1>Simple, Transparent Pricing</h1>
|
||||
<p>Choose the plan that fits your business</p>
|
||||
</div>
|
||||
|
||||
<div class="pricing-grid">
|
||||
<div class="pricing-card">
|
||||
<h3>Starter</h3>
|
||||
<div class="price">€49<span>/month</span></div>
|
||||
<ul>
|
||||
<li>Up to 500 members</li>
|
||||
<li>Basic point system</li>
|
||||
<li>Email support</li>
|
||||
<li>Standard rewards</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pricing-card featured">
|
||||
<h3>Growth</h3>
|
||||
<div class="price">€149<span>/month</span></div>
|
||||
<ul>
|
||||
<li>Up to 5,000 members</li>
|
||||
<li>Advanced point rules</li>
|
||||
<li>Priority support</li>
|
||||
<li>Custom rewards</li>
|
||||
<li>Member tiers</li>
|
||||
<li>Analytics dashboard</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pricing-card">
|
||||
<h3>Enterprise</h3>
|
||||
<div class="price">Custom</div>
|
||||
<ul>
|
||||
<li>Unlimited members</li>
|
||||
<li>Full API access</li>
|
||||
<li>Dedicated support</li>
|
||||
<li>Custom integrations</li>
|
||||
<li>White-label options</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>""",
|
||||
"meta_description": "Loyalty+ pricing plans starting at €49/month. Choose Starter, Growth, or Enterprise for your customer loyalty program.",
|
||||
"show_in_header": True,
|
||||
"show_in_footer": True,
|
||||
"display_order": 1,
|
||||
},
|
||||
{
|
||||
"slug": "features",
|
||||
"title": "Features - Loyalty+",
|
||||
"content": """<div class="features-header">
|
||||
<h1>Powerful Features for Modern Loyalty</h1>
|
||||
<p>Everything you need to build and manage a successful loyalty program</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-section">
|
||||
<h2>Points & Earning Rules</h2>
|
||||
<p>Create flexible point systems with custom earning rules based on purchases, actions, or special events.</p>
|
||||
<ul>
|
||||
<li>Points per euro spent</li>
|
||||
<li>Bonus point campaigns</li>
|
||||
<li>Birthday & anniversary rewards</li>
|
||||
<li>Referral bonuses</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="feature-section">
|
||||
<h2>Rewards Catalog</h2>
|
||||
<p>Offer enticing rewards that keep customers coming back.</p>
|
||||
<ul>
|
||||
<li>Discount vouchers</li>
|
||||
<li>Free products</li>
|
||||
<li>Exclusive experiences</li>
|
||||
<li>Partner rewards</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="feature-section">
|
||||
<h2>Member Tiers</h2>
|
||||
<p>Recognize and reward your most loyal customers with tiered benefits.</p>
|
||||
<ul>
|
||||
<li>Bronze, Silver, Gold, Platinum levels</li>
|
||||
<li>Automatic tier progression</li>
|
||||
<li>Exclusive tier benefits</li>
|
||||
<li>VIP experiences</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="feature-section">
|
||||
<h2>Analytics & Insights</h2>
|
||||
<p>Make data-driven decisions with comprehensive analytics.</p>
|
||||
<ul>
|
||||
<li>Member activity tracking</li>
|
||||
<li>Redemption analytics</li>
|
||||
<li>ROI calculations</li>
|
||||
<li>Custom reports</li>
|
||||
</ul>
|
||||
</div>""",
|
||||
"meta_description": "Explore Loyalty+ features: points systems, rewards catalog, member tiers, and analytics. Build the perfect loyalty program for your business.",
|
||||
"show_in_header": True,
|
||||
"show_in_footer": True,
|
||||
"display_order": 2,
|
||||
},
|
||||
{
|
||||
"slug": "how-it-works",
|
||||
"title": "How It Works - Loyalty+",
|
||||
"content": """<div class="how-header">
|
||||
<h1>Getting Started is Easy</h1>
|
||||
<p>Launch your loyalty program in just a few steps</p>
|
||||
</div>
|
||||
|
||||
<div class="steps">
|
||||
<div class="step">
|
||||
<div class="step-number">1</div>
|
||||
<h3>Sign Up</h3>
|
||||
<p>Create your account and choose your plan. No credit card required for the free trial.</p>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-number">2</div>
|
||||
<h3>Configure Your Program</h3>
|
||||
<p>Set up your point rules, rewards, and member tiers using our intuitive dashboard.</p>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-number">3</div>
|
||||
<h3>Integrate</h3>
|
||||
<p>Connect Loyalty+ to your POS, e-commerce, or app using our APIs and plugins.</p>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-number">4</div>
|
||||
<h3>Launch & Grow</h3>
|
||||
<p>Invite your customers and watch your loyalty program drive results.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cta-section">
|
||||
<h2>Ready to Build Customer Loyalty?</h2>
|
||||
<p>Start your free 14-day trial today.</p>
|
||||
<a href="/loyalty/signup" class="btn-primary">Get Started Free</a>
|
||||
</div>""",
|
||||
"meta_description": "Learn how to launch your Loyalty+ program in 4 easy steps. Sign up, configure, integrate, and start building customer loyalty today.",
|
||||
"show_in_header": True,
|
||||
"show_in_footer": True,
|
||||
"display_order": 3,
|
||||
},
|
||||
]
|
||||
|
||||
for page in platform_pages:
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
INSERT INTO content_pages (platform_id, vendor_id, slug, title, content, content_format,
|
||||
meta_description, is_published, is_platform_page,
|
||||
show_in_header, show_in_footer, display_order,
|
||||
created_at, updated_at)
|
||||
VALUES (:platform_id, NULL, :slug, :title, :content, 'html',
|
||||
:meta_description, true, true,
|
||||
:show_in_header, :show_in_footer, :display_order,
|
||||
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
"""),
|
||||
{
|
||||
"platform_id": loyalty_platform_id,
|
||||
"slug": page["slug"],
|
||||
"title": page["title"],
|
||||
"content": page["content"],
|
||||
"meta_description": page["meta_description"],
|
||||
"show_in_header": page["show_in_header"],
|
||||
"show_in_footer": page["show_in_footer"],
|
||||
"display_order": page["display_order"],
|
||||
}
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# 3. Create vendor default pages (is_platform_page=False)
|
||||
# =========================================================================
|
||||
vendor_defaults = [
|
||||
{
|
||||
"slug": "about",
|
||||
"title": "About Us",
|
||||
"content": """<div class="about-page">
|
||||
<h1>About Our Loyalty Program</h1>
|
||||
<p>Welcome to our customer loyalty program! We value your continued support and want to reward you for being part of our community.</p>
|
||||
|
||||
<h2>Why Join?</h2>
|
||||
<ul>
|
||||
<li><strong>Earn Points:</strong> Get points on every purchase</li>
|
||||
<li><strong>Exclusive Rewards:</strong> Redeem points for discounts and special offers</li>
|
||||
<li><strong>Member Benefits:</strong> Access exclusive deals and early sales</li>
|
||||
<li><strong>Birthday Surprises:</strong> Special rewards on your birthday</li>
|
||||
</ul>
|
||||
|
||||
<h2>How It Works</h2>
|
||||
<p>Simply sign up, start earning points with every purchase, and redeem them for rewards you'll love.</p>
|
||||
</div>""",
|
||||
"meta_description": "Learn about our customer loyalty program. Earn points, unlock rewards, and enjoy exclusive member benefits.",
|
||||
"show_in_header": False,
|
||||
"show_in_footer": True,
|
||||
"display_order": 10,
|
||||
},
|
||||
{
|
||||
"slug": "rewards-catalog",
|
||||
"title": "Rewards Catalog",
|
||||
"content": """<div class="rewards-page">
|
||||
<h1>Rewards Catalog</h1>
|
||||
<p>Browse our selection of rewards and redeem your hard-earned points!</p>
|
||||
|
||||
<div class="rewards-grid">
|
||||
<div class="reward-placeholder">
|
||||
<p>Your rewards catalog will appear here once configured.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>How to Redeem</h2>
|
||||
<ol>
|
||||
<li>Check your point balance in your account</li>
|
||||
<li>Browse available rewards</li>
|
||||
<li>Click "Redeem" on your chosen reward</li>
|
||||
<li>Use your reward code at checkout</li>
|
||||
</ol>
|
||||
</div>""",
|
||||
"meta_description": "Browse and redeem your loyalty points for exclusive rewards, discounts, and special offers.",
|
||||
"show_in_header": True,
|
||||
"show_in_footer": True,
|
||||
"display_order": 11,
|
||||
},
|
||||
{
|
||||
"slug": "terms",
|
||||
"title": "Loyalty Program Terms & Conditions",
|
||||
"content": """<div class="terms-page">
|
||||
<h1>Loyalty Program Terms & Conditions</h1>
|
||||
<p class="last-updated">Last updated: January 2026</p>
|
||||
|
||||
<h2>1. Program Membership</h2>
|
||||
<p>Membership in our loyalty program is free and open to all customers who meet the eligibility requirements.</p>
|
||||
|
||||
<h2>2. Earning Points</h2>
|
||||
<p>Points are earned on qualifying purchases. The earning rate and qualifying purchases are determined by the program operator and may change with notice.</p>
|
||||
|
||||
<h2>3. Redeeming Points</h2>
|
||||
<p>Points can be redeemed for rewards as shown in the rewards catalog. Minimum point thresholds may apply.</p>
|
||||
|
||||
<h2>4. Point Expiration</h2>
|
||||
<p>Points may expire after a period of account inactivity. Members will be notified before points expire.</p>
|
||||
|
||||
<h2>5. Program Changes</h2>
|
||||
<p>We reserve the right to modify, suspend, or terminate the program with reasonable notice to members.</p>
|
||||
|
||||
<h2>6. Privacy</h2>
|
||||
<p>Your personal information is handled in accordance with our Privacy Policy.</p>
|
||||
</div>""",
|
||||
"meta_description": "Read the terms and conditions for our customer loyalty program including earning rules, redemption, and point expiration policies.",
|
||||
"show_in_header": False,
|
||||
"show_in_footer": True,
|
||||
"show_in_legal": True,
|
||||
"display_order": 20,
|
||||
},
|
||||
{
|
||||
"slug": "privacy",
|
||||
"title": "Privacy Policy",
|
||||
"content": """<div class="privacy-page">
|
||||
<h1>Privacy Policy</h1>
|
||||
<p class="last-updated">Last updated: January 2026</p>
|
||||
|
||||
<h2>Information We Collect</h2>
|
||||
<p>We collect information you provide when joining our loyalty program, including:</p>
|
||||
<ul>
|
||||
<li>Name and contact information</li>
|
||||
<li>Purchase history and preferences</li>
|
||||
<li>Point balance and redemption history</li>
|
||||
</ul>
|
||||
|
||||
<h2>How We Use Your Information</h2>
|
||||
<p>Your information helps us:</p>
|
||||
<ul>
|
||||
<li>Manage your loyalty account</li>
|
||||
<li>Process point earnings and redemptions</li>
|
||||
<li>Send program updates and personalized offers</li>
|
||||
<li>Improve our services</li>
|
||||
</ul>
|
||||
|
||||
<h2>Data Protection</h2>
|
||||
<p>We implement appropriate security measures to protect your personal information in accordance with GDPR and Luxembourg data protection laws.</p>
|
||||
|
||||
<h2>Your Rights</h2>
|
||||
<p>You have the right to access, correct, or delete your personal data. Contact us to exercise these rights.</p>
|
||||
|
||||
<h2>Contact</h2>
|
||||
<p>For privacy inquiries, please contact our data protection officer.</p>
|
||||
</div>""",
|
||||
"meta_description": "Our privacy policy explains how we collect, use, and protect your personal information in our loyalty program.",
|
||||
"show_in_header": False,
|
||||
"show_in_footer": True,
|
||||
"show_in_legal": True,
|
||||
"display_order": 21,
|
||||
},
|
||||
]
|
||||
|
||||
for page in vendor_defaults:
|
||||
show_in_legal = page.get("show_in_legal", False)
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
INSERT INTO content_pages (platform_id, vendor_id, slug, title, content, content_format,
|
||||
meta_description, is_published, is_platform_page,
|
||||
show_in_header, show_in_footer, show_in_legal, display_order,
|
||||
created_at, updated_at)
|
||||
VALUES (:platform_id, NULL, :slug, :title, :content, 'html',
|
||||
:meta_description, true, false,
|
||||
:show_in_header, :show_in_footer, :show_in_legal, :display_order,
|
||||
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
"""),
|
||||
{
|
||||
"platform_id": loyalty_platform_id,
|
||||
"slug": page["slug"],
|
||||
"title": page["title"],
|
||||
"content": page["content"],
|
||||
"meta_description": page["meta_description"],
|
||||
"show_in_header": page["show_in_header"],
|
||||
"show_in_footer": page["show_in_footer"],
|
||||
"show_in_legal": show_in_legal,
|
||||
"display_order": page["display_order"],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
|
||||
# Get the Loyalty platform ID
|
||||
result = conn.execute(sa.text("SELECT id FROM platforms WHERE code = 'loyalty'"))
|
||||
row = result.fetchone()
|
||||
if row:
|
||||
loyalty_platform_id = row[0]
|
||||
|
||||
# Delete all content pages for loyalty platform
|
||||
conn.execute(
|
||||
sa.text("DELETE FROM content_pages WHERE platform_id = :platform_id"),
|
||||
{"platform_id": loyalty_platform_id}
|
||||
)
|
||||
|
||||
# Delete vendor_platforms entries for loyalty
|
||||
conn.execute(
|
||||
sa.text("DELETE FROM vendor_platforms WHERE platform_id = :platform_id"),
|
||||
{"platform_id": loyalty_platform_id}
|
||||
)
|
||||
|
||||
# Delete loyalty platform
|
||||
conn.execute(sa.text("DELETE FROM platforms WHERE code = 'loyalty'"))
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user