feat(affiliate): add affiliate_programs table + migration 0027

Creates affiliate_programs for centralised retailer config (URL template,
tracking tag, commission %). Adds nullable program_id + product_identifier
to affiliate_products for backward compat. Seeds "Amazon" program with
oneLink template. Backfills existing products by extracting ASINs from
baked affiliate_url values.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-02-28 22:23:00 +01:00
parent 6aae92fc58
commit b1eeb0a0ac

View File

@@ -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/<ASIN> 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),
)