Skip to main content

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โ€‹

  1. In config/constants.php set:
    'show_payment_type_on_contact_pay' => true,
  2. Go to Contacts โ†’ Customers โ†’ Pay for a customer who has an advance balance.
  3. Pick Debit (Pay to customer) and submit an amount.
  4. 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):

  1. $excess_amount is only assigned inside if (! $is_reverse). For reverse payments it is undefined, so the if (! empty($excess_amount)) check always fails.
  2. 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โ€‹

  1. Pick a customer with a known contacts.balance value (e.g. 100.00).
  2. Contacts โ†’ Customers โ†’ Pay with Debit (Pay to customer), amount 30.
  3. Expected after submit:
    • New row in transaction_payments with payment_type = debit and is_advance = 0.
    • contacts.balance is now 70.00.
  4. Try a debit larger than the balance (e.g. 999). Expected: the request fails with Insufficient advance balance for this contact. and no payment row is written. (If you want strict atomicity wrap the block in DB::transaction(...).)
  5. 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_pay is true. If you have never enabled that constant, you are not affected.
  • The fix only touches payContact. TransactionPaymentAdded listeners and the AccountTransaction::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@update and @destroy separately.
  • File modified in this tutorial is a "handle with care" file (TransactionUtil.php, ~310 KB). Grep for payContact before deploying to ensure no module overrides the same method.

๐Ÿ’› Support this project

Premium Login