diff --git a/web/src/padelnomics/migrations/versions/0027_affiliate_programs.py b/web/src/padelnomics/migrations/versions/0027_affiliate_programs.py new file mode 100644 index 0000000..698c3d6 --- /dev/null +++ b/web/src/padelnomics/migrations/versions/0027_affiliate_programs.py @@ -0,0 +1,79 @@ +"""Migration 0027: Affiliate programs table + program FK on products. + +affiliate_programs: centralises retailer configs (URL template + tag + commission). + - url_template uses {product_id} and {tag} placeholders, assembled at redirect time. + - tracking_tag: e.g. "padelnomics-21" — changing it propagates to all products instantly. + - commission_pct: stored as a decimal (0.03 = 3%) for revenue estimates. + - status: active/inactive — only active programs appear in the product form dropdown. + - notes: internal field for login URLs, account IDs, etc. + +affiliate_products changes: + - program_id (nullable FK): new products use a program; existing products keep their + baked affiliate_url (backward compat via build_affiliate_url() fallback). + - product_identifier: ASIN, product path, or other program-specific ID (e.g. B0XXXXX). + +Amazon OneLink note: we use a single "Amazon" program pointing to amazon.de. +Amazon OneLink (configured in the Associates dashboard, no code changes needed) +auto-redirects visitors to their local marketplace (UK→amazon.co.uk, ES→amazon.es) +with the correct regional tag. One program covers all Amazon marketplaces. +""" +import re + + +def up(conn) -> None: + conn.execute(""" + CREATE TABLE affiliate_programs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + slug TEXT NOT NULL UNIQUE, + url_template TEXT NOT NULL, + tracking_tag TEXT NOT NULL DEFAULT '', + commission_pct REAL NOT NULL DEFAULT 0, + homepage_url TEXT NOT NULL DEFAULT '', + status TEXT NOT NULL DEFAULT 'active', + notes TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT + ) + """) + + # Seed the default Amazon program. + # OneLink handles geo-redirect to local marketplaces — no per-country programs needed. + conn.execute(""" + INSERT INTO affiliate_programs (name, slug, url_template, tracking_tag, commission_pct, homepage_url) + VALUES ('Amazon', 'amazon', 'https://www.amazon.de/dp/{product_id}?tag={tag}', 'padelnomics-21', 3.0, 'https://www.amazon.de') + """) + + # Add program FK + product identifier to products table. + # program_id is nullable — existing rows keep their baked affiliate_url. + conn.execute(""" + ALTER TABLE affiliate_products + ADD COLUMN program_id INTEGER REFERENCES affiliate_programs(id) + """) + conn.execute(""" + ALTER TABLE affiliate_products + ADD COLUMN product_identifier TEXT NOT NULL DEFAULT '' + """) + + # Backfill: extract ASIN from existing Amazon affiliate URLs. + # Pattern: /dp/ where ASIN is 10 uppercase alphanumeric chars. + amazon_program = conn.execute( + "SELECT id FROM affiliate_programs WHERE slug = 'amazon'" + ).fetchone() + assert amazon_program is not None, "Amazon program must exist after seed" + amazon_id = amazon_program[0] + + rows = conn.execute( + "SELECT id, affiliate_url FROM affiliate_products" + ).fetchall() + asin_re = re.compile(r"/dp/([A-Z0-9]{10})") + for product_id, url in rows: + if not url: + continue + m = asin_re.search(url) + if m: + asin = m.group(1) + conn.execute( + "UPDATE affiliate_products SET program_id=?, product_identifier=? WHERE id=?", + (amazon_id, asin, product_id), + )