From 276328af33e15491cd3f0d6b523fdda5b32fbb00 Mon Sep 17 00:00:00 2001 From: Deeman Date: Tue, 3 Mar 2026 15:07:10 +0100 Subject: [PATCH] =?UTF-8?q?feat(billing):=20A1+A3=20=E2=80=94=20payment=5F?= =?UTF-8?q?products=20table=20+=20provider-agnostic=20price=20lookups?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- web/src/padelnomics/core.py | 37 ++++++++++++++++--- .../0028_generalize_payment_products.py | 33 +++++++++++++++++ 2 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 web/src/padelnomics/migrations/versions/0028_generalize_payment_products.py diff --git a/web/src/padelnomics/core.py b/web/src/padelnomics/core.py index ed5c905..1894322 100644 --- a/web/src/padelnomics/core.py +++ b/web/src/padelnomics/core.py @@ -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") # ============================================================================= diff --git a/web/src/padelnomics/migrations/versions/0028_generalize_payment_products.py b/web/src/padelnomics/migrations/versions/0028_generalize_payment_products.py new file mode 100644 index 0000000..2c8beff --- /dev/null +++ b/web/src/padelnomics/migrations/versions/0028_generalize_payment_products.py @@ -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 + """)