feat(affiliate): migration 0026 — affiliate_products + affiliate_clicks tables

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 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-02-28 18:35:27 +01:00
parent 6fb1e990e3
commit 2e149fc1db

View File

@@ -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/<slug> 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)"
)