Просмотр исходного кода

Ticket #3913 - Payments: Integrate Authorized payment.

AntonLV 4 лет назад
Родитель
Сommit
516d8b0fbf

+ 44 - 0
inc/classes/BxDolPayments.php

@@ -42,6 +42,14 @@ class BxDolPayments extends BxDolFactory implements iBxDolSingleton
     	return $this->_sActive;
     }
 
+    public function getActiveObject()
+    {
+        if(!$this->isActive())
+            return null;
+
+    	return BxDolModule::getInstance($this->_sActive);
+    }
+
     public function isActive()
     {
     	if(empty($this->_sActive))
@@ -424,6 +432,42 @@ class BxDolPayments extends BxDolFactory implements iBxDolSingleton
         return bx_srv($this->_sActive, 'send_subscription_expiration_letters', $aSrvParams, 'Subscriptions');
     }
 
+    public function authorizeCheckoutSingle($iVendorId, $sProvider, $aItems = array())
+    {
+        if(!BxDolRequest::serviceExists($this->_sActive, 'authorize_checkout'))
+            return '';
+
+        $aSrvParams = array(BX_PAYMENT_TYPE_SINGLE, $iVendorId, $sProvider, $aItems);
+        return bx_srv($this->_sActive, 'authorize_checkout', $aSrvParams);
+    }
+    
+    public function authorizeCheckoutRecurring($iVendorId, $sProvider, $aItems = array())
+    {
+        if(!BxDolRequest::serviceExists($this->_sActive, 'authorize_checkout'))
+            return '';
+
+        $aSrvParams = array(BX_PAYMENT_TYPE_RECURRING, $iVendorId, $sProvider, $aItems);
+        return bx_srv($this->_sActive, 'authorize_checkout', $aSrvParams);
+    }
+    
+    public function captureAuthorizedCheckoutSingle($sOrder)
+    {
+        if(!BxDolRequest::serviceExists($this->_sActive, 'capture_authorized_checkout'))
+            return '';
+
+        $aSrvParams = array(BX_PAYMENT_TYPE_SINGLE, $sOrder);
+        return bx_srv($this->_sActive, 'capture_authorized_checkout', $aSrvParams);
+    }
+    
+    public function captureAuthorizedCheckoutRecurring($sOrder)
+    {
+        if(!BxDolRequest::serviceExists($this->_sActive, 'capture_authorized_checkout'))
+            return '';
+
+        $aSrvParams = array(BX_PAYMENT_TYPE_RECURRING, $sOrder);
+        return bx_srv($this->_sActive, 'capture_authorized_checkout', $aSrvParams);
+    }
+
     public function initializeCheckout($iVendorId, $sProvider, $aItems = array())
     {
         if(!BxDolRequest::serviceExists($this->_sActive, 'initialize_checkout'))

+ 11 - 1
modules/base/payment/classes/BxBaseModPaymentModule.php

@@ -310,6 +310,11 @@ class BxBaseModPaymentModule extends BxBaseModGeneralModule
     	return BxDolService::call($mixedModule, 'get_cart_items', $aParams);
     }
 
+    public function callAuthorizeCartItem($mixedModule, $aParams)
+    {
+    	return BxDolService::call($mixedModule, 'authorize_cart_item', $aParams);
+    }
+
     public function callRegisterCartItem($mixedModule, $aParams)
     {
     	return BxDolService::call($mixedModule, 'register_cart_item', $aParams);
@@ -320,7 +325,12 @@ class BxBaseModPaymentModule extends BxBaseModGeneralModule
     	return BxDolService::call($mixedModule, 'reregister_cart_item', $aParams);
     }
 
-	public function callRegisterSubscriptionItem($mixedModule, $aParams)
+    public function callAuthorizeSubscriptionItem($mixedModule, $aParams)
+    {
+    	return BxDolService::call($mixedModule, 'authorize_subscription_item', $aParams);
+    }
+
+    public function callRegisterSubscriptionItem($mixedModule, $aParams)
     {
     	return BxDolService::call($mixedModule, 'register_subscription_item', $aParams);
     }

+ 5 - 0
modules/base/payment/classes/BxBaseModPaymentProvider.php

@@ -151,6 +151,11 @@ class BxBaseModPaymentProvider extends BxDol
 
     public function addJsCss() {}
 
+    public function getJsObject()
+    {
+        return $this->_oModule->_oConfig->getJsObject($this->_sName);
+    }
+
     public function finalizedCheckout() {}
 
     public function isSubscriptionStatus($sStatus, $aSubscription, $sStatusKey = 'status')

+ 11 - 9
modules/boonex/payment/classes/BxPaymentCart.php

@@ -287,14 +287,6 @@ class BxPaymentCart extends BxBaseModPaymentCart
         foreach($aContent as $iSellerId => $aSellerItems)
             $aResult[$iSellerId] = $this->_getInfo($sType, $iUserId, $iSellerId, $aSellerItems);
 
-        $this->_oModule->alert('get_cart_info', 0, 0, array(
-           'type' => $sType,
-           'client_id' => $iUserId,
-           'seller_id' => $iSellerId,
-           'items' => $aItems,
-           'override_result' => &$aResult
-       ));
-        
         return $aResult;
     }
 
@@ -365,7 +357,7 @@ class BxPaymentCart extends BxBaseModPaymentCart
         }
 
         $aSeller = $this->_oModule->getVendorInfo((int)$iSellerId);
-        return array(
+        $aResult = array(
             'client_id' => $iClientId,
             'vendor_id' => $aSeller['id'],
             'vendor_name' => $aSeller['name'],
@@ -380,6 +372,16 @@ class BxPaymentCart extends BxBaseModPaymentCart
             'items_price' => $fItemsPrice,
             'items' => $aItemsInfo
         );
+
+        $this->_oModule->alert('get_cart_info', 0, 0, array(
+           'type' => $sType,
+           'client_id' => $iClientId,
+           'seller_id' => $iSellerId,
+           'items' => $aItems,
+           'override_result' => &$aResult
+        ));
+
+        return $aResult;
     }
 }
 

+ 1 - 0
modules/boonex/payment/classes/BxPaymentConfig.php

@@ -35,6 +35,7 @@ class BxPaymentConfig extends BxBaseModPaymentConfig
             'URL_CARTS' => 'page.php?i=payment-carts',
             'URL_CART' => 'page.php?i=payment-cart',
             'URL_CART_CHECKOUT' => $sBaseUrl . 'initialize_checkout/' . BX_PAYMENT_TYPE_SINGLE . '/',
+            'URL_AUTHORIZE' => $sBaseUrl . 'authorize_checkout/',
             'URL_SUBSCRIBE' => $sBaseUrl . 'subscribe/',
             'URL_SUBSCRIBE_JSON' => $sBaseUrl . 'subscribe_json/',
             'URL_SUBSCRIPTIONS' => 'page.php?i=payment-sbs-list-my',

+ 275 - 27
modules/boonex/payment/classes/BxPaymentModule.php

@@ -565,7 +565,7 @@ class BxPaymentModule extends BxBaseModPaymentModule
 			return $this->_oTemplate->displayPageCodeResponse($aResult['message']);
 
         if($bRedirect) {
-    		header('Location: ' . $aResult['redirect']);
+            header('Location: ' . $aResult['redirect']);
             exit;
         }
     }
@@ -692,6 +692,62 @@ class BxPaymentModule extends BxBaseModPaymentModule
     /**
      * Payment Processing Methods
      */
+    public function actionAuthorizeCheckout($sType)
+    {
+    	if(!$this->isLogged())
+            return $this->_oTemplate->displayPageCodeError($this->_sLangsPrefix . 'err_required_login');
+
+        if(bx_get('seller_id') === false || bx_get('provider') === false || bx_get('items') === false)
+            return $this->_oTemplate->displayPageCodeError($this->_sLangsPrefix . 'err_wrong_data');
+
+        $iSellerId = bx_process_input(bx_get('seller_id'), BX_DATA_INT);
+        $sProvider = bx_process_input(bx_get('provider'));
+        $aItems = bx_process_input(bx_get('items'));
+
+        $mixedResult = $this->serviceAuthorizeCheckout($sType, $iSellerId, $sProvider, $aItems);
+        if(is_string($mixedResult))
+            return $this->_oTemplate->displayPageCodeError($mixedResult);
+
+        if(is_array($mixedResult) && !empty($mixedResult['redirect'])) {
+            header('Location: ' . $mixedResult['redirect']);
+            exit;
+        }
+
+        return $this->_oTemplate->displayPageCodeResponse($this->_sLangsPrefix . 'msg_successfully_performed');
+    }
+
+    public function actionCaptureAuthorizedCheckout($sType)
+    {
+        $iProfileId = bx_get_logged_profile_id();
+    	if(!$iProfileId)
+            return $this->_oTemplate->displayPageCodeError($this->_sLangsPrefix . 'err_required_login');
+
+        $sOrderAuth = '';
+        if(($sOrderAuth = bx_get('order')) !== false) 
+            $sOrderAuth = bx_process_input($sOrderAuth);
+
+        if(empty($sOrderAuth))
+            return $this->_oTemplate->displayPageCodeError($this->_sLangsPrefix . 'err_empty_order');
+
+        $aPending = $this->_oDb->getOrderPending(['type' => 'order', 'order' => $sOrderAuth]);
+        if(empty($aPending) || !is_array($aPending))
+            return $this->_oTemplate->displayPageCodeError($this->_sLangsPrefix . 'err_empty_order');
+
+        if((int)$aPending['seller_id'] != $iProfileId)
+            return $this->_oTemplate->displayPageCodeError($this->_sLangsPrefix . 'err_wrong_vendor');
+
+        $mixedResult = $this->serviceCaptureAuthorizedCheckout($sType, $sOrderAuth);
+        if(is_string($mixedResult))
+            return $this->_oTemplate->displayPageCodeError($mixedResult);
+
+        if(!empty($mixedResult['redirect'])) {
+            header('Location: ' . base64_decode(urldecode($mixedResult['redirect'])));
+            exit;
+        }
+
+        return $this->_oTemplate->displayPageCodeResponse($mixedResult['message']);
+    }
+
     public function actionInitializeCheckout($sType)
     {
     	if(!$this->isLogged())
@@ -733,6 +789,137 @@ class BxPaymentModule extends BxBaseModPaymentModule
         return echoJson(['code' => 0, 'redirect' => $this->_oConfig->getUrl('URL_CART')]);
     }
 
+    /**
+     * @page service Service Calls
+     * @section bx_payment Payment
+     * @subsection bx_payment-purchase_processing Purchase Processing
+     * @subsubsection bx_payment-authorize_checkout authorize_checkout
+     * 
+     * @code bx_srv('bx_payment', 'authorize_checkout', [...]); @endcode
+     * 
+     * Authorize order for future checkout.
+     * 
+     * @param $sType string value with payment type (single or recurring). 
+     * @param $iSellerId integer value with seller ID. 
+     * @param $sProvider string value with payment provider name. 
+     * @param $aItems (optional) an array with items to be purchased. 
+     * @param $sRedirect (optional) string value with redirect URL if it's needed.
+     * @return the result depends on the payment provider which is used for processing or represented as string value with error message if something is wrong.
+     * 
+     * @see BxPaymentModule::serviceAuthorizeCheckout
+     */
+    /** 
+     * @ref bx_payment-authorize_checkout "authorize_checkout"
+     */
+    public function serviceAuthorizeCheckout($sType, $iSellerId, $sProvider, $aItems = [], $sRedirect = '')
+    {
+        $bTypeSingle = $sType == BX_PAYMENT_TYPE_SINGLE;
+        $bTypeRecurring = $sType == BX_PAYMENT_TYPE_RECURRING;
+
+        if(!is_array($aItems))
+            $aItems = [$aItems];
+
+        $iSellerId = (int)$iSellerId;
+        if($iSellerId == BX_PAYMENT_EMPTY_ID)
+            return $this->_sLangsPrefix . 'err_unknown_vendor';
+
+        $oProvider = $this->getObjectProvider($sProvider, $iSellerId);
+        if($oProvider === false || !$oProvider->isActive())
+            return $this->_sLangsPrefix . 'err_incorrect_provider';
+
+        $aInfo = $this->getObjectCart()->getInfo($sType, $this->_iUserId, $iSellerId, $aItems);
+        if(empty($aInfo) || $aInfo['vendor_id'] == BX_PAYMENT_EMPTY_ID || empty($aInfo['items']))
+            return $this->_sLangsPrefix . 'err_empty_order';
+
+        $this->alert('before_authorize', 0, $this->_iUserId, array(
+            'client_id' => $this->_iUserId,
+            'seller_id' => $iSellerId, 
+            'type' => &$sType,
+            'provider' => &$sProvider,
+            'cart_info' => &$aInfo,
+        ));
+
+        $iPendingId = $this->_oDb->insertOrderPending($this->_iUserId, $sType, $sProvider, $aInfo);
+        if(empty($iPendingId))
+            return $this->_sLangsPrefix . 'err_access_db';
+
+        return $oProvider->authorizeCheckout($iPendingId, $aInfo, $sRedirect);
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_payment Payment
+     * @subsection bx_payment-purchase_processing Purchase Processing
+     * @subsubsection bx_payment-capture_authorized_checkout capture_authorized_checkout
+     * 
+     * @code bx_srv('bx_payment', 'capture_authorized_checkout', [...]); @endcode
+     * 
+     * Capture funds for previously authorized order.
+     * 
+     * @param $sType string value with payment type (single or recurring). 
+     * @param $sOrderAuth string value with order's authorization ID. 
+     * @return the result depends on the payment provider which is used for processing or represented as string value with error message if something is wrong.
+     * 
+     * @see BxPaymentModule::serviceCaptureAuthorizedCheckout
+     */
+    /** 
+     * @ref bx_payment-capture_authorized_checkout "capture_authorized_checkout"
+     */
+    public function serviceCaptureAuthorizedCheckout($sType, $sOrderAuth, $sRedirect = '')
+    {
+        $aPending = $this->_oDb->getOrderPending(['type' => 'order', 'order' => $sOrderAuth]);
+        if(empty($aPending) || !is_array($aPending))
+            return $this->_sLangsPrefix . 'err_empty_order';
+
+        if((int)$aPending['processed'] != 0)
+            return $this->_sLangsPrefix . 'err_already_processed';
+
+        $iClientId = (int)$aPending['client_id'];
+        $iSellerId = (int)$aPending['seller_id'];
+        $sProvider = $aPending['provider'];
+
+        $oProvider = $this->getObjectProvider($sProvider, $iSellerId);
+        if($oProvider === false || !$oProvider->isActive())
+            return $this->_sLangsPrefix . 'err_incorrect_provider';
+
+        $aInfo = $this->getObjectCart()->getInfo($sType, $iClientId, $iSellerId, $aPending['items']);
+        if(empty($aInfo) || $aInfo['vendor_id'] == BX_PAYMENT_EMPTY_ID || empty($aInfo['items']))
+            return $this->_sLangsPrefix . 'err_empty_order';
+
+        $this->alert('before_capture_authorized', 0, $iClientId, array(
+            'order_auth' => $sOrderAuth,
+            'client_id' => $iClientId,
+            'seller_id' => $iSellerId, 
+            'type' => &$sType,
+            'provider' => &$sProvider,
+            'cart_info' => &$aInfo,
+        ));
+
+        $aResult = $oProvider->captureAuthorizedCheckout($sOrderAuth, $aPending, $aInfo, $sRedirect);
+        if((int)$aResult['code'] != BX_PAYMENT_RESULT_SUCCESS) 
+            return $aResult['message'];
+
+        //--- Get updated pending order
+        $aPending = $this->_oDb->getOrderPending(['type' => 'id', 'id' => $aResult['pending_id']]);
+
+        //--- Register payment for purchased items in associated modules 
+        if(isset($aResult['paid']) && $aResult['paid'])
+            $this->registerPayment($aPending);
+
+        $this->alert('finalize_checkout', 0, bx_get_logged_profile_id(), array(
+            'pending' => $aPending,
+            'transactions' => $this->_oDb->getOrderProcessed(array('type' => 'pending_id', 'pending_id' => (int)$aPending['id'])),
+            'provider' => $oProvider,
+            'message' => &$aResult['message'],
+            'result' => &$aResult,
+        ));
+
+        if(empty($aResult['redirect']) && $oProvider->needRedirect())
+            $aResult['redirect'] = $oProvider->getReturnUrl();
+
+        return $aResult;
+    }
+
     /**
      * @page service Service Calls
      * @section bx_payment Payment
@@ -835,6 +1022,7 @@ class BxPaymentModule extends BxBaseModPaymentModule
 
         $this->alert('before_pending', 0, bx_get_logged_profile_id(), array(
             'client_id' => $this->_iUserId,
+            'seller_id' => $iSellerId, 
             'type' => &$sType,
             'provider' => &$sProvider,
             'cart_info' => &$aInfo,
@@ -880,13 +1068,18 @@ class BxPaymentModule extends BxBaseModPaymentModule
         if((int)$aResult['code'] != BX_PAYMENT_RESULT_SUCCESS) 
             return $this->_oTemplate->displayPageCodeError($aResult['message']);
 
-        $aPending = $this->_oDb->getOrderPending(array('type' => 'id', 'id' => (int)$aResult['pending_id']));
+        $aPending = $this->_oDb->getOrderPending(['type' => 'id', 'id' => (int)$aResult['pending_id']]);
         $bTypeRecurring = $aPending['type'] == BX_PAYMENT_TYPE_RECURRING;
+        $bAuthorized = isset($aResult['authorized']) && $aResult['authorized'];
+        $bPaid = isset($aResult['paid']) && $aResult['paid'];
+        $bTrial = isset($aResult['trial']) && $aResult['trial'];
+
+        //--- Register subscription
         if($bTypeRecurring) {
             $sStatus = BX_PAYMENT_SBS_STATUS_UNPAID;
-            if(isset($aResult['paid']) && $aResult['paid'])
+            if($bPaid)
                 $sStatus = BX_PAYMENT_SBS_STATUS_ACTIVE;
-            else if(isset($aResult['trial']) && $aResult['trial'])
+            else if($bTrial)
                 $sStatus = BX_PAYMENT_SBS_STATUS_TRIAL;
 
             $aSubscription = array(
@@ -905,10 +1098,17 @@ class BxPaymentModule extends BxBaseModPaymentModule
         if((int)$aPending['client_id'] == 0)
             $this->getObjectJoin()->performJoin((int)$aPending['id'], isset($aResult['client_name']) ? $aResult['client_name'] : '', isset($aResult['client_email']) ? $aResult['client_email'] : '');
 
+        //--- Authorize order for selected items in associated modules 
+        if($bAuthorized)
+            $this->authorizePayment($aPending);
+
         //--- Register payment for purchased items in associated modules 
-        if(!empty($aResult['paid']) || ($bTypeRecurring && !empty($aResult['trial'])))
+        if($bPaid || ($bTypeRecurring && $bTrial))
             $this->registerPayment($aPending);
 
+        //--- Get updated pending order
+        $aPending = $this->_oDb->getOrderPending(['type' => 'id', 'id' => $aPending['id']]);
+
         $this->alert('finalize_checkout', 0, bx_get_logged_profile_id(), array(
             'pending' => $aPending,
             'transactions' => $this->_oDb->getOrderProcessed(array('type' => 'pending_id', 'pending_id' => (int)$aPending['id'])),
@@ -1046,12 +1246,12 @@ class BxPaymentModule extends BxBaseModPaymentModule
     {
     	$CNF = &$this->_oConfig->CNF;
 
-		if($iSellerId == BX_PAYMENT_EMPTY_ID || empty($iModuleId) || empty($iItemId) || empty($iItemCount))
+        if($iSellerId == BX_PAYMENT_EMPTY_ID || empty($iModuleId) || empty($iItemId) || empty($iItemCount))
             return array('code' => 1, 'message' => _t($CNF['T']['ERR_WRONG_DATA']));
 
-		$iClientId = $this->getProfileId();
+        $iClientId = $this->getProfileId();
         if(empty($iClientId)) {
-        	$sLoginUrl = BxDolPermalinks::getInstance()->permalink('page.php?i=login');
+            $sLoginUrl = BxDolPermalinks::getInstance()->permalink('page.php?i=login');
             return array('code' => 2, 'eval' => 'window.open("' . $sLoginUrl . '", "_self");');
         }
 
@@ -1073,6 +1273,49 @@ class BxPaymentModule extends BxBaseModPaymentModule
 		return true;
     }
 
+    public function authorizePayment($mixedPending)
+    {
+        $aPending = is_array($mixedPending) ? $mixedPending : $this->_oDb->getOrderPending(['type' => 'id', 'id' => (int)$mixedPending]);
+    	if(empty($aPending) || !is_array($aPending))
+            return false;
+
+        $sType = $aPending['type'];
+        $bTypeSingle = $sType == BX_PAYMENT_TYPE_SINGLE;
+
+        if((int)$aPending['authorized'] == 1)
+            return true;
+
+        $iClientId = (int)$aPending['client_id'];
+
+        $aCart = array();
+        if($bTypeSingle)
+            $aCart = $this->_oDb->getCartContent($iClientId);
+        
+        $bResult = false;
+        $aItems = $this->_oConfig->descriptorsM2A($aPending['items']);
+        foreach($aItems as $aItem) {
+            $sItem = $this->_oConfig->descriptorA2S([$aItem['vendor_id'], $aItem['module_id'], $aItem['item_id']]);
+            
+            $sMethod = $bTypeSingle ? 'callAuthorizeCartItem' : 'callAuthorizeSubscriptionItem';
+            $aItemInfo = $this->$sMethod((int)$aItem['module_id'], [$aPending['client_id'], $aPending['seller_id'], $aItem['item_id'], $aItem['item_count'], $aPending['order']]);
+            if(empty($aItemInfo) || !is_array($aItemInfo))
+                continue;
+
+            if($bTypeSingle)
+            	$aCart['items'] = trim(preg_replace("'" . $this->_oConfig->descriptorA2S($aItem) . ":?'", "", $aCart['items']), ":");
+
+            $bResult = true;
+        }
+
+        if($bResult) {
+            $this->_oDb->updateOrderPending($aPending['id'], ['authorized' => 1]);
+
+            $this->onPaymentAuthorize($aPending);
+        }
+
+        return $bResult;
+    }
+    
     public function registerPayment($mixedPending)
     {
     	$aPending = is_array($mixedPending) ? $mixedPending : $this->_oDb->getOrderPending(array('type' => 'id', 'id' => (int)$mixedPending));
@@ -1372,62 +1615,67 @@ class BxPaymentModule extends BxBaseModPaymentModule
         }
     }
 
-    public function onPaymentRegisterBefore($aPending, $aResult = array())
+    public function onPaymentRegisterBefore($aPending, $aResult = [])
     {
         $this->alert('before_register_payment', 0, $aPending['client_id'], array('pending' => $aPending));
     }
 
-    public function onPaymentRegister($aPending, $aResult = array())
+    public function onPaymentAuthorize($aPending, $aResult = [])
+    {
+        $this->alert('authorize_payment', 0, $aPending['client_id'], ['pending' => $aPending]);
+    }
+
+    public function onPaymentRegister($aPending, $aResult = [])
     {
         $bTypeSingle = $aPending['type'] == BX_PAYMENT_TYPE_SINGLE;
 
         if($bTypeSingle) {
             $aItems = $this->_oConfig->descriptorsM2A($aPending['items']);
             foreach($aItems as $aItem)
-                $this->isAllowedPurchase(array('module_id' => $aItem['module_id'], 'item_id' => $aItem['item_id']), true);
+                $this->isAllowedPurchase(['module_id' => $aItem['module_id'], 'item_id' => $aItem['item_id']], true);
         }
 
-        $this->alert('register_payment', 0, $aPending['client_id'], array('pending' => $aPending));
+        $this->alert('register_payment', 0, $aPending['client_id'], ['pending' => $aPending]);
     }
 
-    public function onPaymentRefund($aPending, $aResult = array())
+    public function onPaymentRefund($aPending, $aResult = [])
     {
-        $this->alert('refund_payment', 0, $aPending['client_id'], array('pending' => $aPending));
+        $this->alert('refund_payment', 0, $aPending['client_id'], ['pending' => $aPending]);
     }
 
-    public function onSubscriptionCreate($aPending, $aSubscription, $aResult = array())
+    public function onSubscriptionCreate($aPending, $aSubscription, $aResult = [])
     {
         $aItems = $this->_oConfig->descriptorsM2A($aPending['items']);
-        $this->isAllowedPurchase(array('module_id' => $aItems[0]['module_id'], 'item_id' => $aItems[0]['item_id']), true);
+        $this->isAllowedPurchase(['module_id' => $aItems[0]['module_id'], 'item_id' => $aItems[0]['item_id']], true);
 
-        $this->alert('create_subscription', 0, $aPending['client_id'], array(
+        $this->alert('create_subscription', 0, $aPending['client_id'], [
             'pending' => $aPending,
             'subscription' => $aSubscription
-        ));
+        ]);
     }
 
-    public function onSubscriptionProlong($aPending, $aSubscription, $aResult = array())
+    public function onSubscriptionProlong($aPending, $aSubscription, $aResult = [])
     {
-        $this->alert('prolong_subscription', 0, $aPending['client_id'], array(
+        $this->alert('prolong_subscription', 0, $aPending['client_id'], [
             'pending' => $aPending,
             'subscription' => $aSubscription
-        ));
+        ]);
     }
 
-    public function onSubscriptionOverdue($aPending, $aSubscription, $aResult = array())
+    public function onSubscriptionOverdue($aPending, $aSubscription, $aResult = [])
     {
-        $this->alert('overdue_subscription', 0, $aPending['client_id'], array(
+        $this->alert('overdue_subscription', 0, $aPending['client_id'], [
             'pending' => $aPending,
             'subscription' => $aSubscription
-        ));
+        ]);
     }
 
-    public function onSubscriptionCancel($aPending, $aSubscription, $aResult = array())
+    public function onSubscriptionCancel($aPending, $aSubscription, $aResult = [])
     {
-        $this->alert('cancel_subscription', 0, $aPending['client_id'], array(
+        $this->alert('cancel_subscription', 0, $aPending['client_id'], [
             'pending' => $aPending,
             'subscription' => $aSubscription
-        ));
+        ]);
     }
 
     public function setSiteSubmenu($sSubmenu, $sSelModule, $sSelName)

+ 178 - 16
modules/boonex/payment/classes/BxPaymentProviderPayPalApi.php

@@ -33,13 +33,20 @@
  * 3.3 WebHooks
  * https://developer.paypal.com/docs/api-basics/notifications/webhooks/rest/
  * 
+ * 4. Authorize Intent and Capture Authorized: 
+ * https://github.com/paypal/Checkout-PHP-SDK/tree/develop/samples
+ * 
  */
 
+use PayPalHttp\HttpException;
 use PayPalCheckoutSdk\Core\PayPalHttpClient;
 use PayPalCheckoutSdk\Core\SandboxEnvironment;
 use PayPalCheckoutSdk\Core\ProductionEnvironment;
 use PayPalCheckoutSdk\Orders\OrdersCreateRequest;
+use PayPalCheckoutSdk\Orders\OrdersGetRequest;
 use PayPalCheckoutSdk\Orders\OrdersCaptureRequest;
+use PayPalCheckoutSdk\Orders\OrdersAuthorizeRequest;
+use PayPalCheckoutSdk\Payments\AuthorizationsCaptureRequest;
 
 define('PP_API_MODE_LIVE', 1);
 define('PP_API_MODE_TEST', 2);
@@ -75,6 +82,67 @@ class BxPaymentProviderPayPalApi extends BxBaseModPaymentProvider implements iBx
         $this->_sEndpoint = 'https://api-m' . ($this->_iMode == PP_API_MODE_TEST ? '.sandbox' : '') . '.paypal.com/';
     }
 
+    public function authorizeCheckout($iPendingId, $aCartInfo)
+    {
+        if(empty($aCartInfo['items']) || !is_array($aCartInfo['items']))
+            return $this->_sLangsPrefix . 'err_empty_items';
+
+        $aPending = $this->_oModule->_oDb->getOrderPending(array('type' => 'id', 'id' => $iPendingId));
+        if(!empty($aPending['order']) || !empty($aPending['error_code']) || !empty($aPending['error_msg']) || (int)$aPending['processed'] != 0)
+            return $this->_sLangsPrefix . 'err_already_processed';
+
+        switch($aPending['type']) {
+            case BX_PAYMENT_TYPE_SINGLE:
+                $mixedResult = $this->_createOrder($iPendingId, $aCartInfo, ['intent' => 'authorize']);
+                if($mixedResult === false)
+                    return $this->_sLangsPrefix . 'err_cannot_perform';
+
+                $this->_setSessionOrder($mixedResult['order']);
+                return [
+                    'code' => BX_PAYMENT_RESULT_SUCCESS,
+                    'redirect' => $mixedResult['redirect']
+                ];
+
+            case BX_PAYMENT_TYPE_RECURRING:
+                return $this->_sLangsPrefix . 'err_not_supported';
+        }
+    }
+
+    public function captureAuthorizedCheckout($sOrderAuth, $mixedPending, $aInfo)
+    {
+        $aPending = is_array($mixedPending) ? $mixedPending : $this->_oModule->_oDb->getOrderPending(['type' => 'id', 'id' => (int)$mixedPending]);
+        if(empty($aPending) || !is_array($aPending))
+            return ['code' => 2, 'message' => $this->_sLangsPrefix . 'err_empty_order'];
+
+        $mixedResult = false;
+        switch($aPending['type']) {
+            case BX_PAYMENT_TYPE_SINGLE:
+                $mixedResult = $this->_captureAuthorization($sOrderAuth);
+                if($mixedResult === false)
+                    return ['code' => 3, 'message' => $this->_sLangsPrefix . 'err_cannot_perform'];
+                break;
+
+            case BX_PAYMENT_TYPE_RECURRING:
+                return ['code' => 1, 'message' => $this->_sLangsPrefix . 'err_not_supported'];
+        }
+
+        $aResult = [
+            'code' => BX_PAYMENT_RESULT_SUCCESS,
+            'message' => $this->_sLangsPrefix . 'pp_api_msg_captured',
+            'pending_id' => $aPending['id'],
+            'paid' => true
+        ];
+
+        //--- Update pending transaction
+        $this->_oModule->_oDb->updateOrderPending($aResult['pending_id'], [
+            'order' => $mixedResult['order'],
+            'error_code' => $aResult['code'],
+            'error_msg' => _t($aResult['message']),
+        ]);
+
+        return $aResult;
+    }
+
     public function initializeCheckout($iPendingId, $aCartInfo)
     {
         if(empty($aCartInfo['items']) || !is_array($aCartInfo['items']))
@@ -149,27 +217,50 @@ class BxPaymentProviderPayPalApi extends BxBaseModPaymentProvider implements iBx
         if(empty($sOrder))
             $sOrder = $this->_getSessionOrder();
         if(empty($sOrder))
-            return array('code' => 1, 'message' => $this->_sLangsPrefix . 'err_empty_order');
+            return['code' => 1, 'message' => $this->_sLangsPrefix . 'err_empty_order'];
 
-        $mixedResult = $this->_captureOrder($sOrder);
-        if($mixedResult === false)
-            return array('code' => 2, 'message' => $this->_sLangsPrefix . 'pp_api_err_cannot_capture');
+        $aOrder = $this->_getOrder($sOrder);
+        if(empty($aOrder) || !is_array($aOrder))
+            return ['code' => 1, 'message' => $this->_sLangsPrefix . 'err_empty_order'];
 
-        $aResult = array(
+        $sMessage = '';
+        $bAuthorized = $bPaid = false;
+        switch($aOrder['intent']) {
+            case 'authorize':
+                $mixedResult = $this->_authorizeOrder($sOrder);
+                if($mixedResult === false)
+                    return ['code' => 2, 'message' => $this->_sLangsPrefix . 'pp_api_err_cannot_authorize'];
+
+                $sMessage = $this->_sLangsPrefix . 'pp_api_msg_authorized';
+                $bAuthorized = true;
+                break;
+
+            case 'capture':
+                $mixedResult = $this->_captureOrder($sOrder);
+                if($mixedResult === false)
+                    return ['code' => 2, 'message' => $this->_sLangsPrefix . 'pp_api_err_cannot_capture'];
+
+                $sMessage = $this->_sLangsPrefix . 'pp_api_msg_captured';
+                $bPaid = true;
+                break;
+        }
+
+        $aResult = [
             'code' => BX_PAYMENT_RESULT_SUCCESS, 
-            'message' => $this->_sLangsPrefix . 'pp_api_msg_captured',
+            'message' => $sMessage,
             'pending_id' => $mixedResult['pending_id'],
             'client_name' => _t($this->_sLangsPrefix . 'txt_buyer_name_mask', $mixedResult['client_first_name'], $mixedResult['client_last_name']),
             'client_email' => $mixedResult['client_email'],
-            'paid' => true
-        );
+            'authorized' => $bAuthorized,
+            'paid' => $bPaid
+        ];
 
         //--- Update pending transaction
-        $this->_oModule->_oDb->updateOrderPending($aResult['pending_id'], array(
+        $this->_oModule->_oDb->updateOrderPending($aResult['pending_id'], [
             'order' => $mixedResult['order'],
             'error_code' => $aResult['code'],
-            'error_msg' => _t($aResult['message'])
-        ));
+            'error_msg' => _t($aResult['message']),
+        ]);
 
         return $aResult;
     }
@@ -503,8 +594,10 @@ class BxPaymentProviderPayPalApi extends BxBaseModPaymentProvider implements iBx
         return new PayPalHttpClient($oEnvironment);
     }
 
-    protected function _createOrder($iPendingId, $aCartInfo, $sReturnType = 'approve')
+    protected function _createOrder($iPendingId, $aCartInfo, $aParams = [])
     {
+        $sReturnType = isset($aParams['return_type']) ? $aParams['return_type'] : 'approve';
+                
         $aItems = array();
         foreach($aCartInfo['items'] as $aItem)
             $aItems[] = array(
@@ -519,7 +612,7 @@ class BxPaymentProviderPayPalApi extends BxBaseModPaymentProvider implements iBx
         $oRequest = new OrdersCreateRequest();
         $oRequest->prefer('return=representation');
         $oRequest->body = array(
-            'intent' => 'CAPTURE',
+            'intent' => isset($aParams['intent']) ? strtoupper($aParams['intent']) : 'CAPTURE',
             'purchase_units' => array(
                 array(
                     'reference_id' => $iPendingId,
@@ -564,6 +657,49 @@ class BxPaymentProviderPayPalApi extends BxBaseModPaymentProvider implements iBx
         }
     }
 
+    protected function _getOrder($sOrderId)
+    {
+        $oRequest = new OrdersGetRequest($sOrderId);
+
+        try {
+            $oResponse = $this->_getClient()->execute($oRequest);
+            if($oResponse->statusCode != 200 || strcmp(strtolower($oResponse->result->status), 'approved') != 0)
+                return false;
+
+            $oPurchase = current($oResponse->result->purchase_units);
+            return array_merge(array(
+                'id' => $oResponse->result->id,
+                'intent' => strtolower($oResponse->result->intent),
+                'pending_id' => (int)$oPurchase->reference_id,
+            ), $this->_getClientInfo($oResponse->result->payer));
+        }
+        catch (HttpException $oException) {
+            return $this->_processException('Get Order Error: ', $oException);
+        }
+        
+    }
+
+    protected function _authorizeOrder($sOrderId)
+    {
+        $oRequest = new OrdersAuthorizeRequest($sOrderId);
+
+        try {
+            $oResponse = $this->_getClient()->execute($oRequest);
+            if($oResponse->statusCode != 201 || strcmp(strtolower($oResponse->result->status), 'completed') != 0)
+                return false;
+
+            $oPurchase = current($oResponse->result->purchase_units);
+            $oAuthorization = current($oPurchase->payments->authorizations);
+            return array_merge(array(
+                'pending_id' => (int)$oPurchase->reference_id,
+                'order' => $oAuthorization->id,
+            ), $this->_getClientInfo($oResponse->result->payer));
+        }
+        catch (HttpException $oException) {
+            return $this->_processException('Authorize Order Error: ', $oException);
+        }
+    }
+
     protected function _captureOrder($sOrderId)
     {
         $oRequest = new OrdersCaptureRequest($sOrderId);
@@ -584,17 +720,43 @@ class BxPaymentProviderPayPalApi extends BxBaseModPaymentProvider implements iBx
             return $this->_processException('Capture Order Error: ', $oException);
         }
     }
+    
+    protected function _captureAuthorization($sAuthorizationId)
+    {
+        $oRequest = new AuthorizationsCaptureRequest($sAuthorizationId);
+
+        try {
+            $oResponse = $this->_getClient()->execute($oRequest);
+            if($oResponse->statusCode != 201 || strcmp(strtolower($oResponse->result->status), 'completed') != 0)
+                return false;
+
+            return [
+                'order' => $oResponse->result->id
+            ];
+        }
+        catch (HttpException $oException) {
+            return $this->_processException('Capture Authorization Error: ', $oException);
+        }
+    }
 
     protected function _processException($sMessage, &$oException)
     {
-        $aError = $oException->getJsonBody();
+        $sDescription = '';
+        $aError = [];
+
+        $sMethod = 'getJsonBody';
+        if(method_exists($oException, $sMethod)) {
+            $aError = $oException->getJsonBody();
+            if(isset($aError['error']['message']))
+                $sDescription = $aError['error']['message'];
+        }
 
-        $sDescription = $aError['error']['message'];
         if(empty($sDescription))
             $sDescription = $oException->getMessage();
 
         $this->log($sMessage . $sDescription);
-        $this->log($aError);
+        if(!empty($aError))
+            $this->log($aError);
 
         return false;
     }

+ 237 - 22
modules/boonex/payment/classes/BxPaymentProviderStripeV3.php

@@ -15,6 +15,8 @@ class BxPaymentProviderStripeV3 extends BxPaymentProviderStripeBasic implements
 {
     protected $_oStripe;
 
+    protected $_iAmountPrecision;
+
     function __construct($aConfig)
     {
         parent::__construct($aConfig);
@@ -24,6 +26,8 @@ class BxPaymentProviderStripeV3 extends BxPaymentProviderStripeBasic implements
         );
 
         $this->_oStripe = null;
+
+        $this->_iAmountPrecision = 2;
     }
 
     public function actionGetSessionRecurring()
@@ -79,6 +83,85 @@ class BxPaymentProviderStripeV3 extends BxPaymentProviderStripeBasic implements
         ));
     }
 
+    public function authorizeCheckout($iPendingId, $aCartInfo, $sRedirect = '')
+    {
+        $sSessionId = bx_process_input(bx_get('session_id'));
+
+    	if(empty($aCartInfo['items']) || !is_array($aCartInfo['items']))
+            return $this->_sLangsPrefix . 'err_empty_items';
+
+        $aClient = $this->_oModule->getProfileInfo();
+        $aVendor = $this->_oModule->getProfileInfo($aCartInfo['vendor_id']);
+
+        $aPending = $this->_oModule->_oDb->getOrderPending(array('type' => 'id', 'id' => $iPendingId));
+        if(!empty($aPending['order']) || !empty($aPending['error_code']) || !empty($aPending['error_msg']) || (int)$aPending['processed'] != 0)
+            return $this->_sLangsPrefix . 'err_already_processed';
+
+        switch($aPending['type']) {
+            case BX_PAYMENT_TYPE_SINGLE:
+                $mixedResult = $this->_getSession(BX_PAYMENT_TYPE_SINGLE, $sSessionId);
+                if($mixedResult === false)
+                    return $this->_sLangsPrefix . 'err_cannot_perform';
+
+                return [
+                    'code' => BX_PAYMENT_RESULT_SUCCESS,
+                    'redirect' => $this->getReturnDataUrl($aVendor['id'], array(
+                        'mode' => $mixedResult['mode'],
+                        'order_id' => $mixedResult['order_id'],
+                        'customer_id' => $mixedResult['customer_id'], 
+                        'pending_id' => $aPending['id'],
+                        'redirect' => $sRedirect
+                    ))
+                ];
+
+            case BX_PAYMENT_TYPE_RECURRING:
+                return $this->_sLangsPrefix . 'err_not_supported';
+        }
+    }
+
+    public function captureAuthorizedCheckout($sOrderAuth, $mixedPending, $aInfo)
+    {
+        $aPending = is_array($mixedPending) ? $mixedPending : $this->_oModule->_oDb->getOrderPending(['type' => 'id', 'id' => (int)$mixedPending]);
+        if(empty($aPending) || !is_array($aPending))
+            return ['code' => 2, 'message' => $this->_sLangsPrefix . 'err_empty_order'];
+
+        $bPaid = false;
+        $sOrder = '';
+        switch($aPending['type']) {
+            case BX_PAYMENT_TYPE_SINGLE:
+                $oPaymentIntent = $this->_createPaymentIntent($sOrderAuth, $aInfo['items_price'], $aInfo['vendor_currency_code']);
+                if($oPaymentIntent === false)
+                    return ['code' => 3, 'message' => $this->_sLangsPrefix . 'err_cannot_perform'];
+
+                $aPaymentIntent = $oPaymentIntent->jsonSerialize();
+                if(empty($aPaymentIntent) || !is_array($aPaymentIntent))
+                    return ['code' => 3, 'message' => $this->_sLangsPrefix . 'err_cannot_perform'];
+
+                $bPaid = $aPaymentIntent['status'] == 'succeeded';
+                $sOrder = $aPaymentIntent['id'];
+                break;
+
+            case BX_PAYMENT_TYPE_RECURRING:
+                return ['code' => 1, 'message' => $this->_sLangsPrefix . 'err_not_supported'];
+        }
+
+        $aResult = [
+            'code' => BX_PAYMENT_RESULT_SUCCESS,
+            'message' => $this->_sLangsPrefix . 'strp_msg_charged',
+            'pending_id' => $aPending['id'],
+            'paid' => $bPaid
+        ];
+
+        //--- Update pending transaction
+        $this->_oModule->_oDb->updateOrderPending($aResult['pending_id'], [
+            'order' => $sOrder,
+            'error_code' => $aResult['code'],
+            'error_msg' => _t($aResult['message']),
+        ]);
+
+        return $aResult;
+    }
+
     public function initializeCheckout($iPendingId, $aCartInfo, $sRedirect = '')
     {
         $sSessionId = bx_process_input(bx_get('session_id'));
@@ -100,6 +183,7 @@ class BxPaymentProviderStripeV3 extends BxPaymentProviderStripeBasic implements
                     return $this->_sLangsPrefix . 'err_cannot_perform';
 
                 header("Location: " . $this->getReturnDataUrl($aVendor['id'], array(
+                    'mode' => $mixedResult['mode'],
                     'order_id' => $mixedResult['order_id'],
                     'customer_id' => $mixedResult['customer_id'], 
                     'pending_id' => $aPending['id'],
@@ -113,6 +197,7 @@ class BxPaymentProviderStripeV3 extends BxPaymentProviderStripeBasic implements
                     return $this->_sLangsPrefix . 'err_cannot_perform';
 
                 header("Location: " . $this->getReturnDataUrl($aVendor['id'], array(
+                    'mode' => $mixedResult['mode'],
                     'order_id' => $mixedResult['order_id'],
                     'customer_id' => $mixedResult['customer_id'],
                     'pending_id' => $aPending['id'],
@@ -124,6 +209,7 @@ class BxPaymentProviderStripeV3 extends BxPaymentProviderStripeBasic implements
 
     public function finalizeCheckout(&$aData)
     {
+        $sMode = bx_process_input($aData['mode']);
     	$sOrderId = bx_process_input($aData['order_id']);
     	$sCustomerId = bx_process_input($aData['customer_id']);
         $iPendingId = bx_process_input($aData['pending_id'], BX_DATA_INT);
@@ -144,6 +230,7 @@ class BxPaymentProviderStripeV3 extends BxPaymentProviderStripeBasic implements
             'subscription_id' => '',
             'client_name' => '',
             'client_email' => '',
+            'authorized' => false,
             'paid' => false,
             'trial' => false,
             'redirect' => $sRedirect
@@ -152,19 +239,48 @@ class BxPaymentProviderStripeV3 extends BxPaymentProviderStripeBasic implements
         switch($aPending['type']) {
             case BX_PAYMENT_TYPE_SINGLE:
                 $oCustomer = $this->_retrieveCustomer(BX_PAYMENT_TYPE_SINGLE, $sCustomerId);
-                $oPaymentIntent = $this->_retrievePaymentIntent($sOrderId);
-                if($oCustomer === false || $oPaymentIntent === false)
+                if($oCustomer === false)
                     return array('code' => 4, 'message' => $this->_sLangsPrefix . 'err_cannot_perform');
 
                 $aCustomer = $oCustomer->jsonSerialize();
-                $aPaymentIntent = $oPaymentIntent->jsonSerialize();
-                if(empty($aCustomer) || !is_array($aCustomer) || empty($aPaymentIntent) || !is_array($aPaymentIntent))
+                if(empty($aCustomer) || !is_array($aCustomer))
                     return array('code' => 4, 'message' => $this->_sLangsPrefix . 'err_cannot_perform');
 
+                $sMessage = '';
+                $bPaid = $bAuthorized = false;
+                switch($sMode) {
+                    case 'setup':
+                        $oSetupIntent = $this->_retrieveSetupIntent($sOrderId);
+                        if($oSetupIntent === false)
+                            return array('code' => 4, 'message' => $this->_sLangsPrefix . 'err_cannot_perform');
+                        
+                        $aSetupIntent = $oSetupIntent->jsonSerialize();
+                        if(empty($aSetupIntent) || !is_array($aSetupIntent))
+                            return array('code' => 4, 'message' => $this->_sLangsPrefix . 'err_cannot_perform');
+                                
+                        $sMessage = $this->_sLangsPrefix . 'strp_msg_authorized';
+                        $bAuthorized = $aSetupIntent['status'] == 'succeeded';
+                        break;
+                    
+                    case 'payment':
+                        $oPaymentIntent = $this->_retrievePaymentIntent($sOrderId);
+                        if($oPaymentIntent === false)
+                            return array('code' => 4, 'message' => $this->_sLangsPrefix . 'err_cannot_perform');
+
+                        $aPaymentIntent = $oPaymentIntent->jsonSerialize();
+                        if(empty($aPaymentIntent) || !is_array($aPaymentIntent))
+                            return array('code' => 4, 'message' => $this->_sLangsPrefix . 'err_cannot_perform');
+                        
+                        $sMessage = $this->_sLangsPrefix . 'strp_msg_charged';
+                        $bPaid = $aPaymentIntent['status'] == 'succeeded';
+                        break;
+                }
+
                 $aResult = array_merge($aResult, array(
-                    'message' => $this->_sLangsPrefix . 'strp_msg_charged',
+                    'message' => $sMessage,
                     'client_email' => $aCustomer['email'],
-                    'paid' => $aPaymentIntent['status'] == 'succeeded'
+                    'authorized' => $bAuthorized,
+                    'paid' => $bPaid,
                 ));
                 break;
 
@@ -234,6 +350,28 @@ class BxPaymentProviderStripeV3 extends BxPaymentProviderStripeBasic implements
         );
     }
 
+    public function createSessionAuthorize($sType, $iClientId, $iSellerId, $sItems, $aSessionParams = [])
+    {
+        $aClient = $this->_oModule->getProfileInfo($iClientId);
+
+        $oCart = $this->_oModule->getObjectCart();
+        $aCartInfo = $oCart->getInfo($sType, $aClient['id'], $iSellerId, $sItems);
+        if(empty($aCartInfo) || !is_array($aCartInfo))
+            return false;
+
+        $aSessionParams = array_merge([
+            'cancel_url' => $oCart->serviceGetCartUrl($iSellerId),
+            'success_url' => bx_append_url_params($this->_oModule->_oConfig->getUrl('URL_AUTHORIZE') . $sType . '/', [
+                'provider' => $this->_sName,
+                'seller_id' => $iSellerId,
+                'items' => $sItems,
+                'session_id' => '{CHECKOUT_SESSION_ID}'
+            ])
+        ], $aSessionParams);
+
+        return $this->_createSession('authorize', $aSessionParams, $aClient, $aCartInfo);
+    }
+
     public function createTax($sName, $fPercentage, $bInclusive = false)
     {
         $oTax = null;
@@ -349,14 +487,16 @@ class BxPaymentProviderStripeV3 extends BxPaymentProviderStripeBasic implements
      */
     protected function _createSession($sType, $aParams, &$aClient, &$aCartInfo)
     {
-        $iAmountPrecision = 2;
-        $fAmount = 100 * round((float)$aCartInfo['items_price'], $iAmountPrecision);
-
         $sMode = '';
         $aLineItems = array();
         $aMetaItems = array();
 
         switch($sType) {
+            case 'authorize':
+                $sMode = 'setup';
+                $aParams['customer_creation'] = 'always';
+                break;
+
             case BX_PAYMENT_TYPE_SINGLE:
                 $sMode = 'payment';
 
@@ -371,7 +511,7 @@ class BxPaymentProviderStripeV3 extends BxPaymentProviderStripeBasic implements
                         'price_data' => array(
                             'currency' => $aCartInfo['vendor_currency_code'],
                             'product_data' => $aProductData,
-                            'unit_amount' => 100 * round($this->_oModule->_oConfig->getPrice($sType, $aItem), $iAmountPrecision),
+                            'unit_amount' => 100 * round($this->_oModule->_oConfig->getPrice($sType, $aItem), $this->_iAmountPrecision),
                         ),
                         'quantity' => $aItem['quantity'],
                     );
@@ -395,21 +535,29 @@ class BxPaymentProviderStripeV3 extends BxPaymentProviderStripeBasic implements
         }
 
         $oSession = null;
-        $aSession = [
+        $aSession = array_merge([
             'payment_method_types' => ['card'],
             'customer_email' => !empty($aClient['email']) ? $aClient['email'] : '',
-            'line_items' => $aLineItems,
             'mode' => $sMode,
-            'success_url' => $aParams['success_url'],
-            'cancel_url' => $aParams['cancel_url'],
-            'metadata' => [
+            'success_url' => '',
+            'cancel_url' => '',
+        ], $aParams);
+
+        $bLineItems = !empty($aLineItems);
+        $aSession['line_items'] = $aLineItems;
+
+        $bMetaData = !empty($aMetaItems);
+        if($bMetaData) {
+            $fAmount = 100 * round((float)$aCartInfo['items_price'], $this->_iAmountPrecision);
+
+            $aSession['metadata'] = [
                 'vendor' => $aCartInfo['vendor_id'],
                 'client' => $aClient['id'],
                 'type' => $sType, 
                 'items' => $this->_oModule->_oConfig->descriptorsA2S($aMetaItems),
                 'verification' => $this->getVerificationCodeSession($aCartInfo['vendor_id'], $aClient['id'], $fAmount, $aCartInfo['vendor_currency_code'])
-            ]
-        ];
+            ];
+        }
 
         bx_alert($this->_oModule->_oConfig->getName(), $this->_sName . '_create_session', 0, false, array(
             'session_object' => &$oSession, 
@@ -428,7 +576,7 @@ class BxPaymentProviderStripeV3 extends BxPaymentProviderStripeBasic implements
         if(empty($aResult) || !is_array($aResult))
             return false;
 
-        if(!$this->checkVerificationCodeSession($aCartInfo['vendor_id'], $aClient['id'], $aResult))
+        if($bMetaData && !$this->checkVerificationCodeSession($aCartInfo['vendor_id'], $aClient['id'], $aResult))
             return false;
 
         return $aResult['id'];
@@ -444,16 +592,21 @@ class BxPaymentProviderStripeV3 extends BxPaymentProviderStripeBasic implements
         if(empty($aSession) || !is_array($aSession))
             return array();
 
+        $sMode = 'payment';
+        if(!empty($aSession['mode']))
+            $sMode = $aSession['mode'];
+        
         $aResult = array(
             'id' => $aSession['id'],
-            'order_id' => $aSession['payment_intent'],
+            'mode' => $sMode,
+            'order_id' => '',
             'customer_id' => $aSession['customer'],
             'status' => $aSession['payment_status']
         );
 
         switch($sType) {
             case BX_PAYMENT_TYPE_SINGLE:
-                $aResult['order_id'] = $aSession['payment_intent'];
+                $aResult['order_id'] = $aSession[($sMode == 'setup' ? 'setup' : 'payment') . '_intent'];
                 break;
 
             case BX_PAYMENT_TYPE_RECURRING:
@@ -467,7 +620,8 @@ class BxPaymentProviderStripeV3 extends BxPaymentProviderStripeBasic implements
     /*
      * Related Docs: https://stripe.com/docs/api/checkout/sessions/retrieve
      */
-    protected function _retrieveSession($sId) {
+    protected function _retrieveSession($sId)
+    {
         $oSession = null;
         bx_alert($this->_oModule->_oConfig->getName(), $this->_sName . '_retrieve_session', 0, false, array(
             'session_id' => &$sId,
@@ -485,10 +639,71 @@ class BxPaymentProviderStripeV3 extends BxPaymentProviderStripeBasic implements
         return $oSession;
     }
     
+    
+    /*
+     * Related Docs: https://stripe.com/docs/api/setup_intents/retrieve
+     */
+    protected function _retrieveSetupIntent($sId)
+    {
+        $oSetupIntent = null;
+        bx_alert($this->_oModule->_oConfig->getName(), $this->_sName . '_retrieve_setup_intent', 0, false, array(
+            'setup_intent_id' => &$sId,
+            'setup_intent_object' => &$oSetupIntent
+        ));
+
+        try {
+            if(empty($oSetupIntent))
+                $oSetupIntent = $this->_getStripe()->setupIntents->retrieve($sId);
+        }
+        catch (Exception $oException) {
+            return $this->_processException('Retrieve Setup Intent Error: ', $oException);
+        }
+
+        return $oSetupIntent;
+    }
+
+    /*
+     * Related Docs: https://stripe.com/docs/api/payment_intents/create
+     */
+    protected function _createPaymentIntent($sSetupIntentId, $fAmount, $sCurrency, $bConfirm = true)
+    {
+        $oPaymentIntent = null;
+        bx_alert($this->_oModule->_oConfig->getName(), $this->_sName . '_create_payment_intent', 0, false, array(
+            'setup_intent_id' => &$sSetupIntentId,
+            'payment_intent_object' => &$oPaymentIntent
+        ));
+
+        try {
+            if(empty($oPaymentIntent)) {
+                $oSetupIntent = $this->_retrieveSetupIntent($sSetupIntentId);
+                if($oSetupIntent === false)
+                    return false;
+
+                $aSetupIntent = $oSetupIntent->jsonSerialize();
+                if(empty($aSetupIntent) || !is_array($aSetupIntent))
+                    return false;
+
+                $oPaymentIntent = $this->_getStripe()->paymentIntents->create([
+                    'amount' => 100 * round($fAmount, $this->_iAmountPrecision),
+                    'currency' => strtolower($sCurrency),
+                    'customer' => $aSetupIntent['customer'],
+                    'payment_method' => $aSetupIntent['payment_method'],
+                    'confirm' => $bConfirm
+                ]);
+            }
+        }
+        catch (Exception $oException) {
+            return $this->_processException('Retrieve Payment Intent Error: ', $oException);
+        }
+
+        return $oPaymentIntent;
+    }
+
     /*
      * Related Docs: https://stripe.com/docs/api/payment_intents/retrieve
      */
-    protected function _retrievePaymentIntent($sId) {
+    protected function _retrievePaymentIntent($sId)
+    {
         $oPaymentIntent = null;
         bx_alert($this->_oModule->_oConfig->getName(), $this->_sName . '_retrieve_payment_intent', 0, false, array(
             'payment_intent_id' => &$sId,

+ 4 - 0
modules/boonex/payment/install/langs/en.xml

@@ -360,6 +360,7 @@
     <string name="_bx_payment_err_self_purchase"><![CDATA[Buying from your own store is not permitted.]]></string>
     <string name="_bx_payment_err_required_login"><![CDATA[You need to login.]]></string>
     <string name="_bx_payment_err_unknown_vendor"><![CDATA[Unknown vendor.]]></string>
+    <string name="_bx_payment_err_wrong_vendor"><![CDATA[Incorrect vendor.]]></string>
     <string name="_bx_payment_err_wrong_data"><![CDATA[Incorrect input data.]]></string>
     <string name="_bx_payment_err_not_found_pending"><![CDATA[Pending order was not found.]]></string>
     <string name="_bx_payment_err_cannot_perform"><![CDATA[Cannot perform the action. Please report.]]></string>
@@ -494,10 +495,12 @@
     <string name="_bx_payment_pp_api_test_secret_dsc"><![CDATA[A part of application's credentials.]]></string>
     <string name="_bx_payment_pp_api_return_data_url_cpt"><![CDATA[Data return URL]]></string>
     <string name="_bx_payment_pp_api_notify_url_cpt"><![CDATA[Webhooks URL]]></string>
+    <string name="_bx_payment_pp_api_err_cannot_authorize"><![CDATA[Can't authorize the approved order.]]></string>
     <string name="_bx_payment_pp_api_err_cannot_capture"><![CDATA[Can't finalize checkout.]]></string>
     <string name="_bx_payment_pp_api_err_no_item"><![CDATA[There is nothing to subscribe.]]></string>
     <string name="_bx_payment_pp_api_err_no_subscription"><![CDATA[The subscription data isn't available or incorrect.]]></string>
     <string name="_bx_payment_pp_api_err_subscription_not_confirmed"><![CDATA[Subscription wasn't confirmed.]]></string>
+    <string name="_bx_payment_pp_api_msg_authorized"><![CDATA[The order was successfully authorized.]]></string>
     <string name="_bx_payment_pp_api_msg_captured"><![CDATA[Payment was successfully processed.]]></string>
     <string name="_bx_payment_pp_api_msg_subscribed"><![CDATA[Thank you! Subscription created. Please, check your email for further instructions.]]></string>
     <string name="_bx_payment_txt_name_paypal_api"><![CDATA[PayPal]]></string>
@@ -571,6 +574,7 @@
     <string name="_bx_payment_strp_notify_url_cpt"><![CDATA[Webhooks URL]]></string>
     <string name="_bx_payment_strp_cancellation_email_cpt"><![CDATA[Cancellation Email]]></string>
     <string name="_bx_payment_strp_expiration_email_cpt"><![CDATA[Expiration Email]]></string>
+    <string name="_bx_payment_strp_msg_authorized"><![CDATA[The order was successfully authorized.]]></string>
     <string name="_bx_payment_strp_msg_charged"><![CDATA[Payment was successfully processed.]]></string>
     <string name="_bx_payment_strp_msg_subscribed"><![CDATA[Thank you! Subscription created. Please, check your email for further instructions.]]></string>
     <string name="_bx_payment_strp_msg_details_changed"><![CDATA[Subscription details were changed.]]></string>

+ 1 - 0
modules/boonex/payment/install/sql/install.sql

@@ -116,6 +116,7 @@ CREATE TABLE IF NOT EXISTS `bx_payment_transactions_pending` (
   `error_code` varchar(16) NOT NULL default '',
   `error_msg` varchar(255) NOT NULL default '',
   `date` int(11) NOT NULL default '0',
+  `authorized` tinyint(4) NOT NULL default '0',
   `processed` tinyint(4) NOT NULL default '0',
   PRIMARY KEY(`id`)
 );

+ 4 - 0
modules/boonex/russian/data/langs/bx_payment/ru.xml

@@ -362,6 +362,7 @@
     <string name="_bx_payment_err_self_purchase"><![CDATA[Покупка из собственного магазина не возможна.]]></string>
     <string name="_bx_payment_err_required_login"><![CDATA[Вам нужно войти.]]></string>
     <string name="_bx_payment_err_unknown_vendor"><![CDATA[Неизвестный продавец.]]></string>
+    <string name="_bx_payment_err_wrong_vendor"><![CDATA[Неверный продавец.]]></string>
     <string name="_bx_payment_err_wrong_data"><![CDATA[Неверные входные данные.]]></string>
     <string name="_bx_payment_err_not_found_pending"><![CDATA[Обрабатываемый заказ не найден.]]></string>
     <string name="_bx_payment_err_cannot_perform"><![CDATA[Действие не может быть выполнено. Свяжитесь с нами.]]></string>
@@ -496,10 +497,12 @@
     <string name="_bx_payment_pp_api_test_secret_dsc"><![CDATA[Часть учетных данных приложения.]]></string>
     <string name="_bx_payment_pp_api_return_data_url_cpt"><![CDATA[URL для возврата данных]]></string>
     <string name="_bx_payment_pp_api_notify_url_cpt"><![CDATA[URL для получения webhook-ов]]></string>
+    <string name="_bx_payment_pp_api_err_cannot_authorize"><![CDATA[Не удалось авторизовать одобренный заказ.]]></string>
     <string name="_bx_payment_pp_api_err_cannot_capture"><![CDATA[Не удалось завершить оплату.]]></string>
     <string name="_bx_payment_pp_api_err_no_item"><![CDATA[Объект для создания подписки отсутствует.]]></string>
     <string name="_bx_payment_pp_api_err_no_subscription"><![CDATA[Данные подписки недоступны или неверны.]]></string>
     <string name="_bx_payment_pp_api_err_subscription_not_confirmed"><![CDATA[Подписка не подтверждена.]]></string>
+    <string name="_bx_payment_pp_api_msg_authorized"><![CDATA[Заказ авторизован.]]></string>
     <string name="_bx_payment_pp_api_msg_captured"><![CDATA[Платеж принят.]]></string>
     <string name="_bx_payment_pp_api_msg_subscribed"><![CDATA[Спасибо! Подписка была создана. Пожалуйста, проверьте ваш емэйл для получения дальнейших инструкций.]]></string>
     <string name="_bx_payment_txt_name_paypal_api"><![CDATA[PayPal]]></string>
@@ -573,6 +576,7 @@
     <string name="_bx_payment_strp_notify_url_cpt"><![CDATA[URL для получения webhook-ов]]></string>
     <string name="_bx_payment_strp_cancellation_email_cpt"><![CDATA[Email для запросов отмены подписки]]></string>
     <string name="_bx_payment_strp_expiration_email_cpt"><![CDATA[Email для предупреждений о заканчивающихся подписках]]></string>
+    <string name="_bx_payment_strp_msg_authorized"><![CDATA[Заказ авторизован.]]></string>
     <string name="_bx_payment_strp_msg_charged"><![CDATA[Платеж успешно принят.]]></string>
     <string name="_bx_payment_strp_msg_subscribed"><![CDATA[Спасибо! Подписка была создана. Пожалуйста, проверьте ваш емэйл для получения дальнейших инструкций.]]></string>
     <string name="_bx_payment_strp_msg_details_changed"><![CDATA[Детали подписки были изменены.]]></string>