-- 992_royalty_splits.sql
-- v1.0.10 légal item 4 — multi-creator royalty distribution.
--
-- Background : a marketplace product (track / sample pack / etc.) is
-- traditionally owned by a single seller (products.seller_id). When a
-- piece is co-produced (featuring artist, ghost producer, splits with
-- a label), the platform must distribute the net payout across all
-- contributors per their agreed share. Without this table, a co-prod
-- gets paid to the registered seller only and the contributors have
-- to chase reimbursement off-platform = contentieux risk.
--
-- Design :
--   - Splits are defined per product. A product without any rows in
--     this table behaves exactly as before (100% to seller_id) — pure
--     additive feature, zero migration risk for existing products.
--   - basis_points is integer percentage × 100 (so 25.00% is 2500).
--     Avoids float-precision drift across sum-to-100 invariants.
--   - The `(product_id, recipient_user_id)` pair is unique : one row
--     per recipient per product. To split 50/30/20, three rows.
--   - The CHECK constraint guards individual rows ; the sum-to-10000
--     invariant is enforced application-side (set_splits service)
--     because PG triggers across rows are fragile + the validation
--     wants to surface a 400 with a friendly message anyway.
--
-- The audit table records every change : who set/edited/removed,
-- when, what the previous shares were. Never deleted ; legal
-- evidence in case of dispute.

CREATE TABLE IF NOT EXISTS public.royalty_splits (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    product_id          UUID NOT NULL REFERENCES public.products(id) ON DELETE CASCADE,
    -- Recipient user. ON DELETE RESTRICT so deleting a user blocks
    -- when they're a split recipient — the operator must redistribute
    -- the splits before erasing the row, preserving payment history
    -- coherence.
    recipient_user_id   UUID NOT NULL REFERENCES public.users(id) ON DELETE RESTRICT,
    -- Share in basis points (1% = 100 bps). Range constraint catches
    -- typos but the sum-to-10000 invariant lives in the service layer.
    basis_points        INTEGER NOT NULL CHECK (basis_points > 0 AND basis_points <= 10000),
    -- Optional human label so the seller's UI can show e.g.
    -- "Producer", "Featuring artist", "Label cut". Free text, no
    -- enum to keep flexibility.
    role_label          VARCHAR(64),
    created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at          TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

COMMENT ON TABLE public.royalty_splits IS
    'Multi-creator payout splits per product. v1.0.10 légal item 4.';
COMMENT ON COLUMN public.royalty_splits.basis_points IS
    'Share in basis points (10000 = 100%). Sum across rows for a product MUST equal 10000 (enforced in the service).';

-- Lookup by product_id is the hot path (every order processing call
-- queries it). Combined unique on (product_id, recipient_user_id) so
-- the same recipient can't appear twice on the same product.
CREATE UNIQUE INDEX IF NOT EXISTS idx_royalty_splits_product_recipient
    ON public.royalty_splits(product_id, recipient_user_id);

-- Reverse lookup : "what splits is user X part of ?" — useful for the
-- creator's dashboard when they're a featuring on someone else's
-- product.
CREATE INDEX IF NOT EXISTS idx_royalty_splits_recipient
    ON public.royalty_splits(recipient_user_id);

-- Audit ledger. One row per change event — set, replace, remove.
-- previous_splits is a JSONB snapshot of the rows as they were
-- before the change ; replaying the audit log reconstructs any
-- historical state.
CREATE TABLE IF NOT EXISTS public.royalty_splits_audit (
    id                UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    product_id        UUID NOT NULL REFERENCES public.products(id) ON DELETE CASCADE,
    -- Operator who triggered the change (the product owner, an admin
    -- making a correction, or a system process for migrations). NULL
    -- only for backfills.
    actor_user_id     UUID REFERENCES public.users(id) ON DELETE SET NULL,
    -- One of 'set' (initial), 'replace' (changed shares), 'remove'
    -- (back to single-seller). Enum-by-convention.
    action            VARCHAR(16) NOT NULL CHECK (action IN ('set', 'replace', 'remove')),
    -- JSONB snapshot of the rows before the change. Empty array on
    -- the initial 'set' action. Each entry :
    --   { "recipient_user_id": "uuid", "basis_points": int, "role_label": "string" }
    previous_splits   JSONB NOT NULL DEFAULT '[]'::jsonb,
    -- JSONB snapshot of the rows after the change. Empty array on
    -- 'remove'.
    new_splits        JSONB NOT NULL DEFAULT '[]'::jsonb,
    -- Free-text reason captured from the operator (e.g. "Featuring
    -- artist agreement v2 signed"). Surface in the audit UI ; not
    -- required.
    reason            TEXT,
    created_at        TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

COMMENT ON TABLE public.royalty_splits_audit IS
    'Append-only ledger of every royalty-splits change. Legal evidence ; never delete.';

CREATE INDEX IF NOT EXISTS idx_royalty_splits_audit_product
    ON public.royalty_splits_audit(product_id, created_at DESC);
