Commit 700cf121 authored by mattwire's avatar mattwire
Browse files

Initial commit

parents
<?php
/**
* https://civicrm.org/licensing
*/
/**
* Shared payment IPN functions that should one day be migrated to CiviCRM core
*/
trait CRM_Core_Payment_MJWIPNTrait {
/**********************
* MJW_Core_Payment_MJWIPNTrait: 20190901
* @requires MJW_Payment_Api: 20190901
*********************/
/**
* @var array Payment processor
*/
private $_paymentProcessor;
/**
* Do we send an email receipt for each contribution?
*
* @var int
*/
protected $is_email_receipt = NULL;
/**
* The recurring contribution ID associated with the transaction
* @var int
*/
protected $contribution_recur_id = NULL;
/**
* The IPN event type
* @var string
*/
protected $event_type = NULL;
/**
* Set the value of is_email_receipt to use when a new contribution is received for a recurring contribution
* If not set, we respect the value set on the ContributionRecur entity.
*
* @param int $sendReceipt The value of is_email_receipt
*/
public function setSendEmailReceipt($sendReceipt) {
switch ($sendReceipt) {
case 0:
$this->is_email_receipt = 0;
break;
case 1:
$this->is_email_receipt = 1;
break;
default:
$this->is_email_receipt = 0;
}
}
/**
* Get the value of is_email_receipt to use when a new contribution is received for a recurring contribution
* If not set, we respect the value set on the ContributionRecur entity.
*
* @return int
* @throws \CiviCRM_API3_Exception
*/
public function getSendEmailReceipt() {
if (isset($this->is_email_receipt)) {
return (int) $this->is_email_receipt;
}
if (!empty($this->contribution_recur_id)) {
$this->is_email_receipt = civicrm_api3('ContributionRecur', 'getvalue', [
'return' => "is_email_receipt",
'id' => $this->contribution_recur_id,
]);
}
return (int) $this->is_email_receipt;
}
/**
* Get the payment processor
* The $_GET['processor_id'] value is set by CRM_Core_Payment::handlePaymentMethod.
*/
protected function getPaymentProcessor() {
$paymentProcessorId = (int) CRM_Utils_Array::value('processor_id', $_GET);
if (empty($paymentProcessorId)) {
$this->exception('Failed to get payment processor id');
}
try {
$this->_paymentProcessor = \Civi\Payment\System::singleton()->getById($paymentProcessorId)->getPaymentProcessor();
}
catch(Exception $e) {
$this->exception('Failed to get payment processor');
}
}
/**
* @deprecated Use recordCancelled()
* Mark a contribution as cancelled and update related entities
*
* @param array $params [ 'id' -> contribution_id, 'payment_processor_id' -> payment_processor_id]
*
* @return bool
* @throws \CiviCRM_API3_Exception
*/
protected function canceltransaction($params) {
return $this->incompletetransaction($params, 'cancel');
}
/**
* @deprecated - Use recordFailed()
* Mark a contribution as failed and update related entities
*
* @param array $params [ 'id' -> contribution_id, 'payment_processor_id' -> payment_processor_id]
*
* @return bool
* @throws \CiviCRM_API3_Exception
*/
protected function failtransaction($params) {
return $this->incompletetransaction($params, 'fail');
}
/**
* @deprecated - Use recordXX methods
* Handler for failtransaction and canceltransaction - do not call directly
*
* @param array $params
* @param string $mode
*
* @return bool
* @throws \CiviCRM_API3_Exception
*/
protected function incompletetransaction($params, $mode) {
$requiredParams = ['id', 'payment_processor_id'];
foreach ($requiredParams as $required) {
if (!isset($params[$required])) {
$this->exception('canceltransaction: Missing mandatory parameter: ' . $required);
}
}
if (isset($params['payment_processor_id'])) {
$input['payment_processor_id'] = $params['payment_processor_id'];
}
$contribution = new CRM_Contribute_BAO_Contribution();
$contribution->id = $params['id'];
if (!$contribution->find(TRUE)) {
throw new CiviCRM_API3_Exception('A valid contribution ID is required', 'invalid_data');
}
if (!$contribution->loadRelatedObjects($input, $ids, TRUE)) {
throw new CiviCRM_API3_Exception('failed to load related objects');
}
$input['trxn_id'] = !empty($params['trxn_id']) ? $params['trxn_id'] : $contribution->trxn_id;
if (!empty($params['fee_amount'])) {
$input['fee_amount'] = $params['fee_amount'];
}
$objects['contribution'] = &$contribution;
$objects = array_merge($objects, $contribution->_relatedObjects);
$transaction = new CRM_Core_Transaction();
switch ($mode) {
case 'cancel':
return $this->cancelled($objects, $transaction);
case 'fail':
return $this->failed($objects, $transaction);
default:
throw new CiviCRM_API3_Exception('Unknown incomplete transaction type: ' . $mode);
}
}
protected function recordPending($params) {
// Nothing to do
// @todo Maybe in the future record things like the pending reason if a payment is temporarily held?
}
/**
* Record a completed (successful) contribution
* @param array $params
*
* @throws \CiviCRM_API3_Exception
*/
protected function recordCompleted($params) {
$description = 'recordCompleted';
$this->checkRequiredParams($description, ['contribution_id'], $params);
$contributionStatusID = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed');
if (!empty($params['contribution_recur_id'])) {
$this->recordRecur($params, $description, $contributionStatusID);
}
else {
$params['id'] = $params['contribution_id'];
$pendingStatusId = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Pending');
if (civicrm_api3('Contribution', 'getvalue', [
'return' => "contribution_status_id",
'id' => $params['id']
]) == $pendingStatusId) {
civicrm_api3('Contribution', 'completetransaction', $params);
}
}
}
/**
* Record a failed contribution
* @param array $params
*
* @throws \CiviCRM_API3_Exception
*/
protected function recordFailed($params) {
$description = 'recordFailed';
$this->checkRequiredParams($description, ['contribution_id'], $params);
$contributionStatusID = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Failed');
if (!empty($params['contribution_recur_id'])) {
$this->recordRecur($params, $description, $contributionStatusID);
}
else {
$this->recordSingle($params, $description, $contributionStatusID);
}
}
/**
* Record a cancelled contribution
* @param array $params
*
* @throws \CiviCRM_API3_Exception
*/
protected function recordCancelled($params) {
$description = 'recordCancelled';
$this->checkRequiredParams($description, ['contribution_id'], $params);
$contributionStatusID = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Cancelled');
if (!empty($params['contribution_recur_id'])) {
$this->recordRecur($params, $description, $contributionStatusID);
}
else {
$this->recordSingle($params, $description, $contributionStatusID);
}
}
/**
* Record a refunded contribution
* @param array $params
*
* @throws \CiviCRM_API3_Exception
*/
protected function recordRefund($params) {
$this->checkRequiredParams('recordRefund', ['contribution_id', 'total_amount'], $params);
if ($params['total_amount'] > 0) {
$params['total_amount'] = -$params['total_amount'];
}
if (empty($params['trxn_date'])) {
$params['trxn_date'] = date('YmdHis');
}
civicrm_api3('Payment', 'create', $params);
}
/**
* Check that required params are present
*
* @param string $description
* For error logs
* @param array $requiredParams
* Array of params that are required
* @param array $params
* Array of params to check
*/
protected function checkRequiredParams($description, $requiredParams, $params) {
foreach ($requiredParams as $required) {
if (!isset($params[$required])) {
$this->exception("{$description}: Missing mandatory parameter: {$required}");
}
}
}
/**
* Record a contribution against a recur (subscription)
* @param array $params
* @param string $description
* @param int $contributionStatusID
*
* @throws \CiviCRM_API3_Exception
*/
private function recordRecur($params, $description, $contributionStatusID) {
// Process as a payment in a recurring series
// We should have been passed a contribution_id, this either needs updating via completetransaction or repeating via repeattransaction
// If we've already processed it then we'll have a payment with the unique transaction ID
$this->checkRequiredParams($description, ['contribution_id', 'contribution_recur_id', 'payment_processor_transaction_id'], $params);
$matchingContributions = civicrm_api3('Mjwpayment', 'get_contribution', ['trxn_id' => $params['payment_processor_transaction_id']]);
if ($matchingContributions['count'] == 0) {
// This is a new transaction Id in a recurring series, trigger repeattransaction
// @fixme: We may need to consider handling partial payments on the same "invoice/order" (contribution)
// but for now we assume that a new "completed" transaction means a new payment
$repeatParams = [
'contribution_status_id' => $contributionStatusID,
'original_contribution_id' => $params['contribution_id'],
'contribution_recur_id' => $params['contribution_recur_id'],
'trxn_id' => $params['payment_processor_transaction_id'],
'is_email_receipt' => $this->getSendEmailReceipt(),
];
civicrm_api3('Contribution', 'repeattransaction', $repeatParams);
}
}
/**
* Record a change to a single contribution (eg. Failed/Cancelled).
*
* @param array $params
* @param string $description
* @param int $contributionStatusID
*
* @throws \CiviCRM_API3_Exception
*/
private function recordSingle($params, $description, $contributionStatusID) {
$params['id'] = $params['contribution_id'];
$params['contribution_status_id'] = $contributionStatusID;
civicrm_api3('Contribution', 'create', $params);
}
protected function recordSubscriptionCancelled($params = []) {
civicrm_api3('ContributionRecur', 'cancel', ['id' => $this->contribution_recur_id]);
}
/**
* Log and throw an IPN exception
*
* @param string $message
*/
protected function exception($message) {
$errorMessage = $this->getPaymentProcessorLabel() . ' Exception: Event: ' . $this->event_type . ' Error: ' . $message;
Civi::log()->debug($errorMessage);
http_response_code(400);
exit(1);
}
}
<?php
/**
* https://civicrm.org/licensing
*/
/**
* Shared payment functions that should one day be migrated to CiviCRM core
*/
trait CRM_Core_Payment_MJWTrait {
/**********************
* MJW_Core_Payment_MJWTrait: 20190901
*********************/
/**
* @var array params passed for payment
*/
protected $_params = [];
/**
* @var string The unique invoice/order reference from the payment processor
*/
private $paymentProcessorOrderID;
/**
* @var string The unique subscription reference from the payment processor
*/
private $paymentProcessorSubscriptionID;
/**
* Get the billing email address
*
* @param array $params
* @param int $contactId
*
* @return string|NULL
*/
protected function getBillingEmail($params, $contactId) {
$billingLocationId = CRM_Core_BAO_LocationType::getBilling();
$emailAddress = CRM_Utils_Array::value("email-{$billingLocationId}", $params,
CRM_Utils_Array::value('email-Primary', $params,
CRM_Utils_Array::value('email', $params, NULL)));
if (empty($emailAddress) && !empty($contactId)) {
// Try and retrieve an email address from Contact ID
try {
$emailAddress = civicrm_api3('Email', 'getvalue', [
'contact_id' => $contactId,
'return' => ['email'],
]);
}
catch (CiviCRM_API3_Exception $e) {
return NULL;
}
}
return $emailAddress;
}
/**
* Get the billing email address
*
* @param array $params
* @param int $contactId
*
* @return string|NULL
*/
protected function getBillingPhone($params, $contactId) {
$billingLocationId = CRM_Core_BAO_LocationType::getBilling();
$phoneNumber = CRM_Utils_Array::value("phone-{$billingLocationId}", $params,
CRM_Utils_Array::value('phone-Primary', $params,
CRM_Utils_Array::value('phone', $params, NULL)));
if (empty($phoneNumber) && !empty($contactId)) {
// Try and retrieve a phone number from Contact ID
try {
$phoneNumber = civicrm_api3('Phone', 'getvalue', [
'contact_id' => $contactId,
'return' => ['phone'],
]);
}
catch (CiviCRM_API3_Exception $e) {
return NULL;
}
}
return $phoneNumber;
}
/**
* Get the contact id
*
* @param array $params
*
* @return int ContactID
*/
protected function getContactId($params) {
// $params['contact_id'] is preferred.
// contactID is set by: membership payment workflow
// cms_contactID is set by: membership payment workflow when "on behalf of" / related contact is used.
$contactId = CRM_Utils_Array::value('contactID', $params,
CRM_Utils_Array::value('contact_id', $params,
CRM_Utils_Array::value('cms_contactID', $params,
CRM_Utils_Array::value('cid', $params, NULL
))));
if (!empty($contactId)) {
return $contactId;
}
// FIXME: Ref: https://lab.civicrm.org/extensions/stripe/issues/16
// The problem is that when registering for a paid event, civicrm does not pass in the
// contact id to the payment processor (civicrm version 5.3). So, I had to patch your
// getContactId to check the session for a contact id. It's a hack and probably should be fixed in core.
// The code below is exactly what CiviEvent does, but does not pass it through to the next function.
$session = CRM_Core_Session::singleton();
return $session->get('transaction.userID', NULL);
}
/**
* Get the contribution ID
*
* @param $params
*
* @return mixed
*/
protected function getContributionId($params) {
/*
* contributionID is set in the contribution workflow
* We do NOT have a contribution ID for event and membership payments as they are created after payment!
* See: https://github.com/civicrm/civicrm-core/pull/13763 (for events)
*/
return CRM_Utils_Array::value('contributionID', $params);
}
/**
* Get the recurring contribution ID from parameters passed in to cancelSubscription
* Historical the data passed to cancelSubscription is pretty poor and doesn't include much!
*
* @param array $params
*
* @return int|null
*/
protected function getRecurringContributionId($params) {
// Not yet passed, but could be added via core PR
$contributionRecurId = CRM_Utils_Array::value('contribution_recur_id', $params,
CRM_Utils_Array::value('contributionRecurID', $params)); // backend live contribution
if (!empty($contributionRecurId)) {
return $contributionRecurId;
}
// Not yet passed, but could be added via core PR
$contributionId = CRM_Utils_Array::value('contribution_id', $params);
try {
return civicrm_api3('Contribution', 'getvalue', ['id' => $contributionId, 'return' => 'contribution_recur_id']);
}
catch (Exception $e) {
$subscriptionId = CRM_Utils_Array::value('subscriptionId', $params);
if (!empty($subscriptionId)) {
try {
return civicrm_api3('ContributionRecur', 'getvalue', ['processor_id' => $subscriptionId, 'return' => 'id']);
}
catch (Exception $e) {
return NULL;
}
}
return NULL;
}
}
/**
*
* @param array $params ['name' => payment instrument name]
*
* @return int|null
* @throws \CiviCRM_API3_Exception
*/
public static function createPaymentInstrument($params) {
$mandatoryParams = ['name'];
foreach ($mandatoryParams as $value) {
if (empty($params[$value])) {
Civi::log()->error('createPaymentInstrument: Missing mandatory parameter: ' . $value);
return NULL;
}
}
// Create a Payment Instrument
// See if we already have this type
$paymentInstrument = civicrm_api3('OptionValue', 'get', [
'option_group_id' => "payment_instrument",
'name' => $params['name'],
]);
if (empty($paymentInstrument['count'])) {
// Otherwise create it
try {
$financialAccount = civicrm_api3('FinancialAccount', 'getsingle', [
'financial_account_type_id' => "Asset",
'name' => "Payment Processor Account",
]);
}
catch (Exception $e) {
$financialAccount = civicrm_api3('FinancialAccount', 'getsingle', [
'financial_account_type_id' => "Asset",
'name' => "Payment Processor Account",
'options' => ['limit' => 1, 'sort' => "id ASC"],
]);
}
$paymentParams = [
'option_group_id' => "payment_instrument",
'name' => $params['name'],
'description' => $params['name'],
'financial_account_id' => $financialAccount['id'],
];
$paymentInstrument = civicrm_api3('OptionValue', 'create', $paymentParams);
$paymentInstrumentId = $paymentInstrument['values'][$paymentInstrument['id']]['value'];
}
else {
$paymentInstrumentId = $paymentInstrument['id'];
}
return $paymentInstrumentId;
}
/**
* Get the error URL to "bounce" the user back to.
* @param $params
*
* @return string|null
*/
public static function getErrorUrl($params) {
// Get proper entry URL for returning on error.
if (!(array_key_exists('qfKey', $params))) {
// Probably not called from a civicrm form (e.g. webform) -
// will return error object to original api caller.
return NULL;
}
else {
$qfKey = $params['qfKey'];
$parsed_url = parse_url($params['entryURL']);
$url_path = substr($parsed_url['path'], 1);
return CRM_Utils_System::url($url_path, $parsed_url['query'] . "&_qf_Main_display=1&qfKey={$qfKey}", FALSE, NULL, FALSE);
}
}
/**
* Are we using a test processor?
*
* @return bool
*/
public function getIsTestMode() {
return isset($this->_paymentProcessor['is_test']) && $this->_paymentProcessor['is_test'] ? TRUE : FALSE;
}
/**
* Format the fields for the payment processor.
* @fixme Copied from CiviCRM Core 5.13. We should remove this when all forms submit using this function (eg updateSubscriptionBillingInfo)
*
* In order to pass fields to the payment processor in a consistent way we add some renamed
* parameters.
*
* @param array $fields
*
* @return array
*/
private function formatParamsForPaymentProcessor($fields) {
$billingLocationId = CRM_Core_BAO_LocationType::getBilling();
// also add location name to the array
$this->_params["address_name-{$billingLocationId}"] = CRM_Utils_Array::value('billing_first_name', $this->_params) . ' ' . CRM_Utils_Array::value('billing_middle_name', $this->_params) . ' ' . CRM_Utils_Array::value('billing_last_name', $this->_params);
$this->_params["address_name-{$billingLocationId}"] = trim($this->_params["address_name-{$billingLocationId}"]);
// Add additional parameters that the payment processors are used to receiving.
if (!empty($this->_params["billing_state_province_id-{$billingLocationId}"])) {
$this->_params['state_province'] = $this->_params["state_province-{$billingLocationId}"] = $this->_params["billing_state_province-{$billingLocationId}"] = CRM_Core_PseudoConstant::stateProvinceAbbreviation($this->_params["billing_state_province_id-{$billingLocationId}"]);
}
if (!empty($this->_params["billing_country_id-{$billingLocationId}"])) {