Files
domokits/local/modules/StripePayment/StripePayment.php

543 lines
20 KiB
PHP

<?php
namespace StripePayment;
use Propel\Runtime\ActiveQuery\Criteria;
use Propel\Runtime\Connection\ConnectionInterface;
use Stripe\Checkout\Session;
use Stripe\Stripe;
use StripePayment\Classes\StripePaymentException;
use StripePayment\Classes\StripePaymentLog;
use Symfony\Component\Config\Definition\Exception\Exception;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Thelia\Core\Event\Image\ImageEvent;
use Thelia\Core\Event\TheliaEvents;
use Thelia\Core\HttpFoundation\Response;
use Thelia\Core\Template\ParserInterface;
use Thelia\Core\Translation\Translator;
use Thelia\Log\Tlog;
use Thelia\Model\ConfigQuery;
use Thelia\Model\Lang;
use Thelia\Model\LangQuery;
use Thelia\Model\Message;
use Thelia\Model\MessageQuery;
use Thelia\Model\ModuleQuery;
use Thelia\Model\Order;
use Thelia\Model\Order as OrderModel;
use Thelia\Model\OrderCouponQuery;
use Thelia\Model\OrderProductAttributeCombinationQuery;
use Thelia\Model\OrderProductQuery;
use Thelia\Model\ProductImageQuery;
use Thelia\Model\ProductQuery;
use Thelia\Module\AbstractPaymentModule;
use Thelia\Tools\URL;
/**
* Class StripePayment
* @package StripePayment
* @author Etienne Perriere - OpenStudio <eperriere@openstudio.fr>
*/
class StripePayment extends AbstractPaymentModule
{
const MESSAGE_DOMAIN = "stripepayment";
const CONFIRMATION_MESSAGE_NAME = "stripe_confirm_payment";
const STRIPE_VERSION_MIN = "3.0.0";
const STRIPE_VERSION_MAX = "7.0.0";
const PAYMENT_INTENT_ID_SESSION_KEY = 'payment_intent_id';
const PAYMENT_INTENT_CUSTOMER_ID_SESSION_KEY = 'payment_intent_customer_id';
const PAYMENT_INTENT_SECRET_SESSION_KEY = 'payment_intent_secret';
public function preActivation(ConnectionInterface $con = null)
{
// Check if Stripe API is present
try {
$this->checkApi();
} catch (\Exception $ex) {
throw $ex;
}
return true;
}
public function postActivation(ConnectionInterface $con = null)
{
// Module image
$moduleModel = $this->getModuleModel();
if (! $moduleModel->isModuleImageDeployed($con)) {
$this->deployImageFolder($moduleModel, sprintf('%s'.DS.'Resource'.DS.'images'.DS.'module', __DIR__), $con);
}
$this->createMailMessage();
}
public function createMailMessage()
{
// Create payment confirmation message from templates, if not already defined
if (null === MessageQuery::create()->findOneByName(self::CONFIRMATION_MESSAGE_NAME)) {
$languages = LangQuery::create()->find();
$message = new Message();
$message
->setName(self::CONFIRMATION_MESSAGE_NAME)
->setHtmlTemplateFileName(self::CONFIRMATION_MESSAGE_NAME.'.html')
->setTextTemplateFileName(self::CONFIRMATION_MESSAGE_NAME.'.txt')
;
foreach ($languages as $language) {
/** @var Lang $language */
$locale = $language->getLocale();
$message
->setLocale($locale)
->setTitle(
Translator::getInstance()->trans(
"Payment confirmation for Stripe Payment",
[],
self::MESSAGE_DOMAIN,
$locale
)
)
->setSubject(
Translator::getInstance()->trans(
'Payment confirmation of your order {$order_ref} on {$store_name}',
[],
self::MESSAGE_DOMAIN,
$locale
)
)
;
}
$message->save();
}
}
public function checkApi()
{
try {
$ReflectedClass = new \ReflectionClass('Stripe\Stripe');
} catch (\Exception $ex) {
throw new \Exception(
Translator::getInstance()->trans(
"Stripe library is missing.",
[],
self::MESSAGE_DOMAIN
)
);
}
$stripeVersion = \Stripe\Stripe::VERSION;
if (version_compare(self::STRIPE_VERSION_MIN, $stripeVersion) == 1) {
throw new \Exception(
Translator::getInstance()->trans(
"Stripe version is lower than min version (%version). Current version: %curVersion.",
[
'%version' => self::STRIPE_VERSION_MIN,
'%curVersion' => $stripeVersion
],
self::MESSAGE_DOMAIN
)
);
}
if (version_compare(self::STRIPE_VERSION_MAX, $stripeVersion) < 1) {
throw new \Exception(
Translator::getInstance()->trans(
"Stripe version is greater than max version (< %version). Current version: %curVersion.",
[
'%version' => self::STRIPE_VERSION_MAX,
'%curVersion' => $stripeVersion
],
self::MESSAGE_DOMAIN
)
);
}
}
/**
*
* Method used by payment gateway.
*
* If this method return a \Thelia\Core\HttpFoundation\Response instance, this response is send to the
* browser.
*
* In many cases, it's necessary to send a form to the payment gateway. On your response you can return this form already
* completed, ready to be sent
*
* @param \Thelia\Model\Order $order processed order
* @return null|\Thelia\Core\HttpFoundation\Response
*/
public function pay(Order $order)
{
if (!$this->isValidPayment()) {
throw new Exception("Your connection is not secured. Check that 'https' is present at the beginning of the site's address.");
}
return $this->doPay($order);
}
protected function doPay(Order $order)
{
Stripe::setApiKey(StripePayment::getConfigValue('secret_key'));
$session = $this->getRequest()->getSession();
try {
if(StripePayment::getConfigValue('stripe_element')){
$order->setTransactionRef($session->get(StripePayment::PAYMENT_INTENT_ID_SESSION_KEY))
->save();
$session->set(StripePayment::PAYMENT_INTENT_ID_SESSION_KEY, null);
$session->set(StripePayment::PAYMENT_INTENT_SECRET_SESSION_KEY, null);
$session->set(StripePayment::PAYMENT_INTENT_CUSTOMER_ID_SESSION_KEY, null);
return;
}else{
$session->set(StripePayment::PAYMENT_INTENT_ID_SESSION_KEY, null);
$session->set(StripePayment::PAYMENT_INTENT_SECRET_SESSION_KEY, null);
$session->set(StripePayment::PAYMENT_INTENT_CUSTOMER_ID_SESSION_KEY, null);
// Create the session on Stripe's servers - this will charge the user's order and save session id into order transaction reference
return $this->createStripeSession($order);
}
} catch(\Stripe\Error\Card $e) {
// The card has been declined
// FIXME Translate message here
$logMessage = sprintf(
'Error paying order %d with Stripe. Card declined. Message: %s',
$order->getId(),
$e->getMessage()
);
$userMessage = Translator::getInstance()
->trans(
'Your card has been declined.',
[],
StripePayment::MESSAGE_DOMAIN
);
} catch (\Stripe\Error\RateLimit $e) {
// Too many requests made to the API too quickly
$logMessage = sprintf(
'Error paying order %d with Stripe. Too many requests. Message: %s',
$order->getId(),
$e->getMessage()
);
$userMessage = Translator::getInstance()
->trans(
'Too many requests too quickly.',
[],
StripePayment::MESSAGE_DOMAIN
);
} catch (\Stripe\Error\InvalidRequest $e) {
// Invalid parameters were supplied to Stripe's API
$logMessage = sprintf(
'Error paying order %d with Stripe. Invalid parameters. Message: %s',
$order->getId(),
$e->getMessage()
);
$userMessage = Translator::getInstance()
->trans(
'Invalid parameters were supplied to Stripe.',
[],
StripePayment::MESSAGE_DOMAIN
);
} catch (\Stripe\Error\Authentication $e) {
// Authentication with Stripe's API failed
// (maybe you changed API keys recently)
$logMessage = sprintf(
'Error paying order %d with Stripe. Authentication failed: API key changed? Message: %s',
$order->getId(),
$e->getMessage()
);
$userMessage = Translator::getInstance()
->trans(
'Authentication with Stripe failed. Please contact administrators.',
[],
StripePayment::MESSAGE_DOMAIN
);
} catch (\Stripe\Error\ApiConnection $e) {
// Network communication with Stripe failed
$logMessage = sprintf(
'Error paying order %d with Stripe. Network communication failed. Message: %s',
$order->getId(),
$e->getMessage()
);
$userMessage = Translator::getInstance()
->trans(
'Network communication failed.',
[],
StripePayment::MESSAGE_DOMAIN
);
} catch (\Stripe\Error\Base $e) {
// Display a very generic error to the user
$logMessage = sprintf(
'Error paying order %d with Stripe. Message: %s',
$order->getId(),
$e->getMessage()
);
$userMessage = Translator::getInstance()
->trans(
'An error occurred with Stripe.',
[],
StripePayment::MESSAGE_DOMAIN
);
} catch (StripePaymentException $e) {
// Amount shown to the user by Stripe & order amount are not equal
$logMessage = sprintf(
'Error paying order %d with Stripe. Amounts are different. Message: %s',
$order->getId(),
$e->getMessage()
);
$userMessage = $e->getMessage();
} catch (\Exception $e) {
// Something else happened, completely unrelated to Stripe
$logMessage = sprintf(
'Error paying order %d with Stripe but maybe unrelated with it. Message: %s',
$order->getId(),
$e->getMessage()
);
$userMessage = Translator::getInstance()
->trans(
'An error occurred during payment.',
[],
StripePayment::MESSAGE_DOMAIN
);
}
if ($logMessage !== NULL) {
(new StripePaymentLog())->logText($logMessage);
return new RedirectResponse(
URL::getInstance()->absoluteUrl("/order/failed/".$order->getId()."/".$userMessage)
);
}
return new Response();
}
public function createStripeSession(OrderModel $order)
{
/* Impossible d'ajouter une ligne spécifique pour la remise, cette partie est mise de côté en attendant que stripe ajoute cette possibilité
$lineItems = $this->prepareLineItems($order);
*/
$currency = $order->getCurrency();
if (null === $currency) {
$currency = $this->getRequest()->getSession()->getCurrency();
}
$lineItems[] = [
'name'=> Translator::getInstance()->trans('Total', [], StripePayment::MESSAGE_DOMAIN ),
'description' => null,
'quantity'=> 1,
'currency' => strtolower($currency->getCode()),
'amount' => round($order->getTotalAmount(), 2) * 100
];
if(empty($lineItems)){
throw new \Exception("Sorry, your cart is empty. There's nothing to pay.");
}
$session = Session::create([
'customer_email' => $order->getCustomer()->getEmail(),
'client_reference_id' => $order->getRef(),
'payment_method_types' => ['card'],
'line_items' => $lineItems,
'success_url' => URL::getInstance()->absoluteUrl('/order/placed/' . $order->getId()),
'cancel_url' => URL::getInstance()->absoluteUrl('/order/failed/' . $order->getId() . '/error'),
]);
$order->setTransactionRef($session->payment_intent)->save();
/** @var ParserInterface $parser */
$parser = $this->getContainer()->get("thelia.parser");
$parser->setTemplateDefinition(
$parser->getTemplateHelper()->getActiveFrontTemplate()
);
$renderedTemplate = $parser->render(
"stripe-paiement.html",
[
'checkout_session_id' => $session->id,
'public_key' => StripePayment::getConfigValue('publishable_key')
]
);
return Response::create($renderedTemplate);
}
/**
*
* This method is call on Payment loop.
*
* If you return true, the payment method will be display
* If you return false, the payment method will not be display
*
* @return boolean
*/
public function isValidPayment()
{
return ( ($this->isDevEnvironment() || $this->isSslEnabled()) && $this->getConfigValue('enabled') );
}
/**
* Return true if the current environment is in Dev mode
*
* @return bool
*/
protected function isDevEnvironment()
{
return 'dev' == $this->getContainer()->getParameter('kernel.environment');
}
/**
* return true if SSL is enabled
*
* @return bool
*/
protected function isSslEnabled()
{
return $this->getRequest()->isSecure();
}
public function checkOrderAmount(OrderModel $order, $stripeAmount)
{
$orderAmount = $order->getTotalAmount() * 100;
if (strval($stripeAmount) != strval($orderAmount)) {
throw new StripePaymentException(Translator::getInstance()
->trans(
'The payment mean does not have the same amount as your cart. Please reload and try again.',
[],
StripePayment::MESSAGE_DOMAIN
)
);
}
}
protected function prepareLineItems(Order $order, $currency)
{
$stripeAmount = 0;
$lineItems = [];
$baseSourceFilePath = ConfigQuery::read('images_library_path');
if ($baseSourceFilePath === null) {
$baseSourceFilePath = THELIA_LOCAL_DIR . 'media' . DS . 'images';
} else {
$baseSourceFilePath = THELIA_ROOT . $baseSourceFilePath;
}
if(null !== $orderProducts = OrderProductQuery::create()->filterByOrderId($order->getId())->joinOrderProductTax('opt', Criteria::LEFT_JOIN)->withColumn('SUM(`opt`.AMOUNT)', 'TOTAL_TAX')->withColumn('SUM(`opt`.PROMO_AMOUNT)', 'TOTAL_PROMO_TAX')->groupById()->find()){
foreach ($orderProducts as $orderProduct) {
$description='';
if(null !== $orderProductAttributeCombinations = OrderProductAttributeCombinationQuery::create()->filterByOrderProductId($orderProduct->getId())->find()){
foreach ($orderProductAttributeCombinations as $orderProductAttributeCombination) {
if($description) $description .= ', ';
$description .= $orderProductAttributeCombination->getAttributeTitle() . ' ' . $orderProductAttributeCombination->getAttributeAvTitle();
}
}
$images=array();
if(null !== $product = ProductQuery::create()->filterByRef($orderProduct->getProductRef())->findOne()){
if(null !== $productImages = ProductImageQuery::create()->filterByProductId($product->getId())->filterByVisible(1)->orderBy('position')->find()){
foreach ($productImages as $productImage) {
// Put source image file path
$sourceFilePath = sprintf(
'%s/%s/%s',
$baseSourceFilePath,
'product',
$productImage->getFile()
);
// Create image processing event
$event = new ImageEvent();
$event->setSourceFilepath($sourceFilePath);
$event->setCacheSubdirectory('product');
$width=100;
try {
// Dispatch image processing event
$event->setWidth($width);
$order->getDispatcher()->dispatch(TheliaEvents::IMAGE_PROCESS, $event);
$images[]=$event->getFileUrl();
} catch (\Exception $ex) {
// Ignore the result and log an error
Tlog::getInstance()->addError(sprintf("Failed to process image in image loop: %s", $ex->getMessage()));
}
}
}
}
if($orderProduct->getWasInPromo()){
$amount = $orderProduct->getPromoPrice() + $orderProduct->getVirtualColumn('TOTAL_PROMO_TAX');
}else{
$amount = $orderProduct->getPrice() + $orderProduct->getVirtualColumn('TOTAL_TAX');
}
$stripeAmount += $amount * $orderProduct->getQuantity() * 100;
$lineItems[] = [
'name' => $orderProduct->getTitle(),
'description' => $description,
'images' => $images,
'amount' => $amount*100,
'currency' => $currency,
'quantity' => $orderProduct->getQuantity(),
];
}
}
if ($order->getPostage()){
if (null !== $module = ModuleQuery::create()->findPk($order->getDeliveryModuleId())){
$locale = $this->getRequest()->getLocale();
if ($locale == 'en') {
$locale = 'en_US';
}
$module->setLocale($locale);
if (!$module->getTitle()) {
$module->setLocale('fr_FR');
}
$lineItems[] = ['name'=> $module->getTitle(), 'description' => $module->getChapo(), 'quantity'=> 1, 'currency' => $currency, 'amount' => ($order->getPostage()*100)];
$stripeAmount += $order->getPostage() * 100;
}
}
if($order->getDiscount() > 0){
$description=null;
if(null !== $orderCoupons = OrderCouponQuery::create()->filterByOrderId($order->getId())->find()){
foreach($orderCoupons as $orderCoupon){
if($description)$description .= ', ';
$description .= $orderCoupon->getTitle();
}
}
$lineItems[] = ['name'=> Translator::getInstance()->trans('Discount', [], StripePayment::MESSAGE_DOMAIN ), 'description' => $description, 'quantity'=> 1, 'currency' => $currency, 'amount' => -($order->getDiscount()*100)];
$stripeAmount -= $order->getDiscount() * 100;
}
$this->checkOrderAmount($order, $stripeAmount);
return $lineItems;
}
/**
* if you want, you can manage stock in your module instead of order process.
* Return false to decrease the stock when order status switch to pay
*
* @return bool
*/
public function manageStockOnCreation()
{
return false;
}
}