*
* @package CRM
* @copyright CiviCRM LLC (c) 2004-2018
- * $Id$
*
*/
class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN {
* @throws CRM_Core_Exception
*/
public function __construct($inputData) {
- //CRM-19676
+ // CRM-19676
$params = (!empty($inputData['custom'])) ?
array_merge($inputData, json_decode($inputData['custom'], TRUE)) :
$inputData;
/**
* @param string $name
- * @param $type
+ * @param string $type
* @param bool $abort
*
* @return mixed
+ * @throws \CRM_Core_Exception
*/
public function retrieve($name, $type, $abort = TRUE) {
- static $store = NULL;
- $value = CRM_Utils_Type::validate(
- CRM_Utils_Array::value($name, $this->_inputParameters),
- $type,
- FALSE
- );
+ $value = CRM_Utils_Type::validate(CRM_Utils_Array::value($name, $this->_inputParameters), $type, FALSE);
if ($abort && $value === NULL) {
- CRM_Core_Error::debug_log_message("Could not find an entry for $name");
+ Civi::log()->debug("PayPalIPN: Could not find an entry for $name");
echo "Failure: Missing Parameter<p>" . CRM_Utils_Type::escape($name, 'String');
- exit();
+ throw new CRM_Core_Exception("PayPalIPN: Could not find an entry for $name");
}
return $value;
}
/**
- * @param $input
- * @param $ids
- * @param $objects
- * @param $first
+ * @param array $input
+ * @param array $ids
+ * @param array $objects
+ * @param bool $first
+ *
+ * @return void
*
- * @return bool
+ * @throws \CRM_Core_Exception
+ * @throws \CiviCRM_API3_Exception
*/
public function recur(&$input, &$ids, &$objects, $first) {
if (!isset($input['txnType'])) {
- CRM_Core_Error::debug_log_message("Could not find txn_type in input request");
+ Civi::log()->debug('PayPalIPN: Could not find txn_type in input request');
echo "Failure: Invalid parameters<p>";
- return FALSE;
+ return;
}
if ($input['txnType'] == 'subscr_payment' &&
$input['paymentStatus'] != 'Completed'
) {
- CRM_Core_Error::debug_log_message("Ignore all IPN payments that are not completed");
+ Civi::log()->debug('PayPalIPN: Ignore all IPN payments that are not completed');
echo "Failure: Invalid parameters<p>";
- return FALSE;
+ return;
}
$recur = &$objects['contributionRecur'];
// make sure the invoice ids match
// make sure the invoice is valid and matches what we have in the contribution record
if ($recur->invoice_id != $input['invoice']) {
- CRM_Core_Error::debug_log_message("Invoice values dont match between database and IPN request");
+ Civi::log()->debug('PayPalIPN: Invoice values dont match between database and IPN request (RecurID: ' . $recur->id . ').');
echo "Failure: Invoice values dont match between database and IPN request<p>";
- return FALSE;
+ return;
}
$now = date('YmdHis');
}
$sendNotification = FALSE;
$subscriptionPaymentStatus = NULL;
- //set transaction type
+ // set transaction type
$txnType = $this->retrieve('txn_type', 'String');
+ $contributionStatuses = array_flip(CRM_Contribute_BAO_Contribution::buildOptions('contribution_status_id', 'validate'));
switch ($txnType) {
case 'subscr_signup':
$recur->create_date = $now;
- //some times subscr_signup response come after the
- //subscr_payment and set to pending mode.
+ // sometimes subscr_signup response come after the subscr_payment and set to pending mode.
+
$statusID = CRM_Core_DAO::getFieldValue('CRM_Contribute_DAO_ContributionRecur',
$recur->id, 'contribution_status_id'
);
- if ($statusID != 5) {
- $recur->contribution_status_id = 2;
+ if ($statusID != $contributionStatuses['In Progress']) {
+ $recur->contribution_status_id = $contributionStatuses['Pending'];
}
$recur->processor_id = $this->retrieve('subscr_id', 'String');
$recur->trxn_id = $recur->processor_id;
break;
case 'subscr_eot':
- if ($recur->contribution_status_id != 3) {
- $recur->contribution_status_id = 1;
+ if ($recur->contribution_status_id != $contributionStatuses['Cancelled']) {
+ $recur->contribution_status_id = $contributionStatuses['Completed'];
}
$recur->end_date = $now;
$sendNotification = TRUE;
break;
case 'subscr_cancel':
- $recur->contribution_status_id = 3;
+ $recur->contribution_status_id = $contributionStatuses['Cancelled'];
$recur->cancel_date = $now;
break;
case 'subscr_failed':
- $recur->contribution_status_id = 4;
+ $recur->contribution_status_id = $contributionStatuses['Failed'];
$recur->modified_date = $now;
break;
case 'subscr_modify':
- CRM_Core_Error::debug_log_message("We do not handle modifications to subscriptions right now");
+ Civi::log()->debug('PayPalIPN: We do not handle modifications to subscriptions right now (RecurID: ' . $recur->id . ').');
echo "Failure: We do not handle modifications to subscriptions right now<p>";
- return FALSE;
+ return;
case 'subscr_payment':
if ($first) {
// make sure the contribution status is not done
// since order of ipn's is unknown
- if ($recur->contribution_status_id != 1) {
- $recur->contribution_status_id = 5;
+ if ($recur->contribution_status_id != $contributionStatuses['Completed']) {
+ $recur->contribution_status_id = $contributionStatuses['In Progress'];
}
break;
}
$recur->save();
if ($sendNotification) {
-
$autoRenewMembership = FALSE;
if ($recur->id &&
isset($ids['membership']) && $ids['membership']
}
if (!$first) {
- //check if this contribution transaction is already processed
- //if not create a contribution and then get it processed
+ // check if this contribution transaction is already processed
+ // if not create a contribution and then get it processed
$contribution = new CRM_Contribute_BAO_Contribution();
$contribution->trxn_id = $input['trxn_id'];
if ($contribution->trxn_id && $contribution->find()) {
- CRM_Core_Error::debug_log_message("returning since contribution has already been handled");
+ Civi::log()->debug('PayPalIPN: Returning since contribution has already been handled (trxn_id: ' . $contribution->trxn_id . ')');
echo "Success: Contribution has already been handled<p>";
- return TRUE;
+ return;
}
- $contribution->contact_id = $ids['contact'];
- $contribution->financial_type_id = $objects['contributionType']->id;
- $contribution->contribution_page_id = $ids['contributionPage'];
- $contribution->contribution_recur_id = $ids['contributionRecur'];
- $contribution->receive_date = $now;
- $contribution->currency = $objects['contribution']->currency;
- $contribution->payment_instrument_id = $objects['contribution']->payment_instrument_id;
- $contribution->amount_level = $objects['contribution']->amount_level;
- $contribution->campaign_id = $objects['contribution']->campaign_id;
-
- $objects['contribution'] = &$contribution;
+ if ($input['paymentStatus'] != 'Completed') {
+ throw new CRM_Core_Exception("Ignore all IPN payments that are not completed");
+ }
+
+ // In future moving to create pending & then complete, but this OK for now.
+ // Also consider accepting 'Failed' like other processors.
+ $input['contribution_status_id'] = $contributionStatuses['Completed'];
+ $input['original_contribution_id'] = $ids['contribution'];
+ $input['contribution_recur_id'] = $ids['contributionRecur'];
+
+ civicrm_api3('Contribution', 'repeattransaction', $input);
+ return;
}
$this->single($input, $ids, $objects,
}
/**
- * @param $input
- * @param $ids
- * @param $objects
+ * @param array $input
+ * @param array $ids
+ * @param array $objects
* @param bool $recur
* @param bool $first
*
- * @return bool
+ * @return void
*/
- public function single(
- &$input, &$ids, &$objects,
- $recur = FALSE,
- $first = FALSE
- ) {
+ public function single(&$input, &$ids, &$objects, $recur = FALSE, $first = FALSE) {
$contribution = &$objects['contribution'];
// make sure the invoice is valid and matches what we have in the contribution record
if ((!$recur) || ($recur && $first)) {
if ($contribution->invoice_id != $input['invoice']) {
- CRM_Core_Error::debug_log_message("Invoice values dont match between database and IPN request");
+ Civi::log()->debug('PayPalIPN: Invoice values dont match between database and IPN request. (ID: ' . $contribution->id . ').');
echo "Failure: Invoice values dont match between database and IPN request<p>";
- return FALSE;
+ return;
}
}
else {
if (!$recur) {
if ($contribution->total_amount != $input['amount']) {
- CRM_Core_Error::debug_log_message("Amount values dont match between database and IPN request");
+ Civi::log()->debug('PayPalIPN: Amount values dont match between database and IPN request. (ID: ' . $contribution->id . ').');
echo "Failure: Amount values dont match between database and IPN request<p>";
- return FALSE;
+ return;
}
}
else {
$transaction = new CRM_Core_Transaction();
- $participant = &$objects['participant'];
- $membership = &$objects['membership'];
-
$status = $input['paymentStatus'];
if ($status == 'Denied' || $status == 'Failed' || $status == 'Voided') {
return $this->failed($objects, $transaction);
}
// check if contribution is already completed, if so we ignore this ipn
- if ($contribution->contribution_status_id == 1) {
+ $completedStatusId = CRM_Core_Pseudoconstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed');
+ if ($contribution->contribution_status_id == $completedStatusId) {
$transaction->commit();
- CRM_Core_Error::debug_log_message("returning since contribution has already been handled");
+ Civi::log()->debug('PayPalIPN: Returning since contribution has already been handled. (ID: ' . $contribution->id . ').');
echo "Success: Contribution has already been handled<p>";
- return TRUE;
+ return;
}
$this->completeTransaction($input, $ids, $objects, $transaction, $recur);
/**
* Main function.
*
- * @return bool
+ * @throws \CRM_Core_Exception
+ * @throws \CiviCRM_API3_Exception
*/
public function main() {
-
$objects = $ids = $input = array();
$component = $this->retrieve('module', 'String');
$input['component'] = $component;
$ids['onbehalf_dupe_alert'] = $this->retrieve('onBehalfDupeAlert', 'Integer', FALSE);
}
- $paymentProcessorID = $this->retrieve('processor_id', 'Integer', FALSE);
- if (empty($paymentProcessorID)) {
- $processorParams = array(
- 'user_name' => $this->retrieve('business', 'String', FALSE),
- 'payment_processor_type_id' => CRM_Core_DAO::getFieldValue('CRM_Financial_DAO_PaymentProcessorType', 'PayPal_Standard', 'id', 'name'),
- 'is_test' => empty($input['is_test']) ? 0 : 1,
- );
+ $paymentProcessorID = self::getPayPalPaymentProcessorID($input, $ids);
- $processorInfo = array();
- if (!CRM_Financial_BAO_PaymentProcessor::retrieve($processorParams, $processorInfo)) {
- return FALSE;
- }
- $paymentProcessorID = $processorInfo['id'];
- }
+ Civi::log()->debug('PayPalIPN: Received (ContactID: ' . $ids['contact'] . '; trxn_id: ' . $input['trxn_id'] . ').');
if (!$this->validateData($input, $ids, $objects, TRUE, $paymentProcessorID)) {
- return FALSE;
+ return;
}
self::$_paymentProcessor = &$objects['paymentProcessor'];
if ($ids['contributionRecur']) {
// check if first contribution is completed, else complete first contribution
$first = TRUE;
- if ($objects['contribution']->contribution_status_id == 1) {
+ $completedStatusId = CRM_Core_Pseudoconstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed');
+ if ($objects['contribution']->contribution_status_id == $completedStatusId) {
$first = FALSE;
}
- return $this->recur($input, $ids, $objects, $first);
+ $this->recur($input, $ids, $objects, $first);
+ return;
}
- else {
- return $this->single($input, $ids, $objects, FALSE, FALSE);
- }
- }
- else {
- return $this->single($input, $ids, $objects, FALSE, FALSE);
}
+ $this->single($input, $ids, $objects, FALSE, FALSE);
}
/**
- * @param $input
- * @param $ids
+ * @param array $input
+ * @param array $ids
*
- * @return bool
+ * @throws \CRM_Core_Exception
*/
public function getInput(&$input, &$ids) {
if (!$this->getBillingID($ids)) {
- return FALSE;
+ return;
}
$input['txnType'] = $this->retrieve('txn_type', 'String', FALSE);
$input['fee_amount'] = $this->retrieve('mc_fee', 'Money', FALSE);
$input['net_amount'] = $this->retrieve('settle_amount', 'Money', FALSE);
$input['trxn_id'] = $this->retrieve('txn_id', 'String', FALSE);
+
+ $paymentDate = $this->retrieve('payment_date', 'String', FALSE);
+ if (!empty($paymentDate)) {
+ $receiveDateTime = new DateTime($paymentDate);
+ $input['receive_date'] = $receiveDateTime->format('YmdHis');
+ }
+ }
+
+
+ /**
+ * Gets PaymentProcessorID for PayPal
+ *
+ * @param array $input
+ * @param array $ids
+ *
+ * @return int
+ * @throws \CRM_Core_Exception
+ * @throws \CiviCRM_API3_Exception
+ */
+ public function getPayPalPaymentProcessorID($input, $ids) {
+ // First we try and retrieve from POST params
+ $paymentProcessorID = $this->retrieve('processor_id', 'Integer', FALSE);
+ if (!empty($paymentProcessorID)) {
+ return $paymentProcessorID;
+ }
+
+ // Then we try and get it from recurring contribution ID
+ if (!empty($ids['contributionRecur'])) {
+ $contributionRecur = civicrm_api3('ContributionRecur', 'getsingle', array(
+ 'id' => $ids['contributionRecur'],
+ 'return' => ['payment_processor_id'],
+ ));
+ if (!empty($contributionRecur['payment_processor_id'])) {
+ return $contributionRecur['payment_processor_id'];
+ }
+ }
+
+ // This is an unreliable method as there could be more than one instance.
+ // Recommended approach is to use the civicrm/payment/ipn/xx url where xx is the payment
+ // processor id & the handleNotification function (which should call the completetransaction api & by-pass this
+ // entirely). The only thing the IPN class should really do is extract data from the request, validate it
+ // & call completetransaction or call fail? (which may not exist yet).
+
+ Civi::log()->warning('Unreliable method used to get payment_processor_id for PayPal IPN - this will cause problems if you have more than one instance');
+ // Then we try and retrieve based on business email ID
+ $paymentProcessorTypeID = CRM_Core_DAO::getFieldValue('CRM_Financial_DAO_PaymentProcessorType', 'PayPal_Standard', 'id', 'name');
+ $processorParams = [
+ 'user_name' => $this->retrieve('business', 'String', FALSE),
+ 'payment_processor_type_id' => $paymentProcessorTypeID,
+ 'is_test' => empty($input['is_test']) ? 0 : 1,
+ 'options' => ['limit' => 1],
+ 'return' => ['id'],
+ ];
+ $paymentProcessorID = civicrm_api3('PaymentProcessor', 'getvalue', $processorParams);
+ if (empty($paymentProcessorID)) {
+ Throw new CRM_Core_Exception('PayPalIPN: Could not get Payment Processor ID');
+ }
+ return $paymentProcessorID;
}
}