Skip to content

feat(expenses): receipts — validated private image/PDF (part of #71) - #75

Merged
vibhavkatre merged 2 commits into
developfrom
feat/expense-receipts-71
Aug 5, 2026
Merged

feat(expenses): receipts — validated private image/PDF (part of #71)#75
vibhavkatre merged 2 commits into
developfrom
feat/expense-receipts-71

Conversation

@vibhavkatre

Copy link
Copy Markdown
Collaborator

Part of #71. Adds receipts to expenses (the deferred bulk of #71; live ECB auto-conversion is the small remainder).

Attach an optional image or PDF receipt to any expense. Security-critical, so built by hand and reusing the Audio file-validation pattern:

  • Validated by magic bytes (toolbox/receipt_files.py) — never the client's claimed MIME/filename — against an allowlist (JPEG/PNG/GIF/WebP/PDF), size-capped at 10 MB.
  • Stored as a private Frappe File attached to the owner's expense; the endpoint is owner-checked + rate-limited.
  • Attaching a new receipt replaces the old file; remove or deleting the expense deletes the file (no orphans).

Frontend

Receipt control in the expense form (attach once saved; view/remove an existing one) and a "Receipt" chip on ledger rows linking to the private file. The composable reads the file to base64, calls the validated endpoint, and updates the form + list in place.

Tests & verification

  • Backend: 8 tests (magic-byte detect accept/reject, private-file create, non-receipt rejected, replace-removes-old, remove clears+deletes, expense-delete removes file, cross-user rejection). Expense-module backend now 49 tests.
  • Frontend: +1 composable test; full suite 652 passing; build clean.
  • Verified live: an expense with a receipt shows the 📎 Receipt chip on its row; the row links to the private file.

Note: Frappe runs its own PIL/pypdf scan on image/PDF uploads (rejecting unparseable or JS-bearing files), which layers on top of the magic-byte allowlist.

🤖 Generated with Claude Code

vibhavkatre and others added 2 commits August 5, 2026 11:47
Adds an optional receipt to each expense (spec §11.11, §14.3). The uploaded file is
validated by its MAGIC BYTES (toolbox/receipt_files.py) — never the client's claimed
type or name — against an allowlist (JPEG/PNG/GIF/WebP/PDF), size-capped at 10 MB,
and stored as a private Frappe File attached to the owner's expense. Attaching a new
receipt replaces the old file; removing or deleting the expense deletes the file
(no orphans). The endpoint is owner-checked and rate-limited.

8 tests: magic-byte detection (accept images/PDF, reject other/short), attach
creates a private file, non-receipt rejected, replace removes the old file, remove
clears + deletes, expense delete removes the file, and cross-user rejection.

Part of #71 (live FX remains).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a receipt control to the expense form (attach an image/PDF once the expense is
saved; view or remove an existing one) and a "Receipt" indicator on ledger rows
linking to the private file. The composable reads the file to base64, calls the
validated endpoint, and reflects the result into the form and list without a
refetch. +1 composable test. Verified live: the row chip renders for an expense
with a receipt.

Part of #71.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vibhavkatre
vibhavkatre merged commit 4c84fc7 into develop Aug 5, 2026
@vibhavkatre
vibhavkatre deleted the feat/expense-receipts-71 branch August 5, 2026 06:18
vibhavkatre added a commit that referenced this pull request Aug 5, 2026
Adds an optional "Fetch rate" action in the expense base-currency section that pulls
a reference cross-rate from the existing Currency Converter (ECB) service and
pre-fills conversion_rate (source `ecb-reference`). The ECB rates are EUR-based, so
the cross rate is (EUR→to)/(EUR→from); toolbox/expense_fx.py owns the pure math and
a rate-limited, read-only endpoint, while currency.py stays the owner of provider
logic. The manual rate remains the default and stored expenses are never rewritten;
if rates are unavailable the form is unchanged and a notice explains why.

9 backend tests (cross-rate incl. base/inverse/between/same/unknown; endpoint with a
stubbed service: success, same-currency, unknown pair, service down) + 2 composable
tests. Verified live: USD→INR resolved to a real ECB rate.

With receipts (#75), this closes #71.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant