Ask an education centre owner where the money leaks and they will not say fraud. They will say reconciliation.
A parent transfers 3,200,000 VND with the note "hoc phi". Another transfers the same amount the same afternoon. Three weeks later two students are marked unpaid, two parents are annoyed, and someone spends an evening comparing a bank export to a spreadsheet.
The reference is the whole trick
VietQR lets a payment code carry a structured reference. ECMS puts the invoice identifier in it, so the transfer arrives already knowing which invoice it belongs to. The parent does not type a note; they scan and confirm.
The reconciliation webhook then writes the payment back against that invoice automatically. No evening of comparing exports.
That is the entire idea, and it is not clever. What is worth writing about is the two places it goes wrong.
Failure one: the webhook nobody signed
A payment-confirmation endpoint is, by construction, an endpoint that marks invoices paid. If anyone on the internet can POST to it, anyone on the internet can clear their own tuition.
ECMS requires VIETQR_WEBHOOK_SECRET. If it is not configured, the webhook is rejected outright rather than processed.
This sounds obvious and it is the opposite of what systems usually do. The common pattern is: if no secret is set, skip verification and process the payload - which is convenient in development and catastrophic in production, because the failure mode of a missing environment variable becomes "silently accept everything" instead of "stop".
The rule we apply to anything that moves money or grants access: a missing credential means refuse, never means skip the check. Someone will eventually deploy without that variable. You want that deploy to page you, not to quietly open the door.
Failure two: the reference that does not survive the bank
A structured reference is only useful if it arrives intact. Banks truncate, uppercase, strip punctuation, and occasionally hand back a description the customer edited by hand.
So the match cannot assume a perfect string. The invoice identifier is short, uses an unambiguous character set, and the matcher falls back to amount plus payer plus time window when the reference is damaged - and when even that is ambiguous, the payment lands in a review queue rather than being guessed onto an invoice.
The queue is the important part. A reconciliation system that always decides will eventually credit the wrong student, and that error is worse than the manual work it replaced, because now nobody is checking.
What the loop is actually worth
The saving is not the accountant's evening, although that is real. It is that the class list and the finance record stop being two opinions.
Attendance, enrolment and tuition then sit on the same data, which makes the derived questions answerable without qualification: which classes carry unpaid balances, which programmes are actually profitable once teacher cost is attributed, and which students are at risk of dropping for financial rather than academic reasons - a distinction a centre can act on, but only if the two halves of the record agree.
