From c7ea516c34dad89febcb6996caaea468a99c2d38 Mon Sep 17 00:00:00 2001 From: siddharthd Date: Fri, 21 Aug 2026 12:10:05 +1000 Subject: [PATCH] =?UTF-8?q?slack:=20dispatch=20the=20four=20intake=20taps?= =?UTF-8?q?=20on=20action=5Fid=20(plan=20=C2=A711=20C5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit intake_use_candidate / intake_create / intake_grocery / intake_meal carry JSON {batch_id, ...} rather than :, so they branch before the order-nudge parse. They act on the persisted batch: enrich the chosen transaction, create ING-10-style (statement_id NULL, owner_id from shared_by, source/source_ref make a second press a no-op), or remember the merchant's kind and route. A tap on an already-resolved batch returns its current card and writes nothing. Card updates go to response_url, as the existing handlers do. --- src/app/api/slack/interactive/route.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/app/api/slack/interactive/route.ts b/src/app/api/slack/interactive/route.ts index a622ccd..08f0689 100644 --- a/src/app/api/slack/interactive/route.ts +++ b/src/app/api/slack/interactive/route.ts @@ -6,6 +6,8 @@ import { slackUserForParticipant, } from "@/lib/slack-verify"; import { nudgeBlocks, detailsModal, partnerNudgeBlocks } from "@/lib/slack-blocks"; +import { handleIntakeAction, isIntakeAction, parseActionValue } from "@/lib/intake-actions"; +import { pantryForwarder } from "@/lib/intake-pantry"; import { completeSplit } from "@/lib/splits"; import { RATINGS, @@ -73,6 +75,22 @@ export async function POST(req: NextRequest) { if (payload.type !== "block_actions") return new NextResponse(null, { status: 200 }); const action = payload.actions?.[0]; + + // Intake taps (plan ยง11 C5) act on a persisted batch, not a transaction, so + // their `value` is JSON `{batch_id, ...}` rather than `:`. They are + // dispatched on action_id before the order-nudge parse below, which would + // otherwise read the JSON as a non-numeric transaction id and give up. + if (isIntakeAction(action?.action_id)) { + const reply = await handleIntakeAction(action.action_id, parseActionValue(action?.value), { pantry: pantryForwarder() }); + await updateMessage( + payload.response_url, + reply.replace + ? { replace_original: true, blocks: reply.blocks, text: reply.text } + : { response_type: "ephemeral", replace_original: false, text: reply.text } + ); + return NextResponse.json({ blocks: reply.blocks, text: reply.text }); + } + // A button carries `value`; a select carries it on the chosen option. The // ratings moved to a select because four buttons became four full-width rows // on Slack mobile, so both shapes have to resolve or rating silently stops