Current File : /var/www/prestashop/modules/ps_checkout/src/Builder/Payload/OrderPayloadBuilder.php |
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License version 3.0
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* 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 license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA and Contributors <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0
*/
namespace PrestaShop\Module\PrestashopCheckout\Builder\Payload;
use Context;
use libphonenumber\PhoneNumberType;
use libphonenumber\PhoneNumberUtil;
use PrestaShop\Module\PrestashopCheckout\Configuration\PrestaShopConfiguration;
use PrestaShop\Module\PrestashopCheckout\Exception\PsCheckoutException;
use PrestaShop\Module\PrestashopCheckout\PayPal\PayPalConfiguration;
use PrestaShop\Module\PrestashopCheckout\PaypalCountryCodeMatrice;
use PrestaShop\Module\PrestashopCheckout\Routing\Router;
/**
* Build the payload for creating paypal order
*/
class OrderPayloadBuilder extends Builder implements PayloadBuilderInterface
{
/**
* @var array
*/
private $cart;
/**
* @var bool Allow to build the payload with more or less content depending if
* the customer use express checkout or not
*/
private $expressCheckout = false;
/**
* Define if we build the payload to create
* or update a paypal order
*
* @var bool
*/
private $isUpdate = false;
/**
* PayPal order id
*
* @var string
*/
private $paypalOrderId;
/**
* @var bool
*/
private $isPatch;
/**
* @var bool
*/
private $isCard = false;
/**
* @var string
*/
private $fundingSource;
/**
* @var string
*/
private $paypalCustomerId;
/**
* @var string
*/
private $paypalVaultId;
/**
* @var bool
*/
private $savePaymentMethod;
/**
* @var bool
*/
private $vault = false;
/**
* @param bool $savePaymentMethod
*/
public function setSavePaymentMethod($savePaymentMethod)
{
$this->savePaymentMethod = $savePaymentMethod;
}
/**
* @param string $fundingSource
*/
public function setFundingSource($fundingSource)
{
$this->fundingSource = $fundingSource;
}
/**
* @param string $paypalCustomerId
*/
public function setPaypalCustomerId($paypalCustomerId)
{
$this->paypalCustomerId = $paypalCustomerId;
}
/**
* @param string $paypalVaultId
*/
public function setPaypalVaultId($paypalVaultId)
{
$this->paypalVaultId = $paypalVaultId;
}
/**
* @param bool $vault
*/
public function setVault($vault)
{
$this->vault = $vault;
}
/**
* @param array $cart
* @param bool $isPatch
*/
public function __construct(array $cart, $isPatch = false)
{
$this->cart = $cart;
$this->isPatch = $isPatch;
parent::__construct();
}
/**
* Build payload with cart details
*
* @throws PsCheckoutException
*/
public function buildFullPayload()
{
parent::buildFullPayload();
$this->checkPaypalOrderIdWhenUpdate();
$this->buildBaseNode();
$this->buildAmountBreakdownNode();
if (false === $this->expressCheckout) {
$this->buildShippingNode();
if (false === $this->isUpdate) {
$this->buildPayerNode();
}
}
if (false === $this->isUpdate) {
$this->buildApplicationContextNode();
}
if ($this->isCard) {
$this->buildCardPaymentSourceNode();
$this->buildSupplementaryDataNode();
}
switch ($this->fundingSource) {
case 'paypal':
$this->buildPayPalPaymentSourceNode();
break;
case 'google_pay':
$this->buildGooglePayPaymentSourceNode();
break;
default:
break;
}
}
/**
* Build payload without cart details
*
* @throws PsCheckoutException
*/
public function buildMinimalPayload()
{
parent::buildMinimalPayload();
$this->checkPaypalOrderIdWhenUpdate();
$this->buildBaseNode();
if (false === $this->expressCheckout) {
$this->buildShippingNode();
if (false === $this->isUpdate) {
$this->buildPayerNode();
}
}
if (false === $this->isUpdate) {
$this->buildApplicationContextNode();
}
if ($this->isCard) {
$this->buildCardPaymentSourceNode();
$this->buildSupplementaryDataNode();
}
}
/**
* Build the basic payload
*/
public function buildBaseNode()
{
/** @var \Ps_checkout $module */
$module = \Module::getInstanceByName('ps_checkout');
/** @var PrestaShopConfiguration $configuration */
$configuration = $module->getService(PrestaShopConfiguration::class);
/** @var PayPalConfiguration $paypalConfiguration */
$paypalConfiguration = $module->getService(PayPalConfiguration::class);
$shopName = $configuration->get('PS_SHOP_NAME');
$merchantId = $paypalConfiguration->getMerchantId();
$node = [
'intent' => $paypalConfiguration->getIntent(), // capture or authorize
'custom_id' => (string) $this->cart['cart']['id'], // id_cart or id_order // link between paypal order and prestashop order
'invoice_id' => '',
'description' => $this->truncate(
'Checking out with your cart #' . $this->cart['cart']['id'] . ' from ' . $shopName,
127
),
'amount' => [
'currency_code' => $this->cart['currency']['iso_code'],
'value' => $this->formatAmount($this->cart['cart']['totals']['total_including_tax']['amount']),
],
'payee' => [
'merchant_id' => $merchantId,
],
'vault' => $this->vault,
];
if (true === $this->isUpdate) {
$node['id'] = $this->paypalOrderId;
} else {
$roundType = $paypalConfiguration->getRoundType();
$roundMode = $paypalConfiguration->getPriceRoundMode();
$node['roundingConfig'] = $roundType . '-' . $roundMode;
}
$this->getPayload()->addAndMergeItems($node);
}
/**
* Build shipping node
*/
public function buildShippingNode()
{
$gender = new \Gender($this->cart['customer']->id_gender, $this->cart['language']->id);
$genderName = $gender->name;
$node['shipping'] = [
'name' => [
'full_name' => $genderName . ' ' . $this->cart['addresses']['shipping']->lastname . ' ' . $this->cart['addresses']['shipping']->firstname,
],
'address' => $this->getAddressPortable('shipping'),
];
$this->getPayload()->addAndMergeItems($node);
}
/**
* Build payer node
*/
public function buildPayerNode()
{
$payerCountryIsoCode = $this->getCountryIsoCodeById($this->cart['addresses']['invoice']->id_country);
/** @var \Ps_checkout $module */
$module = \Module::getInstanceByName('ps_checkout');
$node['payer'] = [
'name' => [
'given_name' => (string) $this->cart['addresses']['invoice']->firstname,
'surname' => (string) $this->cart['addresses']['invoice']->lastname,
],
'address' => $this->getAddressPortable('invoice'),
];
if (\Validate::isEmail($this->cart['customer']->email)) {
$node['payer']['email_address'] = (string) $this->cart['customer']->email;
}
// Add optional birthdate if provided
if (!empty($this->cart['customer']->birthday) && $this->cart['customer']->birthday !== '0000-00-00') {
$node['payer']['birth_date'] = (string) $this->cart['customer']->birthday;
}
$phone = !empty($this->cart['addresses']['invoice']->phone) ? $this->cart['addresses']['invoice']->phone : '';
$phone = empty($phone) && !empty($this->cart['addresses']['invoice']->phone_mobile) ? $this->cart['addresses']['invoice']->phone_mobile : $phone;
if (!empty($phone)) {
try {
$phoneUtil = PhoneNumberUtil::getInstance();
$parsedPhone = $phoneUtil->parse($phone, $payerCountryIsoCode);
if ($phoneUtil->isValidNumber($parsedPhone)) {
$node['payer']['phone']['phone_number']['national_number'] = $parsedPhone->getNationalNumber();
switch ($phoneUtil->getNumberType($parsedPhone)) {
case PhoneNumberType::MOBILE:
$node['payer']['phone']['phone_type'] = 'MOBILE';
break;
case PhoneNumberType::PAGER:
$node['payer']['phone']['phone_type'] = 'PAGER';
break;
default:
$node['payer']['phone']['phone_type'] = 'OTHER';
}
}
} catch (\Exception $exception) {
$module->getLogger()->warning(
'Unable to format phone number on PayPal Order payload',
[
'type' => $this->isUpdate ? 'UPDATE' : 'CREATE',
'paypal_order' => $this->paypalOrderId,
'id_cart' => (int) $this->cart['cart']['id'],
'address_id' => (int) $this->cart['addresses']['invoice']->id,
'phone' => $phone,
'exception' => $exception,
]
);
}
}
$this->getPayload()->addAndMergeItems($node);
}
/**
* Build application context node
*
* NO_SHIPPING: The client can customize his address int the paypal pop-up (used in express checkout mode)
* SET_PROVIDED_ADDRESS: The address is provided by prestashop and the client
* cannot change/edit his address in the paypal pop-up
*/
public function buildApplicationContextNode()
{
$context = \Context::getContext();
/** @var \Ps_checkout $module */
$module = \Module::getInstanceByName('ps_checkout');
/** @var Router $router */
$router = $module->getService(Router::class);
$node['application_context'] = [
'brand_name' => \Configuration::get(
'PS_SHOP_NAME',
null,
null,
(int) $context->shop->id
),
'shipping_preference' => $this->expressCheckout ? 'GET_FROM_FILE' : 'SET_PROVIDED_ADDRESS',
'return_url' => $router->getCheckoutValidateLink(),
'cancel_url' => $router->getCheckoutCancelLink(),
];
$this->getPayload()->addAndMergeItems($node);
}
/**
* Build the amount breakdown node
*/
public function buildAmountBreakdownNode()
{
$node = [];
$amountTotal = $this->cart['cart']['totals']['total_including_tax']['amount'];
$breakdownItemTotal = 0;
$breakdownTaxTotal = 0;
$breakdownShipping = $this->cart['cart']['shipping_cost'];
$breakdownHandling = 0;
$breakdownDiscount = 0;
foreach ($this->cart['products'] as $product => $value) {
$sku = '';
$totalWithoutTax = $value['total'];
$totalWithTax = $value['total_wt'];
$totalTax = $totalWithTax - $totalWithoutTax;
$quantity = $value['quantity'];
$unitPriceWithoutTax = $this->formatAmount($totalWithoutTax / $quantity);
$unitTax = $this->formatAmount($totalTax / $quantity);
$breakdownItemTotal += $unitPriceWithoutTax * $quantity;
$breakdownTaxTotal += $unitTax * $quantity;
if (false === empty($value['reference'])) {
$sku = $value['reference'];
}
if (false === empty($value['ean13'])) {
$sku = $value['ean13'];
}
if (false === empty($value['isbn'])) {
$sku = $value['isbn'];
}
if (false === empty($value['upc'])) {
$sku = $value['upc'];
}
$paypalItem = [];
$paypalItem['name'] = $this->truncate($value['name'], 127);
$paypalItem['description'] = false === empty($value['attributes']) ? $this->truncate($value['attributes'], 127) : '';
$paypalItem['sku'] = $this->truncate($sku, 127);
$paypalItem['unit_amount']['currency_code'] = $this->cart['currency']['iso_code'];
$paypalItem['unit_amount']['value'] = $unitPriceWithoutTax;
$paypalItem['tax']['currency_code'] = $this->cart['currency']['iso_code'];
$paypalItem['tax']['value'] = $unitTax;
$paypalItem['quantity'] = $quantity;
$paypalItem['category'] = $value['is_virtual'] === '1' ? 'DIGITAL_GOODS' : 'PHYSICAL_GOODS';
$node['items'][] = $paypalItem;
}
$node['amount']['breakdown'] = [
'item_total' => [
'currency_code' => $this->cart['currency']['iso_code'],
'value' => $this->formatAmount($breakdownItemTotal),
],
'shipping' => [
'currency_code' => $this->cart['currency']['iso_code'],
'value' => $this->formatAmount($breakdownShipping),
],
'tax_total' => [
'currency_code' => $this->cart['currency']['iso_code'],
'value' => $this->formatAmount($breakdownTaxTotal),
],
];
// set handling cost id needed -> principally used in case of gift_wrapping
if (!empty($this->cart['cart']['subtotals']['gift_wrapping']['amount'])) {
$breakdownHandling += $this->cart['cart']['subtotals']['gift_wrapping']['amount'];
}
$remainderValue = $amountTotal - $breakdownItemTotal - $breakdownTaxTotal - $breakdownShipping - $breakdownHandling;
// In case of rounding issue, if remainder value is negative we use discount value to deduct remainder and if remainder value is positive we use handling value to add remainder
if ($remainderValue < 0) {
$breakdownDiscount += abs($remainderValue);
} else {
$breakdownHandling += $remainderValue;
}
$node['amount']['breakdown']['discount'] = [
'currency_code' => $this->cart['currency']['iso_code'],
'value' => $this->formatAmount($breakdownDiscount),
];
$node['amount']['breakdown']['handling'] = [
'currency_code' => $this->cart['currency']['iso_code'],
'value' => $this->formatAmount($breakdownHandling),
];
$this->getPayload()->addAndMergeItems($node);
}
private function buildCardPaymentSourceNode()
{
/** @var \Ps_checkout $module */
$module = \Module::getInstanceByName('ps_checkout');
/** @var PayPalConfiguration $paypalConfiguration */
$paypalConfiguration = $module->getService(PayPalConfiguration::class);
$node = [
'payment_source' => [
'card' => [
'name' => $this->cart['addresses']['invoice']->firstname . ' ' . $this->cart['addresses']['invoice']->lastname,
'billing_address' => $this->getAddressPortable('invoice'),
],
],
];
if ($paypalConfiguration->is3dSecureEnabled()) {
$node['payment_source']['card']['attributes']['verification']['method'] = $paypalConfiguration->getHostedFieldsContingencies();
}
if ($this->paypalVaultId) {
unset($node['payment_source']['card']['billing_address']);
$node['payment_source']['card']['vault_id'] = $this->paypalVaultId;
}
if ($this->paypalCustomerId) {
$node['payment_source']['card']['attributes']['customer'] = [
'id' => $this->paypalCustomerId,
];
}
if ($this->savePaymentMethod) {
$node['payment_source']['card']['attributes']['vault'] = [
'store_in_vault' => 'ON_SUCCESS',
];
}
$this->getPayload()->addAndMergeItems($node);
}
private function buildSupplementaryDataNode()
{
$payload = $this->getPayload()->getArray();
$node = [
'supplementary_data' => [
'card' => [
'level_2' => [
'tax_total' => $payload['amount']['breakdown']['tax_total'],
],
'level_3' => [
'shipping_amount' => $payload['amount']['breakdown']['shipping'],
'duty_amount' => [
'currency_code' => $payload['amount']['currency_code'],
'value' => $payload['amount']['value'],
],
'discount_amount' => $payload['amount']['breakdown']['discount'],
'shipping_address' => $this->getAddressPortable('shipping'),
'line_items' => $payload['items'],
],
],
],
];
$this->getPayload()->addAndMergeItems($node);
}
/**
* @param "shipping"|"invoice" $addressType
*
* @return string[]
*/
private function getAddressPortable($addressType)
{
$countryCodeMatrice = new PaypalCountryCodeMatrice();
$address = $this->cart['addresses'][$addressType];
$payerCountryIsoCode = $this->getCountryIsoCodeById($address->id_country);
return array_filter([
'address_line_1' => $address->address1,
'address_line_2' => $address->address2,
'admin_area_1' => $this->getStateNameById($address->id_state),
'admin_area_2' => $address->city,
'country_code' => $countryCodeMatrice->getPaypalIsoCode($payerCountryIsoCode),
'postal_code' => $address->postcode,
]);
}
/**
* Function that allow to truncate fields to match the
* paypal api requirements
*
* @param string $str
* @param int $limit
*
* @return string
*/
private function truncate($str, $limit)
{
if (empty($str)) {
return (string) $str;
}
return mb_substr($str, 0, $limit);
}
/**
* Get decimal to round correspondent to the payment currency used
* Advise from PayPal: Always round to 2 decimals except for HUF, JPY and TWD
* currencies which require a round with 0 decimal
*
* @return int
*/
private function getNbDecimalToRound()
{
if (in_array($this->cart['currency']['iso_code'], ['HUF', 'JPY', 'TWD'], true)) {
return 0;
}
return 2;
}
/**
* @param float|int|string $amount
*
* @return string
*/
private function formatAmount($amount)
{
return sprintf("%01.{$this->getNbDecimalToRound()}F", $amount);
}
/**
* Adapter method retrieving a state name from an ID
*
* @param int $stateId State ID
*
* @return string State name
*/
private function getStateNameById($stateId)
{
return \State::getNameById($stateId);
}
/**
* Use the core to retrieve a country ISO code from its ID
*
* @param int $countryId Country ID
*
* @return string Country ISO code
*/
private function getCountryIsoCodeById($countryId)
{
return strtoupper(\Country::getIsoById($countryId));
}
/**
* @throws PsCheckoutException
*/
private function checkPaypalOrderIdWhenUpdate()
{
if (true === $this->isUpdate && empty($this->paypalOrderId)) {
throw new PsCheckoutException('PayPal order ID is required when building payload for update an order', PsCheckoutException::PAYPAL_ORDER_IDENTIFIER_MISSING);
}
}
/**
* Setter $expressCheckout
*
* @param bool $expressCheckout
*/
public function setExpressCheckout($expressCheckout)
{
$this->expressCheckout = $expressCheckout;
}
/**
* Setter $isUpdate
*
* @param bool $bool
*/
public function setIsUpdate($bool)
{
$this->isUpdate = $bool;
}
/**
* Setter $paypalOrderId
*
* @param string $id
*/
public function setPaypalOrderId($id)
{
$this->paypalOrderId = $id;
}
/**
* @return bool
*/
public function isCard()
{
return $this->isCard;
}
/**
* @param bool $isCard
*/
public function setIsCard($isCard)
{
$this->isCard = $isCard;
}
/**
* Getter $paypalOrderId
*/
public function getPaypalOrderId()
{
return $this->paypalOrderId;
}
/**
* Getter $expressCheckout
*/
public function getExpressCheckout()
{
return $this->expressCheckout;
}
private function buildPayPalPaymentSourceNode()
{
$data = [];
if ($this->paypalVaultId) {
return;
// $data['vault_id'] = $this->paypalVaultId;
}
if ($this->paypalCustomerId) {
$data['attributes']['customer'] = [
'id' => $this->paypalCustomerId,
];
}
if ($this->savePaymentMethod) {
$data['attributes']['vault'] = [
'store_in_vault' => 'ON_SUCCESS',
'usage_pattern' => 'IMMEDIATE',
'usage_type' => 'MERCHANT',
'customer_type' => 'CONSUMER',
'permit_multiple_payment_tokens' => false,
];
}
if (empty($data)) {
return;
}
$node = [
'payment_source' => [
'paypal' => $data,
],
];
$this->getPayload()->addAndMergeItems($node);
}
private function buildGooglePayPaymentSourceNode()
{
/** @var \Ps_checkout $module */
$module = \Module::getInstanceByName('ps_checkout');
/** @var PayPalConfiguration $paypalConfiguration */
$paypalConfiguration = $module->getService(PayPalConfiguration::class);
if (!$paypalConfiguration->is3dSecureEnabled()) {
return;
}
$node = [
'payment_source' => [
'google_pay' => [
'attributes' => [
'verification' => [
'method' => $paypalConfiguration->getHostedFieldsContingencies(),
],
],
],
],
];
$this->getPayload()->addAndMergeItems($node);
}
}