diff --git a/CHANGES.md b/CHANGES.md index b6e042e..000be6f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,25 +2,16 @@ Changelog ========= +1.10.4 +----- +* MarketPress: Error upon payment activation fixed +* WooCommerce: Official support for custom intervals (e.g. every 3rd month) added +* WooCommerce: Different bugs in order processing and subscription handling fixed 1.10.3 ----- * Common: Integration of PayFrame to enable use of SAQ A for easier PCI DSS 3.0 compliance. -PayFrame -We’ve introduced a “payment form” option for easier compliance with PCI requirements. - -In addition to having a payment form directly integrated in your checkout page, you -can use our embedded PayFrame solution to ensure that payment data never -touches your website. - -PayFrame is enabled by default, but you can choose between both options in the -plugin settings. Later this year, we’re bringing you the ability to customise the -appearance and text content of the PayFrame version. - -To learn more about the benefits of PayFrame, please visit our FAQ: -https://www.paymill.com/en/faq/howdoespaymillspayframesolutionwork - 1.10.2 ----- * Subscriptions: "required_offer_or_amount_and_currency_and_interval" error fixed diff --git a/README.md b/README.md index 4d8ed14..9f667c0 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ PAYMILL for WordPress * Tags: paymill, creditcard, elv, payment, woocommerce, paybutton, ecommerce, debitcard, subscriptions * Requires at least: 4.2.2 * Tested up to: 4.2.2 -* Stable tag: 1.10.3 +* Stable tag: 1.10.4 * License: [GPLv3 or later](http://www.gnu.org/licenses/gpl-3.0.html) With PAYMILL you are able to provide credit card based payments for your customers. @@ -46,6 +46,21 @@ Accept payments in up to 100 currencies. All major card brands like MasterCard, VISA, American Express, Diner's Club, Maestro etc. supported. Available in 40 countries across Europe so far. +Pay Frame +========= + +We’ve introduced a “payment form” option for easier compliance with PCI requirements. + +In addition to having a payment form directly integrated in your checkout page, you +can use our embedded PayFrame solution to ensure that payment data never +touches your website. + +PayFrame is enabled by default, but you can choose between both options in the +plugin settings. Later this year, we’re bringing you the ability to customise the +appearance and text content of the PayFrame version. + +To learn more about the benefits of PayFrame, please visit our FAQ: +https://www.paymill.com/en/faq/how-does-paymills-payframe-solution-work Team ==== diff --git a/lib/css/paymill.css b/lib/css/paymill.css index 2a4f01a..e26e79b 100644 --- a/lib/css/paymill.css +++ b/lib/css/paymill.css @@ -130,7 +130,7 @@ paymill_#form_credit, paymill_#form_elv{ .paymill_payment_logos{ margin-bottom:10px; } -.paymill_payment_logos img{ +.paymill_payment_logos img, .payment_method_paymill label img{ display:inline; } diff --git a/lib/errors.inc.php b/lib/errors.inc.php index 58725d8..f5e5a7e 100644 --- a/lib/errors.inc.php +++ b/lib/errors.inc.php @@ -49,6 +49,10 @@ public function getErrors($function=false,$flush=false){ $this->errors = array(); } } + + public function reset(){ + $this->errors = array(); + } } ?> \ No newline at end of file diff --git a/lib/integration/marketpress.inc.php b/lib/integration/marketpress.inc.php index 35e491b..292c029 100644 --- a/lib/integration/marketpress.inc.php +++ b/lib/integration/marketpress.inc.php @@ -19,10 +19,10 @@ class MP_Gateway_Paymill_for_WordPress extends MP_Gateway_API { var $plugin_name = 'paymill-for-wordpress'; //name of your gateway, for the admin side. - var $admin_name = ''; + var $admin_name = 'Paymill for WordPress'; //public name of your gateway, for lists and such. - var $public_name = ''; + var $public_name = 'Paymill'; //url for an image for your checkout method. Displayed on checkout form if set var $method_img_url = ''; @@ -164,7 +164,7 @@ function on_creation() { //set names here to be able to translate $this->admin_name = __('Paymill for WordPress', 'paymill'); - $this->public_name = $this->method_button_img_url = $settings['gateways']['paymill-for-wordpress']['name']; + $this->public_name = $settings['gateways']['paymill-for-wordpress']['name'] ? $settings['gateways']['paymill-for-wordpress']['name'] : 'Paymill'; //button img $this->method_img_url = $this->method_button_img_url = $settings['gateways']['paymill-for-wordpress']['image-url']; diff --git a/lib/integration/subscriptions.inc.php b/lib/integration/subscriptions.inc.php index 1cbc3e7..8d7349d 100644 --- a/lib/integration/subscriptions.inc.php +++ b/lib/integration/subscriptions.inc.php @@ -42,7 +42,7 @@ public function details($sub_id){ public function create($client, $offer, $payment, $startAt=false, $periodOfValidity=false){ if(paymill_BENCHMARK)paymill_doBenchmark(true,'paymill_subscription_create'); // benchmark load_paymill(); // this function-call can and should be used whenever working with Paymill API - + try{ $GLOBALS['paymill_loader']->request_subscription->setClient($client); $GLOBALS['paymill_loader']->request_subscription->setOffer($offer); diff --git a/lib/integration/woocommerce.inc.php b/lib/integration/woocommerce.inc.php index 41b83f3..cc2bb4b 100644 --- a/lib/integration/woocommerce.inc.php +++ b/lib/integration/woocommerce.inc.php @@ -279,11 +279,31 @@ function paymill_webhooks(){ */ error_log(var_export($subscription,true)."\n\n", 3, PAYMILL_DIR.'lib/debug/PHP_errors.log'); + // prevent multiple subscription renewals because of multiple webhook attempts. + $whole_period = 0; + switch ($subscription['period']) { + case 'day': + default: + $whole_period = intval($subscription['interval']) * 86400; + break; + case 'week': + $whole_period = intval($subscription['interval']) * 604800; + break; + case 'month': + $whole_period = intval($subscription['interval']) * 2160000; // using 25 days to prevent problems with shorter months + break; + case 'year': + $whole_period = intval($subscription['interval']) * 30240000; // using 350 days to prevent any timezone problems whatsoever + break; + } + if(count($subscription['completed_payments']) >= 1){ - $order = new WC_Order($subscription['order_id']); - - //WC_Subscriptions_Manager::process_subscription_payments_on_order($order, $subscription['product_id']); - WC_Subscriptions_Manager::process_subscription_payments_on_order($order); + if (strtotime(date(DATE_RFC822)) > strtotime($subscription['last_payment_date']) + $whole_period - 18000) { // minus 5 hours to prevent any problems with pending triggers + $order = new WC_Order($subscription['order_id']); + + //WC_Subscriptions_Manager::process_subscription_payments_on_order($order, $subscription['product_id']); + WC_Subscriptions_Manager::process_subscription_payments_on_order($order); + } }else{ $order = new WC_Order($subscription['order_id']); $order->payment_complete(); @@ -374,9 +394,8 @@ function init_paymill_gateway_class(){ if(class_exists('WC_Payment_Gateway')){ class WC_Gateway_Paymill_Gateway extends WC_Payment_Gateway{ - private $total = 0; - private $total_complete = 0; - private $total_sub_refund = 0; + private $totalProducts = 0; + private $totalSub = 0; private $cart = false; private $order_id = false; private $order = false; @@ -472,127 +491,129 @@ private function getCurrentClient(){ return $this->clientClass->getCurrentClient(); } private function getTotals(){ + $this->totalProducts = (floatval($this->order->get_total())*100); + // retrieve subscriptions amount if(class_exists('WC_Subscriptions_Order') && WC_Subscriptions_Order::order_contains_subscription($this->order)){ - foreach($this->cart as $product){ - if(is_object($product) && isset($product->id) && intval($product->id) > 0){ - $woo_sub_key = WC_Subscriptions_Manager::get_subscription_key($this->order_id,$product->id); - - if(!WC_Subscriptions_Manager::user_has_subscription(get_current_user_id(), $woo_sub_key)){ - $sub_amount_total = floatval(floatval(WC_Subscriptions_Order::get_recurring_total($this->order))*100); - $this->total = $this->total-$sub_amount_total; - if($this->total_complete == 0){ - $this->total_complete = $this->total_complete+$sub_amount_total; - } - } - } - } - // currently, there is no initial payment fee possible through paymill, so we are required to make a refund if a coupon is reducing initial fee. - /*if($this->total < 0){ - $this->total_sub_refund = (($this->total)*(-1)); - }*/ + $this->totalSub = floatval(floatval(WC_Subscriptions_Order::get_recurring_total($this->order))*100); + $this->totalProducts = $this->totalProducts-$this->totalSub; } } private function processSubscriptions(){ global $wpdb; - + // check wether subscriptions addon is activated if(class_exists('WC_Subscriptions_Order') && WC_Subscriptions_Order::order_contains_subscription($this->order)){ - $product = $this->cart; - //foreach($this->cart as $product){ - if(is_array($product) && isset($product['product_id']) && intval($product['product_id']) > 0){ + $products = $this->order->get_items(); + foreach($products as $product){ + if(is_array($product) && isset($product['product_id']) && intval($product['product_id']) > 0 && isset($product['subscription_period']) && $product['subscription_period'] != ''){ // product is a subscription? $woo_sub_key = WC_Subscriptions_Manager::get_subscription_key($this->order_id,$product['product_id']); - // check wether user already has subscription - //if(!WC_Subscriptions_Manager::user_has_subscription(get_current_user_id(), $woo_sub_key)){ - - // required vars - $amount = (floatval(WC_Subscriptions_Order::get_recurring_total($this->order))*100); - $currency = get_woocommerce_currency(); - $interval = WC_Subscriptions_Order::get_subscription_interval($this->order,$product['product_id']); - $length = intval(WC_Subscriptions_Order::get_subscription_length($this->order,$product['product_id'])); - $period = strtoupper(WC_Subscriptions_Order::get_subscription_period($this->order,$product['product_id'])); - if ($length > 0) { - $periodOfValidity = $length.' '.$period; - } else{ - $periodOfValidity = false; - } - $trial_end = strtotime(WC_Subscriptions_Product::get_trial_expiration_date($product['product_id'], get_gmt_from_date($this->order->order_date))); - if($trial_end === false){ - $trial_time = 0; - }else{ - $datediff = $trial_end - time(); - $trial_time = ceil($datediff/(60*60*24)); - } + // required vars + $amount = (floatval(WC_Subscriptions_Order::get_recurring_total($this->order))*100); + $currency = get_woocommerce_currency(); + $interval = intval($product['subscription_interval']); + $period = strtoupper($product['subscription_period']); + $length = strtoupper($product['subscription_length']); + + if ($length > 0) { + $periodOfValidity = $length.' '.$period; + } else{ + $periodOfValidity = false; + } + $trial_end = strtotime(WC_Subscriptions_Product::get_trial_expiration_date($product['product_id'], get_gmt_from_date($this->order->order_date))); + if($trial_end === false){ + $trial_time = 0; + }else{ + $datediff = $trial_end - time(); + $trial_time = ceil($datediff/(60*60*24)); + } + + // md5 name + $woo_sub_md5 = md5($amount.$currency.$interval.$trial_time); + + // get offer + $name = 'woo_'.$product['product_id'].'_'.$woo_sub_md5; + $offer = $this->subscriptions->offerGetDetailByName($name); + + // check wether offer exists in paymill + if($offer === false){ + // offer does not exist in paymill yet, create it + $params = array( + 'amount' => $amount, + 'currency' => $currency, + 'interval' => $interval.' '.$period, + 'name' => $name, + 'trial_period_days' => intval($trial_time) + ); + $offer = $this->subscriptions->offerCreate($params); - // md5 name - $woo_sub_md5 = md5($amount.$currency.$interval.$trial_time); - - // get offer - $name = 'woo_'.$product['product_id'].'_'.$woo_sub_md5; - $offer = $this->subscriptions->offerGetDetailByName($name); - - // check wether offer exists in paymill - if($offer === false){ - // offer does not exist in paymill yet, create it - $params = array( - 'amount' => $amount, - 'currency' => $currency, - 'interval' => $interval.' '.$period, - 'name' => $name, - 'trial_period_days' => intval($trial_time) - ); - $offer = $this->subscriptions->offerCreate($params); - - if($GLOBALS['paymill_loader']->paymill_errors->status()){ - $GLOBALS['paymill_loader']->paymill_errors->getErrors(); - return false; - } + if($GLOBALS['paymill_loader']->paymill_errors->status()){ + $GLOBALS['paymill_loader']->paymill_errors->getErrors(); + return false; } - // create user subscription - $user_sub = $this->subscriptions->create($this->client->getId(), $offer, $this->paymentClass->getPaymentID(),(isset($_POST['paymill_delivery_date']) ? $_POST['paymill_delivery_date'] : false),$periodOfValidity); + } + // create user subscription + $user_sub = $this->subscriptions->create($this->client->getId(), $offer, $this->paymentClass->getPaymentID(),(isset($_POST['paymill_delivery_date']) ? $_POST['paymill_delivery_date'] : false),$periodOfValidity); + + if($GLOBALS['paymill_loader']->paymill_errors->status()){ + //maybe offer cache is outdated, recache and try again + + $GLOBALS['paymill_loader']->paymill_errors->reset(); // reset error status + + $this->subscriptions->offerGetList(true); + + $params = array( + 'amount' => $amount, + 'currency' => $currency, + 'interval' => $interval.' '.$period, + 'name' => $name, + 'trial_period_days' => intval($trial_time) + ); + $offer = $this->subscriptions->offerCreate($params); if($GLOBALS['paymill_loader']->paymill_errors->status()){ $GLOBALS['paymill_loader']->paymill_errors->getErrors(); return false; - }else{ - $wpdb->query($wpdb->prepare('INSERT INTO '.$wpdb->prefix.'paymill_subscriptions (paymill_sub_id, woo_user_id, woo_offer_id) VALUES (%s, %s, %s)', - array( - $user_sub, - get_current_user_id(), - $woo_sub_key - ))); - - // subscription successful - do_action('paymill_woocommerce_subscription_created', array( - 'product_id' => $product['product_id'], - 'offer_id' => $offer, - //'offer_data' => $offer - )); - - return true; } - /*}else{ - // @todo: currently, WooCommerce does not support multiple subscriptions on checkout, so we can stop processing here if first subscription is already subscribed - $GLOBALS['paymill_loader']->paymill_errors->setError(__('Subscription already subscribed.', 'paymill')); + + $user_sub = $this->subscriptions->create($this->client->getId(), $offer, $this->paymentClass->getPaymentID(),(isset($_POST['paymill_delivery_date']) ? $_POST['paymill_delivery_date'] : false),$periodOfValidity); + if($GLOBALS['paymill_loader']->paymill_errors->status()){ $GLOBALS['paymill_loader']->paymill_errors->getErrors(); + return false; } - return false; - }*/ + } + + $wpdb->query($wpdb->prepare('INSERT INTO '.$wpdb->prefix.'paymill_subscriptions (paymill_sub_id, woo_user_id, woo_offer_id) VALUES (%s, %s, %s)', + array( + $user_sub, + get_current_user_id(), + $woo_sub_key + ))); + + // subscription successful + do_action('paymill_woocommerce_subscription_created', array( + 'product_id' => $product['product_id'], + 'offer_id' => $offer, + //'offer_data' => $offer + )); + + return true; } - //} + } }else{ return true; } } - private function processProducts(){ + private function processProducts(){ global $wpdb; - if($this->total > 0){ + + if($this->totalProducts > 0){ // make transaction - $GLOBALS['paymill_loader']->request_transaction->setAmount(round($this->total,2)); // e.g. "4200" for 42.00 EUR + $GLOBALS['paymill_loader']->request_transaction->setAmount(round($this->totalProducts,2)); // e.g. "4200" for 42.00 EUR $GLOBALS['paymill_loader']->request_transaction->setCurrency(get_woocommerce_currency()); if($this->paymentClass->getPreauthID() != false){ $GLOBALS['paymill_loader']->request_transaction->setPreauthorization($this->paymentClass->getPreauthID()); @@ -629,7 +650,7 @@ private function processProducts(){ ))); do_action('paymill_woocommerce_products_paid', array( - 'total' => $this->total, + 'total' => $this->totalProducts, 'currency' => get_woocommerce_currency(), 'client' => $response['body']['data']['client']['id'] )); @@ -653,11 +674,6 @@ public function process_payment($order_id){ $this->order_id = $order_id; $this->order_desc = $_SERVER['HTTP_HOST'].': '.__('Order #','paymill').$this->order_id.__(', Customer-ID #','paymill').get_current_user_id(); $this->order = new WC_Order($this->order_id); - $cart = $woocommerce->cart->get_cart(); - $cart = reset($cart); - $this->cart = $cart; - $this->total_complete = - $this->total = (floatval($this->order->get_total())*100); // load subscription class $this->subscriptions = new paymill_subscriptions('woocommerce'); @@ -668,7 +684,7 @@ public function process_payment($order_id){ // create payment object and preauthorization require_once(PAYMILL_DIR.'lib/integration/payment.inc.php'); - $this->paymentClass = new paymill_payment($this->client->getId(),$this->total_complete,get_woocommerce_currency()); // create payment object, as it should be used for next processing instead of the token. + $this->paymentClass = new paymill_payment($this->client->getId(),($this->totalProducts+$this->totalSub),get_woocommerce_currency()); // create payment object, as it should be used for next processing instead of the token. if($GLOBALS['paymill_loader']->paymill_errors->status()){ $GLOBALS['paymill_loader']->paymill_errors->getErrors(); return false; diff --git a/lib/js/paymill.js b/lib/js/paymill.js index a186a50..04dacef 100644 --- a/lib/js/paymill.js +++ b/lib/js/paymill.js @@ -4,9 +4,8 @@ if(paymill_pcidss3 == 1){ //document.getElementById("paymill_payment_errors").textContent += message + "\n"; console.log(message); } - - // Load embedded credit card frame in an iframe. - jQuery('body').on('updated_checkout',function(){ + + function paymill_embed_pcidss3_frame(){ // Prepare container element, either ID or DOM element - all variants are useable. var frameContainer = jQuery('#paymill_form_credit'); @@ -29,15 +28,25 @@ if(paymill_pcidss3 == 1){ }, frameCallback ); + } + + jQuery(document).ready(function(){ + paymill_embed_pcidss3_frame(); + }); + + // Load embedded credit card frame in an iframe. + jQuery('body').on('updated_checkout',function(){ + paymill_embed_pcidss3_frame(); }); } -jQuery(document).ready(function () { +jQuery(document).ready(function(){ var paymill_youshallpass = false; if(typeof paymill_shop_name != 'undefined'){ if(paymill_shop_name == 'woocommerce' || jQuery('body').hasClass('woocommerce-checkout')){ jQuery('body').on('click', paymill_form_checkout_submit_id, function(event) { + console.log('test'); // set delivery date if(jQuery.datepicker && jQuery("#e_deliverydate").length != 0){ var datefield = jQuery('#e_deliverydate').datepicker('getDate'); @@ -83,7 +92,16 @@ jQuery(document).ready(function () { } function bridgePreparePayment(){ // check which payment method is active - if((jQuery('#paymill_form_credit').is(':visible') || (jQuery('#paymill_card_number').length > 0 && jQuery('#paymill_card_number').val() != '')) && (jQuery('#payment_method_paymill').is(':checked') || jQuery('.wgm-second-checkout input[name=payment_method]').val() == 'paymill' || paymill_shop_name == 'cart66')){ + if( + ( + jQuery('#paymill_form_credit').is(':visible') || + (jQuery('#paymill_card_number').length > 0 && jQuery('#paymill_card_number').val() != '') + ) && + ( + jQuery('#payment_method_paymill').is(':checked') || + jQuery('.wgm-second-checkout input[name=payment_method]').val() == 'paymill' || + paymill_shop_name == 'cart66') + ){ if(paymill_pcidss3 == 1){ paymill.createTokenViaFrame({ amount_int: jQuery('.paymill_amount').val(), diff --git a/lib/setup.inc.php b/lib/setup.inc.php index 895c8ce..2907dab 100644 --- a/lib/setup.inc.php +++ b/lib/setup.inc.php @@ -79,7 +79,7 @@ function paymill_check_webhook(){ return '