Current File : /var/www/prestashop/modules/ps_checkout/src/Builder/Payload/CreateOrderPayloadBuilder.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;

/**
 * Build the payload for Create PayPal Order
 */
class CreateOrderPayloadBuilder extends Builder
{
    /**
     * @var array
     */
    private $data;

    public function __construct(array $data)
    {
        parent::__construct();
        $this->data = $data;
    }

    /**
     * Build payload with cart details
     */
    public function buildFullPayload()
    {
        parent::buildFullPayload();

        $this->buildBaseNode();
        $this->buildAmountBreakdownNode();

        if (empty($this->data['ps_checkout']['isExpressCheckout'])) {
            $this->buildShippingNode();

            if (empty($this->data['ps_checkout']['isUpdate'])) {
                $this->buildPayerNode();
            }
        }

        if (empty($this->data['ps_checkout']['isUpdate'])) {
            $this->buildApplicationContextNode();
        }

        if ($this->data['ps_checkout']['isCard']) {
            $this->buildPaymentSourceNode();
            $this->buildSupplementaryDataNode();
        }
    }

    /**
     * Build payload without cart details
     */
    public function buildMinimalPayload()
    {
        parent::buildMinimalPayload();

        $this->buildBaseNode();

        if (empty($this->data['ps_checkout']['isExpressCheckout'])) {
            $this->buildShippingNode();

            if (empty($this->data['ps_checkout']['isUpdate'])) {
                $this->buildPayerNode();
            }
        }

        if (empty($this->data['ps_checkout']['isUpdate'])) {
            $this->buildApplicationContextNode();
        }
    }

    /**
     * Build the basic payload
     */
    public function buildBaseNode()
    {
        $node = [
            'intent' => $this->data['ps_checkout']['intent'],
            'custom_id' => (string) $this->data['cart']['id'],
            'invoice_id' => '',
            'description' => $this->truncate(
                'Checking out with your cart ' . $this->data['cart']['id'] . ' from ' . $this->data['shop']['name'],
                127
            ),
            'amount' => [
                'currency_code' => $this->data['currency']['iso_code'],
                'value' => $this->formatAmount($this->data['totalWithTaxes']),
            ],
            'payee' => [
                'merchant_id' => $this->data['ps_checkout']['merchant_id'],
            ],
        ];

        if (empty($this->data['ps_checkout']['isUpdate']) && !empty($this->data['ps_checkout']['token'])) {
            $node['token'] = $this->data['ps_checkout']['token'];
        }

        if (empty($this->data['ps_checkout']['isUpdate'])) {
            $node['roundingConfig'] = $this->data['ps_checkout']['roundType'] . '-' . $this->data['ps_checkout']['roundMode'];
        }

        $this->getPayload()->addAndMergeItems($node);
    }

    /**
     * Build shipping node
     */
    public function buildShippingNode()
    {
        $node['shipping'] = [
            'name' => [
                'full_name' => trim(
                    (!empty($this->data['deliveryAddress']['firstname']) ? $this->data['deliveryAddress']['firstname'] : '')
                    . ' '
                    . (!empty($this->data['deliveryAddress']['lastname']) ? $this->data['deliveryAddress']['lastname'] : '')
                ),
            ],
            'address' => $this->getAddressPortable('deliveryAddress'),
        ];

        $this->getPayload()->addAndMergeItems($node);
    }

    /**
     * Build payer node
     */
    public function buildPayerNode()
    {
        $node['payer'] = [
            'name' => [
                'given_name' => !empty($this->data['invoiceAddress']['firstname']) ? $this->data['invoiceAddress']['firstname'] : '',
                'surname' => !empty($this->data['invoiceAddress']['lastname']) ? $this->data['invoiceAddress']['lastname'] : '',
            ],
            'email_address' => !empty($this->data['customer']['email']) ? $this->data['customer']['email'] : '',
            'address' => $this->getAddressPortable('invoiceAddress'),
        ];

        // Add optional birthdate if provided
        if (!empty($this->data['customer']['birthday']) && $this->data['customer']['birthday'] !== '0000-00-00') {
            $node['payer']['birth_date'] = $this->data['customer']['birthday'];
        }

        $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()
    {
        $node['application_context'] = [
            'brand_name' => $this->data['shop']['name'],
            'shipping_preference' => empty($this->data['ps_checkout']['isExpressCheckout']) ? 'SET_PROVIDED_ADDRESS' : 'GET_FROM_FILE',
        ];

        $this->getPayload()->addAndMergeItems($node);
    }

    /**
     * Build the amount breakdown node
     */
    public function buildAmountBreakdownNode()
    {
        $node = [];
        $amountTotal = $this->data['totalWithTaxes'];
        $breakdownItemTotal = 0;
        $breakdownTaxTotal = 0;
        $breakdownShipping = $this->data['totalShippingWithTaxes'];
        $breakdownHandling = 0;
        $breakdownDiscount = 0;

        foreach ($this->data['products'] as $product) {
            $sku = '';
            $totalWithoutTax = $product['total'];
            $totalWithTax = $product['total_wt'];
            $totalTax = $totalWithTax - $totalWithoutTax;
            $quantity = $product['quantity'];
            $unitPriceWithoutTax = $this->formatAmount($totalWithoutTax / $quantity);
            $unitTax = $this->formatAmount($totalTax / $quantity);
            $breakdownItemTotal += $unitPriceWithoutTax * $quantity;
            $breakdownTaxTotal += $unitTax * $quantity;

            if (false === empty($product['reference'])) {
                $sku = $product['reference'];
            }

            if (false === empty($product['ean13'])) {
                $sku = $product['ean13'];
            }

            if (false === empty($product['isbn'])) {
                $sku = $product['isbn'];
            }

            if (false === empty($product['upc'])) {
                $sku = $product['upc'];
            }

            $paypalItem = [];
            $paypalItem['name'] = $this->truncate($product['name'], 127);
            $paypalItem['description'] = false === empty($product['attributes']) ? $this->truncate($product['attributes'], 127) : '';
            $paypalItem['sku'] = $this->truncate($sku, 127);
            $paypalItem['unit_amount']['currency_code'] = $this->data['currency']['iso_code'];
            $paypalItem['unit_amount']['value'] = $unitPriceWithoutTax;
            $paypalItem['tax']['currency_code'] = $this->data['currency']['iso_code'];
            $paypalItem['tax']['value'] = $unitTax;
            $paypalItem['quantity'] = $quantity;
            $paypalItem['category'] = $product['is_virtual'] === '1' ? 'DIGITAL_GOODS' : 'PHYSICAL_GOODS';

            $node['items'][] = $paypalItem;
        }

        $node['amount']['breakdown'] = [
            'item_total' => [
                'currency_code' => $this->data['currency']['iso_code'],
                'value' => $this->formatAmount($breakdownItemTotal),
            ],
            'shipping' => [
                'currency_code' => $this->data['currency']['iso_code'],
                'value' => $this->formatAmount($breakdownShipping),
            ],
            'tax_total' => [
                'currency_code' => $this->data['currency']['iso_code'],
                'value' => $this->formatAmount($breakdownTaxTotal),
            ],
        ];

        // set handling cost id needed -> principally used in case of gift_wrapping
        if (!empty($this->data['totalGiftWrappingWithTaxes'])) {
            $breakdownHandling += $this->data['totalGiftWrappingWithTaxes'];
        }

        $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->data['currency']['iso_code'],
            'value' => $this->formatAmount($breakdownDiscount),
        ];

        $node['amount']['breakdown']['handling'] = [
            'currency_code' => $this->data['currency']['iso_code'],
            'value' => $this->formatAmount($breakdownHandling),
        ];

        $this->getPayload()->addAndMergeItems($node);
    }

    private function buildPaymentSourceNode()
    {
        $node = [
            'payment_source' => [
                'card' => [
                    'name' => $this->data['invoiceAddress']['firstname'] . ' ' . $this->data['invoiceAddress']['lastname'],
                    'billing_address' => $this->getAddressPortable('invoiceAddress'),
                    'attributes' => [
                        'verification' => [
                            'method' => $this->data['ps_checkout']['3DS'],
                        ],
                    ],
                ],
            ],
        ];

        $this->getPayload()->addAndMergeItems($node);
    }

    private function buildSupplementaryDataNode()
    {
        $payload = $this->getPayload()->getArray();
        $node = [
            'supplementary_data' => [
                'card' => [
                    'level_2' => [
//                        'invoice_id' => '',
                        '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('deliveryAddress'),
                        'line_items' => $payload['items'],
                    ],
                ],
            ],
        ];

        $this->getPayload()->addAndMergeItems($node);
    }

    /**
     * @param "deliveryAddress"|"invoiceAddress" $addressType
     *
     * @return string[]
     */
    private function getAddressPortable($addressType)
    {
        $address = $this->data[$addressType];

        return [
            'address_line_1' => !empty($address['address1']) ? $address['address1'] : '',
            'address_line_2' => !empty($address['address2']) ? $address['address2'] : '',
            'admin_area_1' => !empty($this->data["{$addressType}State"]['name']) ? $this->data["{$addressType}State"]['name'] : '',
            'admin_area_2' => !empty($address['city']) ? $address['city'] : '',
            'country_code' => !empty($this->data["{$addressType}Country"]['iso_code']) ? $this->data["{$addressType}Country"]['iso_code'] : '',
            'postal_code' => !empty($address['postcode']) ? $address['postcode'] : '',
        ];
    }

    /**
     * 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->data['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);
    }

    /**
     * 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)
    {
        return mb_substr($str, 0, $limit);
    }
}