144 lines
5.5 KiB
Python
144 lines
5.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
DB Migration — Billing Stripe
|
|
Sprint 5-6: HRT-31
|
|
|
|
Adds stripe_subscription_id and status columns to subscriptions table,
|
|
and an invoices / grace-period tracking table.
|
|
|
|
Run once:
|
|
./venv/bin/python billing_db.py
|
|
"""
|
|
|
|
import sqlite3
|
|
import os
|
|
import logging
|
|
|
|
DB_PATH = os.environ.get("TURF_SAAS_DB", "/home/h3r7/turf_saas/turf_saas.db")
|
|
logger = logging.getLogger("turf_saas.billing_db")
|
|
|
|
|
|
def get_db():
|
|
conn = sqlite3.connect(DB_PATH)
|
|
conn.row_factory = sqlite3.Row
|
|
return conn
|
|
|
|
|
|
def migrate_billing_tables():
|
|
"""Idempotent migration: add billing columns and billing_events table.
|
|
|
|
Requires auth tables (users, subscriptions) to exist first.
|
|
Calls init_auth_tables() automatically if subscriptions is absent.
|
|
"""
|
|
from auth_db import init_auth_tables as _init_auth
|
|
|
|
conn = get_db()
|
|
c = conn.cursor()
|
|
|
|
# Ensure base auth tables exist
|
|
tables = {
|
|
row[0] for row in c.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
|
}
|
|
conn.close()
|
|
|
|
if "subscriptions" not in tables:
|
|
_init_auth()
|
|
|
|
conn = get_db()
|
|
c = conn.cursor()
|
|
|
|
# Add stripe_subscription_id if missing
|
|
columns = {row[1] for row in c.execute("PRAGMA table_info(subscriptions)")}
|
|
|
|
if "stripe_subscription_id" not in columns:
|
|
c.execute("ALTER TABLE subscriptions ADD COLUMN stripe_subscription_id TEXT")
|
|
logger.info("[billing_db] Added stripe_subscription_id column to subscriptions")
|
|
|
|
if "status" not in columns:
|
|
c.execute(
|
|
"ALTER TABLE subscriptions ADD COLUMN "
|
|
"status TEXT NOT NULL DEFAULT 'active' "
|
|
"CHECK(status IN ('active','past_due','canceled','trialing','incomplete'))"
|
|
)
|
|
logger.info("[billing_db] Added status column to subscriptions")
|
|
|
|
if "grace_period_end" not in columns:
|
|
c.execute("ALTER TABLE subscriptions ADD COLUMN grace_period_end DATETIME")
|
|
logger.info("[billing_db] Added grace_period_end column to subscriptions")
|
|
|
|
if "current_period_end" not in columns:
|
|
c.execute("ALTER TABLE subscriptions ADD COLUMN current_period_end DATETIME")
|
|
logger.info("[billing_db] Added current_period_end column to subscriptions")
|
|
|
|
# billing_events table — audit trail for all webhook events
|
|
c.executescript("""
|
|
CREATE TABLE IF NOT EXISTS billing_events (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
stripe_event_id TEXT NOT NULL UNIQUE,
|
|
event_type TEXT NOT NULL,
|
|
user_id TEXT,
|
|
payload TEXT,
|
|
processed_at DATETIME NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS saas_subscriptions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id TEXT NOT NULL,
|
|
plan TEXT NOT NULL DEFAULT 'free',
|
|
start_date DATETIME DEFAULT (datetime('now')),
|
|
end_date DATETIME,
|
|
stripe_customer_id TEXT,
|
|
stripe_subscription_id TEXT,
|
|
status TEXT NOT NULL DEFAULT 'active',
|
|
grace_period_end DATETIME,
|
|
current_period_end DATETIME
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_billing_events_user ON billing_events(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_billing_events_type ON billing_events(event_type);
|
|
CREATE INDEX IF NOT EXISTS idx_saas_subs_user ON saas_subscriptions(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_saas_subs_customer ON saas_subscriptions(stripe_customer_id);
|
|
CREATE INDEX IF NOT EXISTS idx_saas_subs_stripe ON saas_subscriptions(stripe_subscription_id);
|
|
CREATE INDEX IF NOT EXISTS idx_subscriptions_stripe ON subscriptions(stripe_subscription_id);
|
|
CREATE INDEX IF NOT EXISTS idx_subscriptions_customer ON subscriptions(stripe_customer_id);
|
|
""")
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
print(
|
|
"[billing_db] Migration complete: subscriptions + billing_events tables ready."
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
logging.basicConfig(level=logging.INFO)
|
|
migrate_billing_tables()
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────
|
|
# Re-exported helpers for test usage
|
|
# (primary implementations live in api_v1/routes/billing.py)
|
|
# ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
def _upsert_subscription(db, user_id: int, **fields):
|
|
"""
|
|
Update existing subscription row or insert a new one.
|
|
Convenience re-export for test helpers.
|
|
"""
|
|
existing = db.execute(
|
|
"SELECT id FROM subscriptions WHERE user_id = ? ORDER BY start_date DESC LIMIT 1",
|
|
(user_id,),
|
|
).fetchone()
|
|
if existing:
|
|
set_parts = ", ".join(f"{k} = ?" for k in fields)
|
|
values = list(fields.values()) + [existing["id"]]
|
|
db.execute(f"UPDATE subscriptions SET {set_parts} WHERE id = ?", values)
|
|
else:
|
|
cols = ", ".join(["user_id"] + list(fields.keys()))
|
|
placeholders = ", ".join(["?"] * (1 + len(fields)))
|
|
values = [user_id] + list(fields.values())
|
|
db.execute(
|
|
f"INSERT INTO subscriptions ({cols}) VALUES ({placeholders})", values
|
|
)
|