Charge posts to the resolved account, never an artist (app#2248)

Merged
SW
1d ago
+3132-156

Implements the api step of app#2248. Contract: marketing#183. Column: database#117.

Decision 2026-10-19 (Sweets): every request has a caller, a resolved account_id (the caller's account, or an org/product admin's override), and an artist within that account. Credits are checked and deducted on the resolved account_id; an artist is never charged.

POST /api/posts

  • New optional artist_account_id (uuid): the artist whose connection posts. The connection is looked up on artist_account_id ?? accountId, at create time and again in publishPostStep.
  • validatePostAccounts (new): 400 account_id is an artist; pass it as artist_account_id when the resolved account is an artist; 403 unless checkAccountArtistAccess(accountId, artist_account_id). Runs before the connection lookup, credit check or insert.
  • isArtistAccount (new): held in account_artist_ids (selectArtistOwnerLink, new) or artist_organization_ids. A lookup error throws → 500, never "not an artist".
  • Credits unchanged in code: ensurePostCredits and the workflow's recordCreditDeduction already use the resolved accountId; with the artist split out, that is always the account.
  • startPost stores artist_account_id (null when omitted).

GET /api/posts: artist_account_id filter; every Post returns artist_account_id.

Tests (new files, RED first): selectArtistOwnerLink, isArtistAccount, validatePostAccounts, createPostHandler.artist, validateCreatePostBody.artist, startPost.artist, publishPostStep.artist, getPostsHandler.artist. Existing tests: validatePostAccounts mocked in the two createPostHandler suites; artist_account_id: null added to startPost's exact insert. Posts, artists, account_artist_ids, scheduled_posts and contracts suites: 263/263.

Types: tsc reports artist_account_id missing from scheduled_posts in types/database.types.ts until database#117 is applied and types are regenerated (pnpm update-types); no other errors in the touched files.

Docs fixture: social.json hash = marketing#183.

Merge order: marketing#183 → database#117 → this (after types are regenerated) → docs publication copy.

🤖 Generated with Claude Code


Summary by cubic

Implements the API step for charging posts to the resolved account, never an artist (app#2248). A post may name an artist via artist_account_id: the artist's connection publishes, while credits are charged to the resolved account. Artist accounts passed as account_id are rejected with 400, and artists the account cannot access with 403.

Changes

  • Validation runs before any connection lookup or charge; lookup failures throw, returning 500 instead of being misread as "not an artist".
  • selectAccountArtistIds now takes account and/or artist filters and returns null on error, replacing selectArtistOwnerLink as the source for isArtistAccount.
  • The publishing workflow resolves the connection on artist_account_id ?? account_id, at create time and again in publishPostStep.
  • GET /api/posts filters by artist_account_id, and every post returns it.
  • scheduled_posts stores artist_account_id (null when omitted); database types are regenerated.

Migration

  • Requires database#117 to be applied.

Summary by CodeRabbit

  • New Features
    • Posts can be published through an artist’s connected account when an artist account is selected; otherwise, publishing uses the account’s own connection.
    • Create, retrieve, and list scheduled posts for a specific artist account using the optional artist account identifier.
  • Bug Fixes
    • Post requests now validate account access before checking connectivity, with clear errors for invalid or inaccessible artist accounts.
    • Scheduled posts now consistently retain and return their associated artist account.