-
-
Save cleggypdc/2a4cea8544a9707e578c2043e393509c to your computer and use it in GitHub Desktop.
Pimcore Stripe Payment Provider (From Pimcore4 needs porting to bundle)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Service.php | |
* | |
* This source file is subject to the GNU General Public License version 3 (GPLv3) | |
* For the full copyright and license information, please view the LICENSE.md | |
* file distributed with this source code. | |
* | |
* @copyright Copyright (c) 2012-2021 Gather Digital Ltd (https://www.gatherdigital.co.uk) | |
* @license https://www.gatherdigital.co.uk/license GNU General Public License version 3 (GPLv3) | |
* @author Paul Clegg | |
*/ | |
namespace StripePaymentProvider; | |
use StripePaymentProvider\Interfaces\ICustomer; | |
use StripePaymentProvider\Interfaces\IStripePlan; | |
use \OnlineShop\Framework\PriceSystem\IPrice; | |
use \Pimcore\Log; | |
use \Stripe as StripeLib; | |
class Service | |
{ | |
/** | |
* @var $payment StripePaymentProvider | |
*/ | |
private $payment; | |
private $logger; | |
public function __construct(StripePaymentProvider $payment) | |
{ | |
$this->payment = $payment; | |
$this->logger = Log\ApplicationLogger::getInstance('stripe-payment-service', true); | |
//init the API | |
StripeLib\Stripe::setApiKey($this->payment->getCurrentApiConfig()->secretKey); | |
} | |
public function translateTokenToStripeChargeDetail($stripeToken, $customer) | |
{ | |
// check for customers with same stripe config | |
$stripeCustomerRef = $customer->getStripeCustomer($this->payment->getMode(), $this->payment->getStripeAccountName()); | |
if (!$stripeCustomerRef) { | |
// create a new customer for this user (uses the token) | |
$stripeCustomer = $this->createCustomer($stripeToken, $customer->getEmail()); | |
$customer->addStripeCustomer($this->payment->getMode(), $this->payment->getStripeAccountName(), $stripeCustomer, $stripeToken); | |
$customer->save(); | |
return ['stripeCustomerId' => $stripeCustomer->id, 'stripePaymentSource' => null, 'customerId' => $customer->getId()]; | |
} | |
// callback to handle setting the default card | |
$stripeCustomer = $this->retrieveCustomer($stripeCustomerRef); //the customer ID | |
// if stripe token is actually a string , we passed a card id to initPayment - so the user has opted to use their default card | |
if (is_string($stripeToken)) { | |
return ['stripeCustomerId' => $stripeCustomer->id, 'stripePaymentSource' => $stripeToken, 'customerId' => $customer->getId()]; | |
} | |
$isSubscription = $this->payment->getAuthorizedData()['paymentType'] == 'subscription'; | |
$checkShouldUpdateDefaultCard = function(&$customer, &$token) | |
use ($isSubscription) { | |
if ($isSubscription) { | |
// force set default source to the latest one the user added | |
$cards = $customer->sources->all(['object' => 'card']); | |
foreach ($cards->data as $card) { | |
if ($card->fingerprint === $token->card->fingerprint) { | |
$customer->default_source = $card->id; | |
$customer->save(); | |
} | |
} | |
} | |
}; | |
//check if the card exists in customer account. | |
/** | |
* @var $cards StripeLib\Collection | |
*/ | |
$match=false; | |
$cards = $stripeCustomer->sources->all(["object" => "card"]); | |
foreach ($cards->data as $source) { | |
if ($source->fingerprint == $stripeToken->card->fingerprint) { | |
$match = $source->id; | |
} | |
} | |
// if the card matches the card used in the customer account then return the customer with the token ID | |
if ($match) { | |
$checkShouldUpdateDefaultCard($stripeCustomer, $stripeToken); | |
return ['stripeCustomerId' => $stripeCustomer->id, 'stripePaymentSource' => $match, 'customerId' => $customer->getId()]; | |
} | |
// if there isn't a card saved with the correct id, then we should have the token ready to use to add a new card | |
// otherwise we have some token problems | |
if ($stripeToken->used) { | |
throw new \Exception('Token has been used, card detail retrieval error'); | |
} | |
// create a new card for the customer | |
$newCard = $stripeCustomer->sources->create(['source' => $stripeToken->id]); | |
if (!$newCard) { | |
throw new \Exception('Could not create a new card'); | |
} | |
$checkShouldUpdateDefaultCard($stripeCustomer, $stripeToken); | |
return ['stripeCustomerId' => $stripeCustomer->id, 'stripePaymentSource' => $newCard->id, 'customerId' => $customer->getId()]; | |
} | |
/** | |
* Returns a plan for this recurring payment | |
* @param $authData | |
* @return int | |
*/ | |
public function translatePaymentAuthDataToStripePlan($authData, IPrice $price, $items) | |
{ | |
/** | |
* @var $className IStripePlan|\Pimcore\Model\Object\Concrete | |
*/ | |
$className = $this->payment->stripePlanClassName; | |
// check for already existing config | |
if (!empty($authData['planId']) && $className::getById($authData['planId'])) { | |
return $authData['planId']; | |
} | |
// create a new plan if needed | |
$intervalCount = (int)($authData['intervalCount'] ?: 1); | |
// determine the plan ID from the auth data | |
$stripePlanId = Tool::getValidStripePlanId($price, $authData['interval'], $intervalCount, $this->payment); | |
$planName = Tool::getValidStripePlanName($price, $authData['interval'], $intervalCount, $this->payment); | |
$amount = Tool::getStripeAmount($price); | |
$currency = $price->getCurrency()->getShortName(); | |
$description = ''; | |
$adopt = ''; | |
$itemName = []; | |
if ($items) { | |
foreach ($items as $item) { | |
if ($item->getProduct()->getProductType() === \Website\DefaultProduct::PRODUCT_TYPE_DONATION) { | |
$description = $item->getProduct()->getName(); | |
} else if ($item->getProduct()->getProductType() === \Website\DefaultProduct::PRODUCT_TYPE_ADOPTION) { | |
$adopt = 'Adopt'; | |
$itemName[] = $item->getProduct()->getName(); | |
} | |
} | |
if ($adopt) { | |
$description = $adopt . ' ' . implode(",", $itemName) . ' ' . $item->getProduct()->getSubscriptionType() . 'ly'; | |
} | |
} | |
// check if the plan exists in Stripe already. | |
$stripePlan = $this->tryApiCall('retrieve-plan', [ | |
'id' => $stripePlanId | |
]); | |
// if it doesn't exist then create a new one. | |
if (!$stripePlan instanceof StripeLib\Plan) { | |
$stripePlan = $this->tryApiCall('create-plan', [ | |
'id' => $stripePlanId, | |
'amount' => $amount, | |
'currency' => $currency, | |
'interval' => $authData['interval'], | |
'name' => $planName, | |
/* 'nickname' => $description, */ // TODO: this is not a valid param. /some idiot/ added it without testing or reading the documentation and assumed it would work - will be fixed in a later update | |
'interval_count' => $intervalCount | |
]); | |
} | |
if (!$stripePlan instanceof StripeLib\Plan) { | |
\Pimcore\Logger::crit('Could not create a stripe plan' . print_r([ | |
$stripePlanId | |
], true)); | |
throw new StripePaymentException('The payment provider could not create a plan for your subscription'); | |
} | |
// now check to make sure that the plan exists in Pimcore | |
$plan = $className::getByStripeIdAndApiStatus( | |
$stripePlanId, | |
$this->payment->getStripeAccountName(), $this->payment->isLivemode() | |
); | |
// or create one since no plan exists | |
if (!$plan) { | |
$this->createNewLocalStripePlan([ | |
'name' => $planName, | |
'stripeId' => $stripePlan->id, | |
'stripeAccount' => $this->payment->getStripeAccountName(), | |
'currency' => $currency, | |
'amount' => $amount, | |
'interval' => $authData['interval'], | |
'intervalCount' => $intervalCount, | |
'livemode' => $stripePlan->livemode | |
]); | |
} | |
return $stripePlan->id; | |
} | |
/** | |
* Provides an implementation of API calls that are retried in case of any network or API issues. | |
* | |
* @param string $call | |
* @param mixed $params | |
* @param string $idempotencyKey | |
* @param int $try | |
* @return \Stripe\ApiResource|\Exception|string | |
* @throws \Exception | |
*/ | |
public function tryApiCall($call, $params, $idempotencyKey = null, $try = 1) | |
{ | |
// check API key is setup first | |
if (!StripeLib\Stripe::getApiKey()) { | |
throw new \Exception('Could not initialise API without first setting the API key'); | |
} | |
// logging (anonymize emails and descriptions) | |
try { | |
$anonLog = array_filter($params, function ($value, $key) { | |
return !in_array(key, ['email', 'description']); | |
}, ARRAY_FILTER_USE_BOTH); | |
$this->logger->debug(sprintf( | |
'API: %s [params=%s, try=%d, scope=%s]', $call, json_encode($anonLog), $try, | |
$this->payment->getStripeAccountName() | |
)); | |
} catch(\Throwable $e) { | |
// in case of any application logging problems still allow the payment to continue. | |
} | |
if (!$idempotencyKey) { | |
$idempotencyKey = Tool::getIdempotencyKey(); | |
} | |
$options = [ | |
"idempotency_key" => $idempotencyKey | |
]; | |
//call stripe API | |
try { | |
if ($try > 4) { | |
$this->logger->error('API: Cannot process request (too many failed api attempts)'); | |
return 'Cannot process the request at this time.'; | |
} | |
switch($call) { | |
case 'charge' : | |
$result = StripeLib\Charge::create($params, $options); | |
break; | |
case 'retrieve-charge' : | |
$result = StripeLib\Charge::retrieve($params, $options); | |
break; | |
case 'retrieve-customer-charge' : | |
$result = StripeLib\Charge::all($params, $options); | |
break; | |
case 'retrieve-token' : | |
$result = StripeLib\Token::retrieve($params, $options); | |
break; | |
case 'create-customer' : | |
$result = StripeLib\Customer::create($params, $options); | |
break; | |
case 'retrieve-customer' : | |
$result = StripeLib\Customer::retrieve($params, $options); | |
break; | |
case 'create-plan' : | |
$result = StripeLib\Plan::create($params, $options); | |
break; | |
case 'retrieve-plan': | |
$result = StripeLib\Plan::retrieve($params, $options); | |
break; | |
case 'create-subscription' : | |
$result = StripeLib\Subscription::create($params, $options); | |
break; | |
case 'retrieve-subscription' : | |
$result = StripeLib\Subscription::retrieve($params, $options); | |
break; | |
case 'retrieve-customer-subscription' : | |
$result = StripeLib\Subscription::all($params, $options); | |
break; | |
case 'retrieve-invoice' : | |
$result = StripeLib\Invoice::retrieve($params, $options); | |
break; | |
case 'retrieve-customer-invoice' : | |
$result = StripeLib\Invoice::all($params, $options); | |
break; | |
default: | |
$result = new \Exception("Unspecified API call '{$call}'"); | |
break; | |
} | |
return $result; | |
} catch (StripeLib\Error\Card $e) { | |
$reason = $this->determineDeclineReason($e->getDeclineCode()); | |
$this->logger->info(sprintf('Card Declined: %s' . $reason)); | |
return 'Card Declined. ' . $reason; //card declined | |
} catch (StripeLib\Error\RateLimit $e) { | |
// Too many requests made to the API too quickly | |
sleep(pow($try, 2)); | |
return $this->tryApiCall($call, $params, null, $try + 1); | |
} catch (StripeLib\Error\InvalidRequest $e) { | |
$this->logger->error('InvalidRequest '.print_r($params,true)); | |
// Invalid parameters were supplied to Stripe's API | |
return $e; | |
} catch (StripeLib\Error\Authentication $e) { | |
// Authentication with Stripe's API failed | |
// (maybe you changed API keys recently) | |
$this->logger->error('AuthError '.print_r($params,true)); | |
return $e; | |
} catch (StripeLib\Error\ApiConnection $e) { | |
// Network communication with Stripe failed | |
return $this->tryApiCall($call, $params, $idempotencyKey, $try + 1); | |
} catch (StripeLib\Error\Base $e) { | |
// Display a very generic error to the user, and maybe send | |
// yourself an email | |
$this->logger->error('Stripe API Base error' . $e->getMessage()); | |
return 'Our payment provider is currently having problems, please try again later'; | |
} catch (\Exception $e) { | |
$this->logger->error('GenericException '.print_r($params,true)); | |
return $e; | |
} | |
} | |
/** | |
* @param StripeLib\Token $token | |
* @param string $email | |
* @param string $stripeAccount | |
* @return StripeLib\Customer | |
* @throws \Exception | |
*/ | |
public function createCustomer(StripeLib\Token $token, $email) | |
{ | |
$stripeAccount = $this->payment->getStripeAccountName(); | |
// first attempt to make a new stripe customer | |
$stripeCustomer = $this->tryApiCall('create-customer', [ | |
"source" => $token->id, | |
"email" => $email, | |
"description" => "Customer for {$email} -> {$stripeAccount}" | |
]); | |
if (!$stripeCustomer instanceof StripeLib\Customer) { | |
$message = (($stripeCustomer instanceof \Exception) ? $stripeCustomer->getMessage() : $stripeCustomer); | |
\Pimcore\Logger::crit('Could not create a customer' . print_r([ | |
$token->jsonSerialize(), | |
$email, | |
$stripeAccount, | |
$message | |
], true)); | |
throw new StripePaymentException($message); | |
} | |
if (!$stripeCustomer->default_source) { | |
\Pimcore\Logger::crit('Could not create a customer with a default payment method' . print_r([ | |
$stripeCustomer->id, | |
$token->jsonSerialize(), | |
$email, | |
$stripeAccount | |
], true)); | |
throw new StripePaymentException('The payment provider could not verify customer payment details'); | |
} | |
return $stripeCustomer; | |
} | |
public function retrieveCustomer($customerId) | |
{ | |
$stripeAccount = $this->payment->getStripeAccountName(); | |
$stripeCustomer = $this->tryApiCall('retrieve-customer', [ | |
'id' => $customerId | |
]); | |
if (!$stripeCustomer instanceof StripeLib\Customer) { | |
$message = (($stripeCustomer instanceof \Exception) ? $stripeCustomer->getMessage() : $stripeCustomer); | |
\Pimcore\Logger::crit('Could not retreive existing customer' . print_r([ | |
$customerId, | |
$stripeAccount, | |
$message | |
], true)); | |
throw new StripePaymentException($message); | |
} | |
return $stripeCustomer; | |
} | |
/** | |
* @param \OnlineShop\Framework\PriceSystem\IPrice $price | |
* @param mixed $authData | |
* @param int $intervalCount | |
* @return IStripePlan | |
* @throws \Exception | |
*/ | |
public function createNewLocalStripePlan($values) | |
{ | |
// create the local stripe plan | |
$classname = $this->payment->stripePlanClassName; | |
/** | |
* @var IStripePlan|\Pimcore\Model\Object\Concrete $localStripePlan | |
*/ | |
$localStripePlan = new $classname(); | |
$localStripePlan->setValues($values); | |
$localStripePlan->generateOwnKey(); | |
$localStripePlan->generateOwnParent(); | |
$localStripePlan->setPublished(true); | |
$localStripePlan->setOmitMandatoryCheck(true); | |
$localStripePlan->save(); | |
return $localStripePlan; | |
} | |
/** | |
* @param $id | |
* @return IStripePlan | |
*/ | |
public function getStripePlan($id) | |
{ | |
/** | |
* @var $classname \Pimcore\Model\Object\Concrete|IStripePlan | |
*/ | |
$classname = $this->payment->stripePlanClassName; | |
return $classname::getById($id); | |
} | |
public function getPayment(){ | |
return $this->payment; | |
} | |
public function determineDeclineReason($declineCode='') | |
{ | |
$reasons = [ | |
"approve_with_id" => "The payment cannot be authorized.", | |
"call_issuer" => "The card has been declined for an unknown reason.", | |
"card_not_supported" => "The card does not support this type of purchase.", | |
"card_velocity_exceeded" => "The customer has exceeded the balance or credit limit available on their card.", | |
"currency_not_supported" => "The card does not support the specified currency.", | |
"do_not_honor" => "The card has been declined for an unknown reason.", | |
"do_not_try_again" => "The card has been declined for an unknown reason.", | |
"duplicate_transaction" => "A transaction with identical amount and credit card information was submitted very recently.", | |
"expired_card" => "The card has expired.", | |
"fraudulent" => "The payment has been declined as Stripe suspects it is fraudulent.", | |
"generic_decline" => "The card has been declined for an unknown reason.", | |
"incorrect_number" => "The card number is incorrect.", | |
"incorrect_cvc" => "The CVC number is incorrect.", | |
"incorrect_pin" => "The PIN entered is incorrect. This decline code only applies to payments made with a card reader.", | |
"incorrect_zip" => "The ZIP/postal code is incorrect.", | |
"insufficient_funds" => "The card has insufficient funds to complete the purchase.", | |
"invalid_account" => "The card, or account the card is connected to, is invalid.", | |
"invalid_amount" => "The payment amount is invalid, or exceeds the amount that is allowed.", | |
"invalid_cvc" => "The CVC number is incorrect.", | |
"invalid_expiry_year" => "The expiration year invalid.", | |
"invalid_number" => "The card number is incorrect.", | |
"invalid_pin" => "The PIN entered is incorrect. This decline code only applies to payments made with a card reader.", | |
"issuer_not_available" => "The card issuer could not be reached, so the payment could not be authorized.", | |
"lost_card" => "The payment has been declined because the card is reported lost.", | |
"new_account_information_available" => "The card, or account the card is connected to, is invalid.", | |
"no_action_taken" => "The card has been declined for an unknown reason.", | |
"not_permitted" => "The payment is not permitted.", | |
"pickup_card" => "The card cannot be used to make th,is payment (it is possible it has been reported lost or stolen).", | |
"pin_try_exceeded" => "The allowable number of PIN tries has been exceeded.", | |
"processing_error" => "An error occurred while processing the card.", | |
"reenter_transaction" => "The payment could not be processed by the issuer for an unknown reason.", | |
"restricted_card" => "The card cannot be used to make this payment (it is possible it has been reported lost or stolen).", | |
"revocation_of_all_authorizations" => "The card has been declined for an unknown reason.", | |
"revocation_of_authorization" => "The card has been declined for an unknown reason.", | |
"security_violation" => "The card has been declined for an unknown reason.", | |
"service_not_allowed" => "The card has been declined for an unknown reason.", | |
"stolen_card" => "The payment has been declined because the card is reported stolen.", | |
"stop_payment_order" => "The card has been declined for an unknown reason.", | |
"testmode_decline" => "A Stripe test card number was used.", | |
"transaction_not_allowed" => "The card has been declined for an unknown reason.", | |
"try_again_later" => "The card has been declined for an unknown reason.", | |
"withdrawal_count_limit_exceeded" => "The customer has exceeded the balance or credit limit available on their card.", | |
]; | |
if (array_key_exists($declineCode, $reasons)) { | |
return $reasons[$declineCode]; | |
} | |
return $declineCode; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* StripePaymentException.php | |
* | |
* This source file is subject to the GNU General Public License version 3 (GPLv3) | |
* For the full copyright and license information, please view the LICENSE.md | |
* file distributed with this source code. | |
* | |
* @copyright Copyright (c) 2012-2021 Gather Digital Ltd (https://www.gatherdigital.co.uk) | |
* @license https://www.gatherdigital.co.uk/license GNU General Public License version 3 (GPLv3) | |
* @author Paul Clegg | |
*/ | |
namespace StripePaymentProvider; | |
class StripePaymentException extends \Exception | |
{ | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* StripePaymentProvider.php | |
* | |
* This source file is subject to the GNU General Public License version 3 (GPLv3) | |
* For the full copyright and license information, please view the LICENSE.md | |
* file distributed with this source code. | |
* | |
* @copyright Copyright (c) 2012-2021 Gather Digital Ltd (https://www.gatherdigital.co.uk) | |
* @license https://www.gatherdigital.co.uk/license GNU General Public License version 3 (GPLv3) | |
* @author Paul Clegg | |
*/ | |
namespace StripePaymentProvider; | |
use OnlineShop\Framework\PaymentManager\IStatus; | |
use \OnlineShop\Framework\PaymentManager\Payment\IPayment; | |
use \OnlineShop\Framework\PaymentManager\Status as PStatus; | |
use \OnlineShop\Framework\Model\AbstractOrder; | |
use \Stripe as StripeLib; | |
use StripePaymentProvider\Interfaces\ICustomer; | |
class StripePaymentProvider implements IPayment | |
{ | |
const CART_TYPE_CHARGE = 'charge'; | |
const CART_TYPE_SUBSCRIPTION = 'subscription'; | |
protected $stripeJsSrc = 'https://js.stripe.com/v2/'; | |
public $stripePlanClassName; | |
public $customerClassName; | |
/** | |
* @var \Zend_Config $apiConfigs | |
*/ | |
protected $apiConfigs; | |
protected $defaultConfigKey = 'default'; | |
protected $currentConfigKey; | |
protected $mode; | |
private $authorizedData; | |
/** | |
* @var $service Service | |
*/ | |
private $service; | |
/** | |
* @param \Zend_Config $config | |
*/ | |
public function __construct(\Zend_Config $config) | |
{ | |
$this->defaultConfigKey = $config->defaultConfig; | |
$this->currentConfigKey = $this->defaultConfigKey; | |
$this->apiConfigs = $config->config; | |
$this->mode = $config->mode; | |
$this->stripePlanClassName = $config->stripePlanClass; | |
$this->customerClassName = $config->customerClass; | |
if (empty($this->apiConfigs)) { | |
throw new \Exception('stripe payment configuration is wrong, no config'); | |
} | |
if (empty($this->apiConfigs->{$this->defaultConfigKey})) { | |
throw new \Exception('stripe payment configuration is wrong, invalid default config'); | |
} | |
if (empty($this->apiConfigs->{$this->defaultConfigKey}->{$this->mode})) { | |
throw new \Exception('stripe payment configuration is wrong, invalid API mode'); | |
} | |
if (!$this->getCurrentApiConfig() instanceof \Zend_Config) { | |
throw new \Exception('stripe payment configuration is wrong, invalid default api config'); | |
} | |
if (!method_exists($this->customerClassName, 'getByEmailAndCardFingerPrint')) { | |
throw new \Exception('stripe payment configuration is wrong, customer class must implement \\StripePaymentProvider\\Interfaces\\ICustomer'); | |
} | |
// enable Stripe Plan management | |
if ($this->stripePlanClassName) { | |
if (!method_exists($this->stripePlanClassName, 'getByStripeIdAndApiStatus')) { | |
throw new \Exception('stripe payment configuration is wrong, stripe class must implement \\StripePaymentProvider\\Interfaces\\IStripePlan'); | |
} | |
} | |
$this->initCurrentConfig(); | |
} | |
/** | |
* @return string | |
*/ | |
public function getStripeJsSrc() | |
{ | |
return $this->stripeJsSrc; | |
} | |
/** | |
* @return string | |
*/ | |
public function getPublishableKey() | |
{ | |
$config = $this->getCurrentApiConfig(); | |
return $config->publishableKey; | |
} | |
/** | |
* @return Service | |
*/ | |
public function getService() | |
{ | |
if (!$this->service) { | |
$this->service = new Service($this); | |
} | |
return $this->service; | |
} | |
/** | |
* @return string | |
*/ | |
public function getMode() | |
{ | |
return $this->mode; | |
} | |
/** | |
* @return \Zend_Config|null | |
*/ | |
public function getCurrentApiConfig() | |
{ | |
$configKey = $this->currentConfigKey ?: $this->defaultConfigKey; | |
return $this->apiConfigs->{$configKey}->{$this->mode}; | |
} | |
/** | |
* Initialises the current configuration with the Stripe API | |
*/ | |
public function initCurrentConfig() | |
{ | |
$this->service = null; | |
$this->getService(); | |
} | |
/** | |
* @param string $key | |
* @return bool | |
*/ | |
public function isValidConfigKey($key) | |
{ | |
return (bool) $this->apiConfigs->get($key); | |
} | |
/** | |
* @param string $key | |
*/ | |
public function setCurrentConfigKey($key) | |
{ | |
$this->currentConfigKey = $key; | |
} | |
/** | |
* @return string | |
*/ | |
public function getStripeAccountName() | |
{ | |
return ucfirst($this->currentConfigKey); | |
} | |
/** | |
* @return bool | |
*/ | |
public function isLivemode() | |
{ | |
return $this->mode == 'live'; | |
} | |
/** | |
* @return string | |
*/ | |
public function getName() | |
{ | |
return 'Stripe'; | |
} | |
/** | |
* start payment | |
* | |
* @param \OnlineShop\Framework\PriceSystem\IPrice $price | |
* @param array $config | |
* | |
* @return StripeLib\Token | |
*/ | |
public function initPayment(\OnlineShop\Framework\PriceSystem\IPrice $price, array $config) | |
{ | |
//initialise different profiles for the stripe API | |
if ($this->isValidConfigKey($config['stripeConfigKey'])) { | |
$this->setCurrentConfigKey($config['stripeConfigKey']); | |
$this->initCurrentConfig(); | |
} | |
if (strpos($config['token'], 'card_') === 0) { | |
// skip if is a 'source | |
return $config['token']; | |
} | |
// retrieve more information about the token given | |
$token = $this->getService()->tryApiCall('retrieve-token', $config['token']); | |
/** | |
* @var $token \Stripe\Token|\Exception | |
*/ | |
// check the token for being used already | |
if ($token instanceof \Stripe\Token) { | |
if ($token->used) { | |
throw new \Exception('Token has been used, please try again'); | |
} | |
/** | |
* @var $card \Stripe\Card | |
*/ | |
$card = $token->card; | |
if (empty($card->fingerprint)) { | |
throw new \Exception('Invalid Card fingerprint'); | |
} | |
} else { | |
throw $token; | |
} | |
return $token; | |
} | |
/** | |
* Handles response of payment provider and creates payment status object | |
* | |
* @param array $response | |
* | |
* @return \OnlineShop\Framework\PaymentManager\IStatus | |
*/ | |
public function handleResponse($response) | |
{ | |
// check required fields | |
$required = [ | |
'stripeToken' => null, | |
'paymentType' => null, | |
'invoice' => null, | |
'customerId' => null | |
]; | |
$authorizedData = [ | |
'stripeToken' => null, | |
'paymentType' => null, | |
'invoice' => null, | |
'customerId' => null, | |
'stripeChargeDetail' => null | |
]; | |
// check interval if paymentType is set | |
if ($response['paymentType'] == self::CART_TYPE_SUBSCRIPTION) { | |
$required['interval'] = null; | |
$authorizedData['interval'] = null; | |
} | |
// check fields | |
$response = array_intersect_key($response, $required); | |
if (count($required) != count($response)) { | |
throw new \Exception(sprintf('required fields are missing! required: %s', | |
implode(', ', array_keys(array_diff_key($required, $response))))); | |
} | |
// load the user by the email address | |
$customerClass = $this->customerClassName; | |
$customer = $customerClass::getById($response['customerId']); | |
/** | |
* @var $customer ICustomer | |
*/ | |
if (!$customer instanceof $customerClass) { | |
throw new \Exception('Customer must implement the chosen class ' . $customerClass); | |
} | |
// do all of the customer creation etc here | |
$response['stripeChargeDetail'] = $this->getService()->translateTokenToStripeChargeDetail($response['stripeToken'], $customer); | |
// handle | |
$authorizedData = array_intersect_key($response, $authorizedData); | |
$this->setAuthorizedData($authorizedData); | |
//return an intermediate status | |
return new \OnlineShop\Framework\PaymentManager\Status( | |
$response['invoice'], | |
$authorizedData['email'], | |
null, | |
\OnlineShop\Framework\Model\AbstractOrder::ORDER_STATE_PAYMENT_PENDING | |
); | |
} | |
/** | |
* return the authorized data from payment provider | |
* | |
* @return array | |
*/ | |
public function getAuthorizedData() | |
{ | |
return $this->authorizedData; | |
} | |
/** | |
* set authorized data from payment provider | |
* | |
* @param array $authorizedData | |
*/ | |
public function setAuthorizedData(array $authorizedData) | |
{ | |
$this->authorizedData = $authorizedData; | |
} | |
/** | |
* execute payment | |
* | |
* @param \OnlineShop\Framework\PriceSystem\IPrice $price | |
* @param string $reference | |
* | |
* @return \OnlineShop\Framework\PaymentManager\IStatus | |
*/ | |
public function executeDebit(\OnlineShop\Framework\PriceSystem\IPrice $price = null, $reference = null, $items = []) | |
{ | |
$authData = $this->getAuthorizedData(); | |
$stripeChargeDetail = $authData['stripeChargeDetail']; | |
$ret = null; | |
if ($authData['paymentType'] === self::CART_TYPE_CHARGE) { | |
// create charge | |
$ret = $this->getService()->tryApiCall('charge', [ | |
"amount" => Tool::getStripeAmount($price), | |
"currency" => $price->getCurrency()->getShortName(), | |
"customer" => $stripeChargeDetail['stripeCustomerId'], | |
"source" => $stripeChargeDetail['stripePaymentSource'], | |
"description" => "Card Charge", | |
"capture" => true, | |
"receipt_email" => null, | |
"metadata" => [ | |
"reference" => $reference, | |
] | |
]); | |
} else if ($authData['paymentType'] === self::CART_TYPE_SUBSCRIPTION) { | |
// get the plan that matches this customers options | |
$authData['planId'] = $this->getService()->translatePaymentAuthDataToStripePlan($authData, $price, $items); | |
// subscribe the current customer to the plan | |
$ret = $this->getService()->tryApiCall('create-subscription', [ | |
"customer" => $stripeChargeDetail['stripeCustomerId'], | |
"source" => $stripeChargeDetail['stripePaymentSource'], | |
'plan' => $authData['planId'] | |
]); | |
} | |
// throw the exception if one is returned | |
if ($ret instanceof \Exception) { | |
throw $ret; | |
} | |
if ($ret instanceof \Stripe\Charge && $ret->paid === true) { | |
return new PStatus($reference, $ret->id, null, AbstractOrder::ORDER_STATE_COMMITTED, [ | |
'stripe_TransactionType' => $ret->object, | |
'stripe_amount' => $ret->amount, | |
'stripe_id' => $ret->id | |
]); | |
} else if ($ret instanceof \Stripe\Subscription) { | |
return new PStatus($reference, $ret->id, null, AbstractOrder::ORDER_STATE_COMMITTED, [ | |
'stripe_TransactionType' => $ret->object, | |
'stripe_amount' => $ret->plan->amount, | |
'stripe_id' => $ret->id | |
]); | |
} | |
// failed ($ret is a string) | |
return new PStatus($reference, $authData['token'], $ret, AbstractOrder::ORDER_STATE_ABORTED); | |
} | |
/** | |
* execute credit | |
* | |
* @param \OnlineShop\Framework\PriceSystem\IPrice $price | |
* @param string $reference | |
* @param $transactionId | |
* @return IStatus | |
* @throws \Exception | |
*/ | |
public function executeCredit(\OnlineShop\Framework\PriceSystem\IPrice $price, $reference, $transactionId) | |
{ | |
throw new \Exception('Not implemented'); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Tool.php | |
* | |
* This source file is subject to the GNU General Public License version 3 (GPLv3) | |
* For the full copyright and license information, please view the LICENSE.md | |
* file distributed with this source code. | |
* | |
* @copyright Copyright (c) 2012-2021 Gather Digital Ltd (https://www.gatherdigital.co.uk) | |
* @license https://www.gatherdigital.co.uk/license GNU General Public License version 3 (GPLv3) | |
* @author Paul Clegg | |
*/ | |
namespace StripePaymentProvider; | |
use OnlineShop\Framework\PriceSystem\IPrice; | |
class Tool | |
{ | |
/** | |
* @param \OnlineShop\Framework\PriceSystem\IPrice $price | |
* @return int|float | |
*/ | |
public static function getStripeAmount(IPrice $price) | |
{ | |
$amount = $price->getAmount(); | |
$zeroDecimalCurrencies = [ | |
'BIF', | |
'CLP', | |
'DJF', | |
'GNF', | |
'JPY', | |
'KMF', | |
'KRW', | |
'MGA', | |
'PYG', | |
'RWF', | |
'VND', | |
'VUV', | |
'XAF', | |
'XOF', | |
'XPF' | |
]; | |
return (int) in_array($price->getCurrency(), $zeroDecimalCurrencies) ? $amount : $amount * 100; | |
} | |
public static function getIdempotencyKey() | |
{ | |
$uuid5 = \Ramsey\Uuid\Uuid::uuid4(); | |
return $uuid5->toString(); | |
} | |
/** | |
* Returns a valid Stripe Plan Name Given plan information | |
* - Will be returned in context to the current API settings | |
* @param \OnlineShop\Framework\PriceSystem\IPrice $price | |
* @param $interval | |
* @param int $intervalCount | |
* @param StripePaymentProvider $payment | |
* @return string | |
*/ | |
public static function getValidStripePlanName(IPrice $price, $interval, $intervalCount=1, StripePaymentProvider $payment) | |
{ | |
// e.g. "1000USD 1M EU Test" | |
return implode(' ', [ | |
$price->getAmount().strtoupper($price->getCurrency()->getShortName()), | |
$intervalCount, | |
strtoupper($interval{0}), // should be D|W|M|Y | |
strtoupper($payment->getStripeAccountName()), | |
ucfirst($payment->getMode()) | |
]); | |
} | |
/** | |
* Returns a valid Stripe Plan ID Given plan information | |
* - Will be returned in context to the current API settings | |
* - Is also compatible with Pimcore Object "Keys", so can be used in setKey() | |
* @param \OnlineShop\Framework\PriceSystem\IPrice $price | |
* @param string $interval | |
* @param int $intervalCount | |
* @param StripePaymentProvider $payment | |
* @return string | |
*/ | |
public static function getValidStripePlanId(IPrice $price, $interval, $intervalCount=1, $payment) | |
{ | |
$planName = self::getValidStripePlanName($price, $interval, $intervalCount, $payment); | |
return self::getValidStripePlanKey($planName); | |
} | |
/** | |
* Taken from Pimcore 4.3.1 File::getValidFilename (Pimcore now allows caps & whitespace) | |
* @param $planName | |
* @return mixed|string | |
*/ | |
public static function getValidStripePlanKey($planName) | |
{ | |
$tmpFilename = \Pimcore\Tool\Transliteration::toASCII($planName); | |
$tmpFilename = strtolower($tmpFilename); | |
$tmpFilename = preg_replace('/[^a-z0-9\-\.~_]+/', '-', $tmpFilename); | |
$tmpFilename = ltrim($tmpFilename, "."); // files shouldn't start with a "." (=hidden file) | |
return $tmpFilename; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment