|
<?php |
|
|
|
namespace Acme\SomeBundle\Services\IpnHandler; |
|
|
|
use Buzz\Browser; |
|
|
|
use JMS\Payment\CoreBundle\Model\PaymentInterface, |
|
JMS\Payment\CoreBundle\PluginController\PluginControllerInterface, |
|
JMS\Payment\CoreBundle\PluginController\Result; |
|
|
|
/** |
|
* A plug-and-play handler for Paypal's Instant Payment Notification system, |
|
* to be used with JMSPaymentCoreBundle and JMSPaymentPaypalBundle. Includes |
|
* support for Refunds. |
|
* |
|
* For requirements and a usage example see: https://gist.github.com/4600319 |
|
* |
|
* @author Paulo Rodrigues Pinto <https://github.com/regularjack> |
|
*/ |
|
class IpnHandler |
|
{ |
|
private $doctrine; |
|
private $ppc; |
|
private $browser; |
|
private $verifyUrl; |
|
|
|
/** |
|
* Constructor. |
|
* |
|
* @param Doctrine $doctrine Doctrine instance |
|
* @param Browser $browser Buzz instance |
|
* @param PluginControllerInterface $ppc Payment plugin controller instance |
|
* @param boolean $is_sandbox True if request verification should |
|
* be done with paypal's Sandbox, false |
|
* if not. |
|
*/ |
|
public function __construct ($doctrine, Browser $browser, |
|
PluginControllerInterface $ppc, $is_sandbox = true) |
|
{ |
|
$this->doctrine = $doctrine; |
|
$this->browser = $browser; |
|
$this->ppc = $ppc; |
|
|
|
// comment the following line if you want cUrl to verify SSL certificates |
|
$this->browser->getClient()->setVerifyPeer(false); |
|
|
|
$this->verifyUrl = $is_sandbox |
|
? "https://www.sandbox.paypal.com/cgi-bin/webscr" |
|
: "https://www.paypal.com/cgi-bin/webscr"; |
|
} |
|
|
|
/** |
|
* Process an IPN notification. |
|
* |
|
* The $notification array should contain all the key-value parameters POSTed |
|
* by Paypal. From a Symfony2 Controller this array can easily be obtained: |
|
* |
|
* $notification = $this->getRequest()->request->all(); |
|
* |
|
* |
|
* An optional callback may be passed to this method so that transaction |
|
* validation is delegated to the caller. If set, this callback is called |
|
* immediately before the transaction is "applied". The callabck takes two |
|
* arguments: an instance of a PaymentInstruction Entity and the same array |
|
* that is passed to this method ($notification). Throwing an Exception from |
|
* the callback will prevent the transaction from being "applied" and the |
|
* exception will be re-thrown from this method. |
|
* |
|
* @param array $notification Key-value parameters sent by paypal |
|
* @param callable $validate_callback Function to which validation is delegated |
|
* @throws Exception |
|
*/ |
|
public function process (array $notification, $validate_callback = null) |
|
{ |
|
$txn_id = $notification['txn_id']; |
|
$repository = $this->doctrine->getEntityManager() |
|
->getRepository('JMS\Payment\CoreBundle\Entity\FinancialTransaction'); |
|
|
|
if (!$this->verifyIpnRequest($notification)) { |
|
throw new \Exception('IPN request validation failed'); |
|
} |
|
|
|
if (isset($notification['parent_txn_id'])) { |
|
// Refund or Reversal |
|
$transaction = $repository->findOneBy(array( |
|
'referenceNumber' => $notification['parent_txn_id'] |
|
)); |
|
} else { |
|
$transaction = $repository->findOneBy(array('referenceNumber' => $txn_id)); |
|
} |
|
|
|
if (!$transaction) { |
|
throw new \Exception("Transaction not found: $txn_id"); |
|
} |
|
|
|
$payment = $transaction->getPayment(); |
|
if (!$payment) { |
|
throw new \Exception("No Payment is associated with the Transaction: $txn_id"); |
|
} |
|
|
|
$instruction = $payment->getPaymentInstruction(); |
|
|
|
// HACK |
|
// We temporarily change the value of "payment_system_name" in the |
|
// PaymentInstruction to 'paypal_ipn'. The original value is re-set once |
|
// the Notification has been handled. This is used in order to workaround |
|
// the internal architecture of JMSPaymentCoreBundle. For information on |
|
// why this is needed see |
|
// https://github.com/schmittjoh/JMSPaymentPaypalBundle/issues/56 |
|
$oldName = $instruction->getPaymentSystemName(); |
|
$this->setPaymentSystemName($instruction, 'paypal_ipn'); |
|
|
|
try { |
|
if ($validate_callback && is_callable($validate_callback)) { |
|
$validate_callback($instruction, $notification); |
|
} |
|
|
|
$this->applyTransaction($notification, $payment); |
|
|
|
} catch (\Exception $e) { |
|
// Must reset the entity manager orelse an "entity manager is closed" |
|
// exception is thrown. |
|
$this->doctrine->resetEntityManager(); |
|
|
|
// Re-set the original value (see hack description above) |
|
$this->setPaymentSystemName($instruction, $oldName); |
|
throw $e; |
|
} |
|
|
|
// Re-set the original value (see hack description above) |
|
$this->setPaymentSystemName($instruction, $oldName); |
|
} |
|
|
|
/** |
|
* "Apply" a transaction by delegating to the PluginController. For the moment, |
|
* the only supported transactions are 'Completed' and 'Refunded'. |
|
* |
|
* The PluginController will delegate external API calls to the IpnPlugin. |
|
* |
|
* @param array $notification Key-value parameters sent by paypal |
|
* @param Payment $payment Payment Entity instance |
|
* @throws Exception If the transaction type is not supported |
|
* @throws Exception Generic error |
|
*/ |
|
private function applyTransaction ($notification, $payment) |
|
{ |
|
$result = null; |
|
$amount = 0; |
|
$em = $this->doctrine->getEntityManager(); |
|
$repository = $em->getRepository('JMS\Payment\CoreBundle\Entity\FinancialTransaction'); |
|
|
|
if (isset($notification['mc_gross'])) { |
|
$amount = $notification['mc_gross']; |
|
} else { |
|
// mc_gross_x |
|
foreach ($notification as $key => $value) { |
|
if (strstr($key, 'mc_gross_') !== FALSE) { |
|
$amount += $value; |
|
} |
|
} |
|
} |
|
|
|
if ($amount === null) { |
|
throw new \Exception('Invalid amount'); |
|
} |
|
|
|
switch ($notification['payment_status']) { |
|
case 'Pending': |
|
// The payment is pending. See pending_reason for more information. |
|
break; |
|
|
|
case 'Completed': |
|
// The payment has been completed and the funds have been added |
|
// to the seller's account balance |
|
if ($payment->getState() === PaymentInterface::STATE_NEW || |
|
$payment->getState() === PaymentInterface::STATE_APPROVING) { |
|
$result = $this->ppc->approveAndDeposit($payment->getId(), $amount); |
|
} |
|
break; |
|
|
|
case 'Refunded': |
|
// The seller refunded the payment |
|
|
|
// HACK |
|
// The Payment must have state APPROVED in order for JMSPaymentCore |
|
// to accept a credit. Since at this point the payment has state |
|
// DEPOSITED, we set it to APPROVED and re-set it back after the |
|
// credit was created. |
|
$oldState = $payment->getState(); |
|
$this->setPaymentState($payment, PaymentInterface::STATE_APPROVED); |
|
|
|
// When a transaction is a Refund, Paypal sends a negative amount. |
|
// However, we want the credited amount to be a positive number. |
|
$amount = abs($amount); |
|
|
|
try { |
|
$credit = $this->ppc->createDependentCredit($payment->getId(), $amount); |
|
$result = $this->ppc->credit($credit->getId(), $amount); |
|
} catch (Exception $e) { |
|
$this->setPaymentState($payment, $oldState); |
|
throw $e; |
|
} |
|
|
|
// set the reference number in the newly created transaction |
|
$new_transaction = $repository->findOneBy(array('credit' => $credit)); |
|
if ($new_transaction) { |
|
$new_transaction->setReferenceNumber($notification['txn_id']); |
|
$em->flush($new_transaction); |
|
} |
|
|
|
$this->setPaymentState($payment, $oldState); |
|
break; |
|
|
|
default: |
|
throw new \Exception('Unsupported Transaction: '.$notification['payment_status']); |
|
break; |
|
} |
|
|
|
if ($result && $result->getStatus() !== Result::STATUS_SUCCESS) { |
|
throw new \Exception('Transaction was not successful: '.$result->getReasonCode()); |
|
} |
|
} |
|
|
|
/** |
|
* Verify an IPN request with Paypal. |
|
* |
|
* @param array $notification Parameters received from paypal |
|
* @return Boolean True if validation successful, false otherwise |
|
*/ |
|
private function verifyIpnRequest ($notification) |
|
{ |
|
$response = $this->browser->post($this->verifyUrl, array(), array_merge( |
|
array('cmd' => '_notify-validate'), |
|
$notification |
|
)); |
|
|
|
return $response->getContent() === "VERIFIED"; |
|
} |
|
|
|
/** |
|
* Set payment_system_name on the payment instruction associated with a |
|
* given Payment. |
|
* |
|
* @param PaymentInstruction $instruction PaymentInstruction entity |
|
* @param string $name The new value for payment_system_name |
|
*/ |
|
private function setPaymentSystemName ($instruction, $name) |
|
{ |
|
$em = $this->doctrine->getEntityManager(); |
|
$em->getConnection()->beginTransaction(); |
|
|
|
// We're forced to set the new value directly in the DB because the |
|
// PaymentInstruction Entity does not define a setter for |
|
// paymentSystemName. |
|
$em->createQuery(" |
|
UPDATE JMS\Payment\CoreBundle\Entity\PaymentInstruction pi |
|
SET pi.paymentSystemName = :psm |
|
WHERE pi = :pi") |
|
->setParameter('psm', $name) |
|
->setParameter('pi', $instruction) |
|
->getResult(); |
|
|
|
$em->getConnection()->commit(); |
|
$em->clear(); |
|
} |
|
|
|
/** |
|
* Set state on a given Payment. |
|
* |
|
* @param Payment $payment Payment entity |
|
* @param integer $state New state |
|
*/ |
|
private function setPaymentState ($payment, $state) |
|
{ |
|
$em = $this->doctrine->getEntityManager(); |
|
$em->getConnection()->beginTransaction(); |
|
|
|
$em->createQuery(" |
|
UPDATE JMS\Payment\CoreBundle\Entity\Payment p |
|
SET p.state = :state |
|
WHERE p = :p") |
|
->setParameter('state', $state) |
|
->setParameter('p', $payment) |
|
->getResult(); |
|
|
|
$em->getConnection()->commit(); |
|
$em->clear(); |
|
} |
|
} |
Can you post a full working exemple?
(Podes colocar um exemplo a funcionar?)
Thanks,
Paulo Dias