Jump to content

WHMCS stop renewal invoice creation if unpaid exists for a service.


Recommended Posts

Posted (edited)

How to prevent WHMCS from generating duplicate invoices for the same service renewal?

We are currently facing an issue in WHMCS (version 8.x, PHP 😎 where multiple invoices are generated for the same service during the renewal process. Here’s the scenario:

  1. A renewal invoice is created but remains unpaid.
  2. When the client clicks the “Renew” button again, WHMCS generates a new invoice and cancel Previous invoice, even though an unpaid invoice already exists for the same service.

This behavior clutter in the system. My goal is to prevent WHMCS from generating a new invoice if there is already an unpaid invoice linked to the same service or product.

I use this hook but problem not solved.

 

<?php

use WHMCS\Database\Capsule;

add_hook('InvoiceCreation', 1, function($vars) {
    $relid = $vars['relatedid'];
    if (empty($relid)) {
        return; 
    }
    $unpaidInvoice = Capsule::table('tblinvoices')
        ->join('tblinvoiceitems', 'tblinvoices.id', '=', 'tblinvoiceitems.invoiceid')
        ->where('tblinvoices.status', 'Unpaid') 
        ->where('tblinvoiceitems.relid', $relid)
        ->exists(); 
    if ($unpaidInvoice) {
        throw new \Exception('An unpaid invoice already exists for this service. Please pay it first.');
    }
});


 

 

Edited by ambaha
Link to comment
Share on other sites

Hello

Hope this code fixes the issue

 

<?php
/**
 * prevent_duplicate_renewal.php
 *
 * Prevent WHMCS from generating a new renewal invoice when
 * an unpaid one already exists for the same service.
 *
 * Drop this file into includes/hooks/ and WHMCS will pick it up.
 */

use WHMCS\Database\Capsule;

add_hook('InvoiceCreation', 1, function($vars) {
    // Only act on client-area triggered renewals
    if ($vars['source'] !== 'clientarea') {
        return;
    }

    $invoiceId = $vars['invoiceid'];

    // Fetch all hosting/service items on the newly created invoice
    $items = Capsule::table('tblinvoiceitems')
        ->where('invoiceid', $invoiceId)
        ->where('type', 'Hosting') // for addons/domains you may need 'Addon' or 'Domain'
        ->get();

    foreach ($items as $item) {
        $serviceId = $item->relid;
        if (!$serviceId) {
            continue;
        }

        // Is there another unpaid invoice for this exact service?
        $duplicateExists = Capsule::table('tblinvoices')
            ->join('tblinvoiceitems', 'tblinvoiceitems.invoiceid', '=', 'tblinvoices.id')
            ->where('tblinvoices.status', 'Unpaid')
            ->where('tblinvoiceitems.relid', $serviceId)
            ->where('tblinvoiceitems.invoiceid', '!=', $invoiceId)
            ->exists();

        if ($duplicateExists) {
            // Cancel the just-created invoice
            Capsule::table('tblinvoices')
                ->where('id', $invoiceId)
                ->update(['status' => 'Cancelled']);

            // Log for your reference
            logActivity(
                "Prevented duplicate renewal: cancelled invoice #{$invoiceId} for service ID {$serviceId}"
            );

            // No need to check further line items
            break;
        }
    }
});
Link to comment
Share on other sites

  • 2 weeks later...
On 5/12/2025 at 12:27 AM, We Connect Nordic AB said:

Hello

Hope this code fixes the issue

 

do not modify the invoice. That will get you in trouble in many jurisdictions, and won't actually solve  the aforementioned problem of cluttering the system. It just adds yet another invoice, more clutter, and more for admins to go through

Hooks are the way to go here. There are a couple of approaches

1: Use sidebar hooks. Don't let them see the links to the renewal service

2: Use ClientAreaPage hooks. This will do the trick. I had to work some magic with PHP and parse_url to obtain the serviceid, so this may not work if you have differing SEO, but it should be pretty easy.

 

<?php
use WHMCS\Database\Capsule;
add_hook('ClientAreaPage', 1, function ($vars) {

    $userid = $_SESSION['uid'];
    $pagetitle = $vars['pagetitle'];
    $systemurl = $vars['systemurl'];
    $serviceitemcount = 0;
  // only triggers if the pagetitle is set to 'Renew'. The uid is already established and required
  
    if ($pagetitle == "Renew") {
        //get the url and parse elements
        $url = $_SERVER['REQUEST_URI'];
        $url_parts = parse_url($url);
        $path = $url_parts['path'];
        $path_parts = explode('/', $path);
        $productid = $path_parts[3];
      
        // Get all open (Unpaid) invoices for the user
        $openInvoices = Capsule::table('tblinvoices')
            ->where('userid', $userid)
            ->where('status', 'Unpaid')
            ->pluck('id');

        $invrows = [];
        $svcrows = [];

        foreach ($openInvoices as $invoiceId) {
            // Check if this invoice has an item for the given productid
            $serviceItem = Capsule::table('tblinvoiceitems')
                ->where('invoiceid', $invoiceId)
                ->where('relid', $productid)
                ->first();

            if ($serviceItem) {
                $serviceitemcount = $serviceitemcount + 1;
                $invrows[] = $invoiceId;
                $svcrows[] = $serviceItem->id;
            }
        }
        // don't just throw an error to the client, send them to the invoice
        if ($serviceitemcount > 0) {
            header("Location: $systemurl/viewinvoice.php?id=$invrows[0]");

        } 
    }
});

Instead of bailing out and throwing an error, this will send the client to the invoices page so they can pay it

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...

Important Information

By using this site, you agree to our Terms of Use & Guidelines and understand your posts will initially be pre-moderated