TBroMEM Posted June 24, 2025 Share Posted June 24, 2025 I am hoping to aid the finance/billing team with writing off bad debt using a hook. The manual process is as follows (high level): 1. add credit 2. apply credit to invoice 3. cancel invoice ultimately term/inactivate client product/service. I am somewhat novice, but curious how things stand with the following code. I hope to simplify step 2 and 3 via hook. Am I approaching the logic properly? Any other guidance would be appreciated. As I understand it, when a credit is added to a client account, it will update the Credit value for the related client. <?php if (!defined("WHMCS")) die("This file cannot be accessed directly"); add_hook("ClientEdit", 1, "hook_applycredit_thencancel"); function hook_applycredit_thencancel($vars) { try { //vars from hook $clientid = $vars['userid']; $adminuser = 'sys.admin'; logActivity('Checking Client unpaid invoices ' . $clientid . ' from hook_applycredit_thencancel...'); // Get Invoices // Define parameters $command = 'GetInvoices'; $values = array( 'userid' => $clientid, 'status' => 'Unpaid', ); // Call the localAPI function $invoiceData = localAPI($command, $values, $adminuser); if ($invoiceData['result'] == 'success') { foreach ($invoiceData['invoices']['invoice'][0] as $invoice) { $invoiceId = $invoice->'id'; $creditamt = $invoice->'credit'; //Apply Credit $command = 'ApplyCredit'; $values = array( 'invoiceid' => $invoiceId, 'amount' => $creditamt, 'noemail' => '1', ); $addCredit = localAPI($command, $values, $adminuser); if ($addCredit['result'] == 'success') { logActivity('Credit amount ' . $creditamt . 'has been applied to Invoice ' . $invoiceId . ' by hook_applycredit_thencancel...'); } else { logActivity('Credit amount ' . $creditamt . ' FAILED to apply to Invoice ' . $invoiceId . ' by hook_applycredit_thencancel...'); } //Cancel Invoice $command = 'UpdateInvoice'; $values = array( 'invoiceid' => $invoiceId, 'status' => 'Cancelled' ); $cancelInvoice = localAPI($command, $values, $adminuser); if ($canceInvoice['result'] == 'success') { logActivity('Invoice ' . $invoiceId . ' has been cancelled by hook_applycredit_thencancel...'); } else { logActivity('Invoice ' . $invoiceId . ' FAILED cancel by hook_applycredit_thencancel...'); } } } else } logActivity('Credit application for ' . $clientid . ' FAILED by hook_applycredit_thencancel...' . $invoiceData['message']); } } catch (\Exception $e) { logActivity("Credit application failed: {$e->getMessage()}"); } } 0 Quote Link to comment Share on other sites More sharing options...
DristiTechnologies Posted June 25, 2025 Share Posted June 25, 2025 There are few issues in the hook logic for applying credit and cancelling invoices: Incorrect array index usage in the loop You had: foreach ($invoiceData['invoices']['invoice'][0] as $invoice) This loops only over the first invoice. Use: foreach ($invoiceData['invoices']['invoice'] as $invoice) Incorrect object access You used: $invoiceId = $invoice->'id'; $creditamt = $invoice->'credit'; But $invoice is an associative array, not an object. Use: $invoiceId = $invoice['id']; $creditamt = $invoice['credit']; Incorrect variable name You had: if ($canceInvoice['result'] == 'success') Correct to: if ($cancelInvoice['result'] == 'success') Syntax error in else You had: else } Correct to: else { 1 Quote Link to comment Share on other sites More sharing options...
TBroMEM Posted June 25, 2025 Author Share Posted June 25, 2025 @DristiTechnologies Thank you so much for the analysis. This was more than I anticipated so it is very VERY much appreciated. 👍 1 Quote Link to comment Share on other sites More sharing options...
pRieStaKos Posted June 25, 2025 Share Posted June 25, 2025 Questions: As invoice is Unpaid, what credit are you trying to apply? How are you triggering this hook (ClientEdit) ? Why don't you upload hook code to GitHub, so anyone who want's to contribute, can send changes? 0 Quote Link to comment Share on other sites More sharing options...
TBroMEM Posted June 25, 2025 Author Share Posted June 25, 2025 @pRieStaKos As I am not typically a developer, I don't delve into github and other developer focused content/tools. The goal was to trigger on ClientEdit, but was told by WHMCS Support that adding a credit does not trigger this, so had considered a different approach to the process. Credits will be added to write-off the unpaid invoices and cancel them. + @DristiTechnologies What is correct parameter to check if a client balance is greater than 0.00 before applying the credit? This is what I mocked up. <?php if (!defined("WHMCS")) die("This file cannot be accessed directly"); add_hook("ClientEdit", 1, "hook_applycredit_thencancel"); function hook_applycredit_thencancel($vars) { try { //vars from hook $clientid = $vars['userid']; $adminuser = 'todd.brotzman'; //may change to a distinct api user for this function. logActivity('Checking Client unpaid invoices ' . $clientid . ' from hook_applycredit_thencancel...'); // Get Invoices // Define parameters $command = 'GetInvoices'; $values = array( 'userid' => $clientid, 'status' => 'Unpaid', ); // Call the localAPI function $invoiceData = localAPI($command, $values, $adminuser); if ($invoiceData['result'] == 'success') { foreach ($invoiceData['invoices']['invoice'] as $invoice) { $invoiceId = $invoice['id']; $creditamt = $invoice['total']; //Get Client Credit Balance $command = 'GetClientDetails'; $values = array( 'clientid' => $clientid, ); $getCreditBal = localAPI($command, $values, $adminuser); if ($getCreditBal['result'] == 'success') { $creditbalance = $getClient['credit']; } else { $creditbalance = '0'; } //Apply Credit $command = 'ApplyCredit'; $values = array( 'invoiceid' => $invoiceId, 'amount' => $creditamt, 'noemail' => '1', ); if ($creditbalance !== '0') { // would > '0' work? $addCredit = localAPI($command, $values, $adminuser); if ($addCredit['result'] == 'success') { logActivity('Credit amount of ' . $creditamt . 'has been applied to Invoice ' . $invoiceId . ' by hook_applycredit_thencancel...'); } else { logActivity('Credit amount ' . $creditamt . ' FAILED to apply to Invoice ' . $invoiceId . ' by hook_applycredit_thencancel...'); } } else { logActivity('Client ' .$clientid . ' has a 0.00 credit balance. Credit not applied to invoice ' . $invoiceId . ' by hook_applycredit_thencancel...'); } //Cancel Invoice $command = 'UpdateInvoice'; $values = array( 'invoiceid' => $invoiceId, 'status' => 'Cancelled' ); $cancelInvoice = localAPI($command, $values, $adminuser); if ($cancelInvoice['result'] == 'success') { logActivity('Invoice ' . $invoiceId . ' has been cancelled by hook_applycredit_thencancel...'); } else { logActivity('Invoice ' . $invoiceId . ' FAILED cancel by hook_applycredit_thencancel...'); } } logActivity('Credit application for ' . $clientid . ' completed by hook_applycredit_thencancel...' . $invoiceData['message']); } else { logActivity('Credit application for ' . $clientid . ' FAILED by hook_applycredit_thencancel...' . $invoiceData['message']); } } catch (\Exception $e) { logActivity("Credit application failed: {$e->getMessage()}"); } } 0 Quote Link to comment Share on other sites More sharing options...
TBroMEM Posted June 27, 2025 Author Share Posted June 27, 2025 Hello All, This latest code appears to be work in testing. I need to test situation where total of invoices unpaid is greater than credit. The logic exclude them. 0 Quote Link to comment Share on other sites More sharing options...
TBroMEM Posted June 27, 2025 Author Share Posted June 27, 2025 Possibly I could modify the sort output of GetInvoices ascending by id, to process oldest to newest. 0 Quote Link to comment Share on other sites More sharing options...
TBroMEM Posted June 27, 2025 Author Share Posted June 27, 2025 I just noticed flaw in the logic. If it cannot apply the credit, it still cancels the invoice. 0 Quote Link to comment Share on other sites More sharing options...
TBroMEM Posted June 27, 2025 Author Share Posted June 27, 2025 Coming full circle on this. I think the best design may be as follows: -get invoice total due -create credit for the invoice total -apply credit -cancel invoice This takes away the complexity of assuring there is a suitable/available credit balance. 0 Quote Link to comment Share on other sites More sharing options...
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.