├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── accesscloudcontrolpanel.php ├── api.php ├── fleio.php ├── fleioaddcredit.php ├── hooks.php ├── index.html ├── migrate_services_external_billing_id.php ├── templates ├── error.tpl └── overview.tpl └── utils.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | v3.2.1 2 | ====== 3 | 4 | https://github.com/fleio/fleio-whmcs/tree/3.2.1 5 | 6 | Release date: 2025-03-18 7 | 8 | ### Added 9 | 10 | \- 11 | 12 | ### Changed 13 | 14 | \- 15 | 16 | ### Fixed 17 | 18 | [fix] #112 Link generated by whmcs is incorrect due to bad log message format 19 | 20 | ### Unreleased 21 | 22 | \- 23 | 24 | ### Deprecated 25 | 26 | \- 27 | 28 | ### Removed 29 | 30 | \- 31 | 32 | ### Security 33 | 34 | \- 35 | 36 | ### Notes 37 | 38 | * Requires Fleio versions 2023.12 and higher 39 | 40 | v3.2.0 41 | ====== 42 | 43 | https://github.com/fleio/fleio-whmcs/tree/3.2.0 44 | 45 | Release date: 2024-04-10 46 | 47 | ### Added 48 | 49 | \- 50 | 51 | ### Changed 52 | 53 | [change] #106 Invoice suspended services 54 | 55 | ### Fixed 56 | 57 | \- 58 | 59 | ### Unreleased 60 | 61 | \- 62 | 63 | ### Deprecated 64 | 65 | \- 66 | 67 | ### Removed 68 | 69 | \- 70 | 71 | ### Security 72 | 73 | \- 74 | 75 | ### Notes 76 | 77 | * Requires Fleio versions 2023.12 and higher 78 | 79 | v3.1.0 80 | ====== 81 | 82 | https://github.com/fleio/fleio-whmcs/tree/3.1.0 83 | 84 | Release date: 2024-03-11 85 | 86 | ### Added 87 | 88 | * [add] #109 Option to activate OpenStack service only after email verification in WHMCS 89 | 90 | ### Changed 91 | 92 | \- 93 | 94 | ### Fixed 95 | 96 | \- 97 | 98 | ### Unreleased 99 | 100 | \- 101 | 102 | ### Deprecated 103 | 104 | \- 105 | 106 | ### Removed 107 | 108 | \- 109 | 110 | ### Security 111 | 112 | \- 113 | 114 | ### Notes 115 | 116 | * Requires Fleio versions 2023.12 and higher 117 | 118 | 119 | v3.0.0 120 | ====== 121 | 122 | https://github.com/fleio/fleio-whmcs/tree/3.0.0 123 | 124 | Release date: 2023-12-05 125 | 126 | ### Added 127 | 128 | \- 129 | 130 | ### Changed 131 | 132 | * [change] #107 Service `resume` API call to `unsuspend` 133 | 134 | ### Fixed 135 | 136 | \- 137 | 138 | ### Unreleased 139 | 140 | \- 141 | 142 | ### Deprecated 143 | 144 | \- 145 | 146 | ### Removed 147 | 148 | \- 149 | 150 | ### Security 151 | 152 | \- 153 | 154 | ### Notes 155 | 156 | * Requires Fleio versions 2023.12 and higher 157 | 158 | 159 | v2.0.6 160 | ====== 161 | 162 | https://github.com/fleio/fleio-whmcs/tree/2.0.6 163 | 164 | Release date: 2023-10-10 165 | 166 | ### Added 167 | 168 | \- 169 | 170 | ### Changed 171 | 172 | \- 173 | 174 | ### Fixed 175 | 176 | * [fix] #103 Service payment method is not set on manual credit invoice create 177 | 178 | 179 | ### Unreleased 180 | 181 | \- 182 | 183 | ### Deprecated 184 | 185 | \- 186 | 187 | ### Removed 188 | 189 | \- 190 | 191 | ### Security 192 | 193 | \- 194 | 195 | ### Notes 196 | 197 | \- 198 | 199 | 200 | v2.0.5 201 | ====== 202 | 203 | https://github.com/fleio/fleio-whmcs/tree/2.0.5 204 | 205 | Release date: 2023-07-20 206 | 207 | ### Added 208 | 209 | \- 210 | 211 | ### Changed 212 | 213 | \- 214 | 215 | ### Fixed 216 | 217 | * [fix] #101 Automatically generated invoices do not change client credit in Fleio once paid 218 | 219 | 220 | ### Unreleased 221 | 222 | \- 223 | 224 | ### Deprecated 225 | 226 | \- 227 | 228 | ### Removed 229 | 230 | \- 231 | 232 | ### Security 233 | 234 | \- 235 | 236 | ### Notes 237 | 238 | * [internal] #100 Specify in readme that `tblclients` - `uuid` column should be indexed 239 | 240 | 241 | v2.0.4 242 | ====== 243 | 244 | https://github.com/fleio/fleio-whmcs/tree/2.0.4 245 | 246 | Release date: 2023-07-14 247 | 248 | ### Added 249 | 250 | \- 251 | 252 | ### Changed 253 | 254 | * [change] #93 Add more detailed logging for auto-invoicing feature 255 | * [change] #98 Sync terminated services from Fleio 256 | 257 | ### Fixed 258 | 259 | * [fix] #92 If last client to auto-invoice throws error, last batch of clients to be processed is skipped 260 | * [fix] #96 getClientProduct does not work without providing Fleio product ID 261 | 262 | 263 | ### Unreleased 264 | 265 | \- 266 | 267 | ### Deprecated 268 | 269 | \- 270 | 271 | ### Removed 272 | 273 | \- 274 | 275 | ### Security 276 | 277 | \- 278 | 279 | ### Notes 280 | 281 | 282 | v2.0.3 283 | ====== 284 | 285 | https://github.com/fleio/fleio-whmcs/tree/2.0.3 286 | 287 | Release date: 2023-06-14 288 | 289 | ### Added 290 | 291 | \- 292 | 293 | ### Changed 294 | 295 | \- 296 | 297 | ### Fixed 298 | 299 | * [fix] #94 Fleio end-user is left inactive after unsuspend from WHMCS 300 | 301 | ### Unreleased 302 | 303 | \- 304 | 305 | ### Deprecated 306 | 307 | \- 308 | 309 | ### Removed 310 | 311 | \- 312 | 313 | ### Security 314 | 315 | \- 316 | 317 | ### Notes 318 | 319 | 320 | v2.0.2 321 | ====== 322 | 323 | https://github.com/fleio/fleio-whmcs/tree/2.0.2 324 | 325 | Release date: 2023-02-20 326 | 327 | ### Added 328 | 329 | \- 330 | 331 | ### Changed 332 | 333 | * [change] #89 Use database transaction when generating invoices 334 | 335 | ### Fixed 336 | 337 | \- 338 | 339 | ### Unreleased 340 | 341 | \- 342 | 343 | ### Deprecated 344 | 345 | \- 346 | 347 | ### Removed 348 | 349 | \- 350 | 351 | ### Security 352 | 353 | \- 354 | 355 | ### Notes 356 | 357 | * Requires Fleio versions 2022.11 and higher 358 | 359 | 360 | v2.0.1 361 | ====== 362 | 363 | https://github.com/fleio/fleio-whmcs/tree/2.0.1 364 | 365 | Release date: 2023-01-12 366 | 367 | ### Added 368 | 369 | \- 370 | 371 | ### Changed 372 | 373 | \- 374 | 375 | ### Fixed 376 | 377 | * [fix] #80 Multiple unnecessary GET calls for client when creating OS service 378 | 379 | ### Unreleased 380 | 381 | \- 382 | 383 | ### Deprecated 384 | 385 | \- 386 | 387 | ### Removed 388 | 389 | \- 390 | 391 | ### Security 392 | 393 | \- 394 | 395 | ### Notes 396 | 397 | * Requires Fleio versions 2022.11 and higher 398 | 399 | 400 | v2.0.0 401 | ====== 402 | 403 | https://github.com/fleio/fleio-whmcs/tree/2.0.0 404 | 405 | Release date: 2022-11-01 406 | 407 | ### Added 408 | 409 | \- 410 | 411 | ### Changed 412 | 413 | * [change] #79 Mention "Mark invoiced periods as paid" setting in Readme 414 | * [change] #78 Client OpenStack services API endpoint 415 | 416 | ### Fixed 417 | 418 | * [fix] #81 API request filter by removed field GET /staffapi/users?username=whmcs76 419 | 420 | ### Unreleased 421 | 422 | \- 423 | 424 | ### Deprecated 425 | 426 | \- 427 | 428 | ### Removed 429 | 430 | \- 431 | 432 | ### Security 433 | 434 | \- 435 | 436 | ### Notes 437 | 438 | * Requires Fleio versions 2022.11 and higher 439 | 440 | v1.0.1 441 | ====== 442 | 443 | https://github.com/fleio/fleio-whmcs/tree/1.0.1 444 | 445 | Release date: 2022-11-01 446 | 447 | ### Fixed 448 | 449 | * [fix] #81 API request filter by removed field GET /staffapi/users?username=whmcs76 450 | 451 | ### Notes 452 | 453 | * Works with Fleio versions up to and including 2022.10 454 | 455 | v1.0.0 456 | ====== 457 | 458 | https://github.com/fleio/fleio-whmcs/tree/1.0.0 459 | 460 | Release date: 2022-09-06 461 | 462 | ### Notes 463 | 464 | * Works with Fleio versions up to and including 2022.10 465 | 466 | * Latest commit that includes version 1.0.0 is: [change] #77 Mention "Fleio username prefix" is not used anymore from Fleio 2022.09.0 467 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD License 2 | 3 | Copyright (c) FLEIO SRL and individual contributors. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, 7 | are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, 10 | this list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright 13 | notice, this list of conditions and the following disclaimer in the 14 | documentation and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of FLEIO SRL nor the names of its contributors may be used 17 | to endorse or promote products derived from this software without 18 | specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 21 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 24 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 25 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 26 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 27 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 29 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Fleio WHMCS module 2 | 3 | 4 | This is the Fleio WHMCS module to allow integration between WHMCS and Fleio. 5 | 6 | Fleio is an OpenStack billing system and self-service portal software that 7 | enables service providers to sell public cloud services. 8 | 9 | 10 | ## Requirements 11 | * WHMCS 8.x, 7.8+ 12 | * Requires PHP 7.x 13 | * Fleio WHMCS module version 3.0.x requires Fleio versions 2023.12 and higher 14 | * Fleio WHMCS module version 2.0.x works with Fleio versions starting with 2022.11 and up to and including 2023.11 15 | * Fleio WHMCS module version 1.0.x works with Fleio versions up to and including 2022.10 16 | 17 | 18 | ## Installation 19 | 20 | 21 | 1. Copy the source files to the WHMCS installation directory in `modules/servers/fleio` 22 | 2. Move `WHMCS_INSTALL_DIR/modules/servers/fleio/fleioaddcredit.php` to `WHMCS_INSTALL_DIR/fleioaddcredit.php` 23 | 3. Move `WHMCS_INSTALL_DIR/modules/servers/fleio/accesscloudcontrolpanel.php` to `WHMCS_INSTALL_DIR/accesscloudcontrolpanel.php` 24 | 4. Login to WHMCS as admin and create a new product from: `Setup -> Product/Services -> Product/Services -> Create New Product` 25 | 5. Under Module Settings on the new product page, select the `Fleio` module 26 | 6. Retrieve a token from Fleio (after logging in as admin in backend (/backend/admin) -> Tokens -> Add Token for a staff user) 27 | and add it to the WHMCS module in module settings 28 | 7. Set the frontend public urls for user and admin 29 | 8. Set the backend public url (eg: http://server_hostname/staffapi) 30 | 9. Set the Maximum and Minimum amounts (these are used to limit the amount a user can pay for a service) 31 | 10. Make sure that WHMCS and Fleio have the same currencies 32 | 11. In case a Configuration name and Group name is set in WHMCS Module Settings, make sure Fleio has the same Configuration and Client Group names. 33 | 34 | The Client Group name can be set through the /backend/admin/ at `Fleio core app` -> `Client groups` 35 | 36 | ## How the module works 37 | 38 | 39 | The WHMCS module supports the following: 40 | * WHMCS product actions: Create. Suspend, Unsuspend and Terminate 41 | * auto creation of Fleio account 42 | * auto login to Fleio from WHMCS Service page for users 43 | * auto credit adjustment for paid invoices 44 | * ability to create new invoices for Fleio credit 45 | * auto update of Fleio client details if WHMCS client is updated 46 | * auto update of Fleio client when he adds a billing agreement or CC in WHMCS 47 | * you can filter the billing agreement ID prefix that leads to client marked as having a billing agreement in Fleio. For instance, you can consider clients with a Stripe billing agreement ( prefix "cus_"), but not does with a PayPal billing agreement (prefix "B-"). 48 | * auto issue of invoices for Fleio clients passing a certain amount in usage before the end of a month 49 | * auto issue of invoices for Fleio clients at the end of a month 50 | * auto charge attempt after an invoice is issued for clients with a billing agreement or CC on file 51 | 52 | ## How does the module issue new invoices 53 | 54 | We will assume a credit limit exists in Fleio: 55 | 56 | * for clients without a billing agreement or CC on file of: -10 USD 57 | * for clients with a billing agreement or CC on file of: -20 USD 58 | 59 | We have the following limit in WHMCS: 60 | *Do not invoice amount below*: 1 USD 61 | 62 | Also, we will have the following terms: 63 | * uninvoiced usage: the difference between total usage and invoiced usage 64 | 65 | If *Invoice clients without billing agreement* is checked in the Fleio Module Settings in WHMCS, all clients that do not have a billing agreement or CC on file in WHMCS will be issued invoices as follows: 66 | 67 | * any time during a month, if uninvoiced usage exceeds the credit limit defined in Fleio configuration (10 USD in our example) 68 | * at the end a billing cycle (one month), for the amount consumed if it's not lower than the *Do not invoice amount below* limit 69 | 70 | Also, if *Invoice only in end of cycle timespan* is checked, invoice won't be issued at the end of the billing cycle (related service cycle having a status of "unpaid") if WHMCS cron runs and processes the client 72 hours after the cycle end date. 71 | 72 | The same as the above applies for clients with a billing agreement of CC on file when *Invoice clients with billing agreement* is checked with the usage being over 20 USD. 73 | If the option *Attempt a charge immediately* is also checked, then a charge is attempted automatically for these clients, for the invoices issued. 74 | 75 | When a client passes his credit limit the second time during a month, a second invoice will be issued and so on. 76 | 77 | On all cases, two invoices will never be issued on the same day for the same product. 78 | There is a delay of 1 day for when a second invoice can be automatically issued. 79 | The payment method set for invoices is the one set on the WHMCS service, not the Client. 80 | Do note however that if you set a new payment method on the WHMCS Client profile tab, that payment method will be used for all unpaid invoices and 81 | auto charging may fail if the new gateway does not support this. 82 | If auto generated invoices are deleted or marked canceled, new invoices will be issued. 83 | 84 | Clients usage is checked every time the WHMCS cron runs. The default recommended period is 5 minutes. 85 | 86 | ## Scenarios 87 | 88 | 89 | #### Scenario 1 90 | 91 | * The *Do not invoice amount below* limit is 1 USD. 92 | * The *Credit limit when on agreement* limit is set to 50 USD. 93 | * The *Delay suspend* limit is set to 30 days 94 | 95 | At the end of billing cycle, the client has a total cost of services of 10 USD. An invoice will be issued, with the value of 10 USD. Invoice status is unpaid. 96 | 97 | The next day (1st day after the end of the billing cycle) the total cost of services rises up to 20 USD. Invoice will not be issued, since the *Credit limit when on agreement* limit was not reached. 98 | 99 | On the 4th day, the total cost of services rises up to 50 USD. Invoice will still not be issued, since the *Credit limit when on agreement* limit was not reached: 100 | Currently, the client has an invoice of 10 USD, the total costs of services is 50 USD. Uninvoiced usage is equal to 40 USD. The limit is not reached, no invoice is issued. The suspension timer starts since it reached the 50 USD owned mark. 101 | 102 | On the 5th day, the total cost gets to 59.9 USD. Invoice will not be issued. 103 | 104 | On the 6th day, the cost gets to 60 USD. Invoice will be issued, with the total cost of 50 USD. Uninvoiced credit resets to 0 USD. 105 | 106 | On the 25th day of the billing cycle, the costs gets to 90 USD. No invoice will be issued. The uninvoiced credit is 30 USD, which is lower than the *Credit limit when on agreement* limit. 107 | 108 | On the 29th day, the costs gets to 110 USD. Uninvoiced credit is 50 USD, so a new invoice will be issued, with the value of 50 USD. 109 | 110 | 111 | Upon this date, all the invoices are still unpaid. If the invoices are still unpaid on 34th day the client gets suspended. Why? 112 | 113 | 1. The suspension timer started on 4th day 114 | 2. The *delay suspend* limit is 30 days. 115 | 116 | #### Scenario 2 117 | 118 | * The *Do not invoice amount below* limit is 1 USD. 119 | * The *Credit limit when on agreement* limit is set to 50 USD. 120 | * The *Delay suspend* limit is set to 30 days 121 | 122 | At the end of the billing cycle, the client has 0 uninvoiced usage, and all the invoices are paid. 123 | 124 | On the 1st day of the billing cycle, the client will consume a total of 0.23 USD. No invoice will be issued since it does not reach the *Credit limit when on agreement* limit is set to 50 USD. 125 | 126 | On the 3rd day, he has a cost total cost os 0.96 USD. No invoice will be issued. He deletes all the resources and does not create any other cost for the rest of the billing cycle. 127 | 128 | At the end of the billing cycle, the client still won't be invoiced, since the uninvoiced usage does not pass the *Do not invoice amount below* limit. 129 | If you wish to invoice the 0.96 USD at the end of the billing cycle, just set `The Do not invoice amount below: 0 USD`. 130 | 131 | #### Scenario 3 132 | 133 | * The *Do not invoice amount below* limit is 1 USD 134 | * The *Credit limit when on agreement* limit is set to 50 USD 135 | * The *Delay suspend* limit is set to 30 days 136 | 137 | At the end of the billing cycle, the client has 0 uninvoiced usage, and all the invoices are paid. 138 | 139 | On the 1st day of the billing cycle, the client will consume a total of 0.23 USD. No invoice will be issued since it does not reach the *Credit limit when on agreement* limit is set to 50 USD. 140 | 141 | On the 3rd day, he has a cost total cost of 1 USD. No invoice will be issued since it does not reach the 2nd limit. He deletes all the resources and no further costs is generated. 142 | 143 | At the end of the billing cycle it will be issued an invoice of 1 USD. 144 | 145 | 146 | Additional notes 147 | ================ 148 | 149 | The module won't work with more than one Fleio product defined (connected to 2 Fleio installations for example). 150 | 151 | Any WHMCS automated billing will not work as expected and is not recommended. 152 | Instead, you should use the Fleio billing system to calculate usage for clients and to require them to pay invoices or add credit. 153 | To achieve this, you can set the product as free or set a one time payment requirement in WHMCS. 154 | 155 | It's really important to remember that when/if a WHMCS customer's currency is changed, the same operation needs to be done in Fleio and 156 | all Fleio client's services should be checked to have prices in the new currency set for the WHMCS client. 157 | 158 | If you're using invoice generation features of fleio-whmcs plugin, automatic settlements and invoice generation settings 159 | `have to be disabled in Fleio`. With this setup, in order to settle invoiced service cycles in Fleio (thus also adjusting 160 | client total credit in Fleio), you may make use of `Mark invoiced periods as paid when using external billing` setting 161 | from Configuration details / Billing cycles expandable row. This setting works as follows: all invoiced service cycles are 162 | settled if client has up-to-date credit greater than 0 after paying a fleio-whmcs invoice, otherwise settling invoiced cycles 163 | depends on their associated price and how much credit the client added. 164 | 165 | Partial refunds must be manually handled. The Fleio credit will not be modified if you partially refund an invoice in WHMCS 166 | 167 | The module does queries based on `uuid` column from WHMCS `tblclients` table. If this field is not indexed in your database (you 168 | can see that by running `show indexes from tblclients;`), we strongly recommend to add it by 169 | running ```ALTER TABLE `tblclients` ADD INDEX `uuid_index` (`uuid`);```. 170 | 171 | 172 | Activate services after clients validate email 173 | ============================================== 174 | 175 | If you want Fleio services to be activated only after client validates his email, you can 176 | choose "Automatically setup the product when you manually accept a pending order" on the Fleio product's module settings, 177 | and also check the "Activate services for clients with verified email" checkbox in the same section. 178 | This way all clients with verified emails and pending services will be processed during cron run and their services will be 179 | activated. Also, Email Verification from WHMCS must be enabled for this to properly work. 180 | 181 | 182 | 183 | 184 | License information 185 | =================== 186 | 187 | fleio-whmcs is licensed under BSD License. See the *LICENSE* file for more information. 188 | -------------------------------------------------------------------------------- /accesscloudcontrolpanel.php: -------------------------------------------------------------------------------- 1 | setPageTitle('Fleio SSO login'); 15 | $ca->initPage(); 16 | 17 | $ca->requireLogin(); 18 | 19 | // Check login status 20 | if ($ca->isLoggedIn()) { 21 | try { 22 | $prod = Capsule::table('tblhosting') 23 | ->join('tblproducts', 'tblhosting.packageid', '=', 'tblproducts.id') 24 | ->where('tblhosting.userid', '=', $ca->getUserID()) 25 | ->whereIn('tblhosting.domainstatus', ['Active', 'Suspended']) 26 | ->where('tblproducts.servertype', '=', 'fleio') 27 | ->select('tblhosting.id') 28 | ->first(); 29 | } catch (Exception $e) { 30 | header('Location: clientarea.php' ); 31 | exit; 32 | } 33 | $fl = Fleio::fromServiceId($prod->id); 34 | $url = $fl->getSSOUrl(); 35 | header("Location: " . $url); 36 | exit; 37 | 38 | } else { 39 | header('Location: ' . 'clientarea.php'); 40 | exit; 41 | } 42 | -------------------------------------------------------------------------------- /api.php: -------------------------------------------------------------------------------- 1 | SERVER = $server; 21 | $this->clientsdetails = $clientsdetails; 22 | $this->flApi = new FlApi($this->SERVER->url, $this->SERVER->token); 23 | } 24 | 25 | public static function fromParams(array $params) { 26 | $server = new stdClass; 27 | $server->url = FleioUtils::trimApiUrlTrailingSlash($params['configoption4']); 28 | $server->frontend_url = $params['configoption2']; 29 | $server->token = $params['configoption1']; 30 | $server->userPrefix = !empty(trim($params['configoption9'])) ? trim($params['configoption9']) : 'whmcs'; 31 | $server->invoiceClientsWithoutAgreement = trim($params['configoption10']) == 'on' ? True : False; 32 | $server->invoiceClientsWithAgreement = trim($params['configoption11']) == 'on' ? True : False; 33 | $server->ClientConfiguration = !empty(trim($params['configoption8'])) ? trim($params['configoption8']) : NULL; 34 | $clientsdetails = (object) $params['clientsdetails']; 35 | return new self($server, $clientsdetails); 36 | } 37 | 38 | public static function fromServiceId($prodid) { 39 | # NOTE(tomo): prodid is actually a tblhosting object ID, not the tblproducts ID 40 | $prodid = (string) $prodid; 41 | if (!is_string($prodid) or empty($prodid)) { // empty treats "0" as empty. We assume a product id will never be 0. 42 | throw new FlApiException('Unable to initialize the Fleio api client.'); 43 | } 44 | $clientsdetails = Capsule::table('tblclients')->join('tblhosting', 'tblhosting.userid', '=', 'tblclients.id')->where('tblhosting.id', '=', $prodid)->first(); 45 | $dbserver = Capsule::table('tblhosting') 46 | ->join('tblproducts', 'tblhosting.packageid', '=', 'tblproducts.id') 47 | ->select('tblproducts.configoption1', 'tblproducts.configoption2', 'tblproducts.configoption4', 'tblproducts.configoption8', 'tblproducts.configoption9', 'tblproducts.configoption10', 'tblproducts.configoption11') 48 | ->where('tblhosting.id', '=', $prodid)->first(); 49 | $server = new stdClass; 50 | $server->url = FleioUtils::trimApiUrlTrailingSlash($dbserver->configoption4); 51 | $server->frontend_url = $dbserver->configoption2; 52 | $server->token = $dbserver->configoption1; 53 | $server->userPrefix = !empty(trim($dbserver->configoption9)) ? trim($dbserver->configoption9) : 'whmcs'; 54 | $server->invoiceClientsWithoutAgreement = trim($dbserver->configoption10) == 'on' ? True : False; 55 | $server->invoiceClientsWithAgreement = trim($dbserver->configoption11) == 'on' ? True : False; 56 | $server->ClientConfiguration = !empty(trim($dbserver->configoption8)) ? trim($dbserver->configoption8) : NULL; 57 | return new self($server, $clientsdetails); 58 | } 59 | 60 | private function generatePassword($size) { 61 | // We don't use the product password since it's stored in clear text in WHMCS 62 | $data = '1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcefghijklmnopqrstuvwxyz&%#$@'; 63 | return substr(str_shuffle($data), 0, $size); 64 | } 65 | 66 | public function getBillingPrice() { 67 | $fleio_client_id = $this->getClientId(); 68 | $url = '/clients/'. $fleio_client_id . '/billing_summary'; 69 | $response = $this->flApi->get($url); 70 | if ($response == null) { 71 | throw new FlApiRequestException("Unable to retrieve billing summary", 404); 72 | } 73 | return $response['price']; 74 | } 75 | 76 | public function updateServiceExternalBillingId($newServiceExtBillingId, $clientUUID=NULL) { 77 | if ($clientUUID === NULL) { 78 | $clientUUID = $this->clientsdetails->uuid; 79 | } 80 | $url = '/billing/services'; 81 | $response = $this->flApi->get($url, array("filtering"=>"client__external_billing_id:".$clientUUID."+product__product_type:openstack")); 82 | if ($response === null) { 83 | throw new FlApiRequestException("Unable to get service for client with uuid: " . $clientUUID, 404); 84 | } 85 | $responseObjects = $response["objects"]; 86 | if (sizeof($responseObjects)) { 87 | $fleioServiceId = $responseObjects[0]["id"]; 88 | $serviceUrl = '/billing/services/'.$fleioServiceId; 89 | try { 90 | $this->flApi->patch($serviceUrl, array("external_billing_id" => $newServiceExtBillingId)); 91 | } catch (Exception $e) { 92 | echo ''.$e->getMessage(); 93 | } 94 | } 95 | } 96 | 97 | public function createBillingClient($groups, $serviceId=NULL) { 98 | try { 99 | $existentClient = $this->getClient(); 100 | } catch (Exception $e) { 101 | $existentClient = NULL; 102 | } 103 | if ($existentClient === NULL) { 104 | // Create client with auto order service 105 | $url = '/clients'; 106 | $currency = getCurrency(); 107 | $clientUsername = $this->SERVER->userPrefix ? $this->SERVER->userPrefix . $this->clientsdetails->userid : 'whmcs' . $this->clientsdetails->userid; 108 | $user = array("username" => $clientUsername, 109 | "email" => $this->clientsdetails->email, 110 | "email_verified" => true, 111 | "first_name" => $this->clientsdetails->firstname, 112 | "last_name" => $this->clientsdetails->lastname, 113 | "password" => $this->generatePassword(16), 114 | "external_billing_id" => $this->clientsdetails->uuid); 115 | 116 | $client = array('first_name' => $this->clientsdetails->firstname, 117 | 'last_name' => $this->clientsdetails->lastname, 118 | 'company' => $this->clientsdetails->companyname, 119 | 'address1' => $this->clientsdetails->address1, 120 | 'address2' => $this->clientsdetails->address2, 121 | 'city' => $this->clientsdetails->city, 122 | 'state' => $this->clientsdetails->state, 123 | 'country' => $this->clientsdetails->countrycode, 124 | 'zip_code' => $this->clientsdetails->postcode, 125 | 'phone' => $this->clientsdetails->phonenumber, 126 | 'fax' => $this->clientsdetails->fax, 127 | 'email' => $this->clientsdetails->email, 128 | 'external_billing_id' => $this->clientsdetails->uuid, 129 | 'auto_order_service_external_billing_id' => $serviceId, // send service external billing id, available only from fleio version 2019.09 130 | 'currency' => $currency['code'], 131 | 'user' => $user, 132 | 'create_auto_order_service' => true); 133 | 134 | $cbset = $this->SERVER->ClientConfiguration; 135 | if (!empty($cbset)) { 136 | $client['configuration'] = $cbset; 137 | }; 138 | if (!empty($groups) && trim($groups) != '') { 139 | $client_groups = array_map('trim', explode(',', $groups, 10)); 140 | $client['groups'] = $client_groups; 141 | }; 142 | // Set the username to display in Products/Services in WHMCS Admin 143 | if ($serviceId) { 144 | try { 145 | Capsule::table( 'tblhosting' ) 146 | ->where( 'id', '=', $serviceId ) 147 | ->update(['username' => $clientUsername, 'password' => encrypt('set-in-fleio')]); 148 | } catch (Exception $e) { logActivity('Unable to set the Fleio username in WHMCS: '. $e->getMessage()); } 149 | } 150 | return $this->flApi->post($url, $client); 151 | } else { 152 | // Client already exists in fleio, update his fields and run activate on the targeted service, also resume client if necessary 153 | // (will run create on service module only if service is not active) 154 | $updatedDetails = array("first_name" => $this->clientsdetails->firstname, 155 | "last_name" => $this->clientsdetails->lastname, 156 | "address1" => $this->clientsdetails->address1, 157 | "address2" => $this->clientsdetails->address2, 158 | "city" => $this->clientsdetails->city, 159 | "state" => $this->clientsdetails->state, 160 | "email" => $this->clientsdetails->email, 161 | "zip_code" => $this->clientsdetails->postcode, 162 | "country" => $this->clientsdetails->country, 163 | "company" => $this->clientsdetails->companyname, 164 | "phone" => $this->clientsdetails->phonenumber); 165 | try { 166 | // update client details 167 | $this->updateFleioClient($updatedDetails, $existentClient['id']); 168 | } catch (Exception $e) { 169 | logActivity( 170 | 'Could not update client ' . 171 | $this->clientsdetails->uuid . 172 | ' details after running create on his existing service. Reason: ' . 173 | $e->getMessage() 174 | ); 175 | } 176 | // resume client if necessary 177 | if ($existentClient["status"] !== "active") { 178 | logActivity('Fleio: Resuming client ' . $this->clientsdetails->uuid . ' before running create on his existing service.'); 179 | $url = '/clients/' . $existentClient['id'] . '/resume'; 180 | try { 181 | $this->flApi->post($url); 182 | } catch (Exception $e) { 183 | logActivity('Fleio: Could not resume client ' . $this->clientsdetails->uuid . ' before running create on his existing service.'); 184 | } 185 | } 186 | // TODO: update user if necessary 187 | // Resume user 188 | try { 189 | $relatedUser = $this->getUser($this->clientsdetails->uuid); 190 | } catch (Exception $e) { 191 | $relatedUser = NULL; 192 | } 193 | if ($relatedUser !== NULL && !$relatedUser["is_active"]) { 194 | try { 195 | $this->updateFleioUser($relatedUser["id"], array("is_active" => true)); 196 | } catch (Exception $e) { 197 | logActivity('Fleio: Could not re-activate user ' . $relatedUser["id"] . ' before running create on his existing service.'); 198 | } 199 | } 200 | // Run create on existing targeted service 201 | $urlFleioService = '/billing/services'; 202 | try { 203 | $filteringString = "external_billing_id:" . $serviceId . "+client__external_billing_id:" . (string)$this->clientsdetails->uuid; 204 | $response = $this->flApi->get($urlFleioService, array("filtering" => $filteringString)); 205 | } catch (Exception $e) { 206 | throw new FlApiRequestException('Fleio: unable to find service ' . $serviceId . ' for activating it. Check if client and service are tied through external_billing_id.', 404); 207 | } 208 | if ($response === null) { 209 | throw new FlApiRequestException('Fleio: unable to find service ' . $serviceId . ' for activating it. Check if client and service are tied through external_billing_id.', 404); 210 | } 211 | $responseObjects = $response["objects"]; 212 | if (sizeof($responseObjects)) { 213 | $fleioServiceId = $responseObjects[0]["id"]; 214 | $url = '/billing/services/' . $fleioServiceId . '/activate'; 215 | return $this->flApi->post($url); 216 | } else { 217 | // This service is not in fleio, check if user has another terminated one and create a new OS service if so (fleio supports at most 1 active/suspended service per client) 218 | try { 219 | $filteringString = "client__external_billing_id:" . (string)$this->clientsdetails->uuid . "+product__product_type:openstack"; 220 | $otherServicesResponse = $this->flApi->get($urlFleioService, array("filtering" => $filteringString)); 221 | } catch (Exception $e) { 222 | throw new FlApiRequestException('Fleio: unable to retrieve services.', 404); 223 | } 224 | $otherServicesObjects = $otherServicesResponse["objects"]; 225 | if (sizeof($otherServicesObjects)) { 226 | for($i = 0; $i < sizeof($otherServicesObjects); $i++) { 227 | if ($otherServicesObjects[$i]["status"] !== "terminated") { 228 | throw new FlApiRequestException( 229 | 'Cannot create OS service in fleio. Only one active/suspended service is available for a client.' 230 | ); 231 | } 232 | } 233 | } 234 | // get product data 235 | $osServicesUrl = '/openstack/billing/services/'; 236 | try { 237 | $newServiceProductsResp = $this->flApi->get($osServicesUrl . 'new_service_data', array( 238 | "client_id" => $existentClient['id'] 239 | )); 240 | $newServiceProducts = $newServiceProductsResp["products"]; 241 | $newServiceProductId = $newServiceProducts[0]["id"]; 242 | $newServiceCycleId = $newServiceProducts[0]["cycles"][0]["id"]; 243 | } catch (Exception $e) { 244 | throw new FlApiRequestException( 245 | 'Fleio: unable to retrieve products for new OS service. Reason: ' . (string)$e->getMessage(), 404 246 | ); 247 | } 248 | // create new service 249 | try { 250 | $this->flApi->post($osServicesUrl . 'create_openstack_service', array( 251 | "client_id" => $existentClient['id'], 252 | "product_id" => $newServiceProductId, 253 | "product_cycle_id" => $newServiceCycleId, 254 | "service_external_id" => $serviceId, 255 | "create_new_project" => true 256 | )); 257 | } catch (Exception $e) { 258 | throw new FlApiRequestException( 259 | 'Fleio: unable to create new OS service for client. Reason: ' . (string)$e->getMessage(), 400 260 | ); 261 | } 262 | } 263 | } 264 | } 265 | 266 | public function updateFleioClient($details, $fleioClientId='') { 267 | // Update the Fleio Client details 268 | if (!$fleioClientId) { 269 | $fleioClientId = $this->getClientId(); 270 | } 271 | $url = '/clients/'.$fleioClientId; 272 | return $this->flApi->patch($url, $details); 273 | } 274 | 275 | public function updateFleioUser($fleioUserId, $details) { 276 | // Update the Fleio user details 277 | $url = '/users/'.$fleioUserId; 278 | return $this->flApi->patch($url, $details); 279 | } 280 | 281 | public function getUser($clientUUID) { 282 | $url = '/users'; 283 | $query_params = array('external_billing_id' => (string)$clientUUID); 284 | $response = $this->flApi->get($url, $query_params); 285 | if ($response == null) { 286 | throw new FlApiRequestException("Unable to retrieve Fleio user with external billing id: " . (string)$clientUUID, 400); 287 | } 288 | $objects = $response['objects']; 289 | if (count($objects) > 1) { 290 | throw new FlApiRequestException("Unable to retrieve Fleio user with external billing id: " . (string)$clientUUID, 409); // Multiple objects returned 291 | } 292 | if (count($objects) == 0) { 293 | throw new FlApiRequestException("Unable to retrieve Fleio user with external billing id: " . (string)$clientUUID, 404); // Not found 294 | } 295 | return $objects[0]; 296 | } 297 | 298 | public function getClient() { 299 | /* Get the Fleio client id from the WHMCS user id */ 300 | # TODO(tomo): throw if the clientId is not found 301 | $url = '/clients'; 302 | $query_params = array('external_billing_id' => $this->clientsdetails->uuid); 303 | $response = $this->flApi->get($url, $query_params); 304 | if ($response == null) { 305 | throw new FlApiRequestException("Unable to retrieve the Fleio client with external billing id: " . (string)$this->clientsdetails->uuid, 400); 306 | } 307 | $objects = $response['objects']; 308 | if (count($objects) > 1) { 309 | throw new FlApiRequestException("Unable to retrieve the Fleio client with external billing id: " . (string)$this->clientsdetails->uuid, 409); // Multiple objects returned 310 | } 311 | if (count($objects) == 0) { 312 | throw new FlApiRequestException("Unable to retrieve the Fleio client with external billing id: " . (string)$this->clientsdetails->uuid, 404); // Not found 313 | } 314 | return $objects[0]; 315 | } 316 | 317 | private function getClientId() { 318 | $client = $this->getClient(); 319 | return $client['id']; 320 | } 321 | 322 | private function getSSOSession() { 323 | $url = '/get-sso-session'; 324 | $params = array('euid' => $this->clientsdetails->uuid); 325 | return $this->flApi->post($url, $params); 326 | } 327 | 328 | public function getSSOUrl() { 329 | $euid = $this->clientsdetails->uuid; 330 | $url = $this->SERVER->frontend_url . '/sso?'; 331 | $rsp = $this->getSSOSession(); 332 | $params = array( 'euid', 'timestamp', 'hash_val' ); 333 | $send_params = array_combine($params, explode(":", $rsp['hash_val'])); 334 | $send_params = http_build_query($send_params); 335 | return $url . $send_params; 336 | } 337 | 338 | public function suspendOpenstack($serviceId) { 339 | // get fleio service id and run suspend on fleio service 340 | $urlFleioService = '/billing/services'; 341 | try { 342 | $filteringString = "external_billing_id:" . $serviceId . "+client__external_billing_id:" . (string)$this->clientsdetails->uuid; 343 | $response = $this->flApi->get($urlFleioService, array("filtering" => $filteringString)); 344 | } catch (Exception $e) { 345 | throw new FlApiRequestException('Fleio: unable to find service ' . $serviceId . ' for suspension. Check if client and service are tied through external_billing_id', 404); 346 | } 347 | if ($response === null) { 348 | throw new FlApiRequestException('Fleio: unable to find service ' . $serviceId . ' for suspension. Check if client and service are tied through external_billing_id', 404); 349 | } 350 | $responseObjects = $response["objects"]; 351 | if (sizeof($responseObjects)) { 352 | $fleioServiceId = $responseObjects[0]["id"]; 353 | $url = '/billing/services/' . $fleioServiceId . '/suspend'; 354 | $this->flApi->post($url); 355 | } 356 | // If client does not have any other active service, suspend client 357 | try { 358 | $otherServicesResponse = $this->flApi->get($urlFleioService, array( 359 | "filtering" => "client__external_billing_id:".$this->clientsdetails->uuid."+id__ne:".$fleioServiceId 360 | )); 361 | } catch (Exception $e) { 362 | $otherServicesResponse = NULL; 363 | } 364 | $otherActiveServicesCount = 0; 365 | if ($otherServicesResponse) { 366 | $otherServicesResponseObjects = $otherServicesResponse["objects"]; 367 | if (sizeof($otherServicesResponseObjects)) { 368 | for($i = 0 ; $i < sizeof($otherServicesResponseObjects); $i++) { 369 | if ($otherServicesResponseObjects[$i]["status"] !== "terminated" && $otherServicesResponseObjects[$i]["status"] !== "suspended") { 370 | $otherActiveServicesCount = $otherActiveServicesCount + 1; 371 | } 372 | } 373 | } 374 | } 375 | if ($otherActiveServicesCount === 0) { 376 | // suspend client 377 | $fleio_client_id = $this->getClientId(); 378 | $url = '/clients/' . $fleio_client_id . '/suspend'; 379 | try { 380 | $this->flApi->post($url); 381 | } catch (Exception $e) { 382 | // do nothing 383 | } 384 | } 385 | } 386 | 387 | public function resumeOpenstack($serviceId) { 388 | // get fleio service id and run resume on fleio service 389 | $urlFleioService = '/billing/services'; 390 | try { 391 | $filteringString = "external_billing_id:" . $serviceId . "+client__external_billing_id:" . (string)$this->clientsdetails->uuid; 392 | $response = $this->flApi->get($urlFleioService, array("filtering" => $filteringString)); 393 | } catch (Exception $e) { 394 | throw new FlApiRequestException('Fleio: unable to find service. ' . $serviceId . ' for resuming it. Check if client and service are tied through external_billing_id.', 404); 395 | } 396 | if ($response === null) { 397 | throw new FlApiRequestException('Fleio: unable to find service. ' . $serviceId . ' for resuming it. Check if client and service are tied through external_billing_id.', 404); 398 | } 399 | $responseObjects = $response["objects"]; 400 | if (sizeof($responseObjects)) { 401 | $fleioServiceId = $responseObjects[0]["id"]; 402 | $url = '/billing/services/' . $fleioServiceId . '/unsuspend'; 403 | $this->flApi->post($url); 404 | } 405 | // if client is suspended, also run resume on client 406 | try { 407 | $fleioClient = $this->getClient(); 408 | } catch (Exception $e) { 409 | $fleioClient = NULL; 410 | } 411 | if ($fleioClient !== NULL) { 412 | if ($fleioClient["status"] === "suspended") { 413 | $url = '/clients/' . $fleioClient["id"] . '/resume'; 414 | try { 415 | $this->flApi->post($url); 416 | } catch (Exception $e) { 417 | // do nothing 418 | } 419 | } 420 | } 421 | 422 | // re-activate user if inactive 423 | try { 424 | $relatedUser = $this->getUser($this->clientsdetails->uuid); 425 | } catch (Exception $e) { 426 | $relatedUser = NULL; 427 | } 428 | if ($relatedUser !== NULL && !$relatedUser["is_active"]) { 429 | try { 430 | $this->updateFleioUser($relatedUser["id"], array("is_active" => true)); 431 | } catch (Exception $e) { 432 | // do nothing 433 | } 434 | } 435 | } 436 | 437 | public function terminateOpenstack($serviceId) { 438 | // get fleio service id 439 | $urlFleioService = '/billing/services'; 440 | try { 441 | $filteringString = "external_billing_id:" . $serviceId . "+client__external_billing_id:" . (string)$this->clientsdetails->uuid; 442 | $response = $this->flApi->get($urlFleioService, array("filtering" => $filteringString)); 443 | } catch (Exception $e) { 444 | throw new FlApiRequestException('Fleio: unable to find service ' . $serviceId . ' for termination. Check if client and service are tied through external_billing_id.', 404); 445 | } 446 | if ($response === null) { 447 | throw new FlApiRequestException('Fleio: unable to find service ' . $serviceId . ' for termination. Check if client and service are tied through external_billing_id.', 404); 448 | } 449 | $responseObjects = $response["objects"]; 450 | if (sizeof($responseObjects)) { 451 | $fleioServiceId = $responseObjects[0]["id"]; 452 | $url = '/billing/services/' . $fleioServiceId . '/terminate'; 453 | $this->flApi->post($url); 454 | } 455 | // If client does not have any other active service, suspend client and de-activate fleio user 456 | try { 457 | $otherServicesResponse = $this->flApi->get($urlFleioService, array( 458 | "filtering" => "client__external_billing_id:".$this->clientsdetails->uuid."+id__ne:".$fleioServiceId 459 | )); 460 | } catch (Exception $e) { 461 | $otherServicesResponse = NULL; 462 | } 463 | $otherActiveServicesCount = 0; 464 | if ($otherServicesResponse) { 465 | $otherServicesResponseObjects = $otherServicesResponse["objects"]; 466 | if (sizeof($otherServicesResponseObjects)) { 467 | for($i = 0 ; $i < sizeof($otherServicesResponseObjects); $i++) { 468 | if ($otherServicesResponseObjects[$i]["status"] !== "terminated" && $otherServicesResponseObjects[$i]["status"] !== "suspended") { 469 | $otherActiveServicesCount = $otherActiveServicesCount + 1; 470 | } 471 | } 472 | } 473 | } 474 | if ($otherActiveServicesCount === 0) { 475 | // suspend client 476 | $fleio_client_id = $this->getClientId(); 477 | $url = '/clients/' . $fleio_client_id . '/suspend'; 478 | try { 479 | $this->flApi->post($url); 480 | } catch (Exception $e) { 481 | // do nothing 482 | } 483 | // de-activate user 484 | try { 485 | $relatedUser = $this->getUser($this->clientsdetails->uuid); 486 | } catch (Exception $e) { 487 | $relatedUser = NULL; 488 | } 489 | if ($relatedUser !== NULL && $relatedUser["is_active"]) { 490 | try { 491 | $this->updateFleioUser($relatedUser["id"], array("is_active" => false)); 492 | } catch (Exception $e) { 493 | // do nothing 494 | } 495 | } 496 | } 497 | } 498 | 499 | public function clientChangeCredit($addCredit, $amount, $currencyCode, $currencyRate, $clientAmount, $clientCurrency, $invoiceId='') { 500 | // originally this would have been: exchange_rate => $currencyrate; source_amount => $clientAmount; source_currency => $clientCurrency 501 | // but Fleio may not have all the currencies from WHMCS. Also, Fleio does not do the actual exchange either; 502 | try { 503 | $fleio_client_id = $this->getClientId(); 504 | $url = '/clients/' . $fleio_client_id . '/change_credit'; 505 | $params = array('amount' => $amount, 506 | 'currency' => $currencyCode, 507 | 'exchange_rate' => 1, 508 | 'source_amount' => $amount, 509 | 'source_currency' => $currencyCode, 510 | 'add_credit' => $addCredit, 511 | 'external_source' => true); 512 | return $this->flApi->post($url, $params); 513 | } catch (Exception $e) { 514 | if ($addCredit) { 515 | logActivity('Fleio unable to add credit in Fleio for Client ID: ' . $this->clientsdetails->userid . ' with ' . (string)$clientAmount); 516 | } else { 517 | logActivity('Fleio unable to withdraw credit from FLeio for Client ID: ' . $this->clientsdetails->userid . ' with ' . (string)$clientAmount); 518 | } 519 | throw $e; 520 | } 521 | } 522 | 523 | } 524 | 525 | 526 | class FlApi { 527 | private $SERVER_URL; 528 | private $SERVER_TOKEN; 529 | private $HEADERS=array('Content-Type: application/json'); 530 | private $TEMP_HEADERS=array(); 531 | 532 | public function __construct($server_url, $token) { 533 | if (!is_string($server_url) or empty($server_url) or (!is_string($token))) { 534 | exit('server url or accesshash not set'); 535 | } 536 | $this->SERVER_URL = $server_url; 537 | $this->SERVER_TOKEN = $token; 538 | $this->HEADERS[] = 'Authorization: Token ' . $token; 539 | } 540 | 541 | public function post( $url, $params = NULL) { 542 | $ch = curl_init(); 543 | if (is_array($params)) { 544 | $json_params = json_encode($params); 545 | curl_setopt($ch, CURLOPT_POSTFIELDS, $json_params); 546 | $this->TEMP_HEADERS[] = 'Content-Length: ' . mb_strlen($json_params); 547 | } 548 | $response = $this->request($ch, 'POST', $url); 549 | curl_close($ch); 550 | return $response; 551 | } 552 | 553 | public function patch( $url, $params = NULL ) { 554 | $ch = curl_init(); 555 | if (is_array($params)) { 556 | $json_params = json_encode($params); 557 | curl_setopt($ch, CURLOPT_POSTFIELDS, $json_params); 558 | $this->TEMP_HEADERS[] = 'Content-Length: ' . mb_strlen($json_params); 559 | } 560 | $response = $this->request($ch, 'PATCH', $url); 561 | curl_close($ch); 562 | return $response; 563 | } 564 | 565 | public function get( $url, $params = NULL ) { 566 | $ch = curl_init(); 567 | $this->TEMP_HEADERS = array(); 568 | if (is_array($params)) { 569 | $getfields = http_build_query($params); 570 | str_replace('.', '%2E', $getfields); 571 | str_replace('-', '%2D', $getfields); 572 | $url .= '?'.$getfields; 573 | } 574 | $response = $this->request($ch, 'GET', $url); 575 | curl_close($ch); 576 | return $response; 577 | } 578 | 579 | public function delete( $url ) { 580 | $ch = curl_init(); 581 | $response = $this->request($ch, 'DELETE', $url); 582 | curl_close($ch); 583 | return $response; 584 | } 585 | 586 | private function drf_get_details($inarray) { 587 | $response = ''; 588 | if (is_array($inarray)) { 589 | foreach ($inarray as $key => $value) { 590 | if (is_array($value)) { 591 | $response .= $key . ': ' . $this->drf_get_details($value); 592 | } else { 593 | $response .= ' ' . $value; 594 | } 595 | } 596 | } else { $response .= ' ' . $inarray; } 597 | return $response; 598 | } 599 | 600 | private function parse_drf_error($drf_error, $httpcode) { 601 | // If the http status is bigger than 399, it signals an error, throw it 602 | $err_msg = 'Bad request with status ' . $httpcode; 603 | if ($httpcode > 499) { // throw for 500 and above 604 | throw new FlApiRequestException('An internal Fleio API error occurred', $httpcode); 605 | } 606 | if (is_array($drf_error)) { 607 | if (array_key_exists('detail', $drf_error)) { 608 | $err_msg = (string)$drf_error['detail']; 609 | } else { 610 | try { 611 | $err_msg = $this->drf_get_details($drf_error); 612 | } catch (Exception $e) { 613 | $err_msg = 'bad request: ' . $httpdoce . '; Unable to parse error message.'; 614 | } 615 | } 616 | } 617 | throw new FlApiRequestException($err_msg, $httpcode); 618 | } 619 | 620 | private function request( $ch, $method, $url ) { 621 | curl_setopt($ch, CURLOPT_URL, $this->SERVER_URL . $url); 622 | curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); 623 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 624 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); 625 | curl_setopt($ch, CURLOPT_MAXREDIRS, 5); 626 | curl_setopt($ch, CURLOPT_AUTOREFERER, true); 627 | curl_setopt($ch, CURLOPT_POSTREDIR, 3); 628 | curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); // Set the connection timeout to 10 seconds. 629 | if ($method == 'GET') { 630 | curl_setopt($ch, CURLOPT_HTTPGET, 1); 631 | } 632 | $headers = array(); 633 | foreach ($this->HEADERS as $h) { 634 | array_push($headers, $h); 635 | } 636 | foreach ($this->TEMP_HEADERS as $th) { 637 | array_push($headers, $th); 638 | } 639 | curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); 640 | $result = curl_exec($ch); 641 | $httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE); 642 | $decoded_result = json_decode($result, true); // We should always receive a JSON response. Decode and check for errors 643 | if (json_last_error() != JSON_ERROR_NONE) { 644 | $decoded_result = 'Invalid response from Fleio API with http code: '. $httpcode; 645 | } 646 | if ($result === false) { // If no result, a curl error may have occured 647 | throw new FlApiCurlException(curl_error($ch), curl_errno($ch)); 648 | } 649 | if ($httpcode > 399) { 650 | return $this->parse_drf_error($decoded_result, $httpcode); 651 | } 652 | return $decoded_result; 653 | } 654 | } 655 | -------------------------------------------------------------------------------- /fleio.php: -------------------------------------------------------------------------------- 1 | 'Fleio', 15 | 'APIVersion' => '1.1', 16 | 'RequiresServer' => false, // Set true if module requires a server to work 17 | 'DefaultNonSSLPort' => '80', 18 | 'DefaultSSLPort' => '443', 19 | 'ServiceSingleSignOnLabel' => 'Login to Fleio', 20 | 'AdminSingleSignOnLabel' => 'Login to Fleio', 21 | ); 22 | } 23 | 24 | function fleio_ConfigOptions() { 25 | global $_LANG; 26 | $default_currency_code = getCurrency()['code']; 27 | $configarray = array( 28 | "admintoken" => array ( 29 | "FriendlyName" => "Fleio staff Token", 30 | "Type" => "password", # Password Field 31 | "Size" => "64", # Defines the Field Width 32 | "Default" => "", 33 | ), 34 | "frontendurl" => array ( 35 | "FriendlyName" => "Frontend URL", 36 | "Type" => "text", # Text Box 37 | "Size" => "64", # Defines the Field Width 38 | "Description" => "", 39 | "Default" => "https://", 40 | ), 41 | "frontendadminurl" => array ( 42 | "FriendlyName" => "Frontend staff URL", 43 | "Type" => "text", # Text Box 44 | "Size" => "64", # Defines the Field Width 45 | "Description" => "", 46 | "Default" => "https://", 47 | ), 48 | "backendadminurl" => array ( 49 | "FriendlyName" => "Backend admin URL", 50 | "Type" => "text", # Text Box 51 | "Size" => "64", # Defines the Field Width 52 | "Description" => "", 53 | "Default" => "https://", 54 | ), 55 | "minamount" => array ( 56 | "FriendlyName" => "Minimum payment amount", 57 | "Type" => "text", # Text Box 58 | "Size" => "10", # Defines the Field Width 59 | "Description" => "". $default_currency_code, 60 | "Default" => "10", 61 | ), 62 | "maxamount" => array ( 63 | "FriendlyName" => "Maximum payment amount", 64 | "Type" => "text", # Text Box 65 | "Size" => "10", # Defines the Field Width 66 | "Description" => "". $default_currency_code, 67 | "Default" => "1000", 68 | ), 69 | "clientgroup" => array ( 70 | "FriendlyName" => "Place clients inside a Fleio client group", 71 | "Type" => "text", # Text Box 72 | "Size" => "64", # Defines the Field Width 73 | "Description" => "", 74 | "Default" => "", 75 | ), 76 | "configuration" => array ( 77 | "FriendlyName" => "Fleio Configuration Name", 78 | "Type" => "text", 79 | "Size" => "32", 80 | "Description" => "Fleio configuration for new clients", 81 | "Default" => "" 82 | ), 83 | "userprefix" => array ( 84 | "FriendlyName" => "Fleio username prefix", 85 | "Type" => "text", 86 | "Size" => "12", 87 | "Description" => "Leave blank for 'whmcs'. Field is not used anymore from Fleio version 2022.09.0.", 88 | "Default" => "whmcs", 89 | ), 90 | "issueinvoice" => array ( 91 | "FriendlyName" => "Invoice clients without billing agreement", 92 | "Type" => "yesno", 93 | "Description" => "Issue invoice at the end of billing cycle for clients without billing agreement", 94 | ), 95 | "issueInvWAgr" => array ( 96 | "FriendlyName" => "Invoice clients with billing agreement", 97 | "Type" => "yesno", 98 | "Description" => "Issue invoice at the end of billing cycle for clients with billing agreement", 99 | ), 100 | "chargeInvoiceRightAway" => array ( 101 | "FriendlyName" => "Attempt a charge immediately", 102 | "Type" => "yesno", 103 | "Description" => "Attempt charging of auto generated invoices when CC is on file immediately after the invoice was issued", 104 | ), 105 | "gatewaysNamesForBillingAg" => array ( 106 | "FriendlyName" => "Names of gateways to use for billing agreement", 107 | "Type" => "text", 108 | "Size" => "32", 109 | "Description" => "Comma separated string, each entry representing a name for a gateway that uses billing agreements. Leave empty to take into account all gateways", 110 | "Default" => "" 111 | ), 112 | "doNotInvoiceAmountBelow" => array ( 113 | "FriendlyName" => "Do not invoice amount below", 114 | "Type" => "text", 115 | "Size" => "10", 116 | "Default" => "0", 117 | "Description" => "".$default_currency_code 118 | ), 119 | "retryChargesEveryXHours" => array ( 120 | "FriendlyName" => "Retry auto charging invoice after defined time (hours)", 121 | "Type" => "text", 122 | "Size" => "5", 123 | "Description" => "If an invoice is not paid automatically, retry every hours you define here. Leave 0 or blank to disable this option.", 124 | "Default" => "0", 125 | ), 126 | "removeAgreementStatusAfterXFailedCharges" => array ( 127 | "FriendlyName" => "Remove on agreement status after defined number of failed charges (currently working with either 1 or 2).", 128 | "Type" => "text", 129 | "Size" => "1", 130 | "Description" => "If an invoice belonging to a client on agreement is not successfully paid after a number of retries you define, he will not be considered on agreement anymore. The number of retries that will work is limited to 2. Choose either 1 or 2. Also, setting this to 2 does not make sense if you do not use the retry auto-charge setting.", 131 | "Default" => "0", 132 | ), 133 | "endUserWidgetNonAgreementMessage" => array ( 134 | "FriendlyName" => "Add a text to show to the enduser on the fleio dashboard widget if he is not on a billing agreement", 135 | "Type" => "text", 136 | "Size" => "10240", 137 | "Description" => "Add a text to show to the enduser on the fleio dashboard widget if he is not on a billing agreement", 138 | "Default" => "You can create a billing agreement when paying an invoice.", 139 | ), 140 | "fleioWidgetExtraHtml" => array ( 141 | "FriendlyName" => "Fleio widget extra html", 142 | "Type" => "text", 143 | "Size" => "10240", 144 | "Description" => "Add html input on the fleio dashboard widget after the existing content", 145 | "Default" => "", 146 | ), 147 | "invoiceOnlyInEndOfCycleTimespan" => array ( 148 | "FriendlyName" => "Invoice only in end of cycle timespan", 149 | "Type" => "yesno", 150 | "Description" => "Will not invoice for end of cycle if more than 72h passed after cycle end date", 151 | ), 152 | "activateVerifiedClientServices" => array ( 153 | "FriendlyName" => "Activate services for clients with verified email", 154 | "Type" => "yesno", 155 | "Description" => "Each time cron runs, will activate Fleio services for clients with verified email", 156 | ), 157 | "invoiceSuspendedClients" => array ( 158 | "FriendlyName" => "Generate invoices for suspended services", 159 | "Type" => "yesno", 160 | "Description" => "Also process suspended clients and generate invoices for them. Works only starting with Fleio 2024.04", 161 | ), 162 | ); 163 | return $configarray; 164 | } 165 | 166 | function fleio_CreateAccount($params){ 167 | $fl = Fleio::fromParams($params); 168 | try { 169 | $fl->createBillingClient($params['configoption7'], $params['serviceid']); 170 | } catch (FlApiException $e) { 171 | return $e->getMessage(); 172 | } 173 | return "success"; 174 | } 175 | 176 | function fleio_SuspendAccount($params) { 177 | $fl = Fleio::fromParams($params); 178 | try { 179 | $result = $fl->suspendOpenstack($params['serviceid']); 180 | } catch (FlApiException $e) { 181 | return $e->getMessage(); 182 | } 183 | return "success"; 184 | } 185 | 186 | function fleio_UnsuspendAccount($params) { 187 | $fl = Fleio::fromParams($params); 188 | try { 189 | $result = $fl->resumeOpenstack($params['serviceid']); 190 | } catch (FlApiException $e) { 191 | return $e->getMessage(); 192 | } 193 | return "success"; 194 | } 195 | 196 | function fleio_TerminateAccount($params) { 197 | $fl = Fleio::fromParams($params); 198 | try { 199 | $result = $fl->terminateOpenstack($params['serviceid']); 200 | } catch (FlApiException $e) { 201 | return ($e->getMessage()); 202 | } 203 | return "success"; 204 | } 205 | 206 | function fleio_login($params) { 207 | $fl = Fleio::fromParams($params); 208 | try { 209 | $url = $fl->getSSOUrl(); 210 | header("Location: " . $url); 211 | exit; 212 | } catch (FlApiException $e) { 213 | logActivity('Fleio SSO login error: ' . $e->getMessage()); 214 | return "Unable to retrieve a SSO session"; 215 | } 216 | } 217 | 218 | function fleio_ClientAreaCustomButtonArray() { 219 | $buttonarray = array( 220 | "Login to Fleio" => "login", 221 | ); 222 | return $buttonarray; 223 | } 224 | 225 | /** 226 | * Client area output logic handling. 227 | * 228 | * This function is used to define module specific client area output. It should 229 | * return an array consisting of a template file and optional additional 230 | * template variables to make available to that template. 231 | * 232 | * The template file you return can be one of two types: 233 | * 234 | * * tabOverviewModuleOutputTemplate - The output of the template provided here 235 | * will be displayed as part of the default product/service client area 236 | * product overview page. 237 | * 238 | * * tabOverviewReplacementTemplate - Alternatively using this option allows you 239 | * to entirely take control of the product/service overview page within the 240 | * client area. 241 | * 242 | * Whichever option you choose, extra template variables are defined in the same 243 | * way. This demonstrates the use of the full replacement. 244 | * 245 | * Please Note: Using tabOverviewReplacementTemplate means you should display 246 | * the standard information such as pricing and billing details in your custom 247 | * template or they will not be visible to the end user. 248 | * 249 | * @param array $params common module parameters 250 | * 251 | * @see http://docs.whmcs.com/Provisioning_Module_SDK_Parameters 252 | * 253 | * @return array 254 | */ 255 | function fleio_ClientArea(array $params) 256 | { 257 | // Min/Max in base currency 258 | $min_amount = 10; 259 | $max_amount = 1000; 260 | // Min/Max in client's currency 261 | $minamount = convertCurrency($min_amount, 1, $params['clientsdetails']['currency']); 262 | $maxamount = convertCurrency($max_amount, 1, $params['clientsdetails']['currency']); 263 | 264 | $requestedAction = isset($_REQUEST['customAction']) ? $_REQUEST['customAction'] : ''; 265 | if ($requestedAction == 'manage') { 266 | $serviceAction = 'get_usage'; 267 | $templateFile = 'templates/manage.tpl'; 268 | $exv2 = 'manage'; 269 | } 270 | if ($requestedAction == 'stats') { 271 | $serviceAction = 'get_stats'; 272 | $templateFile = 'templates/overview.tpl'; 273 | $exv2 = 'stats'; 274 | } 275 | if ($requestedAction == 'createflinvoice') { 276 | $serviceAction = 'actionCreateInvoice'; 277 | $templateFile = 'templates/overview.tpl'; 278 | } else { 279 | $serviceAction = 'actionOverview'; 280 | $templateFile = 'templates/overview.tpl'; 281 | } 282 | try { 283 | // Call the service's function based on the request action, using the 284 | // values provided by WHMCS in `$params`. 285 | return array( 286 | 'tabOverviewReplacementTemplate' => $templateFile, 287 | 'templateVariables' => $serviceAction($params, $_REQUEST), 288 | ); 289 | 290 | } catch (Exception $e) { 291 | // Record the error in WHMCS's module log. 292 | logModuleCall( 293 | 'fleio', 294 | __FUNCTION__, 295 | $params, 296 | $e->getMessage(), 297 | $e->getTraceAsString() 298 | ); 299 | // In an error condition, display an error page. 300 | return array( 301 | 'tabOverviewReplacementTemplate' => 'templates/error.tpl', 302 | 'templateVariables' => array( 303 | 'usefulErrorHelper' => $e->getMessage(), 304 | ), 305 | ); 306 | } 307 | } 308 | 309 | 310 | function actionOverview($params, $request) { 311 | $min_amount = $params['configoption5']; 312 | $max_amount = $params['configoption6']; 313 | // Min/Max in client's currency 314 | $whmcsClientCurrencyId = $params['clientsdetails']['currency']; 315 | $minamount = convertCurrency($min_amount, 1, $whmcsClientCurrencyId); 316 | $maxamount = convertCurrency($max_amount, 1, $whmcsClientCurrencyId); 317 | $fl = Fleio::fromParams($params); 318 | try { 319 | $client = $fl->getClient(); 320 | } catch (Exception $e) { 321 | logModuleCall( 322 | 'fleio', 323 | __FUNCTION__, 324 | $params, 325 | $e->getMessage(), 326 | $e->getTraceAsString() 327 | ); 328 | $client = False; 329 | }; 330 | // Convert from Fleio client currency to the WHMCS client currency 331 | $tax1 = getTaxRate(1, $params['clientsdetails']['state'], $params['clientsdetails']['countrycode']); 332 | $tax2 = getTaxRate(2, $params['clientsdetails']['state'], $params['clientsdetails']['countrycode']); 333 | $taxexempt = $params['clientsdetails']['taxexempt']; 334 | if ($taxexempt) { 335 | $tax1_rate = 0; 336 | $tax2_rate = 0; 337 | } else { 338 | $tax1_rate = $tax1['rate']; 339 | $tax2_rate = $tax2['rate']; 340 | } 341 | 342 | $uptodateCredit = NULL; 343 | $outofcreditDatetime = NULL; 344 | $whmcsClientCurrency = getCurrency($params['clientsdetails']['userid']); 345 | if (is_array($client) && array_key_exists('uptodate_credit', $client) && array_key_exists('outofcredit_datetime', $client)) { 346 | $uptodateCredit = $client['uptodate_credit']; 347 | $outofcreditDatetime = $client['outofcredit_datetime']; 348 | $whmcsCurrency = Capsule::table('tblcurrencies') 349 | ->select('id', 'code') 350 | ->where('code', '=', $client['currency']) 351 | ->first(); 352 | try { 353 | $uptodateCreditFormatted = formatCurrency($uptodateCredit, $whmcsCurrency->id); 354 | } catch (Exception $e) { 355 | $uptodateCreditFormatted = '' . $uptodateCredit; 356 | } 357 | } 358 | return array('minamount' => $minamount, 359 | 'maxamount' => $maxamount, 360 | 'tax1_rate' => $tax1_rate, 361 | 'tax2_rate' => $tax2_rate, 362 | 'currency' => $whmcsClientCurrency, 363 | 'uptodateCredit' => $uptodateCreditFormatted, 364 | 'outofcreditDatetime' => $outofcreditDatetime); 365 | } 366 | 367 | 368 | function validateAmount($original_amount, $min, $max) { 369 | // Validate amount 370 | // Using , instead of . (dot) ? 371 | $amount = str_replace(",", ".", $original_amount); 372 | if (!is_numeric($amount)) { 373 | throw new Exception("Please enter a valid amount."); 374 | } 375 | if ($amount < $min) { 376 | $def_msg = isset($_LANG['addfundsminimumerror']) ? $_LANG['addfundsminimumerror'] : 'Amount must be equal or greated than'; 377 | throw new Exception($def_msg." ".formatCurrency($min)); 378 | } 379 | if ($amount > $max) { 380 | $def_msg = isset($_LANG['addfundsmaximumerror']) ? $_LANG['addfundsmaximumerror'] : 'Amount must be smaller than'; 381 | throw new Exception($def_msg." ".formatCurrency($max)); 382 | } 383 | return $amount; 384 | } 385 | 386 | function canAddCredit($serviceStatus) { 387 | if ($serviceStatus === 'Cancelled' || $serviceStatus === 'Terminated') { 388 | throw new Exception("You cannot add credit while your service status is " . $serviceStatus . "."); 389 | } 390 | } 391 | 392 | 393 | function actionCreateInvoice($params, $request) { 394 | # Action used for the Add Credit functionality 395 | $min_amount = $params['configoption5']; 396 | $max_amount = $params['configoption6']; 397 | // Min/Max in client's currency 398 | $minamount = convertCurrency($min_amount, 1, $params['clientsdetails']['currency']); 399 | $maxamount = convertCurrency($max_amount, 1, $params['clientsdetails']['currency']); 400 | 401 | $original_amount = $request["amount"]; 402 | try { 403 | $amount = validateAmount($original_amount, $minamount, $maxamount); 404 | } catch (Exception $e) { 405 | $overview_vars = actionOverview($params, $request); 406 | return array_merge($overview_vars, array('validateAmountError' => $e->getMessage(),)); 407 | } 408 | 409 | $service = FleioUtils::getServiceById($params['serviceid']); 410 | if ($service) { 411 | try { 412 | canAddCredit($service->domainstatus); 413 | } catch (Exception $e) { 414 | $overview_vars = actionOverview($params, $request); 415 | return array_merge($overview_vars, array('validateServiceError' => $e->getMessage(),)); 416 | } 417 | } 418 | $clientsdetails = $params['clientsdetails']; 419 | $values["userid"] = $clientsdetails['userid']; 420 | $values["sendinvoice"] = true; 421 | $values["itemdescription1"] = $service->name; 422 | $values["itemamount1"] = $amount; 423 | $values["itemtaxed1"] = true; 424 | 425 | if ($service && !is_null($service->paymentmethod)) { 426 | $values['paymentmethod'] = $service->paymentmethod; 427 | } 428 | 429 | try { 430 | $invoice_id = FleioUtils::createFleioInvoice($params['serviceid'], $values); 431 | } catch (Exception $e) { 432 | logActivity('Fleio: unable to create invoice for service ' . $params['serviceid'] . ': ' . $e->getMessage()); 433 | throw new Exception('Unable to create invoice'); 434 | } 435 | $log_msg = "Fleio: Client ID: ".$clientsdetails['userid']." created credit Invoice ID: ".$invoice_id." with amount ".formatCurrency($amount); 436 | logActivity($log_msg); 437 | 438 | redir("id=".(int) $invoice_id,"viewinvoice.php"); 439 | } 440 | 441 | -------------------------------------------------------------------------------- /fleioaddcredit.php: -------------------------------------------------------------------------------- 1 | setPageTitle('Fleio add credit'); 15 | $ca->initPage(); 16 | 17 | $ca->requireLogin(); 18 | 19 | // Check login status 20 | if ($ca->isLoggedIn()) { 21 | try { 22 | $prodId = Capsule::table('tblhosting') 23 | ->join('tblproducts', 'tblhosting.packageid', '=', 'tblproducts.id') 24 | ->where('tblhosting.userid', '=', $ca->getUserID()) 25 | ->whereIn('tblhosting.domainstatus', ['Active', 'Suspended']) 26 | ->where('tblproducts.servertype', '=', 'fleio') 27 | ->select('tblhosting.id') 28 | ->first(); 29 | } catch (Exception $e) { 30 | header('Location: clientarea.php' ); 31 | exit; 32 | } 33 | header('Location: ' . 'clientarea.php?action=productdetails&id=' . $prodId->id); 34 | exit; 35 | 36 | } else { 37 | header('Location: ' . 'clientarea.php'); 38 | exit; 39 | } 40 | -------------------------------------------------------------------------------- /hooks.php: -------------------------------------------------------------------------------- 1 | configoption11 == 'on' ? true : false; // invoice clients with billing agreement 29 | $invoiceWithoutAgreement = $server->configoption10 == 'on' ? true : false; // invoice clients without billing agreement 30 | $capturePaymentImmediately = $server->configoption12 == 'on' ? true : false; // Attempt to capture payment immediately 31 | $usingInvoicingFeature = $invoiceWithAgreement || $invoiceWithoutAgreement; 32 | $flApi = new FlApi(FleioUtils::trimApiUrlTrailingSlash($server->configoption4), $server->configoption1); 33 | 34 | $activateVerifiedClientServices = $server->configoption20 == 'on' ? true : false; 35 | if ($activateVerifiedClientServices) { 36 | FleioUtils::activateEmailVerifiedClientServices(); 37 | } 38 | 39 | if ($usingInvoicingFeature) { 40 | FleioUtils::updateClientsBillingAgreement( 41 | $flApi, 42 | 'Active', 43 | $server->configoption13, 44 | $server->configoption15, 45 | $server->configoption16, 46 | $capturePaymentImmediately, 47 | $server 48 | ); 49 | 50 | $url = "/clients/get_clients_to_invoice"; 51 | $urlParams = array( 52 | "has_external_billing" => 'True', // only clients with external billing set and credit less than 0 53 | "uptodate_credit_max" => 0 54 | ); 55 | $invoiceSuspendedClients = $server->configoption21 == 'on' ? true : false; 56 | if ($invoiceSuspendedClients) { 57 | $urlParams["process_suspended"] = 'true'; 58 | } 59 | 60 | if ($invoiceWithAgreement && $invoiceWithoutAgreement) { 61 | logActivity('Fleio: Looking at clients with and without a billing agreement'); 62 | } 63 | if ($invoiceWithAgreement && !$invoiceWithoutAgreement) { 64 | // Filter only Clients with billing agreement 65 | $urlParams['has_billing_agreement'] = 'True'; 66 | logActivity('Fleio: Looking at clients with billing agreements only'); 67 | } 68 | if (!$invoiceWithAgreement && $invoiceWithoutAgreement) { 69 | // Filter only Clients without billing agreement 70 | $urlParams['has_billing_agreement'] = 'False'; 71 | logActivity('Fleio: Looking at clients without a billing agreement only'); 72 | } 73 | 74 | logActivity('Fleio: retrieving all overdue clients'); 75 | try { 76 | $invoiceOnlyInEndOfCycleTimespan = $server->configoption19 == 'on' ? true : false; // invoice clients only 77 | // if latest unpaid cycle end dt is in the past 72h 78 | logActivity('Fleio: using server ' . $server->configoption4); 79 | $clientsOverLimit = $flApi->get($url, $urlParams); 80 | $numInvoicedClients = 0; 81 | foreach ($clientsOverLimit as $clientOl) { 82 | if (array_key_exists('has_service_cycle_recently_ended', $clientOl) && array_key_exists('reached_credit_limit', $clientOl)) { 83 | if ($invoiceOnlyInEndOfCycleTimespan && 84 | (!$clientOl['has_service_cycle_recently_ended'] && !$clientOl['reached_credit_limit'])) { 85 | // skip clients did not reach credit limit or don't have a recently ended service cycle (in last 72h) 86 | continue; 87 | } 88 | } 89 | $invoiceProcessingUrl = sprintf('/clients/%s/get_client_for_invoice_processing', $clientOl['id']); 90 | try { 91 | $clientToProcess = $flApi->get($invoiceProcessingUrl, array()); 92 | } catch ( Exception $e ) { 93 | $clientToProcess = NULL; 94 | logActivity( 95 | 'Fleio: error when trying to get fleio client (' . $clientOl['id'] . 96 | ') in order to process and invoice him: ' . $e->getMessage() 97 | ); 98 | } 99 | if ($clientToProcess) { 100 | try { 101 | $clientFromUUID = FleioUtils::getUUIDClient($clientToProcess['external_billing_id']); 102 | if ($clientFromUUID != NULL) { 103 | try { 104 | $generatedInvoiceId = FleioUtils::invoiceClient( 105 | $clientFromUUID, 106 | $clientToProcess, 107 | $server->configoption14, 108 | FleioUtils::getFleioProductsInvoicedAmount($clientFromUUID->id, $server->id) 109 | ); 110 | } catch ( Exception $e ) { 111 | logActivity( 112 | 'Fleio: error when trying to invoice client ' . 113 | $clientToProcess['external_billing_id'] . ': ' . $e->getMessage() 114 | ); 115 | $generatedInvoiceId = NULL; 116 | } 117 | if ($generatedInvoiceId) { 118 | $numInvoicedClients += 1; 119 | // TODO: take billing agreement status of client from fleio response from fleio 2020.03 120 | $clientHasBillingAgreementResponse = FleioUtils::clientHasBillingAgreement( 121 | $clientFromUUID, 122 | $server->configoption13 123 | ); 124 | $clientHasBillingAgreement = $clientHasBillingAgreementResponse['hasAgreement']; 125 | if ($capturePaymentImmediately && $clientHasBillingAgreement) { 126 | $captured = FleioUtils::captureInvoicePayment($generatedInvoiceId); 127 | if ($captured === false && $server->configoption16 === '1') { 128 | // capture failed and setting says the client is no more on agreement 129 | FleioUtils::removeClientBillingAgreement( 130 | $flApi, $clientToProcess['external_billing_id'] 131 | ); 132 | } 133 | } 134 | } else { 135 | // re-set the status of invoiced periods 136 | FleioUtils::resetInvoicedPeriodsStatus($flApi, $clientToProcess); 137 | } 138 | } else { 139 | logActivity( 140 | 'Fleio: unable to retrieve WHMCS client with UUID: ' . 141 | $clientToProcess['external_billing_id'] 142 | ); 143 | // re-set the status of invoiced periods 144 | FleioUtils::resetInvoicedPeriodsStatus($flApi, $clientToProcess); 145 | continue; 146 | } 147 | } catch ( Exception $e ) { 148 | logActivity($e->getMessage()); 149 | // re-set the status of invoiced periods 150 | FleioUtils::resetInvoicedPeriodsStatus($flApi, $clientToProcess); 151 | continue; 152 | } 153 | } 154 | } 155 | if ($numInvoicedClients > 0) { 156 | logActivity('Fleio: invoiced ' . $numInvoicedClients . ' overdue clients' ); 157 | } else { 158 | logActivity('Fleio: no overdue clients to invoice found on ' . $server->configoption4); 159 | } 160 | } catch ( Exception $e ) { 161 | logActivity( 162 | 'Fleio: unable to retrieve over credit clients from '. $server->configoption4 . ' (' . 163 | $e->getMessage() . ')' 164 | ); 165 | continue; 166 | } 167 | } 168 | 169 | FleioUtils::markWhmcsSuspendedServices($server->configoption4, $flApi); 170 | 171 | FleioUtils::markWhmcsActiveServices($server->configoption4, $flApi); 172 | 173 | FleioUtils::markWhmcsTerminatedServices($server->configoption4, $flApi); 174 | 175 | if ($usingInvoicingFeature) { 176 | $urlGetAutoInvoiceClients = '/billing/external-billing/get_clients_to_auto_invoice_for_external_billing'; 177 | try { 178 | $clientsToAutoInvoiceResponse = $flApi->get($urlGetAutoInvoiceClients); 179 | } catch ( Exception $e ) { 180 | $clientsToAutoInvoiceResponse = array("objects" => []); 181 | logActivity('Fleio: unable to retrieve clients for auto-invoicing: ' . $e->getMessage()); 182 | } 183 | $retrievedClients = $clientsToAutoInvoiceResponse['objects']; 184 | 185 | logActivity('Fleio: checking ' . count($retrievedClients) . ' client(s) with auto-invoicing enabled'); 186 | 187 | $urlProcessAutoInvoicing = "/billing/external-billing/process_clients_for_credit_auto_invoicing"; 188 | $counter = 0; 189 | $creditAutoInvoicedCount = 0; 190 | foreach($retrievedClients AS $retrievedClientUUID) { 191 | $counter = $counter + 1; 192 | if ($creditAutoInvoicedCount === 0) { 193 | // re-set array as we process 20 clients at a time 194 | $whmcsClientsToAutoInvoice = []; 195 | $urlParams = array(); 196 | } 197 | 198 | // compose data that has to be sent to Fleio (already invoiced but unpaid amount) 199 | $clientFromUUID = FleioUtils::getUUIDClient($retrievedClientUUID); 200 | if ($clientFromUUID !== NULL) { 201 | try { 202 | $invoicedAmountData = FleioUtils::getFleioProductsInvoicedAmount($clientFromUUID->id, $server->id); 203 | $clientToAutoInvoice = array( 204 | "external_billing_id" => $retrievedClientUUID, 205 | "credit_still_to_be_paid" => $invoicedAmountData["amount"], 206 | "credit_still_to_be_paid_currency_code" => $invoicedAmountData["currency"]["code"] 207 | ); 208 | array_push($whmcsClientsToAutoInvoice, $clientToAutoInvoice); 209 | $creditAutoInvoicedCount = $creditAutoInvoicedCount + 1; 210 | } catch ( Exception $e ) { 211 | logActivity( 212 | 'Fleio: unable to get client ' . $clientFromUUID->id . 213 | ' invoiced amount for retrieving auto-invoicing data: ' . $e->getMessage() 214 | ); 215 | if ($counter < count($retrievedClients)) { 216 | continue; 217 | } 218 | } 219 | } 220 | 221 | if ($creditAutoInvoicedCount === 20 || $counter >= count($retrievedClients)) { 222 | // if we reached 20 clients or the end of list, send them to fleio 223 | $creditAutoInvoicedCount = 0; 224 | // process clients we found 225 | $urlParams = array( 226 | "clients" => $whmcsClientsToAutoInvoice 227 | ); 228 | try { 229 | $clientsToAutoInvoice = $flApi->post($urlProcessAutoInvoicing, $urlParams); 230 | } catch ( Exception $e ) { 231 | logActivity( 232 | 'Fleio: error processing ' . $creditAutoInvoicedCount . 233 | ' client(s) for auto-invoicing: ' . $e->getMessage() 234 | ); 235 | continue; 236 | } 237 | foreach ($clientsToAutoInvoice["objects"] as $clientToAutoInvoice) { 238 | try { 239 | $clientFromUUID = FleioUtils::getUUIDClient($clientToAutoInvoice['external_billing_id']); 240 | if ($clientFromUUID != NULL) { 241 | try { 242 | $generatedInvoiceId = FleioUtils::invoiceClientByAmount( 243 | $clientFromUUID, 244 | $clientToAutoInvoice["necessary_credit"], 245 | $clientToAutoInvoice["necessary_credit_currency"], 246 | $server->configoption14, 247 | FleioUtils::getFleioProductsInvoicedAmount($clientFromUUID->id, $server->id) 248 | ); 249 | } catch ( Exception $e ) { 250 | logActivity( 251 | 'Fleio: error when trying to invoice client (for auto-invoicing feature) ' . 252 | $clientToAutoInvoice['external_billing_id'] . ': ' . $e->getMessage() 253 | ); 254 | $generatedInvoiceId = NULL; 255 | } 256 | if ($generatedInvoiceId) { 257 | logActivity( 258 | 'Fleio: created invoice ' . $generatedInvoiceId . ' for client ' . 259 | $clientToAutoInvoice['external_billing_id'] . ' (because of auto-invoicing feature)' 260 | ); 261 | $clientHasBillingAgreementResponse = FleioUtils::clientHasBillingAgreement( 262 | $clientFromUUID, 263 | $server->configoption13 264 | ); 265 | $clientHasBillingAgreement = $clientHasBillingAgreementResponse['hasAgreement']; 266 | if ($capturePaymentImmediately && $clientHasBillingAgreement) { 267 | $captured = FleioUtils::captureInvoicePayment($generatedInvoiceId); 268 | if ($captured === false && $server->configoption16 === '1') { 269 | // capture failed and setting says the client is no more on agreement 270 | FleioUtils::removeClientBillingAgreement( 271 | $flApi, $clientToAutoInvoice['external_billing_id'] 272 | ); 273 | } 274 | } 275 | } 276 | } else { 277 | logActivity( 278 | 'Fleio: unable to retrieve WHMCS client with UUID: ' . 279 | $clientToAutoInvoice['external_billing_id'] . 'while processing auto-invoicing' 280 | ); 281 | } 282 | } catch ( Exception $e ) { 283 | logActivity('Fleio: error while processing clients for auto-invoicing: ' . $e->getMessage()); 284 | continue; 285 | } 286 | } 287 | } 288 | } 289 | } 290 | 291 | } 292 | } 293 | 294 | function fleio_update_invoice_hook($vars) { 295 | if ($vars['source'] != 'autogen') { 296 | # created manually in admin or client area or through localAPI (source = 'api'), skip ? 297 | return; 298 | } 299 | $invoice = Capsule::table('tblinvoices')->where('id', '=', $vars["invoiceid"])->first(); 300 | # NOTE(tomo): Select only Hosting type items. Otherwise we will end up with domains and other types being paid for. 301 | $items = Capsule::table('tblinvoiceitems')->where('invoiceid', '=', $vars["invoiceid"])->get(); 302 | $tax = 0.0; 303 | $tax2 = 0.0; 304 | $subtotal_price = 0.0; 305 | $cost_by_service = array(); 306 | $product_prices = array(); 307 | foreach($items as $item) { 308 | # NOTE(tomo): Check if relid is set and not an empty string 309 | if (($item->relid == '') || !(isset($item->relid))) { 310 | continue; 311 | } 312 | if (isset($cost_by_service[$item->relid])) { 313 | $cost_by_service[$item->relid] += $item->amount; 314 | } else { 315 | $cost_by_service[$item->relid] = $item->amount; 316 | } 317 | } 318 | 319 | foreach($items as $item) { 320 | if (($item->type != 'Hosting') || !isset($cost_by_service[$item->relid])) { 321 | continue; 322 | } 323 | $product = Capsule::table('tblinvoiceitems') 324 | ->join('tblhosting', 'tblinvoiceitems.relid', '=', 'tblhosting.id') 325 | ->join('tblproducts', 'tblhosting.packageid', '=', 'tblproducts.id') 326 | ->where([['tblinvoiceitems.relid', '=', $item->relid], ['tblproducts.servertype', '=', 'fleio'], ['tblhosting.domainstatus', '<>', 'Pending'], ['tblinvoiceitems.type', '=', 'Hosting']]) 327 | ->select('tblproducts.servertype', 'tblhosting.domainstatus')->first(); 328 | if ($product === null) { 329 | continue; 330 | } 331 | try { 332 | $fl = Fleio::fromServiceId($item->relid); 333 | } catch (Exception $e) { 334 | logActivity('Fleio: unable to initialize the Fleio API module: ' . $e->getMessage()); 335 | continue; 336 | } 337 | # If the product is active, try to get the price from Fleio billing 338 | try { 339 | $price = $fl->getBillingPrice(); 340 | } catch (FlApiException $e) { 341 | logActivity('Fleio: unable to get the billing price for Service ID: ' . $item->relid . ' : ' . $e->getMessage()); 342 | # NOTE(tomo): Deleting the item will cause a retry on the next run, which we want but there 343 | # are other complications. 344 | # Also note that an invoice may contain multiple entries with the same relid (eg: Setup and Hosting types costs for a product). 345 | #Capsule::table('tblinvoiceitems')->where('id', '=', $item->id)->delete(); 346 | #logActivity('Fleio: deleted service ID: ' . $item->relid . ' from Invoice ID: ' . $invoice->id); 347 | continue; 348 | } 349 | # NOTE(tomo): The price of a Fleio item is usually 0, until we set it from Fleio. 350 | Capsule::table('tblinvoiceitems') 351 | ->where([['id', (string) $item->id], ['type', '=', 'Hosting']]) 352 | ->increment('amount', $price); 353 | if ($item->taxed) { 354 | $tax += $price * $invoice->taxrate / 100; 355 | $tax2 += $price * $invoice->taxrate2 / 100; 356 | } 357 | $subtotal_price += $price; 358 | } 359 | $total_price = $subtotal_price + $tax + $tax2; 360 | if ($total_price > 0) { 361 | # Capsule::table('tblinvoices')->where('id', '=', $vars['invoiceid'])->update(array("subtotal"=>$total_price, "tax"=>$tax, "tax2"=>$tax2, "total"=>$total_price)); 362 | logActivity('Fleio: incrementing price of Invoice ID: '. $vars['invoiceid'] . ' with ' . $total_price . ' and tax with ' . $tax . ' and tax2 with ' . $tax2); 363 | Capsule::table('tblinvoices')->where('id', '=', $vars['invoiceid'])->increment('subtotal', $subtotal_price); 364 | Capsule::table('tblinvoices')->where('id', '=', $vars['invoiceid'])->increment('total', $total_price); 365 | Capsule::table('tblinvoices')->where('id', '=', $vars['invoiceid'])->increment('tax', $tax); 366 | Capsule::table('tblinvoices')->where('id', '=', $vars['invoiceid'])->increment('tax2', $tax2); 367 | logActivity('Fleio: prices and taxes updated for Invoice ID: ' . $vars['invoiceid']); 368 | } 369 | } 370 | 371 | function openstack_change_funds($invoiceid, $subtract=false) { 372 | /* 373 | Check all invoice items and for each Fleio product, either add or 374 | remove credit based on the total item costs and the action performed. 375 | */ 376 | $items = Capsule::table('tblinvoiceitems')->where('invoiceid', '=', $invoiceid)->get(); 377 | 378 | // Retrieve the invoice total paid. 379 | // If this invoice was not fully paid, we do not subtract from Fleio. 380 | // There is no other way to prevent subtracting from Fleio when cancelling an invoice and marking it unpaid afterwards 381 | try { 382 | $balance = Capsule::table('tblaccounts as ta') 383 | ->where('ta.invoiceid', '=', $invoiceid) 384 | ->join('tblinvoices as ti', 'ta.invoiceid', '=', 'ti.id') 385 | ->select(Capsule::raw('SUM(ta.amountin)-SUM(ta.amountout)-ti.total as balance')) 386 | ->value('balance'); 387 | } catch (Exception $e) { 388 | logActivity($e->getMessage()); 389 | $balance = false; 390 | } 391 | $cost_by_service = array(); 392 | $promo_by_service = array(); 393 | foreach($items as $item) { 394 | # NOTE(tomo): Check if relid is set and not an empty string 395 | if (($item->relid == '') || !isset($item->relid)) { 396 | continue; 397 | } 398 | if ($item->type == 'PromoHosting') { 399 | # NOTE(tomo): Do nothing with $promo_by_service. Just don't add it 400 | # to the final amount to avoid incorrect credit addition in Fleio 401 | if (isset($promo_by_service[$item->relid])) { 402 | $promo_by_service[$item->relid] += $item->amount; 403 | } else { 404 | $promo_by_service[$item->relid] = $item->amount; 405 | } 406 | } else { 407 | if ($item->type == 'Hosting') { 408 | if (isset($cost_by_service[$item->relid])) { 409 | $cost_by_service[$item->relid] += $item->amount; 410 | } else { 411 | $cost_by_service[$item->relid] = $item->amount; 412 | } 413 | } 414 | } 415 | } 416 | 417 | $processedServices = []; 418 | foreach($items as $item) { 419 | if (($item->type != 'Hosting') || !isset($cost_by_service[$item->relid]) || in_array($item->relid, $processedServices)) { 420 | continue; 421 | } 422 | array_push($processedServices, $item->relid); 423 | # We now know that relid is a Hosting package (not a Domain for example) 424 | $service = FleioUtils::getServiceById($item->relid); 425 | if ($service->servertype == 'fleio') { 426 | # NOTE(tomo): Make sure the service is active. If it's not active and we don't handle this, the credit is lost. 427 | # NOTE(tomo): We currently handle this in the add/remove credit methods. 428 | $clientAmount = $cost_by_service[$item->relid]; // Amount + Setup and/or other related prices in client's currency 429 | if ($clientAmount == 0) { 430 | logActivity('Fleio: ignoring Service ID: '. $item->relid . ' with cost equal to 0 from Invoice ID: ' . $invoiceid); 431 | continue; 432 | } 433 | $fl = Fleio::fromServiceId($item->relid); 434 | # TODO(tomo): We use the userid which can be a contact ? 435 | try { 436 | $fleioClient = $fl->getClient(); 437 | } catch (Exception $e) { 438 | logActivity('Unable to get Fleio client currency when trying to change credit: ' . $e->getMessage()); 439 | return; 440 | } 441 | $fleioClientCurrencyCode = $fleioClient['currency']; 442 | $whmcsFleioClientCurrency = Capsule::table('tblcurrencies') 443 | ->select('id', 'code') 444 | ->where('code', '=', $fleioClientCurrencyCode) 445 | ->first(); 446 | if (!$whmcsFleioClientCurrency) { 447 | logActivity( 448 | 'ERROR: Could not proceed changing client (' . 449 | $fleioClient['external_billing_id'] . 450 | ') credit in fleio because his currency from fleio (' . 451 | $fleioClientCurrencyCode . 452 | ') does not exist in whmcs' 453 | ); 454 | return; 455 | } 456 | $originalCurrency = getCurrency($item->userid); 457 | $amount = convertCurrency($clientAmount, $originalCurrency['id'], $whmcsFleioClientCurrency->id); 458 | try { 459 | $addCredit = !$subtract; // Add credit or subtract, boolean 460 | if ($addCredit) { 461 | $msg_format = "Fleio: adding credit for WHMCS Client ID: %s with %.02f %s (%.02f %s from Invoice ID: %s)"; 462 | } else { 463 | $msg_format = "Fleio: removing credit for WHMCS Client ID: %s with %.02f %s (%.02f %s from Invoice ID: %s)"; 464 | } 465 | $msg = sprintf( 466 | $msg_format, 467 | $item->userid, 468 | $amount, 469 | $whmcsFleioClientCurrency->code, 470 | $clientAmount, 471 | $originalCurrency["code"], 472 | $invoiceid 473 | ); 474 | logActivity($msg); 475 | $response = $fl->clientChangeCredit( 476 | $addCredit, $amount, $whmcsFleioClientCurrency->code, $originalCurrency["rate"], $clientAmount, 477 | $originalCurrency["code"], $invoiceid 478 | ); 479 | } catch (FlApiException $e) { 480 | logActivity("Unable to update the client credit in Fleio: " . $e->getMessage()); 481 | return; 482 | } 483 | logActivity( 484 | "Fleio: successfully changed client credit with ".$amount." ".$whmcsFleioClientCurrency->code. 485 | " for Fleio client id: ".$response['client'].". New Fleio balance: ".$response['credit_balance']." ". 486 | $whmcsFleioClientCurrency->code 487 | ); 488 | } 489 | } 490 | } 491 | 492 | 493 | function openstack_add_funds_hook($vars) { 494 | openstack_change_funds($vars["invoiceid"]); 495 | } 496 | 497 | function openstack_del_credit_hook($vars) { 498 | openstack_change_funds($vars["invoiceid"], true); 499 | } 500 | 501 | function fleio_ClientAreaPrimarySidebar(MenuItem $pn) { 502 | $actionsNav = $pn->getChild("Service Details Actions"); 503 | if (is_null($actionsNav)) { 504 | return; 505 | } 506 | $navItem = $actionsNav->getChild('Custom Module Button Login to Fleio'); 507 | if (!is_null($navItem)) { 508 | $navItem->setAttribute("target", '_blank'); 509 | } 510 | } 511 | 512 | function fleio_client_edit($vars) { 513 | try { 514 | $product = FleioUtils::getClientProduct($vars["userid"]); 515 | } catch (Exception $e) { 516 | return; 517 | } 518 | // if this client has no OpenStack products, return 519 | if (!$product) { 520 | return; 521 | } 522 | else { 523 | $details = array( 524 | "first_name" => $vars["firstname"], 525 | "last_name" => $vars["lastname"], 526 | "address1" => $vars["address1"], 527 | "address2" => $vars["address2"], 528 | "city" => $vars["city"], 529 | "state" => $vars["state"], 530 | "email" => $vars["email"], 531 | "zip_code" => $vars["postcode"], 532 | "country" => $vars["country"], 533 | "company" => $vars["companyname"], 534 | "phone" => $vars["phonenumber"] 535 | ); 536 | } 537 | try { 538 | $fl = Fleio::fromServiceId($product->id); 539 | return $fl->updateFleioClient($details); 540 | } catch (Exception $e) { 541 | // FIXME(tomo): Catch all just in case... 542 | return; 543 | } 544 | } 545 | 546 | function limitOrders($vars) { 547 | // doesn't let users order more than one product of type fleio 548 | $productsInCart = $_SESSION['cart']['products']; 549 | $fleioRelatedServicesInCart = 0; 550 | foreach ($productsInCart as $product) { 551 | $dbProduct = Capsule::table('tblproducts') 552 | ->select('tblproducts.servertype') 553 | ->where('tblproducts.id', '=', $product['pid']) 554 | ->first(); 555 | if ($dbProduct->servertype === 'fleio') { 556 | $fleioRelatedServicesInCart = $fleioRelatedServicesInCart + 1; 557 | if ($fleioRelatedServicesInCart > 1) { 558 | global $errormessage; 559 | $errormessage = "
Credit: ' . '' . $uptodateCreditFormatted . ''; 620 | } else { 621 | $bodyHtml = $bodyHtml . '
Credit: ' . $uptodateCreditFormatted; 622 | } 623 | $bodyHtml = $bodyHtml . ', Add credit
'; 625 | } 626 | $bodyHtml = $bodyHtml . 'Auto-pay is '; 627 | if ($client['has_billing_agreement']) { 628 | $bodyHtml = $bodyHtml . 'active.
'; 629 | } else { 630 | $bodyHtml = $bodyHtml . 'NOT active. '; 631 | if ($fleioServers[0]->configoption17) { 632 | $bodyHtml = $bodyHtml . $fleioServers[0]->configoption17 . ''; 633 | } 634 | } 635 | } 636 | if ($fleioServers[0]->configoption18) { 637 | $bodyHtml = $bodyHtml . $fleioServers[0]->configoption18; 638 | } 639 | $fleioPanel = $homePagePanels->addChild('fleio cloud', array( 640 | 'label' => 'Cloud', 641 | 'icon' => 'fa-cloud', 642 | 'extras' => array( 643 | 'color' => 'green', 644 | 'btn-link' => $system_url . 'accesscloudcontrolpanel.php', 645 | 'btn-text' => 'Access Control Panel', 646 | 'btn-icon' => 'fa-arrow-right' 647 | ), 648 | 'bodyHtml' => $bodyHtml, 649 | )); 650 | $fleioPanel->moveToFront(); 651 | } 652 | } 653 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /migrate_services_external_billing_id.php: -------------------------------------------------------------------------------- 1 | join('tblproducts', 'tblhosting.packageid', '=', 'tblproducts.id') 10 | ->join('tblclients as tc', 'tc.id', '=', 'tblhosting.userid') 11 | ->where('tblproducts.servertype', '=', 'fleio') 12 | ->count(); 13 | 14 | 15 | $whmcsClients = Capsule::table('tblclients') 16 | ->select('tblclients.id', 'tblclients.uuid') 17 | ->get(); 18 | 19 | // SCRIPT WORKS IF THERE IS AT MOST ONE SERVICE PER CLIENT IN FLEIO 20 | echo 'There are '. $whmcsServicesCount . ' fleio services'; 21 | echo "\r\n"; 22 | 23 | $processedServices = 0; 24 | foreach($whmcsClients AS $whmcsClient) { 25 | $whmcsClientServices = Capsule::table('tblhosting') 26 | ->join('tblproducts', 'tblhosting.packageid', '=', 'tblproducts.id') 27 | ->where('tblproducts.servertype', '=', 'fleio') 28 | ->where('tblhosting.userid', '=', $whmcsClient->id) 29 | ->select('tblhosting.id', 'tblhosting.domainstatus') 30 | ->get(); 31 | $servicesStatusesCountMap = array( 32 | "Active" => array( 33 | "count" => 0, 34 | "id_list" => array() 35 | ), 36 | "Suspended" => array( 37 | "count" => 0, 38 | "id_list" => array() 39 | ), 40 | "Terminated" => array( 41 | "count" => 0, 42 | "id_list" => array() 43 | ), 44 | "Cancelled" => array( 45 | "count" => 0, 46 | "id_list" => array() 47 | ), 48 | "Fraud" => array( 49 | "count" => 0, 50 | "id_list" => array() 51 | ), 52 | "Pending" => array( 53 | "count" => 0, 54 | "id_list" => array() 55 | ) 56 | ); 57 | foreach($whmcsClientServices AS $whmcsClientService) { 58 | $servicesStatusesCountMap[$whmcsClientService->domainstatus]["count"] = $servicesStatusesCountMap[$whmcsClientService->domainstatus]["count"] + 1; 59 | array_push($servicesStatusesCountMap[$whmcsClientService->domainstatus]["id_list"], $whmcsClientService->id); 60 | $processedServices = $processedServices + 1; 61 | } 62 | if ($servicesStatusesCountMap["Active"]["count"] > 1) { 63 | // if more than one active found, we cannot automatically determine 64 | echo "Cannot process services for client " . $whmcsClient->id . " : More than one active service found\n"; 65 | } else if ($servicesStatusesCountMap["Active"]["count"] == 1) { 66 | // if only one active, update it in fleio 67 | try { 68 | $flApi = Fleio::fromServiceId($servicesStatusesCountMap["Active"]["id_list"][0]); 69 | $flApi->updateServiceExternalBillingId($servicesStatusesCountMap["Active"]["id_list"][0], $whmcsClient->uuid); 70 | } catch (Exception $e) { 71 | echo ''.$e->getMessage(); 72 | } 73 | } else if ($servicesStatusesCountMap["Suspended"]["count"] > 1) { 74 | // if none active found and multiple suspended found, we cannot determine 75 | echo "Cannot process services for client " . $whmcsClient->id . " : More than one suspended service found and none active\n"; 76 | } else if ($servicesStatusesCountMap["Suspended"]["count"] == 1) { 77 | // if none active but one suspended set it 78 | try { 79 | $flApi = Fleio::fromServiceId($servicesStatusesCountMap["Suspended"]["id_list"][0]); 80 | $flApi->updateServiceExternalBillingId($servicesStatusesCountMap["Suspended"]["id_list"][0], $whmcsClient->uuid); 81 | } catch (Exception $e) { 82 | echo ''.$e->getMessage(); 83 | } 84 | } else if ($servicesStatusesCountMap["Terminated"]["count"] > 0 && $servicesStatusesCountMap["Cancelled"]["count"] > 0) { 85 | // if none active or suspended but at least one terminated and cancelled, we cannot determine 86 | echo "Cannot process services for client " . $whmcsClient->id . " : Has no active or suspended service and has at least one terminated and at least one cancelled service.\n"; 87 | } else if ($servicesStatusesCountMap["Terminated"]["count"] > 1) { 88 | // if none active, suspended or cancelled but multiple terminated we cannot determine 89 | echo "Cannot process services for client " . $whmcsClient->id . " : Has no active, suspended or cancelled service and has multiple terminated services.\n"; 90 | } else if ($servicesStatusesCountMap["Cancelled"]["count"] > 1) { 91 | // if none active, suspended or terminated but multiple cancelled we cannot determine 92 | echo "Cannot process services for client " . $whmcsClient->id . " : Has no active, suspended or terminated service and has multiple cancelled services.\n"; 93 | } else if ($servicesStatusesCountMap["Cancelled"]["count"] == 1) { 94 | // if no other status than one of cancelled (excluding pending), update it in fleio 95 | try { 96 | $flApi = Fleio::fromServiceId($servicesStatusesCountMap["Cancelled"]["id_list"][0]); 97 | $flApi->updateServiceExternalBillingId($servicesStatusesCountMap["Cancelled"]["id_list"][0], $whmcsClient->uuid); 98 | } catch (Exception $e) { 99 | echo ''.$e->getMessage(); 100 | } 101 | } else if ($servicesStatusesCountMap["Terminated"]["count"] == 1) { 102 | // if no other status than one of terminated (excluding pending), update it in fleio 103 | try { 104 | $flApi = Fleio::fromServiceId($servicesStatusesCountMap["Terminated"]["id_list"][0]); 105 | $flApi->updateServiceExternalBillingId($servicesStatusesCountMap["Terminated"]["id_list"][0], $whmcsClient->uuid); 106 | } catch (Exception $e) { 107 | echo ''.$e->getMessage(); 108 | } 109 | } else if ($servicesStatusesCountMap["Fraud"]["count"] > 1) { 110 | // if none active, suspended, terminated or cancelled but multiple fraud we cannot determine 111 | echo "Cannot process services for client " . $whmcsClient->id . " : Has no active, suspended, terminated or cancelled services and has multiple fraud services.\n"; 112 | } else if ($servicesStatusesCountMap["Fraud"]["count"] == 1) { 113 | // if no other status than one of fraud (excluding pending), update it in fleio 114 | try { 115 | $flApi = Fleio::fromServiceId($servicesStatusesCountMap["Fraud"]["id_list"][0]); 116 | $flApi->updateServiceExternalBillingId($servicesStatusesCountMap["Fraud"]["id_list"][0], $whmcsClient->uuid); 117 | } catch (Exception $e) { 118 | echo ''.$e->getMessage(); 119 | } 120 | } 121 | } 122 | echo 'Processed services count: '.$processedServices; 123 | 124 | ?> 125 | -------------------------------------------------------------------------------- /templates/error.tpl: -------------------------------------------------------------------------------- 1 | {$usefulErrorHelper} 2 | -------------------------------------------------------------------------------- /templates/overview.tpl: -------------------------------------------------------------------------------- 1 | 8 | 9 |