Il manquait des fichiers dans le module Mercanet (sur Git)

This commit is contained in:
2024-01-12 10:37:52 +01:00
parent 4054fddbe5
commit 485580e0b2
22 changed files with 2034 additions and 0 deletions

View File

@@ -0,0 +1,537 @@
<?php
/*************************************************************************************/
/* Copyright (c) Franck Allimant, CQFDev */
/* email : thelia@cqfdev.fr */
/* web : http://www.cqfdev.fr */
/* */
/* For the full copyright and license information, please view the LICENSE */
/* file that was distributed with this source code. */
/*************************************************************************************/
/**
* Created by Franck Allimant, CQFDev <franck@cqfdev.fr>
* 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;
}
}

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" ?>
<config xmlns="http://thelia.net/schema/dic/config"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://thelia.net/schema/dic/config http://thelia.net/schema/dic/config/thelia-1.0.xsd">
<forms>
<form name="mercanet_configuration" class="Mercanet\Form\ConfigForm" />
</forms>
<services>
<service id="mercanet.confirmation.email" class="Mercanet\EventListeners\SendConfirmationEmail" scope="request">
<argument type="service" id="mailer"/>
<tag name="kernel.event_subscriber"/>
</service>
</services>
<hooks>
<hook id="mercanet.configuration.hook" class="Mercanet\Hook\HookManager" scope="request">
<tag name="hook.event_listener" event="module.configuration" type="back" method="onModuleConfigure" />
</hook>
</hooks>
</config>

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<module xmlns="http://thelia.net/schema/dic/module"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://thelia.net/schema/dic/module http://thelia.net/schema/dic/module/module-2_2.xsd">
<fullnamespace>Mercanet\Mercanet</fullnamespace>
<descriptive locale="en_US">
<title>Credit card payment with BNP Paribas Mercanet</title>
</descriptive>
<descriptive locale="fr_FR">
<title>Paiement par carte bancaire avec BNP Paribas Mercanet</title>
</descriptive>
<!-- <logo></logo> -->
<images-folder>images</images-folder>
<languages>
<language>en_US</language>
<language>fr_FR</language>
</languages>
<version>1.0.6</version>
<authors>
<author>
<name>Franck Allimant</name>
<company>CQFDev</company>
<email>thelia@cqfdev.fr</email>
<website>www.cqfdev.fr</website>
</author>
</authors>
<type>payment</type>
<thelia>2.3.0</thelia>
<stability>prod</stability>
<mandatory>0</mandatory>
<hidden>0</hidden>
</module>

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">
<route id="mercanet.config" path="/admin/module/mercanet/configure" methods="post">
<default key="_controller">Mercanet\Controller\ConfigureController::configure</default>
</route>
<route id="mercanet.download.log" path="/admin/module/mercanet/log">
<default key="_controller">Mercanet\Controller\ConfigureController::downloadLog</default>
</route>
<route id="mercanet.logo" path="mercanet/logo/{image}">
<default key="_controller">Mercanet\Controller\PaymentController::displayLogo</default>
</route>
<route id="mercanet.payment.manual_response" path="mercanet/manual-response" methods="post">
<default key="_controller">Mercanet\Controller\PaymentController::processManualResponse</default>
</route>
<route id="mercanet.payment.confirmation" path="mercanet/callback" methods="post">
<default key="_controller">Mercanet\Controller\PaymentController::processMercanetRequest</default>
</route>
<route id="mercanet.payment.cancel" path="mercanet/cancel/{orderId}">
<default key="_controller">Mercanet\Controller\PaymentController::processUserCancel</default>
<requirement key="orderId">\d+</requirement>
</route>
</routes>

View File

@@ -0,0 +1,104 @@
<?php
/*************************************************************************************/
/* Copyright (c) Franck Allimant, CQFDev */
/* email : thelia@cqfdev.fr */
/* web : http://www.cqfdev.fr */
/* */
/* For the full copyright and license information, please view the LICENSE */
/* file that was distributed with this source code. */
/*************************************************************************************/
namespace Mercanet\Controller;
use Mercanet\Mercanet;
use Symfony\Component\HttpFoundation\Response;
use Thelia\Controller\Admin\BaseAdminController;
use Thelia\Core\Security\AccessManager;
use Thelia\Core\Security\Resource\AdminResources;
use Thelia\Form\Exception\FormValidationException;
use Thelia\Tools\URL;
/**
* Class ConfigureController
* @package Mercanet\Controller
* @author Franck Allimant <franck@cqfdev.fr>
*/
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'));
}
}

View File

@@ -0,0 +1,242 @@
<?php
/*************************************************************************************/
/* Copyright (c) Franck Allimant, CQFDev */
/* email : thelia@cqfdev.fr */
/* web : http://www.cqfdev.fr */
/* */
/* For the full copyright and license information, please view the LICENSE */
/* file that was distributed with this source code. */
/*************************************************************************************/
namespace Mercanet\Controller;
use Mercanet\Api\MercanetApi;
use Mercanet\Mercanet;
use Thelia\Core\Event\Order\OrderEvent;
use Thelia\Core\Event\TheliaEvents;
use Thelia\Core\HttpFoundation\Response;
use Thelia\Exception\TheliaProcessException;
use Thelia\Model\OrderQuery;
use Thelia\Model\OrderStatusQuery;
use Thelia\Module\BasePaymentModuleController;
/**
* Class PaymentController
* @package Mercanet\Controller
* Franck Allimant <franck@cqfdev.fr>
*/
class PaymentController extends BasePaymentModuleController
{
protected static $resultCodes = [
'00' => ' Transaction acceptée',
'02' => ' Demande dautorisation par téléphone à la banque à cause dun dépassement du plafond dautorisation 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 lacheteur',
'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';
}
}

View File

@@ -0,0 +1,94 @@
<?php
/*************************************************************************************/
/* Copyright (c) Franck Allimant, CQFDev */
/* email : thelia@cqfdev.fr */
/* web : http://www.cqfdev.fr */
/* */
/* For the full copyright and license information, please view the LICENSE */
/* file that was distributed with this source code. */
/*************************************************************************************/
namespace Mercanet\EventListeners;
use Mercanet\Mercanet;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Thelia\Core\Event\Order\OrderEvent;
use Thelia\Core\Event\TheliaEvents;
use Thelia\Log\Tlog;
use Thelia\Mailer\MailerFactory;
/**
* Class SendConfirmationEmail
*
* @package Mercanet\EventListeners
* @author Franck Allimant <franck@cqfdev.fr>
*/
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)
);
}
}

View File

@@ -0,0 +1,281 @@
<?php
/*************************************************************************************/
/* Copyright (c) Franck Allimant, CQFDev */
/* email : thelia@cqfdev.fr */
/* web : http://www.cqfdev.fr */
/* */
/* For the full copyright and license information, please view the LICENSE */
/* file that was distributed with this source code. */
/*************************************************************************************/
namespace Mercanet\Form;
use Mercanet\Mercanet;
use Symfony\Component\Validator\Constraints\GreaterThanOrEqual;
use Symfony\Component\Validator\Constraints\NotBlank;
use Thelia\Core\Translation\Translator;
use Thelia\Form\BaseForm;
use Thelia\Model\Module;
use Thelia\Model\ModuleQuery;
/**
* Class ConfigForm
* @package Mercanet\Form
* @author Franck Allimant <franck@cqfdev.fr>
*/
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';
}
}

View File

@@ -0,0 +1,72 @@
<?php
/*************************************************************************************/
/* Copyright (c) Franck Allimant, CQFDev */
/* email : thelia@cqfdev.fr */
/* web : http://www.cqfdev.fr */
/* */
/* For the full copyright and license information, please view the LICENSE */
/* file that was distributed with this source code. */
/*************************************************************************************/
namespace Mercanet\Hook;
use Mercanet\Mercanet;
use Thelia\Core\Event\Hook\HookRenderEvent;
use Thelia\Core\Hook\BaseHook;
use Thelia\Model\ModuleConfig;
use Thelia\Model\ModuleConfigQuery;
/**
* Class HookManager
* @package Mercanet\Hook
* @author Franck Allimant <franck@cqfdev.fr>
*/
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)
);
}
}

View File

@@ -0,0 +1,13 @@
<?php
return array(
'Mercanet Configuration' => '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 <a href="%url">edit the payment confirmation email</a> sent to the customer after a successful payment.' => 'You can <a href="%url">edit the payment confirmation email</a> sent to the customer after a successful payment.',
);

View File

@@ -0,0 +1,13 @@
<?php
return array(
'Download full log' => '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 <a href="%url">edit the payment confirmation email</a> sent to the customer after a successful payment.' => 'Vous pouvez <a href="%url">modifier le mail de confirmation de paiement</a> envoyé au client.',
);

View File

@@ -0,0 +1,12 @@
<?php
return array(
'Dear customer' => '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.',
);

View File

@@ -0,0 +1,12 @@
<?php
return array(
'Dear customer' => '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',
);

View File

@@ -0,0 +1,44 @@
<?php
return array(
'(Previous log is in %file file.)\n' => '(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',
);

View File

@@ -0,0 +1,33 @@
<?php
return array(
'(Previous log is in %file file.)\n' => '(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',
);

View File

@@ -0,0 +1,270 @@
<?php
/*************************************************************************************/
/* Copyright (c) Franck Allimant, CQFDev */
/* email : thelia@cqfdev.fr */
/* web : http://www.cqfdev.fr */
/* */
/* For the full copyright and license information, please view the LICENSE */
/* file that was distributed with this source code. */
/*************************************************************************************/
namespace Mercanet;
use Mercanet\Api\MercanetApi;
use Propel\Runtime\Connection\ConnectionInterface;
use Symfony\Component\Routing\Router;
use Thelia\Model\Message;
use Thelia\Model\MessageQuery;
use Thelia\Model\Order;
use Thelia\Module\AbstractPaymentModule;
use Thelia\Tools\URL;
/**
* Class Mercanet
* @package Mercanet
* @author Franck Allimant <franck@cqfdev.fr>
*/
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 "<html><body><form name=\"redirectForm\" method=\"POST\" action=\"" . $paymentRequest->getUrl() . "\">" .
"<input type=\"hidden\" name=\"Data\" value=\"". $paymentRequest->toParameterString() . "\">" .
"<input type=\"hidden\" name=\"InterfaceVersion\" value=\"". self::INTERFACE_VERSION . "\">" .
"<input type=\"hidden\" name=\"Seal\" value=\"" . $paymentRequest->getShaSign() . "\">" .
"<noscript><input type=\"submit\" name=\"Go\" value=\"Click to continue\"/></noscript> </form>" .
"<script type=\"text/javascript\"> document.redirectForm.submit(); </script>" .
"</body></html>";
*/
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);
}
}

View File

@@ -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 `<thelia root>/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 ```<thelia_root>/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.

View File

@@ -0,0 +1,11 @@
{
"name": "thelia/mercanet-module",
"license": "LGPL-3.0+",
"type": "thelia-module",
"require": {
"thelia/installer": "~1.1"
},
"extra": {
"installer-name": "Mercanet"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@@ -0,0 +1,110 @@
<div class="row">
<div class="col-md-12 general-block-decorator">
<div class="row">
<div class="col-md-12 title title-without-tabs">
{intl d='mercanet.bo.default' l="Mercanet Configuration"}
</div>
</div>
<div class="form-container">
<div class="row">
<div class="col-md-12">
{form name="mercanet_configuration"}
<form action="{url path="/admin/module/mercanet/configure"}" method="post">
{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}
<div class="row">
<div class="col-md-12">
<div class="alert alert-danger">{$form_error_message}</div>
</div>
</div>
{/if}
<div class="row">
<div class="col-md-4">
<p class="title title-without-tabs">{intl d='mercanet.bo.default' l="Mercanet Platform configuration"}</p>
{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}
</div>
<div class="col-md-4">
<p class="title title-without-tabs">{intl d='mercanet.bo.default' l="Operation mode"}</p>
{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}
<p class="title title-without-tabs">{intl d='mercanet.bo.default' l="Payment by N installment"}</p>
{loop name="multi-plugin-enabled" type="module" code="MercanetNx" active="1"}{/loop}
{elseloop rel="multi-plugin-enabled"}
<div class="alert alert-info">
{intl l="Install and activate Mercanet multiple times payment module (MercanetNx) to get configuration options." d='mercanet.bo'}
</div>
{/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}
</div>
<div class="col-md-4">
<p class="title title-without-tabs">{intl d='mercanet.bo.default' l="Payment configuration"}</p>
{custom_render_form_field form=$form field="send_confirmation_message_only_if_paid"}
<input type="checkbox" {form_field_attributes form=$form field="send_confirmation_message_only_if_paid"} {if $send_confirmation_message_only_if_paid}checked{/if}>
{$label}
{/custom_render_form_field}
{custom_render_form_field form=$form field="send_payment_confirmation_message"}
<input type="checkbox" {form_field_attributes form=$form field="send_payment_confirmation_message"} {if $send_payment_confirmation_message}checked{/if}>
{$label}
{/custom_render_form_field}
<div class="well well-sm">
<span class="glyphicon glyphicon-info-sign"></span>
{intl d='mercanet.bo.default' l='You can <a href="%url">edit the payment confirmation email</a> sent to the customer after a successful payment.' url={url path="/admin/configuration/messages"}}
</div>
{render_form_field form=$form field="minimum_amount" value=$minimum_amount}
{render_form_field form=$form field="maximum_amount" value=$maximum_amount}
</div>
</div>
</form>
{/form}
<div class="row">
<div class="col-xs-12">
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">
<span class="glyphicon glyphicon-cog"></span>
{intl d='mercanet.bo.default' l="Mercanet call log to callback URL"}
</h3>
</div>
<div class="panel-body">
<div id="log-container" style="font-family: monospace; font-size: 12px; max-height: 400px; overflow-y: scroll">
{$trace_content nofilter}
</div>
</div>
<div class="panel-footer">
<a href="{url path='/admin/module/mercanet/log'}" class="btn btn-sm btn-primary">{intl d='mercanet.bo.default' l="Download full log"}</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -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"}
<p>
<a href="{url path="/account"}">
{intl l="View this order in your account at %shop_name" shop_name={config key="store_name"}}
</a>
</p>
<p>{intl d='mercanet.email.default' l='Thank you again for your purchase.'}</p>
<p>{intl d='mercanet.email.default' l='The %store_name team.' store_name={config key="store_name"}}</p>
{/block}

View File

@@ -0,0 +1,5 @@
{intl d='mercanet.email.default' l='Dear customer'},<br>
{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}<br>
{intl d='mercanet.email.default' l='Your invoice is now available in your customer account at %url.'} url={config key="url_site"}}<br>
{intl d='mercanet.email.default' l='Thank you again for your purchase.'}<br>
{intl d='mercanet.email.default' l='The %store_name team.' store_name={config key="store_name"}}<br>