Fix Contact Pay "Debit (Pay to Customer)" Not Decreasing Advance Balance
When the constant show_payment_type_on_contact_pay is enabled, the Pay Contact modal exposes a payment-type radio with a Debit (Pay to customer) option (is_reverse = 1). Selecting it creates the transaction_payments row correctly, but the customer's contacts.balance (advance balance) is never decreased.
This tutorial explains the root cause and provides a safe, drop-in fix.
๐ Symptomโ
- In
config/constants.phpset:'show_payment_type_on_contact_pay' => true, - Go to Contacts โ Customers โ Pay for a customer who has an advance balance.
- Pick Debit (Pay to customer) and submit an amount.
- Payment is recorded, but the customer's advance balance is unchanged.
๐ Root causeโ
File: app/Utils/TransactionUtil.php
Method: payContact($request, $format_data = true) (around line 6057)
The relevant tail of the method looks like this:
event(new TransactionPaymentAdded($parent_payment, $inputs));
// Distribute above payment among unpaid transactions
if (! $is_reverse) {
$excess_amount = $this->payAtOnce($parent_payment, $due_payment_type);
}
// Update excess amount
if (! empty($excess_amount)) {
$this->updateContactBalance($contact, $excess_amount);
}
Two problems on the reverse branch (is_reverse = true):
$excess_amountis only assigned insideif (! $is_reverse). For reverse payments it is undefined, so theif (! empty($excess_amount))check always fails.- Even if it were set,
updateContactBalance()defaults to$type = 'add'and would increase the balance instead of decreasing it.
updateContactBalance already supports the right operation โ it just is never called on this path:
public function updateContactBalance($contact, $amount, $type = 'add')
{
if (! is_object($contact)) {
$contact = Contact::findOrFail($contact);
}
if ($type == 'add') {
$contact->balance += $amount;
} elseif ($type == 'deduct') {
$contact->balance -= $amount;
}
$contact->save();
}
So the fix is to (a) compute $excess_amount for the reverse branch, and (b) call updateContactBalance with 'deduct'.
โ The fixโ
1. Patch payContact in TransactionUtil.phpโ
File: app/Utils/TransactionUtil.php
Replace the tail of payContact with the version below.
$parent_payment = TransactionPayment::create($inputs);
$inputs['transaction_type'] = $due_payment_type;
event(new TransactionPaymentAdded($parent_payment, $inputs));
// Determine the balance adjustment amount
if (! $is_reverse) {
// Standard payment: pay against dues and capture any excess as advance.
$excess_amount = $this->payAtOnce($parent_payment, $due_payment_type);
} else {
// Debit (Pay to customer): guard against over-deducting the advance balance.
if ($contact->balance < $inputs['amount']) {
throw new \Exception(__('lang_v1.insufficient_advance_balance'));
}
// The whole amount is paid out from the contact's advance balance.
$excess_amount = $inputs['amount'];
}
if (! empty($excess_amount)) {
if ($is_reverse) {
// Reverse payment: deduct from the contact's advance balance.
$this->updateContactBalance($contact, $excess_amount, 'deduct');
} else {
// Standard payment: add the leftover to the advance balance.
$this->updateContactBalance($contact, $excess_amount);
}
}
return $parent_payment;
2. Harden the backend against bypassing the UI flagโ
The show_payment_type_on_contact_pay constant only hides the radio in the Blade modal. A crafted POST can still send is_reverse=1. Gate the backend too โ replace the $is_reverse line near the top of payContact:
// Before
$is_reverse = $request->has('is_reverse') && $request->input('is_reverse') == 1 ? true : false;
// After
$is_reverse = config('constants.show_payment_type_on_contact_pay')
&& $request->has('is_reverse')
&& $request->input('is_reverse') == 1;
3. Add the translation keyโ
File: lang/en/lang_v1.php
Add (alphabetically or near the existing balance-related keys):
'insufficient_advance_balance' => 'Insufficient advance balance for this contact.',
Translate into the other locales you ship (ar, es, fr, โฆ) as needed.
4. Enable the featureโ
File: config/constants.php
'show_payment_type_on_contact_pay' => true,
5. Clear cachesโ
php artisan cache:clear
php artisan config:clear
php artisan view:clear
php artisan route:clear
๐งช How to verifyโ
- Pick a customer with a known
contacts.balancevalue (e.g.100.00). - Contacts โ Customers โ Pay with Debit (Pay to customer), amount
30. - Expected after submit:
- New row in
transaction_paymentswithpayment_type = debitandis_advance = 0. contacts.balanceis now70.00.
- New row in
- Try a debit larger than the balance (e.g.
999). Expected: the request fails withInsufficient advance balance for this contact.and no payment row is written. (If you want strict atomicity wrap the block inDB::transaction(...).) - Submit a normal Credit payment โ confirm it still works exactly as before (no behavior change on that branch).
๐ Notesโ
- This bug is dormant unless
show_payment_type_on_contact_payistrue. If you have never enabled that constant, you are not affected. - The fix only touches
payContact.TransactionPaymentAddedlisteners and theAccountTransaction::getAccountTransactionType()flow are unchanged โ accounting/ledger output remains correct. - Editing/deleting a reverse payment afterwards is handled by the existing payment-update/delete paths and is not in scope of this fix. If your customer needs reversal-of-reversal support, audit
TransactionPaymentController@updateand@destroyseparately. - File modified in this tutorial is a "handle with care" file (
TransactionUtil.php, ~310 KB). Grep forpayContactbefore deploying to ensure no module overrides the same method.
๐ Support this project