The case and all records below are fictional. The Pascal program was compiled and run on Linux with Free Pascal 3.2.2. No bank connection, supplier call or payment occurred.
An invoice arrives from a familiar supplier. It uses the expected reference and asks for payment into a different account. The bank's account-name check returns a match. The supplier does not answer your call, then someone calls back to confirm the new details.
A small invoice system could display three reassuring facts: known supplier, matching bank name, phone confirmation. None is enough to approve the changed destination.
The useful programming task is to keep those decisions separate. This article builds a small Pascal example that leaves incomplete checks on hold, gives a denial priority over other entries, and never returns an instruction to pay.
Start with the actual payment decision
Barclays advises an outgoing call using trusted contact details already held on file when checking new or amended payment instructions. It warns that incoming caller numbers can be faked. If the person is busy, arrange a time and call them yourself. A callback is not the same event.
On the call, ask the known contact to read back the details through your agreed process. Compare them privately. Do not use the telephone number in the new request to establish its own truth. If the trusted number no longer works, the payment lead needs to establish a contact route independently; the program below cannot do that.
Barclays also explains that a bank-name match cannot prove the payment request is genuine. An impersonator may have an account in a matching name. The contact check, the bank result, changing the saved supplier record and normal payment approval remain separate work.
This demonstration uses a deliberately narrow rule. Only a recorded outgoing confirmation, an account-name match and recorded approval reach human review. Any unresolved name result stays on hold. A real business may have an approved exception process, but this example does not invent one.
Give the record states names that preserve uncertainty
The program has four contact states:
-
NotCalled: no confirmation call is recorded. -
IncomingOnly: an incoming caller spoke, but the outgoing trusted-route check is not recorded. -
ConfirmedOutgoing: the record says the check was completed on the outgoing trusted-route call. -
ChangeDenied: the supplier denied the change.
The last two are statements somebody entered, not facts the program independently discovered.
The three name states are deliberately simplified. NameMatches records a match. NeedsReview covers a close match, no match or an unresolved difference. NotAvailable records that the result is unavailable. A real record should retain the exact bank result separately rather than losing it in this grouping.
ApprovalRecorded means the fictional record has an explicit yes/no entry for the business's separate approval. It does not identify an approver, prove they had permission or retrieve evidence.
The result names matter too. ReadyForHumanReview does not mean approved, genuine or safe. It means this narrow set of entries is present for a person to review.
Compile the complete example
Install Free Pascal using the supported route for your computer. Save the following as PaymentReview.pas. With a normal Free Pascal installation, compile it from a command window in the same folder with fpc -Cr -Co -Ci PaymentReview.pas. These switches enable extra checks for invalid ranges, arithmetic overflow and input/output errors. On Linux, run ./PaymentReview afterwards.
The code has no comments; the article explains it. The code does not read private files, write records, contact a service or initiate a payment.
program PaymentReview;
type
TContactCheck = (NotCalled, IncomingOnly, ConfirmedOutgoing, ChangeDenied);
TNameCheck = (NameMatches, NeedsReview, NotAvailable);
TReviewResult = (StopAndReport, KeepOnHold, ReadyForHumanReview);
TChangeRecord = record
ContactCheck: TContactCheck;
NameCheck: TNameCheck;
ApprovalRecorded: Boolean;
end;
var
Checks: Integer;
Failures: Integer;
Contact: TContactCheck;
NameResult: TNameCheck;
Approved: Boolean;
Item: TChangeRecord;
Expected: TReviewResult;
function ReviewChange(Change: TChangeRecord): TReviewResult;
begin
if Change.ContactCheck = ChangeDenied then
ReviewChange := StopAndReport
else if (Change.ContactCheck = ConfirmedOutgoing)
and (Change.NameCheck = NameMatches)
and Change.ApprovalRecorded then
ReviewChange := ReadyForHumanReview
else
ReviewChange := KeepOnHold;
end;
procedure Expect(LabelText: String; Actual, Wanted: TReviewResult);
begin
Checks := Checks + 1;
if Actual <> Wanted then
begin
Failures := Failures + 1;
WriteLn('FAIL: ', LabelText);
end;
end;
procedure ShowResult(LabelText: String; Change: TChangeRecord);
begin
Write(LabelText, ': ');
case ReviewChange(Change) of
StopAndReport: WriteLn('stop and report');
KeepOnHold: WriteLn('keep on hold');
ReadyForHumanReview: WriteLn('record ready for human review');
end;
end;
begin
Checks := 0;
Failures := 0;
Item.ContactCheck := IncomingOnly;
Item.NameCheck := NameMatches;
Item.ApprovalRecorded := True;
ShowResult('Incoming callback', Item);
Expect('callback cannot replace outgoing check', ReviewChange(Item), KeepOnHold);
Item.ContactCheck := ConfirmedOutgoing;
Item.ApprovalRecorded := False;
ShowResult('Outgoing check but no recorded approval', Item);
Expect('missing approval stays on hold', ReviewChange(Item), KeepOnHold);
Item.ContactCheck := ChangeDenied;
Item.ApprovalRecorded := True;
ShowResult('Supplier denied the change', Item);
Expect('denial overrides other entries', ReviewChange(Item), StopAndReport);
Item.ContactCheck := ConfirmedOutgoing;
ShowResult('All required fictional entries present', Item);
Expect('complete record only reaches review', ReviewChange(Item), ReadyForHumanReview);
for Contact := NotCalled to ChangeDenied do
for NameResult := NameMatches to NotAvailable do
for Approved := False to True do
begin
Item.ContactCheck := Contact;
Item.NameCheck := NameResult;
Item.ApprovalRecorded := Approved;
if Contact = ChangeDenied then
Expected := StopAndReport
else if (Contact = ConfirmedOutgoing)
and (NameResult = NameMatches) and Approved then
Expected := ReadyForHumanReview
else
Expected := KeepOnHold;
Expect('decision table', ReviewChange(Item), Expected);
end;
WriteLn('Checks: ', Checks, '; failures: ', Failures);
if Failures > 0 then
Halt(1);
end.
Read the results as a record check
Actual output from the Linux run:
Incoming callback: keep on hold
Outgoing check but no recorded approval: keep on hold
Supplier denied the change: stop and report
All required fictional entries present: record ready for human review
Checks: 28; failures: 0
The first case is the fictional callback. Even with a matching name and approval entry, it stays on hold. The second case has outgoing confirmation but no approval; it also stays on hold. A denial wins over the matching name and approval. Only the last complete fictional record reaches review.
Four named checks cover those examples. The nested loops then cover all 24 combinations of four contact states, three name states and two approval values. That makes 28 checks total. The loop checks agreement with the written decision table, not a bank's behaviour. Because the expected table closely follows the function, the named examples and independent human review are important too. Passing this suite is not proof that the business rule is suitable for your organisation.
For this run, the compiler executable was extracted from Ubuntu's Free Pascal 3.2.2 packages into a local test folder. Its version was checked and both compilation and execution completed successfully with the additional checks enabled. A Windows installation, a real invoice application and a bank integration were not tested. The compile command above is the normal installed-tool command, not a claim that every computer already has fpc available.
What a real application still needs
The example stores typed choices only. It has no permission checks, evidence store, change history, input-file reader or database. Those omissions are why it must not be connected to a payment button.
A real implementation needs to keep supporting evidence available to the appropriate reviewer. Record the checker, outgoing call time, trusted contact source, confirmer's role, exact bank result and unresolved differences in protected business records. Limit who can change or approve the supplier record. Keep full bank details in the controlled payment system where required, not a public demonstration or action log.
There is another practical boundary: record completeness is not record truth. A person could select ConfirmedOutgoing without making a call. A complete screen should not silently substitute for retrieving and reviewing the evidence. The next reviewer needs to know what was checked and what the software cannot establish.
Use the free companion for the human side
Open Before you change bank details. Its four checks cover your own records, the call you make, confirming details and normal approval.
Use the fictional callback situation to practise. Leave the outgoing-call check unfinished, read the next step and agree who owns it. The page has no bank-detail entry fields. Choices stay only in the open page and clear on reload or closing. The checklist does not inspect an invoice, verify a person or account, authorise a payment or guarantee the absence of fraud.
If money may already have gone to a fraudster, contact your bank immediately through a trusted route. Do not wait for the code exercise. Recovery is not guaranteed.
Sources and limits
- Barclays: verbal checks and the limits of account-name matching.
- Northern Ireland Cyber Security Centre: checking changed payment details.
- Free Pascal documentation.
General guidance, not legal or financial advice. Follow current bank guidance and the business's approved payment process.
Top comments (0)