Skip to content

Instantly share code, notes, and snippets.

@evgv
Last active December 15, 2016 15:12
Show Gist options
  • Save evgv/7204133d28e40ee66296c5ee2d55ca7a to your computer and use it in GitHub Desktop.
Save evgv/7204133d28e40ee66296c5ee2d55ca7a to your computer and use it in GitHub Desktop.
Magento. Use quote currency for PayPal

Use store currency to charge PayPal Express instead base currency

Overwrite some classes for fix bug with charge PP order in base store currency for stores with multicurrency and charge orders in current store currency from.

How to

simple
  1. Copy file Checkout.php to ../app/code/local/Mage/Paypal/Model/Express/Checkout.php
  2. Copy file Cart.php to ../app/code/local/Mage/Paypal/Model/Cart.php
advanced

Overwrite some methods in this classes into your custom extension.

start() in Mage_Paypal_Model_Express_Checkout class
    /**
     * Reserve order ID for specified quote and start checkout on PayPal
     *
     * 
     * Changed by @author https://gist.github.com/evgv
     * 1. Set  $this->_quote->getGrandTotal() instead $this->_quote->getBaseGrandTotal()
     * 2. Set $this->_quote->getQuoteCurrencyCode() instead $this->_quote->getBaseCurrencyCode()
     * 
     * @param string $returnUrl
     * @param string $cancelUrl
     * @param bool|null $button
     * @return mixed
     */
    public function start($returnUrl, $cancelUrl, $button = null)
    {
        $this->_quote->collectTotals();

        if (!$this->_quote->getGrandTotal() && !$this->_quote->hasNominalItems()) {
            Mage::throwException(Mage::helper('paypal')->__('PayPal does not support processing orders with zero amount. To complete your purchase, proceed to the standard checkout process.'));
        }

        $this->_quote->reserveOrderId()->save();
        // prepare API
        $this->_getApi();
        $solutionType = $this->_config->getMerchantCountry() == 'DE'
            ? Mage_Paypal_Model_Config::EC_SOLUTION_TYPE_MARK : $this->_config->solutionType;
        
        $this->_api->setAmount($this->_quote->getGrandTotal())
                   ->setCurrencyCode($this->_quote->getQuoteCurrencyCode())
                   ->setInvNum($this->_quote->getReservedOrderId())
                   ->setReturnUrl($returnUrl)
                   ->setCancelUrl($cancelUrl)
                   ->setSolutionType($solutionType)
                   ->setPaymentAction($this->_config->paymentAction);
        
        if ($this->_giropayUrls) {
            list($successUrl, $cancelUrl, $pendingUrl) = $this->_giropayUrls;
            $this->_api->addData(array(
                'giropay_cancel_url' => $cancelUrl,
                'giropay_success_url' => $successUrl,
                'giropay_bank_txn_pending_url' => $pendingUrl,
            ));
        }

        if ($this->_isBml) {
            $this->_api->setFundingSource('BML');
        }

        $this->_setBillingAgreementRequest();

        if ($this->_config->requireBillingAddress == Mage_Paypal_Model_Config::REQUIRE_BILLING_ADDRESS_ALL) {
            $this->_api->setRequireBillingAddress(1);
        }

        // supress or export shipping address
        if ($this->_quote->getIsVirtual()) {
            if ($this->_config->requireBillingAddress == Mage_Paypal_Model_Config::REQUIRE_BILLING_ADDRESS_VIRTUAL) {
                $this->_api->setRequireBillingAddress(1);
            }
            $this->_api->setSuppressShipping(true);
        } else {
            $address = $this->_quote->getShippingAddress();
            $isOverriden = 0;
            if (true === $address->validate()) {
                $isOverriden = 1;
                $this->_api->setAddress($address);
            }
            $this->_quote->getPayment()->setAdditionalInformation(
                self::PAYMENT_INFO_TRANSPORT_SHIPPING_OVERRIDEN, $isOverriden
            );
            $this->_quote->getPayment()->save();
        }

        // add line items
        $paypalCart = Mage::getModel('paypal/cart', array($this->_quote));
        $this->_api->setPaypalCart($paypalCart)
            ->setIsLineItemsEnabled($this->_config->lineItemsEnabled)
        ;

        // add shipping options if needed and line items are available
        if ($this->_config->lineItemsEnabled && $this->_config->transferShippingOptions && $paypalCart->getItems()) {
            if (!$this->_quote->getIsVirtual() && !$this->_quote->hasNominalItems()) {
                if ($options = $this->_prepareShippingOptions($address, true)) {
                    $this->_api->setShippingOptionsCallbackUrl(
                        Mage::getUrl('*/*/shippingOptionsCallback', array('quote_id' => $this->_quote->getId()))
                    )->setShippingOptions($options);
                }
            }
        }

        // add recurring payment profiles information
        if ($profiles = $this->_quote->prepareRecurringPaymentProfiles()) {
            foreach ($profiles as $profile) {
                $profile->setMethodCode(Mage_Paypal_Model_Config::METHOD_WPP_EXPRESS);
                if (!$profile->isValid()) {
                    Mage::throwException($profile->getValidationErrors(true, true));
                }
            }
            $this->_api->addRecurringPaymentProfiles($profiles);
        }

        $this->_config->exportExpressCheckoutStyleSettings($this->_api);

        // call API and redirect with token
        $this->_api->callSetExpressCheckout();
        $token = $this->_api->getToken();
        $this->_redirectUrl = $button ? $this->_config->getExpressCheckoutStartUrl($token)
            : $this->_config->getPayPalBasicStartUrl($token);

        $this->_quote->getPayment()->unsAdditionalInformation(self::PAYMENT_INFO_TRANSPORT_BILLING_AGREEMENT);

        // Set flag that we came from Express Checkout button
        if (!empty($button)) {
            $this->_quote->getPayment()->setAdditionalInformation(self::PAYMENT_INFO_BUTTON, 1);
        } elseif ($this->_quote->getPayment()->hasAdditionalInformation(self::PAYMENT_INFO_BUTTON)) {
            $this->_quote->getPayment()->unsAdditionalInformation(self::PAYMENT_INFO_BUTTON);
        }

        $this->_quote->getPayment()->save();
        return $token;
    }
_render in Mage_Paypal_Model_Cart class
/**
 * (re)Render all items and totals
 * 
 * Changed by @author https://gist.github.com/evgv
 * 1. $this->_salesEntity->getBaseSubtotal() to $this->_salesEntity->getSubtotal()
 * 2. $this->_salesEntity->getBaseTaxAmount() to $this->_salesEntity->getTaxAmount()
 * 3. $this->_salesEntity->getBaseShippingAmount() to $this->_salesEntity->getShippingAmount()
 * 4. $this->_salesEntity->getBaseDiscountAmount() to $this->_salesEntity->getDiscountAmount()
 */
protected function _render()
{
    if (!$this->_shouldRender) {
        return;
    }

    // regular items from the sales entity
    $this->_items = array();
    foreach ($this->_salesEntity->getAllItems() as $item) {
        if (!$item->getParentItem()) {
            $this->_addRegularItem($item);
        }
    }
    end($this->_items);
    $lastRegularItemKey = key($this->_items);

    // regular totals
    $shippingDescription = '';
    if ($this->_salesEntity instanceof Mage_Sales_Model_Order) {
        $shippingDescription = $this->_salesEntity->getShippingDescription();
        $this->_totals = array(
            self::TOTAL_SUBTOTAL => $this->_salesEntity->getSubtotal(),
            self::TOTAL_TAX      => $this->_salesEntity->getTaxAmount(),
            self::TOTAL_SHIPPING => $this->_salesEntity->getShippingAmount(),
            self::TOTAL_DISCOUNT => abs($this->_salesEntity->getDiscountAmount()),
        );
        $this->_applyHiddenTaxWorkaround($this->_salesEntity);
    } else {
        $address = $this->_salesEntity->getIsVirtual() ?
            $this->_salesEntity->getBillingAddress() : $this->_salesEntity->getShippingAddress();
        $shippingDescription = $address->getShippingDescription();
        $this->_totals = array (
            self::TOTAL_SUBTOTAL => $this->_salesEntity->getSubtotal(),
            self::TOTAL_TAX      => $address->getTaxAmount(),
            self::TOTAL_SHIPPING => $address->getShippingAmount(),
            self::TOTAL_DISCOUNT => abs($address->getDiscountAmount()),
        );
        $this->_applyHiddenTaxWorkaround($address);
    }

    $originalDiscount = $this->_totals[self::TOTAL_DISCOUNT];

    // arbitrary items, total modifications
    Mage::dispatchEvent('paypal_prepare_line_items', array('paypal_cart' => $this));

    // distinguish original discount among the others
    if ($originalDiscount > 0.0001 && isset($this->_totalLineItemDescriptions[self::TOTAL_DISCOUNT])) {
        $this->_totalLineItemDescriptions[self::TOTAL_DISCOUNT][] = Mage::helper('sales')->__('Discount (%s)', Mage::app()->getStore()->convertPrice($originalDiscount, true, false));
    }

    // discount, shipping as items
    if ($this->_isDiscountAsItem && $this->_totals[self::TOTAL_DISCOUNT]) {
        $this->addItem(Mage::helper('paypal')->__('Discount'), 1, -1.00 * $this->_totals[self::TOTAL_DISCOUNT],
            $this->_renderTotalLineItemDescriptions(self::TOTAL_DISCOUNT)
        );
    }
    $shippingItemId = $this->_renderTotalLineItemDescriptions(self::TOTAL_SHIPPING, $shippingDescription);
    if ($this->_isShippingAsItem && (float)$this->_totals[self::TOTAL_SHIPPING]) {
        $this->addItem(Mage::helper('paypal')->__('Shipping'), 1, (float)$this->_totals[self::TOTAL_SHIPPING],
            $shippingItemId
        );
    }

    // compound non-regular items into subtotal
    foreach ($this->_items as $key => $item) {
        if ($key > $lastRegularItemKey && $item->getAmount() != 0) {
            $this->_totals[self::TOTAL_SUBTOTAL] += $item->getAmount();
        }
    }

    $this->_validate();
    // if cart items are invalid, prepare cart for transfer without line items
    if (!$this->_areItemsValid) {
        $this->removeItem($shippingItemId);
    }

    $this->_shouldRender = false;
}
_validate in Mage_Paypal_Model_Cart class
/**
 * Check the line items and totals according to PayPal business logic limitations
 * 
 * Changed by @author https://gist.github.com/evgv
 * 1. $this->_salesEntity->getBaseGrandTotal() to $this->_salesEntity->getGrandTotal(); 
 * 
 */
protected function _validate()
{
    $this->_areItemsValid = true;
    $this->_areTotalsValid = false;

    $referenceAmount = $this->_salesEntity->getGrandTotal();

    $itemsSubtotal = 0;
    foreach ($this->_items as $i) {
        $itemsSubtotal = $itemsSubtotal + $i['qty'] * $i['amount'];
    }
    $sum = $itemsSubtotal + $this->_totals[self::TOTAL_TAX];
    if (!$this->_isShippingAsItem) {
        $sum += $this->_totals[self::TOTAL_SHIPPING];
    }
    if (!$this->_isDiscountAsItem) {
        $sum -= $this->_totals[self::TOTAL_DISCOUNT];
    }
    /**
     * numbers are intentionally converted to strings because of possible comparison error
     * see http://php.net/float
     */
    // match sum of all the items and totals to the reference amount
    if (sprintf('%.4F', $sum) != sprintf('%.4F', $referenceAmount)) {
        $adjustment = $sum - $referenceAmount;
        $this->_totals[self::TOTAL_SUBTOTAL] = $this->_totals[self::TOTAL_SUBTOTAL] - $adjustment;
    }

    // PayPal requires to have discount less than items subtotal
    if (!$this->_isDiscountAsItem) {
        $this->_areTotalsValid = round($this->_totals[self::TOTAL_DISCOUNT], 4) < round($itemsSubtotal, 4);
    } else {
        $this->_areTotalsValid = $itemsSubtotal > 0.00001;
    }

    $this->_areItemsValid = $this->_areItemsValid && $this->_areTotalsValid;
}
_addRegularItem in Mage_Paypal_Model_Cart class
/**
 * Add a usual line item with amount and qty
 * 
 * Changed by @author https://gist.github.com/evgv
 * 1. $salesItem->getBasePrice() to $salesItem->getPrice()
 * 2. $salesItem->getBaseCalculationPrice() to $salesItem->getBaseCalculationPriceOriginal()
 * 3. $salesItem->getBaseRowTotal() to $salesItem->getRowTotal()
 *
 * @param Varien_Object $salesItem
 * @return Varien_Object
 */
protected function _addRegularItem(Varien_Object $salesItem)
{
    if ($this->_salesEntity instanceof Mage_Sales_Model_Order) {
        $qty = (int) $salesItem->getQtyOrdered();
        $amount = (float) $salesItem->getPrice();
        // TODO: nominal item for order
    } else {
        $qty = (int) $salesItem->getTotalQty();
        $amount = $salesItem->isNominal() ? 0 : (float) $salesItem->getBaseCalculationPriceOriginal();
    }
    // workaround in case if item subtotal precision is not compatible with PayPal (.2)
    $subAggregatedLabel = '';
    if ($amount - round($amount, 2)) {
        $amount = $amount * $qty;
        $subAggregatedLabel = ' x' . $qty;
        $qty = 1;
    }

    // aggregate item price if item qty * price does not match row total
    if (($amount * $qty) != $salesItem->getRowTotal()) {
        $amount = (float) $salesItem->getRowTotal();
        $subAggregatedLabel = ' x' . $qty;
        $qty = 1;
    }

    return $this->addItem($salesItem->getName() . $subAggregatedLabel, $qty, $amount, $salesItem->getSku());
}
_applyHiddenTaxWorkaround in Mage_Paypal_Model_Cart class
/**
 * Add "hidden" discount and shipping tax
 *
 * Go ahead, try to understand ]:->
 *
 * Tax settings for getting "discount tax":
 * - Catalog Prices = Including Tax
 * - Apply Customer Tax = After Discount
 * - Apply Discount on Prices = Including Tax
 *
 * Test case for getting "hidden shipping tax":
 * - Make sure shipping is taxable (set shipping tax class)
 * - Catalog Prices = Including Tax
 * - Shipping Prices = Including Tax
 * - Apply Customer Tax = After Discount
 * - Create a shopping cart price rule with % discount applied to the Shipping Amount
 * - run shopping cart and estimate shipping
 * - go to PayPal
 *
 * Changed by @author https://gist.github.com/evgv
 * 1. $salesEntity->getBaseHiddenTaxAmount() to $salesEntity->getHiddenTaxAmount()
 * 2. $salesEntity->getBaseShippingHiddenTaxAmount() to $salesEntity->getShippingHiddenTaxAmount()
 * 
 * @param Mage_Core_Model_Abstract $salesEntity
 */
private function _applyHiddenTaxWorkaround($salesEntity)
{
    $this->_totals[self::TOTAL_TAX] += (float)$salesEntity->getBaseHiddenTaxAmount();
    $this->_totals[self::TOTAL_TAX] += (float)$salesEntity->getBaseShippingHiddenTaxAmount();
}
<?php
/**
* Magento
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://opensource.org/licenses/osl-3.0.php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to [email protected] so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade Magento to newer
* versions in the future. If you wish to customize Magento for your
* needs please refer to http://www.magento.com for more information.
*
* @category Mage
* @package Mage_Paypal
* @copyright Copyright (c) 2006-2016 X.commerce, Inc. and affiliates (http://www.magento.com)
* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
*/
/**
* PayPal-specific model for shopping cart items and totals
* The main idea is to accommodate all possible totals into PayPal-compatible 4 totals and line items
*/
class Mage_Paypal_Model_Cart
{
/**
* Totals that PayPal suppports when passing shopping cart
*
* @var string
*/
const TOTAL_SUBTOTAL = 'subtotal';
const TOTAL_DISCOUNT = 'discount';
const TOTAL_TAX = 'tax';
const TOTAL_SHIPPING = 'shipping';
/**
* Order or quote instance
*
* @var Mage_Sales_Model_Order
* @var Mage_Sales_Model_Quote
*/
protected $_salesEntity = null;
/**
* Rendered cart items
* Array of Varien_Objects
*
* @var array
*/
protected $_items = array();
/**
* Rendered cart totals
* Associative array with the keys from constants above
*
* @var array
*/
protected $_totals = array();
/**
* Set of optional descriptions for the item that may replace a total and composed of several amounts
* Array of strings
*
* @var array
*/
protected $_totalLineItemDescriptions = array();
/**
* Lazy initialization indicator for rendering
*
* @var bool
*/
protected $_shouldRender = true;
/**
* Validation result for the rendered cart items
*
* @var bool
*/
protected $_areItemsValid = false;
/**
* Validation result for the rendered totals
*
* @var bool
*/
protected $_areTotalsValid = false;
/**
* Whether to render discount total as a line item
* Use case: WPP
*
* @var bool
*/
protected $_isDiscountAsItem = false;
/**
* Whether to render shipping total as a line item
* Use case: WPS
*
* @var bool
*/
protected $_isShippingAsItem = false;
/**
* Require instance of an order or a quote
*
* @param array $params
*/
public function __construct($params = array())
{
$salesEntity = array_shift($params);
if (is_object($salesEntity)
&& (($salesEntity instanceof Mage_Sales_Model_Order) || ($salesEntity instanceof Mage_Sales_Model_Quote))) {
$this->_salesEntity = $salesEntity;
} else {
throw new Exception('Invalid sales entity provided.');
}
}
/**
* Getter for the current sales entity
*
* @return Mage_Sales_Model_Order
* @return Mage_Sales_Model_Quote
*/
public function getSalesEntity()
{
return $this->_salesEntity;
}
/**
* Render and get line items
* By default returns false if the items are invalid
*
* @param bool $bypassValidation
* @return array|false
*/
public function getItems($bypassValidation = false)
{
$this->_render();
if (!$bypassValidation && !$this->_areItemsValid) {
return false;
}
return $this->_items;
}
/**
* Render and get totals
* If the totals are invalid for any reason, they will be merged into one amount (subtotal is utilized for it)
* An option to substract discount from the subtotal is available
*
* @param bool $mergeDiscount
* @return array
*/
public function getTotals($mergeDiscount = false)
{
$this->_render();
// cut down totals to one total if they are invalid
if (!$this->_areTotalsValid) {
$totals = array(
self::TOTAL_SUBTOTAL => $this->_totals[self::TOTAL_SUBTOTAL] + $this->_totals[self::TOTAL_TAX]
);
if (!$this->_isShippingAsItem) {
$totals[self::TOTAL_SUBTOTAL] += $this->_totals[self::TOTAL_SHIPPING];
}
if (!$this->_isDiscountAsItem) {
$totals[self::TOTAL_SUBTOTAL] -= $this->_totals[self::TOTAL_DISCOUNT];
}
return $totals;
} elseif ($mergeDiscount) {
$totals = $this->_totals;
unset($totals[self::TOTAL_DISCOUNT]);
if (!$this->_isDiscountAsItem) {
$totals[self::TOTAL_SUBTOTAL] -= $this->_totals[self::TOTAL_DISCOUNT];
}
return $totals;
}
return $this->_totals;
}
/**
* Add a line item
*
* @param string $name
* @param numeric $qty
* @param float $amount
* @param string $identifier
* @return Varien_Object
*/
public function addItem($name, $qty, $amount, $identifier = null)
{
$this->_shouldRender = true;
$item = new Varien_Object(array(
'name' => $name,
'qty' => $qty,
'amount' => (float)$amount,
));
if ($identifier) {
$item->setData('id', $identifier);
}
$this->_items[] = $item;
return $item;
}
/**
* Remove item from cart by identifier
*
* @param string $identifier
* @return bool
*/
public function removeItem($identifier)
{
foreach ($this->_items as $key => $item) {
if ($item->getId() == $identifier) {
unset($this->_items[$key]);
return true;
}
}
return false;
}
/**
* Compound the specified amount with the specified total
*
* @param string $code
* @param float $amount
* @param string $lineItemDescription
* @return Mage_Paypal_Model_Cart
*/
public function updateTotal($code, $amount, $lineItemDescription = null)
{
$this->_shouldRender = true;
if (isset($this->_totals[$code])) {
$this->_totals[$code] += $amount;
if ($lineItemDescription) {
$this->_totalLineItemDescriptions[$code][] = $lineItemDescription;
}
}
return $this;
}
/**
* Get/Set whether to render the discount total as a line item
*
* @param $setValue
* @return bool|Mage_Paypal_Model_Cart
*/
public function isDiscountAsItem($setValue = null)
{
return $this->_totalAsItem('_isDiscountAsItem', $setValue);
}
/**
* Get/Set whether to render the discount total as a line item
*
* @param $setValue
* @return bool|Mage_Paypal_Model_Cart
*/
public function isShippingAsItem($setValue = null)
{
return $this->_totalAsItem('_isShippingAsItem', $setValue);
}
/**
* (re)Render all items and totals
*
* 1. $this->_salesEntity->getBaseSubtotal() to $this->_salesEntity->getSubtotal()
* 2. $this->_salesEntity->getBaseTaxAmount() to $this->_salesEntity->getTaxAmount()
* 3. $this->_salesEntity->getBaseShippingAmount() to $this->_salesEntity->getShippingAmount()
* 4. $this->_salesEntity->getBaseDiscountAmount() to $this->_salesEntity->getDiscountAmount()
*/
protected function _render()
{
if (!$this->_shouldRender) {
return;
}
// regular items from the sales entity
$this->_items = array();
foreach ($this->_salesEntity->getAllItems() as $item) {
if (!$item->getParentItem()) {
$this->_addRegularItem($item);
}
}
end($this->_items);
$lastRegularItemKey = key($this->_items);
// regular totals
$shippingDescription = '';
if ($this->_salesEntity instanceof Mage_Sales_Model_Order) {
$shippingDescription = $this->_salesEntity->getShippingDescription();
$this->_totals = array(
self::TOTAL_SUBTOTAL => $this->_salesEntity->getSubtotal(),
self::TOTAL_TAX => $this->_salesEntity->getTaxAmount(),
self::TOTAL_SHIPPING => $this->_salesEntity->getShippingAmount(),
self::TOTAL_DISCOUNT => abs($this->_salesEntity->getDiscountAmount()),
);
$this->_applyHiddenTaxWorkaround($this->_salesEntity);
} else {
$address = $this->_salesEntity->getIsVirtual() ?
$this->_salesEntity->getBillingAddress() : $this->_salesEntity->getShippingAddress();
$shippingDescription = $address->getShippingDescription();
$this->_totals = array (
self::TOTAL_SUBTOTAL => $this->_salesEntity->getSubtotal(),
self::TOTAL_TAX => $address->getTaxAmount(),
self::TOTAL_SHIPPING => $address->getShippingAmount(),
self::TOTAL_DISCOUNT => abs($address->getDiscountAmount()),
);
$this->_applyHiddenTaxWorkaround($address);
}
$originalDiscount = $this->_totals[self::TOTAL_DISCOUNT];
// arbitrary items, total modifications
Mage::dispatchEvent('paypal_prepare_line_items', array('paypal_cart' => $this));
// distinguish original discount among the others
if ($originalDiscount > 0.0001 && isset($this->_totalLineItemDescriptions[self::TOTAL_DISCOUNT])) {
$this->_totalLineItemDescriptions[self::TOTAL_DISCOUNT][] = Mage::helper('sales')->__('Discount (%s)', Mage::app()->getStore()->convertPrice($originalDiscount, true, false));
}
// discount, shipping as items
if ($this->_isDiscountAsItem && $this->_totals[self::TOTAL_DISCOUNT]) {
$this->addItem(Mage::helper('paypal')->__('Discount'), 1, -1.00 * $this->_totals[self::TOTAL_DISCOUNT],
$this->_renderTotalLineItemDescriptions(self::TOTAL_DISCOUNT)
);
}
$shippingItemId = $this->_renderTotalLineItemDescriptions(self::TOTAL_SHIPPING, $shippingDescription);
if ($this->_isShippingAsItem && (float)$this->_totals[self::TOTAL_SHIPPING]) {
$this->addItem(Mage::helper('paypal')->__('Shipping'), 1, (float)$this->_totals[self::TOTAL_SHIPPING],
$shippingItemId
);
}
// compound non-regular items into subtotal
foreach ($this->_items as $key => $item) {
if ($key > $lastRegularItemKey && $item->getAmount() != 0) {
$this->_totals[self::TOTAL_SUBTOTAL] += $item->getAmount();
}
}
$this->_validate();
// if cart items are invalid, prepare cart for transfer without line items
if (!$this->_areItemsValid) {
$this->removeItem($shippingItemId);
}
$this->_shouldRender = false;
}
/**
* Merge multiple descriptions by a total code into a string
*
* @param string $code
* @param string $prepend
* @param string $append
* @param string $glue
* @return string
*/
protected function _renderTotalLineItemDescriptions($code, $prepend = '', $append = '', $glue = '; ')
{
$result = array();
if ($prepend) {
$result[] = $prepend;
}
if (isset($this->_totalLineItemDescriptions[$code])) {
$result = array_merge($this->_totalLineItemDescriptions[$code]);
}
if ($append) {
$result[] = $append;
}
return implode($glue, $result);
}
/**
* Check the line items and totals according to PayPal business logic limitations
*
* Changed by @author https://gist.github.com/evgv
* 1. $this->_salesEntity->getBaseGrandTotal() to $this->_salesEntity->getGrandTotal();
*
*/
protected function _validate()
{
$this->_areItemsValid = true;
$this->_areTotalsValid = false;
$referenceAmount = $this->_salesEntity->getGrandTotal();
$itemsSubtotal = 0;
foreach ($this->_items as $i) {
$itemsSubtotal = $itemsSubtotal + $i['qty'] * $i['amount'];
}
$sum = $itemsSubtotal + $this->_totals[self::TOTAL_TAX];
if (!$this->_isShippingAsItem) {
$sum += $this->_totals[self::TOTAL_SHIPPING];
}
if (!$this->_isDiscountAsItem) {
$sum -= $this->_totals[self::TOTAL_DISCOUNT];
}
/**
* numbers are intentionally converted to strings because of possible comparison error
* see http://php.net/float
*/
// match sum of all the items and totals to the reference amount
if (sprintf('%.4F', $sum) != sprintf('%.4F', $referenceAmount)) {
$adjustment = $sum - $referenceAmount;
$this->_totals[self::TOTAL_SUBTOTAL] = $this->_totals[self::TOTAL_SUBTOTAL] - $adjustment;
}
// PayPal requires to have discount less than items subtotal
if (!$this->_isDiscountAsItem) {
$this->_areTotalsValid = round($this->_totals[self::TOTAL_DISCOUNT], 4) < round($itemsSubtotal, 4);
} else {
$this->_areTotalsValid = $itemsSubtotal > 0.00001;
}
$this->_areItemsValid = $this->_areItemsValid && $this->_areTotalsValid;
}
/**
* Check whether items are valid
*
* @return bool
*/
public function areItemsValid()
{
return $this->_areItemsValid;
}
/**
* Add a usual line item with amount and qty
*
* Changed by @author https://gist.github.com/evgv
* 1. $salesItem->getBasePrice() to $salesItem->getPrice()
* 2. $salesItem->getBaseCalculationPrice() to $salesItem->getBaseCalculationPriceOriginal()
* 3. $salesItem->getBaseRowTotal() to $salesItem->getRowTotal()
*
* @param Varien_Object $salesItem
* @return Varien_Object
*/
protected function _addRegularItem(Varien_Object $salesItem)
{
if ($this->_salesEntity instanceof Mage_Sales_Model_Order) {
$qty = (int) $salesItem->getQtyOrdered();
$amount = (float) $salesItem->getPrice();
// TODO: nominal item for order
} else {
$qty = (int) $salesItem->getTotalQty();
$amount = $salesItem->isNominal() ? 0 : (float) $salesItem->getBaseCalculationPriceOriginal();
}
// workaround in case if item subtotal precision is not compatible with PayPal (.2)
$subAggregatedLabel = '';
if ($amount - round($amount, 2)) {
$amount = $amount * $qty;
$subAggregatedLabel = ' x' . $qty;
$qty = 1;
}
// aggregate item price if item qty * price does not match row total
if (($amount * $qty) != $salesItem->getRowTotal()) {
$amount = (float) $salesItem->getRowTotal();
$subAggregatedLabel = ' x' . $qty;
$qty = 1;
}
return $this->addItem($salesItem->getName() . $subAggregatedLabel, $qty, $amount, $salesItem->getSku());
}
/**
* Get/Set for the specified variable.
* If the value changes, the re-rendering is commenced
*
* @param string $var
* @param $setValue
* @return bool|Mage_Paypal_Model_Cart
*/
private function _totalAsItem($var, $setValue = null)
{
if (null !== $setValue) {
if ($setValue != $this->$var) {
$this->_shouldRender = true;
}
$this->$var = $setValue;
return $this;
}
return $this->$var;
}
/**
* Add "hidden" discount and shipping tax
*
* Go ahead, try to understand ]:->
*
* Tax settings for getting "discount tax":
* - Catalog Prices = Including Tax
* - Apply Customer Tax = After Discount
* - Apply Discount on Prices = Including Tax
*
* Test case for getting "hidden shipping tax":
* - Make sure shipping is taxable (set shipping tax class)
* - Catalog Prices = Including Tax
* - Shipping Prices = Including Tax
* - Apply Customer Tax = After Discount
* - Create a shopping cart price rule with % discount applied to the Shipping Amount
* - run shopping cart and estimate shipping
* - go to PayPal
*
* Changed by @author https://gist.github.com/evgv
* 1. $salesEntity->getBaseHiddenTaxAmount() to $salesEntity->getHiddenTaxAmount()
* 2. $salesEntity->getBaseShippingHiddenTaxAmount() to $salesEntity->getShippingHiddenTaxAmount()
*
* @param Mage_Core_Model_Abstract $salesEntity
*/
private function _applyHiddenTaxWorkaround($salesEntity)
{
$this->_totals[self::TOTAL_TAX] += (float)$salesEntity->getBaseHiddenTaxAmount();
$this->_totals[self::TOTAL_TAX] += (float)$salesEntity->getBaseShippingHiddenTaxAmount();
}
/**
* Check whether any item has negative amount
*
* @return bool
*/
public function hasNegativeItemAmount()
{
foreach ($this->_items as $item) {
if ($item->getAmount() < 0) {
return true;
}
}
return false;
}
}
<?php
/**
* Magento
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://opensource.org/licenses/osl-3.0.php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to [email protected] so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade Magento to newer
* versions in the future. If you wish to customize Magento for your
* needs please refer to http://www.magento.com for more information.
*
* @category Mage
* @package Mage_Paypal
* @copyright Copyright (c) 2006-2016 X.commerce, Inc. and affiliates (http://www.magento.com)
* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
*/
/**
* Wrapper that performs Paypal Express and Checkout communication
* Use current Paypal Express method instance
*/
class Mage_Paypal_Model_Express_Checkout
{
/**
* Cache ID prefix for "pal" lookup
* @var string
*/
const PAL_CACHE_ID = 'paypal_express_checkout_pal';
/**
* Keys for passthrough variables in sales/quote_payment and sales/order_payment
* Uses additional_information as storage
* @var string
*/
const PAYMENT_INFO_TRANSPORT_TOKEN = 'paypal_express_checkout_token';
const PAYMENT_INFO_TRANSPORT_SHIPPING_OVERRIDEN = 'paypal_express_checkout_shipping_overriden';
const PAYMENT_INFO_TRANSPORT_SHIPPING_METHOD = 'paypal_express_checkout_shipping_method';
const PAYMENT_INFO_TRANSPORT_PAYER_ID = 'paypal_express_checkout_payer_id';
const PAYMENT_INFO_TRANSPORT_REDIRECT = 'paypal_express_checkout_redirect_required';
const PAYMENT_INFO_TRANSPORT_BILLING_AGREEMENT = 'paypal_ec_create_ba';
/**
* Flag which says that was used PayPal Express Checkout button for checkout
* Uses additional_information as storage
* @var string
*/
const PAYMENT_INFO_BUTTON = 'button';
/**
* @var Mage_Sales_Model_Quote
*/
protected $_quote = null;
/**
* Config instance
* @var Mage_Paypal_Model_Config
*/
protected $_config = null;
/**
* API instance
* @var Mage_Paypal_Model_Api_Nvp
*/
protected $_api = null;
/**
* Api Model Type
*
* @var string
*/
protected $_apiType = 'paypal/api_nvp';
/**
* Payment method type
*
* @var unknown_type
*/
protected $_methodType = Mage_Paypal_Model_Config::METHOD_WPP_EXPRESS;
/**
* State helper variables
* @var string
*/
protected $_redirectUrl = '';
protected $_pendingPaymentMessage = '';
protected $_checkoutRedirectUrl = '';
/**
* @var Mage_Customer_Model_Session
*/
protected $_customerSession;
/**
* Redirect urls supposed to be set to support giropay
*
* @var array
*/
protected $_giropayUrls = array();
/**
* Create Billing Agreement flag
*
* @var bool
*/
protected $_isBARequested = false;
/**
* Flag for Bill Me Later mode
*
* @var bool
*/
protected $_isBml = false;
/**
* Customer ID
*
* @var int
*/
protected $_customerId = null;
/**
* Recurring payment profiles
*
* @var array
*/
protected $_recurringPaymentProfiles = array();
/**
* Billing agreement that might be created during order placing
*
* @var Mage_Sales_Model_Billing_Agreement
*/
protected $_billingAgreement = null;
/**
* Order
*
* @var Mage_Sales_Model_QuoteMage_Sales_Model_Quote
*/
protected $_order = null;
/**
* Set quote and config instances
* @param array $params
*/
public function __construct($params = array())
{
if (isset($params['quote']) && $params['quote'] instanceof Mage_Sales_Model_Quote) {
$this->_quote = $params['quote'];
} else {
throw new Exception('Quote instance is required.');
}
if (isset($params['config']) && $params['config'] instanceof Mage_Paypal_Model_Config) {
$this->_config = $params['config'];
} else {
throw new Exception('Config instance is required.');
}
$this->_customerSession = isset($params['session']) && $params['session'] instanceof Mage_Customer_Model_Session
? $params['session'] : Mage::getSingleton('customer/session');
}
/**
* Checkout with PayPal image URL getter
* Spares API calls of getting "pal" variable, by putting it into cache per store view
* @return string
*/
public function getCheckoutShortcutImageUrl()
{
// get "pal" thing from cache or lookup it via API
$pal = null;
if ($this->_config->areButtonsDynamic()) {
$cacheId = self::PAL_CACHE_ID . Mage::app()->getStore()->getId();
$pal = Mage::app()->loadCache($cacheId);
if (-1 == $pal) {
$pal = null;
} elseif (!$pal) {
$pal = null;
$this->_getApi();
try {
$this->_api->callGetPalDetails();
$pal = $this->_api->getPal();
Mage::app()->saveCache($pal, $cacheId, array(Mage_Core_Model_Config::CACHE_TAG));
} catch (Exception $e) {
Mage::app()->saveCache(-1, $cacheId, array(Mage_Core_Model_Config::CACHE_TAG));
Mage::logException($e);
}
}
}
return $this->_config->getExpressCheckoutShortcutImageUrl(
Mage::app()->getLocale()->getLocaleCode(),
$this->_quote->getGrandTotal(),
$pal
);
}
/**
* Setter that enables giropay redirects flow
*
* @param string $successUrl - payment success result
* @param string $cancelUrl - payment cancellation result
* @param string $pendingUrl - pending payment result
* @return Mage_Paypal_Model_Express_Checkout
*/
public function prepareGiropayUrls($successUrl, $cancelUrl, $pendingUrl)
{
$this->_giropayUrls = array($successUrl, $cancelUrl, $pendingUrl);
return $this;
}
/**
* Set create billing agreement flag
*
* @param bool $flag
* @return Mage_Paypal_Model_Express_Checkout
*/
public function setIsBillingAgreementRequested($flag)
{
$this->_isBARequested = $flag;
return $this;
}
/**
* Setter for customer Id
*
* @param int $id
* @return Mage_Paypal_Model_Express_Checkout
* @deprecated please use self::setCustomer
*/
public function setCustomerId($id)
{
$this->_customerId = $id;
return $this;
}
/**
* Set flag that forces to use BillMeLater
*
* @param bool $isBml
*/
public function setIsBml($isBml)
{
$this->_isBml = $isBml;
}
/**
* Setter for customer
*
* @param Mage_Customer_Model_Customer $customer
* @return Mage_Paypal_Model_Express_Checkout
*/
public function setCustomer($customer)
{
$this->_quote->assignCustomer($customer);
$this->_customerId = $customer->getId();
return $this;
}
/**
* Setter for customer with billing and shipping address changing ability
*
* @param Mage_Customer_Model_Customer $customer
* @param Mage_Sales_Model_Quote_Address $billingAddress
* @param Mage_Sales_Model_Quote_Address $shippingAddress
* @return Mage_Paypal_Model_Express_Checkout
*/
public function setCustomerWithAddressChange($customer, $billingAddress = null, $shippingAddress = null)
{
$this->_quote->assignCustomerWithAddressChange($customer, $billingAddress, $shippingAddress);
$this->_customerId = $customer->getId();
return $this;
}
/**
* Reserve order ID for specified quote and start checkout on PayPal
*
*
* Changed by @author https://gist.github.com/evgv
* 1. Set $this->_quote->getGrandTotal() instead $this->_quote->getBaseGrandTotal()
* 2. Set $this->_quote->getQuoteCurrencyCode() instead $this->_quote->getBaseCurrencyCode()
*
* @param string $returnUrl
* @param string $cancelUrl
* @param bool|null $button
* @return mixed
*/
public function start($returnUrl, $cancelUrl, $button = null)
{
$this->_quote->collectTotals();
if (!$this->_quote->getGrandTotal() && !$this->_quote->hasNominalItems()) {
Mage::throwException(Mage::helper('paypal')->__('PayPal does not support processing orders with zero amount. To complete your purchase, proceed to the standard checkout process.'));
}
$this->_quote->reserveOrderId()->save();
// prepare API
$this->_getApi();
$solutionType = $this->_config->getMerchantCountry() == 'DE'
? Mage_Paypal_Model_Config::EC_SOLUTION_TYPE_MARK : $this->_config->solutionType;
$this->_api->setAmount($this->_quote->getGrandTotal())
->setCurrencyCode($this->_quote->getQuoteCurrencyCode())
->setInvNum($this->_quote->getReservedOrderId())
->setReturnUrl($returnUrl)
->setCancelUrl($cancelUrl)
->setSolutionType($solutionType)
->setPaymentAction($this->_config->paymentAction);
if ($this->_giropayUrls) {
list($successUrl, $cancelUrl, $pendingUrl) = $this->_giropayUrls;
$this->_api->addData(array(
'giropay_cancel_url' => $cancelUrl,
'giropay_success_url' => $successUrl,
'giropay_bank_txn_pending_url' => $pendingUrl,
));
}
if ($this->_isBml) {
$this->_api->setFundingSource('BML');
}
$this->_setBillingAgreementRequest();
if ($this->_config->requireBillingAddress == Mage_Paypal_Model_Config::REQUIRE_BILLING_ADDRESS_ALL) {
$this->_api->setRequireBillingAddress(1);
}
// supress or export shipping address
if ($this->_quote->getIsVirtual()) {
if ($this->_config->requireBillingAddress == Mage_Paypal_Model_Config::REQUIRE_BILLING_ADDRESS_VIRTUAL) {
$this->_api->setRequireBillingAddress(1);
}
$this->_api->setSuppressShipping(true);
} else {
$address = $this->_quote->getShippingAddress();
$isOverriden = 0;
if (true === $address->validate()) {
$isOverriden = 1;
$this->_api->setAddress($address);
}
$this->_quote->getPayment()->setAdditionalInformation(
self::PAYMENT_INFO_TRANSPORT_SHIPPING_OVERRIDEN, $isOverriden
);
$this->_quote->getPayment()->save();
}
// add line items
$paypalCart = Mage::getModel('paypal/cart', array($this->_quote));
$this->_api->setPaypalCart($paypalCart)
->setIsLineItemsEnabled($this->_config->lineItemsEnabled)
;
// add shipping options if needed and line items are available
if ($this->_config->lineItemsEnabled && $this->_config->transferShippingOptions && $paypalCart->getItems()) {
if (!$this->_quote->getIsVirtual() && !$this->_quote->hasNominalItems()) {
if ($options = $this->_prepareShippingOptions($address, true)) {
$this->_api->setShippingOptionsCallbackUrl(
Mage::getUrl('*/*/shippingOptionsCallback', array('quote_id' => $this->_quote->getId()))
)->setShippingOptions($options);
}
}
}
// add recurring payment profiles information
if ($profiles = $this->_quote->prepareRecurringPaymentProfiles()) {
foreach ($profiles as $profile) {
$profile->setMethodCode(Mage_Paypal_Model_Config::METHOD_WPP_EXPRESS);
if (!$profile->isValid()) {
Mage::throwException($profile->getValidationErrors(true, true));
}
}
$this->_api->addRecurringPaymentProfiles($profiles);
}
$this->_config->exportExpressCheckoutStyleSettings($this->_api);
// call API and redirect with token
$this->_api->callSetExpressCheckout();
$token = $this->_api->getToken();
$this->_redirectUrl = $button ? $this->_config->getExpressCheckoutStartUrl($token)
: $this->_config->getPayPalBasicStartUrl($token);
$this->_quote->getPayment()->unsAdditionalInformation(self::PAYMENT_INFO_TRANSPORT_BILLING_AGREEMENT);
// Set flag that we came from Express Checkout button
if (!empty($button)) {
$this->_quote->getPayment()->setAdditionalInformation(self::PAYMENT_INFO_BUTTON, 1);
} elseif ($this->_quote->getPayment()->hasAdditionalInformation(self::PAYMENT_INFO_BUTTON)) {
$this->_quote->getPayment()->unsAdditionalInformation(self::PAYMENT_INFO_BUTTON);
}
$this->_quote->getPayment()->save();
return $token;
}
/**
* Check whether system can skip order review page before placing order
*
* @return bool
*/
public function canSkipOrderReviewStep()
{
$isOnepageCheckout = !$this->_quote->getPayment()
->getAdditionalInformation(Mage_Paypal_Model_Express_Checkout::PAYMENT_INFO_BUTTON);
return $this->_config->isOrderReviewStepDisabled() && $isOnepageCheckout;
}
/**
* Update quote when returned from PayPal
* rewrite billing address by paypal
* save old billing address for new customer
* export shipping address in case address absence
*
* @param string $token
*/
public function returnFromPaypal($token)
{
$this->_getApi();
$this->_api->setToken($token)
->callGetExpressCheckoutDetails();
$quote = $this->_quote;
$this->_ignoreAddressValidation();
// import shipping address
$exportedShippingAddress = $this->_api->getExportedShippingAddress();
if (!$quote->getIsVirtual()) {
$shippingAddress = $quote->getShippingAddress();
if ($shippingAddress) {
if ($exportedShippingAddress) {
$this->_setExportedAddressData($shippingAddress, $exportedShippingAddress);
if ($quote->getPayment()->getAdditionalInformation(self::PAYMENT_INFO_BUTTON) == 1) {
// PayPal doesn't provide detailed shipping info: prefix, middlename, lastname, suffix
$shippingAddress->setPrefix(null);
$shippingAddress->setMiddlename(null);
$shippingAddress->setLastname(null);
$shippingAddress->setSuffix(null);
}
$shippingAddress->setCollectShippingRates(true);
$shippingAddress->setSameAsBilling(0);
}
// import shipping method
$code = '';
if ($this->_api->getShippingRateCode()) {
if ($code = $this->_matchShippingMethodCode($shippingAddress, $this->_api->getShippingRateCode())) {
// possible bug of double collecting rates :-/
$shippingAddress->setShippingMethod($code)->setCollectShippingRates(true);
}
}
$quote->getPayment()->setAdditionalInformation(
self::PAYMENT_INFO_TRANSPORT_SHIPPING_METHOD,
$code
);
}
}
// import billing address
$portBillingFromShipping = $quote->getPayment()->getAdditionalInformation(self::PAYMENT_INFO_BUTTON) == 1
&& $this->_config->requireBillingAddress != Mage_Paypal_Model_Config::REQUIRE_BILLING_ADDRESS_ALL
&& !$quote->isVirtual();
if ($portBillingFromShipping) {
$billingAddress = clone $shippingAddress;
$billingAddress->unsAddressId()
->unsAddressType();
$data = $billingAddress->getData();
$data['save_in_address_book'] = 0;
$quote->getBillingAddress()->addData($data);
$quote->getShippingAddress()->setSameAsBilling(1);
} else {
$billingAddress = $quote->getBillingAddress();
}
$exportedBillingAddress = $this->_api->getExportedBillingAddress();
$this->_setExportedAddressData($billingAddress, $exportedBillingAddress);
$billingAddress->setCustomerNotes($exportedBillingAddress->getData('note'));
$quote->setBillingAddress($billingAddress);
// import payment info
$payment = $quote->getPayment();
$payment->setMethod($this->_methodType);
Mage::getSingleton('paypal/info')->importToPayment($this->_api, $payment);
$payment->setAdditionalInformation(self::PAYMENT_INFO_TRANSPORT_PAYER_ID, $this->_api->getPayerId())
->setAdditionalInformation(self::PAYMENT_INFO_TRANSPORT_TOKEN, $token)
;
$quote->collectTotals()->save();
}
/**
* Check whether order review has enough data to initialize
*
* @param $token
* @throws Mage_Core_Exception
*/
public function prepareOrderReview($token = null)
{
$payment = $this->_quote->getPayment();
if (!$payment || !$payment->getAdditionalInformation(self::PAYMENT_INFO_TRANSPORT_PAYER_ID)) {
Mage::throwException(Mage::helper('paypal')->__('Payer is not identified.'));
}
$this->_quote->setMayEditShippingAddress(
1 != $this->_quote->getPayment()->getAdditionalInformation(self::PAYMENT_INFO_TRANSPORT_SHIPPING_OVERRIDEN)
);
$this->_quote->setMayEditShippingMethod(
'' == $this->_quote->getPayment()->getAdditionalInformation(self::PAYMENT_INFO_TRANSPORT_SHIPPING_METHOD)
);
$this->_ignoreAddressValidation();
$this->_quote->collectTotals()->save();
}
/**
* Return callback response with shipping options
*
* @param array $request
* @return string
*/
public function getShippingOptionsCallbackResponse(array $request)
{
// prepare debug data
$logger = Mage::getModel('core/log_adapter', 'payment_' . $this->_methodType . '.log');
$debugData = array('request' => $request, 'response' => array());
try {
// obtain addresses
$this->_getApi();
$address = $this->_api->prepareShippingOptionsCallbackAddress($request);
$quoteAddress = $this->_quote->getShippingAddress();
// compare addresses, calculate shipping rates and prepare response
$options = array();
if ($address && $quoteAddress && !$this->_quote->getIsVirtual()) {
foreach ($address->getExportedKeys() as $key) {
$quoteAddress->setDataUsingMethod($key, $address->getData($key));
}
$quoteAddress->setCollectShippingRates(true)->collectTotals();
$options = $this->_prepareShippingOptions($quoteAddress, false, true);
}
$response = $this->_api->setShippingOptions($options)->formatShippingOptionsCallback();
// log request and response
$debugData['response'] = $response;
$logger->log($debugData);
return $response;
} catch (Exception $e) {
$logger->log($debugData);
throw $e;
}
}
/**
* Set shipping method to quote, if needed
* @param string $methodCode
*/
public function updateShippingMethod($methodCode)
{
if (!$this->_quote->getIsVirtual() && $shippingAddress = $this->_quote->getShippingAddress()) {
if ($methodCode != $shippingAddress->getShippingMethod()) {
$this->_ignoreAddressValidation();
$shippingAddress->setShippingMethod($methodCode)->setCollectShippingRates(true);
$this->_quote->collectTotals()->save();
}
}
}
/**
* Place the order and recurring payment profiles when customer returned from paypal
* Until this moment all quote data must be valid
*
* @param string $token
* @param string $shippingMethodCode
*/
public function place($token, $shippingMethodCode = null)
{
if ($shippingMethodCode) {
$this->updateShippingMethod($shippingMethodCode);
}
$isNewCustomer = false;
switch ($this->getCheckoutMethod()) {
case Mage_Checkout_Model_Type_Onepage::METHOD_GUEST:
$this->_prepareGuestQuote();
break;
case Mage_Checkout_Model_Type_Onepage::METHOD_REGISTER:
$this->_prepareNewCustomerQuote();
$isNewCustomer = true;
break;
default:
$this->_prepareCustomerQuote();
break;
}
$this->_ignoreAddressValidation();
$this->_quote->collectTotals();
$service = Mage::getModel('sales/service_quote', $this->_quote);
$service->submitAll();
$this->_quote->save();
if ($isNewCustomer) {
try {
$this->_involveNewCustomer();
} catch (Exception $e) {
Mage::logException($e);
}
}
$this->_recurringPaymentProfiles = $service->getRecurringPaymentProfiles();
// TODO: send recurring profile emails
/** @var $order Mage_Sales_Model_Order */
$order = $service->getOrder();
if (!$order) {
return;
}
$this->_billingAgreement = $order->getPayment()->getBillingAgreement();
// commence redirecting to finish payment, if paypal requires it
if ($order->getPayment()->getAdditionalInformation(
Mage_Paypal_Model_Express_Checkout::PAYMENT_INFO_TRANSPORT_REDIRECT
)) {
$this->_redirectUrl = $this->_config->getExpressCheckoutCompleteUrl($token);
}
switch ($order->getState()) {
// even after placement paypal can disallow to authorize/capture, but will wait until bank transfers money
case Mage_Sales_Model_Order::STATE_PENDING_PAYMENT:
// TODO
break;
// regular placement, when everything is ok
case Mage_Sales_Model_Order::STATE_PROCESSING:
case Mage_Sales_Model_Order::STATE_COMPLETE:
case Mage_Sales_Model_Order::STATE_PAYMENT_REVIEW:
$order->queueNewOrderEmail();
break;
}
$this->_order = $order;
}
/**
* Make sure addresses will be saved without validation errors
*/
private function _ignoreAddressValidation()
{
$this->_quote->getBillingAddress()->setShouldIgnoreValidation(true);
if (!$this->_quote->getIsVirtual()) {
$this->_quote->getShippingAddress()->setShouldIgnoreValidation(true);
if (!$this->_config->requireBillingAddress && !$this->_quote->getBillingAddress()->getEmail()) {
$this->_quote->getBillingAddress()->setSameAsBilling(1);
}
}
}
/**
* Determine whether redirect somewhere specifically is required
*
* @return string
*/
public function getRedirectUrl()
{
return $this->_redirectUrl;
}
/**
* Return recurring payment profiles
*
* @return array
*/
public function getRecurringPaymentProfiles()
{
return $this->_recurringPaymentProfiles;
}
/**
* Get created billing agreement
*
* @return Mage_Sales_Model_Billing_Agreement|null
*/
public function getBillingAgreement()
{
return $this->_billingAgreement;
}
/**
* Return order
*
* @return Mage_Sales_Model_Order
*/
public function getOrder()
{
return $this->_order;
}
/**
* Get checkout method
*
* @return string
*/
public function getCheckoutMethod()
{
if ($this->getCustomerSession()->isLoggedIn()) {
return Mage_Checkout_Model_Type_Onepage::METHOD_CUSTOMER;
}
if (!$this->_quote->getCheckoutMethod()) {
if (Mage::helper('checkout')->isAllowedGuestCheckout($this->_quote)) {
$this->_quote->setCheckoutMethod(Mage_Checkout_Model_Type_Onepage::METHOD_GUEST);
} else {
$this->_quote->setCheckoutMethod(Mage_Checkout_Model_Type_Onepage::METHOD_REGISTER);
}
}
return $this->_quote->getCheckoutMethod();
}
/**
* Sets address data from exported address
*
* @param Mage_Sales_Model_Quote_Address $address
* @param array $exportedAddress
*/
protected function _setExportedAddressData($address, $exportedAddress)
{
// Exported data is more priority if we came from Express Checkout button
$isButton = (bool)$this->_quote->getPayment()->getAdditionalInformation(self::PAYMENT_INFO_BUTTON);
if (!$isButton) {
foreach ($exportedAddress->getExportedKeys() as $key) {
$oldData = $address->getDataUsingMethod($key);
$isEmpty = null;
if (is_array($oldData)) {
foreach($oldData as $val) {
if(!empty($val)) {
$isEmpty = false;
break;
}
$isEmpty = true;
}
}
if (empty($oldData) || $isEmpty === true) {
$address->setDataUsingMethod($key, $exportedAddress->getData($key));
}
}
} else {
foreach ($exportedAddress->getExportedKeys() as $key) {
$data = $exportedAddress->getData($key);
if (!empty($data)) {
$address->setDataUsingMethod($key, $data);
}
}
}
}
/**
* Set create billing agreement flag to api call
*
* @return Mage_Paypal_Model_Express_Checkout
*/
protected function _setBillingAgreementRequest()
{
if (!$this->_customerId || $this->_quote->hasNominalItems()) {
return $this;
}
$isRequested = $this->_isBARequested || $this->_quote->getPayment()
->getAdditionalInformation(self::PAYMENT_INFO_TRANSPORT_BILLING_AGREEMENT);
if (!($this->_config->allow_ba_signup == Mage_Paypal_Model_Config::EC_BA_SIGNUP_AUTO
|| $isRequested && $this->_config->shouldAskToCreateBillingAgreement())) {
return $this;
}
if (!Mage::getModel('sales/billing_agreement')->needToCreateForCustomer($this->_customerId)) {
return $this;
}
$this->_api->setBillingType($this->_api->getBillingAgreementType());
return $this;
}
/**
* @return Mage_Paypal_Model_Api_Nvp
*/
protected function _getApi()
{
if (null === $this->_api) {
$this->_api = Mage::getModel($this->_apiType)->setConfigObject($this->_config);
}
return $this->_api;
}
/**
* Attempt to collect address shipping rates and return them for further usage in instant update API
* Returns empty array if it was impossible to obtain any shipping rate
* If there are shipping rates obtained, the method must return one of them as default.
*
* @param Mage_Sales_Model_Quote_Address $address
* @param bool $mayReturnEmpty
* @return array|false
*/
protected function _prepareShippingOptions(
Mage_Sales_Model_Quote_Address $address,
$mayReturnEmpty = false, $calculateTax = false
) {
$options = array(); $i = 0; $iMin = false; $min = false;
$userSelectedOption = null;
foreach ($address->getGroupedAllShippingRates() as $group) {
foreach ($group as $rate) {
$amount = (float)$rate->getPrice();
if ($rate->getErrorMessage()) {
continue;
}
$isDefault = $address->getShippingMethod() === $rate->getCode();
$amountExclTax = Mage::helper('tax')->getShippingPrice($amount, false, $address);
$amountInclTax = Mage::helper('tax')->getShippingPrice($amount, true, $address);
$options[$i] = new Varien_Object(array(
'is_default' => $isDefault,
'name' => trim("{$rate->getCarrier()} - {$rate->getMethodTitle()}", ' -'),
'code' => $rate->getCode(),
'amount' => $amountExclTax,
));
if ($calculateTax) {
$options[$i]->setTaxAmount(
$amountInclTax - $amountExclTax
+ $address->getTaxAmount() - $address->getShippingTaxAmount()
);
}
if ($isDefault) {
$userSelectedOption = $options[$i];
}
if (false === $min || $amountInclTax < $min) {
$min = $amountInclTax;
$iMin = $i;
}
$i++;
}
}
if ($mayReturnEmpty && is_null($userSelectedOption)) {
$options[] = new Varien_Object(array(
'is_default' => true,
'name' => Mage::helper('paypal')->__('N/A'),
'code' => 'no_rate',
'amount' => 0.00,
));
if ($calculateTax) {
$options[$i]->setTaxAmount($address->getTaxAmount());
}
} elseif (is_null($userSelectedOption) && isset($options[$iMin])) {
$options[$iMin]->setIsDefault(true);
}
// Magento will transfer only first 10 cheapest shipping options if there are more than 10 available.
if (count($options) > 10) {
usort($options, array(get_class($this),'cmpShippingOptions'));
array_splice($options, 10);
// User selected option will be always included in options list
if (!is_null($userSelectedOption) && !in_array($userSelectedOption, $options)) {
$options[9] = $userSelectedOption;
}
}
return $options;
}
/**
* Compare two shipping options based on their amounts
*
* This function is used as a callback comparison function in shipping options sorting process
* @see self::_prepareShippingOptions()
*
* @param Varien_Object $option1
* @param Varien_Object $option2
* @return integer
*/
protected static function cmpShippingOptions(Varien_Object $option1, Varien_Object $option2)
{
if ($option1->getAmount() == $option2->getAmount()) {
return 0;
}
return ($option1->getAmount() < $option2->getAmount()) ? -1 : 1;
}
/**
* Try to find whether the code provided by PayPal corresponds to any of possible shipping rates
* This method was created only because PayPal has issues with returning the selected code.
* If in future the issue is fixed, we don't need to attempt to match it. It would be enough to set the method code
* before collecting shipping rates
*
* @param Mage_Sales_Model_Quote_Address $address
* @param string $selectedCode
* @return string
*/
protected function _matchShippingMethodCode(Mage_Sales_Model_Quote_Address $address, $selectedCode)
{
$options = $this->_prepareShippingOptions($address, false);
foreach ($options as $option) {
if ($selectedCode === $option['code'] // the proper case as outlined in documentation
|| $selectedCode === $option['name'] // workaround: PayPal may return name instead of the code
// workaround: PayPal may concatenate code and name, and return it instead of the code:
|| $selectedCode === "{$option['code']} {$option['name']}"
) {
return $option['code'];
}
}
return '';
}
/**
* Prepare quote for guest checkout order submit
*
* @return Mage_Paypal_Model_Express_Checkout
*/
protected function _prepareGuestQuote()
{
$quote = $this->_quote;
$quote->setCustomerId(null)
->setCustomerEmail($quote->getBillingAddress()->getEmail())
->setCustomerIsGuest(true)
->setCustomerGroupId(Mage_Customer_Model_Group::NOT_LOGGED_IN_ID);
return $this;
}
/**
* Checks if customer with email coming from Express checkout exists
*
* @return int
*/
protected function _lookupCustomerId()
{
return Mage::getModel('customer/customer')
->setWebsiteId(Mage::app()->getWebsite()->getId())
->loadByEmail($this->_quote->getCustomerEmail())
->getId();
}
/**
* Prepare quote for customer registration and customer order submit
* and restore magento customer data from quote
*
* @return Mage_Paypal_Model_Express_Checkout
*/
protected function _prepareNewCustomerQuote()
{
$quote = $this->_quote;
$billing = $quote->getBillingAddress();
$shipping = $quote->isVirtual() ? null : $quote->getShippingAddress();
$customerId = $this->_lookupCustomerId();
if ($customerId && !$this->_customerEmailExists($quote->getCustomerEmail())) {
$this->getCustomerSession()->loginById($customerId);
return $this->_prepareCustomerQuote();
}
$customer = $quote->getCustomer();
/** @var $customer Mage_Customer_Model_Customer */
$customerBilling = $billing->exportCustomerAddress();
$customer->addAddress($customerBilling);
$billing->setCustomerAddress($customerBilling);
$customerBilling->setIsDefaultBilling(true);
if ($shipping && !$shipping->getSameAsBilling()) {
$customerShipping = $shipping->exportCustomerAddress();
$customer->addAddress($customerShipping);
$shipping->setCustomerAddress($customerShipping);
$customerShipping->setIsDefaultShipping(true);
} elseif ($shipping) {
$customerBilling->setIsDefaultShipping(true);
}
/**
* @todo integration with dynamica attributes customer_dob, customer_taxvat, customer_gender
*/
if ($quote->getCustomerDob() && !$billing->getCustomerDob()) {
$billing->setCustomerDob($quote->getCustomerDob());
}
if ($quote->getCustomerTaxvat() && !$billing->getCustomerTaxvat()) {
$billing->setCustomerTaxvat($quote->getCustomerTaxvat());
}
if ($quote->getCustomerGender() && !$billing->getCustomerGender()) {
$billing->setCustomerGender($quote->getCustomerGender());
}
Mage::helper('core')->copyFieldset('checkout_onepage_billing', 'to_customer', $billing, $customer);
$customer->setEmail($quote->getCustomerEmail());
$customer->setPrefix($quote->getCustomerPrefix());
$customer->setFirstname($quote->getCustomerFirstname());
$customer->setMiddlename($quote->getCustomerMiddlename());
$customer->setLastname($quote->getCustomerLastname());
$customer->setSuffix($quote->getCustomerSuffix());
$customer->setPassword($customer->decryptPassword($quote->getPasswordHash()));
$customer->setPasswordHash($customer->hashPassword($customer->getPassword()));
$customer->save();
$quote->setCustomer($customer);
return $this;
}
/**
* Prepare quote for customer order submit
*
* @return Mage_Paypal_Model_Express_Checkout
*/
protected function _prepareCustomerQuote()
{
$quote = $this->_quote;
$billing = $quote->getBillingAddress();
$shipping = $quote->isVirtual() ? null : $quote->getShippingAddress();
$customer = $this->getCustomerSession()->getCustomer();
if (!$billing->getCustomerId() || $billing->getSaveInAddressBook()) {
$customerBilling = $billing->exportCustomerAddress();
$customer->addAddress($customerBilling);
$billing->setCustomerAddress($customerBilling);
}
if ($shipping && ((!$shipping->getCustomerId() && !$shipping->getSameAsBilling())
|| (!$shipping->getSameAsBilling() && $shipping->getSaveInAddressBook()))) {
$customerShipping = $shipping->exportCustomerAddress();
$customer->addAddress($customerShipping);
$shipping->setCustomerAddress($customerShipping);
}
if (isset($customerBilling) && !$customer->getDefaultBilling()) {
$customerBilling->setIsDefaultBilling(true);
}
if ($shipping && isset($customerBilling) && !$customer->getDefaultShipping() && $shipping->getSameAsBilling()) {
$customerBilling->setIsDefaultShipping(true);
} elseif ($shipping && isset($customerShipping) && !$customer->getDefaultShipping()) {
$customerShipping->setIsDefaultShipping(true);
}
$quote->setCustomer($customer);
return $this;
}
/**
* Involve new customer to system
*
* @return Mage_Paypal_Model_Express_Checkout
*/
protected function _involveNewCustomer()
{
$customer = $this->_quote->getCustomer();
if ($customer->isConfirmationRequired()) {
$customer->sendNewAccountEmail('confirmation');
$url = Mage::helper('customer')->getEmailConfirmationUrl($customer->getEmail());
$this->getCustomerSession()->addSuccess(
Mage::helper('customer')->__('Account confirmation is required. Please, check your e-mail for confirmation link. To resend confirmation email please <a href="%s">click here</a>.', $url)
);
} else {
$customer->sendNewAccountEmail();
$this->getCustomerSession()->loginById($customer->getId());
}
return $this;
}
/**
* Get customer session object
*
* @return Mage_Customer_Model_Session
*/
public function getCustomerSession()
{
return $this->_customerSession;
}
/**
* Check if customer email exists
*
* @param string $email
* @return bool
*/
protected function _customerEmailExists($email)
{
$result = false;
$customer = Mage::getModel('customer/customer');
$websiteId = Mage::app()->getStore()->getWebsiteId();
if (!is_null($websiteId)) {
$customer->setWebsiteId($websiteId);
}
$customer->loadByEmail($email);
if (!is_null($customer->getId())) {
$result = true;
}
return $result;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment