*/ function payanyway_MetaData() { return [ 'DisplayName' => '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(array $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); // System Parameters $langPayNow = payanyway_getOptionalStringWithDefault($params, 'langpaynow', 'Pay Now'); $whmcsVersion = payanyway_getOptionalString($params, 'whmcsVersion'); $url = ('1' === $testMode) ? PAYMENT_URL_DEMO : PAYMENT_URL_PROD; $subscriberId = payanyway_getSubscriberId($params); $signature = md5($mntId . $invoiceId . $formatAmount . $currency . $subscriberId . $testMode . $integrityCode); $postFields = array( 'MNT_ID' => $mntId, 'MNT_TRANSACTION_ID' => payanyway_createTransactionId($invoiceId), 'MNT_AMOUNT' => $formatAmount, 'MNT_CURRENCY_CODE' => $currency, 'MNT_TEST_MODE' => '0', 'MNT_DESCRIPTION' => payanyway_getDescription($invoiceId, $params), 'MNT_SUBSCRIBER_ID' => $subscriberId, 'MNT_SIGNATURE' => $signature, 'MNT_CMS' => payanyway_getCmsModuleVersion($whmcsVersion), ); $htmlOutput = '
'; foreach ($postFields as $key => $value) { $value = htmlspecialchars($value, ENT_QUOTES | ENT_HTML5, 'UTF-8'); $htmlOutput .= ''; } $htmlOutput .= ''; $htmlOutput .= '
'; return $htmlOutput; } catch (\Exception $e) { return '

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

'; } } /** * WHMCS PayAnyWay Payment Gateway Helper Functions */ /** * @param array $data * @return int * @throws \InvalidArgumentException */ function payanyway_getPayAnyWayMntIdOrFail(array $data) { $mntId = payanyway_getOptionalInt($data, 'mnt_id'); if (null === $mntId) { throw new \InvalidArgumentException( 'PayAnyWay module is not configured properly. ' . 'Please set Account Number in WHMCS Admin → Payment Gateways.' ); } return $mntId; } /** * @param array $data * @return string * @throws \InvalidArgumentException */ function payanyway_getPayAnyWayIntegrityCodeOrFail(array $data) { $integrityCode = payanyway_getOptionalString($data, 'mnt_dataintegrity_code'); if (null === $integrityCode) { throw new \InvalidArgumentException( 'PayAnyWay module is not configured properly. ' . 'Please set Code of Data Integrity Verification in WHMCS Admin → Payment Gateways.' ); } return $integrityCode; } /** * @param array $data * @return int * @throws \InvalidArgumentException */ function payanyway_getWhmcsInvoiceIdOrFail(array $data) { $invoiceId = payanyway_getOptionalInt($data, 'invoiceid'); if (null === $invoiceId) { throw new \InvalidArgumentException('Missing or invalid invoice ID (integer required)'); } return $invoiceId; } /** * @param array $data * @return float * @throws \InvalidArgumentException */ function payanyway_getWhmcsInvoiceAmountOrFail(array $data) { $amount = payanyway_getOptionalFloat($data, 'amount'); if (null === $amount) { throw new \InvalidArgumentException('Missing or invalid invoice amount (float required).'); } return $amount; } /** * @param array $data * @return string * @throws \InvalidArgumentException */ function payanyway_getWhmcsInvoiceCurrencyOrFail(array $data) { $supportedCurrencies = array('RUB'); $currency = payanyway_getOptionalString($data, 'currency'); if (null === $currency) { $supported = implode(', ', $supportedCurrencies); throw new \InvalidArgumentException( 'Missing currency code. Module supports only: ' . $supported ); } $currency = strtoupper($currency); if (!in_array($currency, $supportedCurrencies, true)) { $supported = implode(', ', $supportedCurrencies); throw new \InvalidArgumentException( "Unsupported currency code: '{$currency}'. Module supports only: {$supported}" ); } return $currency; } /** * @param array $data * @param string $name * @return string * @throws \InvalidArgumentException */ function payanyway_getWhmcsVariableOrFail(array $data, $name) { $var = payanyway_getOptionalString($data, $name); if (null === $var) { throw new \InvalidArgumentException(sprintf('WHMCS variable $params[\'%s\'] is not set.', $name)); } return $var; } /** * @param array $data * @param string $key * @return int|null */ function payanyway_getOptionalInt(array $data, $key) { if (!isset($data[$key]) || !is_numeric($data[$key])) { return null; } return (int)$data[$key]; } /** * @param array $data * @param string $key * @return float|null */ function payanyway_getOptionalFloat(array $data, $key) { if (!isset($data[$key]) || !is_numeric($data[$key])) { return null; } return (float)$data[$key]; } /** * @param array $data * @param string $key * @return float * @throws \InvalidArgumentException */ function payanyway_getRequiredFloat(array $data, $key) { if (!isset($data[$key])) { throw new \InvalidArgumentException('Missing required field ' . $key); } $value = $data[$key]; if (!is_numeric($value)) { throw new \InvalidArgumentException( sprintf('Field "%s" must be a number, %s given', $key, gettype($value)) ); } return (float)$value; } /** * @param array $data * @param string $key * @return string * @throws \InvalidArgumentException */ function payanyway_getRequiredString(array $data, $key) { if (!isset($data[$key])) { throw new \InvalidArgumentException('Missing required field ' . $key); } $value = $data[$key]; if (!is_string($value)) { throw new \InvalidArgumentException( sprintf('Field "%s" must be a string, %s given', $key, gettype($value)) ); } if ('' === $value) { throw new \InvalidArgumentException(sprintf('Field "%s" must be a non-empty string', $key)); } return $value; } /** * @param array $data * @param string $key * @return string|null */ function payanyway_getOptionalString(array $data, $key) { if (!isset($data[$key])) { return null; } $value = $data[$key]; return (is_string($value) && ('' !== trim($value))) ? $value : null; } /** * @param array $data * @param string $key * @param string $default * @return string */ function payanyway_getOptionalStringWithDefault(array $data, $key, $default = '') { $value = payanyway_getOptionalString($data, $key); return (null !== $value) ? $value : $default; } /** * @param float $amount * @return string */ function payanyway_formatAmount($amount) { return number_format($amount, 2, '.', ''); } /** * @param array $data * @return string */ function payanyway_getSubscriberId(array $data) { $email = isset($data['clientdetails']['email']) ? filter_var($data['clientdetails']['email'], FILTER_SANITIZE_EMAIL) : ''; $phoneNumber = isset($data['clientdetails']['phonenumber']) ? preg_replace('/[^0-9+]/', '', $data['clientdetails']['phonenumber']) : ''; return $email ?: $phoneNumber; } /** * @param int $invoiceId * @return string */ function payanyway_createTransactionId($invoiceId) { $date = date('YmdHis'); return $invoiceId . TRANSACTION_ID_STRING_DELIMITER . $date; } /** * @param string $transactionId * @return int|null */ function payanyway_getInvoiceIdFromTransactionId($transactionId) { $transactionIdData = explode(TRANSACTION_ID_STRING_DELIMITER, $transactionId, 2); return isset($transactionIdData[0]) ? (int)$transactionIdData[0] : null; } /** * @param int $invoiceId * @param array $params * @return string */ function payanyway_getDescription($invoiceId, array $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']) ? $params['description'] : ''; if ('' !== $originalDescription) { $description .= ': ' . htmlspecialchars($originalDescription, ENT_QUOTES, 'UTF-8'); } return payanyway_validateString($description, DESCRIPTION_MAX_LENGTH); } /** * @param array $requestData * @return array */ function payanyway_getCallbackData(array $requestData) { return array( '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 array $gatewayParams * @return void * @throws \Exception */ function payanyway_handleCheckCallback(array $callbackData, array $gatewayParams) { $integrityCode = payanyway_getPayAnyWayIntegrityCodeOrFail($gatewayParams); if (!payanyway_checkSignature($callbackData, $callbackData['MNT_SIGNATURE'], $integrityCode)) { payanyway_sendResponse('FAIL'); } $gatewayName = payanyway_getWhmcsVariableOrFail($gatewayParams, 'name'); $rawInvoiceId = payanyway_getInvoiceIdFromTransactionId($callbackData['MNT_TRANSACTION_ID']); if (null === $rawInvoiceId) { payanyway_sendResponse('FAIL'); } $invoiceId = checkCbInvoiceID($rawInvoiceId, $gatewayName); $invoiceDetails = payanyway_getInvoiceDetails($invoiceId); if (null === $invoiceDetails) { payanyway_sendResponse('FAIL'); } $invoiceItems = payanyway_getInvoiceItems($invoiceDetails); $invoicePaymentStatus = (string)$invoiceDetails['status']; $xmlData = array( 'MNT_ID' => payanyway_getPayAnyWayMntIdOrFail($gatewayParams), 'MNT_TRANSACTION_ID' => $callbackData['MNT_TRANSACTION_ID'], 'MNT_AMOUNT' => payanyway_formatAmount($invoiceDetails['total']), 'MNT_CURRENCY_CODE' => $callbackData['MNT_CURRENCY_CODE'], 'inventory' => payanyway_getInventoryJson($invoiceItems) ?: null, 'client' => $callbackData['MNT_SUBSCRIBER_ID'], 'sno' => null, 'delivery' => null, ); list($xmlData['MNT_RESULT_CODE'], $xmlData['MNT_DESCRIPTION']) = payanyway_determineCheckResult($callbackData, $invoicePaymentStatus); $xmlData['MNT_SIGNATURE'] = md5( $xmlData['MNT_RESULT_CODE'] . $xmlData['MNT_ID'] . $xmlData['MNT_TRANSACTION_ID'] . payanyway_getPayAnyWayIntegrityCodeOrFail($gatewayParams) ); payanyway_sendResponse(payanyway_buildXMLResponse($xmlData)); } /** * @param array $callbackData * @param string $invoicePaymentStatus * @return array * @throws \Exception */ function payanyway_determineCheckResult(array $callbackData, $invoicePaymentStatus) { if (empty($callbackData['MNT_AMOUNT'])) { return array(100, "Invoice status is '{$invoicePaymentStatus}'"); } if ('Paid' === $invoicePaymentStatus) { return array(200, 'Invoice Paid'); } return array(402, "Invoice status is '{$invoicePaymentStatus}'"); } /** * @param array $callbackData * @param array $gatewayParams * @param string $gatewayModuleName * @return void * @throws \Exception */ function payanyway_handlePayCallback(array $callbackData, array $gatewayParams, $gatewayModuleName) { $gatewayName = payanyway_getWhmcsVariableOrFail($gatewayParams, 'name'); $rawInvoiceId = payanyway_getInvoiceIdFromTransactionId($callbackData['MNT_TRANSACTION_ID']); if (null === $rawInvoiceId) { payanyway_sendResponse('FAIL'); } $invoiceId = checkCbInvoiceID($rawInvoiceId, $gatewayName); $invoiceDetails = payanyway_getInvoiceDetails($invoiceId); if (null === $invoiceDetails) { payanyway_sendResponse('FAIL'); } $invoiceTotal = payanyway_formatAmount($invoiceDetails['total']); $shopData = array( 'MNT_ID' => payanyway_getPayAnyWayMntIdOrFail($gatewayParams), 'MNT_TRANSACTION_ID' => $callbackData['MNT_TRANSACTION_ID'], 'MNT_OPERATION_ID' => $callbackData['MNT_OPERATION_ID'], 'MNT_AMOUNT' => $invoiceTotal, 'MNT_CURRENCY_CODE' => $callbackData['MNT_CURRENCY_CODE'], 'MNT_SUBSCRIBER_ID' => $callbackData['MNT_SUBSCRIBER_ID'], 'MNT_TEST_MODE' => $callbackData['MNT_TEST_MODE'], ); $integrityCode = payanyway_getPayAnyWayIntegrityCodeOrFail($gatewayParams); if (!payanyway_checkSignature($shopData, $callbackData['MNT_SIGNATURE'], $integrityCode)) { payanyway_sendResponse('FAIL'); } $invoiceItems = payanyway_getInvoiceItems($invoiceDetails); $invoicePaymentStatus = (string)$invoiceDetails['status']; $mntId = payanyway_getPayAnyWayMntIdOrFail($gatewayParams); $resultCode = 200; $xmlData = array( 'MNT_ID' => $mntId, 'MNT_TRANSACTION_ID' => $callbackData['MNT_TRANSACTION_ID'], 'MNT_RESULT_CODE' => $resultCode, 'MNT_SIGNATURE' => md5($resultCode . $mntId . $callbackData['MNT_TRANSACTION_ID'] . $integrityCode), 'MNT_AMOUNT' => $invoiceTotal, 'MNT_CURRENCY_CODE' => $callbackData['MNT_CURRENCY_CODE'], 'MNT_DESCRIPTION' => ('Paid' !== $invoicePaymentStatus) ? 'Invoice success paid' : 'Invoice already paid', 'inventory' => payanyway_getInventoryJson($invoiceItems) ?: null, 'client' => $callbackData['MNT_SUBSCRIBER_ID'], 'sno' => null, 'delivery' => null, ); if ('Paid' !== $invoicePaymentStatus) { $transactionId = $callbackData['MNT_OPERATION_ID']; checkCbTransID($transactionId); // if $transactionId exist, script die() if (!payanyway_existInvoiceTransaction($invoiceId, $transactionId)) { addInvoicePayment($invoiceId, $transactionId, $invoiceTotal, MODULE_PAYMENT_FEE, $gatewayModuleName); logTransaction($gatewayName, $callbackData, 'Success'); } } payanyway_sendResponse(payanyway_buildXMLResponse($xmlData)); } /** * @param array $data * @param string $callbackSignature * @param string $integrityCode * @return bool */ function payanyway_checkSignature(array $data, $callbackSignature, $integrityCode) { $baseFields = array( 'MNT_ID', 'MNT_TRANSACTION_ID', 'MNT_OPERATION_ID', 'MNT_AMOUNT', 'MNT_CURRENCY_CODE', 'MNT_SUBSCRIBER_ID', 'MNT_TEST_MODE', ); $fields = isset($data['MNT_COMMAND']) && ('CHECK' === $data['MNT_COMMAND']) ? array_merge(['MNT_COMMAND'], $baseFields) : $baseFields; $signatureString = array_reduce($fields, function ($carry, $field) use ($data) { return $carry . $data[$field]; }, '') . $integrityCode; return hash_equals(md5($signatureString), $callbackSignature); } /** * @see https://developers.whmcs.com/api-reference/getinvoice/ * @param int $invoiceId * @return array|null */ function payanyway_getInvoiceDetails($invoiceId) { $command = 'GetInvoice'; $postData = array( 'invoiceid' => $invoiceId, ); $results = localAPI($command, $postData); if ($results['result'] !== 'success') { return null; } return $results; } /** * @param array $invoiceDetails * @return array|null */ function payanyway_getInvoiceItems(array $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 string $transactionId * @return bool */ function payanyway_existInvoiceTransaction($invoiceId, $transactionId) { $results = localAPI('GetTransactions', array( 'invoiceid' => $invoiceId, 'transid' => $transactionId, )); if ($results['result'] !== 'success') { return false; } $transactions = isset($results['transactions']['transaction']) ? $results['transactions']['transaction'] : array(); if (empty($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(array $items) { $inventory = array(); foreach ($items as $item) { $name = isset($item['description']) ? $item['description'] : ''; $price = isset($item['amount']) ? (float)$item['amount'] : 0; $name = payanyway_validateString($name, INVENTORY_ITEM_NAME_MAX_LENGTH); $inventory[] = array( 'name' => !empty($name) ? $name : 'Service', 'price' => (float)payanyway_formatAmount($price), 'quantity' => INVENTORY_ITEM_DEFAULT_QUANTITY, 'vatTag' => INVENTORY_ITEM_DEFAULT_VAT_TAG, 'pm' => INVENTORY_ITEM_DEFAULT_PAYMENT_METHOD, 'po' => INVENTORY_ITEM_DEFAULT_PAYMENT_OBJECT, 'measure' => INVENTORY_ITEM_DEFAULT_MEASURE, ); } return json_encode($inventory, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } /** * @param array $data * @return false|string * @throws \Exception */ function payanyway_buildXMLResponse(array $data) { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->formatOutput = false; $root = $dom->createElement('MNT_RESPONSE'); $dom->appendChild($root); $requiredFields = array( '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]) && ($data[$field] !== '')) { $root->appendChild($dom->createElement($field, (string)$data[$field])); } } $attributes = $dom->createElement('MNT_ATTRIBUTES'); $root->appendChild($attributes); $attributeMap = array( 'INVENTORY' => isset($data['inventory']) ? $data['inventory'] : null, 'CLIENT' => isset($data['client']) ? $data['client'] : null, 'SNO' => isset($data['sno']) ? $data['sno'] : null, 'DELIVERY' => isset($data['delivery']) ? $data['delivery'] : null, ); foreach ($attributeMap as $key => $value) { if (!empty($value)) { $attrElement = $dom->createElement('ATTRIBUTE'); $attrElement->appendChild($dom->createElement('KEY', $key)); $attrElement->appendChild($dom->createElement('VALUE', (string)$value)); $attributes->appendChild($attrElement); } } return $dom->saveXML(); } /** * @param string|null $whmcsVersion * @return string */ function payanyway_getCmsModuleVersion($whmcsVersion) { $whmcsVersion = isset($whmcsVersion) ? $whmcsVersion : '.unknown'; return sprintf('%s v%s | %s v%s', CMS_NAME, $whmcsVersion, MODULE_NAME, MODULE_VERSION); } /** * @param string $value * @param int $maxLength * @return string */ function payanyway_validateString($value, $maxLength) { $maxLength = max(0, $maxLength); $value = payanyway_sanitizeString($value); if (mb_strlen($value, 'UTF-8') <= $maxLength) { return $value; } $trimLength = max(0, $maxLength - 3); return mb_substr($value, 0, $trimLength, 'UTF-8') . ($maxLength > 3 ? '...' : ''); } /** * @param string $value * @return string */ function payanyway_sanitizeString($value) { $decoded = html_entity_decode($value, ENT_QUOTES | ENT_HTML5, 'UTF-8'); $cleaned = preg_replace('/[^\p{L}\p{N}\s.,()_№+-]/u', '', $decoded); $cleaned = str_replace(['&', '/', '\\', ';', '%', '#', '"', "'"], '', $cleaned); return trim(preg_replace('/\s+/', ' ', $cleaned)); } /** * @param string $response * @param int $statusCode * @return void */ function payanyway_sendResponse($response, $statusCode = 200) { http_response_code($statusCode); if (strpos($response, '