veza/veza-backend-api/internal
senke 921889840f feat(marketplace): multi-creator royalty splits with audit ledger
v1.0.10 légal item 4. Marketplace products can now have a per-recipient
payout structure ; each purchase fans out the net (post-platform-fee)
amount across the recipients per their basis_points share. Audit ledger
captures every change for legal-evidence purposes.

Without this, a co-produced track gets paid to the registered seller
only and the contributors must chase reimbursement off-platform =
contentieux risk. F250 in the ORIGIN spec called this out as a v2.0.0
blocker ; this commit closes the gap.

Schema (migrations/992_royalty_splits.sql)
  * royalty_splits        : (product_id, recipient_user_id, basis_points, role_label).
                            UNIQUE on (product_id, recipient_user_id).
                            CHECK : basis_points in (0, 10000]. Sum-to-10000
                            invariant lives in the service layer (cross-row).
  * royalty_splits_audit  : append-only history. action ∈ {set, replace,
                            remove}. previous_splits + new_splits as
                            JSONB snapshots. Never deleted.
  ON DELETE :
    products  → CASCADE   (a deleted product takes its splits with it)
    users     → RESTRICT  (a recipient must be removed from splits before
                            their account can be deleted ; preserves payment
                            history coherence)

Service (internal/core/marketplace/royalty_splits.go)
  * GetRoyaltySplits(productID)                — public read.
  * SetRoyaltySplits(actor, productID, inputs, reason)
      Validations : seller-owned, sum == 10000 bps, no duplicate
      recipients, all recipients exist, each bp in (0, 10000].
      Single transaction : delete old rows + bulk insert new + audit
      entry. action='set' on first write, 'replace' afterwards.
  * RemoveRoyaltySplits(actor, productID, reason)
      Idempotent. action='remove'. Reverts the product to single-seller
      payout on the next purchase.
  * distributePerProductSplits(productID) → recipient → bps map. Used
    by processSellerTransfers ; nil result triggers the legacy path.
  Sentinel errors :
      ErrSplitsForbidden / ErrSplitsSumInvalid / ErrSplitsRecipientDup /
      ErrSplitsRecipientNF / ErrSplitsBPRange.

Hook (service.go::processSellerTransfers)
  Per-item resolution : if the product has splits, fan the net out
  across recipients (rounding remainder absorbed by the dominant
  recipient so the total stays exact) ; otherwise the legacy
  single-seller path runs. SellerTransfer rows still get one per
  recipient, with the originating seller's commission rate carried
  through for audit. Mixed orders (some products with splits, some
  without) are handled correctly.

Handler (internal/handlers/royalty_splits_handler.go)
  * GET    /api/v1/marketplace/products/:id/royalty-splits   public
  * PUT    /api/v1/marketplace/products/:id/royalty-splits   seller-only
  * DELETE /api/v1/marketplace/products/:id/royalty-splits   seller-only
  Error mapping : sentinel → AppError code so the SPA can render the
  right toast without parsing messages. Both PUT and DELETE go through
  the existing RequireOwnershipOrAdmin middleware (defense in depth ;
  service layer also checks).

What v1.0.10 leaves to v2.1
  * UI for managing splits (product editor) — backend-complete here ;
    UI follows. Operators can already configure splits via the API.
  * Dispute workflow (third-party arbitration when a recipient
    contests their share). For v2.0.0 the legal coverage is "splits
    are visible publicly, audit log is append-only, contentieux goes
    through legal channels with the audit log as evidence."
  * Tax allocation (each recipient may be in a different tax
    jurisdiction). Splits today distribute the gross-minus-fee evenly
    by share ; per-jurisdiction tax math comes later.

Tests pass : go test ./internal/core/marketplace ./internal/handlers
              -short → ok.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 20:53:22 +02:00
..
api feat(marketplace): multi-creator royalty splits with audit ledger 2026-05-01 20:53:22 +02:00
common v0.9.2 2026-03-05 19:27:34 +01:00
config feat(stream): HLS default on + marketplace 30s pre-listen + FLAC tier checkbox (W4 Day 17) 2026-04-29 09:56:02 +02:00
core feat(marketplace): multi-creator royalty splits with audit ledger 2026-05-01 20:53:22 +02:00
database v0.9.4 2026-03-05 23:03:43 +01:00
dto feat(auth): RGPD/COPPA age gate at registration (16+ minimum) 2026-05-01 18:05:47 +02:00
elasticsearch fix(backend): unblock handlers + elasticsearch test packages 2026-04-30 14:48:23 +02:00
email refactor(backend,infra): unify SMTP env schema on canonical SMTP_* names 2026-04-16 20:44:09 +02:00
errors v0.9.8 2026-03-06 19:13:16 +01:00
eventbus fix(eventbus): log RabbitMQ publish failures instead of silent drop 2026-04-16 20:50:51 +02:00
features adding initial backend API (Go) 2025-12-03 20:29:37 +01:00
handlers feat(marketplace): multi-creator royalty splits with audit ledger 2026-05-01 20:53:22 +02:00
infrastructure v0.9.4 2026-03-05 23:03:43 +01:00
integration style(backend): gofmt -w on 85 files (whitespace only) 2026-04-14 12:22:14 +02:00
interfaces adding initial backend API (Go) 2025-12-03 20:29:37 +01:00
jobs feat(webhooks): persist raw hyperswitch payloads to audit log — v1.0.7 item E 2026-04-18 02:44:58 +02:00
logging style(backend): gofmt -w on 85 files (whitespace only) 2026-04-14 12:22:14 +02:00
metrics feat(cdn): Bunny.net signed URLs + HLS cache headers + metric collision fix (W3 Day 13) 2026-04-28 14:07:20 +02:00
middleware feat(redis): Sentinel HA + cache hit rate metrics (W3 Day 11) 2026-04-28 13:36:55 +02:00
models feat(legal): DMCA notice handler + admin queue + 451 playback gate (W3 Day 14) 2026-04-28 15:39:33 +02:00
monitoring feat(cdn): Bunny.net signed URLs + HLS cache headers + metric collision fix (W3 Day 13) 2026-04-28 14:07:20 +02:00
pagination v0.9.8 2026-03-06 19:13:16 +01:00
recovery chore(v0.102): consolidate remaining changes — docs, frontend, backend 2026-02-20 13:02:12 +01:00
repositories fix(v0.12.6.1): remediate 2 CRITICAL + 10 HIGH + 1 MEDIUM pentest findings 2026-03-12 05:40:53 +01:00
resilience chore: consolidate CI, E2E, backend and frontend updates 2026-02-17 16:43:21 +01:00
response fix: stabilize builds, tests, and lint across all stacks 2026-04-05 16:48:07 +02:00
security refactor(backend): replace 40 fmt.Printf calls with zap structured logging 2026-02-22 17:44:38 +01:00
services feat(legal): versioned terms acceptance ledger (CGU/CGV/mentions) 2026-05-01 20:47:07 +02:00
shutdown incus deployement fully implemented, Makefile updated and make fmt ran 2026-01-13 19:47:57 +01:00
testutils ci: retire legacy backend-ci.yml, centralize Docker probe in SkipIfNoIntegration 2026-04-15 16:12:45 +02:00
tracing feat(observability): OTel SDK + collector + Tempo + 4 hot path spans (W2 Day 9) 2026-04-28 01:15:11 +02:00
types fix(backend): commit swagger annotation pass + missing handler methods 2026-05-01 10:16:57 +02:00
upload [INT-015] int: Add file upload format standardization 2025-12-25 15:40:01 +01:00
utils fix(v0.12.6): apply all pentest remediations — 36 findings across 36 files 2026-03-14 00:44:46 +01:00
validators feat(v0.13.3): complete - Polish Sécurité Avancée 2026-03-13 10:09:01 +01:00
websocket feat(redis): Sentinel HA + cache hit rate metrics (W3 Day 11) 2026-04-28 13:36:55 +02:00
workers feat(transcode): read from S3 signed URL when track is s3-backed (v1.0.8 P2) 2026-04-23 23:34:51 +02:00