Add receipt/attachment support to journal entries #11

Open
opened 2026-04-25 13:46:08 +00:00 by padreug · 1 comment
Owner

Summary

Users need to attach receipt photos or documents to their expense (and income) submissions. The frontend already has image upload infrastructure via pict-rs (img.<domain>), but castle has no way to store attachment URLs on journal entries.

Proposed Changes

1. Add attachments field to expense submission

class ExpenseEntry(BaseModel):
    # ... existing fields ...
    attachments: Optional[list[str]] = None  # List of image URLs (from pict-rs)

2. Store as Beancount metadata

Attachments can be stored as metadata on the journal entry:

2026-04-25 ! "Groceries at market" ^exp-abc123
  attachment-1: "https://img.example.com/image/abc.jpg"
  attachment-2: "https://img.example.com/image/def.jpg"
  Expenses:Supplies:Food    50.00 EUR @@ 55000 SATS
  Liabilities:Payable:User-xxx  -50.00 EUR @@ 55000 SATS

3. Return attachments in API responses

When querying entries via /entries/user or /entries/pending, include attachment URLs in the response so the frontend can display receipt thumbnails.

Context

The Castle standalone app implements an "expense drafts" feature where users can:

  1. Quickly snap a receipt photo at point of purchase
  2. Save it as a draft with the BTC price snapshot
  3. Complete the expense details later
  4. Submit with the receipt attached

Without this backend support, receipt URLs can only live in the draft (localStorage) and are lost when the expense is submitted.

Image Infrastructure

Images are hosted on pict-rs at VITE_PICTRS_BASE_URL (e.g., https://img.example.com). The frontend's ImageUploadService handles uploads and returns URLs. Castle just needs to store and return these URLs — no image processing needed on the backend.

## Summary Users need to attach receipt photos or documents to their expense (and income) submissions. The frontend already has image upload infrastructure via pict-rs (`img.<domain>`), but castle has no way to store attachment URLs on journal entries. ## Proposed Changes ### 1. Add `attachments` field to expense submission ```python class ExpenseEntry(BaseModel): # ... existing fields ... attachments: Optional[list[str]] = None # List of image URLs (from pict-rs) ``` ### 2. Store as Beancount metadata Attachments can be stored as metadata on the journal entry: ```beancount 2026-04-25 ! "Groceries at market" ^exp-abc123 attachment-1: "https://img.example.com/image/abc.jpg" attachment-2: "https://img.example.com/image/def.jpg" Expenses:Supplies:Food 50.00 EUR @@ 55000 SATS Liabilities:Payable:User-xxx -50.00 EUR @@ 55000 SATS ``` ### 3. Return attachments in API responses When querying entries via `/entries/user` or `/entries/pending`, include attachment URLs in the response so the frontend can display receipt thumbnails. ## Context The Castle standalone app implements an "expense drafts" feature where users can: 1. Quickly snap a receipt photo at point of purchase 2. Save it as a draft with the BTC price snapshot 3. Complete the expense details later 4. Submit with the receipt attached Without this backend support, receipt URLs can only live in the draft (localStorage) and are lost when the expense is submitted. ## Image Infrastructure Images are hosted on pict-rs at `VITE_PICTRS_BASE_URL` (e.g., `https://img.example.com`). The frontend's `ImageUploadService` handles uploads and returns URLs. Castle just needs to store and return these URLs — no image processing needed on the backend.
Author
Owner

Splitting the "drafts" workflow this issue's Context section motivates out into its own dedicated issue: #21 — Add server-persisted entry drafts (snap-and-finish-later workflow).

The two features compose but ship independently:

  • This issue (#11) stays scoped to attachments: the attachments field on entries, Beancount metadata storage, and surfacing URLs in API responses.
  • #21 covers server-persisted partial entries with a BTC/fiat price snapshot at capture time. Drafts will pick up attachments via this issue's mechanism once both land.

This issue's Context section ("expense drafts feature") can be read as motivation rather than scope — the actual draft implementation lives in #21.

Splitting the "drafts" workflow this issue's Context section motivates out into its own dedicated issue: **#21 — Add server-persisted entry drafts (snap-and-finish-later workflow)**. The two features compose but ship independently: - **This issue (#11)** stays scoped to attachments: the `attachments` field on entries, Beancount metadata storage, and surfacing URLs in API responses. - **#21** covers server-persisted partial entries with a BTC/fiat price snapshot at capture time. Drafts will pick up attachments via this issue's mechanism once both land. This issue's Context section ("expense drafts feature") can be read as motivation rather than scope — the actual draft implementation lives in #21.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/libra#11
No description provided.