From 485580e0b2aeed6fe3005b3adbaa32413c1d0542 Mon Sep 17 00:00:00 2001 From: TheCoreDev Date: Fri, 12 Jan 2024 10:37:52 +0100 Subject: [PATCH] Il manquait des fichiers dans le module Mercanet (sur Git) --- local/modules/Mercanet/Api/MercanetApi.php | 537 ++++++++++++++++++ local/modules/Mercanet/Config/config.xml | 22 + local/modules/Mercanet/Config/module.xml | 32 ++ local/modules/Mercanet/Config/routing.xml | 31 + .../Controller/ConfigureController.php | 104 ++++ .../Mercanet/Controller/PaymentController.php | 242 ++++++++ .../EventListeners/SendConfirmationEmail.php | 94 +++ local/modules/Mercanet/Form/ConfigForm.php | 281 +++++++++ local/modules/Mercanet/Hook/HookManager.php | 72 +++ .../I18n/backOffice/default/en_US.php | 13 + .../I18n/backOffice/default/fr_FR.php | 13 + .../Mercanet/I18n/email/default/en_US.php | 12 + .../Mercanet/I18n/email/default/fr_FR.php | 12 + local/modules/Mercanet/I18n/en_US.php | 44 ++ local/modules/Mercanet/I18n/fr_FR.php | 33 ++ local/modules/Mercanet/Mercanet.php | 270 +++++++++ local/modules/Mercanet/Readme.md | 73 +++ local/modules/Mercanet/composer.json | 11 + .../modules/Mercanet/images/logo-mercanet.png | Bin 0 -> 7606 bytes .../mercanet/module-configuration.html | 110 ++++ .../mercanet-payment-confirmation.html | 23 + .../default/mercanet-payment-confirmation.txt | 5 + 22 files changed, 2034 insertions(+) create mode 100644 local/modules/Mercanet/Api/MercanetApi.php create mode 100644 local/modules/Mercanet/Config/config.xml create mode 100644 local/modules/Mercanet/Config/module.xml create mode 100644 local/modules/Mercanet/Config/routing.xml create mode 100644 local/modules/Mercanet/Controller/ConfigureController.php create mode 100644 local/modules/Mercanet/Controller/PaymentController.php create mode 100644 local/modules/Mercanet/EventListeners/SendConfirmationEmail.php create mode 100644 local/modules/Mercanet/Form/ConfigForm.php create mode 100644 local/modules/Mercanet/Hook/HookManager.php create mode 100644 local/modules/Mercanet/I18n/backOffice/default/en_US.php create mode 100644 local/modules/Mercanet/I18n/backOffice/default/fr_FR.php create mode 100644 local/modules/Mercanet/I18n/email/default/en_US.php create mode 100644 local/modules/Mercanet/I18n/email/default/fr_FR.php create mode 100644 local/modules/Mercanet/I18n/en_US.php create mode 100644 local/modules/Mercanet/I18n/fr_FR.php create mode 100644 local/modules/Mercanet/Mercanet.php create mode 100644 local/modules/Mercanet/Readme.md create mode 100644 local/modules/Mercanet/composer.json create mode 100644 local/modules/Mercanet/images/logo-mercanet.png create mode 100644 local/modules/Mercanet/templates/backOffice/default/mercanet/module-configuration.html create mode 100644 local/modules/Mercanet/templates/email/default/mercanet-payment-confirmation.html create mode 100644 local/modules/Mercanet/templates/email/default/mercanet-payment-confirmation.txt diff --git a/local/modules/Mercanet/Api/MercanetApi.php b/local/modules/Mercanet/Api/MercanetApi.php new file mode 100644 index 000000000..d5371ef6b --- /dev/null +++ b/local/modules/Mercanet/Api/MercanetApi.php @@ -0,0 +1,537 @@ + + * Date: 11/06/2018 22:13 + */ + +namespace Mercanet\Api; + +/** + * Class MercanetApi + * @package Mercanet\Api + * @method setMerchantId($string) + * @method setKeyVersion($string) + */ +class MercanetApi +{ + const TEST = "https://payment-webinit-mercanet.test.sips-atos.com/paymentInit"; + const PRODUCTION = "https://payment-webinit.mercanet.bnpparibas.net/paymentInit"; + + const INTERFACE_VERSION = "HP_2.20"; + const INSTALMENT = "INSTALMENT"; + + // BYPASS3DS + const BYPASS3DS_ALL = "ALL"; + const BYPASS3DS_MERCHANTWALLET = "MERCHANTWALLET"; + + private $brandsmap = array( + 'ACCEPTGIRO' => 'CREDIT_TRANSFER', + 'AMEX' => 'CARD', + 'BCMC' => 'CARD', + 'BUYSTER' => 'CARD', + 'BANK CARD' => 'CARD', + 'CB' => 'CARD', + 'IDEAL' => 'CREDIT_TRANSFER', + 'INCASSO' => 'DIRECT_DEBIT', + 'MAESTRO' => 'CARD', + 'MASTERCARD' => 'CARD', + 'MASTERPASS' => 'CARD', + 'MINITIX' => 'OTHER', + 'NETBANKING' => 'CREDIT_TRANSFER', + 'PAYPAL' => 'CARD', + 'PAYLIB' => 'CARD', + 'REFUND' => 'OTHER', + 'SDD' => 'DIRECT_DEBIT', + 'SOFORT' => 'CREDIT_TRANSFER', + 'VISA' => 'CARD', + 'VPAY' => 'CARD', + 'VISA ELECTRON' => 'CARD', + 'CBCONLINE' => 'CREDIT_TRANSFER', + 'KBCONLINE' => 'CREDIT_TRANSFER' + ); + + private $secretKey; + + private $pspURL = self::TEST; + + private $parameters = array(); + + private $pspFields = array( + 'amount', 'currencyCode', 'merchantId', 'normalReturnUrl', + 'transactionReference', 'keyVersion', 'paymentMeanBrand', 'customerLanguage', + 'billingAddress.city', 'billingAddress.company', 'billingAddress.country', + 'billingAddress', 'billingAddress.postBox', 'billingAddress.state', + 'billingAddress.street', 'billingAddress.streetNumber', 'billingAddress.zipCode', + 'billingContact.email', 'billingContact.firstname', 'billingContact.gender', + 'billingContact.lastname', 'billingContact.mobile', 'billingContact.phone', + 'customerAddress', 'customerAddress.city', 'customerAddress.company', + 'customerAddress.country', 'customerAddress.postBox', 'customerAddress.state', + 'customerAddress.street', 'customerAddress.streetNumber', 'customerAddress.zipCode', + 'customerContact', 'customerContact.email', 'customerContact.firstname', + 'customerContact.gender', 'customerContact.lastname', 'customerContact.mobile', + 'customerContact.phone', 'customerContact.title', 'expirationDate', 'automaticResponseUrl', + 'templateName','paymentMeanBrandList', 'instalmentData.number', 'instalmentData.datesList', + 'instalmentData.transactionReferencesList', 'instalmentData.amountsList', 'paymentPattern', + 'captureDay', 'fraudData.bypass3DS', + 's10TransactionReference.s10TransactionId', 'orderId', + ); + + private $requiredFields = array( + 'amount', 'currencyCode', 'merchantId', 'normalReturnUrl', 'keyVersion' + ); + + public $allowedlanguages = array( + 'nl', 'fr', 'de', 'it', 'es', 'cy', 'en' + ); + + private static $currencies = array( + 'EUR' => '978', 'USD' => '840', 'CHF' => '756', 'GBP' => '826', + 'CAD' => '124', 'JPY' => '392', 'MXP' => '484', 'TRY' => '949', + 'AUD' => '036', 'NZD' => '554', 'NOK' => '578', 'BRC' => '986', + 'ARP' => '032', 'KHR' => '116', 'TWD' => '901', 'SEK' => '752', + 'DKK' => '208', 'KRW' => '410', 'SGD' => '702', 'XPF' => '953', + 'XOF' => '952' + ); + + public static function convertCurrencyToCurrencyCode($currency) + { + if(!in_array($currency, array_keys(self::$currencies))) + throw new \InvalidArgumentException("Unknown currencyCode $currency."); + return self::$currencies[$currency]; + } + + public static function convertCurrencyCodeToCurrency($code) + { + if(!in_array($code, array_values(self::$currencies))) + throw new \InvalidArgumentException("Unknown Code $code."); + return array_search($code, self::$currencies); + } + + public static function getCurrencies() + { + return self::$currencies; + } + + public function __construct($secret) + { + $this->secretKey = $secret; + } + + public function shaCompose(array $parameters) + { + // compose SHA string + $shaString = ''; + foreach($parameters as $key => $value) { + $shaString .= $key . '=' . $value; + $shaString .= (array_search($key, array_keys($parameters)) != (count($parameters)-1)) ? '|' : $this->secretKey; + } + + return hash('sha256', $shaString); + } + + /** @return string */ + public function getShaSign() + { + $this->validate(); + return $this->shaCompose($this->toArray()); + } + + /** @return string */ + public function getUrl() + { + return $this->pspURL; + } + + public function setUrl($pspUrl) + { + $this->validateUri($pspUrl); + $this->pspURL = $pspUrl; + } + + public function setNormalReturnUrl($url) + { + $this->validateUri($url); + $this->parameters['normalReturnUrl'] = $url; + } + + public function setAutomaticResponseUrl($url) + { + $this->validateUri($url); + $this->parameters['automaticResponseUrl'] = $url; + } + + public function setTransactionReference($transactionReference) + { + if(preg_match('/[^a-zA-Z0-9_-]/', $transactionReference)) { + throw new \InvalidArgumentException("TransactionReference cannot contain special characters"); + } + $this->parameters['transactionReference'] = $transactionReference; + } + + /** + * Set amount in cents, eg EUR 12.34 is written as 1234 + * @param $amount + */ + public function setAmount($amount) + { + if(!is_int($amount)) { + throw new \InvalidArgumentException("Integer expected. Amount is always in cents"); + } + if($amount <= 0) { + throw new \InvalidArgumentException("Amount must be a positive number"); + } + $this->parameters['amount'] = $amount; + + } + + public function setCurrency($currency) + { + if(!array_key_exists(strtoupper($currency), self::getCurrencies())) { + throw new \InvalidArgumentException("Unknown currency"); + } + $this->parameters['currencyCode'] = self::convertCurrencyToCurrencyCode($currency); + } + + public function setLanguage($language) + { + if(!in_array($language, $this->allowedlanguages)) { + throw new \InvalidArgumentException("Invalid language locale"); + } + $this->parameters['customerLanguage'] = $language; + } + + public function setPaymentBrand($brand) + { + $this->parameters['paymentMeanBrandList'] = ''; + if(!array_key_exists(strtoupper($brand), $this->brandsmap)) { + throw new \InvalidArgumentException("Unknown Brand [$brand]."); + } + $this->parameters['paymentMeanBrandList'] = strtoupper($brand); + } + + public function setCustomerContactEmail($email) + { + if(strlen($email) > 50) { + throw new \InvalidArgumentException("Email is too long"); + } + if(!filter_var($email, FILTER_VALIDATE_EMAIL)) { + throw new \InvalidArgumentException("Email is invalid"); + } + $this->parameters['customerContact.email'] = $email; + } + + public function setBillingContactEmail($email) + { + if(strlen($email) > 50) { + throw new \InvalidArgumentException("Email is too long"); + } + if(!filter_var($email, FILTER_VALIDATE_EMAIL)) { + throw new \InvalidArgumentException("Email is invalid"); + } + $this->parameters['billingContact.email'] = $email; + } + + public function setBillingAddressStreet($street) + { + if(strlen($street) > 35) { + throw new \InvalidArgumentException("street is too long"); + } + $this->parameters['billingAddress.street'] = \Normalizer::normalize($street); + } + + public function setBillingAddressStreetNumber($nr) + { + if(strlen($nr) > 10) { + throw new \InvalidArgumentException("streetNumber is too long"); + } + $this->parameters['billingAddress.streetNumber'] = \Normalizer::normalize($nr); + } + + public function setBillingAddressZipCode($zipCode) + { + if(strlen($zipCode) > 10) { + throw new \InvalidArgumentException("zipCode is too long"); + } + $this->parameters['billingAddress.zipCode'] = \Normalizer::normalize($zipCode); + } + + public function setBillingAddressCity($city) + { + if(strlen($city) > 25) { + throw new \InvalidArgumentException("city is too long"); + } + $this->parameters['billingAddress.city'] = \Normalizer::normalize($city); + } + + public function setBillingContactPhone($phone) + { + if(strlen($phone) > 30) { + throw new \InvalidArgumentException("phone is too long"); + } + $this->parameters['billingContact.phone'] = $phone; + } + + public function setBillingContactFirstname($firstname) + { + $this->parameters['billingContact.firstname'] = str_replace(array("'", '"'), '', \Normalizer::normalize($firstname)); // replace quotes + } + + public function setBillingContactLastname($lastname) + { + $this->parameters['billingContact.lastname'] = str_replace(array("'", '"'), '', \Normalizer::normalize($lastname)); // replace quotes + } + + public function setCaptureDay($number) + { + if (strlen($number) > 2) { + throw new \InvalidArgumentException("captureDay is too long"); + } + $this->parameters['captureDay'] = $number; + } + + // Methodes liees a la lutte contre la fraude + + public function setFraudDataBypass3DS($value) + { + if(strlen($value) > 128) { + throw new \InvalidArgumentException("fraudData.bypass3DS is too long"); + } + $this->parameters['fraudData.bypass3DS'] = $value; + } + + // Methodes liees au paiement one-click + + public function setMerchantWalletId($wallet) + { + if(strlen($wallet) > 21) { + throw new \InvalidArgumentException("merchantWalletId is too long"); + } + $this->parameters['merchantWalletId'] = $wallet; + } + + // instalmentData.number instalmentData.datesList instalmentData.transactionReferencesList instalmentData.amountsList paymentPattern + + // Methodes liees au paiement en n-fois + + public function setInstalmentDataNumber($number) + { + if (strlen($number) > 2) { + throw new \InvalidArgumentException("instalmentData.number is too long"); + } + if ( ($number < 2) || ($number > 50) ) { + throw new \InvalidArgumentException("instalmentData.number invalid value : value must be set between 2 and 50"); + } + $this->parameters['instalmentData.number'] = $number; + } + + public function setInstalmentDatesList($datesList) + { + $this->parameters['instalmentData.datesList'] = $datesList; + } + + public function setInstalmentDataTransactionReferencesList($transactionReferencesList) + { + $this->parameters['instalmentData.transactionReferencesList'] = $transactionReferencesList; + } + + public function setInstalmentDataAmountsList($amountsList) + { + $this->parameters['instalmentData.amountsList'] = $amountsList; + } + + public function setPaymentPattern($paymentPattern) + { + $this->parameters['paymentPattern'] = $paymentPattern; + } + + /** + * @param $transactionId 0 to 999999 + */ + public function setS10TransactionId($transactionId) + { + $this->parameters['s10TransactionReference.s10TransactionId'] = $transactionId; + } + + public function setOrderId(int $orderId) + { + $this->parameters['orderId'] = $orderId; + } + + public function __call($method, $args) + { + if(substr($method, 0, 3) == 'set') { + $field = lcfirst(substr($method, 3)); + if(in_array($field, $this->pspFields)) { + $this->parameters[$field] = $args[0]; + return; + } + } + + if(substr($method, 0, 3) == 'get') { + $field = lcfirst(substr($method, 3)); + if(array_key_exists($field, $this->parameters)) { + return $this->parameters[$field]; + } + } + + throw new \BadMethodCallException("Unknown method $method"); + } + + public function toArray() + { + return $this->parameters; + } + + public function toParameterString() + { + $parameterString = ""; + foreach($this->parameters as $key => $value) { + $parameterString .= $key . '=' . $value; + $parameterString .= (array_search($key, array_keys($this->parameters)) != (count($this->parameters)-1)) ? '|' : ''; + } + + return $parameterString; + } + + public function validate() + { + foreach($this->requiredFields as $field) { + if(empty($this->parameters[$field])) { + throw new \RuntimeException($field . " can not be empty"); + } + } + } + + protected function validateUri($uri) + { + if(!filter_var($uri, FILTER_VALIDATE_URL)) { + throw new \InvalidArgumentException("Uri is not valid"); + } + if(strlen($uri) > 200) { + throw new \InvalidArgumentException("Uri is too long"); + } + } + + // Traitement des reponses de Mercanet + // ----------------------------------- + + /** @var string */ + const SHASIGN_FIELD = "SEAL"; + + /** @var string */ + const DATA_FIELD = "DATA"; + + public function setResponse(array $httpRequest) + { + // use lowercase internally + $httpRequest = array_change_key_case($httpRequest, CASE_UPPER); + + // set sha sign + $this->shaSign = $this->extractShaSign($httpRequest); + + // filter request for Sips parameters + $this->parameters = $this->filterRequestParameters($httpRequest); + } + + /** + * @var string + */ + private $shaSign; + + private $dataString; + + /** + * Filter http request parameters + * @param array $httpRequest + * @return array + */ + private function filterRequestParameters(array $httpRequest) + { + //filter request for Sips parameters + if(!array_key_exists(self::DATA_FIELD, $httpRequest) || $httpRequest[self::DATA_FIELD] == '') { + throw new \InvalidArgumentException('Data parameter not present in parameters.'); + } + $parameters = array(); + $dataString = $httpRequest[self::DATA_FIELD]; + $this->dataString = $dataString; + $dataParams = explode('|', $dataString); + foreach($dataParams as $dataParamString) { + $dataKeyValue = explode('=',$dataParamString,2); + $parameters[$dataKeyValue[0]] = $dataKeyValue[1]; + } + + return $parameters; + } + + public function getSeal() + { + return $this->shaSign; + } + + private function extractShaSign(array $parameters) + { + if(!array_key_exists(self::SHASIGN_FIELD, $parameters) || $parameters[self::SHASIGN_FIELD] == '') { + throw new \InvalidArgumentException('SHASIGN parameter not present in parameters.'); + } + return $parameters[self::SHASIGN_FIELD]; + } + + /** + * Checks if the response is valid + * @return bool + */ + public function isValid() + { + return $this->shaCompose($this->parameters) == $this->shaSign; + } + + /** + * Retrieves a response parameter + * @param string $key + * @return mixed + * @throws \InvalidArgumentException + */ + public function getParam($key) + { + if(method_exists($this, 'get'.$key)) { + return $this->{'get'.$key}(); + } + + // always use uppercase + $key = strtoupper($key); + $parameters = array_change_key_case($this->parameters,CASE_UPPER); + if(!array_key_exists($key, $parameters)) { + throw new \InvalidArgumentException('Parameter ' . $key . ' does not exist.'); + } + + return $parameters[$key]; + } + + /** + * @return int Amount in cents + */ + public function getAmount() + { + $value = trim($this->parameters['amount']); + return (int) ($value); + } + + public function isSuccessful() + { + return in_array($this->getParam('RESPONSECODE'), array("00", "60")); + } + + public function getDataString() + { + return $this->dataString; + } +} diff --git a/local/modules/Mercanet/Config/config.xml b/local/modules/Mercanet/Config/config.xml new file mode 100644 index 000000000..8aa1643ce --- /dev/null +++ b/local/modules/Mercanet/Config/config.xml @@ -0,0 +1,22 @@ + + + + +
+ + + + + + + + + + + + + + + diff --git a/local/modules/Mercanet/Config/module.xml b/local/modules/Mercanet/Config/module.xml new file mode 100644 index 000000000..1b3af0320 --- /dev/null +++ b/local/modules/Mercanet/Config/module.xml @@ -0,0 +1,32 @@ + + + Mercanet\Mercanet + + Credit card payment with BNP Paribas Mercanet + + + Paiement par carte bancaire avec BNP Paribas Mercanet + + + images + + en_US + fr_FR + + 1.0.6 + + + Franck Allimant + CQFDev + thelia@cqfdev.fr + www.cqfdev.fr + + + payment + 2.3.0 + prod + 0 + 0 + diff --git a/local/modules/Mercanet/Config/routing.xml b/local/modules/Mercanet/Config/routing.xml new file mode 100644 index 000000000..72692270a --- /dev/null +++ b/local/modules/Mercanet/Config/routing.xml @@ -0,0 +1,31 @@ + + + + + + Mercanet\Controller\ConfigureController::configure + + + + Mercanet\Controller\ConfigureController::downloadLog + + + + + + Mercanet\Controller\PaymentController::processManualResponse + + + + Mercanet\Controller\PaymentController::processMercanetRequest + + + + Mercanet\Controller\PaymentController::processUserCancel + \d+ + + diff --git a/local/modules/Mercanet/Controller/ConfigureController.php b/local/modules/Mercanet/Controller/ConfigureController.php new file mode 100644 index 000000000..4bff159b0 --- /dev/null +++ b/local/modules/Mercanet/Controller/ConfigureController.php @@ -0,0 +1,104 @@ + + */ +class ConfigureController extends BaseAdminController +{ + public function downloadLog() + { + if (null !== $response = $this->checkAuth(AdminResources::MODULE, 'mercanet', AccessManager::UPDATE)) { + return $response; + } + + $logFilePath = sprintf(THELIA_ROOT."log".DS."%s.log", Mercanet::MODULE_DOMAIN); + + return Response::create( + @file_get_contents($logFilePath), + 200, + array( + 'Content-type' => "text/plain", + 'Content-Disposition' => sprintf('Attachment;filename=mercanet-log.txt') + ) + ); + + } + + public function configure() + { + if (null !== $response = $this->checkAuth(AdminResources::MODULE, 'mercanet', AccessManager::UPDATE)) { + return $response; + } + + $configurationForm = $this->createForm('mercanet_configuration'); + $message = null; + + try { + $form = $this->validateForm($configurationForm); + + // Get the form field values + $data = $form->getData(); + + foreach ($data as $name => $value) { + if (is_array($value)) { + $value = implode(';', $value); + } + + Mercanet::setConfigValue($name, $value); + } + + $merchantId = $data['merchantId']; + + // Log configuration modification + $this->adminLogAppend( + "mercanet.configuration.message", + AccessManager::UPDATE, + "Mercanet configuration updated" + ); + + // Redirect to the success URL, + if ($this->getRequest()->get('save_mode') == 'stay') { + // If we have to stay on the same page, redisplay the configuration page/ + $url = '/admin/module/Mercanet'; + } else { + // If we have to close the page, go back to the module back-office page. + $url = '/admin/modules'; + } + + return $this->generateRedirect(URL::getInstance()->absoluteUrl($url)); + } catch (FormValidationException $ex) { + $message = $this->createStandardFormValidationErrorMessage($ex); + } catch (\Exception $ex) { + $message = $ex->getMessage(); + } + + $this->setupFormErrorContext( + $this->getTranslator()->trans("Mercanet configuration", [], Mercanet::MODULE_DOMAIN), + $message, + $configurationForm, + $ex + ); + + return $this->generateRedirect(URL::getInstance()->absoluteUrl('/admin/module/Mercanet')); + } +} diff --git a/local/modules/Mercanet/Controller/PaymentController.php b/local/modules/Mercanet/Controller/PaymentController.php new file mode 100644 index 000000000..d858fb12f --- /dev/null +++ b/local/modules/Mercanet/Controller/PaymentController.php @@ -0,0 +1,242 @@ + + */ +class PaymentController extends BasePaymentModuleController +{ + protected static $resultCodes = [ + '00' => ' Transaction acceptée', + '02' => ' Demande d’autorisation par téléphone à la banque à cause d’un dépassement du plafond d’autorisation sur la carte, si vous êtes autorisé à forcer les transactions', + '03' => ' Contrat commerçant invalide', + '05' => ' Autorisation refusée', + '11' => ' Utilisé dans le cas d\'un contrôle différé. Le PAN est en opposition', + '12' => ' Transaction invalide, vérifier les paramètres transférés dans la requête', + '14' => ' Coordonnées du moyen de paiement invalides (ex: n° de carte ou cryptogramme visuel de la carte) ou vérification AVS échouée', + '17' => ' Annulation de l’acheteur', + '30' => ' Erreur de format', + '34' => ' Suspicion de fraude (seal erroné)', + '54' => ' Date de validité du moyen de paiement dépassée', + '75' => ' Nombre de tentatives de saisie des coordonnées du moyen de paiement sous Sips Paypage dépassé', + '90' => ' Service temporairement indisponible', + '94' => ' Transaction dupliquée : la référence de transaction est déjà utilisé', + '97' => ' Délai expiré, transaction refusée', + '99' => ' Problème temporaire du serveur de paiement.', + ]; + + /** + * Traitement de la réponse manuelle. La réponse manuelle est l'URL vers laquelle le client est + * redirigé une fois le paiement effectué (ou annulé). + * + * La validation de commande est efectuée dans le traitement de la réponse automatique (le callback de la banque). + */ + public function processManualResponse(): void + { + $this->getLog()->addInfo( + $this->getTranslator()->trans( + "Mercanet manual response processing.", + [], + Mercanet::MODULE_DOMAIN + ) + ); + + $paymentResponse = new MercanetApi(Mercanet::getConfigValue('secretKey')); + + $paymentResponse->setResponse($_POST); + + $this->getLog()->addInfo( + $this->getTranslator()->trans( + 'Response parameters : %resp', + ['%resp' => print_r($paymentResponse->getDataString(), true)], + Mercanet::MODULE_DOMAIN + ) + ); + + $order = OrderQuery::create() + ->filterById($paymentResponse->getParam('ORDERID')) + ->filterByPaymentModuleId(Mercanet::getModuleId()) + ->findOne(); + + if ($paymentResponse->isValid() && $paymentResponse->isSuccessful()) { + $this->redirectToSuccessPage($order->getId()); + } + + $resultCode = $paymentResponse->getParam('RESPONSECODE'); + + // Annulation de la commande + if ((int) $resultCode === 17) { + $this->processUserCancel($order->getId()); + } + + $message = self::$resultCodes[$resultCode] ?? 'Raison inconnue'; + + $this->redirectToFailurePage($order->getId(), $message); + } + + /** + * @return \Symfony\Component\HttpFoundation\Response + * @throws \Exception + */ + public function processMercanetRequest() + { + $this->getLog()->addInfo( + $this->getTranslator()->trans( + "Mercanet automatic response processing.", + [], + Mercanet::MODULE_DOMAIN + ) + ); + + $paymentResponse = new MercanetApi(Mercanet::getConfigValue('secretKey')); + + $paymentResponse->setResponse($_POST); + + $this->getLog()->addInfo( + $this->getTranslator()->trans( + 'Response parameters : %resp', + ['%resp' => print_r($paymentResponse->getDataString(), true)], + Mercanet::MODULE_DOMAIN + ) + ); + + if ($paymentResponse->isValid()) { + if (null !== $order = OrderQuery::create() + ->filterById($paymentResponse->getParam('ORDERID')) + ->filterByPaymentModuleId(Mercanet::getModuleId()) + ->findOne()) { + if ($paymentResponse->isSuccessful()) { + $this->confirmPayment($order->getId()); + + $this->getLog()->addInfo( + $this->getTranslator()->trans( + 'Order ID %id is confirmed, transaction référence "%trans"', + [ + '%id' => $order->getId(), + '%trans' => $paymentResponse->getParam('TRANSACTIONREFERENCE') + ], + Mercanet::MODULE_DOMAIN + ) + ); + } else { + $this->getLog()->addError( + $this->getTranslator()->trans( + 'Cannot validate order. Response code is %resp', + ['%resp' => $paymentResponse->getParam('RESPONSECODE')], + Mercanet::MODULE_DOMAIN + ) + ); + + // Cancel order. + $event = (new OrderEvent($order)) + ->setStatus(OrderStatusQuery::getCancelledStatus()->getId()); + $this->dispatch(TheliaEvents::ORDER_UPDATE_STATUS, $event); + + $this->getLog()->addError( + $this->getTranslator()->trans('Order was canceled.', [], Mercanet::MODULE_DOMAIN) + ); + } + } else { + $this->getLog()->addError( + $this->getTranslator()->trans( + 'Cannot find an order for transaction référence "%trans"', + ['%trans' => $paymentResponse->getParam('TRANSACTIONREFERENCE')], + Mercanet::MODULE_DOMAIN + ) + ); + } + } else { + $this->getTranslator()->trans( + 'Got invalid response from Mercanet', + [ ], + Mercanet::MODULE_DOMAIN + ); + } + + $this->getLog()->addInfo( + $this->getTranslator()->trans( + "Automatic response processing terminated.", + [], + Mercanet::MODULE_DOMAIN + ) + ); + + return new Response('OK'); + } + + /* + * @param $orderId int the order ID + * @return \Thelia\Core\HttpFoundation\Response + */ + public function processUserCancel($orderId): void + { + $this->getLog()->addInfo( + $this->getTranslator()->trans( + 'User canceled payment of order %id', + ['%id' => $orderId], + Mercanet::MODULE_DOMAIN + ) + ); + + try { + if (null !== $order = OrderQuery::create()->findPk($orderId)) { + $currentCustomerId = $this->getSecurityContext()->getCustomerUser()->getId(); + $orderCustomerId = $order->getCustomerId(); + + if ($orderCustomerId !== $currentCustomerId) { + throw new TheliaProcessException( + sprintf( + "User ID %d is trying to cancel order ID %d ordered by user ID %d", + $currentCustomerId, + $orderId, + $orderCustomerId + ) + ); + } + + $event = new OrderEvent($order); + $event->setStatus(OrderStatusQuery::getCancelledStatus()->getId()); + $this->dispatch(TheliaEvents::ORDER_UPDATE_STATUS, $event); + } + } catch (\Exception $ex) { + $this->getLog()->addError("Error occurred while canceling order ID $orderId: " . $ex->getMessage()); + } + + $this->redirectToFailurePage( + $orderId, + $this->getTranslator()->trans('you cancel the payment', [], Mercanet::MODULE_DOMAIN) + ); + } + + /** + * Return a module identifier used to calculate the name of the log file, + * and in the log messages. + * + * @return string the module code + */ + protected function getModuleCode() + { + return 'Mercanet'; + } +} diff --git a/local/modules/Mercanet/EventListeners/SendConfirmationEmail.php b/local/modules/Mercanet/EventListeners/SendConfirmationEmail.php new file mode 100644 index 000000000..2445d03a4 --- /dev/null +++ b/local/modules/Mercanet/EventListeners/SendConfirmationEmail.php @@ -0,0 +1,94 @@ + + */ +class SendConfirmationEmail implements EventSubscriberInterface +{ + /** @var MailerFactory */ + protected $mailer; + + public function __construct(MailerFactory $mailer) + { + $this->mailer = $mailer; + } + + /** + * @param OrderEvent $event + * + * @throws \Exception if the message cannot be loaded. + */ + public function sendConfirmationEmail(OrderEvent $event) + { + if (Mercanet::getConfigValue('send_confirmation_message_only_if_paid')) { + // We send the order confirmation email only if the order is paid + $order = $event->getOrder(); + + if (! $order->isPaid() && $order->getPaymentModuleId() == Mercanet::getModuleId()) { + $event->stopPropagation(); + } + } + } + + /** + * Checks if order payment module is paypal and if order new status is paid, send an email to the customer. + * + * @param OrderEvent $event + * @param $eventName + * @param EventDispatcherInterface $dispatcher + * @throws \Propel\Runtime\Exception\PropelException + */ + public function updateStatus(OrderEvent $event, $eventName, EventDispatcherInterface $dispatcher) + { + $order = $event->getOrder(); + + if ($order->isPaid() && $order->getPaymentModuleId() == Mercanet::getModuleId()) { + if (Mercanet::getConfigValue('send_payment_confirmation_message')) { + $this->mailer->sendEmailToCustomer( + Mercanet::CONFIRMATION_MESSAGE_NAME, + $order->getCustomer(), + [ + 'order_id' => $order->getId(), + 'order_ref' => $order->getRef() + ] + ); + } + + // Send confirmation email if required. + if (Mercanet::getConfigValue('send_confirmation_message_only_if_paid')) { + $dispatcher->dispatch(TheliaEvents::ORDER_SEND_CONFIRMATION_EMAIL, $event); + } + + Tlog::getInstance()->debug("Confirmation email sent to customer " . $order->getCustomer()->getEmail()); + } + } + + public static function getSubscribedEvents() + { + return array( + TheliaEvents::ORDER_UPDATE_STATUS => array("updateStatus", 128), + TheliaEvents::ORDER_SEND_CONFIRMATION_EMAIL => array("sendConfirmationEmail", 129) + ); + } +} diff --git a/local/modules/Mercanet/Form/ConfigForm.php b/local/modules/Mercanet/Form/ConfigForm.php new file mode 100644 index 000000000..98f44e07d --- /dev/null +++ b/local/modules/Mercanet/Form/ConfigForm.php @@ -0,0 +1,281 @@ + + */ +class ConfigForm extends BaseForm +{ + protected function buildForm() + { + // If the Multi plugin is not enabled, all multi_fields are hidden + /** @var Module $multiModule */ + $multiEnabled = (null !== $multiModule = ModuleQuery::create()->findOneByCode('MercanetNx')) && $multiModule->getActivate() != 0; + + $translator = Translator::getInstance(); + + $this->formBuilder + ->add( + 'merchantId', + 'text', + [ + 'constraints' => [ + new NotBlank(), + ], + 'label' => $translator->trans('Shop Merchant ID', [], Mercanet::MODULE_DOMAIN), + 'label_attr' => [ + 'for' => 'merchant_id', + ] + ] + ) + ->add( + 'mode', + 'choice', + [ + 'constraints' => [ + new NotBlank() + ], + 'choices' => [ + 'TEST' => $translator->trans('Test', [], Mercanet::MODULE_DOMAIN), + 'PRODUCTION' => $translator->trans('Production', [], Mercanet::MODULE_DOMAIN), + ], + 'label' => $translator->trans('Operation Mode', [], Mercanet::MODULE_DOMAIN), + 'label_attr' => [ + 'for' => 'mode', + 'help' => $translator->trans('Test or production mode', [], Mercanet::MODULE_DOMAIN) + ] + ] + ) + ->add( + 'allowed_ip_list', + 'textarea', + [ + 'required' => false, + 'label' => $translator->trans('Allowed IPs in test mode', [], Mercanet::MODULE_DOMAIN), + 'label_attr' => [ + 'for' => 'platform_url', + 'help' => $translator->trans( + 'List of IP addresses allowed to use this payment on the front-office when in test mode (your current IP is %ip). One address per line', + [ '%ip' => $this->getRequest()->getClientIp() ], + Mercanet::MODULE_DOMAIN + ) + ], + 'attr' => [ + 'rows' => 3 + ] + ] + ) + ->add( + 'minimum_amount', + 'text', + [ + 'constraints' => [ + new NotBlank(), + new GreaterThanOrEqual(['value' => 0 ]) + ], + 'label' => $translator->trans('Minimum order total', [], Mercanet::MODULE_DOMAIN), + 'label_attr' => [ + 'for' => 'minimum_amount', + 'help' => $translator->trans( + 'Minimum order total in the default currency for which this payment method is available. Enter 0 for no minimum', + [], + Mercanet::MODULE_DOMAIN + ) + ] + ] + ) + ->add( + 'maximum_amount', + 'text', + [ + 'constraints' => [ + new NotBlank(), + new GreaterThanOrEqual([ 'value' => 0 ]) + ], + 'label' => $translator->trans('Maximum order total', [], Mercanet::MODULE_DOMAIN), + 'label_attr' => [ + 'for' => 'maximum_amount', + 'help' => $translator->trans( + 'Maximum order total in the default currency for which this payment method is available. Enter 0 for no maximum', + [], + Mercanet::MODULE_DOMAIN + ) + ] + ] + ) + ->add( + 'secretKey', + 'text', + [ + 'label' => $translator->trans('Mercanet secret key', [], Mercanet::MODULE_DOMAIN), + 'label_attr' => [ + 'for' => 'platform_url', + 'help' => $translator->trans( + 'Please paste here the secret key you get from Mercanet Download', + [], + Mercanet::MODULE_DOMAIN + ), + ], + 'attr' => [ + 'rows' => 10 + ] + ] + ) + ->add( + 'secretKeyVersion', + 'text', + [ + 'label' => $translator->trans('Mercanet secret key version number', [], Mercanet::MODULE_DOMAIN), + 'label_attr' => [ + 'for' => 'platform_url', + 'help' => $translator->trans( + 'The secret key version you get from Mercanet Download, 1 for the first secret key you get', + [], + Mercanet::MODULE_DOMAIN + ), + ], + 'attr' => [ + 'rows' => 10 + ] + ] + ) + ->add( + 'send_confirmation_message_only_if_paid', + 'checkbox', + [ + 'value' => 1, + 'required' => false, + 'label' => $this->translator->trans('Send order confirmation on payment success', [], Mercanet::MODULE_DOMAIN), + 'label_attr' => [ + 'help' => $this->translator->trans( + 'If checked, the order confirmation message is sent to the customer only when the payment is successful. The order notification is always sent to the shop administrator', + [], + Mercanet::MODULE_DOMAIN + ) + ] + ] + ) + ->add( + 'mode_v2_simplifie', + 'checkbox', + [ + 'value' => 1, + 'required' => false, + 'label' => $this->translator->trans('Simplified migration of 1.0 account', [], Mercanet::MODULE_DOMAIN), + 'label_attr' => [ + 'help' => $this->translator->trans( + 'Somes Mercanet 1.0 accounts are migrated in 2.0 in a specific way, called "simplified migration". Please check with your account manager to get this information.', + [], + Mercanet::MODULE_DOMAIN + ) + ] + ] + ) + ->add( + 'send_payment_confirmation_message', + 'checkbox', + [ + 'value' => 1, + 'required' => false, + 'label' => $this->translator->trans('Send a payment confirmation e-mail', [], Mercanet::MODULE_DOMAIN), + 'label_attr' => [ + 'help' => $this->translator->trans( + 'If checked, a payment confirmation e-mail is sent to the customer.', + [], + Mercanet::MODULE_DOMAIN + ) + ] + ] + ) + + // -- Multiple times payement parameters, hidden id the MercanetNx module is not activated. + ->add( + 'nx_nb_installments', + $multiEnabled ? 'text' : 'hidden', + [ + 'constraints' => [ + new NotBlank(), + new GreaterThanOrEqual(['value' => 1 ]) + ], + 'required' => $multiEnabled, + 'label' => $translator->trans('Number of installments', [], Mercanet::MODULE_DOMAIN), + 'label_attr' => [ + 'for' => 'nx_nb_installments', + 'help' => $translator->trans( + 'Number of installements. Should be more than one', + [], + Mercanet::MODULE_DOMAIN + ) + ] + ] + ) + ->add( + 'nx_minimum_amount', + $multiEnabled ? 'text' : 'hidden', + [ + 'constraints' => [ + new NotBlank(), + new GreaterThanOrEqual(['value' => 0 ]) + ], + 'required' => $multiEnabled, + 'label' => $translator->trans('Minimum order total', [], Mercanet::MODULE_DOMAIN), + 'label_attr' => [ + 'for' => 'nx_minimum_amount', + 'help' => $translator->trans( + 'Minimum order total in the default currency for which the multiple times payment method is available. Enter 0 for no minimum', + [], + Mercanet::MODULE_DOMAIN + ) + ] + ] + ) + ->add( + 'nx_maximum_amount', + $multiEnabled ? 'text' : 'hidden', + [ + 'constraints' => [ + new NotBlank(), + new GreaterThanOrEqual([ 'value' => 0 ]) + ], + 'required' => $multiEnabled, + 'label' => $translator->trans('Maximum order total', [], Mercanet::MODULE_DOMAIN), + 'label_attr' => [ + 'for' => 'nx_maximum_amount', + 'help' => $translator->trans( + 'Maximum order total in the default currency for which the multiple times payment method is available. Enter 0 for no maximum', + [], + Mercanet::MODULE_DOMAIN + ) + ] + ] + ) + ; + } + + /** + * @return string the name of you form. This name must be unique + */ + public function getName() + { + return 'config'; + } +} diff --git a/local/modules/Mercanet/Hook/HookManager.php b/local/modules/Mercanet/Hook/HookManager.php new file mode 100644 index 000000000..1b97a6460 --- /dev/null +++ b/local/modules/Mercanet/Hook/HookManager.php @@ -0,0 +1,72 @@ + + */ +class HookManager extends BaseHook +{ + const MAX_TRACE_SIZE_IN_BYTES = 40000; + + public function onModuleConfigure(HookRenderEvent $event) + { + $logFilePath = sprintf(THELIA_ROOT."log".DS."%s.log", Mercanet::MODULE_DOMAIN); + + $traces = @file_get_contents($logFilePath); + + if (false === $traces) { + $traces = $this->translator->trans( + "The log file '%log' does not exists yet.", + [ '%log' => $logFilePath ], + Mercanet::MODULE_DOMAIN + ); + } elseif (empty($traces)) { + $traces = $this->translator->trans("The log file is currently empty.", [], Mercanet::MODULE_DOMAIN); + } else { + // Limiter la taille des traces à 1MO + if (strlen($traces) > self::MAX_TRACE_SIZE_IN_BYTES) { + $traces = substr($traces, strlen($traces) - self::MAX_TRACE_SIZE_IN_BYTES); + // Cut a first line break; + if (false !== $lineBreakPos = strpos($traces, "\n")) { + $traces = substr($traces, $lineBreakPos+1); + } + + $traces = $this->translator->trans( + "(Previous log is in %file file.)\n", + [ '%file' => sprintf("log".DS."%s.log", Mercanet::MODULE_DOMAIN) ], + Mercanet::MODULE_DOMAIN + ) . $traces; + } + } + + $vars = [ 'trace_content' => nl2br($traces) ]; + + if (null !== $params = ModuleConfigQuery::create()->findByModuleId(Mercanet::getModuleId())) { + /** @var ModuleConfig $param */ + foreach ($params as $param) { + $vars[ $param->getName() ] = $param->getValue(); + } + } + + $event->add( + $this->render('mercanet/module-configuration.html', $vars) + ); + } +} diff --git a/local/modules/Mercanet/I18n/backOffice/default/en_US.php b/local/modules/Mercanet/I18n/backOffice/default/en_US.php new file mode 100644 index 000000000..07216f6d5 --- /dev/null +++ b/local/modules/Mercanet/I18n/backOffice/default/en_US.php @@ -0,0 +1,13 @@ + 'Mercanet Configuration', + 'Mercanet Platform configuration' => 'Mercanet Platform configuration', + 'Mercanet call log to callback URL' => 'Mercanet call log to callback URL', + 'Callback URL' => 'Callback URL', + 'Download full log' => 'Download full log', + 'Operation mode' => 'Operation mode', + 'Payment configuration' => 'Payment configuration', + 'This is the callback URL, that will be called by Mercanet after customer payment. Be sure that this URL is reachable by remote mechines' => 'This is the callback URL, that will be called by Mercanet after customer payment. Be sure that this URL is reachable by remote machines', + 'You can edit the payment confirmation email sent to the customer after a successful payment.' => 'You can edit the payment confirmation email sent to the customer after a successful payment.', +); diff --git a/local/modules/Mercanet/I18n/backOffice/default/fr_FR.php b/local/modules/Mercanet/I18n/backOffice/default/fr_FR.php new file mode 100644 index 000000000..9cdd08efb --- /dev/null +++ b/local/modules/Mercanet/I18n/backOffice/default/fr_FR.php @@ -0,0 +1,13 @@ + 'Télécharger l\'historique complet', + 'Install and activate Mercanet multiple times payment module (MercanetNx) to get configuration options.' => 'Installez et activez le module de paiement en plusieurs fois MercanetNx pour configurer ces options.', + 'Mercanet Configuration' => 'Mercanet Configuration', + 'Mercanet Platform configuration' => 'Configuration de la plate-forme Mercanet', + 'Mercanet call log to callback URL' => 'Mercanet callback URL log', + 'Operation mode' => 'Mode de fonctionnement', + 'Payment by N installment' => 'Paiement en N fois', + 'Payment configuration' => 'Configuration du paiement', + 'You can edit the payment confirmation email sent to the customer after a successful payment.' => 'Vous pouvez modifier le mail de confirmation de paiement envoyé au client.', +); diff --git a/local/modules/Mercanet/I18n/email/default/en_US.php b/local/modules/Mercanet/I18n/email/default/en_US.php new file mode 100644 index 000000000..072bbbd58 --- /dev/null +++ b/local/modules/Mercanet/I18n/email/default/en_US.php @@ -0,0 +1,12 @@ + 'Dear customer', + 'Payment of your order %ref' => 'Payment of your order %ref', + 'Thank you again for your purchase.' => 'Thank you again for your purchase.', + 'The %store_name team.' => 'The %store_name team.', + 'The payment of your order %ref with Mercanet is confirmed' => 'The payment of your order %ref with Mercanet is confirmed', + 'This is a confirmation of the payment of your order %ref with SIPS-Mercanet on our shop.' => 'This is a confirmation of the payment of your order %ref with SIPS-Mercanet on our shop.', + 'View this order in your account at %shop_name' => 'View this order in your account at %shop_name', + 'Your invoice is now available in your customer account at %url.' => 'Your invoice is now available in your customer account at %url.', +); diff --git a/local/modules/Mercanet/I18n/email/default/fr_FR.php b/local/modules/Mercanet/I18n/email/default/fr_FR.php new file mode 100644 index 000000000..69c2dc4f0 --- /dev/null +++ b/local/modules/Mercanet/I18n/email/default/fr_FR.php @@ -0,0 +1,12 @@ + 'Cher client', + 'Payment of your order %ref' => 'Paiement de votre commande %ref', + 'Thank you again for your purchase.' => 'Merci encore pour votre commande.', + 'The %store_name team.' => 'L\'équipe %store_name', + 'The payment of your order %ref with Mercanet is confirmed' => 'Le paiement de votre commande %ref avec Mercanet est confirmé.', + 'This is a confirmation of the payment of your order %ref with SIPS-Mercanet on our shop.' => 'Ce message confirme le paiement de votre commande %ref avec SIPS Mercanet dans notre boutique.', + 'View this order in your account at %shop_name' => 'Les détails de cette commande sont disponibles dans votre compte client sur %shop_name', + 'Your invoice is now available in your customer account at %url.' => 'Les détails de cette commande sont disponibles dans votre compte client sur %url', +); diff --git a/local/modules/Mercanet/I18n/en_US.php b/local/modules/Mercanet/I18n/en_US.php new file mode 100644 index 000000000..dbf8a9893 --- /dev/null +++ b/local/modules/Mercanet/I18n/en_US.php @@ -0,0 +1,44 @@ + '(Previous log is in %file file.)\n', + 'Mercanet certificate content' => 'Mercanet certificate content', + 'Allowed IPs in test mode' => 'Allowed IPs in test mode', + 'Mercanet configuration' => 'Mercanet configuration', + 'Mercanet payment module is nort properly configured. Please check module configuration in your back-office.' => 'Mercanet payment module is nort properly configured. Please check module configuration in your back-office.', + 'Mercanet platform request processing terminated.' => 'Mercanet platform request processing terminated.', + 'Mercanet platform request received.' => 'Mercanet platform request received.', + 'Cannot find an order for transaction ID "%trans"' => 'Cannot find an order for transaction ID "%trans"', + 'Cannot validate order. Response code is %resp' => 'Cannot validate order. Response code is %resp', + 'Empty response recevied from Mercanet binary "%path". Please check path and permissions.' => 'Empty response recevied from Mercanet binary "%path". Please check path and permissions.', + 'Error %code while processing response, with message %message' => 'Error %code while processing response, with message %message', + 'Failed to read the %file file. Please check file and directory permissions.' => 'Failed to read the %file file. Please check file and directory permissions.', + 'Failed to write certificate data in file \'%file\'. Please check file permission' => 'Failed to write certificate data in file \'%file\'. Please check file permission', + 'File %file must be writable, please check Mercanet/Config directory permissions.' => 'File %file must be writable, please check Mercanet/Config directory permissions.', + 'Got empty response from executable %binary, check path and permissions' => 'Got empty response from executable %binary, check path and permissions', + 'If checked, a payment confirmation e-mail is sent to the customer.' => 'If checked, a payment confirmation e-mail is sent to the customer.', + 'If checked, the order confirmation message is sent to the customer only when the payment is successful. The order notification is always sent to the shop administrator' => 'If checked, the order confirmation message is sent to the customer only when the payment is successful. The order notification is always sent to the shop administrator', + 'List of IP addresses allowed to use this payment on the front-office when in test mode (your current IP is %ip). One address per line' => 'List of IP addresses allowed to use this payment on the front-office when in test mode (your current IP is %ip). One address per line', + 'Maximum order total' => 'Maximum order total', + 'Maximum order total in the default currency for which this payment method is available. Enter 0 for no maximum' => 'Maximum order total in the default currency for which this payment method is available. Enter 0 for no maximum', + 'Minimum order total' => 'Minimum order total', + 'Minimum order total in the default currency for which this payment method is available. Enter 0 for no minimum' => 'Minimum order total in the default currency for which this payment method is available. Enter 0 for no minimum', + 'Operation Mode' => 'Operation Mode', + 'Order ID %id is confirmed.' => 'Order ID %id is confirmed.', + 'Please paste here the certificate downloaded from the Mercanet platform' => 'Please paste here the certificate downloaded from the Mercanet platform', + 'Production' => 'Production', + 'Request binary not found in "%path"' => 'Request binary not found in "%path"', + 'Request does not contains any data' => 'Request does not contains any data', + 'Response parameters : %resp' => 'Response parameters : %resp', + 'Response request not found in %response' => 'Response request not found in %response', + 'Send a payment confirmation e-mail' => 'Send a payment confirmation e-mail', + 'Send order confirmation on payment success' => 'Send order confirmation on payment success', + 'Shop Merchant ID' => 'Shop Merchant ID', + 'Test' => 'Test', + 'Test or production mode' => 'Test or production mode', + 'The \'%file\' should be executable. Please check file permission' => 'The \'%file\' should be executable. Please check file permission', + 'The log file \'%log\' does not exists yet.' => 'The log file \'%log\' does not exists yet.', + 'The log file is currently empty.' => 'The log file is currently empty.', + 'User canceled payment of order %id' => 'User canceled payment of order ID %id', + 'you cancel the payment' => 'you cancel the payment', +); diff --git a/local/modules/Mercanet/I18n/fr_FR.php b/local/modules/Mercanet/I18n/fr_FR.php new file mode 100644 index 000000000..f1d89015e --- /dev/null +++ b/local/modules/Mercanet/I18n/fr_FR.php @@ -0,0 +1,33 @@ + '(L\'historique précédent se trouve dans n %file file.)\n', + 'Allowed IPs in test mode' => 'Adresse IP autorisées en phase de test', + 'Cannot validate order. Response code is %resp' => 'La commande ne peut être validée. Le code de réponse est %resp', + 'If checked, a payment confirmation e-mail is sent to the customer.' => 'Si cette case est cochée, un mail de confirmation de paiement sera envoyé au client.', + 'If checked, the order confirmation message is sent to the customer only when the payment is successful. The order notification is always sent to the shop administrator' => 'Si cette case est cochée, le mail de confirmation de commande sera envoyé au client seulement si son paiement est validé.', + 'List of IP addresses allowed to use this payment on the front-office when in test mode (your current IP is %ip). One address per line' => 'Liste des adresse IP qui pourront choisir ce module de paiement en front-office pendant la phase de test (votre IP est %ip). Une adresse par ligne.', + 'Maximum order total' => 'Montant de commande maximum', + 'Maximum order total in the default currency for which this payment method is available. Enter 0 for no maximum' => 'Montant maximum dans la devise par défaut pour proposer ce moyen de paiement. Laisser 0 pour ne pas fixer de maximum.', + 'Mercanet configuration' => 'Configuration Mercanet', + 'Mercanet secret key' => 'Clef secrète Mercanet', + 'Mercanet secret key version number' => 'Version de la clef secrète', + 'Minimum order total' => 'Montant minimum de commande', + 'Minimum order total in the default currency for which this payment method is available. Enter 0 for no minimum' => 'Montant minimum dans la devise par défaut pour proposer ce moyen de paiement. Laisser 0 pour ne pas fixer de minimum.', + 'Operation Mode' => 'Mode de fonctionnement', + 'Please paste here the secret key you get from Mercanet Download' => 'Copiez ici la clef secrète obtenue sur le site Mercanet download', + 'Production' => 'Production', + 'Response parameters : %resp' => 'Paramètres de la réponse : %resp', + 'Send a payment confirmation e-mail' => 'Envoyer une confirmation de paiement', + 'Send order confirmation on payment success' => 'Confirmation de commande si le paiement réussit ', + 'Shop Merchant ID' => 'Identifiant Marchand', + 'Simplified migration of 1.0 account' => 'Migration simplifiée d\'un contrat 1.0', + 'Somes Mercanet 1.0 accounts are migrated in 2.0 in a specific way, called "simplified migration". Please check with your account manager to get this information.' => 'Si votre contrat Mercanet 2.0 est une migration simplifiée d\'un contrat 1.0, merci de cocher cette case. Adressez vous à votre conseiller pour obtenir cette information.', + 'Test' => 'Test', + 'Test or production mode' => 'Test ou production', + 'The log file \'%log\' does not exists yet.' => 'Le fichier de log %log n\'existe pas encore.', + 'The log file is currently empty.' => 'Le fichier de log est vide', + 'The secret key version you get from Mercanet Download, 1 for the first secret key you get' => 'La version de la clef secrète, indiquée sur le site Mercanet Download', + 'User canceled payment of order %id' => 'Le client a annulé le paiement de la commande ID %id', + 'you cancel the payment' => 'Vous avez annulé le paiement', +); diff --git a/local/modules/Mercanet/Mercanet.php b/local/modules/Mercanet/Mercanet.php new file mode 100644 index 000000000..6ef018a60 --- /dev/null +++ b/local/modules/Mercanet/Mercanet.php @@ -0,0 +1,270 @@ + + */ +class Mercanet extends AbstractPaymentModule +{ + const MODULE_DOMAIN = 'mercanet'; + + /** + * The confirmation message identifier + */ + const CONFIRMATION_MESSAGE_NAME = 'mercanet_payment_confirmation'; + + /** + * @param ConnectionInterface|null $con + * @throws \Propel\Runtime\Exception\PropelException + */ + public function postActivation(ConnectionInterface $con = null) + { + // Setup some default values + if (null === self ::getConfigValue('merchantId', null)) { + // Initialize with test data + self::setConfigValue('merchantId', '211000021310001'); + self::setConfigValue('secretKeyVersion', 1); + self::setConfigValue('secretKey', 'S9i8qClCnb2CZU3y3Vn0toIOgz3z_aBi79akR30vM9o'); + self::setConfigValue('mode', 'TEST'); + self::setConfigValue('allowed_ip_list', $_SERVER['REMOTE_ADDR']); + self::setConfigValue('minimum_amount', 0); + self::setConfigValue('maximum_amount', 0); + self::setConfigValue('send_payment_confirmation_message', 1); + self::setConfigValue('transactionId', 1); + self::setConfigValue('mode_v2_simplifie', 0); + } + + if (null === MessageQuery::create()->findOneByName(self::CONFIRMATION_MESSAGE_NAME)) { + $message = new Message(); + + $message + ->setName(self::CONFIRMATION_MESSAGE_NAME) + ->setHtmlTemplateFileName('mercanet-payment-confirmation.html') + ->setTextTemplateFileName('mercanet-payment-confirmation.txt') + ->setLocale('en_US') + ->setTitle('Mercanet payment confirmation') + ->setSubject('Payment of order {$order_ref}') + ->setLocale('fr_FR') + ->setTitle('Confirmation de paiement par Mercanet') + ->setSubject('Confirmation du paiement de votre commande {$order_ref}') + ->save() + ; + } + } + + /** + * @param ConnectionInterface|null $con + * @param bool $deleteModuleData + * @throws \Propel\Runtime\Exception\PropelException + */ + public function destroy(ConnectionInterface $con = null, $deleteModuleData = false) + { + if ($deleteModuleData) { + MessageQuery::create()->findOneByName(self::CONFIRMATION_MESSAGE_NAME)->delete(); + } + } + + /** + * + * generate a transaction id + * @return int|mixed + */ + private function generateTransactionID() + { + $transId = self::getConfigValue('transactionId', 1); + + $transId = 1 + (int)$transId; + + self::setConfigValue('transactionId', $transId); + + return sprintf('%s%d', uniqid('', false), $transId); + } + + /** + * + * generate a V1 transaction id 'reset every 24 hour + * @return int|mixed + */ + private function generateV1SimplifieTransactionID() + { + $transId = self::getConfigValue('v1TransactionId', 1); + + $transId = 1 + (int)$transId; + + // This ID is supposed unique for a single day. We wiil not rester it everyday, nut we will + // set is to 1 when the limit size (6 digits) is reached. + if ($transId > 999999) { + $transId = 1; + } + + self::setConfigValue('v1TransactionId', $transId); + + return sprintf("%06d", $transId); + } + + /** + * + * 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 Order $order processed order + * @return null|\Thelia\Core\HttpFoundation\Response + * @throws \Propel\Runtime\Exception\PropelException + */ + public function pay(Order $order) + { + $estModeV2Simplifile = (bool) self::getConfigValue('mode_v2_simplifie'); + + $amount = $order->getTotalAmount(); + $customer = $order->getCustomer(); + + /** @var Router $router */ + $router = $this->getContainer()->get('router.mercanet'); + + // Initialisation de la classe Mercanet avec passage en parametre de la cle secrete + $paymentRequest = new MercanetApi(self::getConfigValue('secretKey')); + + if ($estModeV2Simplifile) { + $transactionId = $this->generateV1SimplifieTransactionID(); + $paymentRequest->sets10TransactionId($this->generateV1SimplifieTransactionID()); + } else { + $transactionId = $this->generateTransactionID(); + $paymentRequest->setTransactionReference($transactionId); + } + + // Indiquer quelle page de paiement appeler : TEST ou PRODUCTION + if ('TEST' === self::getConfigValue('mode', 'TEST')) { + $paymentRequest->setUrl(MercanetApi::TEST); + } else { + $paymentRequest->setUrl(MercanetApi::PRODUCTION); + } + + // Renseigner les parametres obligatoires pour l'appel de la page de paiement + $paymentRequest->setMerchantId(self::getConfigValue('merchantId')); + $paymentRequest->setKeyVersion(self::getConfigValue('secretKeyVersion')); + + $paymentRequest->setAmount((int)round(100 * $amount)); + + $paymentRequest->setCurrency($order->getCurrency()->getCode()); + + $paymentRequest->setNormalReturnUrl(URL::getInstance()->absoluteUrl($router->generate('mercanet.payment.manual_response'))); + $paymentRequest->setAutomaticResponseUrl(URL::getInstance()->absoluteUrl($router->generate('mercanet.payment.confirmation'))); + + // Renseigner les parametres facultatifs pour l'appel de la page de paiement + try { + $paymentRequest->setLanguage(substr($order->getLang()->getCode(), 0, 2)); + } catch (\Exception $ex) { + $paymentRequest->setLanguage('en'); + } + + $paymentRequest->setCustomerContactEmail($customer->getEmail()); + $paymentRequest->setOrderId($order->getId()); + $order->setTransactionRef($transactionId)->save(); + + // Verification de la validite des parametres renseignes + $paymentRequest->validate(); + + + // Appel de la page de paiement Mercanet avec le connecteur POST en passant en parametres : Data, InterfaceVersion, Seal + /* + echo "getUrl() . "\">" . + "toParameterString() . "\">" . + "" . + "getShaSign() . "\">" . + "
" . + "" . + ""; + */ + + return $this->generateGatewayFormResponse( + $order, + $paymentRequest->getUrl(), + [ + 'Data' => $paymentRequest->toParameterString(), + 'InterfaceVersion' => MercanetApi::INTERFACE_VERSION, + 'Seal' => $paymentRequest->getShaSign(), + ] + ); + } + + /** + * @return boolean true to allow usage of this payment module, false otherwise. + */ + public function isValidPayment() + { + $valid = (null !== self::getConfigValue('secretKey')) && (null !== self::getConfigValue('merchantId')); + + if ($valid) { + $mode = self::getConfigValue('mode', false); + + // If we're in test mode, do not display Payzen on the front office, except for allowed IP addresses. + if ('TEST' === $mode) { + $raw_ips = explode("\n", self::getConfigValue('allowed_ip_list', '')); + + $allowed_client_ips = array(); + + foreach ($raw_ips as $ip) { + $allowed_client_ips[] = trim($ip); + } + + $client_ip = $this->getRequest()->getClientIp(); + + $valid = in_array($client_ip , $allowed_client_ips , true); + + } elseif ('PRODUCTION' === $mode) { + $valid = true; + } + + if ($valid) { + // Check if total order amount is in the module's limits + $valid = $this->checkMinMaxAmount(); + } + } + + return $valid; + } + + /** + * Check if total order amount is in the module's limits + * + * @return bool true if the current order total is within the min and max limits + */ + protected function checkMinMaxAmount() + { + // Check if total order amount is in the module's limits + $order_total = $this->getCurrentOrderTotalAmount(); + + $min_amount = self::getConfigValue('minimum_amount', 0); + $max_amount = self::getConfigValue('maximum_amount', 0); + + return + $order_total > 0 + && + ($min_amount <= 0 || $order_total >= $min_amount) && ($max_amount <= 0 || $order_total <= $max_amount); + } +} diff --git a/local/modules/Mercanet/Readme.md b/local/modules/Mercanet/Readme.md new file mode 100644 index 000000000..8eb48a997 --- /dev/null +++ b/local/modules/Mercanet/Readme.md @@ -0,0 +1,73 @@ +# Mercanet Payment Module +------------------------ + +## English instructions + +This module offers to your customers the Mercanet payment system, which is used by BNP Paribas. + +The module is based on the "Connecteur POST" (more details here : https://documentation.mercanet.bnpparibas.net/index.php?title=Connecteur_POST ) + +The module supports simplified 1.0 migrated accounts. + +### Installation + +#### Manually + +Install the Mercanet module using the Module page of your back office to upload the archive. + +You can also extract the archive in the `/local/modules` directory. Be sure that the name of the module's directory is `Mercanet` (and not `Mercanet-master`, for example). + +Activate the module from the Modules page of your back-office. + +The module is pre-configured with test shop data (see details here : https://documentation.mercanet.bnpparibas.net/index.php?title=Boutique_de_test, and test card data here https://documentation.mercanet.bnpparibas.net/index.php?title=Cartes_de_test ). + +#### composer + +``` +$ composer require thelia/Mercanet-module:~1.0 +``` + +### Usage + +You have to configure the Mercanet module before starting to use it. To do so, go to the "Modules" tab of your Thelia back-office, and activate the Mercanet module. + +Then click the "Configure" button, and enter the required information. In most case, you'll receive your merchant ID by e-mail, and you'll receive instructions to download your secret key. + +The module performs several checks when the configuration is saved, especially the execution permissions on the Mercanet binaries. + +During the test phase, you can define the IP addresses allowed to use the Mercanet module on the front office, so that your customers will not be able to pay with Mercanet during this test phase. + +A log of Mercanet post-payment callbacks is displayed in the configuration page. + +## Instructions en français + +Ce module permet à vos clients de payer leurs commande par carte bancaire via la plate-forme Mercanet, utilisée par la BNP Paribas. + +Le module estbasé sur le "Connecteur POST" (plus de détails technique ici: https://documentation.mercanet.bnpparibas.net/index.php?title=Connecteur_POST) + +Le module prend en charge les migrations simplifiées de comptes en version 1.0 + +## Installation + +### Manuellement + +Installez ce module directement depuis la page Modules de votre back-office, en envoyant le fichier zip du module. + +Vous pouvez aussi décompresser le module, et le placer manuellement dans le dossier ```/local/modules```. Assurez-vous que le nom du dossier est bien ```Mercanet```, et pas ```Mercanet-master``` + +Le module est préconfiguré avec les données de la boutique de test BNP Paribas (plus de détails ici : https://documentation.mercanet.bnpparibas.net/index.php?title=Boutique_de_test, les détails sur les cartes de test sont ici : https://documentation.mercanet.bnpparibas.net/index.php?title=Cartes_de_test ) + +### composer + +``` +$ composer require thelia/Mercanet-module:~1.0 +``` + + +## Utilisation + +Pour utiliser le module Mercanet, vous devez tout d'abord le configurer. Pour ce faire, rendez-vous dans votre back-office, onglet Modules, et activez le module Mercanet. + +Cliquez ensuite sur "Configurer" sur la ligne du module, et renseignez les informations requises. Dans la plupart des cas, l'ID Marchand vous a été communiqué par votre banque par e-mail, et vous devez recevoir les instructions qui vous permettront de télécharger la clef secrète. + +Lors de la phase de test, vous pouvez définir les adresses IP qui seront autorisées à utiliser le module en front-office, afin de ne pas laisser vos clients payer leur commandes avec Mercanet pendant cette phase. diff --git a/local/modules/Mercanet/composer.json b/local/modules/Mercanet/composer.json new file mode 100644 index 000000000..162585eb3 --- /dev/null +++ b/local/modules/Mercanet/composer.json @@ -0,0 +1,11 @@ +{ + "name": "thelia/mercanet-module", + "license": "LGPL-3.0+", + "type": "thelia-module", + "require": { + "thelia/installer": "~1.1" + }, + "extra": { + "installer-name": "Mercanet" + } +} diff --git a/local/modules/Mercanet/images/logo-mercanet.png b/local/modules/Mercanet/images/logo-mercanet.png new file mode 100644 index 0000000000000000000000000000000000000000..4f156a938a9ce3ca01a99f05b6cb03117527e63d GIT binary patch literal 7606 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 literal 0 HcmV?d00001 diff --git a/local/modules/Mercanet/templates/backOffice/default/mercanet/module-configuration.html b/local/modules/Mercanet/templates/backOffice/default/mercanet/module-configuration.html new file mode 100644 index 000000000..bf6d11695 --- /dev/null +++ b/local/modules/Mercanet/templates/backOffice/default/mercanet/module-configuration.html @@ -0,0 +1,110 @@ +
+
+
+
+ {intl d='mercanet.bo.default' l="Mercanet Configuration"} +
+
+ +
+
+
+ {form name="mercanet_configuration"} +
+ {form_hidden_fields form=$form} + + {include file = "includes/inner-form-toolbar.html" + hide_flags = true + page_url = "{url path='/admin/module/Mercanet'}" + close_url = "{url path='/admin/modules'}" + } + + {if $form_error} +
+
+
{$form_error_message}
+
+
+ {/if} + +
+
+

{intl d='mercanet.bo.default' l="Mercanet Platform configuration"}

+ + {render_form_field form=$form field="merchantId" value=$merchantId} + {render_form_field form=$form field="secretKey" value=$secretKey} + {render_form_field form=$form field="secretKeyVersion" value=$secretKeyVersion} +
+ +
+

{intl d='mercanet.bo.default' l="Operation mode"}

+ + {render_form_field form=$form field="mode_v2_simplifie" value=$mode_v2_simplifie} + {render_form_field form=$form field="mode" value=$mode} + {render_form_field form=$form field="allowed_ip_list" value=$allowed_ip_list} + +

{intl d='mercanet.bo.default' l="Payment by N installment"}

+ + {loop name="multi-plugin-enabled" type="module" code="MercanetNx" active="1"}{/loop} + {elseloop rel="multi-plugin-enabled"} +
+ {intl l="Install and activate Mercanet multiple times payment module (MercanetNx) to get configuration options." d='mercanet.bo'} +
+ {/elseloop} + + {render_form_field form=$form field="nx_nb_installments" value=$nx_nb_installments|default:3} + {render_form_field form=$form field="nx_minimum_amount" value=$nx_minimum_amount|default:0} + {render_form_field form=$form field="nx_maximum_amount" value=$nx_maximum_amount|default:0} +
+ +
+

{intl d='mercanet.bo.default' l="Payment configuration"}

+ + {custom_render_form_field form=$form field="send_confirmation_message_only_if_paid"} + + {$label} + {/custom_render_form_field} + + {custom_render_form_field form=$form field="send_payment_confirmation_message"} + + {$label} + {/custom_render_form_field} + +
+ + {intl d='mercanet.bo.default' l='You can edit the payment confirmation email sent to the customer after a successful payment.' url={url path="/admin/configuration/messages"}} +
+ + {render_form_field form=$form field="minimum_amount" value=$minimum_amount} + {render_form_field form=$form field="maximum_amount" value=$maximum_amount} +
+
+
+ {/form} + +
+
+
+
+

+ + {intl d='mercanet.bo.default' l="Mercanet call log to callback URL"} +

+
+
+
+ {$trace_content nofilter} +
+
+ +
+
+
+ +
+
+
+
+
diff --git a/local/modules/Mercanet/templates/email/default/mercanet-payment-confirmation.html b/local/modules/Mercanet/templates/email/default/mercanet-payment-confirmation.html new file mode 100644 index 000000000..e9e324f70 --- /dev/null +++ b/local/modules/Mercanet/templates/email/default/mercanet-payment-confirmation.html @@ -0,0 +1,23 @@ +{extends file="email-layout.tpl"} + +{* Do not provide a "Open in browser" link *} +{block name="browser"}{/block} +{* No pre-header *} +{block name="pre-header"}{/block} + +{* Subject *} +{block name="email-subject"}{intl d='mercanet.email.default' l="Payment of your order %ref" ref=$order_ref}{/block} + +{* Title *} +{block name="email-title"}{intl d='mercanet.email.default' l="The payment of your order %ref with Mercanet is confirmed" ref=$order_ref}{/block} + +{* Content *} +{block name="email-content"} +

+ + {intl l="View this order in your account at %shop_name" shop_name={config key="store_name"}} + +

+

{intl d='mercanet.email.default' l='Thank you again for your purchase.'}

+

{intl d='mercanet.email.default' l='The %store_name team.' store_name={config key="store_name"}}

+{/block} diff --git a/local/modules/Mercanet/templates/email/default/mercanet-payment-confirmation.txt b/local/modules/Mercanet/templates/email/default/mercanet-payment-confirmation.txt new file mode 100644 index 000000000..669f96923 --- /dev/null +++ b/local/modules/Mercanet/templates/email/default/mercanet-payment-confirmation.txt @@ -0,0 +1,5 @@ +{intl d='mercanet.email.default' l='Dear customer'},
+{intl d='mercanet.email.default' l='This is a confirmation of the payment of your order %ref with Mercanet on our shop.' ref=$order_ref}
+{intl d='mercanet.email.default' l='Your invoice is now available in your customer account at %url.'} url={config key="url_site"}}
+{intl d='mercanet.email.default' l='Thank you again for your purchase.'}
+{intl d='mercanet.email.default' l='The %store_name team.' store_name={config key="store_name"}}