feat(billing): A1+A3 — payment_products table + provider-agnostic price lookups

- Migration 0028: create payment_products table, copy paddle_products rows
- Add STRIPE_SECRET_KEY, STRIPE_PUBLISHABLE_KEY, STRIPE_WEBHOOK_SECRET config
- Make PAYMENT_PROVIDER read from env (was hardcoded "paddle")
- Add get_price_id() / get_all_price_ids() querying payment_products
- Keep get_paddle_price() as deprecated fallback alias

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-03-03 15:07:10 +01:00
parent 0fc0ca66b1
commit 276328af33
2 changed files with 65 additions and 5 deletions

View File

@@ -49,13 +49,17 @@ class Config:
MAGIC_LINK_EXPIRY_MINUTES: int = int(os.getenv("MAGIC_LINK_EXPIRY_MINUTES", "15"))
SESSION_LIFETIME_DAYS: int = int(os.getenv("SESSION_LIFETIME_DAYS", "30"))
PAYMENT_PROVIDER: str = "paddle"
PAYMENT_PROVIDER: str = _env("PAYMENT_PROVIDER", "paddle")
PADDLE_API_KEY: str = os.getenv("PADDLE_API_KEY", "")
PADDLE_CLIENT_TOKEN: str = os.getenv("PADDLE_CLIENT_TOKEN", "")
PADDLE_WEBHOOK_SECRET: str = os.getenv("PADDLE_WEBHOOK_SECRET", "")
PADDLE_ENVIRONMENT: str = _env("PADDLE_ENVIRONMENT", "sandbox")
STRIPE_SECRET_KEY: str = os.getenv("STRIPE_SECRET_KEY", "")
STRIPE_PUBLISHABLE_KEY: str = os.getenv("STRIPE_PUBLISHABLE_KEY", "")
STRIPE_WEBHOOK_SECRET: str = os.getenv("STRIPE_WEBHOOK_SECRET", "")
UMAMI_API_URL: str = os.getenv("UMAMI_API_URL", "https://umami.padelnomics.io")
UMAMI_API_TOKEN: str = os.getenv("UMAMI_API_TOKEN", "")
UMAMI_WEBSITE_ID: str = "4474414b-58d6-4c6e-89a1-df5ea1f49d70"
@@ -722,16 +726,39 @@ async def purge_deleted(table: str, days: int = 30) -> int:
# =============================================================================
async def get_price_id(key: str, provider: str = None) -> str | None:
"""Look up a provider price ID by product key from the payment_products table."""
provider = provider or config.PAYMENT_PROVIDER
row = await fetch_one(
"SELECT provider_price_id FROM payment_products WHERE provider = ? AND key = ?",
(provider, key),
)
return row["provider_price_id"] if row else None
async def get_all_price_ids(provider: str = None) -> dict[str, str]:
"""Load all price IDs for a provider as a {key: price_id} dict."""
provider = provider or config.PAYMENT_PROVIDER
rows = await fetch_all(
"SELECT key, provider_price_id FROM payment_products WHERE provider = ?",
(provider,),
)
return {r["key"]: r["provider_price_id"] for r in rows}
async def get_paddle_price(key: str) -> str | None:
"""Look up a Paddle price ID by product key from the paddle_products table."""
"""Deprecated: use get_price_id(). Falls back to paddle_products for pre-migration DBs."""
result = await get_price_id(key, provider="paddle")
if result:
return result
# Fallback to old table if payment_products not yet populated
row = await fetch_one("SELECT paddle_price_id FROM paddle_products WHERE key = ?", (key,))
return row["paddle_price_id"] if row else None
async def get_all_paddle_prices() -> dict[str, str]:
"""Load all Paddle price IDs as a {key: price_id} dict."""
rows = await fetch_all("SELECT key, paddle_price_id FROM paddle_products")
return {r["key"]: r["paddle_price_id"] for r in rows}
"""Deprecated: use get_all_price_ids()."""
return await get_all_price_ids(provider="paddle")
# =============================================================================

View File

@@ -0,0 +1,33 @@
"""Migration 0028: Generalize paddle_products → payment_products.
New table supports multiple payment providers (paddle, stripe).
Existing paddle_products rows are copied with provider='paddle'.
The old paddle_products table is kept (no drop) for backwards compatibility.
"""
def up(conn) -> None:
conn.execute("""
CREATE TABLE IF NOT EXISTS payment_products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
provider TEXT NOT NULL,
key TEXT NOT NULL,
provider_product_id TEXT NOT NULL,
provider_price_id TEXT NOT NULL,
name TEXT NOT NULL,
price_cents INTEGER NOT NULL,
currency TEXT NOT NULL DEFAULT 'EUR',
billing_type TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE(provider, key)
)
""")
# Copy existing paddle_products rows
conn.execute("""
INSERT OR IGNORE INTO payment_products
(provider, key, provider_product_id, provider_price_id, name, price_cents, currency, billing_type, created_at)
SELECT
'paddle', key, paddle_product_id, paddle_price_id, name, price_cents, currency, billing_type, created_at
FROM paddle_products
""")