'PayAnyWay', 'APIVersion' => '1.1', 'DisableLocalCreditCardInput' => true, 'TokenisedStorage' => false, ]; } /** * Define gateway configuration options. * * The fields you define here determine the configuration options that are * presented to administrator users when activating and configuring your * payment gateway module for use. * * @return array */ function payanyway_config() { return [ 'FriendlyName' => [ 'Type' => 'System', 'Value' => 'PayAnyWay', ], 'mnt_id' => [ 'FriendlyName' => 'Account Number', 'Type' => 'text', 'Size' => '20', 'Default' => '', 'Description' => 'Enter your account number provided by PayAnyWay ', ], 'mnt_dataintegrity_code' => [ 'FriendlyName' => 'Code of data integrity verification', 'Type' => 'text', 'Size' => '20', 'Default' => '', 'Description' => 'Enter your integrity code provided by PayAnyWay', ], 'mnt_test_mode' => [ 'FriendlyName' => 'Test Mode', 'Type' => 'yesno', 'Description' => 'Tick to enable test mode', ], ]; } /** * Generate a PayAnyWay payment link for a WHMCS invoice. * * Creates a payment preference via PayAnyWay and returns an * anchor tag () pointing to the hosted checkout page. WHMCS will render * this HTML inside the invoice payment section. * * @param array $params Standard WHMCS gateway params array. * See: https://developers.whmcs.com/payment-gateways/third-party-gateway/ * @return string HTML output to display on the invoice page. */ function payanyway_link($params) { try { // Gateway Configuration Parameters $mntId = payanyway_getPayAnyWayMntIdOrFail($params); $testMode = !empty($params['mnt_test_mode']) ? '1' : '0'; $integrityCode = payanyway_getPayAnyWayIntegrityCodeOrFail($params); // Invoice Parameters $invoiceId = payanyway_getWhmcsInvoiceIdOrFail($params); $amount = payanyway_getWhmcsInvoiceAmountOrFail($params); $formatAmount = payanyway_formatAmount($amount); $currency = payanyway_getWhmcsInvoiceCurrencyOrFail($params); } catch (\PayAnyWayException $e) { return '

Error: ' . htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8') . '

'; } // System Parameters $langPayNow = payanyway_getOptionalStringWithDefault($params, 'langpaynow', 'Pay Now'); $whmcsVersionNumber = payanyway_getOptionalString($params, 'whmcsVersion'); $whmcsVersion = isset($whmcsVersionNumber) ? 'WHMCS_' . $whmcsVersionNumber : 'WHMCS_unknown_version'; $url = ('1' === $testMode) ? PAYANYWAY_API_URL_DEMO : PAYANYWAY_API_URL_PROD; $subscriberId = payanyway_getSubscriberId($params); $signature = md5($mntId . $invoiceId . $formatAmount . $currency . $subscriberId . $testMode . $integrityCode); $postFields = [ 'MNT_ID' => $mntId, 'MNT_TRANSACTION_ID' => $invoiceId, 'MNT_AMOUNT' => $formatAmount, 'MNT_CURRENCY_CODE' => $currency, 'MNT_TEST_MODE' => $testMode, 'MNT_DESCRIPTION' => payanyway_getDescription($invoiceId, $params), 'MNT_SUBSCRIBER_ID' => $subscriberId, 'MNT_SIGNATURE' => $signature, 'MNT_CMS' => $whmcsVersion . '|' . PAYANYWAY_PAYMENT_GATEWAY_MODULE_VERSION, ]; $htmlOutput = '
'; foreach ($postFields as $key => $value) { $value = htmlspecialchars($value, ENT_QUOTES | ENT_HTML5, 'UTF-8'); $htmlOutput .= ''; } $htmlOutput .= ''; $htmlOutput .= '
'; return $htmlOutput; } /** * WHMCS PayAnyWay Payment Gateway Helper Functions */ /** * @param array $params * @return string * @throws PayAnyWayException */ function payanyway_getPayAnyWayMntIdOrFail($params) { $mntId = payanyway_getOptionalString($params, 'mnt_id'); if (null === $mntId) { throw new \PayAnyWayException( 'PayAnyWay module is not configured properly. ' . 'Please set Account Number in WHMCS Admin → Payment Gateways.' ); } return $mntId; } /** * @param array $params * @return string * @throws PayAnyWayException */ function payanyway_getPayAnyWayIntegrityCodeOrFail($params) { $integrityCode = payanyway_getOptionalString($params, 'mnt_dataintegrity_code'); if (null === $integrityCode) { throw new \PayAnyWayException( 'PayAnyWay module is not configured properly. ' . 'Please set Code of Data Integrity Verification in WHMCS Admin → Payment Gateways.' ); } return $integrityCode; } /** * @param array $params * @return int * @throws PayAnyWayException */ function payanyway_getWhmcsInvoiceIdOrFail($params) { $invoiceId = payanyway_getOptionalInt($params, 'invoiceid'); if (null === $invoiceId) { throw new \PayAnyWayException('Missing or invalid invoice ID (integer required)'); } return $invoiceId; } /** * @param array $params * @return float * @throws PayAnyWayException */ function payanyway_getWhmcsInvoiceAmountOrFail($params) { $amount = payanyway_getOptionalFloat($params, 'amount'); if (null === $amount) { throw new \PayAnyWayException('Missing or invalid invoice amount (float required).'); } return $amount; } /** * @param array $params * @return non-empty-string * @throws PayAnyWayException */ function payanyway_getWhmcsInvoiceCurrencyOrFail($params) { $currency = payanyway_getOptionalString($params, 'currency'); if (null === $currency) { $supported = implode(', ', PAYANYWAY_SUPPORTED_CURRENCIES); throw new \PayAnyWayException( 'Missing currency code. Module supports only: ' . $supported ); } $currency = strtoupper($currency); if (!in_array($currency, PAYANYWAY_SUPPORTED_CURRENCIES, true)) { $supported = implode(', ', PAYANYWAY_SUPPORTED_CURRENCIES); throw new \PayAnyWayException( "Unsupported currency code: '{$currency}'. Module supports only: {$supported}" ); } return $currency; } /** * @param array $params * @return string * @throws PayAnyWayException */ function payanyway_getWhmcsVariableOrFail($params, $name) { $var = payanyway_getOptionalString($params, $name); if (null === $var) { throw new \PayAnyWayException(sprintf('Whmcs variable $params[\'%s\'] is not set.', $name)); } return $var; } /** * @param array $array * @param string $key * @return int|null */ function payanyway_getOptionalInt($array, $key) { if (!isset($array[$key]) || !is_numeric($array[$key])) { return null; } return (int)$array[$key]; } /** * @param array $array * @param string $key * @return float|null */ function payanyway_getOptionalFloat($array, $key) { if (!isset($array[$key]) || !is_numeric($array[$key])) { return null; } return (float)$array[$key]; } /** * @param array $array * @param string $key * @return float * @throws PayAnyWayException */ function payanyway_getRequiredFloat($array, $key) { if (!isset($array[$key])) { throw new \PayAnyWayException('Missing required field ' . $key); } $value = $array[$key]; if (!is_numeric($value)) { throw new \PayAnyWayException( sprintf('Field "%s" must be a number, %s given', $key, gettype($value)) ); } return (float)$value; } /** * @param array $array * @param string $key * @return string * @throws PayAnyWayException */ function payanyway_getRequiredString($array, $key) { if (!isset($array[$key])) { throw new \PayAnyWayException('Missing required field ' . $key); } $value = $array[$key]; if (!is_string($value)) { throw new \PayAnyWayException( sprintf('Field "%s" must be a string, %s given', $key, gettype($value)) ); } if ('' === $value) { throw new \PayAnyWayException(sprintf('Field "%s" must be a non-empty string', $key)); } return $value; } /** * @param array $array * @param string $key * @return string|null */ function payanyway_getOptionalString($array, $key) { if (!isset($array[$key])) { return null; } $value = $array[$key]; return (is_string($value) && ('' !== trim($value))) ? $value : null; } /** * @param array $params * @param non-empty-string $key * @param string $default * @return string */ function payanyway_getOptionalStringWithDefault($params, $key, $default = '') { $value = payanyway_getOptionalString($params, $key); return (null !== $value) ? $value : $default; } /** * @param float $amount * @return non-empty-string */ function payanyway_formatAmount($amount) { return number_format($amount, 2, '.', ''); } /** * @param array $params * @return string */ function payanyway_getSubscriberId($params) { $email = isset($params['clientdetails']['email']) ? filter_var($params['clientdetails']['email'], FILTER_SANITIZE_EMAIL) : ''; $phoneNumber = isset($params['clientdetails']['phonenumber']) ? preg_replace('/[^0-9+]/', '', $params['clientdetails']['phonenumber']) : ''; return $email ?: $phoneNumber; } /** * @param int $invoiceId * @param array $params * @return non-empty-string */ function payanyway_getDescription($invoiceId, $params) { $clientFirstName = isset($params['clientdetails']['firstname']) ? $params['clientdetails']['firstname'] : ''; $clientLastName = isset($params['clientdetails']['lastname']) ? $params['clientdetails']['lastname'] : ''; $clientName = trim($clientFirstName . ' ' . $clientLastName); $description = "Payment for Invoice #{$invoiceId}"; if ('' !== $clientName) { $description .= " from {$clientName}"; } $originalDescription = isset($params['description']) ? trim($params['description']) : ''; if ('' !== $originalDescription) { $description .= ': ' . $originalDescription; } if (mb_strlen($description, 'UTF-8') > PAYANYWAY_DESCRIPTION_MAX_LENGTH) { $description = mb_substr($description, 0, 497, 'UTF-8') . '...'; } return $description; } /** * @param array $requestData * @return array * @throws PayAnyWayException */ function payanyway_getCallbackData($requestData) { return [ 'MNT_COMMAND' => payanyway_getOptionalStringWithDefault($requestData, 'MNT_COMMAND'), 'MNT_ID' => payanyway_getRequiredString($requestData, 'MNT_ID'), 'MNT_TRANSACTION_ID' => payanyway_getRequiredString($requestData, 'MNT_TRANSACTION_ID'), 'MNT_OPERATION_ID' => payanyway_getOptionalStringWithDefault($requestData, 'MNT_OPERATION_ID'), 'MNT_AMOUNT' => payanyway_getOptionalStringWithDefault($requestData, 'MNT_AMOUNT'), 'MNT_CURRENCY_CODE' => payanyway_getRequiredString($requestData, 'MNT_CURRENCY_CODE'), 'MNT_SUBSCRIBER_ID' => payanyway_getOptionalStringWithDefault($requestData, 'MNT_SUBSCRIBER_ID'), 'MNT_TEST_MODE' => payanyway_getRequiredString($requestData, 'MNT_TEST_MODE'), 'MNT_SIGNATURE' => payanyway_getRequiredString($requestData, 'MNT_SIGNATURE'), ]; } /** * @param array $callbackData * @param non-empty-string $gatewayName * @param non-empty-string $integrityCode * @return void * @throws DOMException */ function payanyway_handleCheckCallback($callbackData, $gatewayName, $integrityCode) { if (!payanyway_checkSignature($callbackData, $integrityCode, $callbackData['MNT_COMMAND'])) { payanyway_sendResponse('FAIL', 500); } $rawInvoiceId = (int)$callbackData['MNT_TRANSACTION_ID']; $invoiceId = checkCbInvoiceID($rawInvoiceId, $gatewayName); $invoiceDetails = payanyway_getInvoiceDetails($invoiceId); if (null === $invoiceDetails) { payanyway_sendResponse('FAIL', 500); } $invoiceItems = payanyway_getInvoiceItems($invoiceDetails); $invoiceTotal = payanyway_formatAmount($invoiceDetails['total']); $invoicePaymentStatus = $invoiceDetails['status']; $xmlData = [ 'MNT_ID' => $callbackData['MNT_ID'], 'MNT_TRANSACTION_ID' => $callbackData['MNT_TRANSACTION_ID'], 'MNT_AMOUNT' => $invoiceTotal, 'MNT_CURRENCY_CODE' => $callbackData['MNT_CURRENCY_CODE'], 'inventory' => payanyway_getInventoryJson($invoiceItems) ?: null, 'client' => $callbackData['MNT_SUBSCRIBER_ID'], 'sno' => null, 'delivery' => $invoiceTotal, ]; switch (true) { case empty($callbackData['MNT_AMOUNT']): $xmlData['MNT_RESULT_CODE'] = 100; $xmlData['MNT_DESCRIPTION'] = 'Invoice created, but amount not set'; break; case ('Paid' === $invoicePaymentStatus): $xmlData['MNT_RESULT_CODE'] = 200; $xmlData['MNT_DESCRIPTION'] = 'Invoice Paid'; break; default: $xmlData['MNT_RESULT_CODE'] = 402; $xmlData['MNT_DESCRIPTION'] = 'Invoice Unpaid'; } $xmlData['MNT_SIGNATURE'] = md5( $xmlData['MNT_RESULT_CODE'] . $xmlData['MNT_ID'] . $xmlData['MNT_TRANSACTION_ID'] . $integrityCode ); payanyway_sendResponse(payanyway_buildXMLResponse($xmlData), 200); } /** * @param array $callbackData * @param non-empty-string $gatewayName * @param non-empty-string $integrityCode * @param non-empty-string $gatewayModuleName * @return void * @throws DOMException */ function payanyway_handlePayCallback($callbackData, $gatewayName, $integrityCode, $gatewayModuleName) { if (!payanyway_checkSignature($callbackData, $integrityCode)) { payanyway_sendResponse('FAIL', 500); } $rawInvoiceId = $callbackData['MNT_TRANSACTION_ID']; $invoiceId = checkCbInvoiceID($rawInvoiceId, $gatewayName); $paidAmount = $callbackData['MNT_AMOUNT']; $invoiceDetails = payanyway_getInvoiceDetailsOrFail($invoiceId); if (null === $invoiceDetails) { payanyway_sendResponse('FAIL', 500); } $invoiceItems = payanyway_getInvoiceItems($invoiceDetails); $invoicePaymentStatus = $invoiceDetails['status']; $resultCode = 200; $xmlData = [ 'MNT_ID' => $callbackData['MNT_ID'], 'MNT_TRANSACTION_ID' => $callbackData['MNT_TRANSACTION_ID'], 'MNT_RESULT_CODE' => $resultCode, 'MNT_SIGNATURE' => md5( $resultCode . $callbackData['MNT_ID'] . $callbackData['MNT_TRANSACTION_ID'] . $integrityCode ), 'MNT_AMOUNT' => $paidAmount, 'MNT_CURRENCY_CODE' => $callbackData['MNT_CURRENCY_CODE'], 'MNT_DESCRIPTION' => ('Paid' !== $invoicePaymentStatus) ? 'Invoice success paid' : 'Invoice already paid', 'inventory' => payanyway_getInventoryJson($invoiceItems), 'client' => $callbackData['MNT_SUBSCRIBER_ID'], 'sno' => null, 'delivery' => $paidAmount, ]; if ('Paid' !== $invoicePaymentStatus) { $transactionId = $callbackData['MNT_OPERATION_ID']; checkCbTransID($transactionId); // if $transactionId exist, script die() if (!payanyway_existInvoiceTransaction($invoiceId, $transactionId)) { addInvoicePayment($invoiceId, $transactionId, $paidAmount, PAYANYWAY_PAYMENT_FEE, $gatewayModuleName); logTransaction($gatewayName, $callbackData, 'Success'); } } payanyway_sendResponse(payanyway_buildXMLResponse($xmlData), 200); } /** * @param array $callbackData * @param string $integrityCode * @param non-empty-string|null $command * @return bool */ function payanyway_checkSignature($callbackData, $integrityCode, $command = null) { $baseFields = [ 'MNT_ID', 'MNT_TRANSACTION_ID', 'MNT_OPERATION_ID', 'MNT_AMOUNT', 'MNT_CURRENCY_CODE', 'MNT_SUBSCRIBER_ID', 'MNT_TEST_MODE', ]; $fields = ('CHECK' === $command) ? array_merge(['MNT_COMMAND'], $baseFields) : $baseFields; $signatureString = array_reduce($fields, static function ($carry, $field) use ($callbackData) { return $carry . $callbackData[$field]; }, '') . $integrityCode; return hash_equals(md5($signatureString), $callbackData['MNT_SIGNATURE']); } /** * @see https://developers.whmcs.com/api-reference/getinvoice/ * @param int $invoiceId * @return array|null */ function payanyway_getInvoiceDetails($invoiceId) { $command = 'GetInvoice'; $postData = [ 'invoiceid' => $invoiceId, ]; $results = localAPI($command, $postData); if ($results['result'] !== 'success') { return null; } return $results; } /** * @param array $invoiceDetails * @return array|null */ function payanyway_getInvoiceItems($invoiceDetails) { return (isset($invoiceDetails['items']['item']) && is_array($invoiceDetails['items']['item'])) ? $invoiceDetails['items']['item'] : null; } /** * @see https://developers.whmcs.com/api-reference/gettransactions/ * @param int $invoiceId * @param non-empty-string $transactionId * @return bool */ function payanyway_existInvoiceTransaction($invoiceId, $transactionId) { $results = localAPI('GetTransactions', [ 'invoiceid' => $invoiceId, 'transid' => $transactionId, ]); if ($results['result'] !== 'success') { return false; } $transactions = isset($results['transactions']['transaction']) ? $results['transactions']['transaction'] : []; if ([] === $transactions) { return false; } foreach ($transactions as $transaction) { if (isset($transaction['transid']) && ($transaction['transid'] === $transactionId)) { return true; } } return false; } /** * @param array $items * @return false|string */ function payanyway_getInventoryJson($items) { $inventory = []; foreach ($items as $item) { $description = html_entity_decode($item['description'], ENT_QUOTES | ENT_HTML5, 'UTF-8'); $positionName = trim(preg_replace('/[^\p{L}\p{N}\s\-]/u', '', $description)); if (empty($positionName)) { $positionName = 'Service'; } $inventory[] = [ 'name' => mb_substr($positionName, 0, PAYANYWAY_INVENTORY_ITEM_NAME_MAX_LENGTH, 'UTF-8'), 'price' => (float)payanyway_formatAmount($item['amount']), 'quantity' => PAYANYWAY_INVENTORY_ITEM_DEFAULT_QUANTITY, 'vatTag' => PAYANYWAY_VAT_TAG_NONE, 'pm' => PAYANYWAY_INVENTORY_ITEM_DEFAULT_PAYMENT_METHOD, 'po' => PAYANYWAY_INVENTORY_ITEM_DEFAULT_PAYMENT_OBJECT, 'measure' => PAYANYWAY_INVENTORY_ITEM_DEFAULT_MEASURE, ]; } return json_encode($inventory, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } /** * @param array $data * @return false|string * @throws DOMException */ function payanyway_buildXMLResponse($data) { $dom = new DOMDocument('1.0', 'UTF-8'); $dom->formatOutput = false; $root = $dom->createElement('MNT_RESPONSE'); $dom->appendChild($root); $requiredFields = [ 'MNT_ID', 'MNT_TRANSACTION_ID', 'MNT_RESULT_CODE', 'MNT_DESCRIPTION', 'MNT_AMOUNT', 'MNT_CURRENCY_CODE', 'MNT_SIGNATURE', ]; foreach ($requiredFields as $field) { if (isset($data[$field])) { $element = $dom->createElement($field, $data[$field]); $root->appendChild($element); } } $attributes = $dom->createElement('MNT_ATTRIBUTES'); $root->appendChild($attributes); $addAttribute = static function ($key, $value) use ($dom, $attributes) { if (!empty($value)) { $attrElement = $dom->createElement('ATTRIBUTE'); $keyElement = $dom->createElement('KEY', $key); $valueElement = $dom->createElement('VALUE', $value); $attrElement->appendChild($keyElement); $attrElement->appendChild($valueElement); $attributes->appendChild($attrElement); } }; $addAttribute('INVENTORY', json_encode($data['inventory'], JSON_UNESCAPED_UNICODE)); $addAttribute('CLIENT', $data['client']); if (!empty($data['sno'])) { $addAttribute('SNO', $data['sno']); } if (!empty($data['delivery'])) { $addAttribute('DELIVERY', $data['delivery']); } return $dom->saveXML(); } /** * @param string $response * @param int $statusCode * @return void */ function payanyway_sendResponse($response, $statusCode) { http_response_code($statusCode); if (strpos($response, '