T 176
uid: T-176 title: Receipt bot β one morphing message per receipt (photo prompt β status β summary) status: done area: telegram-receipts created: 2026-07-22 updated: 2026-07-22 owner: girafeev1 assignee: Records (Infrastructure) related: T-169, T-173, T-174
Goal (owner, 2026-07-22)¶
"I want the bot to handle each receipt with its own message without creating too many messages. So, user uploads receipt image with no substantiation notes > prompt substantiation note required message with the original uploaded image > user send substantiation note in reply to the warning message, the warning message itself transforms into the next steps."
Before: a no-caption upload produced a text note-prompt (with an ugly note:<pendingId> token),
which lingered forever; the note reply spawned a status message (deleted later); the summary was a
third message. After: the bot holds one message per receipt for its entire life.
What shipped¶
- The prompt carries the image (
sendPhotoNotePrompt): the user's upload is re-sent byfile_id(no byte round-trip) as a photo message captioned with the note ask + force-reply. Nonote:token β replies are matched by the prompt's ownmessage_id, stored on the pending doc aspromptMessageId(equality-only Firestore lookup; the token matcher stays for legacy prompts and the fallback below). - The prompt morphs in place at every stage: note captured β caption becomes
π Got it: <note>+πΈ Filing under β¦(T-173 header); the Inngest worker live-edits the caption through Downloading β Reading β Cleaning β Cropping β Sending; finally the summary replaces the same message's media + caption (editMessageMedia: cleaned image + parsed summary + T-174 keyboard). No status-message delete, no fresh summary message. The multi-subsidiary picker, the "subsidiary not wired up" dead-end, the not-a-receipt rejection, and the retries-exhausted failure notice all edit the same message too. - Worker contract:
ReceiptPhotoEvent.statusMessageKind?: 'text' | 'photo'β photo cards take caption edits and the in-place summary morph (with fresh-message fallback if the morph fails); absent/'text' keeps the pre-T-176 path byte-identical. Thesend-summarystep now returns{id, morphed}; replay of in-flight runs that memoized a bare number is tolerated. - Fallbacks preserved: image-as-DOCUMENT uploads (whose file_ids can't be re-sent as photos) and any sendPhoto failure fall back to the old text prompt + token matching; the caption-note flow (Use/Edit keyboard) is unchanged and keeps the T-173 text ack.
Log¶
- 2026-07-22 β created + done (Records Infrastructure; owner flow spec quoted above, proposed and approved in the same message β "Do you understand what i mean?" answered by this build). β Read AGENTS.md Β· checked the board by scope (T-169 = prompt differentiation, T-173 = note-in-status, T-174 = category picker; none owns the single-morphing-message flow β no dup). Source: Records (Infrastructure) Β· https://claude.ai/code/session_018RDB37kCqfouHdygVXTAtD
- SHAs:
a0cbbffc(code) + the docs commit carrying this record; merged tomainvia PR. Full unfilteredNODE_OPTIONS=--max-old-space-size=8192 npx tsc --noEmitclean pre-merge. - Deploy pending β merged, not deployed (deploys are owner-ordered). Owner runtime-verify
(after the next deploy): upload a captionless photo β ONE bot message appears showing your
image + the note ask (no
note:token); reply with a note β that same message walks the processing statuses and ends as the summary card (cleaned image + buttons). Final chat per receipt: your photo, your note reply, one bot card. - Blast-radius: Telegram receipt UX β webhook prompt/routing (
resolveNotePendingId,handleNoteReplysignature, morph paths) + worker status/summary plumbing (statusMessageKind,send-summaryreturn shape). Pending docs gainpromptMessageId. In-flight receipts at deploy time: old prompts still match via token; old Inngest runs replay with the tolerant step-shape read. Web app, store schema, category/edit flows untouched.