Files
Nikolai Fesenko 8d227c9191 First upload
2025-02-02 13:37:56 +01:00

366 lines
14 KiB
PHP

<?php
/**
* This file is part of O3-Shop Paypal module.
*
* O3-Shop is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* O3-Shop is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with O3-Shop. If not, see <http://www.gnu.org/licenses/>
*
* @copyright Copyright (c) 2022 OXID eSales AG (https://www.oxid-esales.com)
* @copyright Copyright (c) 2022 O3-Shop (https://www.o3-shop.com)
* @license https://www.gnu.org/licenses/gpl-3.0 GNU General Public License 3 (GPLv3)
*/
namespace OxidEsales\PayPalModule\Model;
use OxidEsales\Eshop\Application\Model\Basket;
use OxidEsales\Eshop\Application\Model\User;
use OxidEsales\Eshop\Application\Model\User as EshopUserModel;
use OxidEsales\Eshop\Application\Model\UserBasket as EshopUserBasketModel;
use OxidEsales\Eshop\Application\Model\DeliverySetList as EshopDeliverySetListModel;
use OxidEsales\Eshop\Core\Registry;
use OxidEsales\Eshop\Core\Exception\ArticleException as EshopArticleException;
use OxidEsales\Eshop\Core\Exception\StandardException as EshopStandardException;
use OxidEsales\PayPalModule\Core\Config as PayPalConfig;
use OxidEsales\PayPalModule\Core\Exception\PayPalException;
use OxidEsales\PayPalModule\Core\PayPalService;
use OxidEsales\PayPalModule\Model\PayPalRequest\GetExpressCheckoutDetailsRequestBuilder;
use OxidEsales\PayPalModule\Model\PayPalRequest\SetExpressCheckoutRequestBuilder;
use OxidEsales\PayPalModule\Model\Response\ResponseGetExpressCheckoutDetails;
use OxidEsales\PayPalModule\Model\Response\ResponseSetExpressCheckout;
use OxidEsales\PayPalModule\Core\PayPalCheckValidator;
/**
* Class \OxidEsales\PayPalModule\Model\PaymentManager.
*/
class PaymentManager
{
public const PAYPAL_SERVICE_TYPE_STANDARD = 1;
public const PAYPAL_SERVICE_TYPE_EXPRESS = 2;
/** @var PayPalService */
private $payPalService;
/** @var PayPalConfig */
private $payPalConfig;
public function __construct(PayPalService $payPalService)
{
$this->payPalService = $payPalService;
$this->payPalConfig = oxNew(PayPalConfig::class);
}
public function setStandardCheckout(
Basket $basket,
?User $user,
string $returnUrl,
string $cancelUrl,
bool $showCartInPayPal,
string $deliveryAddressId
): ResponseSetExpressCheckout {
$builder = oxNew(SetExpressCheckoutRequestBuilder::class);
if ($deliveryAddressId && $user) {
$user->setSelectedAddressId($deliveryAddressId);
$basket->setUser($user);
}
$basket->setPayment("oxidpaypal");
$basket->onUpdate();
$basket->calculateBasket(true);
$this->validatePayment($user, $basket);
$builder->setPayPalConfig($this->payPalConfig);
$builder->setBasket($basket);
$builder->setUser($user);
$builder->setReturnUrl($returnUrl);
$builder->setCancelUrl($cancelUrl);
$showCartInPayPal = $showCartInPayPal && !$basket->isFractionQuantityItemsPresent();
$builder->setShowCartInPayPal($showCartInPayPal);
$builder->setTransactionMode($this->getTransactionMode($basket, $this->payPalConfig));
$request = $builder->buildStandardCheckoutRequest();
return $this->payPalService->setExpressCheckout($request);
}
public function setExpressCheckout(
Basket $basket,
?User $user,
string $returnUrl,
string $cancelUrl,
string $callbackUrl,
bool $showCartInPayPal,
string $shippingId = ''
): ResponseSetExpressCheckout
{
$builder = oxNew(SetExpressCheckoutRequestBuilder::class);
$basket->setPayment('oxidpaypal');
$basket->setShipping($shippingId);
//calculate basket
$mobileDefaultShippingId = $this->payPalConfig->getMobileECDefaultShippingId();
$prevOptionValue = Registry::getConfig()->getConfigParam('blCalculateDelCostIfNotLoggedIn');
Registry::getConfig()->setConfigParam('blCalculateDelCostIfNotLoggedIn', false);
if (!$this->payPalConfig->isDeviceMobile()) {
$builder->setCallBackUrl($callbackUrl);
$builder->setMaxDeliveryAmount($this->payPalConfig->getMaxPayPalDeliveryAmount());
} elseif (!empty($mobileDefaultShippingId) && ($shippingId === $mobileDefaultShippingId)) {
Registry::getConfig()->setConfigParam('blCalculateDelCostIfNotLoggedIn', true);
}
$basket->onUpdate();
$basket->calculateBasket(true);
Registry::getConfig()->setConfigParam('blCalculateDelCostIfNotLoggedIn', $prevOptionValue);
$this->validatePayment($user, $basket, true);
$builder->setPayPalConfig($this->payPalConfig);
$builder->setBasket($basket);
$builder->setUser($user);
$builder->setReturnUrl($returnUrl);
$builder->setCancelUrl($cancelUrl);
$showCartInPayPal = $showCartInPayPal && !$basket->isFractionQuantityItemsPresent();
$builder->setShowCartInPayPal($showCartInPayPal);
$builder->setTransactionMode($this->getTransactionMode($basket, $this->payPalConfig));
$request = $builder->buildExpressCheckoutRequest();
return $this->payPalService->setExpressCheckout($request);
}
public function getExpressCheckoutDetails(?string $token = null): ResponseGetExpressCheckoutDetails
{
$builder = oxNew(GetExpressCheckoutDetailsRequestBuilder::class);
if ($token) {
$builder->setToken($token);
}
$request = $builder->buildRequest();
return $this->payPalService->getExpressCheckoutDetails($request);
}
public function validatePayment(?User $user, Basket $basket, bool $isExpressCheckout = false): void
{
$validator = oxNew(PaymentValidator::class);
$validator->setUser($user);
$validator->setConfig(Registry::getConfig());
$validator->setPrice($basket->getPrice()->getPrice());
if ($isExpressCheckout) {
$validator->setCheckCountry(false);
}
if (!$validator->isPaymentValid()) {
$message = Registry::getLang()->translateString("OEPAYPAL_PAYMENT_NOT_VALID");
throw oxNew(PayPalException::class, $message);
}
}
public function getTransactionMode(Basket $basket, PayPalConfig $payPalConfig): string
{
$transactionMode = $payPalConfig->getTransactionMode();
if ($transactionMode == "Automatic") {
$outOfStockValidator = new OutOfStockValidator();
$outOfStockValidator->setBasket($basket);
$outOfStockValidator->setEmptyStockLevel($payPalConfig->getEmptyStockLevel());
$transactionMode = ($outOfStockValidator->hasOutOfStockArticles()) ? "Authorization" : "Sale";
return $transactionMode;
}
return $transactionMode;
}
public function validateApprovedBasketAmount(float $currentAmount, float $approvedAmount): bool
{
$payPalCheckValidator = oxNew(PayPalCheckValidator::class);
$payPalCheckValidator->setNewBasketAmount($currentAmount);
$payPalCheckValidator->setOldBasketAmount($approvedAmount);
return $payPalCheckValidator->isPayPalCheckValid();
}
public function initializeUserData(ResponseGetExpressCheckoutDetails $details, string $authenticatedUserId): EshopUserModel
{
$userEmail = $details->getEmail();
/** @var EshopUserModel $user */
$authenticatedUser = oxNew(EshopUserModel::class);
$authenticatedUserExists = false;
if ($authenticatedUser->load($authenticatedUserId)) {
$userEmail = $authenticatedUser->getFieldData('oxusername');
$authenticatedUserExists = true;
}
$user = oxNew(EshopUserModel::class);
if ($userId = $user->isRealPayPalUser($userEmail)) {
// if user exist
$user->load($userId);
if (!$authenticatedUserExists) {
if ($user->hasNoInvoiceAddress()) {
//this can only happen when user was registered via graphql, is anonymous and did not yet set invoice data
$user->setInvoiceDataFromPayPalResult($details);
} elseif (!$user->isSamePayPalUser($details)) {
$exception = new EshopStandardException();
$exception->setMessage('OEPAYPAL_ERROR_USER_ADDRESS');
throw $exception;
}
} elseif ($user->hasNoInvoiceAddress()) {
//this can only happen when user was registered via graphql, is logged in and did not yet set invoice data
$user->setInvoiceDataFromPayPalResult($details);
} elseif (!$user->isSameAddressUserPayPalUser($details) || !$user->isSameAddressPayPalUser($details)) {
// user has selected different address in PayPal (not equal with usr shop address)
// so adding PayPal address as new user address to shop user account
$address = oxNew(\OxidEsales\Eshop\Application\Model\Address::class);
$address->createPayPalAddress($details, $userId);
$user->setSelectedAddressId($address->getId());
} else {
// user uses billing address for shipping
$user->setSelectedAddressId(null);
}
} else {
$user->setId($authenticatedUserId);
$user->createPayPalUser($details);
$user->load($authenticatedUserId);
}
$user->setAnonymousUserId($authenticatedUserId);
return $user;
}
public function extractShippingId(
string $shippingOptionName,
?EshopUserModel $user = null,
array $deliverySetList = null
): ?string
{
$result = null;
$shippingOptionName = $this->reencodeHtmlEntities($shippingOptionName);
$name = trim(str_replace(Registry::getLang()->translateString("OEPAYPAL_PRICE"), "", $shippingOptionName));
if (!$deliverySetList) {
$delSetList = $this->getDeliverySetList($user);
$deliverySetList = $this->makeUniqueNames($delSetList);
}
if (is_array($deliverySetList)) {
$flipped = array_flip($deliverySetList);
$result = $flipped[$name];
}
return $result;
}
public function getDeliverySetList(EshopUserModel $user): array
{
$delSetList = oxNew(EshopDeliverySetListModel::class);
return $delSetList->getDeliverySetList($user, $this->getUserShippingCountryId($user));
}
public function getUserShippingCountryId(EshopUserModel $user): string
{
if ($user->getSelectedAddressId() && $user->getSelectedAddress()) {
$countryId = $user->getSelectedAddress()->getFieldData('oxcountryid');
} else {
$countryId = (string) $user->getFieldData('oxcountryid');
}
return $countryId;
}
public function reencodeHtmlEntities(string $input): string
{
$charset = $this->payPalConfig->getCharset();
return htmlentities(html_entity_decode($input, ENT_QUOTES, $charset), ENT_QUOTES, $charset);
}
/**
* @var EshopDeliverySetListModel[] $deliverySetList
*/
public function makeUniqueNames(array $deliverySetList): array
{
$result = [];
$nameCounts = [];
foreach ($deliverySetList as $deliverySet) {
$deliverySetName = trim($deliverySet->oxdeliveryset__oxtitle->value);
if (isset($nameCounts[$deliverySetName])) {
$nameCounts[$deliverySetName] += 1;
} else {
$nameCounts[$deliverySetName] = 1;
}
$suffix = ($nameCounts[$deliverySetName] > 1) ? " (" . $nameCounts[$deliverySetName] . ")" : '';
$result[$deliverySet->oxdeliveryset__oxid->value] = $this->reencodeHtmlEntities($deliverySetName . $suffix);
}
return $result;
}
public function prepareCallback(string $basketId): Basket
{
$sessionBasket = oxNew(Basket::class);
$userBasket = oxNew(EshopUserBasketModel::class);
if (!$userBasket->load($basketId)) {
return $sessionBasket;
}
foreach ($userBasket->getItems() as $basketItem) {
try {
$selectList = $basketItem->getSelList();
$sessionBasket->addToBasket(
$basketItem->getFieldData('oxartid'),
$basketItem->getFieldData('oxamount'),
$selectList,
$basketItem->getPersParams(),
true
);
} catch (EshopArticleException $exception) {
// caught and ignored
}
}
//calculate the basket
Registry::getConfig()->setConfigParam('blCalculateDelCostIfNotLoggedIn', true);
if ($userBasket->getFieldData('OEGQL_DELIVERYMETHODID')) {
$sessionBasket->setShipping($userBasket->getFieldData('OEGQL_DELIVERYMETHODID'));
}
$sessionBasket->calculateBasket(true);
return $sessionBasket;
}
/**
* We do not have the session stored when using the graphql API.
* Callback needs to use the basket id instead.
*/
public function getGraphQLCallBackUrl(string $basketId): string
{
return Registry::getConfig()->getSslShopUrl() . "index.php?lang=" . Registry::getLang()->getBaseLanguage() .
"&basketid=" . $basketId .
"&shp=" . Registry::getConfig()->getShopId() .
"&cl=oepaypalexpresscheckoutdispatcher&fnc=processGraphQLCallBack";
}
}