Case study · Web app

FişAsistan

("fiş" means receipt)

Employees photograph expense receipts. A vision model reads them, the employee confirms, and the accountant gets a clean, rule-checked ledger.

Receipt list for one month, grouped by day, with warning chips on two receipts
Dashboard with the month total, a category bar and a Today to-do list
Assistant answer card with a monthly total and a category breakdown
580automated tests, all passing
28database migrations
93versions in about five weeks
0secret keys in the browser

Why I built it

The first version worked, but I did not trust it

An earlier version ran as a Google Apps Script web app that wrote to a spreadsheet and a Drive folder. It worked, but uploads felt slow, receipts could not be corrected or deleted cleanly, links could not point at a single receipt, and I did not fully trust the data or the backups.

So I rebuilt it around five goals: faster uploads, a live dashboard, correcting and deleting receipts safely, links that open one receipt, and data I can trust, with real backups. The app runs every day for a small team, which keeps the feature list honest.

Upload

Photograph, check, send

Take one photo, or use the multi-receipt mode and photograph several in a row. The model reads the receipt; the employee adds a short note and sends it. A "paid in cash" checkbox marks cash spending.

  • The same photo is caught as a duplicate even if the model reads it differently the second time
  • Several receipts can be sent in one go
  • Employees see only their own receipts, month by month

The app is in Turkish. "Fişlerim" means "my receipts"; "Elinizdeki avans" means "advance you hold".

My receipts screen with a balance card, a month picker and a list of receipts with payment status
The employee's own view

Rules

Rules that live in a table, not in someone's head

Every accounting rule is a row in a decision table. A test exists for each row, and the code follows the test. When a rule changes, the table changes first, so a contradiction shows up as a failing test instead of a wrong report.

  • VAT is checked against the gross total, and receipts with two VAT rates are solved explicitly
  • Spending without a proper invoice is classified by its own rule
  • Per-person exceptions are written as rules too, never as one-off fixes

"Eşik üstü" = over the threshold. "KDV" = VAT.

Receipt list where two receipts carry warning chips from the rules engine
Warnings come from the rules engine

Dashboard

One screen for the person who answers the questions

The dashboard updates live: the server only sends a "something changed" signal, and the page reloads its data through the authorised API. Receipts can be filtered by month, period, person, payment type and amount, and opened from a link.

  • A "Today" card lists what to do next, such as the previous month's unchecked receipts
  • A month-end checklist shows what is done, what is waiting and what is just informative
  • Next and previous arrows move through a filtered list of receipts

"Bugün yapılacaklar" = to do today. "Kapanış" = month-end close.

Dashboard with a Today to-do list
Month-end checklist with done and waiting items

Cash

A running balance per person

When an employee spends their own cash, the company owes them; when the company advances cash, the employee owes the company. One running balance per person combines an opening balance, cash receipts and payments, with a change history kept by a database trigger.

  • Amounts are stored as exact decimals, never floats
  • The employee sees their own balance; managers see everyone's
  • Every opening-balance change is logged with who and when

"Açılış bakiyesi" = opening balance. "Ödeme / avans" = payment / advance.

Cash ledger with an opening balance, a payment and three receipts, each with a running balance
Running balance after every movement

Assistant

Ask in plain Turkish

A small mascot answers questions such as "how much did we spend on fuel this month". It is a rule-based assistant, not a language model: it parses the sentence with plain rules, and the server calculates every number from the data. Sentences it cannot understand are logged, and I extend the rules from that log.

  • Employees can ask only about their own spending
  • Answers show the rows behind the number, with links to them

"Kategorilere göre" = by category. "Kişilere göre" = by person.

Assistant answer card
An answer card

Lessons from real bugs

What actually went wrong

Animation

The mascot that jumped non-stop

On one office PC the assistant's animations ran frantically; at home they were calm. The site's "reduce motion" style shortened animations to 0.01 ms but left infinite loops running, so on a computer that reported animations as off they restarted thousands of times a second. The fix: play once and stop.

Test on the slowest machine in the office.

Dependencies

Reading got slow overnight

Receipt reads went from 2–3 seconds to 10–46 seconds with no change in the response size. The fix: a lean main request, and a backup model request fired after 6 seconds, where the first answer wins. The winning model is saved with the record.

Design for a dependency that is sometimes slow.

Money

VAT that did not add up

A receipt with two VAT rates was split wrongly by the model. The fix: a gross-total sanity check and a two-rate solver, defined first in the decision table, then in tests, then in code.

Never trust a model's arithmetic; check the totals.

Data

A date the model invented

A receipt with a faded date was read as a plausible but wrong date, and it was only noticed while reviewing imported data. The fix: dates far from today are flagged, and a person corrects them.

Add a check that does not depend on the model.

Under the hood

The parts that are not on screen

Money

Amounts are exact decimals in the database, with no floating point. Calculations live in one place, a database function or a single TypeScript module.

Security

The browser never talks to the database. The secret key exists only on the server. Every table has row-level security turned on and grants nothing to anonymous or signed-in browser roles. Sessions are signed, HTTP-only cookies; employees can touch only their own uploads. Every pull request is scanned for leaked secrets and for unsafe workflow settings, and dependency updates arrive monthly, two at a time.

Tests

Decision table, then test, then code. Migrations are tested on a real Postgres engine, and a read-only database health check runs every month. The upload screen and the shared dialogs are also exercised in a real browser (18 scenarios), which caught a keyboard-focus bug that unit tests could not see.

AI and privacy

A receipt photo (or PDF) and a fixed prompt go to a vision model, and a person confirms the result. There is no free-form chat with a model. The assistant described above uses no model at all.

Backup and deletion

The database is backed up daily. Deleting a receipt is soft: it can be restored for 30 days, then it is permanently removed together with its stored files.

Phone first

It installs from the browser. Tap targets are at least 44 px, and sticky bars hide when the keyboard opens.

Timeline

One month, in four steps

  1. Late Sep 2026First deployment, upload and storage working.
  2. Early OctDashboard, email notices, accounting rules and monthly reports. Go-live on 6 Oct, replacing the old system.
  3. Mid OctVAT checks, spending thresholds and receipt filters.
  4. Late OctCash balances, live filters, the rule-based assistant and the month-end checklist.

About

Who did what

Product decisions, accounting rules and testing on real phones are mine. The code was written together with Claude.

All screens on this page use invented names and amounts. The interface is in Turkish, and the labels are explained next to each screen.