Skip to content

Commit

Permalink
Merge pull request #1 from thelia-modules/develop
Browse files Browse the repository at this point in the history
Integration of variable amount and/or frequency subscriptions
  • Loading branch information
roadster31 authored Jan 25, 2024
2 parents ae4c310 + 06405d5 commit 603cb0e
Show file tree
Hide file tree
Showing 23 changed files with 1,139 additions and 359 deletions.
169 changes: 98 additions & 71 deletions Axepta.php
Original file line number Diff line number Diff line change
@@ -1,25 +1,34 @@
<?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. */
/*************************************************************************************/

/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

/* 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\Service\PaymentService;
use Axepta\Util\Axepta as AxeptaPayment;
use Propel\Runtime\Connection\ConnectionInterface;
use Symfony\Component\DependencyInjection\Loader\Configurator\ServicesConfigurator;
use Thelia\Core\Translation\Translator;
use Symfony\Component\Finder\Finder;
use Thelia\Core\HttpFoundation\Response;
use Thelia\Install\Database;
use Thelia\Log\Tlog;
use Thelia\Model\Order;
use Thelia\Module\AbstractPaymentModule;
use Thelia\Tools\MoneyFormat;
use Thelia\Tools\URL;

class Axepta extends AbstractPaymentModule
{
Expand All @@ -33,97 +42,76 @@ class Axepta extends AbstractPaymentModule
public const ALLOWED_IP_LIST = 'allowed_ip_list';
public const MINIMUM_AMOUNT = 'minimum_amount';
public const MAXIMUM_AMOUNT = 'maximum_amount';
const LOG_AXCEPTA_RESPONSE = 'log_axcepta_response';
public const LOG_AXCEPTA_RESPONSE = 'log_axcepta_response';

public const SEND_CONFIRMATION_MESSAGE_ONLY_IF_PAID = 'send_confirmation_message_only_if_paid';
public const PAYMENT_FEATURE = 'active_payment_feature';

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);
public const PAYMENT_FEATURE_UNIQUE = 'unique';
public const PAYMENT_FEATURE_FIXED_AMOUNT_SUBSCRIPTION = 'fixed_subscription';
public const PAYMENT_FEATURE_VARIABLE_AMOUNT_SUBSCRIPTION = 'variable_subscription';

$urlAnnulation = URL::getInstance()->absoluteUrl("/axepta/cancel/" . $order->getId());
$urlNotification = URL::getInstance()->absoluteUrl(path:'/axepta/notification');
public const SEND_CONFIRMATION_MESSAGE_ONLY_IF_PAID = 'send_confirmation_message_only_if_paid';

$paymentRequest = new AxeptaPayment($hmac);
$paymentRequest->setCryptKey($cryptKey);
public const AXCEPTA_CREATE_PAYMENT_EVENT = 'axepta.axcepta_create_payment_event';

$transId = time().$order->getId();
public static function isSubscriptionMode() : bool
{
return in_array(self::getConfigValue(Axepta::PAYMENT_FEATURE), [
self::PAYMENT_FEATURE_VARIABLE_AMOUNT_SUBSCRIPTION,
self::PAYMENT_FEATURE_FIXED_AMOUNT_SUBSCRIPTION
], true);
}

$paymentRequest->setMsgVer('2.0');
$paymentRequest->setUrl(AxeptaPayment::PAYSSL);
$paymentRequest->setMerchantID($merchantId);
$paymentRequest->setTransID($transId);
$paymentRequest->setAmount((int) ($order->getTotalAmount()*100));
$paymentRequest->setCurrency($order->getCurrency()->getCode());
$paymentRequest->setRefNr($order->getId());
$paymentRequest->setURLSuccess($urlNotification);
$paymentRequest->setURLFailure($urlNotification);
$paymentRequest->setURLNotify($urlNotification);
$paymentRequest->setURLBack($urlAnnulation);
$paymentRequest->setReponse('encrypt');
$paymentRequest->setLanguage($this->getRequest()->getSession()->getLang()->getLocale());
public static function isUniqueMode() : bool
{
return self::getConfigValue(Axepta::PAYMENT_FEATURE) === self::PAYMENT_FEATURE_UNIQUE;
}

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());
}
/**
* @throws \JsonException
* @throws \Propel\Runtime\Exception\PropelException
*/
public function pay(Order $order): ?Response
{
/** @var PaymentService $paymentService */
$paymentService = $this->getContainer()->get('axepta_payment_service');

$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()
];
/** @var AxeptaPayment $paymentRequest, array $data */
[$paymentRequest, $data] = $paymentService->createPaymentData($order);

$order
->setTransactionRef($transId)
->setTransactionRef($paymentRequest->getTransID())
->save();

return $this->generateGatewayFormResponse($order, $paymentRequest->getUrl(), $transmit);
return $this->generateGatewayFormResponse($order, $paymentRequest->getUrl(), $data);
}

public function isValidPayment()
{
$pf = self::getConfigValue(self::PAYMENT_FEATURE, null);
$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()->error("Axepta module is not properly configured, some configuration data are missing.");
if (($pf === null || $hmac === null || $merchantId === null || $cryptKey === null) && $mode !== 'TEST') {
Tlog::getInstance()->error('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();
$allowed_client_ips = [];

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);
$valid = \in_array($client_ip, $allowed_client_ips) || \in_array('*', $allowed_client_ips);
}

if ($valid) {
Expand All @@ -144,11 +132,50 @@ protected function checkMinMaxAmount($min, $max)
return $order_total > 0 && ($min_amount <= 0 || $order_total >= $min_amount) && ($max_amount <= 0 || $order_total <= $max_amount);
}

/**
* Defines how services are loaded in your modules.
*/
public static function configureServices(ServicesConfigurator $servicesConfigurator): void
{
$servicesConfigurator->load(self::getModuleCode().'\\', __DIR__)
->exclude([THELIA_MODULE_DIR . ucfirst(self::getModuleCode()). "/I18n/*"])
->exclude([THELIA_MODULE_DIR.ucfirst(self::getModuleCode()).'/I18n/*'])
->autowire(true)
->autoconfigure(true);
}

public function preActivation(ConnectionInterface $con = null)
{
if (!self::getConfigValue('is_initialized', false)) {
$database = new Database($con);

$database->insertSql(null, [__DIR__.'/Config/TheliaMain.sql']);

self::setConfigValue('is_initialized', true);
}

return true;
}

/**
* Execute sql files in Config/update/ folder named with module version (ex: 1.0.1.sql).
*
* @param ConnectionInterface $con
*/
public function update($currentVersion, $newVersion, ConnectionInterface $con = null): void
{
$finder = Finder::create()
->name('*.sql')
->depth(0)
->sortByName()
->in(__DIR__.DS.'Config'.DS.'update');

$database = new Database($con);

/** @var \SplFileInfo $file */
foreach ($finder as $file) {
if (version_compare($currentVersion, $file->getBasename('.sql'), '<')) {
$database->insertSql(null, [$file->getPathname()]);
}
}
}
}
94 changes: 94 additions & 0 deletions Command/CreatePayment.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php

/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

/* web : https://www.openstudio.fr */

/* For the full copyright and license information, please view the LICENSE */
/* file that was distributed with this source code. */

namespace Axepta\Command;

use Axepta\Axepta;
use Axepta\Event\CreatePaymentEvent;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Thelia\Command\ContainerAwareCommand;
use Thelia\Exception\TheliaProcessException;
use Thelia\Model\OrderQuery;

/**
* Created by Franck Allimant, OpenStudio <fallimant@openstudio.fr>
* Projet: thelia25
* Date: 24/01/2024.
*/
class CreatePayment extends ContainerAwareCommand
{
protected function configure(): void
{
$this
->setName('create-axcepta-payment')
->setDescription('Create a new Axepta payment for a given order')
->addArgument(
'original_order_ref',
InputArgument::REQUIRED,
'Original order reference.'
)
->addArgument(
'new_order_ref',
InputArgument::REQUIRED,
'New order reference.'
)
;
}

protected function execute(InputInterface $input, OutputInterface $output)
{
if (! Axepta::isSubscriptionMode()) {
$output->writeln(
'<error>This command is intended for use when the subscription payment feature is enabled.'
.'Subscription feature is not currently activated. ($feat is activated)</error>'
);

return Command::INVALID;
}

$this->initRequest();

$orderRef = $input->getArgument('original_order_ref');

if (null === $originalOrder = OrderQuery::create()->findOneByRef($orderRef)) {
throw new TheliaProcessException("Unknown order $orderRef");
}

$newOrderRef = $input->getArgument('new_order_ref');

if (null === $newOrder = OrderQuery::create()->findOneByRef($newOrderRef)) {
throw new TheliaProcessException("Unknown order $newOrderRef");
}

$event = new CreatePaymentEvent($originalOrder, $newOrder);

$this->getDispatcher()->dispatch($event, Axepta::AXCEPTA_CREATE_PAYMENT_EVENT);

$output->writeln('Payment status : '.($event->isSuccess() ? 'success' : 'failure'));

if (!$event->isSuccess()) {
$output->writeln('<error> : '.$event->getErrorMessage().'</error>');

return Command::FAILURE;
}

return Command::SUCCESS;
}
}
27 changes: 27 additions & 0 deletions Config/TheliaMain.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@

# This is a fix for InnoDB in MySQL >= 4.1.x
# It "suspends judgement" for fkey relationships until are tables are set.
SET FOREIGN_KEY_CHECKS = 0;

-- ---------------------------------------------------------------------
-- axcepta_scheme
-- ---------------------------------------------------------------------

DROP TABLE IF EXISTS `axcepta_scheme`;

CREATE TABLE `axcepta_scheme`
(
`id` INTEGER NOT NULL AUTO_INCREMENT,
`order_id` INTEGER,
`name` VARCHAR(45) NOT NULL,
`number` VARCHAR(19) NOT NULL,
`brand` VARCHAR(33) NOT NULL,
`expiry_date` VARCHAR(6) NOT NULL,
`scheme_reference_id` VARCHAR(255) NOT NULL,
`created_at` DATETIME,
`updated_at` DATETIME,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;

# This restores the fkey checks, after having unset them earlier
SET FOREIGN_KEY_CHECKS = 1;
11 changes: 4 additions & 7 deletions Config/config.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,11 @@
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>
<service id="axepta_payment_service" alias="Axepta\Service\PaymentService" public="true">
<argument type="service" id="request_stack"/>
<argument type="service" id="event_dispatcher"/>
</service>
</services>

<hooks>
Expand Down
8 changes: 7 additions & 1 deletion Config/module.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,18 @@
<language>en_US</language>
<language>fr_FR</language>
</languages>
<version>2.0.3</version>
<version>2.1.0</version>
<authors>
<author>
<name>Nicolas Barbey</name>
<email>nbarbey@openstudio.fr</email>
</author>
<author>
<name>Franck Allimant</name>
<company>OpenStudio</company>
<email>fallimant@openstudio.fr</email>
<website>www.openstudio.fr</website>
</author>
</authors>
<type>classic</type>
<thelia>2.4.0</thelia>
Expand Down
Loading

0 comments on commit 603cb0e

Please sign in to comment.