From acd7889d7fee1e1dbedb905476b3d6689e458bd0 Mon Sep 17 00:00:00 2001 From: Dr Knudsen Date: Tue, 18 Feb 2020 10:45:30 +0100 Subject: [PATCH 1/3] Update class-wc-gateway-reepay-checkout.php --- includes/class-wc-gateway-reepay-checkout.php | 96 +++++++++++++++++-- 1 file changed, 87 insertions(+), 9 deletions(-) diff --git a/includes/class-wc-gateway-reepay-checkout.php b/includes/class-wc-gateway-reepay-checkout.php index 483b6942..0e8b1b1d 100755 --- a/includes/class-wc-gateway-reepay-checkout.php +++ b/includes/class-wc-gateway-reepay-checkout.php @@ -40,7 +40,9 @@ class WC_Gateway_Reepay_Checkout extends WC_Payment_Gateway_Reepay { * Settle * @var string */ - public $settle = 'yes'; + public $settle = array( + 'online', 'virtual', 'physical', 'recurring' + ); /** * Debug Mode @@ -250,12 +252,17 @@ public function init_form_fields() { ), 'default' => $this->payment_type ), - 'settle' => array( - 'title' => __( 'Instant Settle', 'woocommerce-gateway-reepay-checkout' ), - 'type' => 'checkbox', - 'label' => __( 'Enable instant settle', 'woocommerce-gateway-reepay-checkout' ), + 'settle' => array( + 'title' => __( 'Instant Settle', 'woocommerce-gateway-reepay-checkout' ), 'description' => __( 'Instant Settle will charge your customers right away', 'woocommerce-gateway-reepay-checkout' ), - 'default' => $this->settle + 'type' => 'multiselect', + 'css' => 'height: 150px', + 'options' => array( + 'online_virtual' => __( 'Instant Settle online / virtualproducts', 'woocommerce-gateway-reepay-checkout' ), + 'physical' => __( 'Instant Settle physical products', 'woocommerce-gateway-reepay-checkout' ), + 'recurring' => __( 'Instant Settle recurring (subscription) products', 'woocommerce-gateway-reepay-checkout' ), + ), + 'select_buttons' => TRUE, ), 'language' => array( 'title' => __( 'Language In Payment Window', 'woocommerce-gateway-reepay-checkout' ), @@ -732,11 +739,16 @@ public function process_payment( $order_id ) { 'cancel_url' => $order->get_cancel_order_url() ); } - + + // + // Calculate if the order is to be settled instantly (from products and configuration) + // + $settleInstant = $this->order_instant_settle($order); + // Initialize Payment $params = [ 'locale' => $this->get_language(), - 'settle' => $this->settle === 'yes', + 'settle' => $settleInstant, 'recurring' => $maybe_save_card || self::order_contains_subscription( $order ) || self::wcs_is_payment_change(), 'order' => [ 'handle' => $this->get_order_handle( $order ), @@ -832,6 +844,66 @@ public function process_payment( $order_id ) { 'cancel_url' => $order->get_cancel_order_url() ); } + + /** + * Validates if an order is to be settled instantly based by the order items + * @Param $order - is the WooCommerce order object + * @return bool - set to true if the order is to be settled instantly + */ + public function order_instant_settle($order) { + // + // 3 x different states if an order should be settled instant + // + $orderCanInstantSettle = array( + "online_virtual" => true, + "physical" => true, + "recurring" => true + ); + + // + // flag if the product is recurring + // + $isRecurring = self::order_contains_subscription( $order ); + + // + // Now walk through the order-lines and check per order if it is virtual, downloadable, recurring or physical + // if any of the items doesn't match the reepay instant settle configuration, the flag is set as false + // + foreach ( $order->get_items() as $item ) { + $_product = $order->get_product_from_item( $item ); // fetch product details from the order + $isVirtual = $_product->is_virtual(); // flag if the product is virtual + $isDownload = $_product->is_downloadable(); // flag if the product is downloadable + + // + // Invalidate options if mitch match + // + if ( $isVirtual && !in_array("online_virtual", $this->settle) ) { + $orderCanInstantSettle["online_virtual"] = false; + break; + } else if ( $isDownload && !in_array("online_virtual", $this->settle) ) { + $orderCanInstantSettle["online_virtual"] = false; + break; + } else if ( $isRecurring && !in_array("recurring", $this->settle) ) { + $orderCanInstantSettle["recurring"] = false; + break; + } else if ( !$isVirtual && !$isDownload && !in_array("physical", $this->settle) ) { + $orderCanInstantSettle["physical"] = false; + break; + } + } + + // + // Only instant settle if all variations are true + // + $res = ((bool) ( + $orderCanInstantSettle["online_virtual"] && + $orderCanInstantSettle["physical"] && + $orderCanInstantSettle["recurring"] + )); + + $this->log( sprintf( '%s::%s Result %s', __CLASS__, __METHOD__, "instant settle: " . var_export($res, true) ) ); + return $res; + } /** * Payment confirm action @@ -1486,14 +1558,20 @@ public function maybe_render_subscription_payment_method( $payment_method_to_dis */ protected function reepay_charge( $order, $token, $amount = null ) { + // @todo Use order lines instead of amount try { + // + // Calculate if the order is to be instant settled + // + $instantSettle = order_instant_settle( $order ); + $params = [ 'handle' => $this->get_order_handle( $order ), 'amount' => round(100 * $amount), 'currency' => $order->get_currency(), 'source' => $token->get_token(), - 'settle' => $this->settle === 'yes', + 'settle' => $instantSettle, 'recurring' => $this->order_contains_subscription( $order ), 'customer' => [ 'test' => $this->test_mode === 'yes', From 9da01d863ab974ba46dcbc4e14b4cbce5dd680ab Mon Sep 17 00:00:00 2001 From: Dr Knudsen Date: Tue, 18 Feb 2020 13:45:29 +0100 Subject: [PATCH 2/3] Update class-wc-gateway-reepay-checkout.php Not critical - only made the initialization of the $settle member variable to contain the possible options --- includes/class-wc-gateway-reepay-checkout.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-wc-gateway-reepay-checkout.php b/includes/class-wc-gateway-reepay-checkout.php index 0e8b1b1d..77dba3e5 100755 --- a/includes/class-wc-gateway-reepay-checkout.php +++ b/includes/class-wc-gateway-reepay-checkout.php @@ -41,7 +41,7 @@ class WC_Gateway_Reepay_Checkout extends WC_Payment_Gateway_Reepay { * @var string */ public $settle = array( - 'online', 'virtual', 'physical', 'recurring' + 'online_virtual', 'physical', 'recurring' ); /** From fd39227d14056668e9274b59b277c4490731ccc3 Mon Sep 17 00:00:00 2001 From: Dr Knudsen Date: Thu, 12 Mar 2020 20:58:07 +0100 Subject: [PATCH 3/3] Added widget to back-office Added widget within the back-office where order payment actions are excuted. --- assets/css/style.css | 7 - assets/css/style.min.css | 3 +- assets/css/style.scss | 115 +++++- assets/images/cup.png | Bin 0 -> 10564 bytes assets/images/paypal.png | Bin 0 -> 5527 bytes assets/images/resurs.png | Bin 0 -> 4141 bytes assets/js/admin.js | 123 +++++- assets/js/admin.min.js | 3 +- .../abstract-wc-payment-gateway-reepay.php | 102 ++++- includes/class-wc-gateway-reepay-checkout.php | 160 +++++++- includes/class-wc-reepay-order-statuses.php | 7 +- reepay-woocommerce-payment.php | 364 +++++++++++++++++- templates/admin/action-buttons.php | 2 +- 13 files changed, 830 insertions(+), 56 deletions(-) delete mode 100755 assets/css/style.css create mode 100644 assets/images/cup.png create mode 100644 assets/images/paypal.png create mode 100644 assets/images/resurs.png diff --git a/assets/css/style.css b/assets/css/style.css deleted file mode 100755 index e6b7932c..00000000 --- a/assets/css/style.css +++ /dev/null @@ -1,7 +0,0 @@ -.reepay-logos { - display: table-cell; } - -.reepay-logo { - float: left; - display: inline-block; - padding: 5px; } diff --git a/assets/css/style.min.css b/assets/css/style.min.css index d7afdaca..b5ea5eb7 100755 --- a/assets/css/style.min.css +++ b/assets/css/style.min.css @@ -1,2 +1 @@ -.reepay-logos{display:table-cell}.reepay-logo{float:left;display:inline-block;padding:5px} -/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInN0eWxlLnNjc3MiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsY0FDQyxRQUFTLFdBRVYsYUFDQyxNQUFPLEtBQ1AsUUFBUyxhQUNULFFBQVMiLCJmaWxlIjoic3R5bGUubWluLmNzcyIsInNvdXJjZXNDb250ZW50IjpbIi5yZWVwYXktbG9nb3Mge1xuXHRkaXNwbGF5OiB0YWJsZS1jZWxsO1xufVxuLnJlZXBheS1sb2dvIHtcblx0ZmxvYXQ6IGxlZnQ7XG5cdGRpc3BsYXk6IGlubGluZS1ibG9jaztcblx0cGFkZGluZzogNXB4O1xufVxuIl19 */ +.reepay-logos{display:table-cell}.reepay-logo{float:left;display:inline-block;padding:5px}.reepay-admin-section-li-header{width:100%;font-weight:700}.reepay-admin-section-li-header-small{width:100%;font-weight:700;font-size:small;margin-bottom:0}.reepay-admin-section-li{width:100%}.reepay-admin-section-li-small{width:100%;font-size:small;margin-top:0}.reepay-full-width,.reepay-full-width .button{width:100%;display:block;text-align:center;clear:both}.reepay-balance{margin-bottom:0}.reepay-balance.last{margin-bottom:5px}.reepay-balance:after{clear:both;content:"";display:table}.reepay-balance__amount,.reepay-balance__label{background-color:#f9f9f9;box-sizing:border-box;float:left;padding:7px 10px;text-transform:uppercase;font-size:10px;color:#8a8a8a}.reepay-balance__label{font-weight:700;width:60%;border-right:2.5px solid #fff}.reepay-balance__amount{border-right:0;width:40%;border-left:2.5px solid #fff;display:-webkit-box;display:-ms-flexbox;display:-webkit-flex;display:flex}.reepay-balance__currency{margin-right:10px}.reepay-balance.last .reepay-balance__currency{margin-right:0}.reepay-admin-card-logo{text-align:center;margin-right:25px;max-width:100px}.reepay-partly_capture_amount{background-color:#f9f9f9;box-sizing:border-box;float:left;padding:4px 10px;text-transform:uppercase;font-size:10px;color:#8a8a8a;border-right:0;width:40%;border-left:2.5px solid #fff;display:-webkit-box;display:-ms-flexbox;display:-webkit-flex;display:flex}.reepay-capture_partly_amount-field{font-size:small;max-height:20px;min-height:1px!important;text-align:center} \ No newline at end of file diff --git a/assets/css/style.scss b/assets/css/style.scss index 7d1c27e3..0dfdb463 100755 --- a/assets/css/style.scss +++ b/assets/css/style.scss @@ -1,8 +1,115 @@ .reepay-logos { - display: table-cell; -} + display: table-cell; } + .reepay-logo { + float: left; + display: inline-block; + padding: 5px; } + +.reepay-admin-section-li-header { + width: 100%; + font-weight: bold; +} + +.reepay-admin-section-li-header-small { + width: 100%; + font-weight: bold; + font-size: small; + margin-bottom: 0px; +} + +.reepay-admin-section-li { + width: 100%; +} + +.reepay-admin-section-li-small { + width: 100%; + font-size: small; + margin-top: 0px; +} + +.reepay-full-width, .reepay-full-width .button { + width: 100%; + display: block; + text-align: center; + clear: both; +} + +.reepay-balance { + margin-bottom: 0; +} + +.reepay-balance.last { + margin-bottom: 5px; +} + +.reepay-balance:after { + clear: both; + content: ""; + display: table; +} + +.reepay-balance__label, +.reepay-balance__amount { + background-color: #f9f9f9; + box-sizing: border-box; + float: left; + padding: 7px 10px; + text-transform: uppercase; + font-size: 10px; + color: #8a8a8a; +} + +.reepay-balance__label { + font-weight: bold; + width: 60%; + border-right: 2.5px solid #FFF; +} +.reepay-balance__amount { + border-right: 0; + width: 40%; + border-left: 2.5px solid #FFF; + display: -webkit-box; + display: -ms-flexbox; + display: -webkit-flex; + display: flex; +} + +.reepay-balance__currency { + margin-right: 10px; +} + +.reepay-balance.last .reepay-balance__currency { + margin-right: 0; +} + +.reepay-admin-card-logo { + text-align: center; + margin-right: 25px; + max-width: 100px; +} + +.reepay-partly_capture_amount { + background-color: #f9f9f9; + box-sizing: border-box; float: left; - display: inline-block; - padding: 5px; + padding: 4px 10px; + text-transform: uppercase; + font-size: 10px; + color: #8a8a8a; + + border-right: 0; + width: 40%; + border-left: 2.5px solid #FFF; + display: -webkit-box; /* OLD - iOS 6-, Safari 3.1-6, BB7 */ + display: -ms-flexbox; /* TWEENER - IE 10 */ + display: -webkit-flex; /* NEW - Safari 6.1+. iOS 7.1+, BB10 */ + display: flex; /* NEW, Spec - Firefox, Chrome, Opera */ } + +.reepay-capture_partly_amount-field { + font-size: small; + max-height: 20px; + min-height: 1px !important; + text-align: center; +} \ No newline at end of file diff --git a/assets/images/cup.png b/assets/images/cup.png new file mode 100644 index 0000000000000000000000000000000000000000..fd3b4c5a82f8221bc5bb33f772366bf9c5a2cb04 GIT binary patch literal 10564 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

d=KhM%Kl=s`uqBl>Y@uWMFPgO`RjB=)-lH-g& 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 literal 0 HcmV?d00001 diff --git a/assets/images/paypal.png b/assets/images/paypal.png new file mode 100644 index 0000000000000000000000000000000000000000..3b60c4d42e38c0d4d0895b31b689226f224ff46d GIT binary patch literal 5527 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 } - + + //$this->log( sprintf( '%s::%s Result %s', __CLASS__, __METHOD__, "Amount to capture: " . $amount ) ); + $handle = $this->get_order_handle( $order ); try { + $result = $this->request( 'GET', 'https://api.reepay.com/v1/invoice/' . $handle ); + //$this->log( sprintf( '%s::%s Result %s', __CLASS__, __METHOD__, var_export($result, true) ) ); + //exit(); } catch (Exception $e) { return FALSE; } - - return $result['state'] === 'authorized'; + + $authorizedAmount = $result['authorized_amount']; + $settledAmount = $result['settled_amount']; + $refundedAmount = $result['refunded_amount']; + + // $this->log( sprintf( '%s::%s Result %s', __CLASS__, __METHOD__, "STATE: " . $result['state'] ) ); + $this->log( sprintf( '%s::%s Result %s', __CLASS__, __METHOD__, "authorizedAmount: " . $authorizedAmount ) ); + $this->log( sprintf( '%s::%s Result %s', __CLASS__, __METHOD__, "settledAmount: " . $settledAmount ) ); + $this->log( sprintf( '%s::%s Result %s', __CLASS__, __METHOD__, "amount: " . $amount ) ); + + //exit(); + return ( + ( $result['state'] === 'authorized' ) || + ( $result['state'] === 'settled' && $authorizedAmount >= $settledAmount + $amount ) + ); } /** @@ -58,12 +76,16 @@ public function can_cancel( $order ) { $handle = $this->get_order_handle( $order ); try { + //$this->log( sprintf( '%s::%s Try to cancel %s', __CLASS__, __METHOD__, $order ) ); $result = $this->request( 'GET', 'https://api.reepay.com/v1/invoice/' . $handle ); + $this->log( sprintf( '%s::%s Result %s', __CLASS__, __METHOD__, json_encode($result) ) ); } catch (Exception $e) { return FALSE; } - return $result['state'] === 'authorized'; + // return $result['state'] === 'authorized' || ( $result['state'] === "settled" && $result["settled_amount"] < $result["authorized_amount"] ); + // can only cancel payments when the state is authorized (partly void is not supported yet) + return ( $result['state'] === 'authorized' ); } /** @@ -77,6 +99,13 @@ public function can_refund( $order, $amount = FALSE ) { if ( is_int( $order ) ) { $order = wc_get_order( $order ); } + + // + // Check if hte order is cancelled - if so - then return as nothing has happened + // + if ( $order->get_meta( '_reepay_order_cancelled', true ) === "1" ) { + return; + } if ( $order->get_payment_method() !== $this->id ) { return FALSE; @@ -110,6 +139,13 @@ public function capture_payment( $order, $amount = FALSE ) { if ( is_int( $order ) ) { $order = wc_get_order( $order ); } + + // + // Check if hte order is cancelled - if so - then return as nothing has happened + // + if ( $order->get_meta( '_reepay_order_cancelled', true ) === "1" ) { + return; + } if ( ! $amount ) { $amount = $order->get_total(); @@ -130,22 +166,24 @@ public function capture_payment( $order, $amount = FALSE ) { ]; $result = $this->request( 'POST', 'https://api.reepay.com/v1/charge/' . $handle . '/settle', $params ); } catch ( Exception $e ) { - throw new Exception( sprintf( 'API Error: %s', $e->getMessage() ) ); + throw new Exception( sprintf( 'API Error (capture_payment): %s', $e->getMessage() ) ); } - + update_post_meta( $order->get_id(), '_reepay_capture_transaction', $result['transaction'] ); $order->set_transaction_id( $result['transaction'] ); $order->save(); + + if ( !$amount) { + $order->payment_complete( $result['transaction'] ); - $order->payment_complete( $result['transaction'] ); + if (defined('REEPAY_STATUS_SETTLED')) { + $order->set_status( REEPAY_STATUS_SETTLED ); + $order->save(); + } - if (defined('REEPAY_STATUS_SETTLED')) { - $order->set_status( REEPAY_STATUS_SETTLED ); - $order->save(); + $order->add_order_note( __( 'Transaction settled.', 'woocommerce-gateway-reepay-checkout' ) ); } - - $order->add_order_note( __( 'Transaction settled.', 'woocommerce-gateway-reepay-checkout' ) ); } /** @@ -160,6 +198,13 @@ public function cancel_payment( $order ) { if ( is_int( $order ) ) { $order = wc_get_order( $order ); } + + // + // Check if hte order is cancelled - if so - then return as nothing has happened + // + if ( $order->get_meta( '_reepay_order_cancelled', true ) === "1" ) { + return; + } if ( ! $this->can_cancel( $order ) ) { throw new Exception( 'Payment can\'t be cancelled.' ); @@ -173,7 +218,7 @@ public function cancel_payment( $order ) { try { $result = $this->request( 'POST', 'https://api.reepay.com/v1/charge/' . $handle . '/cancel' ); } catch ( Exception $e ) { - throw new Exception( sprintf( 'API Error: %s', $e->getMessage() ) ); + throw new Exception( sprintf( 'API Error (cancel_payment): %s', $e->getMessage() ) ); } update_post_meta($order->get_id(), '_reepay_cancel_transaction', $result['transaction'] ); @@ -200,6 +245,13 @@ public function refund_payment( $order, $amount = FALSE, $reason = '' ) { if ( is_int( $order ) ) { $order = wc_get_order( $order ); } + + // + // Check if hte order is cancelled - if so - then return as nothing has happened + // + if ( $order->get_meta( '_reepay_order_cancelled', true ) === "1" ) { + return; + } if ( ! $this->can_refund( $order, $amount ) ) { throw new Exception( 'Payment can\'t be refunded.' ); @@ -217,7 +269,7 @@ public function refund_payment( $order, $amount = FALSE, $reason = '' ) { ]; $result = $this->request( 'POST', 'https://api.reepay.com/v1/refund', $params ); } catch ( Exception $e ) { - throw new Exception( sprintf( 'API Error: %s', $e->getMessage() ) ); + throw new Exception( sprintf( 'API Error (refund_payment): %s', $e->getMessage() ) ); } // Save Credit Note ID @@ -490,7 +542,7 @@ public function request($method, $url, $params = array()) { return $result; } - throw new Exception(sprintf('API Error: %s. HTTP Code: %s', $body, $info['http_code'])); + throw new Exception(sprintf('API Error (request): %s. HTTP Code: %s', $body, $info['http_code'])); default: throw new Exception(sprintf('Invalid HTTP Code: %s', $info['http_code'])); } @@ -972,4 +1024,24 @@ public function get_language() { return 'en_US'; } + + public function get_invoice_data( $order ) { + if ( is_int( $order ) ) { + $order = wc_get_order( $order ); + } + + if ( $order->get_payment_method() !== $this->id ) { + return FALSE; + } + + $handle = $this->get_order_handle( $order ); + + try { + + $result = $this->request( 'GET', 'https://api.reepay.com/v1/invoice/' . $handle ); + return $result; + } catch (Exception $e) { + return FALSE; + } + } } diff --git a/includes/class-wc-gateway-reepay-checkout.php b/includes/class-wc-gateway-reepay-checkout.php index 77dba3e5..8967e17f 100755 --- a/includes/class-wc-gateway-reepay-checkout.php +++ b/includes/class-wc-gateway-reepay-checkout.php @@ -125,6 +125,10 @@ public function __construct() { if ( $this->save_cc !== 'yes' && ($key = array_search('add_payment_method', $this->supports)) !== false ) { unset($this->supports[$key]); } + + if (!is_array($this->settle)) { + $this->settle = array(); + } add_action( 'admin_notices', array( $this, 'admin_notice_warning' ) ); @@ -743,7 +747,7 @@ public function process_payment( $order_id ) { // // Calculate if the order is to be settled instantly (from products and configuration) // - $settleInstant = $this->order_instant_settle($order); + $instant_settle_ary = $this->order_instant_settle( $order ); // Initialize Payment $params = [ @@ -813,6 +817,9 @@ public function process_payment( $order_id ) { } } + //$this->log( sprintf( '%s::%s Charge params %s', __CLASS__, __METHOD__, var_export( $params, true ) ) ); + //return; + $result = $this->request('POST', 'https://checkout-api.reepay.com/v1/session/charge', $params); $this->log( sprintf( '%s::%s Result %s', __CLASS__, __METHOD__, var_export( $result, true ) ) ); @@ -848,9 +855,11 @@ public function process_payment( $order_id ) { /** * Validates if an order is to be settled instantly based by the order items * @Param $order - is the WooCommerce order object + * @Param $order_settle_amount - is a reference to the variable which will contain the calculated settle amount. That value is used if the entire order cannot be captured * @return bool - set to true if the order is to be settled instantly */ - public function order_instant_settle($order) { + public function order_instant_settle( $order ) { + $instant_settle = false; // // 3 x different states if an order should be settled instant // @@ -865,44 +874,78 @@ public function order_instant_settle($order) { // $isRecurring = self::order_contains_subscription( $order ); + // + // Increment this value with the amount to settle + // + $instant_settle_amount = 0; + + // + // Total number products + // + $order_products_count = 0; + + // + // + // + $instant_settle_products_count = 0; + // // Now walk through the order-lines and check per order if it is virtual, downloadable, recurring or physical // if any of the items doesn't match the reepay instant settle configuration, the flag is set as false // foreach ( $order->get_items() as $item ) { + $quantity = $item->get_quantity(); $_product = $order->get_product_from_item( $item ); // fetch product details from the order $isVirtual = $_product->is_virtual(); // flag if the product is virtual $isDownload = $_product->is_downloadable(); // flag if the product is downloadable - + $productAmount = ( $quantity * $_product->price * 100 ); // get the product price; + + //echo "is download: " . $isDownload; exit(); + //$this->log( sprintf( '%s::%s Result %s', __CLASS__, __METHOD__, "product: " . var_export($_product, true) ) ); + // // Invalidate options if mitch match // - if ( $isVirtual && !in_array("online_virtual", $this->settle) ) { + if ( $isVirtual && !in_array("online_virtual", $this->settle ) ) { $orderCanInstantSettle["online_virtual"] = false; + $productAmount = 0; break; - } else if ( $isDownload && !in_array("online_virtual", $this->settle) ) { + } else if ( $isDownload && !in_array("online_virtual", $this->settle ) ) { $orderCanInstantSettle["online_virtual"] = false; + $productAmount = 0; break; - } else if ( $isRecurring && !in_array("recurring", $this->settle) ) { + } else if ( $isRecurring && !in_array( "recurring", $this->settle ) ) { $orderCanInstantSettle["recurring"] = false; + $productAmount = 0; break; - } else if ( !$isVirtual && !$isDownload && !in_array("physical", $this->settle) ) { + } else if ( !$isVirtual && !$isDownload && !in_array( "physical", $this->settle ) ) { $orderCanInstantSettle["physical"] = false; + $productAmount = 0; break; } + + if ($productAmount > 0) $instant_settle_products_count++; + $instant_settle_amount += $productAmount; + $order_products_count++; } // // Only instant settle if all variations are true // - $res = ((bool) ( + $instant_settle = ((bool) ( $orderCanInstantSettle["online_virtual"] && $orderCanInstantSettle["physical"] && $orderCanInstantSettle["recurring"] )); - $this->log( sprintf( '%s::%s Result %s', __CLASS__, __METHOD__, "instant settle: " . var_export($res, true) ) ); - return $res; + $this->log( sprintf( '%s::%s Result %s', __CLASS__, __METHOD__, "instant settle (" . $instant_settle . ") amount: " . $instant_settle_amount . ", order items: " . count( $order->get_items() ) ) ); + + return array ( + "instant_settle" => $instant_settle, + "instant_settle_amount" => $instant_settle_amount, + "order_products_count" => $order_products_count, + "instant_settle_products_count" => $instant_settle_products_count + ); } /** @@ -964,7 +1007,7 @@ public function payment_confirm() { do { usleep( 500 ); $attempts++; - if ( $attempts > 12 ) { + if ( $attempts > 30 ) { $status_failback = true; break; } @@ -973,8 +1016,11 @@ public function payment_confirm() { $order = wc_get_order( $order_id ); } while ( $order->has_status( apply_filters( 'woocommerce_default_order_status', 'pending' ) ) ); +//$this->process_instant_settle( $order ); +//exit(); // Update order status if ( $status_failback ) { + $this->log( sprintf( '%s::%s Processing status_fallback ', __CLASS__, __METHOD__ ) ); try { $result = $this->request( 'GET', 'https://api.reepay.com/v1/invoice/' . wc_clean( $_GET['invoice'] ) ); $this->log( sprintf( '%s::%s Invoice data %s', __CLASS__, __METHOD__, var_export( $result, true ) ) ); @@ -983,9 +1029,11 @@ public function payment_confirm() { return; } + $this->log( sprintf( '%s::%s status_fallback state %s ', __CLASS__, __METHOD__, $result['state'] ) ); switch ($result['state']) { case 'authorized': $this->set_authorized_status( $order ); + $this->process_instant_settle( $order ); break; case 'settled': $order->payment_complete(); @@ -1007,6 +1055,68 @@ public function payment_confirm() { // Lock payment confirmation //update_post_meta( $order->get_id(), '_reepay_payment_confirmed', 1 ); } + + public function process_instant_settle( $order ) { + // + // Calculate if the order is to be instant settled + // + $instant_settle_ary = (object)$this->order_instant_settle( $order ); + + $this->log( sprintf( '%s::%s instant-settle-array calculated %s', __CLASS__, __METHOD__, var_export( $instant_settle_ary, true ) ) ); + + $settle_amount = 0; + + if ( count( $order->get_items() ) == $instant_settle_ary->instant_settle_products_count ) { + $settle_amount = $order->get_total() * 100; + } else { + $settle_amount = $instant_settle_ary->instant_settle_amount; + } + + $this->log( sprintf( '%s::%s Settle amount %s', __CLASS__, __METHOD__, $settle_amount ) ); + + // + // If settle + // + if ($settle_amount > 0) { + $this->log( sprintf( '%s::%s Settle amount towards reepay %s', __CLASS__, __METHOD__, $settle_amount ) ); + $params = array ( + "amount" => $settle_amount + ); + // + // + // + try { + $result = $this->request( 'POST', 'https://api.reepay.com/v1/charge/order-' . $order->get_id() . '/settle', $params ); + //$this->log( sprintf( '%s::%s Invoice settled success %s', __CLASS__, __METHOD__, var_export( $result, true ) ) ); + } catch (Exception $e) { + $this->log( sprintf( '%s::%s API Error: %s', __CLASS__, __METHOD__, var_export( $e->getMessage(), true ) ) ); + return false; + } + + // + // If entire order is settled - go and mark as complete + // + if ( ( $order->get_total() * 100) == $settle_amount) { + // + // Log the order has been fully settled + // + $order->add_order_note( __( 'Transaction settled with amount ' . number_format( $settle_amount / 100, 2, wc_get_price_decimal_separator(), '' ) . '.', 'woocommerce-gateway-reepay-checkout' ) ); + // + // Set the order as settled + // + // $order->payment_complete(); + } else { + // + // Partly settle + // Log the amout + // + $order->add_order_note( __( 'Transaction partly settled with amount ' . number_format( $settle_amount / 100, 2, wc_get_price_decimal_separator(), '' ) . '.', 'woocommerce-gateway-reepay-checkout' ) ); + } + } + + //print_r($instant_settle_ary); exit(); + return true; + } /** * WebHook Callback @@ -1067,6 +1177,9 @@ public function return_handler() { if ( ! $order_stock_reduced ) { wc_reduce_stock_levels( $order->get_id() ); } + + // process instant settle + $this->process_instant_settle( $order ); $this->set_authorized_status( $order ); @@ -1096,8 +1209,16 @@ public function return_handler() { return; } - $order->payment_complete( $data['transaction'] ); - $order->add_order_note( __( 'Transaction settled.', 'woocommerce-gateway-reepay-checkout' ) ); + // + // Only complete the order if... partly settle is disabled + // + $instant_settle_ary = (object)$this->order_instant_settle( $order ); + if ( $instant_settle_ary->instant_settle_amount == 0 ) { + $order->payment_complete( $data['transaction'] ); + $order->add_order_note( __( 'Transaction settled.', 'woocommerce-gateway-reepay-checkout' ) ); + } else { + // $order->add_order_note( __( 'Transaction already partly settled.', 'woocommerce-gateway-reepay-checkout' ) ); + } update_post_meta( $order->get_id(), '_reepay_capture_transaction', $data['transaction'] ); $this->log( sprintf( 'WebHook: Success event type: %s', $data['event_type'] ) ); @@ -1564,14 +1685,14 @@ protected function reepay_charge( $order, $token, $amount = null ) // // Calculate if the order is to be instant settled // - $instantSettle = order_instant_settle( $order ); + //$instant_settle_ary = $this->order_instant_settle( $order ); $params = [ 'handle' => $this->get_order_handle( $order ), 'amount' => round(100 * $amount), 'currency' => $order->get_currency(), 'source' => $token->get_token(), - 'settle' => $instantSettle, + // 'settle' => $instantSettle, 'recurring' => $this->order_contains_subscription( $order ), 'customer' => [ 'test' => $this->test_mode === 'yes', @@ -1641,9 +1762,12 @@ protected function reepay_charge( $order, $token, $amount = null ) update_post_meta( $order->get_id(), 'reepay_charge', '0' ); break; case 'settled': - $order->payment_complete( $result['transaction'] ); - $order->add_order_note( __( 'Transaction settled.', 'woocommerce-gateway-reepay-checkout' ) ); - update_post_meta( $order->get_id(), 'reepay_charge', '1' ); + $instant_settle_ary = (object)$this->order_instant_settle( $order ); + if ($instant_settle_ary->instant_settle_amount == 0) { + $order->payment_complete( $result['transaction'] ); + $order->add_order_note( __( 'Transaction settled.', 'woocommerce-gateway-reepay-checkout' ) ); + update_post_meta( $order->get_id(), 'reepay_charge', '1' ); + } break; default: throw new Exception( 'Generic error' ); diff --git a/includes/class-wc-reepay-order-statuses.php b/includes/class-wc-reepay-order-statuses.php index 370e22d0..563919bf 100755 --- a/includes/class-wc-reepay-order-statuses.php +++ b/includes/class-wc-reepay-order-statuses.php @@ -309,6 +309,7 @@ public function reepay_authorized_order_status( $status, $order_id, $order ) { * @param WC_Order $order */ public static function order_status_changed( $order_id, $from, $to, $order ) { + $payment_method = $order->get_payment_method(); if ( ! in_array( $payment_method, WC_ReepayCheckout::PAYMENT_METHODS ) ) { return; @@ -346,8 +347,12 @@ public static function order_status_changed( $order_id, $from, $to, $order ) { case REEPAY_STATUS_SETTLED: // Capture payment if ( $gateway->can_capture( $order ) ) { + + $order_data = $gateway->get_invoice_data( $order ); + $amount = $order_data["authorized_amount"] - $order_data["settled_amount"]; + $amount = (float)((float)$amount / 100); try { - $gateway->capture_payment( $order ); + $gateway->capture_payment( $order, $amount ); } catch ( Exception $e ) { $message = $e->getMessage(); WC_Admin_Meta_Boxes::add_error( $message ); diff --git a/reepay-woocommerce-payment.php b/reepay-woocommerce-payment.php index 78d1d536..4aacc9d2 100755 --- a/reepay-woocommerce-payment.php +++ b/reepay-woocommerce-payment.php @@ -45,7 +45,7 @@ public function __construct() { //add_action( 'add_meta_boxes', __CLASS__ . '::add_meta_boxes' ); // Add action buttons - add_action( 'woocommerce_order_item_add_action_buttons', __CLASS__ . '::add_action_buttons', 10, 1 ); + //add_action( 'woocommerce_order_item_add_action_buttons', __CLASS__ . '::add_action_buttons', 10, 1 ); // Add scripts and styles for admin add_action( 'admin_enqueue_scripts', __CLASS__ . '::admin_enqueue_scripts' ); @@ -63,11 +63,29 @@ public function __construct() { $this, 'ajax_reepay_cancel' ) ); + + add_action( 'wp_ajax_reepay_refund', array( + $this, + 'ajax_reepay_refund' + ) ); + + add_action( 'wp_ajax_reepay_capture_partly', array( + $this, + 'ajax_reepay_capture_partly' + ) ); + + add_action( 'wp_ajax_reepay_refund_partly', array( + $this, + 'ajax_reepay_refund_partly' + ) ); $this->includes(); // Add admin menu add_action( 'admin_menu', array( &$this, 'admin_menu' ), 99 ); + + // add meta boxes + add_action( 'add_meta_boxes', [ $this, 'add_meta_boxes' ] ); } /** @@ -203,7 +221,7 @@ public function add_valid_order_statuses( $statuses, $order ) { * @return void */ public static function add_meta_boxes() { - global $post_id; + /*global $post_id; if ( $order = wc_get_order( $post_id ) ) { $payment_method = $order->get_payment_method(); if ( in_array( $payment_method, self::PAYMENT_METHODS ) ) { @@ -216,6 +234,26 @@ public static function add_meta_boxes() { 'default' ); } + }*/ + + global $post; + $screen = get_current_screen(); + $post_types = [ 'shop_order', 'shop_subscription' ]; + + if ( in_array( $screen->id, $post_types, true ) && in_array( $post->post_type, $post_types, true ) ) { + if ( $order = wc_get_order( $post->ID ) ) { + $payment_method = $order->get_payment_method(); + if ( in_array( $payment_method, self::PAYMENT_METHODS ) ) { + add_meta_box( 'reepay-payment-actions', __( 'Reepay Payment', 'woocommerce-gateway-reepay-checkout' ), [ + &$this, + 'meta_box_payment', + ], 'shop_order', 'side', 'high' ); + add_meta_box( 'reepay-payment-actions', __( 'Reepay Subscription', 'woocommerce-gateway-reepay-checkout' ), [ + &$this, + 'meta_box_subscription', + ], 'shop_subscription', 'side', 'high' ); + } + } } } @@ -283,6 +321,7 @@ public static function admin_enqueue_scripts( $hook ) { // Scripts $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min'; wp_register_script( 'reepay-admin-js', plugin_dir_url( __FILE__ ) . 'assets/js/admin' . $suffix . '.js' ); + wp_enqueue_style( 'wc-gateway-reepay-checkout', plugins_url( '/assets/css/style' . $suffix . '.css', __FILE__ ), array(), FALSE, 'all' ); // Localize the script $translation_array = array( @@ -332,6 +371,15 @@ public function ajax_reepay_cancel() { $order_id = (int) $_REQUEST['order_id']; $order = wc_get_order( $order_id ); + + // + // Check if the order is already cancelled + // ensure no more actions are made + // + if ( $order->get_meta( '_reepay_order_cancelled', true ) === "1" ) { + wp_send_json_success( __( 'Order already cancelled.', 'woocommerce-gateway-reepay-checkout' ) ); + return; + } try { // Get Payment Gateway @@ -340,8 +388,112 @@ public function ajax_reepay_cancel() { /** @var WC_Gateway_Reepay_Checkout $gateway */ $gateway = $gateways[ $payment_method ]; - $gateway->cancel_payment( $order ); - wp_send_json_success( __( 'Capture success.', 'woocommerce-gateway-reepay-checkout' ) ); + + // Check if the payment can be cancelled + // $order->update_meta_data( '_' . $key, $value ); + // order->get_meta( '_reepay_token', true ) + if ($gateway->can_cancel( $order )) { + $gateway->cancel_payment( $order ); + } + + // + // Mark the order as cancelled - no more communciation to reepay is done! + // + $order->update_meta_data( '_reepay_order_cancelled', 1 ); + $order->save_meta_data(); + + // Return success + wp_send_json_success( __( 'Cancel success.', 'woocommerce-gateway-reepay-checkout' ) ); + } catch ( Exception $e ) { + $message = $e->getMessage(); + wp_send_json_error( $message ); + } + } + + /** + * Action for Cancel + */ + public function ajax_reepay_refund() { + if ( ! wp_verify_nonce( $_REQUEST['nonce'], 'reepay' ) ) { + exit( 'No naughty business' ); + } + + + $amount = (int) $_REQUEST['amount']; + $order_id = (int) $_REQUEST['order_id']; + $order = wc_get_order( $order_id ); + + try { + // Get Payment Gateway + $payment_method = $order->get_payment_method(); + $gateways = WC()->payment_gateways()->get_available_payment_gateways(); + + /** @var WC_Gateway_Reepay_Checkout $gateway */ + $gateway = $gateways[ $payment_method ]; + $gateway->refund_payment( $order, $amount ); + wp_send_json_success( __( 'Refund success.', 'woocommerce-gateway-reepay-checkout' ) ); + } catch ( Exception $e ) { + $message = $e->getMessage(); + wp_send_json_error( $message ); + } + } + + /** + * Action for Cancel + */ + public function ajax_reepay_capture_partly() { + + if ( ! wp_verify_nonce( $_REQUEST['nonce'], 'reepay' ) ) { + exit( 'No naughty business' ); + } + + $amount = $_REQUEST['amount']; + $order_id = (int) $_REQUEST['order_id']; + $order = wc_get_order( $order_id ); + + $amount = str_replace(",", "", $amount); + $amount = str_replace(".", "", $amount); + + try { + // Get Payment Gateway + $payment_method = $order->get_payment_method(); + $gateways = WC()->payment_gateways()->get_available_payment_gateways(); + + /** @var WC_Gateway_Reepay_Checkout $gateway */ + $gateway = $gateways[ $payment_method ]; + $gateway->capture_payment( $order, (float)((float)$amount / 100) ); + wp_send_json_success( __( 'Capture partly success.', 'woocommerce-gateway-reepay-checkout' ) ); + } catch ( Exception $e ) { + $message = $e->getMessage(); + wp_send_json_error( $message ); + } + } + + /** + * Action for Cancel + */ + public function ajax_reepay_refund_partly() { + + if ( ! wp_verify_nonce( $_REQUEST['nonce'], 'reepay' ) ) { + exit( 'No naughty business' ); + } + + $amount = $_REQUEST['amount']; + $order_id = (int) $_REQUEST['order_id']; + $order = wc_get_order( $order_id ); + + $amount = str_replace(",", "", $amount); + $amount = str_replace(".", "", $amount); + + try { + // Get Payment Gateway + $payment_method = $order->get_payment_method(); + $gateways = WC()->payment_gateways()->get_available_payment_gateways(); + + /** @var WC_Gateway_Reepay_Checkout $gateway */ + $gateway = $gateways[ $payment_method ]; + $gateway->refund_payment( $order, (float)((float)$amount / 100) ); + wp_send_json_success( __( 'Refund partly success.', 'woocommerce-gateway-reepay-checkout' ) ); } catch ( Exception $e ) { $message = $e->getMessage(); wp_send_json_error( $message ); @@ -393,6 +545,210 @@ public static function upgrade_notice() { ID ) ) { + + $payment_method = $order->get_payment_method(); + if ( in_array( $payment_method, self::PAYMENT_METHODS ) ) { + + do_action( 'woocommerce_reepay_meta_box_payment_before_content', $order ); + if ( true ) { + + $state = null; + try { + // + // Fetch the gateway and order data + // + $gateways = WC()->payment_gateways()->get_available_payment_gateways(); + $gateway = $gateways[ $payment_method ]; + $order_data = $gateway->get_invoice_data( $order ); + + // + // Calculate if hte user has cancelled the payment + // + $order_is_cancelled = ( $order->get_meta( '_reepay_order_cancelled', true ) === "1" ); + + // + // Header payment information + // + echo "

    "; + echo "
  • " . __( 'State', 'woo-reepay' ) . ": " . $order_data["state"] . "
  • "; + if ($order_is_cancelled && "cancelled" != $order_data["state"]) { + echo "
  • " . __( 'Order is cancelled', 'woo-reepay' ) . "
  • "; + } + printf( "
  • %s:%s%s
  • ", __( 'Remaining balance', 'woo-reepay' ), $order_data["currency"], $this->format_price_decimals($order_data["authorized_amount"] - $order_data["settled_amount"]) ); + printf( "
  • %s:%s%s
  • ", __( 'Total authorized', 'woo-reepay' ), $order_data["currency"], $this->format_price_decimals($order_data["authorized_amount"]) ); + printf( "
  • %s:%s%s
  • ", __( 'Total settled', 'woo-reepay' ), $order_data["currency"], $this->format_price_decimals($order_data["settled_amount"]) ); + printf( "
  • %s:%s%s
  • ", __( 'Total refunded', 'woo-reepay' ), $order_data["currency"], $this->format_price_decimals($order_data["refunded_amount"]) ); + echo "
  •  
  • "; + // + // Capture payment full + // + if ( $order_data["settled_amount"] == 0 && "cancelled" != $order_data["state"] && !$order_is_cancelled ) { + echo "
  • ID . "\" data-confirm=\"" . __( 'You are about to CAPTURE this payment', 'woo-reepay' ) . "\">" . sprintf( __( 'Capture Full Amount (%s)', 'woo-reepay' ), $this->format_price_decimals( $order_data["authorized_amount"] ) ) . "
  • "; + + } + // + // Cancel payment + // + if ( $order_data["settled_amount"] < $order_data["authorized_amount"] && !$order_is_cancelled ) { + echo "
  • ID . "\">" . __( 'Cancel remaining balance', 'woo-reepay' ) . "
  • "; + echo "
  •  
  • "; + } + // + // Partly capture + // + if ( $order_data["authorized_amount"] > $order_data["settled_amount"] && "cancelled" != $order_data["state"] && !$order_is_cancelled ) { + printf( "
  • %s
  • ", __( 'Partly capture', 'woo-reepay' ) ); + printf( "
  • %s:
  • ", __( 'Capture amount', 'woo-reepay' ), $this->format_price_decimals( $order_data["authorized_amount"] - $order_data["settled_amount"] ) ); + echo "
  • ID . "\">" . __( 'Capture Specified Amount', 'woo-reepay' ) . "
  • "; + echo "
  •  
  • "; + } + // + // Partly refund + // + if ( $order_data["settled_amount"] > $order_data["refunded_amount"] && "cancelled" != $order_data["state"] && !$order_is_cancelled ) { + printf( "
  • %s
  • ", __( 'Partly refund', 'woo-reepay' ) ); + printf( "
  • %s:
  • ", __( 'Refund amount', 'woo-reepay' ), $this->format_price_decimals( $order_data["settled_amount"] - $order_data["refunded_amount"] ) ); + echo "
  • ID . "\">" . __( 'Refund Specified Amount', 'woo-reepay' ) . "
  • "; + echo "
  •  
  • "; + } + // + // Default payment information + // + echo "
  • " . __( 'Order ID', 'woo-reepay' ) . "
  • "; + echo "
  • " . $order_data["handle"] . "
  • "; + echo "
  • " . __( 'Transaction ID', 'woo-reepay' ) . "
  • "; + echo "
  • " . $order_data["id"] . "
  • "; + echo "
  • " . __( 'Card number', 'woo-reepay' ) . "
  • "; + echo "
  • " . $this->formatCreditCard( $order_data["transactions"][0]["card_transaction"]["masked_card"]) . "
  • "; + printf( '

    ', $this->get_logo( $order_data["transactions"][0]["card_transaction"]["card_type"] ) ); + echo "
"; + } catch ( Exception $e ) { + echo "
    "; + echo "
  • " . __( 'Error: ' . $e->getMessage(), 'woo-reepay' ) . "
  • "; + echo "
"; + } + } + } + } + } + + /* + * Formats a minor unit value into float with two decimals + * @priceMinor is the amount to format + * @return the nicely formatted value + */ + public function format_price_decimals( $priceMinor ) { + return number_format( $priceMinor / 100, 2, wc_get_price_decimal_separator(), '' ); + } + + /* + * Formats a credit card nicely + * @cc is the card number to format nicely + * @return the nicely formatted value + */ + public function formatCreditCard( $cc ) { + $cc = str_replace(array('-', ' '), '', $cc); + $cc_length = strlen($cc); + $newCreditCard = substr($cc, -4); + + for ( $i = $cc_length - 5; $i >= 0; $i-- ) { + if ( (($i + 1) - $cc_length) % 4 == 0 ) { + $newCreditCard = ' ' . $newCreditCard; + } + $newCreditCard = $cc[$i] . $newCreditCard; + } + + for ( $i = 7; $i < $cc_length - 4; $i++ ) { + if ( $newCreditCard[$i] == ' ' ) { + continue; + } + $newCreditCard[$i] = 'X'; + } + + return $newCreditCard; + } + + /* + * Converts a Reepay card_type into a logo + * @card_type is the Reepay card type + * @returns the logo + */ + public function get_logo( $card_type ) { + $image = null; + switch ( $card_type ) { + case 'visa': { + $image = 'visa.png'; break; + } + case 'mc': { + $image = 'mastercard.png'; break; + } + case 'dankort': { + $image = 'dankort.png'; break; + } + case 'visa_dk': { + $image = 'dankort.png'; break; + } + case 'ffk': { + $image = 'forbrugsforeningen.png'; break; + } + case 'visa_elec': { + $image = 'visa-electron.png'; break; + } + case 'maestro': { + $image = 'maestro.png'; break; + } + case 'amex': { + $image = 'american-express.png'; break; + } + case 'diners': { + $image = 'diners.png'; break; + } + case 'discover': { + $image = 'discover.png'; break; + } + case 'jcb': { + $image = 'jcb.png'; break; + } + case 'mobilepay': { + $image = 'mobilepay.png'; break; + } + case 'viabill': { + $image = 'viabill.png'; break; + } + case 'klarna_pay_later': { + $image = 'klarna.png'; break; + } + case 'klarna_pay_now': { + $image = 'klarna.png'; break; + } + case 'resurs': { + $image = 'resurs.png'; break; + } + case 'china_union_pay': { + $image = 'cup.png'; break; + } + case 'paypal': { + $image = 'paypal.png'; break; + } + case 'applepay': { + $image = 'applepay.png'; break; + } + default: { + $image = 'reepay.png'; break; + } + } + + return WP_PLUGIN_URL . "/reepay-checkout-gateway/assets/images/" . $image; + } + } new WC_ReepayCheckout(); diff --git a/templates/admin/action-buttons.php b/templates/admin/action-buttons.php index eb2ac251..2a727ce5 100755 --- a/templates/admin/action-buttons.php +++ b/templates/admin/action-buttons.php @@ -8,7 +8,7 @@ ?> -can_capture( $order ) ): ?> +can_capture( $order ) || true ): ?>