swpm-smart-checkout-ipn.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. <?php
  2. require_once 'swpm_handle_subsc_ipn.php';
  3. // Ignoring invalid class name PHPCS warning
  4. class swpm_smart_checkout_ipn_handler { // phpcs:ignore
  5. public $ipn_log = false; // bool: log IPN results to text file?
  6. public $ipn_log_file; // filename of the IPN log.
  7. public $ipn_response; // holds the IPN response from paypal.
  8. public $ipn_data = array(); // array contains the POST values for IPN.
  9. public $fields = array(); // array holds the fields to submit to paypal.
  10. public $sandbox_mode = false;
  11. public function __construct() {
  12. $this->paypal_url = 'https://www.paypal.com/cgi-bin/webscr';
  13. $this->ipn_log_file = 'ipn_handle_debug_swpm.log';
  14. $this->ipn_response = '';
  15. }
  16. public function swpm_validate_and_create_membership() {
  17. // Check Product Name , Price , Currency , Receivers email.
  18. $error_msg = '';
  19. // Read the IPN and validate.
  20. $gross_total = $this->ipn_data['mc_gross'];
  21. $transaction_type = $this->ipn_data['txn_type'];
  22. $txn_id = $this->ipn_data['txn_id'];
  23. $payment_status = $this->ipn_data['payment_status'];
  24. // Check payment status.
  25. if ( ! empty( $payment_status ) ) {
  26. if ( 'Denied' == $payment_status ) {
  27. $this->debug_log( 'Payment status for this transaction is DENIED. You denied the transaction... most likely a cancellation of an eCheque. Nothing to do here.', false );
  28. return false;
  29. }
  30. if ( 'Canceled_Reversal' == $payment_status ) {
  31. $this->debug_log( 'This is a dispute closed notification in your favour. The plugin will not do anyting.', false );
  32. return true;
  33. }
  34. if ( 'Completed' != $payment_status && 'Processed' != $payment_status && 'Refunded' != $payment_status && 'Reversed' != $payment_status ) {
  35. $error_msg .= 'Funds have not been cleared yet. Transaction will be processed when the funds clear!';
  36. $this->debug_log( $error_msg, false );
  37. $this->debug_log( wp_json_encode( $this->ipn_data ), false );
  38. return false;
  39. }
  40. }
  41. // Check txn type.
  42. if ( 'new_case' == $transaction_type ) {
  43. $this->debug_log( 'This is a dispute case. Nothing to do here.', true );
  44. return true;
  45. }
  46. $custom = urldecode( $this->ipn_data['custom'] );
  47. $this->ipn_data['custom'] = $custom;
  48. $customvariables = SwpmTransactions::parse_custom_var( $custom );
  49. // Handle refunds.
  50. if ( $gross_total < 0 ) {
  51. // This is a refund or reversal.
  52. $this->debug_log( 'This is a refund notification. Refund amount: ' . $gross_total, true );
  53. swpm_handle_subsc_cancel_stand_alone( $this->ipn_data, true );
  54. return true;
  55. }
  56. if ( isset( $this->ipn_data['reason_code'] ) && 'refund' == $this->ipn_data['reason_code'] ) {
  57. $this->debug_log( 'This is a refund notification. Refund amount: ' . $gross_total, true );
  58. swpm_handle_subsc_cancel_stand_alone( $this->ipn_data, true );
  59. return true;
  60. }
  61. if ( ( 'subscr_signup' == $transaction_type ) ) {
  62. $this->debug_log( 'Subscription signup IPN received... (handled by the subscription IPN handler)', true );
  63. // Code to handle the signup IPN for subscription.
  64. $subsc_ref = $customvariables['subsc_ref'];
  65. if ( ! empty( $subsc_ref ) ) {
  66. $this->debug_log( 'Found a membership level ID. Creating member account...', true );
  67. $swpm_id = $customvariables['swpm_id'];
  68. swpm_handle_subsc_signup_stand_alone( $this->ipn_data, $subsc_ref, $this->ipn_data['subscr_id'], $swpm_id );
  69. // Handle customized subscription signup.
  70. }
  71. return true;
  72. } elseif ( ( 'subscr_cancel' == $transaction_type ) || ( 'subscr_eot' == $transaction_type ) || ( 'subscr_failed' == $transaction_type ) ) {
  73. // Code to handle the IPN for subscription cancellation.
  74. $this->debug_log( 'Subscription cancellation IPN received... (handled by the subscription IPN handler)', true );
  75. swpm_handle_subsc_cancel_stand_alone( $this->ipn_data );
  76. return true;
  77. } else {
  78. $cart_items = array();
  79. $this->debug_log( 'Transaction Type: Buy Now/Subscribe', true );
  80. $item_number = $this->ipn_data['item_number'];
  81. $item_name = $this->ipn_data['item_name'];
  82. $quantity = $this->ipn_data['quantity'];
  83. $mc_gross = $this->ipn_data['mc_gross'];
  84. $mc_currency = $this->ipn_data['mc_currency'];
  85. $current_item = array(
  86. 'item_number' => $item_number,
  87. 'item_name' => $item_name,
  88. 'quantity' => $quantity,
  89. 'mc_gross' => $mc_gross,
  90. 'mc_currency' => $mc_currency,
  91. );
  92. array_push( $cart_items, $current_item );
  93. }
  94. $counter = 0;
  95. foreach ( $cart_items as $current_cart_item ) {
  96. $cart_item_data_num = $current_cart_item['item_number'];
  97. $cart_item_data_name = trim( $current_cart_item['item_name'] );
  98. $cart_item_data_quantity = $current_cart_item['quantity'];
  99. $cart_item_data_total = $current_cart_item['mc_gross'];
  100. $cart_item_data_currency = $current_cart_item['mc_currency'];
  101. if ( empty( $cart_item_data_quantity ) ) {
  102. $cart_item_data_quantity = 1;
  103. }
  104. $this->debug_log( 'Item Number: ' . $cart_item_data_num, true );
  105. $this->debug_log( 'Item Name: ' . $cart_item_data_name, true );
  106. $this->debug_log( 'Item Quantity: ' . $cart_item_data_quantity, true );
  107. $this->debug_log( 'Item Total: ' . $cart_item_data_total, true );
  108. $this->debug_log( 'Item Currency: ' . $cart_item_data_currency, true );
  109. // Get the button id.
  110. $pp_hosted_button = false;
  111. $button_id = $cart_item_data_num; // Button id is the item number.
  112. $membership_level_id = get_post_meta( $button_id, 'membership_level_id', true );
  113. if ( ! SwpmUtils::membership_level_id_exists( $membership_level_id ) ) {
  114. $this->debug_log( 'This payment button was not created in the plugin. This is a paypal hosted button.', true );
  115. $pp_hosted_button = true;
  116. }
  117. // Price check.
  118. $check_price = true;
  119. $msg = '';
  120. $msg = apply_filters( 'swpm_before_price_check_filter', $msg, $current_cart_item );
  121. if ( ! empty( $msg ) && 'price-check-override' == $msg ) {// This filter allows an extension to do a customized version of price check (if needed).
  122. $check_price = false;
  123. $this->debug_log( 'Price and currency check has been overridden by an addon/extension.', true );
  124. }
  125. if ( $check_price && ! $pp_hosted_button ) {
  126. // Check according to buy now payment or subscription payment.
  127. $button_type = get_post_meta( $button_id, 'button_type', true );
  128. if ( 'pp_smart_checkout' == $button_type ) {// This is a PayPal Smart Checkout type button.
  129. $expected_amount = ( get_post_meta( $button_id, 'payment_amount', true ) ) * $cart_item_data_quantity;
  130. $expected_amount = round( $expected_amount, 2 );
  131. $expected_amount = apply_filters( 'swpm_payment_amount_filter', $expected_amount, $button_id );
  132. $received_amount = $cart_item_data_total;
  133. } else {
  134. $this->debug_log( 'Error! Unexpected button type: ' . $button_type, false );
  135. return false;
  136. }
  137. if ( $received_amount < $expected_amount ) {
  138. // Error! amount received is less than expected. This is invalid.
  139. $this->debug_log( 'Expected amount: ' . $expected_amount, true );
  140. $this->debug_log( 'Received amount: ' . $received_amount, true );
  141. $this->debug_log( 'Price check failed. Amount received is less than the amount expected. This payment will not be processed.', false );
  142. return false;
  143. }
  144. }
  145. // *** Handle Membership Payment ***
  146. // --------------------------------------------------------------------------------------
  147. // ========= Need to find the (level ID) in the custom variable ============
  148. $subsc_ref = $customvariables['subsc_ref']; // Membership level ID.
  149. $this->debug_log( 'Membership payment paid for membership level ID: ' . $subsc_ref, true );
  150. if ( ! empty( $subsc_ref ) ) {
  151. $swpm_id = '';
  152. if ( isset( $customvariables['swpm_id'] ) ) {
  153. $swpm_id = $customvariables['swpm_id'];
  154. }
  155. if ( 'smart_checkout' == $transaction_type ) {
  156. $this->debug_log( 'Transaction type: web_accept. Creating member account...', true );
  157. swpm_handle_subsc_signup_stand_alone( $this->ipn_data, $subsc_ref, $this->ipn_data['txn_id'], $swpm_id );
  158. }
  159. } else {
  160. $this->debug_log( 'Membership level ID is missing in the payment notification! Cannot process this notification.', false );
  161. }
  162. // == End of Membership payment handling ==
  163. $counter++;
  164. }
  165. /*
  166. * * Do Post payment operation and cleanup * *
  167. */
  168. // Save the transaction data.
  169. $this->debug_log( 'Saving transaction data to the database table.', true );
  170. $this->ipn_data['gateway'] = 'pp_smart_checkout';
  171. $this->ipn_data['status'] = $this->ipn_data['payment_status'];
  172. SwpmTransactions::save_txn_record( $this->ipn_data, $cart_items );
  173. $this->debug_log( 'Transaction data saved.', true );
  174. // Trigger the PayPal IPN processed action hook (so other plugins can can listen for this event).
  175. do_action( 'swpm_pp_smart_checkout_ipn_processed', $this->ipn_data );
  176. do_action( 'swpm_payment_ipn_processed', $this->ipn_data );
  177. return true;
  178. }
  179. public function create_ipn_from_smart_checkout( $data ) {
  180. $ipn['custom'] = $data['custom_field'];
  181. $ipn['item_number'] = $data['button_id'];
  182. $ipn['item_name'] = $data['item_name'];
  183. $ipn['pay_id'] = $data['id'];
  184. $ipn['create_time'] = $data['create_time'];
  185. $ipn['txn_id'] = $data['transactions'][0]['related_resources'][0]['sale']['id'];
  186. $ipn['reason_code'] = ! empty( $data['transactions'][0]['related_resources'][0]['sale']['reason_code'] ) ? $data['transactions'][0]['related_resources'][0]['sale']['reason_code'] : '';
  187. $ipn['txn_type'] = 'smart_checkout';
  188. $ipn['payment_status'] = ucfirst( $data['transactions'][0]['related_resources'][0]['sale']['state'] );
  189. $ipn['transaction_subject'] = '';
  190. $ipn['mc_currency'] = $data['transactions'][0]['amount']['currency'];
  191. $ipn['mc_gross'] = $data['transactions'][0]['amount']['total'];
  192. $ipn['quantity'] = 1;
  193. $ipn['receiver_email'] = get_option( 'cart_paypal_email' );
  194. // customer info.
  195. $ipn['first_name'] = $data['payer']['payer_info']['first_name'];
  196. $ipn['last_name'] = $data['payer']['payer_info']['last_name'];
  197. $ipn['payer_email'] = $data['payer']['payer_info']['email'];
  198. $ipn['address_street'] = $data['payer']['payer_info']['shipping_address']['line1'];
  199. $ipn['address_city'] = $data['payer']['payer_info']['shipping_address']['city'];
  200. $ipn['address_state'] = $data['payer']['payer_info']['shipping_address']['state'];
  201. $ipn['address_zip'] = $data['payer']['payer_info']['shipping_address']['postal_code'];
  202. $ipn['address_country'] = $data['payer']['payer_info']['shipping_address']['country_code'];
  203. $this->ipn_data = $ipn;
  204. return true;
  205. }
  206. public function validate_ipn_smart_checkout() {
  207. if ( $this->sandbox_mode ) {
  208. $client_id = get_post_meta( $this->ipn_data['item_number'], 'pp_smart_checkout_test_id', true );
  209. $secret = get_post_meta( $this->ipn_data['item_number'], 'pp_smart_checkout_test_sec', true );
  210. $api_base = 'https://api.sandbox.paypal.com';
  211. } else {
  212. $client_id = get_post_meta( $this->ipn_data['item_number'], 'pp_smart_checkout_live_id', true );
  213. $secret = get_post_meta( $this->ipn_data['item_number'], 'pp_smart_checkout_live_sec', true );
  214. $api_base = 'https://api.paypal.com';
  215. }
  216. $wp_request_headers = array(
  217. 'Accept' => 'application/json',
  218. // Ignoring base64_encode() PHPCS warning as it's being properly used in this case.
  219. 'Authorization' => 'Basic ' . base64_encode( $client_id . ':' . $secret ), // phpcs:ignore
  220. );
  221. $res = wp_remote_request(
  222. $api_base . '/v1/oauth2/token',
  223. array(
  224. 'method' => 'POST',
  225. 'headers' => $wp_request_headers,
  226. 'body' => 'grant_type=client_credentials',
  227. )
  228. );
  229. $code = wp_remote_retrieve_response_code( $res );
  230. if ( 200 !== $code ) {
  231. // Some error occured.
  232. $body = wp_remote_retrieve_body( $res );
  233. // translators: %1$d is error code; %2$s is error message.
  234. return sprintf( __( 'Error occured during payment verification. Error code: %1$d. Message: %2$s', 'simple-membership' ), $code, $body );
  235. }
  236. $body = wp_remote_retrieve_body( $res );
  237. $body = json_decode( $body );
  238. $token = $body->access_token;
  239. $wp_request_headers = array(
  240. 'Accept' => 'application/json',
  241. 'Authorization' => 'Bearer ' . $token,
  242. );
  243. $res = wp_remote_request(
  244. $api_base . '/v1/payments/payment/' . $this->ipn_data['pay_id'],
  245. array(
  246. 'method' => 'GET',
  247. 'headers' => $wp_request_headers,
  248. )
  249. );
  250. $code = wp_remote_retrieve_response_code( $res );
  251. if ( 200 !== $code ) {
  252. // Some error occured.
  253. $body = wp_remote_retrieve_body( $res );
  254. // translators: %1$d is error code; %2$s is error message.
  255. return sprintf( __( 'Error occured during payment verification. Error code: %1$d. Message: %2$s', 'simple-membership' ), $code, $body );
  256. }
  257. $body = wp_remote_retrieve_body( $res );
  258. $body = json_decode( $body );
  259. // check payment details.
  260. if ( $body->transactions[0]->amount->total === $this->ipn_data['mc_gross'] &&
  261. $body->transactions[0]->amount->currency === $this->ipn_data['mc_currency'] ) {
  262. // payment is valid.
  263. return true;
  264. } else {
  265. // payment is invalid.
  266. // translators: %1$s is expected amount, %2$s is expected currency.
  267. return sprintf( __( 'Payment check failed: invalid amount received. Expected %1$s %2$s, got %3$s %4$s.', 'simple-membership' ), $this->ipn_data['mc_gross'], $this->ipn_data['mc_currency'], $body->transactions[0]->amount->total, $body->transactions[0]->amount->currency );
  268. }
  269. }
  270. public function debug_log( $message, $success, $end = false ) {
  271. SwpmLog::log_simple_debug( $message, $success, $end );
  272. }
  273. }
  274. function swpm_pp_smart_checkout_ajax_hanlder() {
  275. // Start of IPN handling (script execution).
  276. // check nonce.
  277. $uniqid = filter_input( INPUT_POST, 'uniqid', FILTER_SANITIZE_STRING );
  278. if ( ! check_ajax_referer( 'swpm-pp-smart-checkout-ajax-nonce-' . $uniqid, 'nonce', false ) ) {
  279. wp_send_json(
  280. array(
  281. 'success' => false,
  282. 'errMsg' => __(
  283. 'Nonce check failed. Please reload page.',
  284. 'simple-membership'
  285. ),
  286. )
  287. );
  288. exit;
  289. }
  290. $data = filter_input( INPUT_POST, 'swpm_pp_smart_checkout_payment_data', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY );
  291. if ( empty( $data ) ) {
  292. wp_send_json(
  293. array(
  294. 'success' => false,
  295. 'errMsg' => __(
  296. 'Empty payment data received.',
  297. 'simple-membership'
  298. ),
  299. )
  300. );
  301. }
  302. $ipn_handler_instance = new swpm_smart_checkout_ipn_handler();
  303. $ipn_data_success = $ipn_handler_instance->create_ipn_from_smart_checkout( $data );
  304. if ( true !== $ipn_data_success ) {
  305. // error occured during IPN array creation.
  306. wp_send_json(
  307. array(
  308. 'success' => false,
  309. 'errMsg' => $ipn_data_success,
  310. )
  311. );
  312. }
  313. $settings = SwpmSettings::get_instance();
  314. $debug_enabled = $settings->get_value( 'enable-debug' );
  315. if ( ! empty( $debug_enabled ) ) {// debug is enabled in the system.
  316. $debug_log = 'log.txt'; // Debug log file name.
  317. $ipn_handler_instance->ipn_log = true;
  318. $ipn_handler_instance->ipn_log_file = $debug_log;
  319. }
  320. $sandbox_enabled = $settings->get_value( 'enable-sandbox-testing' );
  321. if ( ! empty( $sandbox_enabled ) ) { // Sandbox testing enabled.
  322. $ipn_handler_instance->paypal_url = 'https://www.sandbox.paypal.com/cgi-bin/webscr';
  323. $ipn_handler_instance->sandbox_mode = true;
  324. }
  325. $ip = filter_input( INPUT_SERVER, 'REMOTE_ADDR', FILTER_SANITIZE_STRING );
  326. $ipn_handler_instance->debug_log( 'Paypal Smart Checkout Class Initiated by ' . $ip, true );
  327. // Validate the IPN.
  328. $res = $ipn_handler_instance->validate_ipn_smart_checkout();
  329. if ( true !== $res ) {
  330. wp_send_json(
  331. array(
  332. 'success' => false,
  333. 'errMsg' => $res,
  334. )
  335. );
  336. }
  337. $ipn_handler_instance->debug_log( 'Creating product Information to send.', true );
  338. if ( ! $ipn_handler_instance->swpm_validate_and_create_membership() ) {
  339. $ipn_handler_instance->debug_log( 'IPN product validation failed.', false );
  340. wp_send_json(
  341. array(
  342. 'success' => false,
  343. 'errMsg' => __(
  344. 'IPN product validation failed. Check debug log for more details.',
  345. 'simple-membership'
  346. ),
  347. )
  348. );
  349. }
  350. $ipn_handler_instance->debug_log( 'Paypal class finished.', true, true );
  351. wp_send_json( array( 'success' => true ) );
  352. }