lexik / LexikPayboxBundle

LexikPayboxBundle eases the implementation of the Paybox payment system
MIT License
40 stars 47 forks source link

Can not perform action in custom IPN listener #90

Closed ShapesGraphicStudio closed 6 years ago

ShapesGraphicStudio commented 6 years ago

Hello,

I'm trying to perform an action in a custom IPN listener, but it does not seem to work.

vendor/lexik/paybox-bundle/Resources/config/routing.yml :

lexik_paybox_ipn:
    path:     /payment-ipn/{time}
    defaults: { _controller: LexikPayboxBundle:Default:ipn }
    methods:  [GET, POST]

lexik_paybox_membership_return:
    path:     /payment/return/{status}
    defaults: { _controller: AppBundle:Membership:return, status: error }
    requirements:
        status: success|canceled|denied

vendor/lexik/paybox-bundle/Resources/config/services.yml :

parameters:
    lexik_paybox.request_handler.class:              'Lexik\Bundle\PayboxBundle\Paybox\System\Base\Request'
    lexik_paybox.request_cancellation_handler.class: 'Lexik\Bundle\PayboxBundle\Paybox\System\Cancellation\Request'
    lexik_paybox.response_handler.class:             'Lexik\Bundle\PayboxBundle\Paybox\System\Base\Response'
    lexik_paybox.direc_plus.request_handler:         'Lexik\Bundle\PayboxBundle\Paybox\DirectPlus\Request'
    lexik_paybox.membership_response_listener.class: 'AppBundle\Controller\MembershipIpnListener'

services:
    lexik_paybox.request_handler:
        class:     '%lexik_paybox.request_handler.class%'
        arguments: ['%lexik_paybox.parameters%', '%lexik_paybox.servers%', '@form.factory']

    lexik_paybox.request_cancellation_handler:
        class:     '%lexik_paybox.request_cancellation_handler.class%'
        arguments: ['%lexik_paybox.parameters%', '%lexik_paybox.servers%', '@lexik_paybox.transport']

    lexik_paybox.response_handler:
        class:     '%lexik_paybox.response_handler.class%'
        arguments: ['@request_stack', '@logger', '@event_dispatcher', '%lexik_paybox.parameters%']
        tags:
            - { name: monolog.logger, channel: payment }

    lexik_paybox.direc_plus.request_handler:
        class:     '%lexik_paybox.direc_plus.request_handler%'
        arguments: ['%lexik_paybox.parameters%', '%lexik_paybox.servers%', '@logger', '@event_dispatcher', '@lexik_paybox.transport']
        tags:
            - { name: monolog.logger, channel: payment }

    lexik_paybox.transport:
        class:     '%lexik_paybox.transport.class%'

    lexik_paybox.membership_response_listener:
        class:     '%lexik_paybox.membership_response_listener.class%'
        arguments: ['%kernel.root_dir%', '@filesystem']
        tags:
            - { name: kernel.event_listener, event: paybox.ipn_response, method: onPayboxIpnResponse }

src/AppBundle/Controller/MembershipController.php :

<?php

namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

/**
 * Class MembershipController
 *
 * @package Lexik\Bundle\PayboxBundle\Controller
 *
 * @author Lexik <dev@lexik.fr>
 * @author Olivier Maisonneuve <o.maisonneuve@lexik.fr>
 */
class MembershipController extends Controller
{
    /**
     * OneYear action creates the form for a one year membership payment call.
     *
     * @return Response
     */
    public function OneYearAction()
    {

        $user = ParseUser::getCurrentUser();
        $email = $user->get('email');

        $paybox = $this->get('lexik_paybox.request_handler');
        $paybox->setParameters(array(
            'PBX_CMD'          => 'CMD-' . $user->getObjectId() . '-190-' .time(),
            'PBX_DEVISE'       => '978',
            'PBX_PORTEUR'      => $email,
            'PBX_RETOUR'       => 'Mt:M;Ref:R;Auto:A;Erreur:E',
            'PBX_TOTAL'        => '1490',
            'PBX_TYPEPAIEMENT' => 'CARTE',
            'PBX_TYPECARTE'    => 'CB',
            'PBX_EFFECTUE'     => $this->generateUrl('lexik_paybox_membership_return', array('status' => 'success'), UrlGeneratorInterface::ABSOLUTE_URL),
            'PBX_REFUSE'       => $this->generateUrl('lexik_paybox_membership_return', array('status' => 'denied'), UrlGeneratorInterface::ABSOLUTE_URL),
            'PBX_ANNULE'       => $this->generateUrl('lexik_paybox_membership_return', array('status' => 'canceled'), UrlGeneratorInterface::ABSOLUTE_URL),
            'PBX_RUF1'         => 'POST',
            'PBX_REPONDRE_A'   => $this->generateUrl('lexik_paybox_ipn', array('time' => time()), UrlGeneratorInterface::ABSOLUTE_URL),
        ));

        return $this->render(
            'my_bundle/user/membershippayboxform.html.twig',
            array(
                'url'  => $paybox->getUrl(),
                'form' => $paybox->getForm()->createView(),
            )
        );
    }

    /**
     * OneMonth action creates the form for a one month membership payment call.
     *
     * @return Response
     */
    public function OneMonthAction()
    {

        $user = ParseUser::getCurrentUser();
        $email = $user->get('email');

        $paybox = $this->get('lexik_paybox.request_handler');
        $paybox->setParameters(array(
            'PBX_CMD'          => 'CMD-' . $user->getObjectId() . '-1490-' .time(),
            'PBX_DEVISE'       => '978',
            'PBX_PORTEUR'      => $email,
            'PBX_RETOUR'       => 'Mt:M;Ref:R;Auto:A;Erreur:E',
            'PBX_TOTAL'        => '190',
            'PBX_TYPEPAIEMENT' => 'CARTE',
            'PBX_TYPECARTE'    => 'CB',
            'PBX_EFFECTUE'     => $this->generateUrl('lexik_paybox_membership_return', array('status' => 'success'), UrlGeneratorInterface::ABSOLUTE_URL),
            'PBX_REFUSE'       => $this->generateUrl('lexik_paybox_membership_return', array('status' => 'denied'), UrlGeneratorInterface::ABSOLUTE_URL),
            'PBX_ANNULE'       => $this->generateUrl('lexik_paybox_membership_return', array('status' => 'canceled'), UrlGeneratorInterface::ABSOLUTE_URL),
            'PBX_RUF1'         => 'POST',
            'PBX_REPONDRE_A'   => $this->generateUrl('lexik_paybox_ipn', array('time' => time()), UrlGeneratorInterface::ABSOLUTE_URL),
        ));

        return $this->render(
            'my_bundle/user/membershippayboxform.html.twig',
            array(
                'url'  => $paybox->getUrl(),
                'form' => $paybox->getForm()->createView(),
            )
        );
    }

    /**
     * Return action for a confirmation payment page on which the user is sent
     * after he seizes his payment informations on the Paybox's platform.
     * This action might only containts presentation logic.
     *
     * @param Request $request
     * @param string  $status
     *
     * @return Response
     */
    public function returnAction(Request $request, $status)
    {

        $request = Request::createFromGlobals();
        $montantAbonnement = $request->query->get('Mt');

        return $this->render('my_bundle/user/membershippayboxreturn.html.twig', array(
            'status'     => $status,
            'parameters' => $request->query, 
            'montantAbonnement' => $montantAbonnement
        ));
    }
}

src/AppBundle/Controller/MembershipIpnListener.php :

<?php

namespace AppBundle\Controller;

use Lexik\Bundle\PayboxBundle\Event\PayboxResponseEvent;
use Symfony\Component\Filesystem\Filesystem;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

/**
 * Sample listener that create a file for each ipn call.
 *
 * @author Olivier Maisonneuve <o.maisonneuve@lexik.fr>
 */
class MembershipIpnListener
{
    /**
     * @var string
     */
    private $rootDir;

    /**
     * @var Filesystem
     */
    private $filesystem;

    /**
     * Constructor.
     *
     * @param string     $rootDir
     * @param Filesystem $filesystem
     */
    public function __construct($rootDir, Filesystem $filesystem)
    {
        $this->rootDir = $rootDir;
        $this->filesystem = $filesystem;
    }

    /**
     * Creates a txt file containing all parameters for each IPN.
     *
     * @param PayboxResponseEvent $event
     */
    public function onPayboxIpnResponse(PayboxResponseEvent $event)
    {
        $path = sprintf('%s/../data/%s', $this->rootDir, date('Y\/m\/d\/'));
        $this->filesystem->mkdir($path);

        $content = sprintf('Signature verification : %s%s', $event->isVerified() ? 'OK' : 'KO', PHP_EOL);
        foreach ($event->getData() as $key => $value) {
            $content .= sprintf("%s:%s%s", $key, $value, PHP_EOL);
        }

        file_put_contents(
            sprintf('%s%s.txt', $path, time()),
            $content
        );

        $firstname = 'firstname';
        $lastname = 'lastname';
        $email = 'email';
        $montantFacture = "14,90";

        $message = \Swift_Message::newInstance()
            ->setSubject('Abonnement')
            ->setFrom('contact@my-domain.com')
            ->setTo($email)
            ->setBcc(['contact@my-domain.com', 'webmaster@my-domain.com'])
            ->setBody(
                $this->renderView(
                    'my_bundle/user/membershipmessage.html.twig',
                    array('firstname' => $firstname, 'lastname' => $lastname, 'email' => $email, 'montantFacture' => $montantFacture)
                ),
                'text/html'
            );

        $this->get('mailer')
            ->send($message);
    }
}

The redirection to membershippayboxreturn.html.twig works fine after payment, and I get the right info displayed but the Listener should create txt file just like the sample, and send a mail. I get none of those.

I did not write variables yet for the e-mail content for testing purposes.

If I write the mail sending function in returnAction function in src/AppBundle/Controller/MembershipController.php it works fine.

Any idea on what I could have missed here please ? Where could I be wrong ?

Best regards, David

acidjames commented 6 years ago

Hi David,

first of all and keep that in mind in the future, you must never (NEVER) edit files in the vendor/ directory đź’Ż

I'm not really sure this is the right way to specify the file path, maybe you should try the official symfony method with kernelrootdir variable

$path = sprintf('%s/../data/%s', $this->rootDir, date('Y\/m\/d\/'));

$this->get('kernel')->getRootDir(); And if you want the web root: $this->get('kernel')->getRootDir() . '/../web' . $this->getRequest()->getBasePath();

ShapesGraphicStudio commented 6 years ago

Hi, thanks for your answer.

First thing, the only files I changed in the bundle are : vendor/lexik/paybox-bundle/Resources/config/routing.yml and vendor/lexik/paybox-bundle/Resources/config/services.yml I tought it was right. What would be the appropriate thing to do please ?

I found the file path in your SampleIpnListener.php file. I'll try some workarounds on the path and tell you how it's working.

The mail fiunction does not work neither, so I thought the src/AppBundle/Controller/MembershipIpnListener.php file was not called properly or that the onPayboxIpnResponse function was not triggered.

acidjames commented 6 years ago

The files you have to edit are in your AppBundle/ directory

if you update your deps with composer, you will lose everything modified in the vendor/ directory.

ShapesGraphicStudio commented 6 years ago

I had another piece of script to create txt file to store transactions :

$IdTransaction = 'Transaction:"' . $request->query->get('Ref') . '-' . $request->query->get('Sign') . '"' . PHP_EOL;
                    $fp = fopen($this->get('kernel')->getRootDir() . '/PBRecords/transactions.txt', 'a');
                    fwrite($fp, $IdTransaction);
                    fclose($fp);

It's triggered and working fine in src/AppBundle/Controller/MembershipController.php but not in : src/AppBundle/Controller/MembershipIpnListener.php and I can't seem to find the issue..

ShapesGraphicStudio commented 6 years ago

Sorry, it's my first Symfony website, I do not get all the best practices yet.

I wrote my changes in : vendor/lexik/paybox-bundle/Resources/config/routing.yml to app/config/routing.yml

and the changes in : vendor/lexik/paybox-bundle/Resources/config/services.yml to app/config/services.yml

Better like that ?

ShapesGraphicStudio commented 6 years ago

I think the problem might be that I do not get to find where lexik_paybox.sample_response_listener is called.. ?

I can see lexik_paybox_ipn is being called though, but it's associated to vendor/lexik/paybox-bundle/Controller/DefaultController.php..

I would need to know where I can safely put my actions when the payment is OK. When I put them in src/AppBundle/Controller/MembershipController.php they're working fine, but too easy to be hacked as they rely on the url in the user's browser.

Any help would really be appreciated.

acidjames commented 6 years ago

If you are you doing your tests in dev environment, the paybox system can't access your IPN If you are using a dev computer that is behind a firewall, same issue

I usually test IPN on production system

ShapesGraphicStudio commented 6 years ago

OK thank you for the info.

By production environment, you mean Symfony ? Or both Symfony and Lexik bundle ? Can I still use preprod parameters form PayBox to test ?

acidjames commented 6 years ago

I mean symfony and your web server.

In your controller, when using the setParameters method, you specify a key called 'PBX_REPONDRE_A'.

In your MembershipController, it's the line :

'PBX_REPONDRE_A' => $this->generateUrl('lexik_paybox_ipn', array('time' => time()), UrlGeneratorInterface::ABSOLUTE_URL),

This means a request is silently sent to the paybox preprod server to call the IPN. If the paybox server can't access your environment (and i mean firewalls and stuff), then the IPN won't do anything :)

So, short answer, it's symfony/web server prod environment + LexikBundle : preprod for testing (and prod when everything is working)

ShapesGraphicStudio commented 6 years ago

Thanks again ! That's what I thought yes after pulling my hair for quite a while.

As I could not see what could be wrong in my code, I asked the guy managing server (for this project it's unfortunately not me) to open it all so I can test, because the access to the website is now blocked with login and password. I'm waiting for a change on his side to continue testing.

ShapesGraphicStudio commented 6 years ago

It works way better in production mode indeed, thanks !

I would have one more question please, how can I call transaction parameters in public function onPayboxIpnResponse(PayboxResponseEvent $event)

I would need to put some if statements on status and Mt.

ShapesGraphicStudio commented 6 years ago

I managed to retreive some info in public function onPayboxIpnResponse(PayboxResponseEvent $event)

$transactionId = $event->getData()['Ref'];
$montantAbonnement = $event->getData()['Mt'];

I just have to write an if statement now there if the transaction is successful.

acidjames commented 6 years ago

Yes that's how it works

ShapesGraphicStudio commented 6 years ago

Yes, I figured that out.

I'm still having some issues, as the IPN controller is declared as a service, I can not use some functions like ->get() in public function onPayboxIpnResponse(PayboxResponseEvent $event).

I called them successfully in public function returnAction(Request $request, $status) for testing purposes, but that was not very secure.

So I'm close, but I still have some workarounds to find out.

ShapesGraphicStudio commented 6 years ago

Do you think I could send some variables from : 'PBX_REPONDRE_A' => $this->generateUrl('lexik_paybox_ipn', array('time' => time()), UrlGeneratorInterface::ABSOLUTE_URL) using the array ?

I was thinking of something like : 'PBX_REPONDRE_A' => $this->generateUrl('lexik_paybox_ipn', array('time' => time() , 'email' => $email), UrlGeneratorInterface::ABSOLUTE_URL) as I get email info using :

$user = ParseUser::getCurrentUser();
$email = $user->get('email');

just before :

$paybox = $this->get('lexik_paybox.request_handler');
$paybox->setParameters(array(...

?

Sorry for bothering so much, but I'm really getting crazy on this one.

I can't seem to use Parse PHP SDK as a service like I did with doctrine to store transactions instead of creating .txt files, that's blocking me..

ShapesGraphicStudio commented 6 years ago

Well that seemd to be the good idea.. For info I took a look at http://symfony.com/doc/current/service_container/request.html

I just have to manage to be able to use $user->set(... now to finish..

acidjames commented 6 years ago

I think you have lost yourself ^^ I don't really understand what you are trying to do but i guess you want to link an order to a user.

Personally, i use the Ref variable, it's the order number. When i initiate a payment, i tell paybox the order number, it's the same as 'PBX_CMD' => 'CMD-' . $user->getObjectId() . '-190-' .time(),

Once i have the Ref variable, i look for it using the entity manager. Your Listener must have access to the entity manager. Here is how to do it :

services.yml

    app_bundle.paybox_response_listener:
        class: App\AppBundle\Listener\PayboxListener
        arguments: [ '@doctrine.orm.entity_manager', '@mailer' ]
        tags:
            - { name: kernel.event_listener, event: paybox.ipn_response, method: onPayboxIpnResponse }

I have also injected the mailer service to send emails in my listener calls. This is an example of a Listener i use :

<?php

/**
 * Description of PayboxListener
 *
 * @author James ASHTON
 */

namespace App\FooBundle\Listener;

use Lexik\Bundle\PayboxBundle\Event\PayboxResponseEvent;

/**
 * Simple listener that adds application logic.
 */
class PayboxListener {

    private $em;
    private $mailer;

    /**
     * Constructor.
     *
     * @param         $em
     * @param         $mailer
     */
    public function __construct($em, $mailer) {
        $this->em = $em;
        $this->mailer = $mailer;
    }

    /**
     *
     * @param  PayboxResponseEvent $event
     */
    public function onPayboxIpnResponse(PayboxResponseEvent $event) {
        $data = $event->getData();
        $date = new \DateTime();

        //  here my Ref order number is the same as the id of the order
        $entity = $this->em->getRepository('FooBundle:Order')->find($data['Ref']);

        if ($data['Erreur'] == '00000') {
            $entity->setPaymentStatus('Confirmed');
            $entity->setPaymentCode($data['Erreur']);
        } else {
            $entity->setPaymentStatus('Error');
            $entity->setPaymentCode($data['Erreur']);
        }

        $entity->setPaymentTotal($data['Mt']);
        $entity->setPaymentDate($date);

        $this->em->persist($entity);
        $this->em->flush();

        foreach ($event->getData() as $key => $value) {
            $content .= sprintf("%s:%s\n", $key, $value);
        }

        $message = \Swift_Message::newInstance()
                ->setSubject('Payment notification')
                ->setFrom('james@foo.fr')
                ->setTo('james@foo.fr')
                ->setBody($content, 'text/html')
        ;

        $this->mailer->send($message);
    }

}
ShapesGraphicStudio commented 6 years ago

Yes, regarding Doctrine and mailer, I hade already made them work, but I had some hard time to get user inbfo from Parse Database as I do not use symfony user bundle.

I have to connect to databse, to get info, that's OK with the variables I added in 'PBX_REPONDRE_A' array, although it seems you can not pass more than a defined amount of bvariables / data.

The only thing I have still to test is send information to Parse Database, that's where I store membership expiration date for each user.

Anyway, thanks a lot again for support and concern !

ShapesGraphicStudio commented 6 years ago

As I thought, as I could not use $user->get('...'); in the listener, problem I manged to solve using requestStack.

I can also not use $user->set('...'); or $user->addUnique('...');

Here's my code in case you got an idea for me..

MembershipController :

<?php

namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

/* PARSE SDK */
use Parse\ParseClient;
$app_id = "****************************************";
$rest_key = "****************************************";
$master_key = "****************************************";
ParseClient::initialize($app_id, $rest_key, $master_key);
ParseClient::setServerURL('https://parse.my-domain.com','parse');
use Parse\ParseObject;
use Parse\ParseQuery;
use Parse\ParseACL;
use Parse\ParsePush;
use Parse\ParseUser;
use Parse\ParseInstallation;
use Parse\ParseException;
use Parse\ParseAnalytics;
use Parse\ParseFile;
use Parse\ParseCloud;
/* PARSE SDK */

/**
 * Class MembershipController
 *
 * @package Lexik\Bundle\PayboxBundle\Controller
 *
 * @author Lexik <dev@lexik.fr>
 * @author Olivier Maisonneuve <o.maisonneuve@lexik.fr>
 */
class MembershipController extends Controller
{

    /**
     * OneMonth action creates the form for a one month membership payment call.
     *
     * @return Response
     */
    public function OneMonthAction()
    {

        $user = ParseUser::getCurrentUser();
        $userid = $user->getObjectId();
        $email = $user->get('email');
        $expirationdate = $user->get('ExpirationDate');
        if (!empty($expirationdate)) {
            $currentexpirationdate = end($expirationdate);
        }
        else {
            $currentexpirationdate = $today;
        }

        $paybox = $this->get('lexik_paybox.request_handler');
        $paybox->setParameters(array(
            'PBX_CMD'          => 'CMD-' . $user->getObjectId() . '-190-' .time(),
            'PBX_DEVISE'       => '978',
            'PBX_PORTEUR'      => $email,
            'PBX_RETOUR'       => 'Mt:M;Ref:R;Auto:A;Erreur:E',
            'PBX_TOTAL'        => '190',
            'PBX_TYPEPAIEMENT' => 'CARTE',
            'PBX_TYPECARTE'    => 'CB',
            'PBX_EFFECTUE'     => $this->generateUrl('lexik_paybox_membership_return', array('status' => 'success'), UrlGeneratorInterface::ABSOLUTE_URL),
            'PBX_REFUSE'       => $this->generateUrl('lexik_paybox_membership_return', array('status' => 'denied'), UrlGeneratorInterface::ABSOLUTE_URL),
            'PBX_ANNULE'       => $this->generateUrl('lexik_paybox_membership_return', array('status' => 'canceled'), UrlGeneratorInterface::ABSOLUTE_URL),
            'PBX_RUF1'         => 'GET',
            'PBX_REPONDRE_A'   => $this->generateUrl('lexik_paybox_ipn', array('time' => time() , 'user' => $user , 'userid' => $userid , 'email' => $email , 'currentexpirationdate' => $currentexpirationdate), UrlGeneratorInterface::ABSOLUTE_URL),
        ));

        return $this->render(
            'my_bundle/user/membershippayboxform.html.twig',
            array(
                'url'  => $paybox->getUrl(),
                'form' => $paybox->getForm()->createView(),
            )
        );
    }

    /**
     * OneYear action creates the form for a one year membership payment call.
     *
     * @return Response
     */
    public function OneYearAction()
    {

        $user = ParseUser::getCurrentUser();
        $userid = $user->getObjectId();
        $email = $user->get('email');
        $expirationdate = $user->get('ExpirationDate');
        if (!empty($expirationdate)) {
            $currentexpirationdate = end($expirationdate);
        }
        else {
            $currentexpirationdate = $today;
        }

        $paybox = $this->get('lexik_paybox.request_handler');
        $paybox->setParameters(array(
            'PBX_CMD'          => 'CMD-' . $user->getObjectId() . '-1490-' .time(),
            'PBX_DEVISE'       => '978',
            'PBX_PORTEUR'      => $email,
            'PBX_RETOUR'       => 'Mt:M;Ref:R;Auto:A;Erreur:E',
            'PBX_TOTAL'        => '1490',
            'PBX_TYPEPAIEMENT' => 'CARTE',
            'PBX_TYPECARTE'    => 'CB',
            'PBX_EFFECTUE'     => $this->generateUrl('lexik_paybox_membership_return', array('status' => 'success'), UrlGeneratorInterface::ABSOLUTE_URL),
            'PBX_REFUSE'       => $this->generateUrl('lexik_paybox_membership_return', array('status' => 'denied'), UrlGeneratorInterface::ABSOLUTE_URL),
            'PBX_ANNULE'       => $this->generateUrl('lexik_paybox_membership_return', array('status' => 'canceled'), UrlGeneratorInterface::ABSOLUTE_URL),
            'PBX_RUF1'         => 'GET',
            'PBX_REPONDRE_A'   => $this->generateUrl('lexik_paybox_ipn', array('time' => time() , 'user' => $user , 'userid' => $userid , 'email' => $email , 'currentexpirationdate' => $currentexpirationdate), UrlGeneratorInterface::ABSOLUTE_URL),
        ));

        return $this->render(
            'my_bundle/user/membershippayboxform.html.twig',
            array(
                'url'  => $paybox->getUrl(),
                'form' => $paybox->getForm()->createView(),
            )
        );
    }

    /**
     * Return action for a confirmation payment page on which the user is sent
     * after he seizes his payment informations on the Paybox's platform.
     * This action might only containts presentation logic.
     *
     * @param Request $request
     * @param string  $status
     *
     * @return Response
     */
    public function returnAction(Request $request, $status)
    {

        return $this->render('my_bundle/user/membershippayboxreturn.html.twig', array(
            'status'     => $status,
            'parameters' => $request->query
        ));
    }
}

MembershipIpnListener.php :

<?php

namespace AppBundle\Controller;

use Lexik\Bundle\PayboxBundle\Event\PayboxResponseEvent;
use Symfony\Component\Filesystem\Filesystem;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RequestStack;

use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

use Symfony\Component\DependencyInjection\ContainerInterface;

/* Transaction en BDD */
use AppBundle\Entity\Transactions;
use Doctrine\ORM\EntityManagerInterface;
/* Transaction en BDD */

/* PARSE SDK */
use Parse\ParseClient;
$app_id = "***************************************";
$rest_key = "***************************************";
$master_key = "***************************************";
ParseClient::initialize($app_id, $rest_key, $master_key);
ParseClient::setServerURL('https://parse.my-domain.com','parse');
use Parse\ParseObject;
use Parse\ParseQuery;
use Parse\ParseACL;
use Parse\ParsePush;
use Parse\ParseUser;
use Parse\ParseInstallation;
use Parse\ParseException;
use Parse\ParseAnalytics;
use Parse\ParseFile;
use Parse\ParseCloud;
/* PARSE SDK */

/**
 * Sample listener that create a file for each ipn call.
 *
 * @author Olivier Maisonneuve <o.maisonneuve@lexik.fr>
 */
class MembershipIpnListener
{
    /**
     * @var string
     */
    private $rootDir;

    /**
     * @var Filesystem
     */
    private $filesystem;

    protected $em;
    protected $twig;
    protected $mailer;

    /**
     * Constructor.
     *
     * @param string     $rootDir
     * @param Filesystem $filesystem
     */
    public function __construct($rootDir, Filesystem $filesystem, $doctrine, $twig, $mailer, RequestStack $requestStack)
    {
        $this->rootDir = $rootDir;
        $this->filesystem = $filesystem;
        $this->em = $doctrine->getManager();
        $this->twig = $twig;
        $this->mailer = $mailer;
        $this->requestStack = $requestStack;
    }

    /**
     * Creates a txt file containing all parameters for each IPN.
     *
     * @param PayboxResponseEvent $event
     */
    public function onPayboxIpnResponse(PayboxResponseEvent $event)
    {

        $path = $this->rootDir . '/../data/' . date('Y\/m\/d\/');
        $this->filesystem->mkdir($path);
        $content = sprintf("Signature verification : %s\n", $event->isVerified() ? 'OK' : 'KO');
        foreach ($event->getData() as $key => $value) {
            $content .= sprintf("%s:%s\n", $key, $value);
        }
        file_put_contents($path . time() . '.txt', $content);

        /* RÉCUPÉRATION DES INFOS DE TRANSACTION */
        $transactionId = $event->getData()['Ref'];
        $transactionCode = $event->getData()['Erreur'];
        $montantAbonnement = $event->getData()['Mt'];
        /* RÉCUPÉRATION DES INFOS DE TRANSACTION */

        /* RÉCUPÉRATION DONNÉES PARSE */
        $request = $this->requestStack->getCurrentRequest();
        $user = $request->get('user');
        $userId = $request->get('userid');
        $userEmail = $request->get('email');
        $expirationDate = $request->get('currentexpirationdate');
        /* RÉCUPÉRATION DONNÉES PARSE */

        /* SAUVEGARDE DE LA TRANSACTION EN BDD */
        /* $em = $this->getDoctrine()->getManager(); */
        $em = $this->em;
        $transaction = new Transactions();
        $transaction->setTransactionID($transactionId);
        $transaction->setTransactionCode($transactionCode);
        $transaction->setUserID($userId);
        $transaction->setUserEmail($userEmail);
        $transaction->setDateTransaction(date('Y-m-d\TH:i:s.\0\0\0\Z'));
        $transaction->setMontant($montantAbonnement);
        $transaction->setExpirationDate($nextexpirationDate);
        $em->persist($transaction);
        $em->flush();
        /* SAUVEGARDE DE LA TRANSACTION EN BDD */

        /* VALIDATION ABONNEMENT */
        if ($montantAbonnement == 190) {
            try { 
                $nextexpirationdate = date('Y-m-d\TH:i:s.\0\0\0\Z',strtotime(date($currentexpirationdate, time()) . '+1 Month'));
                $user->addUnique("ExpirationDate", array($nextexpirationdate));
                $user->save();
            }
            catch (ParseException $error) { 
                echo 'Error message: '.$error->getMessage(); 
            }
        }
        if ($montantAbonnement == 1490) {
            try { 
                $nextexpirationdate = date('Y-m-d\TH:i:s.\0\0\0\Z',strtotime(date($currentexpirationdate, time()) . '+12 Month'));
                $user->addUnique("ExpirationDate", array($nextexpirationdate));
                $user->save();
            }
            catch (ParseException $error) { 
                echo 'Error message: '.$error->getMessage(); 
            }
        }
        /* VALIDATION ABONNEMENT */

        /* ENVOI DU MESSAGE */
        $body = $this->renderTemplate($invoice);

        $message = \Swift_Message::newInstance()
            ->setSubject('Abonnement')
            ->setFrom('contact@my-domain.com')
            ->setTo($email)
            ->setBcc(['contact@my-domain.com', 'webmaster@my-domain.com'])
            ->setBody($body,'text/html');

        $this->mailer->send($message);
        /* ENVOI DU MESSAGE */

    }

    public function renderTemplate($invoice)
    {
        return $this->twig->render(
                    'webcam_hd/user/membershipmessage.html.twig',
                    array('firstname' => $firstname, 'lastname' => $lastname, 'email' => $email, 'montantFacture' => $montantFacture, 'nextexpirationdate' => $nextexpirationdate)
            );
    }
}

Everything seems to work except $user->addUnique('...'); which trows :

php.CRITICAL: Call to a member function addUnique() on null {"exception":"[object] (Symfony\Component\Debug\Exception\FatalThrowableError(code: 0): Call to a member function addUnique() on null at ../src/AppBundle/Controller/MembershipIpnListener.php:127)"} []

All because I use Parse php sdk, which I can not seem to use as service or entity. I know it may not be the best practice, but I have to as I already wrote an iOS app running with Parse sdk to handle users and it has to be synched.

The only thing I can see working now is to use $user->addUnique('...'); in MembershipController.php by reading and updating my Transactions entity, but it does not seem to be the best practice neither regarding security..

ShapesGraphicStudio commented 6 years ago

I think I finally managed to find a way to make it all work clean and safe.

I would have one last question though, do you think it would be possible to execute public function returnAction(Request $request, $status) after public function onPayboxIpnResponse(PayboxResponseEvent $event) has finished to do all its job ?

acidjames commented 6 years ago

Hi,

you musn't mix the controller concept that interacts with a browser and the IPN that works behind the scenes without any human intervention.

It doesn't make sense to link those two concepts.

The returnAction is presented to a user, it's sole purpose is to give some information after the payment has succeeded or failed.

The IPN exists because you can't rely on user action to validate a payment. You may think at first that after return you use the IPN but it's not safe to think like that.

You have to separate those two concepts. Only Paybox IPN event can trigger payment validation in your database :)

ShapesGraphicStudio commented 6 years ago

Hi,

Thanks again for the feedback. Yes I know, I managed to make the IPN listener validate and store the transaction in the database.

My only problem remaining is that the IPN listener can not "connect" with Parse sdk functions, and I need it to make my system work. I need to update my Parse user after payment, as the listener is a service and not really a controller as I understood, it can not use user->get('...') (which I managed to solve with a workaround) but also $user->addUnique('...') to make my update, that I can call in returnAction (or another Controller) but not in the IPN listener. That's my problem.

ShapesGraphicStudio commented 6 years ago

Is there a way to use the IPN listener as a controller, and not a service ?

If I understand well, the use of a service makes it easier to override, as it's handled in app/config/services.yml, so that the bundle can be updated without losing changes we made.. ?

But there must be a workaround, no ?

acidjames commented 6 years ago

Hi,

I don't know what parseSDK is.

The IPN is a listener, and cannot be a controller, you are mixing concepts here sorry, you have to learn more about symfony, services, dependency injection before using bundles like this one.

I can't answer more here sorry because this goes beyond the scope of this bundle.

Maybe you could try asking on stackoverflow

ShapesGraphicStudio commented 6 years ago

Hi,

OK, I understand.

I'm almost there, made it all work safely, my only remaining problem is to be able to execute public function returnAction(Request $request, $status) after public function onPayboxIpnResponse(PayboxResponseEvent $event) (a page refresh does the trick, but it seems a really poor hack.. ;-))

Indeed, it's my first Symfony website, I've been scratching my hair so many hours on this. It's unfortunately the last stone of the project.

Anyway, thanks a lot for you support and concern.

Best regards