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:
@@ -49,13 +49,17 @@ class Config:
|
|||||||
MAGIC_LINK_EXPIRY_MINUTES: int = int(os.getenv("MAGIC_LINK_EXPIRY_MINUTES", "15"))
|
MAGIC_LINK_EXPIRY_MINUTES: int = int(os.getenv("MAGIC_LINK_EXPIRY_MINUTES", "15"))
|
||||||
SESSION_LIFETIME_DAYS: int = int(os.getenv("SESSION_LIFETIME_DAYS", "30"))
|
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_API_KEY: str = os.getenv("PADDLE_API_KEY", "")
|
||||||
PADDLE_CLIENT_TOKEN: str = os.getenv("PADDLE_CLIENT_TOKEN", "")
|
PADDLE_CLIENT_TOKEN: str = os.getenv("PADDLE_CLIENT_TOKEN", "")
|
||||||
PADDLE_WEBHOOK_SECRET: str = os.getenv("PADDLE_WEBHOOK_SECRET", "")
|
PADDLE_WEBHOOK_SECRET: str = os.getenv("PADDLE_WEBHOOK_SECRET", "")
|
||||||
PADDLE_ENVIRONMENT: str = _env("PADDLE_ENVIRONMENT", "sandbox")
|
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_URL: str = os.getenv("UMAMI_API_URL", "https://umami.padelnomics.io")
|
||||||
UMAMI_API_TOKEN: str = os.getenv("UMAMI_API_TOKEN", "")
|
UMAMI_API_TOKEN: str = os.getenv("UMAMI_API_TOKEN", "")
|
||||||
UMAMI_WEBSITE_ID: str = "4474414b-58d6-4c6e-89a1-df5ea1f49d70"
|
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:
|
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,))
|
row = await fetch_one("SELECT paddle_price_id FROM paddle_products WHERE key = ?", (key,))
|
||||||
return row["paddle_price_id"] if row else None
|
return row["paddle_price_id"] if row else None
|
||||||
|
|
||||||
|
|
||||||
async def get_all_paddle_prices() -> dict[str, str]:
|
async def get_all_paddle_prices() -> dict[str, str]:
|
||||||
"""Load all Paddle price IDs as a {key: price_id} dict."""
|
"""Deprecated: use get_all_price_ids()."""
|
||||||
rows = await fetch_all("SELECT key, paddle_price_id FROM paddle_products")
|
return await get_all_price_ids(provider="paddle")
|
||||||
return {r["key"]: r["paddle_price_id"] for r in rows}
|
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
@@ -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
|
||||||
|
""")
|
||||||
Reference in New Issue
Block a user