Fatura Defteri: bill reminders that read the bill for you

Flutter · Android · On-device OCR · 188 tests

Product page (Turkish)

Bill list: overdue, this week, this month and paid, each with amount and due dateConfirm screen: provider, amount and due date read from the photo, saved only after reviewBill detail: amount, due date, reminder time and monthly repeatReminder settings: how many days before the due date, and at what time
The interface is in Turkish; captions describe each screen.

The problem

Utility bills arrive on paper and in PDFs, and late fees follow a missed due date. Typing each bill in is the step people skip.

What I built

Users photograph a bill. The app reads the provider, the amount and the due date on the phone, asks the user to confirm any field it is unsure of, and schedules a reminder. Bills never leave the device: the app’s own code makes no network request, and the only traffic is one banner ad at the bottom of the bill detail screen.

Key decisions

  • Offline over size. The bundled text-recognition model adds 12.2 MB to the arm64 download. The download-on-demand variant is far smaller, but it needs a network on first use. I measured both and kept the bundled one, because reading a bill in airplane mode was a requirement.
  • Telemetry removed. The recognition library quietly adds internet permission for usage telemetry sent to Google. That contradicted the privacy policy, so the first release stripped both network permissions from the manifest. When a banner ad brought the permission back, I removed the telemetry’s backend registration instead: events are dropped before they are stored or sent, and recognition still works offline in a release build.
  • Accuracy is measured before release. Each field has a pass mark: provider 85%, amount 90%, due date 95%, because a wrong date defeats the app’s only job. A test runs the parser over anonymized bill text and fails on any regression.
  • Honest about the sample. The first set of 10 bills scored 100% on every field. I logged that as “not yet proven”, not as a win, until a 100-bill set exists.

What the device showed that tests could not

  • Text comes back in columns, not lines. On a two-column bill, the recognizer returned every label first and every value after. The parser’s rule that a value sits next to its label failed silently; the fallback picked the right numbers by luck. The finding is logged with the raw output so the parser can be fixed against real data.
  • Reminders that never fired. The notification plugin does not declare its broadcast receivers. The alarm went off, Android found no receiver, and dropped it with no error and no log line. I added both receivers, including the one that restores alarms after a reboot, then wrote a CI rule that fails any app in the repo that uses the plugin without them.
  • 31 January broke every month after it. Monthly reminders were chained month to month, so a due date on the 31st slid to the 28th in February and stayed there. Each month is now computed from the original due day, and paid bills are no longer reminded again.

Proof

  • 188 automated tests.
  • The CI rule above, enforced for every app in the repo.
  • R8 keep rules after release shrinking broke recognition (the form opened empty) and the reminder receiver.
  • Published on Google Play.

Stack: Flutter, Dart, Google ML Kit text recognition, Google Mobile Ads, flutter_local_notifications, timezone, intl

Need this kind of work on your app? Email muhammedsaidcakir@msclabs.tr.

Next: İş HesaplamaAll case studies