class-wcs-paypal.php
19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
<?php
/**
* PayPal Subscription Class.
*
* Filters necessary functions in the WC_Paypal class to allow for subscriptions, either via PayPal Standard (default)
* or PayPal Express Checkout using Reference Transactions (preferred)
*
* @package WooCommerce Subscriptions
* @subpackage Gateways/PayPal
* @category Class
* @author Prospress
* @since 2.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
require_once( 'includes/wcs-paypal-functions.php' );
require_once( 'includes/class-wcs-paypal-supports.php' );
require_once( 'includes/class-wcs-paypal-status-manager.php' );
require_once( 'includes/class-wcs-paypal-standard-switcher.php' );
require_once( 'includes/class-wcs-paypal-standard-request.php' );
require_once( 'includes/class-wcs-paypal-standard-change-payment-method.php' );
require_once( 'includes/admin/class-wcs-paypal-admin.php' );
require_once( 'includes/admin/class-wcs-paypal-change-payment-method-admin.php' );
require_once( 'includes/deprecated/class-wc-paypal-standard-subscriptions.php' );
class WCS_PayPal {
/** @var WCS_PayPal_Express_API for communicating with PayPal */
protected static $api;
/** @var WCS_PayPal single instance of this class */
protected static $instance;
/** @var Array cache of PayPal IPN Handler */
protected static $ipn_handlers;
/** @var Array cache of PayPal Standard settings in WooCommerce */
protected static $paypal_settings;
/**
* Main PayPal Instance, ensures only one instance is/can be loaded
*
* @see wc_paypal_express()
* @return WC_PayPal_Express
* @since 2.0
*/
public static function instance() {
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Bootstraps the class and hooks required actions & filters.
*
* @since 2.0
*/
public static function init() {
self::$paypal_settings = self::get_options();
// wc-api handler for express checkout transactions
if ( ! has_action( 'woocommerce_api_wcs_paypal' ) ) {
add_action( 'woocommerce_api_wcs_paypal', __CLASS__ . '::handle_wc_api' );
}
// When necessary, set the PayPal args to be for a subscription instead of shopping cart
add_action( 'woocommerce_update_options_payment_gateways_paypal', __CLASS__ . '::reload_options', 100 );
// When necessary, set the PayPal args to be for a subscription instead of shopping cart
add_action( 'woocommerce_update_options_payment_gateways_paypal', __CLASS__ . '::are_reference_transactions_enabled', 100 );
// When necessary, set the PayPal args to be for a subscription instead of shopping cart
add_filter( 'woocommerce_paypal_args', __CLASS__ . '::get_paypal_args', 10, 2 );
// Check a valid PayPal IPN request to see if it's a subscription *before* WCS_Gateway_Paypal::successful_request()
add_action( 'valid-paypal-standard-ipn-request', __CLASS__ . '::process_ipn_request', 0 );
add_action( 'woocommerce_scheduled_subscription_payment_paypal', __CLASS__ . '::process_subscription_payment', 10, 2 );
// Don't copy over PayPal details to Resubscribe Orders
add_filter( 'wcs_resubscribe_order_created', __CLASS__ . '::remove_resubscribe_order_meta', 10, 2 );
// Triggered by WCS_SV_API_Base::broadcast_request() whenever an API request is made
add_action( 'wc_paypal_api_request_performed', __CLASS__ . '::log_api_requests', 10, 2 );
add_filter( 'woocommerce_subscriptions_admin_meta_boxes_script_parameters', __CLASS__ . '::maybe_add_change_payment_method_warning' );
WCS_PayPal_Supports::init();
WCS_PayPal_Status_Manager::init();
WCS_PayPal_Standard_Switcher::init();
if ( is_admin() ) {
WCS_PayPal_Admin::init();
WCS_PayPal_Change_Payment_Method_Admin::init();
}
}
/**
* Get a WooCommerce setting value for the PayPal Standard Gateway
*
* @since 2.0
*/
public static function get_option( $setting_key ) {
return ( isset( self::$paypal_settings[ $setting_key ] ) ) ? self::$paypal_settings[ $setting_key ] : '';
}
/**
* Checks if the PayPal API credentials are set.
*
* @since 2.0
*/
public static function are_credentials_set() {
$credentials_are_set = false;
if ( '' !== self::get_option( 'api_username' ) && '' !== self::get_option( 'api_password' ) && '' !== self::get_option( 'api_signature' ) ) {
$credentials_are_set = true;
}
return apply_filters( 'wooocommerce_paypal_credentials_are_set', $credentials_are_set );
}
/**
* Checks if the PayPal account has reference transactions setup
*
* Subscriptions keeps a record of all accounts where reference transactions were found to be enabled just in case the
* store manager switches to and from accounts. This record is stored as a JSON encoded array in the options table.
*
* @since 2.0
*/
public static function are_reference_transactions_enabled( $bypass_cache = '' ) {
$api_username = self::get_option( 'api_username' );
$transient_key = 'wcs_paypal_rt_enabled';
$reference_transactions_enabled = false;
if ( self::are_credentials_set() ) {
$accounts_with_reference_transactions_enabled = json_decode( get_option( 'wcs_paypal_rt_enabled_accounts' , wcs_json_encode( array() ) ) );
if ( in_array( $api_username, $accounts_with_reference_transactions_enabled ) ) {
$reference_transactions_enabled = true;
} elseif ( 'bypass_cache' === $bypass_cache || get_transient( $transient_key ) !== $api_username ) {
if ( self::get_api()->are_reference_transactions_enabled() ) {
$accounts_with_reference_transactions_enabled[] = $api_username;
update_option( 'wcs_paypal_rt_enabled_accounts', wcs_json_encode( $accounts_with_reference_transactions_enabled ) );
$reference_transactions_enabled = true;
} else {
set_transient( $transient_key, $api_username, DAY_IN_SECONDS );
}
}
}
return apply_filters( 'wooocommerce_subscriptions_paypal_reference_transactions_enabled', $reference_transactions_enabled );
}
/**
* Handle WC API requests where we need to run a reference transaction API operation
*
* @since 2.0
*/
public static function handle_wc_api() {
if ( ! isset( $_GET['action'] ) ) {
return;
}
switch ( $_GET['action'] ) {
// called when the customer is returned from PayPal after authorizing their payment, used for retrieving the customer's checkout details
case 'create_billing_agreement' :
// bail if no token
if ( ! isset( $_GET['token'] ) ) {
return;
}
// get token to retrieve checkout details with
$token = esc_attr( $_GET['token'] );
try {
$express_checkout_details_response = self::get_api()->get_express_checkout_details( $token );
// Make sure the billing agreement was accepted
if ( 1 == $express_checkout_details_response->get_billing_agreement_status() ) {
$order = $express_checkout_details_response->get_order();
if ( is_null( $order ) ) {
throw new Exception( __( 'Unable to find order for PayPal billing agreement.', 'woocommerce-subscriptions' ) );
}
// we need to process an initial payment
if ( $order->get_total() > 0 && ! wcs_is_subscription( $order ) ) {
$billing_agreement_response = self::get_api()->do_express_checkout( $token, $order, array(
'payment_action' => 'Sale',
'payer_id' => $express_checkout_details_response->get_payer_id(),
) );
} else {
$billing_agreement_response = self::get_api()->create_billing_agreement( $token );
}
if ( $billing_agreement_response->has_api_error() ) {
throw new Exception( $billing_agreement_response->get_api_error_message(), $billing_agreement_response->get_api_error_code() );
}
// We're changing the payment method for a subscription, make sure we update it before updating the billing agreement ID so that an old PayPal subscription can be cancelled if the existing payment method is also PayPal
if ( wcs_is_subscription( $order ) ) {
WC_Subscriptions_Change_Payment_Gateway::update_payment_method( $order, 'paypal' );
$redirect_url = add_query_arg( 'utm_nooverride', '1', $order->get_view_order_url() );
}
// Store the billing agreement ID on the order and subscriptions
wcs_set_paypal_id( $order, $billing_agreement_response->get_billing_agreement_id() );
foreach ( wcs_get_subscriptions_for_order( $order, array( 'order_type' => 'any' ) ) as $subscription ) {
wcs_set_paypal_id( $subscription, $billing_agreement_response->get_billing_agreement_id() );
}
if ( ! wcs_is_subscription( $order ) ) {
if ( 0 == $order->get_total() ) {
$order->payment_complete();
} else {
self::process_subscription_payment_response( $order, $billing_agreement_response );
}
$redirect_url = add_query_arg( 'utm_nooverride', '1', $order->get_checkout_order_received_url() );
}
// redirect customer to order received page
wp_safe_redirect( esc_url_raw( $redirect_url ) );
} else {
wp_safe_redirect( WC()->cart->get_cart_url() );
}
} catch ( Exception $e ) {
wc_add_notice( __( 'An error occurred, please try again or try an alternate form of payment.', 'woocommerce-subscriptions' ), 'error' );
wp_redirect( WC()->cart->get_cart_url() );
}
exit;
case 'reference_transaction_account_check' :
exit;
}
}
/**
* Override the default PayPal standard args in WooCommerce for subscription purchases when
* automatic payments are enabled and when the recurring order totals is over $0.00 (because
* PayPal doesn't support subscriptions with a $0 recurring total, we need to circumvent it and
* manage it entirely ourselves.)
*
* @since 2.0
*/
public static function get_paypal_args( $paypal_args, $order ) {
if ( wcs_order_contains_subscription( $order, array( 'parent', 'renewal', 'resubscribe', 'switch' ) ) || wcs_is_subscription( $order ) ) {
if ( self::are_reference_transactions_enabled() ) {
$paypal_args = self::get_api()->get_paypal_args( $paypal_args, $order );
} else {
$paypal_args = WCS_PayPal_Standard_Request::get_paypal_args( $paypal_args, $order );
}
}
return $paypal_args;
}
/**
* When a PayPal IPN messaged is received for a subscription transaction,
* check the transaction details and
*
* @link https://developer.paypal.com/docs/classic/ipn/integration-guide/IPNandPDTVariables/
*
* @since 2.0
*/
public static function process_ipn_request( $transaction_details ) {
require_once( 'includes/class-wcs-paypal-standard-ipn-handler.php' );
require_once( 'includes/class-wcs-paypal-reference-transaction-ipn-handler.php' );
if ( ! isset( $transaction_details['txn_type'] ) || ! in_array( $transaction_details['txn_type'], array_merge( self::get_ipn_handler( 'standard' )->get_transaction_types(), self::get_ipn_handler( 'reference' )->get_transaction_types() ) ) ) {
return;
}
WC_Gateway_Paypal::log( 'Subscription Transaction Type: ' . $transaction_details['txn_type'] );
WC_Gateway_Paypal::log( 'Subscription Transaction Details: ' . print_r( $transaction_details, true ) );
if ( in_array( $transaction_details['txn_type'], self::get_ipn_handler( 'standard' )->get_transaction_types() ) ) {
self::get_ipn_handler( 'standard' )->valid_response( $transaction_details );
} elseif ( in_array( $transaction_details['txn_type'], self::get_ipn_handler( 'reference' )->get_transaction_types() ) ) {
self::get_ipn_handler( 'reference' )->valid_response( $transaction_details );
}
}
/**
* Check whether a given subscription is using reference transactions and if so process the payment.
*
* @since 2.0
*/
public static function process_subscription_payment( $amount, $order ) {
// If the subscription is using reference transactions, we can process the payment ourselves
$paypal_profile_id = wcs_get_paypal_id( $order->id );
if ( wcs_is_paypal_profile_a( $paypal_profile_id, 'billing_agreement' ) ) {
if ( 0 == $amount ) {
$order->payment_complete();
return;
}
$response = self::get_api()->do_reference_transaction( $paypal_profile_id, $order, array(
'amount' => $amount,
'invoice_number' => self::get_option( 'invoice_prefix' ) . wcs_str_to_ascii( ltrim( $order->get_order_number(), _x( '#', 'hash before the order number. Used as a character to remove from the actual order number', 'woocommerce-subscriptions' ) ) ),
) );
self::process_subscription_payment_response( $order, $response );
}
}
/**
* Process a payment based on a response
*
* @since 2.0.9
*/
public static function process_subscription_payment_response( $order, $response ) {
if ( $response->has_api_error() ) {
$error_message = $response->get_api_error_message();
// Some PayPal error messages end with a fullstop, others do not, we prefer our punctuation consistent, so add one if we don't already have one.
if ( '.' !== substr( $error_message, -1 ) ) {
$error_message .= '.';
}
// translators: placeholders are PayPal API error code and PayPal API error message
$order->update_status( 'failed', sprintf( __( 'PayPal API error: (%d) %s', 'woocommerce-subscriptions' ), $response->get_api_error_code(), $error_message ) );
} elseif ( $response->transaction_held() ) {
// translators: placeholder is PayPal transaction status message
$order_note = sprintf( __( 'PayPal Transaction Held: %s', 'woocommerce-subscriptions' ), $response->get_status_message() );
$order_status = apply_filters( 'wcs_paypal_held_payment_order_status', 'on-hold', $order, $response );
// mark order as held
if ( ! $order->has_status( $order_status ) ) {
$order->update_status( $order_status, $order_note );
} else {
$order->add_order_note( $order_note );
}
} elseif ( ! $response->transaction_approved() ) {
// translators: placeholder is PayPal transaction status message
$order->update_status( 'failed', sprintf( __( 'PayPal payment declined: %s', 'woocommerce-subscriptions' ), $response->get_status_message() ) );
} elseif ( $response->transaction_approved() ) {
$order->add_order_note( sprintf( __( 'PayPal payment approved (ID: %s)', 'woocommerce-subscriptions' ), $response->get_transaction_id() ) );
$order->payment_complete( $response->get_transaction_id() );
}
}
/**
* Don't transfer PayPal meta to resubscribe orders.
*
* @param object $resubscribe_order The order created for resubscribing the subscription
* @param object $subscription The subscription to which the resubscribe order relates
* @return object
* @since 2.0
*/
public static function remove_resubscribe_order_meta( $resubscribe_order, $subscription ) {
$post_meta_keys = array(
'Transaction ID',
'Payer first name',
'Payer last name',
'Payer PayPal address',
'Payer PayPal first name',
'Payer PayPal last name',
'PayPal Subscriber ID',
'Payment type',
);
foreach ( $post_meta_keys as $post_meta_key ) {
delete_post_meta( $resubscribe_order->id, $post_meta_key );
}
return $resubscribe_order;
}
/**
* Maybe adds a warning message to subscription script parameters which is used in a Javascript dialog if the
* payment method of the subscription is set to be changed. The warning message is only added if the subscriptions
* payment gateway is PayPal Standard.
*
* @param array $script_parameters The script parameters used in subscription meta boxes.
* @return array $script_parameters
* @since 2.0
*/
public static function maybe_add_change_payment_method_warning( $script_parameters ) {
global $post;
$subscription = wcs_get_subscription( $post );
if ( 'paypal' === $subscription->payment_method ) {
$paypal_profile_id = wcs_get_paypal_id( $subscription->id );
$is_paypal_standard = ! wcs_is_paypal_profile_a( $paypal_profile_id, 'billing_agreement' );
if ( $is_paypal_standard ) {
$script_parameters['change_payment_method_warning'] = __( "Are you sure you want to change the payment method from PayPal standard?\n\nThis will suspend the subscription at PayPal.", 'woocommerce-subscriptions' );
}
}
return $script_parameters;
}
/** Getters ******************************************************/
/**
* Get the API object
*
* @see SV_WC_Payment_Gateway::get_api()
* @return WC_PayPal_Express_API API instance
* @since 2.0
*/
protected static function get_ipn_handler( $ipn_type = 'standard' ) {
$use_sandbox = ( 'yes' === self::get_option( 'testmode' ) ) ? true : false;
if ( 'reference' === $ipn_type ) {
if ( ! isset( self::$ipn_handlers['reference'] ) ) {
require_once( 'includes/class-wcs-paypal-reference-transaction-ipn-handler.php' );
self::$ipn_handlers['reference'] = new WCS_Paypal_Reference_Transaction_IPN_Handler( $use_sandbox, self::get_option( 'receiver_email' ) );
}
$ipn_handler = self::$ipn_handlers['reference'];
} else {
if ( ! isset( self::$ipn_handlers['standard'] ) ) {
require_once( 'includes/class-wcs-paypal-standard-ipn-handler.php' );
self::$ipn_handlers['standard'] = new WCS_Paypal_Standard_IPN_Handler( $use_sandbox, self::get_option( 'receiver_email' ) );
}
$ipn_handler = self::$ipn_handlers['standard'];
}
return $ipn_handler;
}
/**
* Get the API object
*
* @return WCS_PayPal_Express_API API instance
* @since 2.0
*/
public static function get_api() {
if ( is_object( self::$api ) ) {
return self::$api;
}
if ( ! class_exists( 'WC_Gateway_Paypal_Response' ) ) {
require_once( WC()->plugin_path() . '/includes/gateways/paypal/includes/class-wc-gateway-paypal-response.php' );
}
$classes = array(
'api',
'api-request',
'api-response',
'api-response-checkout',
'api-response-billing-agreement',
'api-response-payment',
'api-response-recurring-payment',
);
foreach ( $classes as $class ) {
require_once( "includes/class-wcs-paypal-reference-transaction-{$class}.php" );
}
$environment = ( 'yes' === self::get_option( 'testmode' ) ) ? 'sandbox' : 'production';
return self::$api = new WCS_PayPal_Reference_Transaction_API( 'paypal', $environment, self::get_option( 'api_username' ), self::get_option( 'api_password' ), self::get_option( 'api_signature' ) );
}
/**
* Return the default WC PayPal gateway's settings.
*
* @since 2.0
*/
public static function reload_options() {
self::get_options();
}
/**
* Return the default WC PayPal gateway's settings.
*
* @since 2.0
*/
protected static function get_options() {
self::$paypal_settings = get_option( 'woocommerce_paypal_settings' );
return self::$paypal_settings;
}
/** Logging **/
/**
* Log API request/response data
*
* @since 2.0
*/
public static function log_api_requests( $request_data, $response_data ) {
WC_Gateway_Paypal::log( 'Subscription Request Parameters: ' . print_r( $request_data, true ) );
WC_Gateway_Paypal::log( 'Subscription Request Response: ' . print_r( $response_data, true ) );
}
/** Method required by WCS_SV_API_Base, which normally requires an instance of SV_WC_Plugin **/
public function get_plugin_name() {
return _x( 'WooCommerce Subscriptions PayPal', 'used in User Agent data sent to PayPal to help identify where a payment came from', 'woocommerce-subscriptions' );
}
public function get_version() {
return WC_Subscriptions::$version;
}
public function get_id() {
return 'paypal';
}
}