_helper->getHelper('AjaxContext'); $ajaxContext->addActionContext('vat-validator', 'json') ->addActionContext('is-country-in-eu', 'json') ->initContext(); } public function indexAction() { $this->_redirect('billing/upgrade'); } public function upgradeAction() { $CC_CONFIG = Config::getConfig(); $baseUrl = Application_Common_OsPath::getBaseDir(); $this->view->headLink()->appendStylesheet($baseUrl.'css/billing.css?'.$CC_CONFIG['airtime_version']); BillingController::ensureClientIdIsValid(); $request = $this->getRequest(); $form = new Application_Form_BillingUpgradeDowngrade(); if ($request->isPost()) { $formData = $request->getPost(); if ($form->isValid($formData)) { $credentials = self::getAPICredentials(); //Check if VAT should be applied or not to this invoice. if (in_array("7", $formData["customfields"])) { $apply_vat = BillingController::checkIfVatShouldBeApplied($formData["customfields"]["7"], $formData["country"]); } else { $apply_vat = false; } $placeAnUpgradeOrder = true; $currentPlanProduct = BillingController::getClientCurrentAirtimeProduct(); $currentPlanProductId = $currentPlanProduct["pid"]; $currentPlanProductBillingCycle = strtolower($currentPlanProduct["billingcycle"]); //If there's been no change in the plan or the billing cycle, we should not //place an upgrade order. WHMCS doesn't allow this in its web interface, //and it freaks out and does the wrong thing if we do it via the API //so we have to do avoid that. if (($currentPlanProductId == $formData["newproductid"]) && ($currentPlanProductBillingCycle == $formData["newproductbillingcycle"])) { $placeAnUpgradeOrder = false; } $postfields = array(); $postfields["username"] = $credentials["username"]; $postfields["password"] = md5($credentials["password"]); $postfields["action"] = "upgradeproduct"; $postfields["clientid"] = Application_Model_Preference::GetClientId(); $postfields["serviceid"] = self::getClientInstanceId(); $postfields["type"] = "product"; $postfields["newproductid"] = $formData["newproductid"]; $postfields["newproductbillingcycle"] = $formData["newproductbillingcycle"]; $postfields["paymentmethod"] = $formData["paymentmethod"]; $postfields["responsetype"] = "json"; $upgrade_query_string = ""; foreach ($postfields AS $k=>$v) $upgrade_query_string .= "$k=".urlencode($v)."&"; //update client info $clientfields = array(); $clientfields["username"] = $credentials["username"]; $clientfields["password"] = md5($credentials["password"]); $clientfields["action"] = "updateclient"; $clientfields["clientid"] = Application_Model_Preference::GetClientId(); $clientfields["customfields"] = base64_encode(serialize($formData["customfields"])); unset($formData["customfields"]); $clientfields["responsetype"] = "json"; unset($formData["newproductid"]); unset($formData["newproductbillingcycle"]); unset($formData["paymentmethod"]); unset($formData["action"]); $clientfields = array_merge($clientfields, $formData); unset($clientfields["password2verify"]); unset($clientfields["submit"]); $client_query_string = ""; foreach ($clientfields AS $k=>$v) $client_query_string .= "$k=".urlencode($v)."&"; //Update the client details in WHMCS first $result = $this->makeRequest($credentials["url"], $client_query_string); Logging::info($result); if ($result["result"] == "error") { $this->setErrorMessage(); $this->view->form = $form; return; } //If there were no changes to the plan or billing cycle, we just redirect you to the //invoices screen and show a message. if (!$placeAnUpgradeOrder) { $this->_redirect('billing/invoices?planupdated'); return; } //Then place an upgrade order in WHMCS $result = $this->makeRequest($credentials["url"], $upgrade_query_string); if ($result["result"] == "error") { Logging::info($_SERVER['HTTP_HOST']." - Account upgrade failed. - ".$result["message"]); $this->setErrorMessage(); $this->view->form = $form; } else { Logging::info($_SERVER['HTTP_HOST']. "Account plan upgrade request:"); Logging::info($result); // Disable the view and the layout here, squashes an error. $this->view->layout()->disableLayout(); $this->_helper->viewRenderer->setNoRender(true); if ($apply_vat) { $this->addVatToInvoice($result["invoiceid"]); } self::viewInvoice($result["invoiceid"]); } } else { $this->view->form = $form; } } else { $this->view->form = $form; } } public function isCountryInEuAction() { // Disable the view and the layout $this->view->layout()->disableLayout(); $this->_helper->viewRenderer->setNoRender(true); $request = $this->getRequest(); if (!$request->isPost()) { throw new Exception("Must POST data to isCountryInEuAction."); } $formData = $request->getPost(); //Set the return JSON value $this->_helper->json(array("result"=>BillingController::isCountryInEU($formData["country"]))); } public function vatValidatorAction() { // Disable the view and the layout $this->view->layout()->disableLayout(); $this->_helper->viewRenderer->setNoRender(true); $request = $this->getRequest(); if (!$request->isPost()) { throw new Exception("Must POST data to vatValidatorAction."); } $formData = $request->getPost(); $vatNumber = trim($formData["vatnumber"]); if (empty($vatNumber)) { $this->_helper->json(array("result"=>false)); } //Set the return JSON value $this->_helper->json(array("result"=>BillingController::checkIfVatShouldBeApplied($vatNumber, $formData["country"]))); } /** * @return True if VAT should be applied to the order, false otherwise. */ private static function checkIfVatShouldBeApplied($vatNumber, $countryCode) { if ($countryCode === 'UK') { $countryCode = 'GB'; //VIES database has it as GB } //We don't charge you VAT if you're not in the EU if (!BillingController::isCountryInEU($countryCode)) { return false; } //So by here, we know you're in the EU. //No VAT number? Then we charge you VAT. if (empty($vatNumber)) { return true; } //Check if VAT number is valid return BillingController::validateVATNumber($vatNumber, $countryCode); } private static function isCountryInEU($countryCode) { $euCountryCodes = array('BE', 'BG', 'CZ', 'DK', 'DE', 'EE', 'IE', 'EL', 'ES', 'FR', 'HR', 'IT', 'CY', 'LV', 'LT', 'LU', 'HU', 'MT', 'NL', 'AT', 'PL', 'PT', 'RO', 'SI', 'SK', 'FI', 'SE', 'UK', 'GB'); if (!in_array($countryCode, $euCountryCodes)) { return false; } return true; } /** * Check if an EU VAT number is valid, using the EU VIES validation web API. * * @param string $vatNumber - A VAT identifier (number), with or without the two letter country code at the * start (either one works) . * @param string $countryCode - A two letter country code * @return boolean true if the VAT number is valid, false otherwise. */ private static function validateVATNumber($vatNumber, $countryCode) { $vatNumber = str_replace(array(' ', '.', '-', ',', ', '), '', trim($vatNumber)); //If the first two letters are a country code, use that as the country code and remove those letters. $firstTwoCharacters = substr($vatNumber, 0, 2); if (preg_match("/[a-zA-Z][a-zA-Z]/", $firstTwoCharacters) === 1) { $countryCode = strtoupper($firstTwoCharacters); //The country code from the VAT number overrides your country. $vatNumber = substr($vatNumber, 2); } $client = new SoapClient("http://ec.europa.eu/taxation_customs/vies/checkVatService.wsdl"); if($client){ $params = array('countryCode' => $countryCode, 'vatNumber' => $vatNumber); try{ $r = $client->checkVat($params); if($r->valid == true){ // VAT-ID is valid return true; } else { // VAT-ID is NOT valid return false; } } catch(SoapFault $e) { Logging::error('VIES EU VAT validation error: '.$e->faultstring); if ($e->faultstring == "INVALID_INPUT") { return false; } //If there was another error with the VAT validation service, we allow //the VAT number to pass. (eg. SERVER_BUSY, MS_UNAVAILABLE, TIMEOUT, SERVICE_UNAVAILABLE) return true; } } else { // Connection to host not possible, europe.eu down? Logging::error('VIES EU VAT validation error: Host unreachable'); //If there was an error with the VAT validation service, we allow //the VAT number to pass. return true; } return false; } private function addVatToInvoice($invoice_id) { $credentials = self::getAPICredentials(); //First we need to get the invoice details: sub total, and total //so we can calcuate the amount of VAT to add $invoicefields = array(); $invoicefields["username"] = $credentials["username"]; $invoicefields["password"] = md5($credentials["password"]); $invoicefields["action"] = "getinvoice"; $invoicefields["invoiceid"] = $invoice_id; $invoicefields["responsetype"] = "json"; $invoice_query_string = ""; foreach ($invoicefields as $k=>$v) $invoice_query_string .= "$k=".urlencode($v)."&"; //TODO: error checking $result = $this->makeRequest($credentials["url"], $invoice_query_string); $vat_amount = $result["subtotal"] * (VAT_RATE/100); $invoice_total = $result["total"] + $vat_amount; //Second, update the invoice with the VAT amount and updated total $postfields = array(); $postfields["username"] = $credentials["username"]; $postfields["password"] = md5($credentials["password"]); $postfields["action"] = "updateinvoice"; $postfields["invoiceid"] = $invoice_id; $postfields["tax"] = "$vat_amount"; $postfields["taxrate"] = strval(VAT_RATE); $postfields["total"] = "$invoice_total"; $postfields["responsetype"] = "json"; $query_string = ""; foreach ($postfields as $k=>$v) $query_string .= "$k=".urlencode($v)."&"; //TODO: error checking $result = $this->makeRequest($credentials["url"], $query_string); } private function setErrorMessage($msg=null) { if (!is_null($msg)) { $this->view->errorMessage = $msg; } else { $this->view->errorMessage = "An error occurred and we could not update your account. Please contact support for help."; } } private function setSuccessMessage($msg=null) { if (!is_null($msg)) { $this->view->successMessage = $msg; } else { $this->view->successMessage = "Your account has been updated."; } } private static function getAPICredentials() { return array( "username" => $_SERVER["WHMCS_USERNAME"], "password" => $_SERVER["WHMCS_PASSWORD"], "url" => "https://account.sourcefabric.com/includes/api.php?accesskey=".$_SERVER["WHMCS_ACCESS_KEY"], ); } private static function viewInvoice($invoice_id) { $whmcsurl = "https://account.sourcefabric.com/dologin.php"; $autoauthkey = $_SERVER["WHMCS_AUTOAUTH_KEY"]; $timestamp = time(); //whmcs timezone? $client = self::getClientDetails(); $email = $client["email"]; $hash = sha1($email.$timestamp.$autoauthkey); $goto = "viewinvoice.php?id=".$invoice_id; header("Location: ".$whmcsurl."?email=$email×tamp=$timestamp&hash=$hash&goto=$goto"); } public function clientAction() { $CC_CONFIG = Config::getConfig(); $baseUrl = Application_Common_OsPath::getBaseDir(); $this->view->headLink()->appendStylesheet($baseUrl.'css/billing.css?'.$CC_CONFIG['airtime_version']); $request = $this->getRequest(); $form = new Application_Form_BillingClient(); BillingController::ensureClientIdIsValid(); if ($request->isPost()) { $formData = $request->getPost(); if ($form->isValid($formData)) { $credentials = self::getAPICredentials(); $postfields = array(); $postfields["username"] = $credentials["username"]; $postfields["password"] = md5($credentials["password"]); $postfields["action"] = "updateclient"; $postfields["customfields"] = base64_encode(serialize($formData["customfields"])); unset($formData["customfields"]); $postfields["clientid"] = Application_Model_Preference::GetClientId(); $postfields["responsetype"] = "json"; $postfields = array_merge($postfields, $formData); unset($postfields["password2verify"]); unset($postfields["submit"]); $query_string = ""; foreach ($postfields AS $k=>$v) $query_string .= "$k=".urlencode($v)."&"; $result = $this->makeRequest($credentials["url"], $query_string); if ($result["result"] == "error") { $this->setErrorMessage(); } else { $form = new Application_Form_BillingClient(); $this->setSuccessMessage(); } $this->view->form = $form; } else { $this->view->form = $form; } } else { $this->view->form = $form; } } public function invoicesAction() { $CC_CONFIG = Config::getConfig(); $baseUrl = Application_Common_OsPath::getBaseDir(); $this->view->headLink()->appendStylesheet($baseUrl.'css/billing.css?'.$CC_CONFIG['airtime_version']); BillingController::ensureClientIdIsValid(); $credentials = self::getAPICredentials(); $postfields = array(); $postfields["username"] = $credentials["username"]; $postfields["password"] = md5($credentials["password"]); $postfields["action"] = "getinvoices"; $postfields["responsetype"] = "json"; $postfields["userid"] = Application_Model_Preference::GetClientId(); $query_string = ""; foreach ($postfields AS $k=>$v) $query_string .= "$k=".urlencode($v)."&"; $result = self::makeRequest($credentials["url"], $query_string); if ($result["invoices"]) { $this->view->invoices = $result["invoices"]["invoice"];; } else { $this->view->invoices = array(); } } public function invoiceAction() { BillingController::ensureClientIdIsValid(); $request = $this->getRequest(); $invoice_id = $request->getParam('invoiceid'); self::viewInvoice($invoice_id); } /** Get the Airtime instance ID of the instance the customer is currently viewing. */ private static function getClientInstanceId() { $currentProduct = BillingController::getClientCurrentAirtimeProduct(); return $currentProduct["id"]; } public static function getProducts() { //Making this static to cache the products during a single HTTP request. //This saves us roundtrips to WHMCS if getProducts() is called multiple times. static $result = array(); if (!empty($result)) { return $result["products"]["product"]; } $credentials = self::getAPICredentials(); $postfields = array(); $postfields["username"] = $credentials["username"]; $postfields["password"] = md5($credentials["password"]); $postfields["action"] = "getproducts"; $postfields["responsetype"] = "json"; //gid is the Airtime product group id on whmcs $postfields["gid"] = "15"; $query_string = ""; foreach ($postfields AS $k=>$v) $query_string .= "$k=".urlencode($v)."&"; $result = self::makeRequest($credentials["url"], $query_string); //Logging::info($result["products"]["product"]); $products = $result["products"]["product"]; //Blacklist all free plans foreach ($products as $k => $p) { Logging::info($p); if ($p["paytype"] === "free") { unset($products[$k]); } } return $products; } public static function getProductPricesAndTypes() { $products = BillingController::getProducts(); foreach ($products as $k => $p) { $productPrices[$p["name"]] = array( "monthly" => $p["pricing"]["USD"]["monthly"], "annually" => $p["pricing"]["USD"]["annually"] ); $productTypes[$p["pid"]] = $p["name"] . " ($" . $productPrices[$p['name']]['monthly'] . "/mo)"; } return array($productPrices, $productTypes); } /** Get the plan (or product in WHMCS lingo) that the customer is currently on. * @return An associative array containing the fields for the product * */ public static function getClientCurrentAirtimeProduct() { static $airtimeProduct = null; //Ghetto caching to avoid multiple round trips to WHMCS if ($airtimeProduct) { return $airtimeProduct; } $credentials = self::getAPICredentials(); $postfields = array(); $postfields["username"] = $credentials["username"]; $postfields["password"] = md5($credentials["password"]); $postfields["action"] = "getclientsproducts"; $postfields["responsetype"] = "json"; $postfields["clientid"] = Application_Model_Preference::GetClientId(); $query_string = ""; foreach ($postfields AS $k=>$v) $query_string .= "$k=".urlencode($v)."&"; $result = self::makeRequest($credentials["url"], $query_string); //XXX: Debugging / local testing if ($_SERVER['SERVER_NAME'] == "airtime.localhost") { $_SERVER['SERVER_NAME'] = "bananas.airtime.pro"; } //This code must run on airtime.pro for it to work... it's trying to match //the server's hostname with the client subdomain. Once it finds a match //between the product and the server's hostname/subdomain, then it //returns the ID of that product (aka. the service ID of an Airtime instance) foreach ($result["products"]["product"] as $product) { if (strpos($product["groupname"], "Airtime") === FALSE) { //Ignore non-Airtime products continue; } else { if ($product["status"] === "Active") { $airtimeProduct = $product; $subdomain = ''; foreach ($airtimeProduct['customfields']['customfield'] as $customField) { if ($customField['name'] === SUBDOMAIN_WHMCS_CUSTOM_FIELD_NAME) { $subdomain = $customField['value']; if (($subdomain . ".airtime.pro") === $_SERVER['SERVER_NAME']) { return $airtimeProduct; } } } } } } throw new Exception("Unable to match subdomain to a service ID"); } public static function getClientDetails() { try { $credentials = self::getAPICredentials(); $postfields = array(); $postfields["username"] = $credentials["username"]; $postfields["password"] = md5($credentials["password"]); $postfields["action"] = "getclientsdetails"; $postfields["stats"] = true; $postfields["clientid"] = Application_Model_Preference::GetClientId(); $postfields["responsetype"] = "json"; $query_string = ""; foreach ($postfields AS $k=>$v) $query_string .= "$k=".urlencode($v)."&"; $arr = self::makeRequest($credentials["url"], $query_string); return $arr["client"]; } catch (Exception $e) { Logging::info($e->getMessage()); } } private static function makeRequest($url, $query_string) { try { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_TIMEOUT, 5); //Aggressive 5 second timeout curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $query_string); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); $jsondata = curl_exec($ch); if (curl_error($ch)) { //die("Connection Error: ".curl_errno($ch).' - '.curl_error($ch)); throw new Exception("WHMCS server down or invalid request."); } curl_close($ch); return json_decode($jsondata, true); } catch (Exception $e) { Logging::info($e->getMessage()); } } private static function ensureClientIdIsValid() { if (Application_Model_Preference::GetClientId() == null) { throw new Exception("Invalid client ID: " . Application_Model_Preference::GetClientId()); } } }