DeenLytics

هندسة البرمجيات

Taking payments in the field: bKash, Laravel and an Android app

Fees arrived as cash and mobile money, reconciled by hand. We built a Laravel ledger, verified every bKash payment server-side, and shipped an Android app.

بقلم Injamam Ahmed6 دقائق قراءة

Iqra Billing Software invoice dashboard

Iqra Billing Software started as a reconciliation problem, not a payments problem.

Fees were arriving as cash and as mobile money. Different people collected them. Receipts were written differently depending on who wrote them. At the end of the month somebody sat down and tried to work out who had actually paid, from records that had never been designed to be compared.

We built two things: a Laravel web application that holds the ledger, and a native Android app for the people collecting in the field. Between them sits a bKash integration that is verified on our server, never on the phone. This post is about how those three pieces fit together, and the rules we followed for the money.

Money arriving in three places

Before we wrote any code we listed every way a payment could reach the organisation. There were three: cash handed to a collector, a bKash transfer sent by the customer, and a payment taken by an administrator at the office.

Three routes in, one ledger. That framing decided most of the architecture.

Every one of those routes has to end in the same place, with the same shape of record, or the reconciliation problem simply moves rather than disappears. A system that handles bKash beautifully but treats cash as an afterthought will still leave somebody adding up receipts at month end.

The ledger is the web application

The Laravel application is the system of record. Customers, invoices, payments and ledgers live there and nowhere else.

It handles the parts that are unglamorous and constant:

  • Invoicing, including recurring billing for customers on a repeating schedule.
  • Due-date reminders, so chasing is something the system does rather than something a person remembers.
  • Payment history and statement export, so any balance can be explained line by line.
  • Role-based access, because an administrator and a collector should not see or be able to do the same things.

That last one is worth dwelling on. In a billing system, permissions are not an administrative nicety. A collector needs to record a payment against a customer. A collector does not need to edit an invoice, void a payment or change a price. Drawing that line properly at the start is far easier than retrofitting it after somebody has done something they should not have been able to do.

Never trust the client about money

The bKash integration follows one rule, and it is the only rule that matters: the phone tells us a payment happened, and then the server goes and checks.

The flow is deliberately boring. The application creates the payment on the server side. The customer completes it with bKash. Control comes back to us with a result. Before anything is written to the ledger, our server asks the gateway directly what the status of that payment is, and only a confirmed answer from the gateway marks an invoice paid.

Nothing about that confirmation comes from the device. A client — a browser, an Android app, anything — can be modified. If a "payment succeeded" message from a phone is enough to clear an invoice, then clearing an invoice is free for anyone willing to look at the traffic.

Two more precautions came with it:

Credentials stay on the server. The gateway keys live in server-side environment variables. They are never compiled into the Android app and never exposed to a browser. An API key shipped inside a mobile app is a public API key; it just takes slightly longer for someone to find it.

Every write is idempotent. Networks in the field are unreliable, and a collector on a weak connection will tap the button again. Each payment carries an identifier, and processing the same identifier twice records one payment, not two. This is the kind of detail nobody asks for in a requirements meeting and everybody notices when it is missing.

Why the collector app is native Android

We build a great deal in the browser and we are happy to argue that most business software belongs there. This one did not.

Collectors work in places with poor connectivity, on inexpensive Android handsets, moving between locations for a whole shift. The app has to open instantly, tolerate a bad signal, and keep working when the network drops for a minute. It also has to feel like a normal phone application to people who use phone applications all day and web applications rarely.

So the field tool is native Android and it does one job: record a payment against a customer and issue a receipt. It is deliberately small. Everything that is not collection — invoicing, pricing, reporting, corrections — stays in the web application where it belongs, behind the permissions that belong to it.

Keeping the app narrow is also what keeps it safe. There is very little in it worth attacking, because it cannot do very much.

Receipts people can keep

The receipt was the part of this project with the least technical content and the most impact on trust.

Before, a receipt was whatever the person collecting happened to write down. Two collectors produced two different documents, and neither was easy to check against anything.

Now every payment produces the same receipt, generated from the same record, with the same reference. The customer gets a consistent document. The office can find the matching entry in the ledger without interpretation. When somebody says they have already paid, that claim is now checkable in seconds instead of being a difference of opinion.

Reconciliation stopped being a monthly event

The change the client describes is not really about payments. It is about when the books are correct.

Reconciliation used to be something that happened at the end of the month, as an exercise in reconstruction. Now a cash payment, a bKash payment and an office payment all land in the ledger as they happen, in the same format, verified. The books are correct continuously, so month end is a report rather than an investigation.

That is the qualitative outcome, and it is the honest one. We are not going to publish a percentage we have not measured.

If you are integrating a gateway

Four things we would tell anyone doing this work, in Bangladesh or anywhere else:

  • Decide where the ledger lives before you write a line of integration code. The gateway is a source of events, not a source of truth.
  • Verify server to server, always. Treat anything the client reports as a hint that something might have happened.
  • Make every payment write idempotent. Assume the request will arrive twice, because eventually it will.
  • Handle cash as a first-class payment method. In most Bangladeshi businesses it is still the majority of what comes in, and a system that only respects digital payments solves half a problem.

Iqra Billing Software runs on this design today, with the web application, the Android app and the bKash integration in production. If you are collecting payments across several channels and reconciling them by hand, that is a solvable problem, and we would be glad to talk about it.

كتبه

Injamam AhmedFounder