Update:- Membership Plan- Donation- Member detail for Member Directory

This commit is contained in:
Koncept Kit
2025-12-11 19:28:48 +07:00
parent f051976881
commit e875700b8e
7 changed files with 890 additions and 69 deletions

View File

@@ -0,0 +1,314 @@
"""
Database Migration: Billing Enhancements
=========================================
This migration adds support for:
1. Custom billing cycles (recurring date ranges like Jan 1 - Dec 31)
2. Dynamic pricing with minimum/suggested amounts
3. Donation tracking separate from base subscription fees
Changes:
- SubscriptionPlan: Add 8 new fields for custom cycles and dynamic pricing
- Subscription: Add 2 new fields for donation tracking
Backward compatibility:
- Keeps existing fields (price_cents, stripe_price_id) for safe rollback
- Backfills new fields from existing data
Usage:
python migrate_billing_enhancements.py
"""
import os
import sys
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker
from datetime import datetime
# Import database connection
from database import engine, SessionLocal
def run_migration():
"""Execute the migration to add billing enhancement fields."""
print("=" * 60)
print("LOAF Membership - Billing Enhancements Migration")
print("=" * 60)
print()
# Create session
db = SessionLocal()
try:
# =====================================================================
# STEP 1: Add new columns to subscription_plans table
# =====================================================================
print("STEP 1: Adding new columns to subscription_plans table...")
subscription_plan_migrations = [
# Custom billing cycle fields
"ALTER TABLE subscription_plans ADD COLUMN IF NOT EXISTS custom_cycle_enabled BOOLEAN DEFAULT FALSE",
"ALTER TABLE subscription_plans ADD COLUMN IF NOT EXISTS custom_cycle_start_month INTEGER",
"ALTER TABLE subscription_plans ADD COLUMN IF NOT EXISTS custom_cycle_start_day INTEGER",
"ALTER TABLE subscription_plans ADD COLUMN IF NOT EXISTS custom_cycle_end_month INTEGER",
"ALTER TABLE subscription_plans ADD COLUMN IF NOT EXISTS custom_cycle_end_day INTEGER",
# Dynamic pricing fields
"ALTER TABLE subscription_plans ADD COLUMN IF NOT EXISTS minimum_price_cents INTEGER DEFAULT 3000 NOT NULL",
"ALTER TABLE subscription_plans ADD COLUMN IF NOT EXISTS suggested_price_cents INTEGER",
"ALTER TABLE subscription_plans ADD COLUMN IF NOT EXISTS allow_donation BOOLEAN DEFAULT TRUE",
]
for sql in subscription_plan_migrations:
try:
db.execute(text(sql))
print(f"{sql.split('ADD COLUMN IF NOT EXISTS')[1].split()[0]}")
except Exception as e:
print(f"{sql.split('ADD COLUMN IF NOT EXISTS')[1].split()[0]} - {str(e)}")
db.commit()
print(" ✓ Subscription plans table columns added successfully")
print()
# =====================================================================
# STEP 2: Backfill subscription_plans data
# =====================================================================
print("STEP 2: Backfilling subscription_plans data...")
backfill_plans_sql = """
UPDATE subscription_plans
SET
minimum_price_cents = COALESCE(minimum_price_cents, price_cents),
suggested_price_cents = COALESCE(suggested_price_cents, price_cents),
custom_cycle_enabled = COALESCE(custom_cycle_enabled, FALSE),
allow_donation = COALESCE(allow_donation, TRUE)
WHERE minimum_price_cents IS NULL OR suggested_price_cents IS NULL
"""
result = db.execute(text(backfill_plans_sql))
db.commit()
print(f" ✓ Backfilled {result.rowcount} subscription plan records")
print()
# =====================================================================
# STEP 3: Add new columns to subscriptions table
# =====================================================================
print("STEP 3: Adding new columns to subscriptions table...")
subscription_migrations = [
"ALTER TABLE subscriptions ADD COLUMN IF NOT EXISTS base_subscription_cents INTEGER",
"ALTER TABLE subscriptions ADD COLUMN IF NOT EXISTS donation_cents INTEGER DEFAULT 0 NOT NULL",
]
for sql in subscription_migrations:
try:
db.execute(text(sql))
print(f"{sql.split('ADD COLUMN IF NOT EXISTS')[1].split()[0]}")
except Exception as e:
print(f"{sql.split('ADD COLUMN IF NOT EXISTS')[1].split()[0]} - {str(e)}")
db.commit()
print(" ✓ Subscriptions table columns added successfully")
print()
# =====================================================================
# STEP 4: Backfill subscriptions data
# =====================================================================
print("STEP 4: Backfilling subscriptions data...")
backfill_subscriptions_sql = """
UPDATE subscriptions
SET
base_subscription_cents = COALESCE(base_subscription_cents, amount_paid_cents, 0),
donation_cents = COALESCE(donation_cents, 0)
WHERE base_subscription_cents IS NULL
"""
result = db.execute(text(backfill_subscriptions_sql))
db.commit()
print(f" ✓ Backfilled {result.rowcount} subscription records")
print()
# =====================================================================
# STEP 5: Verify migration
# =====================================================================
print("STEP 5: Verifying migration...")
# Check subscription_plans columns
check_plans_sql = """
SELECT
COUNT(*) as total,
COUNT(minimum_price_cents) as has_minimum,
COUNT(suggested_price_cents) as has_suggested,
COUNT(custom_cycle_enabled) as has_custom_enabled
FROM subscription_plans
"""
result = db.execute(text(check_plans_sql)).fetchone()
print(f" Subscription Plans: {result[0]} total")
print(f" - {result[1]} have minimum_price_cents")
print(f" - {result[2]} have suggested_price_cents")
print(f" - {result[3]} have custom_cycle_enabled")
# Check subscriptions columns
check_subscriptions_sql = """
SELECT
COUNT(*) as total,
COUNT(base_subscription_cents) as has_base,
COUNT(donation_cents) as has_donation,
SUM(base_subscription_cents) as total_base,
SUM(donation_cents) as total_donations
FROM subscriptions
"""
result = db.execute(text(check_subscriptions_sql)).fetchone()
print(f" Subscriptions: {result[0]} total")
print(f" - {result[1]} have base_subscription_cents")
print(f" - {result[2]} have donation_cents")
print(f" - Total base: ${(result[3] or 0) / 100:.2f}")
print(f" - Total donations: ${(result[4] or 0) / 100:.2f}")
print()
# =====================================================================
# STEP 6: Add constraints (optional validation)
# =====================================================================
print("STEP 6: Adding data validation constraints...")
# Add check constraint for custom cycle months (1-12)
try:
db.execute(text("""
ALTER TABLE subscription_plans
ADD CONSTRAINT IF NOT EXISTS check_custom_cycle_months
CHECK (
(custom_cycle_start_month IS NULL OR (custom_cycle_start_month >= 1 AND custom_cycle_start_month <= 12)) AND
(custom_cycle_end_month IS NULL OR (custom_cycle_end_month >= 1 AND custom_cycle_end_month <= 12))
)
"""))
print(" ✓ Added month validation constraint (1-12)")
except Exception as e:
print(f" ⚠ Month constraint: {str(e)}")
# Add check constraint for custom cycle days (1-31)
try:
db.execute(text("""
ALTER TABLE subscription_plans
ADD CONSTRAINT IF NOT EXISTS check_custom_cycle_days
CHECK (
(custom_cycle_start_day IS NULL OR (custom_cycle_start_day >= 1 AND custom_cycle_start_day <= 31)) AND
(custom_cycle_end_day IS NULL OR (custom_cycle_end_day >= 1 AND custom_cycle_end_day <= 31))
)
"""))
print(" ✓ Added day validation constraint (1-31)")
except Exception as e:
print(f" ⚠ Day constraint: {str(e)}")
# Add check constraint for minimum price (>= $30)
try:
db.execute(text("""
ALTER TABLE subscription_plans
ADD CONSTRAINT IF NOT EXISTS check_minimum_price
CHECK (minimum_price_cents >= 3000)
"""))
print(" ✓ Added minimum price constraint ($30+)")
except Exception as e:
print(f" ⚠ Minimum price constraint: {str(e)}")
db.commit()
print()
# =====================================================================
# MIGRATION COMPLETE
# =====================================================================
print("=" * 60)
print("✓ MIGRATION COMPLETED SUCCESSFULLY")
print("=" * 60)
print()
print("Summary:")
print(" - Added 8 new columns to subscription_plans table")
print(" - Added 2 new columns to subscriptions table")
print(" - Backfilled all existing data")
print(" - Added validation constraints")
print()
print("Next Steps:")
print(" 1. Update models.py with new column definitions")
print(" 2. Update server.py endpoints for custom billing")
print(" 3. Update frontend components for dynamic pricing")
print()
print("Rollback Notes:")
print(" - Old fields (price_cents, stripe_price_id) are preserved")
print(" - To rollback, revert code and ignore new columns")
print(" - Database can coexist with old and new fields")
print()
except Exception as e:
db.rollback()
print()
print("=" * 60)
print("✗ MIGRATION FAILED")
print("=" * 60)
print(f"Error: {str(e)}")
print()
print("No changes have been committed to the database.")
sys.exit(1)
finally:
db.close()
def rollback_migration():
"""Rollback the migration by removing new columns (use with caution)."""
print("=" * 60)
print("ROLLBACK: Billing Enhancements Migration")
print("=" * 60)
print()
print("WARNING: This will remove all custom billing cycle and donation data!")
confirm = input("Type 'ROLLBACK' to confirm: ")
if confirm != "ROLLBACK":
print("Rollback cancelled.")
return
db = SessionLocal()
try:
print("Removing columns from subscription_plans...")
db.execute(text("ALTER TABLE subscription_plans DROP COLUMN IF EXISTS custom_cycle_enabled"))
db.execute(text("ALTER TABLE subscription_plans DROP COLUMN IF EXISTS custom_cycle_start_month"))
db.execute(text("ALTER TABLE subscription_plans DROP COLUMN IF EXISTS custom_cycle_start_day"))
db.execute(text("ALTER TABLE subscription_plans DROP COLUMN IF EXISTS custom_cycle_end_month"))
db.execute(text("ALTER TABLE subscription_plans DROP COLUMN IF EXISTS custom_cycle_end_day"))
db.execute(text("ALTER TABLE subscription_plans DROP COLUMN IF EXISTS minimum_price_cents"))
db.execute(text("ALTER TABLE subscription_plans DROP COLUMN IF EXISTS suggested_price_cents"))
db.execute(text("ALTER TABLE subscription_plans DROP COLUMN IF EXISTS allow_donation"))
print("Removing columns from subscriptions...")
db.execute(text("ALTER TABLE subscriptions DROP COLUMN IF EXISTS base_subscription_cents"))
db.execute(text("ALTER TABLE subscriptions DROP COLUMN IF EXISTS donation_cents"))
db.commit()
print("✓ Rollback completed successfully")
except Exception as e:
db.rollback()
print(f"✗ Rollback failed: {str(e)}")
sys.exit(1)
finally:
db.close()
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Billing Enhancements Migration")
parser.add_argument(
"--rollback",
action="store_true",
help="Rollback the migration (removes new columns)"
)
args = parser.parse_args()
if args.rollback:
rollback_migration()
else:
run_migration()