| 1 |
<?php declare(strict_types=1); |
| 2 |
|
| 3 |
namespace MultiSafepay\WooCommerce\PaymentMethods; |
| 4 |
|
| 5 |
use MultiSafepay\Api\Transactions\Transaction; |
| 6 |
use MultiSafepay\Api\Transactions\TransactionResponse; |
| 7 |
use MultiSafepay\Exception\ApiException; |
| 8 |
use MultiSafepay\WooCommerce\Services\PaymentMethodService; |
| 9 |
use MultiSafepay\WooCommerce\Services\SdkService; |
| 10 |
use MultiSafepay\WooCommerce\Settings\SettingsFields; |
| 11 |
use MultiSafepay\WooCommerce\Utils\Logger; |
| 12 |
use MultiSafepay\WooCommerce\Utils\Order as OrderUtil; |
| 13 |
use MultiSafepay\WooCommerce\Utils\PaymentMethodTitleBuilder; |
| 14 |
use Psr\Http\Client\ClientExceptionInterface; |
| 15 |
use WC_Data_Exception; |
| 16 |
use WC_Order; |
| 17 |
|
| 18 |
/** |
| 19 |
* The payment method callback handle the notification process. |
| 20 |
*/ |
| 21 |
class PaymentMethodCallback { |
| 22 |
|
| 23 |
public const CREDIT_CARD_GATEWAYS = array( 'VISA', 'MASTERCARD', 'AMEX', 'MAESTRO' ); |
| 24 |
|
| 25 |
/** |
| 26 |
* The WooCommerce Order Id. |
| 27 |
* |
| 28 |
* @var int The WooCommerce Order Id. |
| 29 |
*/ |
| 30 |
private $woocommerce_order_id; |
| 31 |
|
| 32 |
/** |
| 33 |
* The MultiSafepay Order Id. |
| 34 |
* |
| 35 |
* @var string The MultiSafepay Order Id. |
| 36 |
*/ |
| 37 |
private $multisafepay_order_id; |
| 38 |
|
| 39 |
/** |
| 40 |
* The time stamp of the callback |
| 41 |
* |
| 42 |
* @var string The timestamp |
| 43 |
*/ |
| 44 |
private $time_stamp; |
| 45 |
|
| 46 |
/** |
| 47 |
* The WooCommerce order object |
| 48 |
* |
| 49 |
* @var WC_Order The WooCommerce order object |
| 50 |
*/ |
| 51 |
private $order; |
| 52 |
|
| 53 |
/** |
| 54 |
* The MultiSafepay transaction |
| 55 |
* |
| 56 |
* @var TransactionResponse The MultiSafepay transaction |
| 57 |
*/ |
| 58 |
private $multisafepay_transaction; |
| 59 |
|
| 60 |
/** |
| 61 |
* @var Logger |
| 62 |
*/ |
| 63 |
private $logger; |
| 64 |
|
| 65 |
/** |
| 66 |
* @param string $multisafepay_order_id |
| 67 |
* @param TransactionResponse|null $multisafepay_transaction |
| 68 |
* @param Logger|null $logger |
| 69 |
*/ |
| 70 |
public function __construct( string $multisafepay_order_id, ?TransactionResponse $multisafepay_transaction = null, ?Logger $logger = null ) { |
| 71 |
$this->logger = $logger ?? new Logger(); |
| 72 |
$this->multisafepay_order_id = $multisafepay_order_id; |
| 73 |
$this->multisafepay_transaction = $multisafepay_transaction ?? $this->get_transaction(); |
| 74 |
$this->woocommerce_order_id = $this->get_woocommerce_order_id(); |
| 75 |
$this->time_stamp = date( 'd/m/Y H:i:s' ); |
| 76 |
$this->order = wc_get_order( $this->woocommerce_order_id ); |
| 77 |
} |
| 78 |
|
| 79 |
/** |
| 80 |
* Return the MultiSafepay Transaction |
| 81 |
* |
| 82 |
* @return TransactionResponse |
| 83 |
*/ |
| 84 |
private function get_transaction(): TransactionResponse { |
| 85 |
$transaction_manager = ( new SdkService() )->get_transaction_manager(); |
| 86 |
try { |
| 87 |
$transaction = $transaction_manager->get( $this->multisafepay_order_id ); |
| 88 |
} catch ( ClientExceptionInterface $client_exception ) { |
| 89 |
$this->logger->log_error( $client_exception->getMessage() ); |
| 90 |
wp_die( esc_html__( 'Invalid request', 'multisafepay' ), esc_html__( 'Invalid request', 'multisafepay' ), 400 ); |
| 91 |
} catch ( ApiException $api_exception ) { |
| 92 |
$this->logger->log_error( $api_exception->getMessage() ); |
| 93 |
wp_die( esc_html__( 'Invalid request', 'multisafepay' ), esc_html__( 'Invalid request', 'multisafepay' ), 400 ); |
| 94 |
} |
| 95 |
return $transaction; |
| 96 |
} |
| 97 |
|
| 98 |
/** |
| 99 |
* Return the WooCommerce order id |
| 100 |
* |
| 101 |
* @return int |
| 102 |
*/ |
| 103 |
private function get_woocommerce_order_id(): int { |
| 104 |
// For most transactions, var2 contains the order id since the order request is being registered using order number |
| 105 |
if ( ! empty( $this->multisafepay_transaction->getVar2() ) ) { |
| 106 |
return (int) $this->multisafepay_transaction->getVar2(); |
| 107 |
} |
| 108 |
return apply_filters( 'multisafepay_transaction_order_id', $this->multisafepay_order_id ); |
| 109 |
} |
| 110 |
|
| 111 |
/** |
| 112 |
* Return the WooCommerce order status |
| 113 |
* |
| 114 |
* @return string |
| 115 |
*/ |
| 116 |
private function get_woocommerce_order_status(): string { |
| 117 |
return $this->order->get_status(); |
| 118 |
} |
| 119 |
|
| 120 |
/** |
| 121 |
* Return the gateway code registered in MultiSafepay for this transaction |
| 122 |
* |
| 123 |
* @param bool $apply_group_credit_cards |
| 124 |
* @return string |
| 125 |
*/ |
| 126 |
private function get_multisafepay_transaction_gateway_code( bool $apply_group_credit_cards = true ): string { |
| 127 |
$code = $this->multisafepay_transaction->getPaymentDetails()->getType(); |
| 128 |
if ( |
| 129 |
$apply_group_credit_cards && |
| 130 |
in_array( $code, self::CREDIT_CARD_GATEWAYS, true ) && |
| 131 |
get_option( 'multisafepay_group_credit_cards', false ) |
| 132 |
) { |
| 133 |
$code = 'CREDITCARD'; |
| 134 |
} |
| 135 |
if ( 0 === stripos( $code, 'coupon::' ) ) { |
| 136 |
$data = $this->multisafepay_transaction->getPaymentDetails()->getData(); |
| 137 |
return $data['coupon_brand']; |
| 138 |
} |
| 139 |
return $code; |
| 140 |
} |
| 141 |
|
| 142 |
/** |
| 143 |
* Return the wallet code registered in MultiSafepay for this transaction |
| 144 |
* |
| 145 |
* @return string |
| 146 |
*/ |
| 147 |
private function get_multisafepay_transaction_wallet_code(): string { |
| 148 |
return (string) $this->multisafepay_transaction->getPaymentDetails()->get( 'wallet' ); |
| 149 |
} |
| 150 |
|
| 151 |
/** |
| 152 |
* Return the status of the transaction in MultiSafepay |
| 153 |
* |
| 154 |
* @return string |
| 155 |
*/ |
| 156 |
private function get_multisafepay_transaction_status(): string { |
| 157 |
return $this->multisafepay_transaction->getStatus(); |
| 158 |
} |
| 159 |
|
| 160 |
/** |
| 161 |
* Return the PSP ID |
| 162 |
* |
| 163 |
* @return string |
| 164 |
*/ |
| 165 |
private function get_multisafepay_transaction_id(): string { |
| 166 |
return $this->multisafepay_transaction->getTransactionId(); |
| 167 |
} |
| 168 |
|
| 169 |
/** |
| 170 |
* Return if the order status is the same as the final |
| 171 |
* order status configured in the plugin settings |
| 172 |
* |
| 173 |
* @param string $order_status |
| 174 |
* @return bool |
| 175 |
*/ |
| 176 |
private function is_completed_the_final_status( string $order_status ): bool { |
| 177 |
$final_order_status = get_option( 'multisafepay_final_order_status', false ); |
| 178 |
return $final_order_status && ( 'completed' === $order_status ); |
| 179 |
} |
| 180 |
|
| 181 |
/** |
| 182 |
* Return the initial order status configured in the payment method settings |
| 183 |
* |
| 184 |
* @param PaymentMethodService $payment_method_service |
| 185 |
* @return string|null |
| 186 |
*/ |
| 187 |
private function get_initial_order_status( PaymentMethodService $payment_method_service ): ?string { |
| 188 |
$registered_payment_method = $payment_method_service->get_woocommerce_payment_gateway_by_id( $this->order->get_payment_method() ); |
| 189 |
return $registered_payment_method ? $registered_payment_method->initial_order_status : null; |
| 190 |
} |
| 191 |
|
| 192 |
/** |
| 193 |
* Check if the order status should be updated or not |
| 194 |
* |
| 195 |
* @return bool |
| 196 |
*/ |
| 197 |
private function should_status_be_updated(): bool { |
| 198 |
// Check if the WooCommerce completed order status is considered as the final one |
| 199 |
if ( $this->is_completed_the_final_status( $this->get_woocommerce_order_status() ) ) { |
| 200 |
// phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 201 |
$message = 'It seems a notification is trying to process an order which already has defined completed as the final order status. For this reason notification is being ignored. Transaction ID received is ' . sanitize_text_field( (string) wp_unslash( $this->get_multisafepay_transaction_id() ) ) . ' with status ' . $this->get_multisafepay_transaction_status(); |
| 202 |
$this->logger->log_warning( $message ); |
| 203 |
$this->order->add_order_note( $message ); |
| 204 |
return false; |
| 205 |
} |
| 206 |
|
| 207 |
// The order status can be updated |
| 208 |
return true; |
| 209 |
} |
| 210 |
|
| 211 |
/** |
| 212 |
* Process the callback. |
| 213 |
* |
| 214 |
* @return void |
| 215 |
* @throws WC_Data_Exception |
| 216 |
*/ |
| 217 |
public function process_callback(): void { |
| 218 |
// On pre-transactions notification, and using sequential order numbers plugins, var 2 is not received in the notification, then order doesn't exist |
| 219 |
if ( ! $this->order ) { |
| 220 |
$this->logger->log_info( 'Notification has been received for the transaction ID ' . $this->multisafepay_order_id . ' but WooCommerce order object has not been found' ); |
| 221 |
header( 'Content-type: text/plain' ); |
| 222 |
die( 'OK' ); |
| 223 |
} |
| 224 |
|
| 225 |
// If payment method of the order does not belong to MultiSafepay |
| 226 |
if ( ! OrderUtil::is_multisafepay_order( $this->order ) ) { |
| 227 |
$message = 'It seems a notification is trying to process an order processed by another payment method. Transaction ID received is ' . $this->order->get_id(); |
| 228 |
$this->logger->log_info( $message ); |
| 229 |
header( 'Content-type: text/plain' ); |
| 230 |
die( 'OK' ); |
| 231 |
} |
| 232 |
|
| 233 |
if ( $this->get_woocommerce_order_status() === 'trash' ) { |
| 234 |
$message = 'It seems a notification is trying to change the order status, but the order has been moved to the trash. Transaction ID received is ' . $this->order->get_id() . ' and transaction status is ' . $this->get_multisafepay_transaction_status(); |
| 235 |
$this->logger->log_info( $message ); |
| 236 |
OrderUtil::add_order_note( $this->order, $message, true ); |
| 237 |
header( 'Content-type: text/plain' ); |
| 238 |
die( 'OK' ); |
| 239 |
} |
| 240 |
|
| 241 |
// If the transaction status is partial_refunded, we just register a new order note. |
| 242 |
if ( $this->get_multisafepay_transaction_status() === Transaction::PARTIAL_REFUNDED ) { |
| 243 |
$message = 'A partial refund has been registered within MultiSafepay Control for Order ID: ' . $this->woocommerce_order_id . ' and Order Number: ' . $this->multisafepay_order_id; |
| 244 |
OrderUtil::add_order_note( $this->order, $message ); |
| 245 |
header( 'Content-type: text/plain' ); |
| 246 |
die( 'OK' ); |
| 247 |
} |
| 248 |
|
| 249 |
$payment_method_service = new PaymentMethodService(); |
| 250 |
$payment_method_title_builder = new PaymentMethodTitleBuilder(); |
| 251 |
$registered_by_multisafepay_payment_method_object = $payment_method_service->get_woocommerce_payment_gateway_by_multisafepay_gateway_code( $this->get_multisafepay_transaction_gateway_code() ); |
| 252 |
$payment_method_title_registered_by_multisafepay = $registered_by_multisafepay_payment_method_object ? $registered_by_multisafepay_payment_method_object->get_payment_method_title() : ''; |
| 253 |
$wallet_code = $this->get_multisafepay_transaction_wallet_code(); |
| 254 |
$wallet_payment_method_object = $wallet_code ? $payment_method_service->get_woocommerce_payment_gateway_by_multisafepay_gateway_code( $wallet_code ) : false; |
| 255 |
$wallet_payment_method_title = $wallet_payment_method_object ? $wallet_payment_method_object->get_payment_method_title() : ''; |
| 256 |
$payment_method_object_to_persist = $registered_by_multisafepay_payment_method_object; |
| 257 |
if ( $wallet_payment_method_object ) { |
| 258 |
$payment_method_object_to_persist = $wallet_payment_method_object; |
| 259 |
} |
| 260 |
$payment_method_id_to_persist = $payment_method_object_to_persist ? $payment_method_object_to_persist->get_payment_method_id() : false; |
| 261 |
// Keep the gateway title as fallback and only enrich it when wallet context is available. |
| 262 |
$payment_method_title_to_display = $payment_method_title_registered_by_multisafepay; |
| 263 |
if ( ! empty( $wallet_payment_method_title ) ) { |
| 264 |
$payment_method_title_to_display = $wallet_payment_method_title; |
| 265 |
// Use the raw transaction gateway code (without CREDITCARD grouping) to decide wallet-title eligibility. |
| 266 |
$underlying_payment_method_gateway_code = $this->get_multisafepay_transaction_gateway_code( false ); |
| 267 |
if ( $payment_method_title_builder->can_build_wallet_combined_payment_method_title( $underlying_payment_method_gateway_code ) ) { |
| 268 |
$underlying_payment_method_title = $payment_method_title_builder->get_underlying_payment_method_title( $underlying_payment_method_gateway_code ); |
| 269 |
$payment_method_title_to_display = $payment_method_title_builder->build_combined_payment_method_title( $wallet_payment_method_title, $underlying_payment_method_title ); |
| 270 |
} |
| 271 |
} |
| 272 |
$payment_method_id_registered_by_wc = $this->order->get_payment_method(); |
| 273 |
$payment_method_title_registered_by_wc = $this->order->get_payment_method_title(); |
| 274 |
$initial_order_status = $this->get_initial_order_status( $payment_method_service ); |
| 275 |
$default_order_status = SettingsFields::get_multisafepay_order_statuses(); |
| 276 |
|
| 277 |
// Check if the WooCommerce Order status do not match with the order status received in notification, to avoid to process repeated of notification. |
| 278 |
// Or if the custom initial order status of the gateway is different than the general one, and the MultiSafepay transaction status is initialized, and custom initial order status is different than the current WooCommerce order status |
| 279 |
if ( |
| 280 |
$this->get_woocommerce_order_status() !== str_replace( 'wc-', '', get_option( 'multisafepay_' . $this->get_multisafepay_transaction_status() . '_status', $default_order_status[ $this->get_multisafepay_transaction_status() . '_status' ]['default'] ) ) || |
| 281 |
get_option( 'multisafepay_' . $this->get_multisafepay_transaction_status() . '_status', $default_order_status[ $this->get_multisafepay_transaction_status() . '_status' ]['default'] ) !== $initial_order_status && $this->get_multisafepay_transaction_status() === Transaction::INITIALIZED && $this->get_woocommerce_order_status() !== $initial_order_status |
| 282 |
) { |
| 283 |
// If MultiSafepay transaction status is initialized, check if there is a custom initial order status for this payment method. |
| 284 |
if ( $this->get_multisafepay_transaction_status() === Transaction::INITIALIZED ) { |
| 285 |
if ( $initial_order_status && 'wc-default' !== $initial_order_status && $this->get_woocommerce_order_status() !== $initial_order_status ) { |
| 286 |
$this->order->update_status( str_replace( 'wc-', '', $initial_order_status ), __( 'Transaction has been initialized.', 'multisafepay' ) ); |
| 287 |
} |
| 288 |
if ( ! $initial_order_status || 'wc-default' === $initial_order_status ) { |
| 289 |
$this->order->update_status( str_replace( 'wc-', '', get_option( 'multisafepay_' . $this->get_multisafepay_transaction_status() . '_status', $default_order_status[ $this->get_multisafepay_transaction_status() . '_status' ]['default'] ) ) ); |
| 290 |
} |
| 291 |
} |
| 292 |
|
| 293 |
// If MultiSafepay transaction status is completed, payment_complete function will handle the order status change |
| 294 |
if ( $this->get_multisafepay_transaction_status() === Transaction::COMPLETED ) { |
| 295 |
$payment_complete = $this->order->payment_complete( $this->get_multisafepay_transaction_id() ); |
| 296 |
if ( $payment_complete ) { |
| 297 |
$this->order->update_meta_data( '_multisafepay_order_environment', get_option( 'multisafepay_testmode', false ) ? 'test' : 'live' ); |
| 298 |
$this->order->save(); |
| 299 |
} |
| 300 |
} |
| 301 |
|
| 302 |
// If MultiSafepay transaction status is not completed and not initialized, process the notification according order status settings |
| 303 |
if ( |
| 304 |
$this->get_multisafepay_transaction_status() !== 'completed' && |
| 305 |
$this->get_multisafepay_transaction_status() !== 'initialized' && |
| 306 |
$this->should_status_be_updated() |
| 307 |
) { |
| 308 |
$this->order->update_status( str_replace( 'wc-', '', get_option( 'multisafepay_' . $this->get_multisafepay_transaction_status() . '_status', $default_order_status[ $this->get_multisafepay_transaction_status() . '_status' ]['default'] ) ) ); |
| 309 |
} |
| 310 |
|
| 311 |
$message = 'Callback received for Order ID: ' . $this->woocommerce_order_id . ' and Order Number: ' . $this->multisafepay_order_id . ' on ' . $this->time_stamp . ' with status: ' . $this->get_multisafepay_transaction_status() . ' and PSP ID: ' . $this->get_multisafepay_transaction_id() . '.'; |
| 312 |
$this->logger->log_info( $message ); |
| 313 |
OrderUtil::add_order_note( $this->order, $message, true ); |
| 314 |
} |
| 315 |
|
| 316 |
// If the payment method changed in MultiSafepay payment page, after leave WooCommerce checkout page |
| 317 |
if ( $payment_method_id_to_persist && $payment_method_id_registered_by_wc !== $payment_method_id_to_persist ) { |
| 318 |
$message = 'Callback received with a different payment method for Order ID: ' . $this->woocommerce_order_id . ' and Order Number: ' . $this->multisafepay_order_id . ' on ' . $this->time_stamp . '. Payment method changed from ' . $payment_method_title_registered_by_wc . ' to ' . $payment_method_title_to_display . '.'; |
| 319 |
$this->logger->log_info( $message ); |
| 320 |
OrderUtil::add_order_note( $this->order, $message, true ); |
| 321 |
$this->order = wc_get_order( $this->woocommerce_order_id ); |
| 322 |
$this->order->set_payment_method( $payment_method_object_to_persist ); |
| 323 |
if ( ! empty( $payment_method_title_to_display ) ) { |
| 324 |
$this->order->set_payment_method_title( $payment_method_title_to_display ); |
| 325 |
} |
| 326 |
$this->order->save(); |
| 327 |
} |
| 328 |
|
| 329 |
if ( ! empty( $wallet_payment_method_title ) && ! empty( $payment_method_title_to_display ) && $this->order->get_payment_method_title() !== $payment_method_title_to_display ) { |
| 330 |
$this->order->set_payment_method_title( $payment_method_title_to_display ); |
| 331 |
$this->order->save(); |
| 332 |
} |
| 333 |
|
| 334 |
header( 'Content-type: text/plain' ); |
| 335 |
die( 'OK' ); |
| 336 |
} |
| 337 |
} |
| 338 |
|