From 03c97992d8fc0c04e1bd2074ae6d4e8bc7ff56a9 Mon Sep 17 00:00:00 2001 From: SteinCode Date: Tue, 15 Jul 2025 13:13:55 +0300 Subject: [PATCH 1/7] adapt new callback functionality --- README.md | 6 +- src/Controller/SpectroCoinController.php | 173 +++++++++--- .../Commerce/PaymentGateway/SpectroCoin.php | 8 +- src/SCMerchantClient/SCMerchantClient.php | 260 ++++++++++-------- .../data/SpectroCoin_OldOrderCallback.php | 157 +++++++++++ .../data/SpectroCoin_OrderCallback.php | 134 ++------- .../data/SpectroCoin_OrderStatusEnum.php | 47 +++- 7 files changed, 501 insertions(+), 284 deletions(-) create mode 100755 src/SCMerchantClient/data/SpectroCoin_OldOrderCallback.php diff --git a/README.md b/README.md index e9ed3d6..6fa08c0 100755 --- a/README.md +++ b/README.md @@ -81,7 +81,11 @@ Order callbacks in the SpectroCoin plugin allow your WordPress site to automatic ## Changelog -### 1.0.0 MAJOR (27/02/2025): +### 1.1.0 (): + +_Added_ support for new JSON and old URL-encoded form data callbacks format. New callbacks will be automatically enabled with new SpectroCoin merchant projects created. With old projects, old callback format will be used. In the future versions old callback format will be removed. + +### 1.0.0 (27/02/2025): This major update introduces several improvements, including enhanced security, updated coding standards, and a streamlined integration process. **Important:** Users must generate new API credentials (Client ID and Client Secret) in their SpectroCoin account settings to continue using the plugin. The previous private key and merchant ID functionality have been deprecated. diff --git a/src/Controller/SpectroCoinController.php b/src/Controller/SpectroCoinController.php index 4e823aa..c583831 100755 --- a/src/Controller/SpectroCoinController.php +++ b/src/Controller/SpectroCoinController.php @@ -1,8 +1,12 @@ getMethod() !== 'POST') { \Drupal::logger('commerce_spectrocoin') @@ -31,24 +37,46 @@ public function callback() { return new Response('Invalid request method', 405); } try { - $order_callback = $this->initCallbackFromPost(); - if (!$order_callback) { - \Drupal::logger('commerce_spectrocoin') - ->error('SpectroCoin Error: No data received in callback'); - return new Response('Invalid callback data', 400); + $contentType = $_SERVER['CONTENT_TYPE'] ?? ''; + if (stripos($contentType, 'application/json') !== false) { + // if new callback + $order_callback = $this->initCallbackFromJson(); + if (! $order_callback) { + throw new InvalidArgumentException('Invalid JSON callback payload'); + } + $sc_merchant_client = new SCMerchantClient( + $this->configuration['project_id'], + $this->configuration['client_id'], + $this->configuration['client_secret'] + ); + + $order_data = $sc_merchant_client->getOrderById($order_callback->getUuid()); + + if (! is_array($order_data) || empty($order_data['orderId']) || empty($order_data['status'])) { + throw new InvalidArgumentException('Malformed order data from API'); + } + + $raw_status = $order_data['status']; + $raw_order_id = $order_data['orderId']; + } else { + // if legacy callback + $order_callback = $this->initCallbackFromPost(); + $raw_status = $order_callback->getStatus(); + $raw_order_id = $order_callback->getOrderId(); } - - $combined = $order_callback->getOrderId(); - \Drupal::logger('commerce_spectrocoin')->debug('Combined order/payment ID: ' . $combined); - - list($order_id, $payment_id) = explode('-', $combined); - + + list($order_id, $payment_id) = explode('-', $raw_order_id); + if (!$order_id || !$payment_id) { \Drupal::logger('commerce_spectrocoin') ->error('SpectroCoin Error: Invalid combined order/payment ID.'); return new Response('Invalid order/payment id', 400); } - + if (!$order_callback) { + \Drupal::logger('commerce_spectrocoin') + ->error('SpectroCoin Error: No data received in callback'); + return new Response('Invalid callback data', 400); + } $order = Order::load($order_id); if (!$order) { \Drupal::logger('commerce_spectrocoin') @@ -56,19 +84,22 @@ public function callback() { return new Response('Order not found', 404); } - $status = strtolower($order_callback->getStatus()); - - switch ($status) { - case 5: - $order->set('state', 'expired'); + $statusEnum = SpectroCoin_OrderStatusEnum::normalize($raw_status); + switch ($statusEnum) { + case OrderStatus::NEW: + break; + case OrderStatus::PENDING: + break; + case OrderStatus::PAID: + $order->set('state', 'completed'); $order->set('cart', 0); break; - case 4: + case OrderStatus::FAILED: $order->set('state', 'canceled'); $order->set('cart', 0); break; - case 3: - $order->set('state', 'completed'); + case OrderStatus::EXPIRED: + $order->set('state', 'expired'); $order->set('cart', 0); break; default: @@ -78,14 +109,13 @@ public function callback() { } $order->save(); - if ($status == 3) { // completed + if ($statusEnum === SpectroCoin_OrderStatusEnum::PAID) { $payment_storage = \Drupal::entityTypeManager()->getStorage('commerce_payment'); $payment = $payment_storage->load($payment_id); if ($payment) { $payment->setState('completed'); $payment->save(); - } - else { + } else { \Drupal::logger('commerce_spectrocoin') ->error('Payment not found for payment ID: ' . $payment_id); } @@ -94,13 +124,11 @@ public function callback() { $response = new Response('*ok*', 200); $response->headers->set('Content-Type', 'text/plain'); return $response; - } - catch (InvalidArgumentException $e) { + } catch (InvalidArgumentException $e) { \Drupal::logger('commerce_spectrocoin') ->error("Error processing callback: " . $e->getMessage()); return new Response('Error processing callback', 400); - } - catch (Exception $e) { + } catch (Exception $e) { \Drupal::logger('commerce_spectrocoin') ->error("Error processing callback: " . $e->getMessage()); return new Response('Error processing callback', 500); @@ -108,7 +136,8 @@ public function callback() { } - public function success() { + public function success() + { $order_id = \Drupal::request()->query->get('order_id'); if ($order_id) { $order = Order::load((int) $order_id); @@ -121,20 +150,19 @@ public function success() { return new RedirectResponse('/'); } - public function failure() { + public function failure() + { $order_id = \Drupal::request()->query->get('order_id'); if ($order_id) { $order = Order::load((int) $order_id); if ($order) { $order->set('state', 'canceled'); $order->save(); - } - else { + } else { \Drupal::logger('commerce_spectrocoin') ->error('SpectroCoin Error: Invalid Order ID in failure callback.'); } - } - else { + } else { \Drupal::logger('commerce_spectrocoin') ->error('SpectroCoin Error: Order ID is not available in failure callback.'); } @@ -143,16 +171,38 @@ public function failure() { } /** - * Initializes the callback data from the POST request. + * Initializes the callback data from POST (form-encoded) request. + * + * Callback format processed by this method is URL-encoded form data. + * Example: merchantId=1387551&apiId=105548&userId=…&sign=… + * Content-Type: application/x-www-form-urlencoded + * These callbacks are being sent by old merchant projects. + * + * Extracts the expected fields from `$_POST`, validates the signature, + * and returns an `OldOrderCallback` instance wrapping that data. * - * @return \Drupal\commerce_spectrocoin\SCMerchantClient\data\SpectroCoin_OrderCallback|null - * The OrderCallback object if valid data was found, or NULL otherwise. + * @deprecated since v2.1.0 + * + * @return SpectroCoin_OldOrderCallback|null An `OldOrderCallback` if the POST body + * contained valid data; `null` otherwise. */ - private function initCallbackFromPost() { + private function initCallbackFromPost() + { $expected_keys = [ - 'userId', 'merchantApiId', 'merchantId', 'apiId', 'orderId', - 'payCurrency', 'payAmount', 'receiveCurrency', 'receiveAmount', - 'receivedAmount', 'description', 'orderRequestId', 'status', 'sign' + 'userId', + 'merchantApiId', + 'merchantId', + 'apiId', + 'orderId', + 'payCurrency', + 'payAmount', + 'receiveCurrency', + 'receiveAmount', + 'receivedAmount', + 'description', + 'orderRequestId', + 'status', + 'sign' ]; $callback_data = []; @@ -165,6 +215,43 @@ private function initCallbackFromPost() { \Drupal::logger('commerce_spectrocoin')->error("No data received in callback"); return null; } - return new SpectroCoin_OrderCallback($callback_data); + return new SpectroCoin_OldOrderCallback($callback_data); + } + + + /** + * Initializes the callback data from JSON request body. + * + * Reads the raw HTTP request body, decodes it as JSON, and returns + * an OrderCallback instance if the payload is valid. + * + * @return SpectroCoin_OrderCallback|null An OrderCallback if the JSON payload + * contained valid data; null if the body + * was empty. + * + * @throws \JsonException If the request body is not valid JSON. + * @throws \InvalidArgumentException If required fields are missing + * or validation fails in OrderCallback. + * + */ + private function initCallbackFromJson(): ?SpectroCoin_OrderCallback + { + $body = (string) \file_get_contents('php://input'); + if ($body === '') { + \Drupal::logger('commerce_spectrocoin')->error('Empty JSON callback payload'); + return null; + } + + $data = \json_decode($body, true, 512, JSON_THROW_ON_ERROR); + + if (!\is_array($data)) { + \Drupal::logger('commerce_spectrocoin')->error('JSON callback payload is not an object'); + return null; + } + + return new SpectroCoin_OrderCallback( + $data['id'] ?? null, + $data['merchantApiId'] ?? null + ); } } diff --git a/src/Plugin/Commerce/PaymentGateway/SpectroCoin.php b/src/Plugin/Commerce/PaymentGateway/SpectroCoin.php index 49bcd9f..0d7a90b 100755 --- a/src/Plugin/Commerce/PaymentGateway/SpectroCoin.php +++ b/src/Plugin/Commerce/PaymentGateway/SpectroCoin.php @@ -9,8 +9,6 @@ use Drupal\commerce_spectrocoin\SCMerchantClient\data\SpectroCoin_ApiError; use Drupal\commerce_spectrocoin\SCMerchantClient\SCMerchantClient; -define('API_URL', 'https://spectrocoin.com/api/public'); -define('AUTH_URL', 'https://spectrocoin.com/api/public/oauth/token'); /** * Provides the SpectroCoin payment gateway. @@ -126,7 +124,6 @@ public function createSpectroCoinInvoice(PaymentInterface $payment, array $extra 'commerce_order' => $order_id ], ['absolute' => TRUE, 'https' => TRUE])->toString(); - $locale = 'en'; $createOrderRequest = new SpectroCoin_CreateOrderRequest( $combined_order_id, $order_description, @@ -136,12 +133,9 @@ public function createSpectroCoinInvoice(PaymentInterface $payment, array $extra $pay_currency_code, $callback_url, $success_url, - $failure_url, - $locale + $failure_url ); $createOrderResponse = $client = (new SCMerchantClient( - AUTH_URL, - API_URL, $this->configuration['project_id'], $this->configuration['client_id'], $this->configuration['client_secret'] diff --git a/src/SCMerchantClient/SCMerchantClient.php b/src/SCMerchantClient/SCMerchantClient.php index c403d0c..7e155f7 100755 --- a/src/SCMerchantClient/SCMerchantClient.php +++ b/src/SCMerchantClient/SCMerchantClient.php @@ -13,11 +13,15 @@ use GuzzleHttp\RequestOptions; use Drupal\commerce_spectrocoin\SCMerchantClient\data\SpectroCoin_ApiError; -use Drupal\commerce_spectrocoin\SCMerchantClient\data\SpectroCoin_OrderCallback; +use Drupal\commerce_spectrocoin\SCMerchantClient\data\SpectroCoin_OldOrderCallback; use Drupal\commerce_spectrocoin\SCMerchantClient\messages\SpectroCoin_CreateOrderRequest; use Drupal\commerce_spectrocoin\SCMerchantClient\messages\SpectroCoin_CreateOrderResponse; use Drupal\commerce_spectrocoin\SCMerchantClient\components\SpectroCoin_Utilities; +define('API_URL', 'https://spectrocoin.com/api/public'); +define('AUTH_URL', 'https://spectrocoin.com/api/public/oauth/token'); +define('PUBLIC_CERT_LOCATION', 'https://spectrocoin.com/files/merchant.public.pem'); + class SCMerchantClient { @@ -27,7 +31,7 @@ class SCMerchantClient private $client_secret; private $auth_url; private $auth_encryption_key; - + private $access_token_data; private $public_spectrocoin_cert_location; protected $guzzle_client; @@ -41,15 +45,15 @@ class SCMerchantClient * @param $guzzle_client * @param $public_spectrocoin_cert_location */ - function __construct($auth_url, $merchant_api_url, $project_id, $client_id, $client_secret) + function __construct($project_id, $client_id, $client_secret) { - $this->auth_url = $auth_url; - $this->merchant_api_url = $merchant_api_url; + $this->auth_url = AUTH_URL; + $this->merchant_api_url = API_URL; $this->project_id = $project_id; $this->client_id = $client_id; $this->client_secret = $client_secret; $this->guzzle_client = new Client(); - $this->public_spectrocoin_cert_location = "https://spectrocoin.com/files/merchant.public.pem"; + $this->public_spectrocoin_cert_location = PUBLIC_CERT_LOCATION; $this->auth_encryption_key = $this->initializeEncryptionKey(); } @@ -66,11 +70,10 @@ function __construct($auth_url, $merchant_api_url, $project_id, $client_id, $cli public function spectrocoinCreateOrder(SpectroCoin_CreateOrderRequest $request) { $this->access_token_data = $this->spectrocoinGetAccessTokenData(); - + if (!$this->access_token_data) { return new SpectroCoin_ApiError('AuthError', 'Failed to obtain or refresh access token'); - } - else if ($this->access_token_data instanceof SpectroCoin_ApiError) { + } else if ($this->access_token_data instanceof SpectroCoin_ApiError) { return $this->access_token_data; } @@ -89,11 +92,11 @@ public function spectrocoinCreateOrder(SpectroCoin_CreateOrderRequest $request) $sanitized_payload = $this->spectrocoinSanitizeOrderPayload($payload); if (!$this->spectrocoinValidateOrderPayload($sanitized_payload)) { - return new SpectroCoin_ApiError(-1, 'Invalid order creation payload, payload: ' . json_encode($sanitized_payload)); + return new SpectroCoin_ApiError(-1, 'Invalid order creation payload, payload: ' . json_encode($sanitized_payload)); } $json_payload = json_encode($sanitized_payload); - try { + try { $response = $this->guzzle_client->request('POST', $this->merchant_api_url . '/merchants/orders/create', [ RequestOptions::HEADERS => [ 'Authorization' => 'Bearer ' . $this->access_token_data['access_token'], @@ -105,19 +108,18 @@ public function spectrocoinCreateOrder(SpectroCoin_CreateOrderRequest $request) $body = json_decode($response->getBody()->getContents(), true); return new SpectroCoin_CreateOrderResponse( - $body['preOrderId'], - $body['orderId'], - $body['validUntil'], - $body['payCurrencyCode'], - $body['payNetworkCode'], - $body['receiveCurrencyCode'], - $body['payAmount'], - $body['receiveAmount'], - $body['depositAddress'], - $body['memo'], - $body['redirectUrl'], + $body['preOrderId'], + $body['orderId'], + $body['validUntil'], + $body['payCurrencyCode'], + $body['payNetworkCode'], + $body['receiveCurrencyCode'], + $body['payAmount'], + $body['receiveAmount'], + $body['depositAddress'], + $body['memo'], + $body['redirectUrl'], ); - } catch (RequestException $e) { if ($e->getResponse() && $e->getResponse()->getStatusCode() == 403) { $this->access_token_data = $this->spectrocoinRefreshAccessToken(time()); @@ -135,24 +137,24 @@ public function spectrocoinCreateOrder(SpectroCoin_CreateOrderRequest $request) } return new SpectroCoin_ApiError('UnknownError', 'An unknown error occurred during order creation'); - } /** * Initializes the encryption key for the client. * If the key is not present in Drupal's configuration, generates a new one and stores it. */ - private function initializeEncryptionKey() { - $config = \Drupal::config('commerce_spectrocoin.settings'); - $this->auth_encryption_key = $config->get('auth_encryption_key'); - - if (empty($this->auth_encryption_key)) { - $this->auth_encryption_key = base64_encode(random_bytes(32)); - \Drupal::configFactory()->getEditable('commerce_spectrocoin.settings') - ->set('auth_encryption_key', $this->auth_encryption_key) - ->save(); - } - } - + private function initializeEncryptionKey() + { + $config = \Drupal::config('commerce_spectrocoin.settings'); + $this->auth_encryption_key = $config->get('auth_encryption_key'); + + if (empty($this->auth_encryption_key)) { + $this->auth_encryption_key = base64_encode(random_bytes(32)); + \Drupal::configFactory()->getEditable('commerce_spectrocoin.settings') + ->set('auth_encryption_key', $this->auth_encryption_key) + ->save(); + } + } + /** * Retries the order creation request with a refreshed token. * @@ -184,21 +186,21 @@ private function SpectrocoinRetryOrder($json_payload) $body['depositAddress'], $body['memo'], $body['redirectUrl'], - ); - + ); } catch (GuzzleException $e) { return new SpectroCoin_ApiError($e->getCode(), $e->getMessage()); } } /** - * Retrieves the current access token data from configuration. - * If the token is expired or not present, attempts to refresh it. - * - * @return array|null The access token data array if valid or successfully refreshed, null otherwise. - */ - private function spectrocoinGetAccessTokenData() { - $current_time = time(); + * Retrieves the current access token data from configuration. + * If the token is expired or not present, attempts to refresh it. + * + * @return array|null The access token data array if valid or successfully refreshed, null otherwise. + */ + private function spectrocoinGetAccessTokenData() + { + $current_time = time(); $encrypted_access_token_data = $this->retrieveEncryptedData(); if ($encrypted_access_token_data) { $decrypted_data = SpectroCoin_Utilities::spectrocoinDecryptAuthData($encrypted_access_token_data, $this->auth_encryption_key); @@ -207,8 +209,8 @@ private function spectrocoinGetAccessTokenData() { return $this->access_token_data; } } - return $this->spectrocoinRefreshAccessToken($current_time); - } + return $this->spectrocoinRefreshAccessToken($current_time); + } /** * Refreshes the access token by making a request to the SpectroCoin authorization server using client credentials. If successful, it updates the stored token data in WordPress transients. @@ -218,7 +220,8 @@ private function spectrocoinGetAccessTokenData() { * @return array|null Returns the new access token data if the refresh operation is successful. Returns null if the operation fails due to a network error or invalid response from the server. * @throws GuzzleException Thrown if there is an error in the HTTP request to the SpectroCoin authorization server. */ - private function spectrocoinRefreshAccessToken($current_time) { + private function spectrocoinRefreshAccessToken($current_time) + { try { $response = $this->guzzle_client->post($this->auth_url, [ 'form_params' => [ @@ -227,15 +230,15 @@ private function spectrocoinRefreshAccessToken($current_time) { 'client_secret' => $this->client_secret, ], ]); - + $data = json_decode($response->getBody(), true); if (!isset($data['access_token'], $data['expires_in'])) { return new SpectroCoin_ApiError('Invalid access token response', 'No valid response received.'); } - + $data['expires_at'] = $current_time + $data['expires_in']; $encrypted_access_token_data = SpectroCoin_Utilities::spectrocoinEncryptAuthData(json_encode($data), $this->auth_encryption_key); - + $this->storeEncryptedData($encrypted_access_token_data); $this->access_token_data = $data; @@ -252,7 +255,8 @@ private function spectrocoinRefreshAccessToken($current_time) { * @param int $currentTime The current timestamp, typically obtained using `time()`. * @return bool Returns true if the token is valid (i.e., not expired), false otherwise. */ - private function spectrocoinIsTokenValid($currentTime) { + private function spectrocoinIsTokenValid($currentTime) + { return isset($this->access_token_data['expires_at']) && $currentTime < $this->access_token_data['expires_at']; } @@ -261,7 +265,8 @@ private function spectrocoinIsTokenValid($currentTime) { * * @param string $encrypted_access_token_data */ - private function storeEncryptedData($encrypted_access_token_data) { + private function storeEncryptedData($encrypted_access_token_data) + { $session = \Drupal::request()->getSession(); $session->set('encrypted_access_token', $encrypted_access_token_data); } @@ -271,20 +276,22 @@ private function storeEncryptedData($encrypted_access_token_data) { * * @return string|null The encrypted access token data if set, null otherwise. */ - private function retrieveEncryptedData() { + private function retrieveEncryptedData() + { $session = \Drupal::request()->getSession(); return $session->get('encrypted_access_token'); } - + // --------------- VALIDATION AND SANITIZATION BEFORE REQUEST ----------------- /** - * Payload data sanitization for create order - * @param array $payload - * @return array - */ - private function spectrocoinSanitizeOrderPayload($payload) { + * Payload data sanitization for create order + * @param array $payload + * @return array + */ + private function spectrocoinSanitizeOrderPayload($payload) + { $sanitized_payload = [ 'orderId' => htmlspecialchars(trim($payload['orderId'])), // Removes any HTML tags and trims whitespace 'projectId' => htmlspecialchars(trim($payload['projectId'])), // Removes any HTML tags and trims whitespace @@ -300,12 +307,13 @@ private function spectrocoinSanitizeOrderPayload($payload) { return $sanitized_payload; } - /** - * Payload data validation for create order - * @param array $sanitized_payload - * @return bool - */ - private function spectrocoinValidateOrderPayload($sanitized_payload) { + /** + * Payload data validation for create order + * @param array $sanitized_payload + * @return bool + */ + private function spectrocoinValidateOrderPayload($sanitized_payload) + { return isset( $sanitized_payload['orderId'], $sanitized_payload['projectId'], @@ -318,25 +326,26 @@ private function spectrocoinValidateOrderPayload($sanitized_payload) { $sanitized_payload['successUrl'], $sanitized_payload['failureUrl'], ) && - !empty($sanitized_payload['orderId']) && - !empty($sanitized_payload['projectId']) && - strlen($sanitized_payload['payCurrencyCode']) === 3 && - is_numeric($sanitized_payload['payAmount']) && - is_numeric($sanitized_payload['receiveAmount']) && - strlen($sanitized_payload['receiveCurrencyCode']) === 3 && - filter_var($sanitized_payload['callbackUrl'], FILTER_VALIDATE_URL) && - filter_var($sanitized_payload['successUrl'], FILTER_VALIDATE_URL) && - filter_var($sanitized_payload['failureUrl'], FILTER_VALIDATE_URL) && - ($sanitized_payload['payAmount'] > 0 || $sanitized_payload['receiveAmount'] > 0); + !empty($sanitized_payload['orderId']) && + !empty($sanitized_payload['projectId']) && + strlen($sanitized_payload['payCurrencyCode']) === 3 && + is_numeric($sanitized_payload['payAmount']) && + is_numeric($sanitized_payload['receiveAmount']) && + strlen($sanitized_payload['receiveCurrencyCode']) === 3 && + filter_var($sanitized_payload['callbackUrl'], FILTER_VALIDATE_URL) && + filter_var($sanitized_payload['successUrl'], FILTER_VALIDATE_URL) && + filter_var($sanitized_payload['failureUrl'], FILTER_VALIDATE_URL) && + ($sanitized_payload['payAmount'] > 0 || $sanitized_payload['receiveAmount'] > 0); } - + // --------------- VALIDATION AND SANITIZATION AFTER CALLBACK ----------------- /** * @param $post_data * @return SpectroCoin_OrderCallback|null */ - public function spectrocoinProcessCallback($post_data) { + public function spectrocoinProcessCallback($post_data) + { if ($post_data != null) { $sanitized_data = $this->spectrocoinSanitizeCallback($post_data); $is_valid = $this->spectrocoinValidateCallback($sanitized_data); @@ -346,7 +355,6 @@ public function spectrocoinProcessCallback($post_data) { return $order_callback; } } - } return null; } @@ -356,7 +364,8 @@ public function spectrocoinProcessCallback($post_data) { * @param $post_data * @return array */ - public function spectrocoinSanitizeCallback($post_data) { + public function spectrocoinSanitizeCallback($post_data) + { return [ 'userId' => htmlspecialchars(trim($post_data['userId'])), 'merchantApiId' => htmlspecialchars(trim($post_data['merchantApiId'])), @@ -380,30 +389,31 @@ public function spectrocoinSanitizeCallback($post_data) { * @param $sanitized_data * @return bool */ - public function spectrocoinValidateCallback($sanitized_data) { + public function spectrocoinValidateCallback($sanitized_data) + { $is_valid = true; $failed_fields = []; if (!isset( - $sanitized_data['userId'], - $sanitized_data['merchantApiId'], - $sanitized_data['merchantId'], - $sanitized_data['apiId'], - $sanitized_data['orderId'], - $sanitized_data['payCurrencyCode'], - $sanitized_data['payAmount'], - $sanitized_data['receiveCurrencyCode'], - $sanitized_data['receiveAmount'], - $sanitized_data['receivedAmount'], - $sanitized_data['description'], - $sanitized_data['orderRequestId'], - $sanitized_data['status'], + $sanitized_data['userId'], + $sanitized_data['merchantApiId'], + $sanitized_data['merchantId'], + $sanitized_data['apiId'], + $sanitized_data['orderId'], + $sanitized_data['payCurrencyCode'], + $sanitized_data['payAmount'], + $sanitized_data['receiveCurrencyCode'], + $sanitized_data['receiveAmount'], + $sanitized_data['receivedAmount'], + $sanitized_data['description'], + $sanitized_data['orderRequestId'], + $sanitized_data['status'], $sanitized_data['sign'] )) { $is_valid = false; $failed_fields[] = 'One or more required fields are missing.'; } else { - if (empty($sanitized_data['userId'])) { + if (empty($sanitized_data['userId'])) { $is_valid = false; $failed_fields[] = 'userId is empty.'; } @@ -411,14 +421,14 @@ public function spectrocoinValidateCallback($sanitized_data) { $is_valid = false; $failed_fields[] = 'merchantApiId is empty.'; } - if (empty($sanitized_data['merchantId'])) { - $is_valid = false; - $failed_fields[] = 'merchantId is empty.'; - } - if (empty($sanitized_data['apiId'])) { - $is_valid = false; - $failed_fields[] = 'apiId is empty.'; - } + if (empty($sanitized_data['merchantId'])) { + $is_valid = false; + $failed_fields[] = 'merchantId is empty.'; + } + if (empty($sanitized_data['apiId'])) { + $is_valid = false; + $failed_fields[] = 'apiId is empty.'; + } if (strlen($sanitized_data['payCurrencyCode']) !== 3) { $is_valid = false; $failed_fields[] = 'payCurrencyCode is not 3 characters long.'; @@ -464,10 +474,10 @@ public function spectrocoinValidateCallback($sanitized_data) { /** * Order callback payload validation - * @param SpectroCoin_OrderCallback $order_callback + * @param SpectroCoin_OldOrderCallback $order_callback * @return bool */ - public function spectrocoinValidateCallbackPayload(SpectroCoin_OrderCallback $order_callback) + public function spectrocoinValidateCallbackPayload(SpectroCoin_OldOrderCallback $order_callback) { if ($order_callback != null) { @@ -484,9 +494,9 @@ public function spectrocoinValidateCallbackPayload(SpectroCoin_OrderCallback $or 'orderRequestId' => $order_callback->getOrderRequestId(), 'status' => $order_callback->getStatus(), ); - + $data = http_build_query($payload); - if ($this->spectrocoinValidateSignature($data, $order_callback->getSign()) == 1) { + if ($this->spectrocoinValidateSignature($data, $order_callback->getSign()) == 1) { return true; } else { error_log('SpectroCoin Error: Signature validation failed'); @@ -510,4 +520,38 @@ private function spectrocoinValidateSignature($data, $signature) $r = openssl_verify($data, $sig, $public_key_pem, OPENSSL_ALGO_SHA1); return $r; } -} \ No newline at end of file + + /** + * Uses unique order's UUID and access token data to request GET /merchants/orders/{$id} and retrieve the data of the order in array format. + * @param string $order_id + * @param array $access_token_data + * + * @return array|ApiError|GenericError The response array containing order details or an error object if an error occurs. + */ + public function getOrderById(string $order_id) + { + try { + $access_token_data = $this->spectrocoinGetAccessTokenData(); + $response = $this->guzzle_client->request( + 'GET', + $this->merchant_api_url . '/merchants/orders/' . $order_id, + [ + RequestOptions::HEADERS => [ + 'Authorization' => 'Bearer ' . $access_token_data['access_token'], + 'Content-Type' => 'application/json', + ], + ] + ); + + $order = json_decode($response->getBody()->getContents(), true); + + return $order; + } catch (\InvalidArgumentException $e) { + return new GenericError($e->getMessage(), $e->getCode()); + } catch (RequestException $e) { + return new ApiError($e->getMessage(), $e->getCode()); + } catch (\Exception $e) { + return new GenericError($e->getMessage(), $e->getCode()); + } + } +} diff --git a/src/SCMerchantClient/data/SpectroCoin_OldOrderCallback.php b/src/SCMerchantClient/data/SpectroCoin_OldOrderCallback.php new file mode 100755 index 0000000..755edab --- /dev/null +++ b/src/SCMerchantClient/data/SpectroCoin_OldOrderCallback.php @@ -0,0 +1,157 @@ +userId = isset($data['userId']) ? SpectroCoin_Utilities::sanitize_text_field((string)$data['userId']) : null; + $this->merchantApiId = isset($data['merchantApiId']) ? SpectroCoin_Utilities::sanitize_text_field((string)$data['merchantApiId']) : null; + $this->merchantId = isset($data['merchantId']) ? SpectroCoin_Utilities::sanitize_text_field((string)$data['merchantId']) : null; + $this->apiId = isset($data['apiId']) ? SpectroCoin_Utilities::sanitize_text_field((string)$data['apiId']) : null; + $this->orderId = isset($data['orderId']) ? SpectroCoin_Utilities::sanitize_text_field((string)$data['orderId']) : null; + $this->payCurrency = isset($data['payCurrency']) ? SpectroCoin_Utilities::sanitize_text_field((string)$data['payCurrency']) : null; + $this->payAmount = isset($data['payAmount']) ? SpectroCoin_Utilities::sanitize_text_field((string)$data['payAmount']) : null; + $this->receiveCurrency = isset($data['receiveCurrency']) ? SpectroCoin_Utilities::sanitize_text_field((string)$data['receiveCurrency']) : null; + $this->receiveAmount = isset($data['receiveAmount']) ? SpectroCoin_Utilities::sanitize_text_field((string)$data['receiveAmount']) : null; + $this->receivedAmount = isset($data['receivedAmount']) ? SpectroCoin_Utilities::sanitize_text_field((string)$data['receivedAmount']) : null; + $this->description = isset($data['description']) ? SpectroCoin_Utilities::sanitize_text_field((string)$data['description']) : null; + $this->orderRequestId = isset($data['orderRequestId']) ? SpectroCoin_Utilities::sanitize_text_field((string)$data['orderRequestId']) : null; + $this->status = isset($data['status']) ? SpectroCoin_Utilities::sanitize_text_field((string)$data['status']) : null; + $this->sign = isset($data['sign']) ? SpectroCoin_Utilities::sanitize_text_field((string)$data['sign']) : null; + + $validation_result = $this->validate(); + if (is_array($validation_result)) { + $errorMessage = 'Invalid order callback payload. Failed fields: ' . implode(', ', $validation_result); + throw new InvalidArgumentException($errorMessage); + } + + if (!$this->validatePayloadSignature()) { + throw new Exception('Invalid payload signature.'); + } + } + + /** + * Validate the input data. + * + * @return bool|array True if validation passes, otherwise an array of error messages. + */ + private function validate(): bool|array + { + $errors = []; + + if (empty($this->getUserId())) { + $errors[] = 'userId is empty'; + } + if (empty($this->getMerchantApiId())) { + $errors[] = 'merchantApiId is empty'; + } + if (empty($this->getMerchantId())) { + $errors[] = 'merchantId is empty'; + } + if (empty($this->getApiId())) { + $errors[] = 'apiId is empty'; + } + if (empty($this->getOrderId())) { + $errors[] = 'orderId is empty'; + } + if (empty($this->getStatus())){ + $errors[] = 'status is empty'; + } + if (strlen($this->getPayCurrency()) !== 3) { + $errors[] = 'payCurrency is not 3 characters long'; + } + if (!is_numeric($this->getPayAmount()) || (float)$this->getPayAmount() <= 0) { + $errors[] = 'payAmount is not a valid positive number'; + } + if (strlen($this->getReceiveCurrency()) !== 3) { + $errors[] = 'receiveCurrency is not 3 characters long'; + } + if (!is_numeric($this->getReceiveAmount()) || (float)$this->getReceiveAmount() <= 0) { + $errors[] = 'receiveAmount is not a valid positive number'; + } + if (!isset($this->receivedAmount)) { + $errors[] = 'receivedAmount is not set'; + } + if (!is_numeric($this->getOrderRequestId()) || (float)$this->getOrderRequestId() <= 0) { + $errors[] = 'orderRequestId is not a valid positive number'; + } + if (empty($this->getSign())) { + $errors[] = 'signature is empty'; + } + + return empty($errors) ? true : $errors; + } + + /** + * Validate the payload signature. + * + * @return bool True if the signature is valid, otherwise false. + */ + public function validatePayloadSignature(): bool + { + $payload = [ + 'merchantId' => $this->getMerchantId(), + 'apiId' => $this->getApiId(), + 'orderId' => $this->getOrderId(), + 'payCurrency' => $this->getPayCurrency(), + 'payAmount' => $this->getPayAmount(), + 'receiveCurrency' => $this->getReceiveCurrency(), + 'receiveAmount' => $this->getReceiveAmount(), + 'receivedAmount' => $this->getReceivedAmount(), + 'description' => $this->getDescription(), + 'orderRequestId' => $this->getOrderRequestId(), + 'status' => $this->getStatus(), + ]; + $data = http_build_query($payload); + $decoded_signature = base64_decode($this->sign); + $public_key = file_get_contents('https://spectrocoin.com/files/merchant.public.pem'); + $public_key_pem = openssl_pkey_get_public($public_key); + return openssl_verify($data, $decoded_signature, $public_key_pem, OPENSSL_ALGO_SHA1) === 1; + } + + public function getUserId() { return $this->userId; } + public function getMerchantApiId() { return $this->merchantApiId; } + public function getMerchantId() { return $this->merchantId; } + public function getApiId() { return $this->apiId; } + public function getOrderId() { return $this->orderId; } + public function getPayCurrency() { return $this->payCurrency; } + public function getPayAmount() { return SpectroCoin_Utilities::spectrocoinFormatCurrency($this->payAmount); } + public function getReceiveCurrency() { return $this->receiveCurrency; } + public function getReceiveAmount() { return SpectroCoin_Utilities::spectrocoinFormatCurrency($this->receiveAmount); } + public function getReceivedAmount() { return SpectroCoin_Utilities::spectrocoinFormatCurrency($this->receivedAmount); } + public function getDescription() { return $this->description; } + public function getOrderRequestId() { return $this->orderRequestId; } + public function getStatus() { return $this->status; } + public function getSign() { return $this->sign; } +} +?> diff --git a/src/SCMerchantClient/data/SpectroCoin_OrderCallback.php b/src/SCMerchantClient/data/SpectroCoin_OrderCallback.php index 88f5cdb..61f2a23 100755 --- a/src/SCMerchantClient/data/SpectroCoin_OrderCallback.php +++ b/src/SCMerchantClient/data/SpectroCoin_OrderCallback.php @@ -6,58 +6,21 @@ use \InvalidArgumentException; -use \Exception; - class SpectroCoin_OrderCallback { - private ?string $userId; + private ?string $uuid; private ?string $merchantApiId; - private ?string $merchantId; - private ?string $apiId; - private ?string $orderId; - private ?string $payCurrency; - private ?string $payAmount; - private ?string $receiveCurrency; - private ?string $receiveAmount; - private ?string $receivedAmount; - private ?string $description; - private ?string $orderRequestId; - private ?string $status; - private ?string $sign; - /** - * Constructor for OrderCallback. - * - * @param array $data The data for initializing the callback. - * - * @throws InvalidArgumentException If the payload is invalid. - */ - public function __construct(array $data) + public function __construct(?string $uuid, ?string $merchantApiId) { - $this->userId = isset($data['userId']) ? SpectroCoin_Utilities::sanitize_text_field((string)$data['userId']) : null; - $this->merchantApiId = isset($data['merchantApiId']) ? SpectroCoin_Utilities::sanitize_text_field((string)$data['merchantApiId']) : null; - $this->merchantId = isset($data['merchantId']) ? SpectroCoin_Utilities::sanitize_text_field((string)$data['merchantId']) : null; - $this->apiId = isset($data['apiId']) ? SpectroCoin_Utilities::sanitize_text_field((string)$data['apiId']) : null; - $this->orderId = isset($data['orderId']) ? SpectroCoin_Utilities::sanitize_text_field((string)$data['orderId']) : null; - $this->payCurrency = isset($data['payCurrency']) ? SpectroCoin_Utilities::sanitize_text_field((string)$data['payCurrency']) : null; - $this->payAmount = isset($data['payAmount']) ? SpectroCoin_Utilities::sanitize_text_field((string)$data['payAmount']) : null; - $this->receiveCurrency = isset($data['receiveCurrency']) ? SpectroCoin_Utilities::sanitize_text_field((string)$data['receiveCurrency']) : null; - $this->receiveAmount = isset($data['receiveAmount']) ? SpectroCoin_Utilities::sanitize_text_field((string)$data['receiveAmount']) : null; - $this->receivedAmount = isset($data['receivedAmount']) ? SpectroCoin_Utilities::sanitize_text_field((string)$data['receivedAmount']) : null; - $this->description = isset($data['description']) ? SpectroCoin_Utilities::sanitize_text_field((string)$data['description']) : null; - $this->orderRequestId = isset($data['orderRequestId']) ? SpectroCoin_Utilities::sanitize_text_field((string)$data['orderRequestId']) : null; - $this->status = isset($data['status']) ? SpectroCoin_Utilities::sanitize_text_field((string)$data['status']) : null; - $this->sign = isset($data['sign']) ? SpectroCoin_Utilities::sanitize_text_field((string)$data['sign']) : null; + $this->uuid = isset($uuid) ? SpectroCoin_Utilities::sanitize_text_field((string)$uuid) : null; + $this->merchantApiId = isset($merchantApiId) ? SpectroCoin_Utilities::sanitize_text_field((string)$merchantApiId) : null; $validation_result = $this->validate(); if (is_array($validation_result)) { - $errorMessage = 'Invalid order callback payload. Failed fields: ' . implode(', ', $validation_result); + $errorMessage = 'Invalid order callback. Failed fields: ' . implode(', ', $validation_result); throw new InvalidArgumentException($errorMessage); } - - if (!$this->validatePayloadSignature()) { - throw new Exception('Invalid payload signature.'); - } } /** @@ -69,89 +32,24 @@ private function validate(): bool|array { $errors = []; - if (empty($this->getUserId())) { - $errors[] = 'userId is empty'; + if (empty($this->getUuid())) { + $errors[] = 'Uuid is empty'; } - if (empty($this->getMerchantApiId())) { + + if (empty($this->getmerchantApiId())) { $errors[] = 'merchantApiId is empty'; } - if (empty($this->getMerchantId())) { - $errors[] = 'merchantId is empty'; - } - if (empty($this->getApiId())) { - $errors[] = 'apiId is empty'; - } - if (empty($this->getOrderId())) { - $errors[] = 'orderId is empty'; - } - if (empty($this->getStatus())){ - $errors[] = 'status is empty'; - } - if (strlen($this->getPayCurrency()) !== 3) { - $errors[] = 'payCurrency is not 3 characters long'; - } - if (!is_numeric($this->getPayAmount()) || (float)$this->getPayAmount() <= 0) { - $errors[] = 'payAmount is not a valid positive number'; - } - if (strlen($this->getReceiveCurrency()) !== 3) { - $errors[] = 'receiveCurrency is not 3 characters long'; - } - if (!is_numeric($this->getReceiveAmount()) || (float)$this->getReceiveAmount() <= 0) { - $errors[] = 'receiveAmount is not a valid positive number'; - } - if (!isset($this->receivedAmount)) { - $errors[] = 'receivedAmount is not set'; - } - if (!is_numeric($this->getOrderRequestId()) || (float)$this->getOrderRequestId() <= 0) { - $errors[] = 'orderRequestId is not a valid positive number'; - } - if (empty($this->getSign())) { - $errors[] = 'signature is empty'; - } return empty($errors) ? true : $errors; } - /** - * Validate the payload signature. - * - * @return bool True if the signature is valid, otherwise false. - */ - public function validatePayloadSignature(): bool + public function getUuid() + { + return $this->uuid; + } + public function getmerchantApiId() { - $payload = [ - 'merchantId' => $this->getMerchantId(), - 'apiId' => $this->getApiId(), - 'orderId' => $this->getOrderId(), - 'payCurrency' => $this->getPayCurrency(), - 'payAmount' => $this->getPayAmount(), - 'receiveCurrency' => $this->getReceiveCurrency(), - 'receiveAmount' => $this->getReceiveAmount(), - 'receivedAmount' => $this->getReceivedAmount(), - 'description' => $this->getDescription(), - 'orderRequestId' => $this->getOrderRequestId(), - 'status' => $this->getStatus(), - ]; - $data = http_build_query($payload); - $decoded_signature = base64_decode($this->sign); - $public_key = file_get_contents('https://spectrocoin.com/files/merchant.public.pem'); - $public_key_pem = openssl_pkey_get_public($public_key); - return openssl_verify($data, $decoded_signature, $public_key_pem, OPENSSL_ALGO_SHA1) === 1; + return $this->merchantApiId; } - public function getUserId() { return $this->userId; } - public function getMerchantApiId() { return $this->merchantApiId; } - public function getMerchantId() { return $this->merchantId; } - public function getApiId() { return $this->apiId; } - public function getOrderId() { return $this->orderId; } - public function getPayCurrency() { return $this->payCurrency; } - public function getPayAmount() { return SpectroCoin_Utilities::spectrocoinFormatCurrency($this->payAmount); } - public function getReceiveCurrency() { return $this->receiveCurrency; } - public function getReceiveAmount() { return SpectroCoin_Utilities::spectrocoinFormatCurrency($this->receiveAmount); } - public function getReceivedAmount() { return SpectroCoin_Utilities::spectrocoinFormatCurrency($this->receivedAmount); } - public function getDescription() { return $this->description; } - public function getOrderRequestId() { return $this->orderRequestId; } - public function getStatus() { return $this->status; } - public function getSign() { return $this->sign; } -} -?> +} \ No newline at end of file diff --git a/src/SCMerchantClient/data/SpectroCoin_OrderStatusEnum.php b/src/SCMerchantClient/data/SpectroCoin_OrderStatusEnum.php index 6ce82e0..eaa8d48 100755 --- a/src/SCMerchantClient/data/SpectroCoin_OrderStatusEnum.php +++ b/src/SCMerchantClient/data/SpectroCoin_OrderStatusEnum.php @@ -2,12 +2,45 @@ namespace Drupal\commerce_spectrocoin\SCMerchantClient\data; -class SpectroCoin_OrderStatusEnum +enum SpectroCoin_OrderStatusEnum: string { - public static $New = 1; - public static $Pending = 2; - public static $Paid = 3; - public static $Failed = 4; - public static $Expired = 5; - public static $Test = 6; + case NEW = 'NEW'; + case PENDING = 'PENDING'; + case PAID = 'PAID'; + case FAILED = 'FAILED'; + case EXPIRED = 'EXPIRED'; + + /** + * Map old numeric codes to new enum. + */ + public static function fromCode(int $code): self + { + return match ($code) { + 1 => self::NEW, + 2 => self::PENDING, + 3 => self::PAID, + 4 => self::FAILED, + 5 => self::EXPIRED, + default => throw new \InvalidArgumentException("Unknown numeric status code: $code"), + }; + } + + /** + * Normalize either an integer (legacy) or a string. + */ + public static function normalize(string|int $raw): self + { + if (is_int($raw) || ctype_digit((string)$raw)) { + return self::fromCode((int)$raw); + } + $upper = strtoupper((string)$raw); + return match ($upper) { + 'NEW' => self::NEW, + 'PENDING' => self::PENDING, + 'PAID' => self::PAID, + 'FAILED' => self::FAILED, + 'EXPIRED' => self::EXPIRED, + default => throw new \InvalidArgumentException("Unknown status string: $raw"), + }; + } } From d83932f50546a7d8310be8d053ca1fba414078c8 Mon Sep 17 00:00:00 2001 From: SteinCode Date: Tue, 15 Jul 2025 13:37:10 +0300 Subject: [PATCH 2/7] change versions and make test name --- commerce_spectrocoin.info.yml | 2 +- composer.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/commerce_spectrocoin.info.yml b/commerce_spectrocoin.info.yml index c7a1253..58b9989 100755 --- a/commerce_spectrocoin.info.yml +++ b/commerce_spectrocoin.info.yml @@ -7,6 +7,6 @@ dependencies: - commerce:commerce_checkout - commerce:commerce_payment -version: "1.0.0" +version: "1.1.0" project: "commerce_spectrocoin" datestamp: 1684701392 diff --git a/composer.json b/composer.json index 6d70602..9db9719 100755 --- a/composer.json +++ b/composer.json @@ -1,8 +1,8 @@ { - "name": "spectrocoin/drupal-merchant", + "name": "testspectrocoin/testdrupal-merchant", "description": "SpectroCoin cryptocurrency payment gateway for Drupal", "type": "drupal-module", - "version": "1.0.0", + "version": "1.1.0", "keywords": [ "bitcoin", "btc", From dcda10c7ffaa602a3316ed5ff8c82d8b7acfa75d Mon Sep 17 00:00:00 2001 From: SteinCode Date: Tue, 15 Jul 2025 13:41:17 +0300 Subject: [PATCH 3/7] update package name for testing --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 9db9719..b92e4a8 100755 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "testspectrocoin/testdrupal-merchant", + "name": "spectrocointest/drupal-merchant-test", "description": "SpectroCoin cryptocurrency payment gateway for Drupal", "type": "drupal-module", "version": "1.1.0", From 9a20c0a04a0461512043f41aedc34d9c7f2496d0 Mon Sep 17 00:00:00 2001 From: SteinCode Date: Tue, 15 Jul 2025 14:06:17 +0300 Subject: [PATCH 4/7] change package name for test --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index b92e4a8..10c48f6 100755 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "spectrocointest/drupal-merchant-test", + "name": "testspectrocoin/test-drupal-merchant", "description": "SpectroCoin cryptocurrency payment gateway for Drupal", "type": "drupal-module", "version": "1.1.0", From 59da3c010fe5abbd6fa7eaf09048c1e56a0db8ac Mon Sep 17 00:00:00 2001 From: SteinCode Date: Tue, 15 Jul 2025 14:42:13 +0300 Subject: [PATCH 5/7] update callback bug --- src/Controller/SpectroCoinController.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Controller/SpectroCoinController.php b/src/Controller/SpectroCoinController.php index c583831..1b8a301 100755 --- a/src/Controller/SpectroCoinController.php +++ b/src/Controller/SpectroCoinController.php @@ -86,19 +86,19 @@ public function callback() $statusEnum = SpectroCoin_OrderStatusEnum::normalize($raw_status); switch ($statusEnum) { - case OrderStatus::NEW: + case SpectroCoin_OrderStatusEnum::NEW: break; - case OrderStatus::PENDING: + case SpectroCoin_OrderStatusEnum::PENDING: break; - case OrderStatus::PAID: + case SpectroCoin_OrderStatusEnum::PAID: $order->set('state', 'completed'); $order->set('cart', 0); break; - case OrderStatus::FAILED: + case SpectroCoin_OrderStatusEnum::FAILED: $order->set('state', 'canceled'); $order->set('cart', 0); break; - case OrderStatus::EXPIRED: + case SpectroCoin_OrderStatusEnum::EXPIRED: $order->set('state', 'expired'); $order->set('cart', 0); break; From adf81ea58ca26f3620977feb062aeaa560261135 Mon Sep 17 00:00:00 2001 From: SteinCode Date: Wed, 16 Jul 2025 09:42:11 +0300 Subject: [PATCH 6/7] fix callback --- src/Controller/SpectroCoinController.php | 31 +++++++++++++++--------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/Controller/SpectroCoinController.php b/src/Controller/SpectroCoinController.php index 1b8a301..aad58e2 100755 --- a/src/Controller/SpectroCoinController.php +++ b/src/Controller/SpectroCoinController.php @@ -39,25 +39,34 @@ public function callback() try { $contentType = $_SERVER['CONTENT_TYPE'] ?? ''; if (stripos($contentType, 'application/json') !== false) { - // if new callback + // 1) parse the JSON payload $order_callback = $this->initCallbackFromJson(); if (! $order_callback) { throw new InvalidArgumentException('Invalid JSON callback payload'); } + + $gateways = \Drupal::entityTypeManager() + ->getStorage('commerce_payment_gateway') + ->loadByProperties(['plugin' => 'spectrocoin']); + $gateway_entity = reset($gateways); + if (! $gateway_entity) { + throw new \RuntimeException('SpectroCoin gateway not configured.'); + } + $plugin = $gateway_entity->getPlugin(); + $cfg = $plugin->getConfiguration(); + $sc_merchant_client = new SCMerchantClient( - $this->configuration['project_id'], - $this->configuration['client_id'], - $this->configuration['client_secret'] + $cfg['project_id'], + $cfg['client_id'], + $cfg['client_secret'] ); - $order_data = $sc_merchant_client->getOrderById($order_callback->getUuid()); - - if (! is_array($order_data) || empty($order_data['orderId']) || empty($order_data['status'])) { + $sc_order = $sc_merchant_client->getOrderById($order_callback->getUuid()); + if (empty($sc_order['orderId']) || empty($sc_order['status'])) { throw new InvalidArgumentException('Malformed order data from API'); } - - $raw_status = $order_data['status']; - $raw_order_id = $order_data['orderId']; + $raw_status = $sc_order['status']; + $raw_order_id = $sc_order['orderId']; } else { // if legacy callback $order_callback = $this->initCallbackFromPost(); @@ -109,7 +118,7 @@ public function callback() } $order->save(); - if ($statusEnum === SpectroCoin_OrderStatusEnum::PAID) { + if ($statusEnum === SpectroCoin_OrderStatusEnum::PAID) { $payment_storage = \Drupal::entityTypeManager()->getStorage('commerce_payment'); $payment = $payment_storage->load($payment_id); if ($payment) { From 6e6c48f29dc93ecdb09838753cd092eff866a4f9 Mon Sep 17 00:00:00 2001 From: SteinCode Date: Wed, 16 Jul 2025 10:22:05 +0300 Subject: [PATCH 7/7] 1.1.0 Release --- README.md | 2 +- composer.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6fa08c0..f079329 100755 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ Order callbacks in the SpectroCoin plugin allow your WordPress site to automatic ## Changelog -### 1.1.0 (): +### 1.1.0 (16/07/2025): _Added_ support for new JSON and old URL-encoded form data callbacks format. New callbacks will be automatically enabled with new SpectroCoin merchant projects created. With old projects, old callback format will be used. In the future versions old callback format will be removed. diff --git a/composer.json b/composer.json index 10c48f6..bb196c2 100755 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "testspectrocoin/test-drupal-merchant", + "name": "spectrocoin/drupal-merchant", "description": "SpectroCoin cryptocurrency payment gateway for Drupal", "type": "drupal-module", "version": "1.1.0",