Jump to content


  • Content Count

  • Joined

  • Last visited

  • Days Won


Everything posted by Kian

  1. I spent years next to persons with alzheimer and cancer but don't underestimate what work-related stress can do. Burnout syndrome is as dangerous as any other type of stress. You can't just "walk away tomorrow" since it follows you everywhere. What if I'd tell you that your stress is fake? It would be a bs. There's no real or fake stress. Stress is stress. It depends on the person. I should work for more than 24 hours per day including holidays to handle tickets and what people write. Hiring employees is not enough since I'd still need to work 16.5 hours per day. I need more employees. It costs a lot hence I should double prices but I can't. Let's just say that I learned the hard way why WHMCS has feature requests and enterprise support. When you realize that you can't solve this problem even working 24 hours per day including Chirstmas, you stick to your 8 hours, no matter what. And yes, this includes spending time on this community coding a script that I'd love to use for myself and give for free to anyone just because I like to be helpful. That's the very reason we all started coding. Right now I'm forced to focus on big providers that can afford an expensive service level agreement at the expense of smaller ones. I don't like it. Not even a little. That's why I'm thinking to transition from closed-source to open-source but that's another story. If you have a better idea (I'm not sarcastic) and are willing to share it with me, just tell me. I highly value your opinions. Of course it is a meaningless rank. I used it just to state that almost any WHMCS-related company, including ones with a bit of history, experience and "reputation", have problems with tickets.
  2. I'm worse 🥳 Let's face it. Almost every company that works with WHMCS has a bad reputation myself included. Every review I have is negative. The funny thing is that I'm "GearHead" on this community and I'm working on WHMCS since 2008. The same applies to almost any other company. Every day I see tickets like «Give me a license to try your software because after {Insert-Any-WHMCS-Developer} I don't trust anybody». I shouldn't say this but being a developer on this platform is atrocious and unrewarding for a lot of reasons. Personally I'm considering a move to open source or quit. From my experience the only "happy" developer is the one dealing with templates, translations, pre-written stuff, small scripts and things that can be made out of documentation and API. We all have problems with tickets. Opening one takes a minute but solving it could require weeks of working due to how WHMCS is designed. Everything seems so simple and easy but it's not. Providers blame us for being late at answering tickets as if we spend the day watching movies. The truth is that dealing with action hooks feels like a bondage session. One day we had to spent 21 days of coding just to fix a redirect. If you want a taste of it, years ago I posted this screenshot just to show what I had to do in order to fix "Tax" on invoices. Don't get me wrong. Customers are right at expecting answers within a reasonable amount of time but numbers involved are too big. As I said earlier, there are probably about 55k WHMCS users and such a small bunch of companies can't deal with them. Not to mention every provider needs custom-stuff, changes and support that not even WHMCS can provide. I mean, just look at Feature Requests 😵 Sadly now I think that the only business model that works is focusing on BIG providers (ones with deep pocket) and "best effort" the rest. Open source could be an option but I still have to try it.
  3. You just need to submit a ticket and provide both IPs so that WHMCS can unlock them.
  4. Then you shouldn't buy anything from any WHMCS-based company because they all fail to meet customers' expectations. There are 55.000 providers out there and less than 10 companies like WGS to choose from. We're always late because we can't handle so many customers. Moreover working with WHMCS is complicated and any library, API, registrar, platform, programming language (...) can change overnight. Not to mention bugs that still exist after so many years. The thing is that a module should cost 1000 / year to meet expectations but no one is willing to pay for it hence it is what it is. I digress. Good luck with your problem. Maybe one day there will be no more companies like WGS. Who you gonna call? Ghostbusters 👻
  5. Your only option is jQuery. Only few pages of admin area have a tpl file. Anyway with jQuery you can do pretty much anything.
  6. Okay I think I fixed it. I had to use more complex conditions. Moreover I restored the "variation" data that was accidentally removed.
  7. I noticed the same on my test WHMCS. I'll let you know when I manage to find the source of the problem.
  8. I'm using this "where" to avoid counting as "Active" products/services with the following status: Suspended, Cancelled, Terminated, Fraud, Pending. Nope. WHMCS simply "counts" products grouping them by "domainstatus". For churn rate you need more than that. We need to know WHEN a specific product/service has been terminated. WHMCS doesn't store any "termination date" (at least not one that we can use) that's why I count "Terminated" products based on "nextduedate". If "nextduedate" is < "current date" no one renewed the product/service in question so I count it as "Terminated". I don't think there's another way to do that. I mean if I count products/services just looking at "Terminated" and "Active" status I end up with products/services and domains from 2017 and 2018 and 2019 and 2020 and 2021 and 2022 (...) all combined. Calculating churn rate would be impossible since every month/year will be the same. The only downside is that statistics for current month can't be accurate regarding "Terminated" stuff. In the latest version of the script I also added the following tooltip next to current month «Statistics for current month are inaccurate as renewals still have to occur». I hope it's clear.
  9. Don't forget you can also upload files manually via FTP.
  10. Setup > Payments > Tax Configuration > Advanced Settings > Calculation Mode. Make sure Calculate based on collective sum of the taxable line items is selected.
  11. There's no way to do that with standard WHMCS. The first solution that comes to mind is boring but should work. Use EmailPreSend to detect invoice-related emails and abort sending (you can't handle attachments via API) Re-create the same email directly including phpMailer class Download the invoice from dl.php file (Admin authentication required) and store it in a PHP variable (eg. $output = curl_exec($ch);) Query db to retreive invoice status and anything you want Name the PDF as you wish like for example "I am Sexy 2020 this invoice is Unpaid You are client ID 120 with USD currency.pdf") bold words come from database echo $output in that file setting haders (application/pdf) Attach the newly generated file in th email and send it As I said it is boring.
  12. Got it. There was an unnecessary filter by date <= current date on "Terminated" products/domains. I updated the script. It should work now. Edit: I realized now that I was also counting "Free" products/services. I'm going to change the script so that they're not part of the report.
  13. Yes, DecryptPassword always returns the original password. On the other hand EncryptPassword always returns a different value. $results = localAPI('EncryptPassword', array('password2' => 'aaa')); echo "<pre>"; print_r($results); echo "</pre>"; $results = localAPI('DecryptPassword', array('password2' => $results['password'])); echo "<pre>"; print_r($results); echo "</pre>"; If you run the above script 3 times you'll get the following. 1st run Array ( [result] => success [password] => 56Caa8prWCL/Y5rAie/0bfOAlMDoYMk= ) Array ( [result] => success [password] => aaa ) 2nd run Array ( [result] => success [password] => 9z1gjm1s8mjLhVNzMoTh42QaAoj4/TM= ) Array ( [result] => success [password] => aaa ) 3rd run Array ( [result] => success [password] => xYg2vuJuYMdMcYShOPKO/4wKXrzKSGU= ) Array ( [result] => success [password] => aaa ) That's why I suspect @hogava is referring to EncryptPassword.
  14. Probably you are running the code on every page load.
  15. It was It was harder than I thought 🥶 I don't want to annoy you with details so let's get straight to the point. Can you confirm me that the following numbers are correct? Anyway you can also test it on your WHMCS. I updated the script on Github.
  16. I noticed more than one problem. The core of the issue is that I'm starting the year with a wrong calculation. I'll fix it as soon as I can.
  17. Exactly, just unset($_SESSION['uid']);
  18. Done. Here's the preview (click to enlarge). There should be everything. Here is the code but since this community doesn't allow me to edit posts after X hours, you can also find it on Github. I'm still making small changes. I don't like color scheme 😑 <?php /** * Churn Rate * * @writtenby Kian * */ use WHMCS\Carbon; use WHMCS\Database\Capsule; if (!defined("WHMCS")) { die("This file cannot be accessed directly"); } $dateFilter = Carbon::create($year, $month, 1); $startOfMonth = $dateFilter->startOfMonth()->toDateTimeString(); $endOfMonth = $dateFilter->endOfMonth()->toDateTimeString(); $reportdata["title"] = 'Churn Rate for ' . $year; $reportdata["description"] = "Rate at which customers stop doing business with you."; $reportdata["yearspagination"] = true; $reportdata["tableheadings"] = array( 'Date', 'Products', '<strong class="text-success"><i class="far fa-plus-square"></i></strong>', '<strong class="text-danger"><i class="far fa-minus-square"></i></strong>', '<strong><i class="fas fa-percentage"></i></strong>', 'Domains', '<strong class="text-success"><i class="far fa-plus-square"></i></strong>', '<strong class="text-danger"><i class="far fa-minus-square"></i></strong>', '<strong><i class="fas fa-percentage"></i></strong>', 'Overall', '<strong class="text-success"><i class="far fa-plus-square"></i></strong>', '<strong class="text-danger"><i class="far fa-minus-square"></i></strong>', '<strong><i class="fas fa-percentage"></i></strong>', ); $reportvalues = array(); // Products/Services $groupBy = Capsule::raw('date_format(`regdate`, "%Y-%c")'); $reportvalues['productsNew'] = Capsule::table('tblhosting')->whereYear('regdate', '=', $year)->where('domainstatus', 'Active')->groupBy($groupBy)->orderBy('regdate')->pluck(Capsule::raw('count(id) as total'), Capsule::raw('date_format(`regdate`, "%Y-%c") as month')); $groupBy = Capsule::raw('date_format(`nextduedate`, "%Y-%c")'); $reportvalues['productsTerminated'] = Capsule::table('tblhosting')->whereYear('nextduedate', '=', $year)->where('nextduedate', '<=', $dateFilter->format('Y-m-d'))->whereNotIn('billingcycle', ['One Time', 'Completed'])->groupBy($groupBy)->orderBy('nextduedate')->pluck(Capsule::raw('count(id) as total'), Capsule::raw('date_format(`nextduedate`, "%Y-%c") as month')); $activeProducts = Capsule::table('tblhosting')->where('domainstatus', 'Active')->pluck(Capsule::raw('count(id) as total'))[0]; // Domains $groupBy = Capsule::raw('date_format(`registrationdate`, "%Y-%c")'); $reportvalues['domainsNew'] = Capsule::table('tbldomains')->where('status', 'Active')->whereYear('registrationdate', '=', $year)->groupBy($groupBy)->orderBy('registrationdate')->pluck(Capsule::raw('count(id) as total'), Capsule::raw('date_format(`registrationdate`, "%Y-%c") as month')); $groupBy = Capsule::raw('date_format(`nextduedate`, "%Y-%c")'); $reportvalues['domainsTerminated'] = Capsule::table('tbldomains')->whereYear('nextduedate', '=', $year)->where('nextduedate', '<=', $dateFilter->format('Y-m-d'))->groupBy($groupBy)->orderBy('nextduedate')->pluck(Capsule::raw('count(id) as total'), Capsule::raw('date_format(`nextduedate`, "%Y-%c") as month')); $activeDomains = Capsule::table('tbldomains')->where('status', 'Active')->pluck(Capsule::raw('count(id) as total'))[0]; for ($tmonth = 1; $tmonth <= 12; $tmonth++) { if (date('Y') == $year AND $tmonth > str_replace('0', '', $month)): continue; endif; $date = Carbon::create($year, $tmonth, 1); $dateMonthYear = $date->format('M Y'); $dateMonth = $date->format('M'); $key = $year . '-' . $tmonth; // Products $activeProducts = $activeProducts + $reportvalues['productsNew'][$key]; $productStart[$tmonth] = $activeProducts; $reportvalues['productsCumulative'][$key] = $activeProducts; $productsNew = isset($reportvalues['productsNew'][$key]) ? $reportvalues['productsNew'][$key] : '0'; $productsTerminated = isset($reportvalues['productsTerminated'][$key]) ? $reportvalues['productsTerminated'][$key] : '0'; $productsCumulative = isset($reportvalues['productsCumulative'][$key]) ? $reportvalues['productsCumulative'][$key] : '0'; $productVariation = $productsNew - $productsTerminated; $productChurnRate = number_format(($productsTerminated / $productStart[($tmonth == '1' ? '1' : $tmonth - 1)]) * 100, 1, '.', '') + 0; // Domains $activeDomains = $activeDomains + $reportvalues['domainsNew'][$key]; $domainStart[$tmonth] = $activeDomains; $reportvalues['domainsCumulative'][$key] = $activeDomains; $domainsNew = isset($reportvalues['domainsNew'][$key]) ? $reportvalues['domainsNew'][$key] : '0'; $domainsTerminated = isset($reportvalues['domainsTerminated'][$key]) ? $reportvalues['domainsTerminated'][$key] : '0'; $domainsCumulative = isset($reportvalues['domainsCumulative'][$key]) ? $reportvalues['domainsCumulative'][$key] : '0'; $domainVariation = $domainsNew - $domainsTerminated; $domainChurnRate = number_format(($domainsTerminated / $domainStart[($tmonth == '1' ? '1' : $tmonth - 1)]) * 100, 1, '.', '') + 0; // Overall $activeOverall = $activeProducts + $activeDomains; $overallStart[$tmonth] = $activeOverall; $reportvalues['overallCumulative'][$key] = $activeOverall; $overallNew = $productsNew + $domainsNew; $overallTerminated = $productsTerminated + $domainsTerminated; $overallCumulative = $productsCumulative + $domainsCumulative; $overallVariation = $productVariation + $domainVariation; $overallChurnRate = $productChurnRate + $domainChurnRate; $reportdata['tablevalues'][] = array( $dateMonthYear, formatCell(array('col' => 'products=', 'variation' => $productVariation, 'start' => $productStart[($tmonth == '1' ? '1' : $tmonth - 1)], 'end' => $productStart[($tmonth == '1' ? '1' : $tmonth - 1)] + $productVariation)), formatCell(array('col' => 'products+', 'increase' => $productsNew)), formatCell(array('col' => 'products-', 'decrease' => $productsTerminated)), formatCell(array('col' => 'products%', 'churnRate' => $productChurnRate)), formatCell(array('col' => 'domains=', 'variation' => $domainVariation, 'start' => $domainStart[($tmonth == '1' ? '1' : $tmonth - 1)], 'end' => $domainStart[($tmonth == '1' ? '1' : $tmonth - 1)] + $domainVariation)), formatCell(array('col' => 'domains+', 'increase' => $domainsNew)), formatCell(array('col' => 'domains-', 'decrease' => $domainsTerminated)), formatCell(array('col' => 'domains%', 'churnRate' => $domainChurnRate)), formatCell(array('col' => 'overall=', 'variation' => $overallVariation, 'start' => $overallStart[($tmonth == '1' ? '1' : $tmonth - 1)], 'end' => $overallStart[($tmonth == '1' ? '1' : $tmonth - 1)] + $overallVariation)), formatCell(array('col' => 'overall+', 'increase' => $overallNew)), formatCell(array('col' => 'overall-', 'decrease' => $overallTerminated)), formatCell(array('col' => 'overall%', 'churnRate' => $overallChurnRate)), ); $chartdata['rows'][] = array( 'c'=>array( array('v' => $dateMonth), array('v' => (int)$productStart[($tmonth == '1' ? '1' : $tmonth - 1)] + $productVariation), array('v' => (int)$domainStart[($tmonth == '1' ? '1' : $tmonth - 1)] + $domainVariation), array('v' => (int)$overallStart[($tmonth == '1' ? '1' : $tmonth - 1)] + $overallVariation), ) ); } function formatCell($data) { /** * @param string $col Column type * @param string $variation Monthly change * @param string $increase New purchases * @param string $decrease New terminations * @param string $start No. of customers (at the start of the period) * @param string $end No. of customers (at the end of the period) * @param string $churnRate Churn Rate * @return string Formatted HTML cell */ $data['variation'] = ($data['variation'] ? $data['variation'] : '0'); $data['increase'] = ($data['increase'] ? $data['increase'] : '0'); $data['decrease'] = ($data['decrease'] ? $data['decrease'] : '0'); $data['start'] = ($data['start'] ? $data['start'] : '0'); $data['end'] = ($data['end'] ? $data['end'] : '0'); $data['churnRate'] = ($data['churnRate'] ? $data['churnRate'] : false); if (in_array($data['col'], array('products+', 'domains+', 'overall+'))) { if ($data['increase']) { return '<span>' . $data['increase'] . '</span>'; } else { return '-'; } } elseif (in_array($data['col'], array('products-', 'domains-', 'overall-'))) { if ($data['decrease']) { return '<span>' . $data['decrease'] . '</span>'; } else { return '-'; } } elseif (in_array($data['col'], array('products=', 'domains=', 'overall='))) { if ($data['variation'] > '0') { $variation = '<small class="pull-right" style="opacity:0.8;">' . abs($data['variation']) . '<i class="fad fa-angle-double-up fa-fw text-success"></i></span>'; } elseif ($data['variation'] < '0') { $variation = '<small class="pull-right" style="opacity:0.8;">' . abs($data['variation']) . '<i class="fad fa-angle-double-down fa-fw text-danger"></i></span>'; } if ($data['start'] != $data['end']) { return $data['start'] . ' <i class="fas fa-angle-right fa-fw"></i> ' . $data['end'] . $variation; } else { return $data['start']; } } elseif (in_array($data['col'], array('products%', 'domains%', 'overall%'))) { if ($data['churnRate'] > 0) { return '<span class="label label-danger">' . $data['churnRate'] . '%</span>'; } else { return '-'; } } } $chartdata['cols'][] = array('label'=>'Day','type'=>'string'); $chartdata['cols'][] = array('label'=>'Products','type'=>'number'); $chartdata['cols'][] = array('label'=>'Domains','type'=>'number'); $chartdata['cols'][] = array('label'=>'Overall','type'=>'number'); $args = array(); $args['legendpos'] = 'right'; $reportdata["headertext"] = $chart->drawChart('Area',$chartdata,$args,'400px');
  19. Please wait. I decided that the best way to calculate & display churn rate is via WHMCS report. I'm coding one so that you can see that directly from WHMCS (graphs included). Edit: Okay, I've almost finished the script. Below there's a preview. I need you to confirm me that numbers make sense but first let me explain what every digit value means. Let's take Products Jan 2020 as example: -1 previous month variation 8.3% churn rate 1++ new purchase 2-- new terminations 23 No. of products (at the end of the month)
  20. Meta keywords tag has been deprecated more than a decade ago by all search engines. Here is a video from Google uploaded 10 years ago. For titles and meta description you can take a look at this post. That said keep in mind that meta description is NOT a ranking factor and that WHMCS doesn't allow to get any significant benefit on SERP. Simply put WHMCS and SEO are on two different planets so don't waste too much time with titles and stuff link that.
  21. Okay 🤔 One question just to make sure I got it right. I order the following services on Jan: Product A Product B Product C I terminate them as follows: Product A Feb Product B Feb Product C Mar Bonus: I order Product D on Feb. What result are you expecting to see?
  22. There's no need to join the same table. Just count by CASE. Try with this one. SELECT (CONCAT(YEAR(regdate), "-", MONTH(regdate))) AS period, COUNT(CASE WHEN domainstatus = "Terminated" THEN 1 ELSE NULL END) AS terminatedAccounts, COUNT(CASE WHEN domainstatus = 'Active' THEN 1 ELSE NULL END) AS activeAccounts, (COUNT(CASE WHEN domainstatus = "Terminated" THEN 1 ELSE NULL END) / COUNT(CASE WHEN domainstatus = 'Active' THEN 1 ELSE NULL END) * 100) AS churnRate FROM tblhosting GROUP BY YEAR(regdate), MONTH(regdate) DESC Here's a sample result.
  23. <?php use WHMCS\Database\Capsule; add_hook('FraudOrder', 1, function($vars) { $admins = Capsule::table('tbladmins')->where('disabled', '=', '0')->pluck('username'); foreach ($admins as $username) { localAPI('SendAdminEmail', array('type' => 'system', 'customsubject' => 'Fraud Order Detected', 'custommessage' => 'Order #' . $vars['orderid'] . ' detected as Fraudulent'), $username); } }); It should work. It sends the notification to all active administrators.
  24. I think you can use FraudOrder hook point and send emails to administrators via SendEmail API.
  • 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