Skip to content

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 by file_id (no byte round-trip) as a photo message captioned with the note ask + force-reply. No note: token β€” replies are matched by the prompt's own message_id, stored on the pending doc as promptMessageId (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. The send-summary step 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 to main via PR. Full unfiltered NODE_OPTIONS=--max-old-space-size=8192 npx tsc --noEmit clean 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, handleNoteReply signature, morph paths) + worker status/summary plumbing (statusMessageKind, send-summary return shape). Pending docs gain promptMessageId. 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.