PluginProbe
MultiSafepay plugin for WooCommerce / trunk
MultiSafepay plugin for WooCommerce vtrunk
6.11.1 6.12.0 6.13.0 6.2.0 6.2.1 6.3.0 6.3.1 6.4.0 6.4.1 6.4.2 6.4.3 6.5.0 6.5.1 6.6.0 6.6.1 6.6.2 6.7.0 6.7.1 6.7.2 6.7.3 6.8.0 6.8.1 6.8.2 6.8.3 6.9.0 All 84 releases
multisafepay / src / Services / OrderService.php

OrderService.php in MultiSafepay plugin for WooCommerce trunk, at src/Services/OrderService.php

326 lines 13.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php declare(strict_types=1);
2
3 namespace MultiSafepay\WooCommerce\Services;
4
5 use MultiSafepay\Api\Transactions\Gateways as GatewaysSdk;
6 use MultiSafepay\Api\Transactions\OrderRequest;
7 use MultiSafepay\Api\Transactions\OrderRequest\Arguments\GatewayInfo\Wallet;
8 use MultiSafepay\Api\Transactions\OrderRequest\Arguments\PaymentOptions;
9 use MultiSafepay\Api\Transactions\OrderRequest\Arguments\PluginDetails;
10 use MultiSafepay\Api\Transactions\OrderRequest\Arguments\SecondChance;
11 use MultiSafepay\Api\Transactions\OrderRequest\Arguments\TaxTable\TaxRate;
12 use MultiSafepay\Api\Transactions\OrderRequest\Arguments\TaxTable\TaxRule;
13 use MultiSafepay\Exception\InvalidArgumentException;
14 use MultiSafepay\WooCommerce\Utils\MoneyUtil;
15 use MultiSafepay\WooCommerce\Services\Blocks\BlocksContextService;
16 use WC_Order;
17
18 /**
19 * Class OrderService
20 *
21 * @package MultiSafepay\WooCommerce\Services
22 */
23 class OrderService {
24
25 /**
26 * @var CustomerService
27 */
28 public $customer_service;
29
30 /**
31 * @var ShoppingCartService
32 */
33 public $shopping_cart_service;
34
35 /**
36 * @var PaymentMethodService
37 */
38 public $payment_method_service;
39
40 /**
41 * @var BlocksPaymentDataService
42 */
43 public $blocks_payment_data_service;
44
45 /**
46 * OrderService constructor.
47 */
48 public function __construct() {
49 $this->customer_service = new CustomerService();
50 $this->shopping_cart_service = new ShoppingCartService();
51 $this->payment_method_service = new PaymentMethodService();
52 $this->blocks_payment_data_service = new BlocksPaymentDataService();
53 }
54
55 /**
56 * Wallet payloads (Google Pay token, etc.) are JSON strings.
57 * Treat them as opaque: do not run sanitize_text_field() to avoid corrupting JSON.
58 *
59 * @param string $value
60 * @return string
61 */
62 private function normalize_wallet_payload( string $value ): string {
63 $value = trim( $value );
64 if ( '' === $value ) {
65 return '';
66 }
67
68 // Remove null bytes to avoid storage / transport issues.
69 $value = str_replace( "\0", '', $value );
70
71 // Defensive limit: Google Pay tokens are ~1-3KB; allow plenty.
72 if ( strlen( $value ) > 20000 ) {
73 $value = substr( $value, 0, 20000 );
74 }
75
76 return $value;
77 }
78
79 /**
80 * Get a wallet payment token from the current request or from Blocks order meta.
81 *
82 * Wallet Direct (Apple Pay / Google Pay) can send the token either as:
83 * - $_POST['payment_token'] (legacy)
84 * - $_POST['<payment_method_id>_payment_token'] (Blocks/JS)
85 * - Order meta '_multisafepay_blocks_payment_data[<payment_method_id>_payment_token]' (Blocks persisted)
86 *
87 * @param WC_Order $order
88 * @return string
89 */
90 public function get_wallet_payment_token( WC_Order $order ): string {
91 $payment_method_id = (string) $order->get_payment_method();
92
93 $is_store_api_request = ( new BlocksContextService() )->is_store_api_request();
94 $read_request_value = static function ( string $key ) use ( $is_store_api_request ): string {
95 if ( ! isset( $_POST[ $key ] ) ) {
96 return '';
97 }
98
99 // Store API requests are already unslashed by the REST layer.
100 // Do not modify opaque wallet payloads (tokens/JSON).
101 // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
102 $value = '';
103
104 if ( $is_store_api_request ) {
105 $value = (string) $_POST[ $key ]; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
106 }
107
108 if ( ! $is_store_api_request ) {
109 $value = (string) wp_unslash( $_POST[ $key ] );
110 }
111 // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
112
113 return $value;
114 };
115
116 $payment_token = $this->normalize_wallet_payload( $read_request_value( 'payment_token' ) );
117 if ( ! empty( $payment_token ) ) {
118 return $payment_token;
119 }
120
121 $payment_method_wallet_key = $payment_method_id . '_payment_token';
122 $payment_token_from_method_key = $this->normalize_wallet_payload( $read_request_value( $payment_method_wallet_key ) );
123 if ( ! empty( $payment_token_from_method_key ) ) {
124 return $payment_token_from_method_key;
125 }
126
127 $token_from_blocks_method_key = $this->normalize_wallet_payload(
128 (string) $this->blocks_payment_data_service->get_blocks_payment_data_value( $order, $payment_method_wallet_key )
129 );
130 if ( ! empty( $token_from_blocks_method_key ) ) {
131 return $token_from_blocks_method_key;
132 }
133
134 // Legacy-compatible fallback for Blocks: some flows can send/store just `payment_token`.
135 return $this->normalize_wallet_payload(
136 (string) $this->blocks_payment_data_service->get_blocks_payment_data_value( $order, 'payment_token' )
137 );
138 }
139
140 /**
141 * @param WC_Order $order
142 * @param string $gateway_code
143 * @param string $type
144 * @return OrderRequest
145 * @throws InvalidArgumentException
146 */
147 public function create_order_request( WC_Order $order, string $gateway_code, string $type ): OrderRequest {
148 $order_request = new OrderRequest();
149 $order_request
150 ->addOrderId( $order->get_order_number() )
151 ->addMoney( MoneyUtil::create_money( (float) ( $order->get_total() ), $order->get_currency() ) )
152 ->addGatewayCode( $gateway_code )
153 ->addType( $type )
154 ->addPluginDetails( $this->create_plugin_details() )
155 ->addDescriptionText( $this->get_order_description_text( $order->get_order_number() ) )
156 ->addCustomer( $this->customer_service->create_customer_details( $order ) )
157 ->addPaymentOptions( $this->create_payment_options( $order ) )
158 ->addSecondsActive( $this->get_seconds_active() )
159 ->addSecondChance( ( new SecondChance() )->addSendEmail( (bool) get_option( 'multisafepay_second_chance', false ) ) )
160 ->addData( array( 'var2' => $order->get_id() ) );
161
162 if ( $order->needs_shipping_address() && $order->has_shipping_address() ) {
163 $order_request->addDelivery( $this->customer_service->create_delivery_details( $order ) );
164 }
165
166 if ( ! get_option( 'multisafepay_disable_shopping_cart', false ) || in_array( $gateway_code, GatewaysSdk::SHOPPING_CART_REQUIRED_GATEWAYS, true ) ) {
167 $order_request->addShoppingCart( $this->shopping_cart_service->create_shopping_cart( $order, $order->get_currency(), $gateway_code ) );
168 }
169
170 $payment_method_id = $order->get_payment_method();
171 $payment_component_payload_key = $payment_method_id . '_payment_component_payload';
172
173 $payment_component_payload = sanitize_text_field( wp_unslash( $_POST[ $payment_component_payload_key ] ?? '' ) );
174 if ( empty( $payment_component_payload ) ) {
175 $payment_component_payload = $this->blocks_payment_data_service->get_blocks_payment_data_value( $order, $payment_component_payload_key );
176 }
177
178 if ( ! empty( $payment_component_payload ) ) {
179 $order_request->addType( 'direct' );
180 $order_request->addData(
181 array(
182 'payment_data' => array(
183 'payload' => $payment_component_payload,
184 ),
185 )
186 );
187 }
188
189 $payment_token = $this->get_wallet_payment_token( $order );
190 if ( ! empty( $payment_token ) && ( ( 'APPLEPAY' === $gateway_code ) || ( 'GOOGLEPAY' === $gateway_code ) ) ) {
191 $order_request->addType( 'direct' );
192 $order_request->addGatewayInfo( ( new Wallet() )->addPaymentToken( $payment_token ) );
193 }
194
195 // Force BILLINK B2B and B2C transactions to be direct
196 if ( 'BILLINK' === $gateway_code ) {
197 $payment_method_id = $order->get_payment_method();
198 $payment_method = $this->payment_method_service->get_woocommerce_payment_gateway_by_id( $payment_method_id );
199
200 if ( $payment_method ) {
201 $payment_component_option = $payment_method->get_option( 'payment_component', 'no' );
202
203 if ( 'P' === $payment_component_option || 'B' === $payment_component_option ) {
204 $order_request->addType( 'direct' );
205 }
206 }
207 }
208
209 $order_request = $this->add_none_tax_rate( $order_request );
210
211 return apply_filters( 'multisafepay_order_request', $order_request );
212 }
213
214 /**
215 * @return PluginDetails
216 */
217 protected function create_plugin_details(): PluginDetails {
218 $plugin_details = new PluginDetails();
219 global $wp_version;
220 return $plugin_details
221 ->addApplicationName( 'Wordpress-WooCommerce' )
222 ->addApplicationVersion( 'WordPress version: ' . $wp_version . '. WooCommerce version: ' . WC_VERSION )
223 ->addPluginVersion( MULTISAFEPAY_PLUGIN_VERSION )
224 ->addShopRootUrl( get_bloginfo( 'url' ) );
225 }
226
227 /**
228 * @param WC_Order $order
229 * @return PaymentOptions
230 * @throws InvalidArgumentException
231 */
232 private function create_payment_options( WC_Order $order ): PaymentOptions {
233 $payment_options = new PaymentOptions();
234 $payment_options->addNotificationUrl( get_rest_url( get_current_blog_id(), 'multisafepay/v1/notification' ) );
235
236 $cancel_endpoint = ( get_option( 'multisafepay_redirect_after_cancel', 'cart' ) === 'cart' ? '' : wc_get_checkout_url() );
237 $cancel_url = wp_specialchars_decode( $order->get_cancel_order_url( $cancel_endpoint ) );
238
239 if ( is_wc_endpoint_url( 'order-pay' ) ) {
240 $cancel_url = wp_specialchars_decode( $order->get_checkout_payment_url() );
241 }
242
243 $payment_options->addCancelUrl( $cancel_url );
244 $payment_options->addRedirectUrl( $order->get_checkout_order_received_url() );
245 if ( ! apply_filters( 'multisafepay_post_notification', true ) ) {
246 $payment_options->addNotificationUrl( add_query_arg( 'wc-api', 'multisafepay', home_url( '/' ) ) );
247 $payment_options->addNotificationMethod( 'GET' );
248 }
249
250 // Add BILLINK specific settings
251 $payment_method_id = $order->get_payment_method();
252 $payment_method = $this->payment_method_service->get_woocommerce_payment_gateway_by_id( $payment_method_id );
253
254 if ( $payment_method && 'BILLINK' === $payment_method->get_payment_method_gateway_code() ) {
255 $payment_component_option = $payment_method->get_option( 'payment_component', 'no' );
256
257 if ( 'P' === $payment_component_option || 'B' === $payment_component_option ) {
258 $payment_options->addSettings( array( 'gateways' => array( 'BILLINK' => array( 'type' => $payment_component_option ) ) ) );
259 }
260 }
261
262 return $payment_options;
263 }
264
265 /**
266 * Return the order description.
267 *
268 * @param string $order_number
269 * @return string $order_description
270 */
271 protected function get_order_description_text( string $order_number ): string {
272 /* translators: %s: order id */
273 $order_description = sprintf( __( 'Payment for order: %s', 'multisafepay' ), $order_number );
274 if ( get_option( 'multisafepay_order_request_description', false ) ) {
275 $order_description = str_replace( '{order_number}', $order_number, get_option( 'multisafepay_order_request_description', false ) );
276 }
277 return $order_description;
278 }
279
280 /**
281 * Return the time active in seconds defined in the plugin settings page
282 *
283 * @return int
284 */
285 protected function get_seconds_active(): int {
286 $time_active = get_option( 'multisafepay_time_active', '30' );
287 $time_active_unit = get_option( 'multisafepay_time_unit', 'days' );
288 if ( 'days' === $time_active_unit ) {
289 $time_active = $time_active * 24 * 60 * 60;
290 }
291 if ( 'hours' === $time_active_unit ) {
292 $time_active = $time_active * 60 * 60;
293 }
294 return $time_active;
295 }
296
297 /**
298 * This method adds a tax rate of 0, in case is not being created automatically by the shopping cart.
299 * This is required to process refunds, based on shopping cart items
300 *
301 * @param OrderRequest $order_request
302 * @return OrderRequest
303 * @throws InvalidArgumentException
304 */
305 public function add_none_tax_rate( OrderRequest $order_request ): OrderRequest {
306 if ( $order_request->getShoppingCart() === null ) {
307 return $order_request;
308 }
309 if ( $order_request->getCheckoutOptions()->getTaxTable() === null ) {
310 return $order_request;
311 }
312 $shopping_cart = $order_request->getShoppingCart()->getData();
313 if ( isset( $shopping_cart['items'] ) ) {
314 foreach ( $shopping_cart['items'] as $item ) {
315 if ( '0' === $item['tax_table_selector'] ) {
316 return $order_request;
317 }
318 }
319 }
320 $tax_rate = ( new TaxRate() )->addRate( 0 );
321 $tax_rule = ( new TaxRule() )->addTaxRate( $tax_rate )->addName( '0' );
322 $order_request->getCheckoutOptions()->getTaxTable()->addTaxRule( $tax_rule );
323 return $order_request;
324 }
325 }
326