From 2e149fc1db4a100a98ed4dff198f1c9fc7a9ba36 Mon Sep 17 00:00:00 2001 From: Deeman Date: Sat, 28 Feb 2026 18:35:27 +0100 Subject: [PATCH] =?UTF-8?q?feat(affiliate):=20migration=200026=20=E2=80=94?= =?UTF-8?q?=20affiliate=5Fproducts=20+=20affiliate=5Fclicks=20tables?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds affiliate product catalog and click tracking tables. UNIQUE(slug, language) mirrors articles schema for multi-language support. Co-Authored-By: Claude Sonnet 4.6 --- .../versions/0026_affiliate_products.py | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 web/src/padelnomics/migrations/versions/0026_affiliate_products.py diff --git a/web/src/padelnomics/migrations/versions/0026_affiliate_products.py b/web/src/padelnomics/migrations/versions/0026_affiliate_products.py new file mode 100644 index 0000000..4d74599 --- /dev/null +++ b/web/src/padelnomics/migrations/versions/0026_affiliate_products.py @@ -0,0 +1,65 @@ +"""Migration 0026: Affiliate product catalog + click tracking tables. + +affiliate_products: admin-managed product catalog for editorial affiliate cards. + - slug+language uniqueness mirrors articles (same slug can exist in DE + EN + with different affiliate URLs, copy, and pros/cons). + - retailer: display name (Amazon, Padel Nuestro, etc.) — stored in full URL + with tracking params already baked into affiliate_url. + - cta_label: per-product override; empty → use i18n default "Zum Angebot". + - status: draft/active/archived — only active products are baked into articles. + +affiliate_clicks: one row per /go/ redirect hit. + - ip_hash: SHA256(ip + YYYY-MM-DD + SECRET_KEY[:16]), daily rotation for GDPR. + - article_slug: best-effort extraction from Referer header. +""" + + +def up(conn) -> None: + conn.execute(""" + CREATE TABLE affiliate_products ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + slug TEXT NOT NULL, + name TEXT NOT NULL, + brand TEXT NOT NULL DEFAULT '', + category TEXT NOT NULL DEFAULT 'accessory', + retailer TEXT NOT NULL DEFAULT '', + affiliate_url TEXT NOT NULL, + image_url TEXT NOT NULL DEFAULT '', + price_cents INTEGER, + currency TEXT NOT NULL DEFAULT 'EUR', + rating REAL, + pros TEXT NOT NULL DEFAULT '[]', + cons TEXT NOT NULL DEFAULT '[]', + description TEXT NOT NULL DEFAULT '', + cta_label TEXT NOT NULL DEFAULT '', + status TEXT NOT NULL DEFAULT 'draft', + language TEXT NOT NULL DEFAULT 'de', + sort_order INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT, + UNIQUE(slug, language) + ) + """) + conn.execute(""" + CREATE TABLE affiliate_clicks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + product_id INTEGER NOT NULL REFERENCES affiliate_products(id), + article_slug TEXT, + referrer TEXT, + ip_hash TEXT NOT NULL, + clicked_at TEXT NOT NULL DEFAULT (datetime('now')) + ) + """) + # Queries: products by category+status, clicks by product and time + conn.execute( + "CREATE INDEX idx_affiliate_products_category_status" + " ON affiliate_products(category, status)" + ) + conn.execute( + "CREATE INDEX idx_affiliate_clicks_product_id" + " ON affiliate_clicks(product_id)" + ) + conn.execute( + "CREATE INDEX idx_affiliate_clicks_clicked_at" + " ON affiliate_clicks(clicked_at)" + )