From 10d853cdb3d1be96921ad9886a05b52347a66d41 Mon Sep 17 00:00:00 2001 From: Laurent Destailleur Date: Thu, 2 May 2019 21:54:28 +0200 Subject: [PATCH] Work on stripe payment using intent with option STRIPE_USE_INTENT_WITH_AUTOMATIC_CONFIRMATION --- htdocs/langs/en_US/paypal.lang | 3 +- htdocs/public/payment/newpayment.php | 265 +++++++++++++++++++++++++-- htdocs/stripe/class/stripe.class.php | 235 ++++++++++++------------ 3 files changed, 375 insertions(+), 128 deletions(-) diff --git a/htdocs/langs/en_US/paypal.lang b/htdocs/langs/en_US/paypal.lang index 51e98ccc8a2..1f8a9c2bd35 100644 --- a/htdocs/langs/en_US/paypal.lang +++ b/htdocs/langs/en_US/paypal.lang @@ -1,7 +1,7 @@ # Dolibarr language file - Source file is en_US - paypal PaypalSetup=PayPal module setup PaypalDesc=This module allows payment by customers via PayPal. This can be used for a ad-hoc payment or for a payment related to a Dolibarr object (invoice, order, ...) -PaypalOrCBDoPayment=Pay with PayPal (Credit Card or PayPal) +PaypalOrCBDoPayment=Pay with PayPal (Card or PayPal) PaypalDoPayment=Pay with PayPal PAYPAL_API_SANDBOX=Mode test/sandbox PAYPAL_API_USER=API username @@ -33,3 +33,4 @@ PaypalImportPayment=Import PayPal payments PostActionAfterPayment=Post actions after payments ARollbackWasPerformedOnPostActions=A rollback was performed on all Post actions. You must complete post actions manually if they are necessary. ValidationOfPaymentFailed=Validation of payment has failed +CardOwner=Card owner diff --git a/htdocs/public/payment/newpayment.php b/htdocs/public/payment/newpayment.php index f06228291ef..8489f0ef071 100644 --- a/htdocs/public/payment/newpayment.php +++ b/htdocs/public/payment/newpayment.php @@ -803,6 +803,28 @@ if (! $source) print ''; print ''."\n"; + if (! empty($conf->stripe->enabled) && $paymentmethod == 'stripe' && ! empty($conf->global->STRIPE_USE_INTENT_WITH_AUTOMATIC_CONFIRMATION)) + { + require_once DOL_DOCUMENT_ROOT.'/stripe/class/stripe.class.php'; + + $service = 'StripeLive'; + $servicestatus = 1; + + if (empty($conf->global->STRIPE_LIVE) || GETPOST('forcesandbox', 'alpha')) + { + $service = 'StripeTest'; + $servicestatus = 0; + } + $stripe = new Stripe($db); + $stripeacc = $stripe->getStripeAccount($service); + $stripecu = null; + // for dev only + print ''.$langs->trans("PaymentIntent"); + print ''; + $paymentintent=$stripe->getPaymentIntent($amount, $currency, $tag, $object, $stripecu, $stripeacc, $servicestatus); + print ''.$paymentintent->id.''; + print ''."\n"; + } // We do not add fields shipToName, shipToStreet, shipToCity, shipToState, shipToCountryCode, shipToZip, shipToStreet2, phoneNum // as they don't exists (buyer is unknown, tag is free). } @@ -896,6 +918,29 @@ if ($source == 'order') print ''; print ''."\n"; + if (! empty($conf->stripe->enabled) && $paymentmethod == 'stripe' && empty($order->billed) && ! empty($conf->global->STRIPE_USE_INTENT_WITH_AUTOMATIC_CONFIRMATION)) + { + require_once DOL_DOCUMENT_ROOT.'/stripe/class/stripe.class.php'; + + $service = 'StripeLive'; + $servicestatus = 1; + + if (empty($conf->global->STRIPE_LIVE) || GETPOST('forcesandbox', 'alpha')) + { + $service = 'StripeTest'; + $servicestatus = 0; + } + $stripe = new Stripe($db); + $stripeacc = $stripe->getStripeAccount($service); + $stripecu = $stripe->customerStripe($order->thirdparty, $stripeacc, $servicestatus, 1); + // for dev only + print ''.$langs->trans("PaymentIntent"); + print ''; + $paymentintent=$stripe->getPaymentIntent($amount, $currency, $tag, $object, $stripecu, $stripeacc, $servicestatus); + print ''.$paymentintent->id.''; + print ''."\n"; + } + // Shipping address $shipToName=$order->thirdparty->name; $shipToStreet=$order->thirdparty->address; @@ -1007,14 +1052,14 @@ if ($source == 'invoice') print ''; print ''; } - // Currency - print ' '.$langs->trans("Currency".$currency).''; - print ''; } else { - print price($object->total_ttc, 1, $langs); + print ''.price($object->total_ttc, 1, $langs).''; } + // Currency + print ' '.$langs->trans("Currency".$currency).''; + print ''; print ''."\n"; // Tag @@ -1024,6 +1069,28 @@ if ($source == 'invoice') print ''; print ''."\n"; + if (! empty($conf->stripe->enabled) && ($paymentmethod == 'stripe') && empty($object->paye) && ! empty($conf->global->STRIPE_USE_INTENT_WITH_AUTOMATIC_CONFIRMATION)) + { + require_once DOL_DOCUMENT_ROOT.'/stripe/class/stripe.class.php'; + + $service = 'StripeLive'; + $servicestatus = 1; + if (empty($conf->global->STRIPE_LIVE) || GETPOST('forcesandbox', 'alpha')) + { + $service = 'StripeTest'; + $servicestatus = 0; + } + $stripe = new Stripe($db); + $stripeacc = $stripe->getStripeAccount($service); + $stripecu = $stripe->customerStripe($invoice->thirdparty, $stripeacc, $servicestatus, 1); + // for dev only + print ''.$langs->trans("PaymentIntent"); + print ''; + $paymentintent=$stripe->getPaymentIntent($amount, $currency, $tag, $object, $stripecu, $stripeacc, $servicestatus); + print ''.$paymentintent->id.''; + print ''."\n"; + } + // Shipping address $shipToName=$invoice->thirdparty->name; $shipToStreet=$invoice->thirdparty->address; @@ -1149,6 +1216,7 @@ if ($source == 'contractline') // Debitor print ''.$langs->trans("ThirdParty"); print ''.$contract->thirdparty->name.''; + print ''."\n"; // Object $text=''.$langs->trans("PaymentRenewContractId", $contract->ref, $contractline->ref).''; @@ -1234,6 +1302,29 @@ if ($source == 'contractline') print ''; print ''."\n"; + if (! empty($conf->stripe->enabled) && $paymentmethod == 'stripe' && ! empty($conf->global->STRIPE_USE_INTENT_WITH_AUTOMATIC_CONFIRMATION)) + { + require_once DOL_DOCUMENT_ROOT.'/stripe/class/stripe.class.php'; + + $service = 'StripeLive'; + $servicestatus = 1; + + if (empty($conf->global->STRIPE_LIVE) || GETPOST('forcesandbox', 'alpha')) + { + $service = 'StripeTest'; + $servicestatus = 0; + } + $stripe = new Stripe($db); + $stripeacc = $stripe->getStripeAccount($service); + $stripecu = null; + // for dev only + print ''.$langs->trans("PaymentIntent"); + print ''; + $paymentintent=$stripe->getPaymentIntent($amount, $currency, $tag, $object, $stripecu, $stripeacc, $servicestatus); + print ''.$paymentintent->id.''; + print ''."\n"; + } + // Shipping address $shipToName=$contract->thirdparty->name; $shipToStreet=$contract->thirdparty->address; @@ -1400,6 +1491,29 @@ if ($source == 'membersubscription') print ''; print ''."\n"; + if (! empty($conf->stripe->enabled) && $paymentmethod == 'stripe' && ! empty($conf->global->STRIPE_USE_INTENT_WITH_AUTOMATIC_CONFIRMATION)) + { + require_once DOL_DOCUMENT_ROOT.'/stripe/class/stripe.class.php'; + + $service = 'StripeLive'; + $servicestatus = 1; + + if (empty($conf->global->STRIPE_LIVE) || GETPOST('forcesandbox', 'alpha')) + { + $service = 'StripeTest'; + $servicestatus = 0; + } + $stripe = new Stripe($db); + $stripeacc = $stripe->getStripeAccount($service); + $stripecu = null; + // for dev only + print ''.$langs->trans("PaymentIntent"); + print ''; + $paymentintent=$stripe->getPaymentIntent($amount, $currency, $tag, $object, $stripecu, $stripeacc, $servicestatus); + print ''.$paymentintent->id.''; + print ''."\n"; + } + // Shipping address $shipToName=$member->getFullName($langs); $shipToStreet=$member->address; @@ -1541,6 +1655,29 @@ if ($source == 'donation') print ''; print ''."\n"; + if (! empty($conf->stripe->enabled) && ($paymentmethod == 'stripe') && empty($object->paid) && ! empty($conf->global->STRIPE_USE_INTENT_WITH_AUTOMATIC_CONFIRMATION)) + { + require_once DOL_DOCUMENT_ROOT.'/stripe/class/stripe.class.php'; + + $service = 'StripeLive'; + $servicestatus = 1; + + if (empty($conf->global->STRIPE_LIVE) || GETPOST('forcesandbox', 'alpha')) + { + $service = 'StripeTest'; + $servicestatus = 0; + } + $stripe = new Stripe($db); + $stripeacc = $stripe->getStripeAccount($service); + $stripecu = $stripe->customerStripe($don->thirdparty, $stripeacc, $servicestatus, 1); + // for dev only + print ''.$langs->trans("PaymentIntent"); + print ''; + $paymentintent=$stripe->getPaymentIntent($amount, $currency, $tag, $object, $stripecu, $stripeacc, $servicestatus); + print ''.$paymentintent->id.''; + print ''."\n"; + } + // Shipping address $shipToName=$don->getFullName($langs); $shipToStreet=$don->address; @@ -1724,25 +1861,38 @@ if (preg_match('/^dopayment/', $action)) print ''; print ' - - +
+ +
'; -
+ if (! empty($conf->global->STRIPE_USE_INTENT_WITH_AUTOMATIC_CONFIRMATION)) + { + print '
'; + } - -
+ print ' +
+ + '; + + if (! empty($conf->global->STRIPE_USE_INTENT_WITH_AUTOMATIC_CONFIRMATION)) + { + print '
'; + } + + print '
-
+
'; -
- + print '
+ -
+
'."\n"; @@ -1750,8 +1900,94 @@ if (preg_match('/^dopayment/', $action)) // Code to ask the credit card. This use the default "API version". No way to force API version when using JS code. print ''; } } diff --git a/htdocs/stripe/class/stripe.class.php b/htdocs/stripe/class/stripe.class.php index d604dcdd3f2..1f2f39719a8 100644 --- a/htdocs/stripe/class/stripe.class.php +++ b/htdocs/stripe/class/stripe.class.php @@ -140,7 +140,7 @@ class Stripe extends CommonObject if (empty($object->id)) { - dol_syslog("customerStripe is called with param object not loaded"); + dol_syslog("customerStripe is called with the parameter object that is not loaded"); return null; } @@ -240,6 +240,9 @@ class Stripe extends CommonObject /** * Get the Stripe payment intent. Create it with confirm=false * + * @param double $amount Amount + * @param string $currency_code Currency code + * @param string $tag Tag * @param Societe $object Object to pay with Stripe * @param string $customer Stripe customer ref 'cus_xxxxxxxxxxxxx' via customerStripe() * @param string $key ''=Use common API. If not '', it is the Stripe connect account 'acc_....' to use Stripe connect @@ -249,127 +252,133 @@ class Stripe extends CommonObject * @param boolean $confirmnow false=default, true=try to confirm immediatly after create (if conditions are ok) * @return \Stripe\PaymentIntent|null Stripe PaymentIntent or null if not found */ - public function getPaymentIntent($object, $customer, $key = null, $status = 0, $usethirdpartyemailforreceiptemail = 0, $mode = 'automatic', $confirmnow = false) + public function getPaymentIntent($amount, $currency_code, $tag, $object = null, $customer = null, $key = null, $status = 0, $usethirdpartyemailforreceiptemail = 0, $mode = 'automatic', $confirmnow = false) { global $conf, $user, $mysoc; - if (empty($object->id)) - { - dol_syslog("object not loaded"); - return null; - } - - $error = 0; - if (empty($status)) $service = 'StripeTest'; else $service = 'StripeLive'; + $arrayzerounitcurrency=array('BIF', 'CLP', 'DJF', 'GNF', 'JPY', 'KMF', 'KRW', 'MGA', 'PYG', 'RWF', 'VND', 'VUV', 'XAF', 'XOF', 'XPF'); + if (! in_array($currency_code, $arrayzerounitcurrency)) $stripeamount = $amount * 100; + else $stripeamount = $amount; + + $fee = round(($$stripeamount * ($conf->global->STRIPE_APPLICATION_FEE_PERCENT / 100) + $conf->global->STRIPE_APPLICATION_FEE) * 100); + if ($fee >= ($conf->global->STRIPE_APPLICATION_FEE_MAXIMAL * 100) && $conf->global->STRIPE_APPLICATION_FEE_MAXIMAL>$conf->global->STRIPE_APPLICATION_FEE_MINIMAL) { + $fee = round($conf->global->STRIPE_APPLICATION_FEE_MAXIMAL * 100); + } elseif ($fee < ($conf->global->STRIPE_APPLICATION_FEE_MINIMAL * 100)) { + $fee = round($conf->global->STRIPE_APPLICATION_FEE_MINIMAL * 100); + } + $paymentintent = null; - $sql = "SELECT pi.ext_payment_id, pi.entity, pi.fk_facture, pi.sourcetype, pi.ext_payment_site"; - $sql.= " FROM " . MAIN_DB_PREFIX . "prelevement_facture_demande as pi"; - $sql.= " WHERE pi.fk_facture = " . $object->id; - $sql.= " AND pi.sourcetype = '" . $object->element . "'"; - $sql.= " AND pi.entity IN (".getEntity('societe').")"; - $sql.= " AND pi.ext_payment_site = '" . $service . "'"; - - dol_syslog(get_class($this) . "::getPaymentIntent search stripe customer id for thirdparty id=".$object->id, LOG_DEBUG); - $resql = $this->db->query($sql); - if ($resql) { - $num = $this->db->num_rows($resql); - if ($num) - { - $obj = $this->db->fetch_object($resql); - $intent = $obj->ext_payment_id; - - dol_syslog(get_class($this) . "::getPaymentIntent found existing payment intent record"); - - // Force to use the correct API key - global $stripearrayofkeysbyenv; - \Stripe\Stripe::setApiKey($stripearrayofkeysbyenv[$status]['secret_key']); - - try { - if (empty($key)) { // If the Stripe connect account not set, we use common API usage - $paymentintent = \Stripe\PaymentIntent::retrieve($intent); - } else { - $paymentintent = \Stripe\PaymentIntent::retrieve($intent, array("stripe_account" => $key)); - } - } - catch(Exception $e) - { - $this->error = $e->getMessage(); - } - } - else //if ($createifnotlinkedtostripe) - { - $arrayzerounitcurrency=array('BIF', 'CLP', 'DJF', 'GNF', 'JPY', 'KMF', 'KRW', 'MGA', 'PYG', 'RWF', 'VND', 'VUV', 'XAF', 'XOF', 'XPF'); - if (! in_array($object->multicurrency_code, $arrayzerounitcurrency)) $stripeamount=$object->multicurrency_total_ttc * 100; - else $stripeamount = $object->multicurrency_total_ttc; - - $fee = round(($object->total_ttc * ($conf->global->STRIPE_APPLICATION_FEE_PERCENT / 100) + $conf->global->STRIPE_APPLICATION_FEE) * 100); - if ($fee >= ($conf->global->STRIPE_APPLICATION_FEE_MAXIMAL * 100) && $conf->global->STRIPE_APPLICATION_FEE_MAXIMAL>$conf->global->STRIPE_APPLICATION_FEE_MINIMAL) { - $fee = round($conf->global->STRIPE_APPLICATION_FEE_MAXIMAL * 100); - } elseif ($fee < ($conf->global->STRIPE_APPLICATION_FEE_MINIMAL * 100)) { - $fee = round($conf->global->STRIPE_APPLICATION_FEE_MINIMAL * 100); - } - - $ipaddress=getUserRemoteIP(); - // Not enough space for a ref so we store id. Also with multicompany we can have same ref for 2 different - // object and we need a unique (this is used later as idempotency_key) - $description=$object->element.$object->id; - - $dataforintent = array( - "confirm" => $confirmnow, // Do not confirm immediatly during creation of intent - "confirmation_method" => $mode, - "amount" => $stripeamount, - "currency" => $object->multicurrency_code, - "customer" => $customer, - "allowed_source_types" => ["card"], - "statement_descriptor" => dol_trunc($description, 10, 'right', 'UTF-8', 1), // 22 chars that appears on bank receipt (company + description) - "metadata" => array('dol_type'=>$object->element, 'dol_id'=>$object->id, 'dol_version'=>DOL_VERSION, 'dol_entity'=>$conf->entity, 'ipaddress'=>$ipaddress) - ); - // save_payment_method = true, - // payment_method_types = - // payment_method = - - if ($conf->entity!=$conf->global->STRIPECONNECT_PRINCIPAL && $fee>0) - { - $dataforintent["application_fee"] = $fee; - } - if ($usethirdpartyemailforreceiptemail && $object->thirdparty->email) - { - $dataforintent["receipt_email"] = $object->thirdparty->email; - } - - try { - // Force to use the correct API key - global $stripearrayofkeysbyenv; - \Stripe\Stripe::setApiKey($stripearrayofkeysbyenv[$status]['secret_key']); - - if (empty($key)) { // If the Stripe connect account not set, we use common API usage - $paymentintent = \Stripe\PaymentIntent::create($dataforintent, array("idempotency_key" => "$description")); - } else { - $paymentintent = \Stripe\PaymentIntent::create($dataforintent, array("idempotency_key" => "$description","stripe_account" => $key)); - } - $now=dol_now(); - $sql = "INSERT INTO " . MAIN_DB_PREFIX . "prelevement_facture_demande (fk_soc, date_demande, fk_user_demande, ext_payment_id, fk_facture, sourcetype, entity, ext_payment_site)"; - $sql .= " VALUES ('".$object->socid."','".$this->db->idate($now)."', '0', '".$this->db->escape($paymentintent->id)."', ".$object->id.", '".$this->db->escape($object->element)."', " . $conf->entity . ", '" . $service . "')"; - $resql = $this->db->query($sql); - if (! $resql) - { - $this->error = $this->db->lasterror(); - dol_syslog(get_class($this) . "::PaymentIntent failed to insert paymentintent with id=".$paymentintent->id." into database."); - } - } - catch(Exception $e) - { - $this->error = $e->getMessage(); - } - } - } - else + if (is_object($object)) { - dol_print_error($this->db); + $sql = "SELECT pi.ext_payment_id, pi.entity, pi.fk_facture, pi.sourcetype, pi.ext_payment_site"; + $sql.= " FROM " . MAIN_DB_PREFIX . "prelevement_facture_demande as pi"; + $sql.= " WHERE pi.fk_facture = " . $object->id; + $sql.= " AND pi.sourcetype = '" . $object->element . "'"; + $sql.= " AND pi.entity IN (".getEntity('societe').")"; + $sql.= " AND pi.ext_payment_site = '" . $service . "'"; + + dol_syslog(get_class($this) . "::getPaymentIntent search stripe payment intent for object id = ".$object->id, LOG_DEBUG); + $resql = $this->db->query($sql); + if ($resql) { + $num = $this->db->num_rows($resql); + if ($num) + { + $obj = $this->db->fetch_object($resql); + $intent = $obj->ext_payment_id; + + dol_syslog(get_class($this) . "::getPaymentIntent found existing payment intent record"); + + // Force to use the correct API key + global $stripearrayofkeysbyenv; + \Stripe\Stripe::setApiKey($stripearrayofkeysbyenv[$status]['secret_key']); + + try { + if (empty($key)) { // If the Stripe connect account not set, we use common API usage + $paymentintent = \Stripe\PaymentIntent::retrieve($intent); + } else { + $paymentintent = \Stripe\PaymentIntent::retrieve($intent, array("stripe_account" => $key)); + } + } + catch(Exception $e) + { + $this->error = $e->getMessage(); + } + } + } + } + + if (empty($paymentintent)) + { + $ipaddress=getUserRemoteIP(); + // Not enough space for a ref so we store id. Also with multicompany we can have same ref for 2 different + // object and we need a unique (this is used later as idempotency_key) + $description=$tag; + $metadata = array('dol_version'=>DOL_VERSION, 'dol_entity'=>$conf->entity, 'ipaddress'=>$ipaddress); + if (is_object($object)) + { + $metadata['dol_type'] = $object->element; + $metadata['dol_id'] = $object->id; + } + + $dataforintent = array( + "confirm" => $confirmnow, // Do not confirm immediatly during creation of intent + "confirmation_method" => $mode, + "amount" => $stripeamount, + "currency" => $currency_code, + "payment_method_types" => ["card"], + "statement_descriptor" => dol_trunc($description, 10, 'right', 'UTF-8', 1), // 22 chars that appears on bank receipt (company + description) + "metadata" => $metadata + ); + if (! is_null($customer)) $dataforintent["customer"]=$customer; + // save_payment_method = true, + // payment_method = + + if ($conf->entity!=$conf->global->STRIPECONNECT_PRINCIPAL && $fee>0) + { + $dataforintent["application_fee"] = $fee; + } + if ($usethirdpartyemailforreceiptemail && is_object($object) && $object->thirdparty->email) + { + $dataforintent["receipt_email"] = $object->thirdparty->email; + } + + try { + // Force to use the correct API key + global $stripearrayofkeysbyenv; + \Stripe\Stripe::setApiKey($stripearrayofkeysbyenv[$status]['secret_key']); + + if (empty($key)) { // If the Stripe connect account not set, we use common API usage + $paymentintent = \Stripe\PaymentIntent::create($dataforintent, array("idempotency_key" => "$description")); + } else { + $paymentintent = \Stripe\PaymentIntent::create($dataforintent, array("idempotency_key" => "$description", "stripe_account" => $key)); + } + + // Store the payment intent + if (is_object($object)) + { + $now=dol_now(); + $sql = "INSERT INTO " . MAIN_DB_PREFIX . "prelevement_facture_demande (fk_soc, date_demande, fk_user_demande, ext_payment_id, fk_facture, sourcetype, entity, ext_payment_site)"; + $sql .= " VALUES ('".$object->socid."','".$this->db->idate($now)."', '0', '".$this->db->escape($paymentintent->id)."', ".$object->id.", '".$this->db->escape($object->element)."', " . $conf->entity . ", '" . $service . "')"; + $resql = $this->db->query($sql); + if (! $resql) + { + $this->error = $this->db->lasterror(); + dol_syslog(get_class($this) . "::PaymentIntent failed to insert paymentintent with id=".$paymentintent->id." into database."); + } + } + else + { + $_SESSION["stripe_payment_intent"] = $paymentintent; + } + } + catch(Exception $e) + { + $this->error = $e->getMessage(); + } } return $paymentintent;