Il manquait quelques fichiers dans Git

This commit is contained in:
2024-01-18 12:02:32 +01:00
parent 485580e0b2
commit 15053f58f2
194 changed files with 63120 additions and 9403 deletions

146
local/modules/Axepta/Axepta.php Executable file
View File

@@ -0,0 +1,146 @@
<?php
/*************************************************************************************/
/* This file is part of the Thelia package. */
/* */
/* Copyright (c) OpenStudio */
/* email : dev@thelia.net */
/* web : http://www.thelia.net */
/* */
/* For the full copyright and license information, please view the LICENSE.txt */
/* file that was distributed with this source code. */
/*************************************************************************************/
namespace Axepta;
use Axepta\Util\Axepta as AxeptaPayment;
use Thelia\Core\Translation\Translator;
use Thelia\Log\Tlog;
use Thelia\Model\Order;
use Thelia\Module\AbstractPaymentModule;
use Thelia\Tools\MoneyFormat;
use Thelia\Tools\URL;
class Axepta extends AbstractPaymentModule
{
/** @var string */
public const DOMAIN_NAME = 'axepta';
public const MERCHANT_ID = 'merchant_id';
public const HMAC = 'hmac';
public const CRYPT_KEY = 'crypt_key';
public const MODE = 'run_mode';
public const ALLOWED_IP_LIST = 'allowed_ip_list';
public const MINIMUM_AMOUNT = 'minimum_amount';
public const MAXIMUM_AMOUNT = 'maximum_amount';
public const SEND_CONFIRMATION_MESSAGE_ONLY_IF_PAID = 'send_confirmation_message_only_if_paid';
public function pay(Order $order)
{
$hmac = self::getConfigValue(self::HMAC, null);
$merchantId = self::getConfigValue(self::MERCHANT_ID, null);
$cryptKey = self::getConfigValue(self::CRYPT_KEY, null);
$mode = self::getConfigValue(self::MODE, null);
$urlAnnulation = $this->getPaymentFailurePageUrl($order->getId(), Translator::getInstance()->trans('Vous avez annulé le paiement', [], Axepta::DOMAIN_NAME));
$urlNotification = URL::getInstance()->absoluteUrl('/axepta/notification');
$paymentRequest = new AxeptaPayment($hmac);
$paymentRequest->setCryptKey($cryptKey);
$transId = time().$order->getId();
$paymentRequest->setUrl(AxeptaPayment::PAYSSL);
$paymentRequest->setMerchantID($merchantId);
$paymentRequest->setTransID($transId);
$paymentRequest->setAmount((int) ($order->getTotalAmount()*100));
$paymentRequest->setCurrency($order->getCurrency()->getCode());
$paymentRequest->setRefNr($order->getRef());
$paymentRequest->setURLSuccess($urlNotification);
$paymentRequest->setURLFailure($urlNotification);
$paymentRequest->setURLNotify($urlNotification);
$paymentRequest->setURLBack($urlAnnulation);
$paymentRequest->setReponse('encrypt');
$paymentRequest->setLanguage($this->getRequest()->getSession()->getLang()->getLocale());
if ($mode === 'TEST') {
// See https://docs.axepta.bnpparibas/display/DOCBNP/Test+environment
// In the encrypted data request, use the default parameter OrderDesc with the value "Test:0000". This will give you a correspondingly successful authorization after successful authentication.
$paymentRequest->setOrderDesc('Test:0000');
} else {
$paymentRequest->setOrderDesc($order->getCustomer()->getFirstname() . ' ' . $order->getCustomer()->getLastname());
}
$paymentRequest->validate();
$mac = $paymentRequest->getShaSign();
$data = $paymentRequest->getBfishCrypt();
$len = $paymentRequest->getLen();
$transmit = [
'MerchantID' => $paymentRequest->getMerchantID(),
'Len' => $len,
'Data' => $data,
'URLBack' => $urlAnnulation,
'CustomField1' => sprintf(
"%s, %s",
MoneyFormat::getInstance($this->getRequest())->format($order->getTotalAmount(), 2),
$order->getCurrency()->getCode()
),
'CustomField2' => $order->getRef()
];
TLog::getInstance()->error("Données Axcepta : " . print_r($paymentRequest->parameters, 1));
TLog::getInstance()->error("URL Axcepta : " . $paymentRequest->getUrl());
$order
->setTransactionRef($transId)
->save();
return $this->generateGatewayFormResponse($order, $paymentRequest->getUrl(), $transmit);
}
public function isValidPayment()
{
$hmac = self::getConfigValue(self::HMAC, null);
$merchantId = self::getConfigValue(self::MERCHANT_ID, null);
$cryptKey = self::getConfigValue(self::CRYPT_KEY, null);
$mode = self::getConfigValue(self::MODE, null);
$valid = true;
if (($hmac === null || $merchantId === null || $cryptKey === null) && $mode !== 'TEST') {
Tlog::getInstance()->errro("Axepta module is not properly configured, some configuration data are missing.");
return false;
}
if ($mode === 'TEST') {
$raw_ips = explode("\n", self::getConfigValue(self::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) || in_array('*', $allowed_client_ips);
}
if ($valid) {
// Check if total order amount is in the module's limits
$valid = $this->checkMinMaxAmount(self::MINIMUM_AMOUNT, self::MAXIMUM_AMOUNT);
}
return $valid;
}
protected function checkMinMaxAmount($min, $max)
{
$order_total = $this->getCurrentOrderTotalAmount();
$min_amount = self::getConfigValue($min, 0);
$max_amount = self::getConfigValue($max, 0);
return $order_total > 0 && ($min_amount <= 0 || $order_total >= $min_amount) && ($max_amount <= 0 || $order_total <= $max_amount);
}
}

View File

@@ -0,0 +1,25 @@
<?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="axepta_configuration" class="Axepta\Form\ConfigurationForm" />
</forms>
<services>
<service id="axepta.send.confirmation_mail" class="Axepta\EventListeners\SendConfirmationEmail">
<tag name="kernel.event_subscriber"/>
</service>
</services>
<hooks>
<hook id="axepta.configuration.hook" class="Axepta\Hook\HookManager">
<tag name="hook.event_listener" event="module.configuration" type="back" method="onModuleConfigure" />
</hook>
<hook id="axepta.orderpaymentgateway.hook" class="Axepta\Hook\HookManager">
<tag name="hook.event_listener" event="order-payment-gateway.javascript" type="front" method="onOrderPaymentGatewayJavascript" />
</hook>
</hooks>
</config>

View File

@@ -0,0 +1,28 @@
<?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>Axepta\Axepta</fullnamespace>
<descriptive locale="en_US">
<title>Payement with Axepta</title>
</descriptive>
<descriptive locale="fr_FR">
<title>Paiement avec Axepta</title>
</descriptive>
<languages>
<language>en_US</language>
<language>fr_FR</language>
</languages>
<version>1.0.1</version>
<authors>
<author>
<name>Nicolas Barbey</name>
<email>nbarbey@openstudio.fr</email>
</author>
</authors>
<type>classic</type>
<thelia>2.3.0</thelia>
<stability>other</stability>
<mandatory>0</mandatory>
<hidden>0</hidden>
</module>

View File

@@ -0,0 +1,14 @@
<?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="axepta.configure" path="/admin/module/axepta/configure" methods="post">
<default key="_controller">Axepta\Controller\ConfigurationController::configure</default>
</route>
<route id="axepta.notification_url" path="/axepta/notification">
<default key="_controller">Axepta\Controller\NotificationController::notificationAction</default>
</route>
</routes>

View File

@@ -0,0 +1,80 @@
<?php
namespace Axepta\Controller;
use Axepta\Axepta;
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 ConfigurationController extends BaseAdminController
{
public function configure()
{
if (null !== $response = $this->checkAuth(AdminResources::MODULE, 'Axepta', AccessManager::UPDATE)) {
return $response;
}
// Create the Form from the request
$configurationForm = $this->createForm('axepta_configuration');
try {
// Check the form against constraints violations
$form = $this->validateForm($configurationForm, "POST");
// Get the form field values
$data = $form->getData();
foreach ($data as $name => $value) {
if (is_array($value)) {
$value = implode(';', $value);
}
Axepta::setConfigValue($name, $value);
}
// Log configuration modification
$this->adminLogAppend(
"axepta.configuration.message",
AccessManager::UPDATE,
"Axepta 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/
$route = '/admin/module/Axepta';
} else {
// If we have to close the page, go back to the module back-office page.
$route = '/admin/modules';
}
return $this->generateRedirect(URL::getInstance()->absoluteUrl($route));
// An exit is performed after redirect.+
} catch (FormValidationException $ex) {
// Form cannot be validated. Create the error message using
// the BaseAdminController helper method.
$error_msg = $this->createStandardFormValidationErrorMessage($ex);
} catch (\Exception $ex) {
// Any other error
$error_msg = $ex->getMessage();
}
// At this point, the form has errors, and should be redisplayed. We do not redirect,
// just redisplay the same template.
// Set up the Form error context, to make error information available in the template.
$this->setupFormErrorContext(
$this->getTranslator()->trans("Axepta configuration", [], Axepta::DOMAIN_NAME),
$error_msg,
$configurationForm,
$ex
);
// Do not redirect at this point, or the error context will be lost.
// Just redisplay the current template.
return $this->render('module-configure', array('module_code' => 'Payline'));
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Axepta\Controller;
use Axepta\Axepta;
use Axepta\Util\Axepta as AxeptaPayment;
use Thelia\Core\Event\Order\OrderEvent;
use Thelia\Core\Event\TheliaEvents;
use Thelia\Core\Translation\Translator;
use Thelia\Exception\TheliaProcessException;
use Thelia\Model\Base\OrderQuery;
use Thelia\Model\OrderStatusQuery;
use Thelia\Module\BasePaymentModuleController;
class NotificationController extends BasePaymentModuleController
{
protected function getModuleCode()
{
return 'Axepta';
}
/**
* @throws \Propel\Runtime\Exception\PropelException
*/
public function notificationAction()
{
$this->getLog()->addInfo("Processing Axcepta notification");
$paymentResponse = new AxeptaPayment(Axepta::getConfigValue(Axepta::HMAC));
$paymentResponse->setCryptKey(Axepta::getConfigValue(Axepta::CRYPT_KEY));
$paymentResponse->setResponse($this->getRequest()->query->all());
$this->getLog()->addError("Notification parameters: ".print_r($paymentResponse->parameters, 1));
$transId = $paymentResponse->getTransID();
if (null === $order = OrderQuery::create()->filterByTransactionRef($transId)->findOne()) {
$this->getLog()->addInfo("Failed to fin order for transaction ID $transId. Aborting.");
throw new TheliaProcessException(
Translator::getInstance()->trans("Failed to find order for transaction ID %id", ['id' => $transId ], Axepta::DOMAIN_NAME)
);
}
$this->getLog()->addInfo("Processing payment of order " . $order->getRef());
$event = new OrderEvent($order);
if ($paymentResponse->isValid() && $paymentResponse->isSuccessful()) {
$this->getLog()->addInfo("Payment of order ".$order->getRef()." is successful.");
if (!$order->isPaid()) {
$this->getLog()->addInfo("Setting order status to 'paid'.");
$event->setStatus(OrderStatusQuery::getPaidStatus()->getId());
$this->dispatch(TheliaEvents::ORDER_UPDATE_STATUS, $event);
}
$this->redirectToSuccessPage($order->getId());
}
$this->getLog()->addInfo("Payment failed, cancelling order " . $order->getRef());
$event->setStatus(OrderStatusQuery::getCancelledStatus()->getId());
$this->dispatch(TheliaEvents::ORDER_UPDATE_STATUS, $event);
$this->getLog()->addInfo("Failure cause:".$paymentResponse->getDescription() . ' ('.$paymentResponse->getCode());
$this->redirectToFailurePage($order->getId(), $paymentResponse->getDescription() . ' ('.$paymentResponse->getCode().')');
}
}

View File

@@ -0,0 +1,82 @@
<?php
/*************************************************************************************/
/* */
/* Thelia */
/* */
/* Copyright (c) OpenStudio */
/* email : info@thelia.net */
/* web : http://www.thelia.net */
/* */
/* This program is free software; you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation; either version 3 of the License */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
/* */
/*************************************************************************************/
namespace Axepta\EventListeners;
use Axepta\Axepta;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Thelia\Action\BaseAction;
use Thelia\Core\Event\Order\OrderEvent;
use Thelia\Core\Event\TheliaEvents;
/**
* Axepta payment module
*
* @author Franck Allimant <franck@cqfdev.fr>
*/
class SendConfirmationEmail extends BaseAction implements EventSubscriberInterface
{
/**
* @param OrderEvent $event
*
* @throws \Exception if the message cannot be loaded.
*/
public function sendConfirmationEmail(OrderEvent $event)
{
if (Axepta::getConfigValue(Axepta::SEND_CONFIRMATION_MESSAGE_ONLY_IF_PAID, true)) {
// We send the order confirmation email only if the order is paid
$order = $event->getOrder();
if (!$order->isPaid() && $order->getPaymentModuleId() === (int) Axepta::getModuleId()) {
$event->stopPropagation();
}
}
}
/**
* Checks if order payment module is Axepta and if order new status is paid, send a confirmation 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() === Axepta::getModuleId()) {
$dispatcher->dispatch(TheliaEvents::ORDER_SEND_CONFIRMATION_EMAIL, $event);
}
}
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,150 @@
<?php
namespace Axepta\Form;
use Axepta\Axepta;
use Payline\Payline;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Validator\Constraints\GreaterThanOrEqual;
use Symfony\Component\Validator\Constraints\NotBlank;
use Thelia\Core\Translation\Translator;
use Thelia\Form\BaseForm;
class ConfigurationForm extends BaseForm
{
protected function buildForm()
{
$this->formBuilder
->add(
Axepta::MODE,
ChoiceType::class,
[
'constraints' => [new NotBlank()],
'required' => true,
'choices' => [
'TEST' => 'Test',
'PRODUCTION' => 'Production',
],
'label' => $this->trans('Mode de fonctionnement', []),
'data' => Axepta::getConfigValue(Axepta::MODE),
]
)
->add(
Axepta::MERCHANT_ID,
TextType::class,
[
'constraints' => [new NotBlank()],
'required' => true,
'label' => $this->trans('Merchant ID'),
'data' => Axepta::getConfigValue(Axepta::MERCHANT_ID, ''),
]
)
->add(
Axepta::HMAC,
TextType::class,
[
'constraints' => [new NotBlank()],
'required' => true,
'label' => $this->trans('HMAC key'),
'data' => Axepta::getConfigValue(Axepta::HMAC, ''),
]
)
->add(
Axepta::CRYPT_KEY,
TextType::class,
[
'constraints' => [new NotBlank()],
'required' => true,
'label' => $this->trans('Blowfish encryption key'),
'data' => Axepta::getConfigValue(Axepta::CRYPT_KEY, ''),
]
)
->add(
Axepta::ALLOWED_IP_LIST,
TextareaType::class,
[
'required' => false,
'label' => $this->trans('Allowed IPs in test mode'),
'data' => Axepta::getConfigValue(Axepta::ALLOWED_IP_LIST),
'label_attr' => array(
'for' => Axepta::ALLOWED_IP_LIST,
'help' => $this->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',
array('%ip' => $this->getRequest()->getClientIp())
),
'rows' => 3
)
]
)
->add(
Axepta::MINIMUM_AMOUNT,
NumberType::class,
array(
'constraints' => array(
new NotBlank(),
new GreaterThanOrEqual(array('value' => 0))
),
'required' => true,
'label' => $this->trans('Minimum order total'),
'data' => Axepta::getConfigValue(Axepta::MINIMUM_AMOUNT, 0),
'label_attr' => array(
'for' => 'minimum_amount',
'help' => $this->trans('Minimum order total in the default currency for which this payment method is available. Enter 0 for no minimum')
),
'attr' => [
'step' => 'any'
]
)
)
->add(
Axepta::MAXIMUM_AMOUNT,
NumberType::class,
array(
'constraints' => array(
new NotBlank(),
new GreaterThanOrEqual(array('value' => 0))
),
'required' => true,
'label' => $this->trans('Maximum order total'),
'data' => Axepta::getConfigValue(Axepta::MAXIMUM_AMOUNT, 0),
'label_attr' => array(
'for' => 'maximum_amount',
'help' => $this->trans('Maximum order total in the default currency for which this payment method is available. Enter 0 for no maximum')
),
'attr' => [
'step' => 'any'
]
)
)
->add(
Axepta::SEND_CONFIRMATION_MESSAGE_ONLY_IF_PAID,
'checkbox',
[
'value' => 1,
'required' => false,
'label' => $this->trans('Send order confirmation on payment success'),
'data' => (boolean)(Axepta::getConfigValue(Axepta::SEND_CONFIRMATION_MESSAGE_ONLY_IF_PAID, true)),
'label_attr' => [
'help' => $this->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'
)
]
]
)
;
}
public function getName()
{
return 'axepta_configuration';
}
protected function trans($str, $params = [])
{
return Translator::getInstance()->trans($str, $params, Axepta::DOMAIN_NAME);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Axepta\Hook;
use Thelia\Core\Event\Hook\HookRenderEvent;
use Thelia\Core\Hook\BaseHook;
class HookManager extends BaseHook
{
public function onModuleConfigure(HookRenderEvent $event)
{
$event->add(
$this->render('module-configuration.html')
);
}
public function onOrderPaymentGatewayJavascript(HookRenderEvent $event)
{
$event->add(
$this->render('hook/order-payment-gateway-javascript.html')
);
}
}

View File

@@ -0,0 +1,6 @@
<?php
return array(
'Access to Axepta Platform' => 'Accès a Axepta',
'Payment configuration' => 'Configuration de paiement',
);

View File

@@ -0,0 +1,6 @@
<?php
return array(
'Mode de fonctionnement' => 'Operation Mode',
'Vous avez annulé le paiement' => 'You have cancel the payment',
);

View File

@@ -0,0 +1,16 @@
<?php
return array(
'Allowed IPs in test mode' => 'Adresse IP autorisé en mode de test ',
'Blowfish encryption key' => 'Clé de cryptage Blowfish',
'Failed to find order reference %ref' => 'La référence de commande %ref n\'a pas été trouvé',
'HMAC key' => 'Clé HMAC',
'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 adresses IP qui pourront choisir ce module de paiement en front-office pendant la phase de test (votre IP est 127.0.0.1). Une adresse par ligne.',
'Maximum order total' => 'Montant maximum de commande',
'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',
'Merchant ID' => 'Id du commerçant',
'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',
'Send order confirmation on payment success' => 'Confirmation de commande si le paiement réussit',
);

27
local/modules/Axepta/Readme.md Executable file
View File

@@ -0,0 +1,27 @@
# Axepta
This module adds the payment solution Axepta.
## Installation
### Manually
* Copy the module into ```<thelia_root>/local/modules/``` directory and be sure that the name of the module is Axepta.
* Activate it in your thelia administration panel
### Composer
Add it in your main thelia composer.json file
```
composer require thelia/axepta-module:~1.0
```
## Usage
* Contact Axepta to create an account.
* Go to the module configuration and add your HMAC key, Blowfish encryption key, and your merchant id.
* Set the operation mode to production
Documentation : https://docs.axepta.bnpparibas
If you want to test your configuration you can use this credit cards : https://docs.axepta.bnpparibas/display/DOCBNP/Test+Cards

View File

@@ -0,0 +1,488 @@
<?php
namespace Axepta\Util;
class Axepta
{
const PAYSSL = "https://paymentpage.axepta.bnpparibas/payssl.aspx";
const DIRECT = "https://paymentpage.axepta.bnpparibas/direct.aspx";
const DIRECT3D = "https://paymentpage.axepta.bnpparibas/direct3d.aspx";
const CAPTURE = "https://paymentpage.axepta.bnpparibas/capture.aspx";
const CREDIT = "https://paymentpage.axepta.bnpparibas/credit.aspx";
const INSTALMENT = "INSTALMENT";
private $secretKey;
private $cryptKey;
private $pspURL = self::PAYSSL;
public $parameters = array();
/** Axepta fields **/
private $pspFields = array(
'Debug',
'PayID',
'TransID',
'MerchantID',
'Amount',
'Currency',
'MAC',
'RefNr',
'Amount3D',
'URLSuccess',
'URLFailure',
'URLNotify',
'Response',
'UserData',
'Capture',
'OrderDesc',
'ReqID',
'Plain',
'Custom',
'expirationTime',
'AccVerify',
'RTF',
'ChDesc',
'Len',
'Data',
'Template',
'Language',
'Background',
'URLBack',
'CCSelect',
'MID',
'mid',
'refnr',
'XID',
'Status',
'Description',
'Code',
'PCNr',
'CCNr',
'CCCVC',
'CCBrand',
'CCExpiry',
'TermURL',
'UserAgent',
'HTTPAccept',
'AboID',
'ACSXID',
'MaskedPan',
'CAVV',
'ECI',
'DDD',
'Type',
'Plain',
'Custom',
'CustomField1','CustomField2','CustomField3','CustomField4','CustomField5','CustomField6','CustomField7',
'CustomField8','CustomField9','CustomField10','CustomField11','CustomField12','CustomField13','CustomField14'
);
/** Axepta request hmac fields **/
private $QHMACFields = array(
'PayID', 'TransID', 'MerchantID', 'Amount','Currency'
);
/** Axepta response hmac fields **/
private $RHMACFields = array(
'PayID', 'TransID', 'MerchantID', 'Status','Code'
);
/** Axepta blowfish crypt fields **/
private $BfishFields = array(
'PayID','TransID','Amount','Currency','MAC',
'RefNr','Amount3D','URLSuccess','URLFailure','URLNotify','Response','UserData','Capture','OrderDesc','ReqID',
'Plain','Custom','expirationTime','AccVerify','RTF','ChDesc',
'MID','XID','Status','Description','Code','PCNr','CCNr','CCCVC','CCBrand','CCExpiry','TermURL','UserAgent',
'HTTPAccept','AboID','ACSXID','MaskedPan','CAVV','ECI','DDD','Type','Plain','Custom'
// 'CustomField1','CustomField2','CustomField3','CustomField4','CustomField5','CustomField6','CustomField7',
// 'CustomField8','CustomField9','CustomField10','CustomField11','CustomField12','CustomField13','CustomField14'
);
/** Axepta request required fields **/
private $requiredFields = array(
// 'MerchantID', 'TransID', 'Amount', 'Currency','URLSuccess','URLFailure','URLNotify','OrderDesc'
'MerchantID', 'TransID', 'Amount', 'Currency','OrderDesc'
);
public $allowedlanguages = array(
'nl', 'fr', 'de', 'it', 'es', 'cy', 'en'
);
public function __construct($secret)
{
$this->secretKey = $secret; // HMAC key
}
public function setCryptKey($secret)
{
$this->cryptKey = $secret; // blowfish crypt key
}
/** hack to retrieve response field **/
public function setReponse($encrypt='encrypt')
{
$this->parameters['Response'] = $encrypt;
}
/** HMAC compute and store in MAC field**/
public function shaCompose(array $parameters)
{
// compose SHA string
$shaString = '';
foreach($parameters as $key) {
if(array_key_exists($key, $this->parameters) && !empty($this->parameters[$key])) {
$value = $this->parameters[$key];
$shaString .= $value;
}
$shaString .= (array_search($key, $parameters) != (count($parameters)-1)) ? '*' : '';
}
$this->parameters['MAC'] = hash_hmac('sha256', $shaString, $this->secretKey);
return $this->parameters['MAC'];
}
/** @return string */
public function getShaSign()
{
$this->validate();
return $this->shaCompose($this->QHMACFields);
}
public function BfishCompose(array $parameters)
{
// compose Blowfish hex string
$blowfishString = '';
foreach($parameters as $key) {
if(array_key_exists($key, $this->parameters) && !empty($this->parameters[$key])) {
$value = $this->parameters[$key];
$blowfishString .= $key.'='.$value.'&';
}
}
$blowfishString = rtrim($blowfishString,'&');
$this->parameters['Debug'] = $blowfishString;
$this->parameters['Len'] = strlen($blowfishString);
$this->parameters[self::DATA_FIELD] = bin2hex($this->encrypt($blowfishString,$this->cryptKey));
return $this->parameters[self::DATA_FIELD];
}
/** @return string */
public function getBfishCrypt()
{
$this->validate();
return $this->BFishCompose($this->BfishFields);
}
private function encrypt($data, $key)
{
$l = strlen($key);
if ($l < 16)
$key = str_repeat($key, ceil(16/$l));
if ($m = strlen($data)%8)
$data .= str_repeat("\x00", 8 - $m);
if (function_exists('mcrypt_encrypt'))
$val = mcrypt_encrypt(MCRYPT_BLOWFISH, $key, $data, MCRYPT_MODE_ECB);
else
$val = openssl_encrypt($data, 'BF-ECB', $key, OPENSSL_RAW_DATA | OPENSSL_NO_PADDING);
return $val;
}
private function decrypt($data, $key)
{
$l = strlen($key);
if ($l < 16)
$key = str_repeat($key, ceil(16/$l));
if (function_exists('mcrypt_encrypt'))
$val = mcrypt_decrypt(MCRYPT_BLOWFISH, $key, $data, MCRYPT_MODE_ECB);
else
$val = openssl_decrypt($data, 'BF-ECB', $key, OPENSSL_RAW_DATA | OPENSSL_NO_PADDING);
return rtrim($val, "\0");
}
/** @return string */
public function getUrl()
{
return $this->pspURL;
}
public function setUrl($pspUrl)
{
$this->validateUri($pspUrl);
$this->pspURL = $pspUrl;
}
public function setURLSuccess($url)
{
$this->validateUri($url);
$this->parameters['URLSuccess'] = $url;
}
public function setURLFailure($url)
{
$this->validateUri($url);
$this->parameters['URLFailure'] = $url;
}
public function setURLNotify($url)
{
$this->validateUri($url);
$this->parameters['URLNotify'] = $url;
}
public function setTransID($transactionReference)
{
if(preg_match('/[^a-zA-Z0-9_-]/', $transactionReference)) {
throw new \InvalidArgumentException("TransactionReference cannot contain special characters");
}
$this->parameters['TransID'] = $transactionReference;
}
/**
* Set amount in cents, eg EUR 12.34 is written as 1234
*/
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 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;
}
public function setPaymentPattern($paymentPattern)
{
$this->parameters['paymentPattern'] = $paymentPattern;
}
public function __call($method, $args)
{
if(substr($method, 0, 3) == 'set') {
// $field = lcfirst(substr($method, 3));
$field = 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));
$field = 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 static function createFromArray($shaComposer, array $parameters)
{
$instance = new static($shaComposer);
foreach($parameters as $key => $value)
{
$instance->{"set$key"}($value);
}
return $instance;
}
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 d'Axepta
// -----------------------------------
/** @var string */
const SHASIGN_FIELD = "MAC";
/** @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 $requestParameters
*/
private function filterRequestParameters(array $httpRequest)
{
//filter request for Sips parameters
$parameters = $this->parameters;
if(!array_key_exists(self::DATA_FIELD, $httpRequest) || $httpRequest[self::DATA_FIELD] == '') {
// throw new InvalidArgumentException('Data parameter not present in parameters.');
$parameters['Debug'] = implode('&',$httpRequest);
foreach($httpRequest as $key=>$value) {
$key = ($key=='mid')? 'MerchantID':$key;
$parameters[$key]=$value;
}
} else {
$parameters[self::DATA_FIELD] = $httpRequest[self::DATA_FIELD];
$this->dataString = $this->decrypt(hex2bin($parameters[self::DATA_FIELD]),$this->cryptKey);
$parameters['Debug'] = $this->dataString;
$dataParams = explode('&', $this->dataString);
foreach($dataParams as $dataParamString) {
$dataKeyValue = explode('=',$dataParamString,2);
$key = ($dataKeyValue[0]=='mid')?'MerchantID':$dataKeyValue[0];
$parameters[$key] = $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];
}
public function isValid()
{
// return $this->shaCompose($this->RHMACFields) == $this->shaSign;
return $this->shaCompose($this->RHMACFields) == $this->parameters['MAC'];
}
/**
* Retrieves a response parameter
* @param string $param
* @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);
$parameters = $this->parameters;
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('Status'), array("OK", "AUTHORIZED"));
}
public function getDataString()
{
return $this->dataString;
}
}

View File

@@ -0,0 +1,12 @@
{
"name": "thelia/axepta-module",
"description": "Axepta payment module for Thelia",
"license": "LGPL-3.0-or-later",
"type": "thelia-module",
"require": {
"thelia/installer": "~1.1"
},
"extra": {
"installer-name": "Axepta"
}
}

View File

@@ -0,0 +1,52 @@
<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='axepta.bo.default' l="Axepta Configuration"}
</div>
</div>
<div class="form-container">
<div class="row">
<div class="col-md-12">
{form name="axepta_configuration"}
<form action="{url path="/admin/module/axepta/configure"}" method="post">
{form_hidden_fields}
{include file = "includes/inner-form-toolbar.html"
hide_flags = true
page_url = "{url path='/admin/module/Axepta'}"
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='axepta.bo.default' l="Access to Axepta Platform"}</p>
{render_form_field field="merchant_id"}
{render_form_field field="hmac"}
{render_form_field field="crypt_key"}
</div>
<div class="col-md-4">
<p class="title title-without-tabs">{intl d='axepta.bo.default' l="Payment configuration"}</p>
{render_form_field field="send_confirmation_message_only_if_paid"}
{render_form_field field="run_mode"}
{render_form_field field="allowed_ip_list"}
{render_form_field field="minimum_amount"}
{render_form_field field="maximum_amount"}
</div>
</div>
</form>
{/form}
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,16 @@
{* We shoud use the "GET" method fo rsome fields to be properly processed byt the gateway *}
{* see https://docs.axepta.bnpparibas/display/DOCBNP/Payment+page#Paymentpage-Howtocallthepaymentmethodsselectionpage? *}
<script type="text/javascript">
jQuery(function($) {
var $form = $('#payement_gateway_form');
$form.attr('method', 'get');
$form.submit();
$('#force-submit-payment-form').click(function(ev) {
$form.submit();
ev.preventDefault();
});
});
</script>