Measured, not asserted

Does obfuscation break client trust accounting?

One bank account holds every client's money. The control that matters is not the bank balance -- it is that no client's own ledger may go below zero, because a client ledger below zero means that client's disbursement was funded by somebody else's money sitting in the same account. This test renamed the field that says which client a posting belongs to, and watched exactly that happen with an empty breach list, a passing check, and a three-way reconciliation that balanced to the cent.

The regulated control is not the one the bank can see

A pooled client account is a single balance at a bank and a set of sub-ledgers in your software. The reconciliation a firm is required to run is three-way: the bank statement, the firm's own book of the account, and the sum of the individual client ledgers must all agree. It is a good control and every arm in this test passed it.

It is also defined over a total, and a total cannot see a transfer between its own parts.

The fixture posts four movements across two matters. One takes in 500000 cents and pays out 200000, ending at 300000. The other takes in 100000 and is asked to pay out 250000. That second disbursement would push that client's ledger to minus 150000, so it must be refused -- even though the pooled account holds 400000 at that moment and the payment would clear the bank without a bounce. A separate run covers the fee rule, so the two controls cannot mask each other.

Protection on its own moved nothing

Five areas, five presets, twenty-five comparisons, all identical. Nothing below is caused by protecting the code.

One field, and one client paid another client's bill

With the matter identifier renamed on the postings, every posting grouped under the same key. Two clients became one ledger. The run COMMITTED, printing ledgers=1[undefined=150000] pooled=150000c three-way=balanced posted=4 breaches[] => COMMITTED, and the firm's own rule passed and reported ledger-check(1 client ledger(s), none below zero, three-way balanced at 150000).

Every clause in that check is true. There is one ledger. It is not below zero. The books balance. The money left the account and no client was ever recorded as having been overdrawn, because after the merge there is no second client to overdraw. The violation did not become invisible; it stopped existing in the data.

The only trace in the whole output is the ledger's name, printed as undefined. That is one token in a report nobody reads line by line, and it is the sort of thing that gets explained away as a display bug in a month-end summary.

The same money, by a route that leaves evidence

The contrast is what makes this measurable rather than merely alarming, and it is one option key away.

Renaming the flag that prohibits a negative client balance -- the policy, not the identity -- reverts it to the library's permissive default, because a general ledger engine cannot know your bar rules. On its own it was CAUGHT: run-rejected(ledger-check(M-5501=-150000 is below zero)). The firm's rule did exactly what it was written to do.

Add the flag that enables that rule and the run COMMITTED -- but printed ledgers=2[M-4410=300000 M-5501=-150000] ... breaches[M-5501 fell to -150000]. The violation is on the page. A person reading the run sees a negative client balance and a breach list with an entry in it. Renaming the rule itself produced the same output ending checked by library-builtin(accepts any run of postings), where the word checked survives and the checking does not.

So the same money moves by two routes. The policy route leaves the violation legible and, at one name, catchable. The identity route erases the violation itself. That is the practical reason to test a rename pattern one name at a time: the broad pattern everybody actually ships contains both, and the union looks like the policy result until you read the ledger names.

Why the fee rule behaved differently

The second run covers a rule with no identity in it: a fee may only be transferred out against an invoice. Renaming that flag reverts it to the permissive default and the uninvoiced fee of 30000 was taken. Renaming the invoice field on the postings refused both fees, including the properly invoiced one.

Neither is subtle, and that is the point of including it. A rule expressed as a policy over a value fails in a direction somebody can see. A rule expressed over an identity fails by changing who the parties are.

What failed loudly

Renaming the posting kind produced unknown-posting-kind(undefined, refused before posting) and renaming the amount produced non-finite-amount(refused before posting). Both refuse the whole run before anything moves, which is the cheap failure: a trust ledger that will not post is discovered the same day.

The opening balances were cosmetic here, changing a figure in the configuration dump and no decision, and the pinned money-unit control measured identical, as predicted.

If you protect a legal accounting application

Reconcile the parts, not only the total. A three-way reconciliation that agrees at the account level tells you the arithmetic is consistent; it tells you nothing about who owns what inside it. Assert the number of distinct client ledgers against the number of matters with movements this period, and refuse a posting whose client code does not resolve to an open matter.

Make an unresolved grouping key an error. Nothing in this test refused a posting for carrying an unfamiliar client code, because no general ledger engine does. Your code is the only layer that knows the code has to mean something.

And treat a run that produces a single merged ledger, or a ledger named for nothing, as a hard stop. If you are shipping accounting software to firms, the controls evidence in our SOX and GLBA guide covers how to present the same reconciliation to an auditor.

Frequently asked questions

Did obfuscation move any money on its own?

No. Twenty-five base comparisons across five areas and five presets were identical to the unprotected runs. The result in this article required member renaming aimed at a property name the installed ledger engine also reads.

What exactly happened when the matter identifier was renamed?

Every posting grouped under the same key, so two clients became one ledger. A disbursement that should have overdrawn one client's ledger by 150000 cents committed instead, funded by the other client's money in the same pooled account.

Did the three-way reconciliation catch it?

No. It balanced exactly, in that arm and every other. Bank against book against the sum of the client ledgers is a total, and a total does not change when money moves between its own parts.

Was there any sign at all in the output?

One: the merged ledger printed its name as undefined. There was no negative balance, no breach entry and no failed check, because after the merge there was no second client left to overdraw.

Is renaming the policy flag just as bad?

No, and the contrast is the useful part. Renaming the flag that prohibits a negative client balance was caught outright by the firm's own rule. Adding the flag that enables that rule let the run commit, but it printed the negative balance and a breach entry on the same line. The policy route leaves evidence; the identity route removes it.

Which fields failed loudly?

The posting kind and the amount both refused the whole run before anything moved. Renaming the invoice field refused both fee transfers, including the properly invoiced one. Those are the failures a firm finds the same day.

What is the cheapest fix?

Reconcile the parts as well as the total: assert the number of distinct client ledgers against the number of matters with movements, and refuse a posting whose client code does not resolve to an open matter. Both are worth doing regardless of obfuscation, because a dropped column in an import produces the same merge.

Related reading