-- Per-channel publishing for milestone posts. Run once against the target -- database BEFORE deploying the application version that uses this table. -- -- Consent is channel-bound: agreeing to a post on X says nothing about -- Instagram - different audience, different disclosure. Publishing state -- therefore moves out of the SocialMilestone row into one row per channel. -- The old "brand*" columns stay in place as a fallback and are backfilled -- below; nothing reads them any more. ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "instagramHandle" TEXT; CREATE TABLE IF NOT EXISTS "SocialMilestonePost" ( "id" TEXT PRIMARY KEY, "milestoneId" TEXT NOT NULL REFERENCES "SocialMilestone"("id") ON DELETE CASCADE, "channel" TEXT NOT NULL, -- 'x' | 'instagram' "status" TEXT NOT NULL DEFAULT 'approved', -- approved | processing | posted | failed | revoked -- The text the customer read before consenting. Published verbatim. "consentText" TEXT NOT NULL, "handle" TEXT, "approvedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "claimedAt" TIMESTAMP(3), "postedAt" TIMESTAMP(3), "postUrl" TEXT, "error" TEXT, "attempts" INTEGER NOT NULL DEFAULT 0, "nextAttemptAt" TIMESTAMP(3), -- A row exists only where consent exists. No row means: not approved. CONSTRAINT "SocialMilestonePost_milestone_channel_key" UNIQUE ("milestoneId", "channel") ); CREATE INDEX IF NOT EXISTS "SocialMilestonePost_channel_status_approvedAt_idx" ON "SocialMilestonePost" ("channel", "status", "approvedAt"); -- Existing X consents keep working. `processing` becomes `approved` again: the -- publisher reconciles against the timeline before it posts, so a re-claim -- cannot duplicate a post that already went out. INSERT INTO "SocialMilestonePost" ("id", "milestoneId", "channel", "status", "consentText", "approvedAt", "postedAt", "postUrl", "error", "attempts") SELECT gen_random_uuid()::text, "id", 'x', CASE WHEN "brandStatus" = 'processing' THEN 'approved' ELSE "brandStatus" END, "consentText", COALESCE("brandApprovedAt", "detectedAt"), "brandPostedAt", "brandPostUrl", "brandPostError", "attempts" FROM "SocialMilestone" WHERE "consentText" IS NOT NULL AND "brandStatus" IN ('approved', 'processing', 'posted', 'failed', 'revoked') ON CONFLICT ("milestoneId", "channel") DO NOTHING;