diff --git a/au_com_agileware_ewayrecurring.class.php b/CRM/Core/Payment/eWAYRecurring.php similarity index 96% rename from au_com_agileware_ewayrecurring.class.php rename to CRM/Core/Payment/eWAYRecurring.php index 8f9bad2..3a5afba 100644 --- a/au_com_agileware_ewayrecurring.class.php +++ b/CRM/Core/Payment/eWAYRecurring.php @@ -1,19 +1,29 @@ $domain, 'id' => $id ] = $paymentProcessor; + + if(empty(self::$instances["$domain/$id"])) { + $mode = empty( $paymentProcessor['is_test'] ) ? 'test' : 'live'; + self::$instances["$domain/$id"] = new self( $mode, $paymentProcessor ); + } + + return self::$instances["$domain/$id"]; + } /** * Constructor @@ -28,24 +38,6 @@ function __construct($mode, &$paymentProcessor) { $this->_processorName = ts('eWay Recurring'); } - /** - * Create eWay client using credentials from payment processor. - * - * @return \Eway\Rapid\Contract\Client - */ - function getEWayClient() { - if(!$this->eWayClient) { - $eWayApiKey = $this->_paymentProcessor['user_name']; // eWay Api Key - $eWayApiPassword = $this->_paymentProcessor['password']; // eWay Api Password - $eWayEndPoint = ($this->_paymentProcessor['is_test']) ? \Eway\Rapid\Client::MODE_SANDBOX : \Eway\Rapid\Client::MODE_PRODUCTION; - - - $this->eWayClient = \Eway\Rapid::createClient($eWayApiKey, $eWayApiPassword, $eWayEndPoint); - } - - return $this->eWayClient; - } - /** * Validate contribution on successful response. * @@ -118,7 +110,7 @@ function getPaymentFormFieldsMetadata() { $jsSetting .= "CRM.eway.ppid = {$this->_paymentProcessor['id']};"; $jsSetting .= 'CRM.eway.paymentTokenInitialize();'; - CRM_Core_Resources::singleton()->addScript($jsSetting); + CRM_Core_Resources::singleton()->addScript($jsSetting, ['weight' => 10, 'region' => 'page-footer']); $this->jsEmbedded = TRUE; } return [ @@ -669,7 +661,7 @@ function checkConfig() { * @return bool */ function handlePaymentCron() { - return process_recurring_payments($this->_paymentProcessor, $this); + return $this->process_recurring_payments( $this->_paymentProcessor ); } /** diff --git a/CRM/eWAYRecurring/Page/VerifyPayment.php b/CRM/eWAYRecurring/Page/VerifyPayment.php index fdc939f..0ae3dd2 100644 --- a/CRM/eWAYRecurring/Page/VerifyPayment.php +++ b/CRM/eWAYRecurring/Page/VerifyPayment.php @@ -24,11 +24,11 @@ public function run() { if (count($paymentProcessorInfo) > 0) { $paymentProcessorInfo = $paymentProcessorInfo[0]; - $paymentProcessor = new au_com_agileware_ewayrecurring(($paymentProcessorInfo['is_test']) ? 'test' : 'live', $paymentProcessorInfo); + $paymentProcessor = new CRM_Core_Payment_eWAYRecurring(($paymentProcessorInfo['is_test']) ? 'test' : 'live', $paymentProcessorInfo); try { // This function will do redirect if the payment failed - $response = validateEwayContribution($paymentProcessor, $contributionInvoiceID); + $response = CRM_eWAYRecurring_Utils::validateEwayContribution($paymentProcessor, $contributionInvoiceID); } catch (CRM_Core_Exception $e) { } catch (CiviCRM_API3_Exception $e) { } diff --git a/CRM/eWAYRecurring/PaymentToken.php b/CRM/eWAYRecurring/PaymentToken.php index f299861..6822189 100644 --- a/CRM/eWAYRecurring/PaymentToken.php +++ b/CRM/eWAYRecurring/PaymentToken.php @@ -1,5 +1,10 @@ $data) { + foreach (array_keys($_POST) as $key) { + $data = CRM_Utils_Request::retrieve($key, 'String'); if (strpos($key, 'billing_street_address') !== FALSE) { $billingDetails['billing_street_address'] = $data; } @@ -288,7 +296,77 @@ public static function getPaymentProcessorById($id) { return NULL; } $paymentProcessorInfo = $paymentProcessorInfo[0]; - $paymentProcessor = new au_com_agileware_ewayrecurring(($paymentProcessorInfo['is_test']) ? 'test' : 'live', $paymentProcessorInfo); - return $paymentProcessor->getPaymentProcessor(); + + return CRM_Core_Payment_eWAYRecurring::getInstance($paymentProcessorInfo) + ->getPaymentProcessor(); + } + + public static function fillTokensMeta() { + // Caches payment processors + $processors = []; + + $results = ['count' => 0]; + + $tokens = PaymentToken::get(FALSE) + ->addWhere('payment_processor_id.payment_processor_type_id.name', '=', 'eWay_Recurring') + // Unretrieved tokens can end up with a saved token with id 0 + // ->addWhere('token', '!=', '0') + ->addClause('OR', [ + 'expiry_date', + 'IS NULL' + ], [ + 'masked_account_number', + 'IS NULL' + ]) + ->execute(); + + foreach ($tokens as $token) { + // Get Login details for eWAY from payment processor + $processor = $processors[$token['payment_processor_id']] ??= + PaymentProcessor::get(FALSE) + ->addWhere('id', '=', $token['payment_processor_id']) + ->execute()->first(); + + $eway_client = $processor['eway_client'] ??= CRM_eWAYRecurring_Utils::getEWayClient($processor); + + // Skip if unable to log in to eWAY + if($eway_client->getErrors()) { + continue; + } + + $token_customer = $eway_client->queryCustomer($token['token']); + + // Skip if custom query fails + $errors = $token_customer->getErrors(); + + if($errors){ + foreach($errors as &$error) { + $error = E::ts('Error retrieving data for token id %1: %2', [1 => $token['id'], 2 => $error]); + } + + $result['errors'] = array_merge($result['errors'] ?? [], $errors); + $result['is_error'] = TRUE; + continue; + } + + $card_details = $token_customer->Customers[0]->CardDetails; + + $card_number = $card_details->Number; + + $expiry_date = new DateTime('00:00:00.000'); + $expiry_date->setDate(2000 + (int) $card_details->ExpiryYear, $card_details->ExpiryMonth, 1); + $expiry_date->modify('+ 1 month - 1 second'); + + $expiry_date = $expiry_date->format('Ymd'); + + $update = PaymentToken::update(FALSE) + ->addWhere('id', '=', $token['id']) + ->addValue('masked_account_number', $card_number) + ->addValue('expiry_date', $expiry_date) + ->execute(); + + $results['count']++; + } + return $results; } -} \ No newline at end of file +} diff --git a/CRM/eWAYRecurring/ProcessTrait.php b/CRM/eWAYRecurring/ProcessTrait.php new file mode 100644 index 0000000..64b1466 --- /dev/null +++ b/CRM/eWAYRecurring/ProcessTrait.php @@ -0,0 +1,610 @@ +eWayClient) { + $eWayApiKey = $this->_paymentProcessor['user_name']; // eWay Api Key + $eWayApiPassword = $this->_paymentProcessor['password']; // eWay Api Password + $eWayEndPoint = ($this->_paymentProcessor['is_test']) ? \Eway\Rapid\Client::MODE_SANDBOX : \Eway\Rapid\Client::MODE_PRODUCTION; + + $this->eWayClient = \Eway\Rapid::createClient($eWayApiKey, $eWayApiPassword, $eWayEndPoint); + } + + return $this->eWayClient; + } + + public function process_recurring_payments() { + // If an ewayrecurring job is already running, we want to exit as soon as possible. + $lock = \Civi\Core\Container::singleton() + ->get('lockManager') + ->create('worker.ewayrecurring'); + if (!$lock->isFree() || !$lock->acquire()) { + Civi::log() + ->warning("Detected processing race for scheduled payments, aborting"); + + return FALSE; + } + + // Process today's scheduled contributions. + $scheduled_contributions = $this->get_scheduled_contributions(); + $scheduled_failed_contributions = $this->get_scheduled_failed_contributions(); + + $scheduled_contributions = array_merge($scheduled_failed_contributions, $scheduled_contributions); + + foreach ($scheduled_contributions as $contribution) { + if ($contribution->payment_processor_id != $this->getPaymentProcessor()['id']) { + continue; + } + + // Re-check schedule time, in case contribution already processed. + $next_sched = CRM_Core_DAO::getFieldValue('CRM_Contribute_DAO_ContributionRecur', + $contribution->id, + 'next_sched_contribution_date', + 'id', + TRUE); + + /* Get the number of Contributions already recorded for this Schedule. */ + $mainContributions = civicrm_api3('Contribution', 'get', [ + 'options' => ['limit' => 0], + 'sequential' => 1, + 'return' => ['total_amount', 'tax_amount'], + 'contribution_recur_id' => $contribution->id, + ]); + + $mainContributions = $mainContributions['values']; + $ccount = count($mainContributions); + + /* Schedule next contribution */ + if (($contribution->installments <= 0) || ($contribution->installments > $ccount + 1)) { + $next_sched = date('Y-m-d 00:00:00', strtotime($next_sched . " +{$contribution->frequency_interval} {$contribution->frequency_unit}s")); + } + else { + $next_sched = NULL; + /* Mark recurring contribution as complteted*/ + civicrm_api( + 'ContributionRecur', 'create', + [ + 'version' => '3', + 'id' => $contribution->id, + 'contribution_recur_status_id' => _contribution_status_id('Completed'), + ] + ); + } + + // Process payment + $amount_in_cents = preg_replace('/\.([0-9]{0,2}).*$/', '$1', + $contribution->amount); + + $addresses = civicrm_api('Address', 'get', + [ + 'version' => '3', + 'contact_id' => $contribution->contact_id, + ]); + + $billing_address = array_shift($addresses['values']); + + $invoice_id = md5(uniqid(rand(), TRUE)); + $eWayResponse = NULL; + + try { + if (!$contribution->failure_retry_date) { + // Only update the next schedule if we're not in a retry state. + $this->update_contribution_status($next_sched, $contribution); + } + + $mainContributions = $mainContributions[0]; + $new_contribution_record = []; + if (empty($mainContributions['tax_amount'])) { + $mainContributions['tax_amount'] = 0; + } + + $repeat_params = [ + 'contribution_recur_id' => $contribution->id, + 'contribution_status_id' => _contribution_status_id('Pending'), + 'total_amount' => $contribution->amount, + 'is_email_receipt' => 0, + ]; + + $repeated = civicrm_api3('Contribution', 'repeattransaction', $repeat_params); + + $new_contribution_record = $repeated; + + $new_contribution_record['contact_id'] = $contribution->contact_id; + $new_contribution_record['receive_date'] = CRM_Utils_Date::isoToMysql(date('Y-m-d H:i:s')); + $new_contribution_record['total_amount'] = ($contribution->amount - $mainContributions['tax_amount']); + $new_contribution_record['contribution_recur_id'] = $contribution->id; + $new_contribution_record['payment_instrument_id'] = $contribution->payment_instrument_id; + $new_contribution_record['address_id'] = $billing_address['id']; + $new_contribution_record['invoice_id'] = $invoice_id; + $new_contribution_record['campaign_id'] = $contribution->campaign_id; + $new_contribution_record['financial_type_id'] = $contribution->financial_type_id; + $new_contribution_record['payment_processor'] = $contribution->payment_processor_id; + $new_contribution_record['payment_processor_id'] = $contribution->payment_processor_id; + + $contributions = civicrm_api3( + 'Contribution', 'get', [ + 'sequential' => 1, + 'contribution_recur_id' => $contribution->id, + 'options' => ['sort' => "id ASC"], + ] + ); + + $precedent = new CRM_Contribute_BAO_Contribution(); + $precedent->contribution_recur_id = $contribution->id; + + $contributionSource = ''; + $contributionPageId = ''; + $contributionIsTest = 0; + + if ($precedent->find(TRUE)) { + $contributionSource = $precedent->source; + $contributionPageId = $precedent->contribution_page_id; + $contributionIsTest = $precedent->is_test; + } + + try { + $financial_type = civicrm_api3( + 'FinancialType', 'getsingle', [ + 'sequential' => 1, + 'return' => "name", + 'id' => $contribution->financial_type_id, + ]); + } + catch (CiviCRM_API3_Exception $e) { // Most likely due to FinancialType API not being available in < 4.5 - try DAO directly + $ft_bao = new CRM_Financial_BAO_FinancialType(); + $ft_bao->id = $contribution->financial_type_id; + $found = $ft_bao->find(TRUE); + + $financial_type = (array) $ft_bao; + } + + if (!isset($financial_type['name'])) { + throw new Exception ( + "Financial type could not be loaded for {$contribution->id}" + ); + } + + $new_contribution_record['source'] = "eWay Recurring {$financial_type['name']}:\n{$contributionSource}"; + $new_contribution_record['contribution_page_id'] = $contributionPageId; + $new_contribution_record['is_test'] = $contributionIsTest; + + // Retrieve the eWAY token + + if (!empty($contribution->payment_token_id)) { + try { + $token = civicrm_api3('PaymentToken', 'getvalue', [ + 'return' => 'token', + 'id' => $contribution->payment_token_id, + ]); + } + catch (CiviCRM_API3_Exception $e) { + $token = $contribution->processor_id; + } + } + else { + $token = $contribution->processor_id; + } + + if (!$token) { + throw new CRM_Core_Exception(\CRM_eWAYRecurring_ExtensionUtil::ts('No eWAY token found for Recurring Contribution %1', [1 => $contribution->id])); + } + + $eWayResponse = self::process_payment( + $token, + $amount_in_cents, + substr($invoice_id, 0, 16), + $financial_type['name'] . ($contributionSource ? + ":\n" . $contributionSource : '') + ); + + $new_contribution_record['trxn_id'] = $eWayResponse->getAttribute('TransactionID'); + + $responseErrors = $this->getEWayResponseErrors($eWayResponse); + + if (!$eWayResponse->TransactionStatus) { + $responseMessages = array_map('\Eway\Rapid::getMessage', explode(', ', $eWayResponse->ResponseMessage)); + $responseErrors = array_merge($responseMessages, $responseErrors); + } + + if (count($responseErrors)) { + // Mark transaction as failed + $new_contribution_record['contribution_status_id'] = _contribution_status_id('Failed'); + $this->mark_recurring_contribution_failed($contribution); + } + else { + // $this->send_receipt_email($new_contribution_record->id); + $new_contribution_record['contribution_status_id'] = _contribution_status_id('Completed'); + + $new_contribution_record['is_email_receipt'] = Civi::settings() + ->get('eway_recurring_keep_sending_receipts'); + + if ($contribution->failure_count > 0 && $contribution->contribution_status_id == _contribution_status_id('Failed')) { + // Failed recurring contribution completed successfuly after several retry. + $this->update_contribution_status($next_sched, $contribution); + CRM_Core_DAO::setFieldValue('CRM_Contribute_DAO_ContributionRecur', + $contribution->id, + 'contribution_status_id', + _contribution_status_id('In Progress')); + + try { + civicrm_api3('Activity', 'create', [ + 'source_contact_id' => $contribution->contact_id, + 'activity_type_id' => 'eWay Transaction Succeeded', + 'source_record' => $contribution->id, + 'details' => 'Transaction Succeeded after ' . $contribution->failure_count . ' retries', + ]); + } + catch (CiviCRM_API3_Exception $e) { + \Civi::log() + ->info('eWAY Recurring: Couldn\'t record success activity: ' . $e->getMessage()); + } + } + + CRM_Core_DAO::setFieldValue('CRM_Contribute_DAO_ContributionRecur', + $contribution->id, 'failure_count', 0); + + CRM_Core_DAO::setFieldValue('CRM_Contribute_DAO_ContributionRecur', + $contribution->id, 'failure_retry_date', ''); + } + + $api_action = ( + $new_contribution_record['contribution_status_id'] == _contribution_status_id('Completed') + ? 'completetransaction' + : 'create' + ); + + $updated = civicrm_api3('Contribution', $api_action, $new_contribution_record); + + $new_contribution_record = reset($updated['values']); + + // The invoice_id does not seem to be recorded by + // Contribution.completetransaction, so let's update it directly. + if ($api_action === 'completetransaction') { + $updated = civicrm_api3('Contribution', 'create', [ + 'id' => $new_contribution_record['id'], + 'invoice_id' => $invoice_id, + ]); + $new_contribution_record = reset($updated['values']); + } + + if (count($responseErrors)) { + $note = new CRM_Core_BAO_Note(); + + $note->entity_table = 'civicrm_contribution'; + $note->contact_id = $contribution->contact_id; + $note->entity_id = $new_contribution_record['id']; + $note->subject = ts('Transaction Error'); + $note->note = implode("\n", $responseErrors); + + $note->save(); + } + } + catch (Exception $e) { + Civi::log() + ->warning("Processing payment {$contribution->id} for {$contribution->contact_id}: " . $e->getMessage()); + + // already talk to eway? then we need to check the payment status + if ($eWayResponse) { + $new_contribution_record['contribution_status_id'] = _contribution_status_id('Pending'); + } + else { + $new_contribution_record['contribution_status_id'] = _contribution_status_id('Failed'); + } + + try { + $updated = civicrm_api3('Contribution', 'create', $new_contribution_record); + + $new_contribution_record = reset($updated['values']); + } + catch (CiviCRM_API3_Exception $e) { + Civi::log() + ->warning("Recurring contribution - Unable to set status of new contribution: " . $e->getMessage(), $new_contribution_record); + } + + // CIVIEWAY-147 there is an unknown system error that happen after civi talks to eway + // It might be a cache cleaning task happening at the same time that break this task + // Defer the query later to update the contribution status + if ($eWayResponse) { + $ewayParams = [ + 'access_code' => $eWayResponse->TransactionID, + 'contribution_id' => $new_contribution_record['id'], + 'payment_processor_id' => $contribution->payment_processor_id, + ]; + civicrm_api3('EwayContributionTransactions', 'create', $ewayParams); + } + else { + // Just mark it failed when eWay have no info about this at all + $this->mark_recurring_contribution_failed($contribution); + } + + $note = new CRM_Core_BAO_Note(); + + $note->entity_table = 'civicrm_contribution'; + $note->contact_id = $contribution->contact_id; + $note->entity_id = $new_contribution_record['id']; + $note->subject = ts('Contribution Error'); + $note->note = $e->getMessage(); + + $note->save(); + } + + unset($eWayResponse); + } + + $lock->release(); + } + + protected function update_contribution_status($next_sched, $contribution) { + $d_now = new DateTime(); + if ($next_sched) { + CRM_Core_DAO::setFieldValue('CRM_Contribute_DAO_ContributionRecur', + $contribution->id, + 'next_sched_contribution_date', + CRM_Utils_Date::isoToMysql($next_sched)); + } + else { + CRM_Core_DAO::setFieldValue('CRM_Contribute_DAO_ContributionRecur', + $contribution->id, + 'contribution_status_id', + _contribution_status_id('Completed')); + CRM_Core_DAO::setFieldValue('CRM_Contribute_DAO_ContributionRecur', + $contribution->id, + 'end_date', + CRM_Utils_Date::isoToMysql($d_now)); + } + } + + public function mark_recurring_contribution_failed($contribution) { + $today = new DateTime(); + $retryDelayInDays = Civi::settings() + ->get('eway_recurring_contribution_retry_delay'); + $today->modify("+" . $retryDelayInDays . " days"); + $today->setTime(0, 0, 0); + + try { + civicrm_api3('Activity', 'create', [ + 'source_contact_id' => $contribution->contact_id, + 'activity_type_id' => 'eWay Transaction Failed', + 'source_record' => $contribution->id, + ]); + } + catch (CiviCRM_API3_Exception $e) { + /* Failing to create the failure activity should not prevent the + ContributionRecur entity from being updated. Log it and move on. */ + \Civi::log() + ->info('eWAY Recurring: Couldn\'t record failure activity: ' . $e->getMessage()); + } + + civicrm_api3('ContributionRecur', 'create', [ + 'id' => $contribution->id, + 'failure_count' => (++$contribution->failure_count), + 'failure_retry_date' => $today->format("Y-m-d H:i:s"), + // CIVIEWAY-125: Don't actually mark as failed, because that causes the UI + // to melt down. + // 'contribution_status_id' => _contribution_status_id('Failed'), + ]); + } + + /** + * get_scheduled_contributions + * + * Gets recurring contributions that are scheduled to be processed today + * + * @return array An array of contribtion_recur objects + */ + protected function get_scheduled_contributions() { + $payment_processor = $this->getPaymentProcessor(); + $scheduled_today = new CRM_Contribute_BAO_ContributionRecur(); + + // Only get contributions for the current processor + $scheduled_today->payment_processor_id = $payment_processor['id']; + + // Only get contribution that are on or past schedule + $scheduled_today->whereAdd("`next_sched_contribution_date` <= now()"); + + // Don't get cancelled or failed contributions + $status_ids = implode(', ', [ + _contribution_status_id('In Progress'), + _contribution_status_id('Pending'), + ]); + $scheduled_today->whereAdd("`contribution_status_id` IN ({$status_ids})"); + + // Ignore transactions that have a failure_retry_date, these are subject to different conditions + $scheduled_today->whereAdd("`failure_retry_date` IS NULL"); + + // CIVIEWAY-124: Exclude contributions that never completed + $t = $scheduled_today->tableName(); + $ct = CRM_Contribute_BAO_Contribution::getTableName(); + $scheduled_today->whereAdd("EXISTS (SELECT 1 FROM `{$ct}` WHERE `contribution_status_id` = 1 AND `{$t}`.id = `{$ct}`.`contribution_recur_id`)"); + + // Exclude contributions that have already been processed + $scheduled_today->whereAdd("NOT EXISTS (SELECT 1 FROM `{$ct}` WHERE `{$ct}`.`receive_date` >= `{$t}`.`next_sched_contribution_date` AND `{$t}`.id = `{$ct}`.`contribution_recur_id`)"); + + $scheduled_today->find(); + + $scheduled_contributions = []; + + while ($scheduled_today->fetch()) { + $scheduled_contributions[] = clone $scheduled_today; + } + + return $scheduled_contributions; + } + + /** + * get_scheduled_failed_contributions + * + * Gets recurring contributions that are failed and to be processed today + * + * @return array An array of contribtion_recur objects + */ + public function get_scheduled_failed_contributions() { + $payment_processor = $this->getPaymentProcessor(); + + $maxFailRetry = Civi::settings() + ->get('eway_recurring_contribution_max_retry'); + + $scheduled_today = new CRM_Contribute_BAO_ContributionRecur(); + + // Only get contributions for the current processor + $scheduled_today->payment_processor_id = $payment_processor['id']; + + $scheduled_today->whereAdd("`failure_retry_date` <= now()"); + + $scheduled_today->contribution_status_id = _contribution_status_id('In Progress'); + $scheduled_today->whereAdd("`failure_count` < " . $maxFailRetry); + $scheduled_today->whereAdd("`failure_count` > 0"); + + // CIVIEWAY-124: Exclude contributions that never completed + $t = $scheduled_today->tableName(); + $ct = CRM_Contribute_BAO_Contribution::getTableName(); + $scheduled_today->whereAdd("EXISTS (SELECT 1 FROM `{$ct}` WHERE `contribution_status_id` = 1 AND `{$t}`.id = `contribution_recur_id`)"); + + // Exclude contributions that have already been processed + $scheduled_today->whereAdd("NOT EXISTS (SELECT 1 FROM `{$ct}` WHERE `{$ct}`.`receive_date` >= `{$t}`.`failure_retry_date` AND `{$t}`.id = `{$ct}`.`contribution_recur_id`)"); + + $scheduled_today->find(); + + $scheduled_failed_contributions = []; + + while ($scheduled_today->fetch()) { + $scheduled_failed_contributions[] = clone $scheduled_today; + } + + return $scheduled_failed_contributions; + } + + /** + * process_eway_payment + * + * Processes an eWay token payment + * + * @param object $eWayClient An eWay client set up and ready to go + * @param string $managed_customer_id The eWay token ID for the credit card + * you want to process + * @param string $amount_in_cents The amount in cents to charge the customer + * @param string $invoice_reference InvoiceReference to send to eWay + * @param string $invoice_description InvoiceDescription to send to eWay + * + * @return object eWay response object + * @throws SoapFault exceptions + */ + public function process_payment($managed_customer_id, $amount_in_cents, $invoice_reference, $invoice_description) { + static $prev_response = NULL; + + $eWayClient = $this->getEWayClient(); + + $paymentTransaction = [ + 'Customer' => [ + 'TokenCustomerID' => substr($managed_customer_id, 0, 16), + ], + 'Payment' => [ + 'TotalAmount' => substr($amount_in_cents, 0, 10), + 'InvoiceDescription' => substr(trim($invoice_description), 0, 64), + 'InvoiceReference' => substr($invoice_reference, 0, 64), + ], + 'TransactionType' => \Eway\Rapid\Enum\TransactionType::MOTO, + ]; + $eWayResponse = $eWayClient->createTransaction(\Eway\Rapid\Enum\ApiMethod::DIRECT, $paymentTransaction); + + if (isset($prev_response) && $prev_response->getAttribute('TransactionID') == $eWayResponse->getAttribute('TransactionID')) { + throw new Exception ( + 'eWay ProcessPayment returned duplicate transaction number: ' . + $prev_response->getAttribute('TransactionID') . ' vs ' . $eWayResponse->getAttribute('TransactionID') + ); + } + + $prev_response = &$eWayResponse; + + return $eWayResponse; + } + + /** + * send_receipt_email + * + * Sends a receipt for a contribution + * + * @param string $contribution_id The ID of the contribution to mark as + * complete + * @param CRM_Core_Payment $paymentObject CRM_Core_Payment object + * + * @return bool Success or failure + * @throws CRM_Core_Exception + */ + protected function send_receipt_email($contribution_id, $paymentObject) { + $contribution = new CRM_Contribute_BAO_Contribution(); + $contribution->id = $contribution_id; + $contribution->find(TRUE); + + $is_email_receipt = civicrm_api3('ContributionPage', 'getvalue', [ + 'id' => $contribution->contribution_page_id, + 'return' => 'is_email_receipt', + ]); + + if (!$is_email_receipt) { + return NULL; + } + + [ + $name, + $email, + ] = CRM_Contact_BAO_Contact_Location::getEmailDetails($contribution->contact_id); + + $domainValues = CRM_Core_BAO_Domain::getNameAndEmail(); + $receiptFrom = "$domainValues[0] <$domainValues[1]>"; + $receiptFromEmail = $domainValues[1]; + + $params = [ + 'groupName' => 'msg_tpl_workflow_contribution', + 'valueName' => 'contribution_online_receipt', + 'contactId' => $contribution->contact_id, + 'tplParams' => [ + 'contributeMode' => 'directIPN', + // Tells the person to contact us for cancellations + 'receiptFromEmail' => $receiptFromEmail, + 'amount' => $contribution->total_amount, + 'title' => self::RECEIPT_SUBJECT_TITLE, + 'is_recur' => TRUE, + 'is_monetary' => TRUE, + 'is_pay_later' => FALSE, + 'billingName' => $name, + 'email' => $email, + 'trxn_id' => $contribution->trxn_id, + 'receive_date' => CRM_Utils_Date::format($contribution->receive_date), + 'updateSubscriptionBillingUrl' => $paymentObject->subscriptionURL($contribution_id, 'contribution', 'billing'), + ], + 'from' => $receiptFrom, + 'toName' => $name, + 'toEmail' => $email, + 'isTest' => $contribution->is_test, + ]; + + [ + $sent, + $subject, + $message, + $html, + ] = CRM_Core_BAO_MessageTemplate::sendTemplate($params); + + return $sent; + } + +} diff --git a/CRM/eWAYRecurring/Upgrader.php b/CRM/eWAYRecurring/Upgrader.php index 1d3e01d..d6c414d 100644 --- a/CRM/eWAYRecurring/Upgrader.php +++ b/CRM/eWAYRecurring/Upgrader.php @@ -1,5 +1,6 @@ ctx->log->info('Apply 2.6.0 update; Update class names for eWAYRecurring payment processor type.'); + PaymentProcessorType::update(FALSE) + ->addValue('class_name', 'Payment_eWAYRecurring') + ->addWhere('class_name', '=', 'au.com.agileware.ewayrecurring') + ->execute(); + return TRUE; + } + } diff --git a/CRM/eWAYRecurring/Utils.php b/CRM/eWAYRecurring/Utils.php index 669ddf1..95662d9 100644 --- a/CRM/eWAYRecurring/Utils.php +++ b/CRM/eWAYRecurring/Utils.php @@ -1,5 +1,7 @@ id = $contribution['contribution_recur_id']; $bao->find(); - _eWAYRecurring_mark_recurring_contribution_Failed($bao); + CRM_Core_Payment_eWAYRecurring::getInstance($paymentProcessor) + ->mark_recurring_contribution_failed($bao); } $transactionToValidate['status'] = self::STATUS_FAILED; $transactionToValidate['failed_message'] = $response['transactionResponseError']; @@ -418,9 +421,52 @@ private static function updateRecurringContribution($contribution, $customerToke $recurringContribution['trxn_id'] = $transactionID; civicrm_api3('ContributionRecur', 'create', $recurringContribution); + } + catch (CiviCRM_API3_Exception $e) { + } + } - } catch (CiviCRM_API3_Exception $e) { + /** + * Validate eWay contribution by AccessCode, Invoice ID and Payment Processor. + * + * @param $paymentProcessor + * @param $invoiceID + * + * @return array|null + * @throws CRM_Core_Exception + * @throws CiviCRM_API3_Exception + */ + public static function validateEwayContribution($paymentProcessor, $invoiceID) { + if ($paymentProcessor instanceof CRM_Core_Payment_eWAYRecurring) { + $contribution = civicrm_api3('Contribution', 'get', [ + 'invoice_id' => $invoiceID, + 'sequential' => TRUE, + 'return' => [ + 'contribution_page_id', + 'contribution_recur_id', + 'total_amount', + 'is_test', + ], + 'is_test' => ($paymentProcessor->_mode == 'test') ? 1 : 0, + ]); + if (count($contribution['values']) > 0 && $contribution['values'][0]['total_amount'] > 0) { + // Include eWay SDK. + require_once E::path('vendor/autoload.php'); + $store = NULL; + + $contribution = $contribution['values'][0]; + // @TODO $form is an undefined variable + $eWayAccessCode = CRM_Utils_Request::retrieve('AccessCode', 'String', $store, FALSE, ""); + $qfKey = CRM_Utils_Request::retrieve('qfKey', 'String', $store, FALSE, ""); + + $paymentProcessor->validateContribution($eWayAccessCode, $contribution, $qfKey, $paymentProcessor->getPaymentProcessor()); + + return [ + 'contribution' => $contribution, + ]; + } } + return NULL; } } diff --git a/api/v3/EwayContributionTransactions.php b/api/v3/EwayContributionTransactions.php index fc6da70..82996ce 100644 --- a/api/v3/EwayContributionTransactions.php +++ b/api/v3/EwayContributionTransactions.php @@ -1,6 +1,6 @@ validatePendingTransactions($params); return civicrm_api3_create_success($response, $params, 'EwayContributionTransactions', 'validate'); -} \ No newline at end of file +} diff --git a/api/v3/EwayRecurring.php b/api/v3/EwayRecurring.php new file mode 100644 index 0000000..2b40f9e --- /dev/null +++ b/api/v3/EwayRecurring.php @@ -0,0 +1,7 @@ + 'eWay_Recurring', 'title' => 'eWay Recurring', 'description' => 'Recurring payments payment processor for eWay', - 'class_name' => 'au.com.agileware.ewayrecurring', + 'class_name' => 'Payment_eWAYRecurring', 'user_name_label' => 'API Key', 'password_label' => 'API Password', 'billing_mode' => 'notify', @@ -144,6 +138,22 @@ function ewayrecurring_civicrm_managed(&$entities) { 'is_active' => '1', ], ]; + $entities[] = [ + 'module' => 'au.com.agileware.ewayrecurring', + 'name' => 'eWay_fillTokensMeta_cron', + 'entity' => 'Job', + 'update' => 'never', + 'params' => [ + 'version' => 3, + 'run_frequency' => 'Hourly', + 'name' => 'eWay Recurring: fill missing tokens metadata', + 'description' => 'Loops through PaymentTokens for eWAY Recurring linked PaymentTokens that are missing expiry date or masked card number and queries eWAY Rapid API to fill these details in', + 'api_entity' => 'EwayRecurring', + 'api_action' => 'fillTokensMeta', + 'paramters' => '', + 'is_active' => '1', + ], + ]; } /** @@ -173,7 +183,7 @@ function _contribution_status_id($name) { function ewayrecurring_civicrm_buildForm($formName, &$form) { if ($formName == 'CRM_Contribute_Form_UpdateSubscription') { $paymentProcessor = $form->getVar('_paymentProcessorObj'); - if (($paymentProcessor instanceof au_com_agileware_ewayrecurring)) { + if (($paymentProcessor instanceof CRM_Core_Payment_eWAYRecurring)) { ($crid = $form->getVar('contributionRecurID')) || ($crid = $form->getVar('_crid')); if ($crid) { $sql = 'SELECT next_sched_contribution_date FROM civicrm_contribution_recur WHERE id = %1'; @@ -184,8 +194,10 @@ function ewayrecurring_civicrm_buildForm($formName, &$form) { 'Int', ], ])) { - [$defaults['next_scheduled_date'], - $defaults['next_scheduled_date_time']] = CRM_Utils_Date::setDateDefaults($default_nsd); + [ + $defaults['next_scheduled_date'], + $defaults['next_scheduled_date_time'], + ] = CRM_Utils_Date::setDateDefaults($default_nsd); $form->setDefaults($defaults); } // add next scheduled date field @@ -204,7 +216,7 @@ function ewayrecurring_civicrm_buildForm($formName, &$form) { } } } - elseif ($formName == 'CRM_Contribute_Form_CancelSubscription' && $form->getVar('_paymentProcessorObj') instanceof au_com_agileware_ewayrecurring) { + elseif ($formName == 'CRM_Contribute_Form_CancelSubscription' && $form->getVar('_paymentProcessorObj') instanceof CRM_Core_Payment_eWAYRecurring) { // remove send request to eway field $form->removeElement('send_cancel_request'); } @@ -263,7 +275,7 @@ function ewayrecurring_civicrm_preProcess($formName, &$form) { $invoiceID = $form->_params['invoiceID']; $validated = &Civi::$statics[__FUNCTION__ . '::validated']; if(!isset($validated[$invoiceID])) { - validateEwayContribution($paymentProcessor, $invoiceID); + CRM_eWAYRecurring_Utils::validateEwayContribution($paymentProcessor, $invoiceID); } // fixme CIVIEWAY-144 temporary fix, remove this if the issue is solved in core if (!$form->_priceSetId || CRM_Core_DAO::getFieldValue('CRM_Price_DAO_PriceSet', $form->_priceSetId, 'is_quick_config')) { @@ -274,17 +286,18 @@ function ewayrecurring_civicrm_preProcess($formName, &$form) { case 'CRM_Contribute_Form_Contribution_Confirm': case 'CRM_Event_Form_Registration_Confirm': $qfKey = $form->get('qfKey'); - $eWAYResponse = $qfKey ? unserialize(CRM_Core_Session::singleton()->get('eWAYResponse', $qfKey)) : false; + $eWAYResponse = $qfKey ? unserialize(CRM_Core_Session::singleton() + ->get('eWAYResponse', $qfKey)) : FALSE; $paymentProcessor = $form->getVar('_paymentProcessor') ?? NULL; - if (!empty($eWAYResponse->AccessCode) && ($paymentProcessor['object'] instanceof au_com_agileware_ewayrecurring)) { + if (!empty($eWAYResponse->AccessCode) && ($paymentProcessor['object'] instanceof CRM_Core_Payment_eWAYRecurring)) { $transaction = CRM_eWAYRecurring_Utils::validateEwayAccessCode($eWAYResponse->AccessCode, $paymentProcessor); - if($transaction['hasTransactionFailed']) { + if ($transaction['hasTransactionFailed']) { CRM_Core_session::setStatus(E::ts( 'A transaction has already been submitted, but failed. Continuing will result in a new transaction.' )); - CRM_Core_Session::singleton()->set('eWAYResponse', null, $qfKey); + CRM_Core_Session::singleton()->set('eWAYResponse', NULL, $qfKey); } - elseif(!$transaction['transactionNotProcessedYet']) { + elseif (!$transaction['transactionNotProcessedYet']) { throw new PaymentProcessorException( $form instanceof CRM_Event_Form_Registration_Confirm ? E::ts('Payment already completed for this Registration') @@ -296,50 +309,6 @@ function ewayrecurring_civicrm_preProcess($formName, &$form) { } } -/** - * Validate eWay contribution by AccessCode, Invoice ID and Payment Processor. - * - * @param $paymentProcessor - * @param $invoiceID - * - * @return array|null - * @throws CRM_Core_Exception - * @throws CiviCRM_API3_Exception - */ -function validateEwayContribution($paymentProcessor, $invoiceID) { - if ($paymentProcessor instanceof au_com_agileware_ewayrecurring) { - $contribution = civicrm_api3('Contribution', 'get', [ - 'invoice_id' => $invoiceID, - 'sequential' => TRUE, - 'return' => [ - 'contribution_page_id', - 'contribution_recur_id', - 'total_amount', - 'is_test', - ], - 'is_test' => ($paymentProcessor->_mode == 'test') ? 1 : 0, - ]); - - if (count($contribution['values']) > 0 && $contribution['values'][0]['total_amount'] > 0) { - // Include eWay SDK. - require_once extensionPath('vendor/autoload.php'); - $store = NULL; - - $contribution = $contribution['values'][0]; - // @TODO $form is an undefined variable - $eWayAccessCode = CRM_Utils_Request::retrieve('AccessCode', 'String', $store, FALSE, ""); - $qfKey = CRM_Utils_Request::retrieve('qfKey', 'String', $store, FALSE, ""); - - $paymentProcessor->validateContribution($eWayAccessCode, $contribution, $qfKey, $paymentProcessor->getPaymentProcessor()); - - return [ - 'contribution' => $contribution, - ]; - } - return NULL; - } -} - function _ewayrecurring_upgrade_schema_version(CRM_Queue_TaskContext $ctx, $schema) { CRM_Core_BAO_Extension::setSchemaVersion('au.com.agileware.ewayrecurring', $schema); return CRM_Queue_Task::TASK_SUCCESS; @@ -361,22 +330,6 @@ function _ewayrecurring_get_pp_id($processor) { return $processor['id']; } -/** - * Get the path of a resource file (in this extension). - * - * @param string|NULL $file - * Ex: NULL. - * Ex: 'css/foo.css'. - * - * @return string - * Ex: '/var/www/example.org/sites/default/ext/org.example.foo'. - * Ex: '/var/www/example.org/sites/default/ext/org.example.foo/css/foo.css'. - */ -function extensionPath($file = NULL) { - // return CRM_Core_Resources::singleton()->getPath(self::LONG_NAME, $file); - return __DIR__ . ($file === NULL ? '' : (DIRECTORY_SEPARATOR . $file)); -} - function ewayrecurring_civicrm_navigationMenu(&$menu) { _ewayrecurring_civix_insert_navigation_menu($menu, 'Administer', [ 'label' => E::ts('eWay Recurring Settings'), @@ -393,7 +346,7 @@ function ewayrecurring_civicrm_navigationMenu(&$menu) { */ function ewayrecurring_civicrm_coreResourceList(&$list, $region) { if ($region == 'html-header') { - Civi::resources()->addScriptFile('au.com.agileware.ewayrecurring', 'js/eway.js', $region); + Civi::resources()->addScriptFile('au.com.agileware.ewayrecurring', 'js/eway.js', [ 'region' => $region, 'weight' => 9 ]); $result = civicrm_api3('PaymentProcessorType', 'get', [ 'sequential' => 1, 'name' => "eWay_Recurring", @@ -418,4 +371,4 @@ function ewayrecurring_civicrm_coreResourceList(&$list, $region) { function ewayrecurring_civicrm_permission(&$permissions) { $permissions['view payment tokens'] = E::ts('CiviContribute: view payment tokens'); $permissions['edit payment tokens'] = E::ts('CiviContribute: edit payment tokens'); -} \ No newline at end of file +} diff --git a/eWAYRecurring.process.inc b/eWAYRecurring.process.inc deleted file mode 100644 index b7a83cb..0000000 --- a/eWAYRecurring.process.inc +++ /dev/null @@ -1,583 +0,0 @@ -get('lockManager') - ->create('worker.ewayrecurring'); - if (!$lock->isFree() || !$lock->acquire()) { - Civi::log()->warning("Detected processing race for scheduled payments, aborting"); - return FALSE; - } - - // Create eWay token client - $eWayClient = $paymentObject->getEWayClient(); - - // Process today's scheduled contributions. - $scheduled_contributions = get_scheduled_contributions($payment_processor); - $scheduled_failed_contributions = get_scheduled_failed_contributions($payment_processor); - - $scheduled_contributions = array_merge($scheduled_failed_contributions, $scheduled_contributions); - - foreach ($scheduled_contributions as $contribution) { - if ($contribution->payment_processor_id != $payment_processor['id']) { - continue; - } - - // Re-check schedule time, in case contribution already processed. - $next_sched = CRM_Core_DAO::getFieldValue('CRM_Contribute_DAO_ContributionRecur', - $contribution->id, - 'next_sched_contribution_date', - 'id', - TRUE); - - /* Get the number of Contributions already recorded for this Schedule. */ - $mainContributions = civicrm_api3('Contribution', 'get', [ - 'options' => ['limit' => 0], - 'sequential' => 1, - 'return' => ['total_amount', 'tax_amount'], - 'contribution_recur_id' => $contribution->id, - ]); - - $mainContributions = $mainContributions['values']; - $ccount = count($mainContributions); - - /* Schedule next contribution */ - if (($contribution->installments <= 0) || ($contribution->installments > $ccount + 1)) { - $next_sched = date('Y-m-d 00:00:00', strtotime($next_sched . " +{$contribution->frequency_interval} {$contribution->frequency_unit}s")); - } - else { - $next_sched = NULL; - /* Mark recurring contribution as complteted*/ - civicrm_api( - 'ContributionRecur', 'create', - [ - 'version' => '3', - 'id' => $contribution->id, - 'contribution_recur_status_id' => _contribution_status_id('Completed'), - ] - ); - } - - // Process payment - $amount_in_cents = preg_replace('/\.([0-9]{0,2}).*$/', '$1', - $contribution->amount); - - $addresses = civicrm_api('Address', 'get', - [ - 'version' => '3', - 'contact_id' => $contribution->contact_id, - ]); - - $billing_address = array_shift($addresses['values']); - - $invoice_id = md5(uniqid(rand(), TRUE)); - $eWayResponse = NULL; - - try { - if (!$contribution->failure_retry_date) { - // Only update the next schedule if we're not in a retry state. - _eWayRecurring_update_contribution_status($next_sched, $contribution); - } - - $mainContributions = $mainContributions[0]; - $new_contribution_record = []; - if (empty($mainContributions['tax_amount'])) { - $mainContributions['tax_amount'] = 0; - } - - $repeat_params = [ - 'contribution_recur_id' => $contribution->id, - 'contribution_status_id' => _contribution_status_id('Pending'), - 'total_amount' => $contribution->amount, - 'is_email_receipt' => 0, - ]; - - $repeated = civicrm_api3('Contribution', 'repeattransaction', $repeat_params); - - $new_contribution_record = $repeated; - - $new_contribution_record['contact_id'] = $contribution->contact_id; - $new_contribution_record['receive_date'] = CRM_Utils_Date::isoToMysql(date('Y-m-d H:i:s')); - $new_contribution_record['total_amount'] = ($contribution->amount - $mainContributions['tax_amount']); - $new_contribution_record['contribution_recur_id'] = $contribution->id; - $new_contribution_record['payment_instrument_id'] = $contribution->payment_instrument_id; - $new_contribution_record['address_id'] = $billing_address['id']; - $new_contribution_record['invoice_id'] = $invoice_id; - $new_contribution_record['campaign_id'] = $contribution->campaign_id; - $new_contribution_record['financial_type_id'] = $contribution->financial_type_id; - $new_contribution_record['payment_processor'] = $contribution->payment_processor_id; - $new_contribution_record['payment_processor_id'] = $contribution->payment_processor_id; - - $contributions = civicrm_api3( - 'Contribution', 'get', [ - 'sequential' => 1, - 'contribution_recur_id' => $contribution->id, - 'options' => ['sort' => "id ASC"], - ] - ); - - $precedent = new CRM_Contribute_BAO_Contribution(); - $precedent->contribution_recur_id = $contribution->id; - - $contributionSource = ''; - $contributionPageId = ''; - $contributionIsTest = 0; - - if ($precedent->find(TRUE)) { - $contributionSource = $precedent->source; - $contributionPageId = $precedent->contribution_page_id; - $contributionIsTest = $precedent->is_test; - } - - try { - $financial_type = civicrm_api3( - 'FinancialType', 'getsingle', [ - 'sequential' => 1, - 'return' => "name", - 'id' => $contribution->financial_type_id, - ]); - } catch (CiviCRM_API3_Exception $e) { // Most likely due to FinancialType API not being available in < 4.5 - try DAO directly - $ft_bao = new CRM_Financial_BAO_FinancialType(); - $ft_bao->id = $contribution->financial_type_id; - $found = $ft_bao->find(TRUE); - - $financial_type = (array) $ft_bao; - } - - - if (!isset($financial_type['name'])) { - throw new Exception ( - "Financial type could not be loaded for {$contribution->id}" - ); - } - - $new_contribution_record['source'] = "eWay Recurring {$financial_type['name']}:\n{$contributionSource}"; - $new_contribution_record['contribution_page_id'] = $contributionPageId; - $new_contribution_record['is_test'] = $contributionIsTest; - - // Retrieve the eWAY token - - if (!empty($contribution->payment_token_id)) { - try { - $token = civicrm_api3('PaymentToken', 'getvalue', [ - 'return' => 'token', - 'id' => $contribution->payment_token_id, - ]); - } catch (CiviCRM_API3_Exception $e) { - $token = $contribution->processor_id; - } - } - else { - $token = $contribution->processor_id; - } - - if (!$token) { - throw new CRM_Core_Exception(E::ts('No eWAY token found for Recurring Contribution %1', [1 => $contribution->id])); - } - - $eWayResponse = process_eway_payment( - $eWayClient, - $token, - $amount_in_cents, - substr($invoice_id, 0, 16), - $financial_type['name'] . ($contributionSource ? - ":\n" . $contributionSource : '') - ); - - $new_contribution_record['trxn_id'] = $eWayResponse->getAttribute('TransactionID'); - - $responseErrors = $paymentObject->getEWayResponseErrors($eWayResponse); - - if (!$eWayResponse->TransactionStatus) { - $responseMessages = array_map('\Eway\Rapid::getMessage', explode(', ', $eWayResponse->ResponseMessage)); - $responseErrors = array_merge($responseMessages, $responseErrors); - } - - if (count($responseErrors)) { - // Mark transaction as failed - $new_contribution_record['contribution_status_id'] = _contribution_status_id('Failed'); - _eWAYRecurring_mark_recurring_contribution_Failed($contribution); - } - else { - // send_receipt_email($new_contribution_record->id); - $new_contribution_record['contribution_status_id'] = _contribution_status_id('Completed'); - - $new_contribution_record['is_email_receipt'] = Civi::settings()->get('eway_recurring_keep_sending_receipts'); - - if ($contribution->failure_count > 0 && $contribution->contribution_status_id == _contribution_status_id('Failed')) { - // Failed recurring contribution completed successfuly after several retry. - _eWayRecurring_update_contribution_status($next_sched, $contribution); - CRM_Core_DAO::setFieldValue('CRM_Contribute_DAO_ContributionRecur', - $contribution->id, - 'contribution_status_id', - _contribution_status_id('In Progress')); - - try { - civicrm_api3('Activity', 'create', [ - 'source_contact_id' => $contribution->contact_id, - 'activity_type_id' => 'eWay Transaction Succeeded', - 'source_record' => $contribution->id, - 'details' => 'Transaction Succeeded after ' . $contribution->failure_count . ' retries', - ]); - } - catch (CiviCRM_API3_Exception $e) { - \Civi::log()->info('eWAY Recurring: Couldn\'t record success activity: ' . $e->getMessage()); - } - } - - CRM_Core_DAO::setFieldValue('CRM_Contribute_DAO_ContributionRecur', - $contribution->id, 'failure_count', 0); - - CRM_Core_DAO::setFieldValue('CRM_Contribute_DAO_ContributionRecur', - $contribution->id, 'failure_retry_date', ''); - } - - $api_action = ( - $new_contribution_record['contribution_status_id'] == _contribution_status_id('Completed') - ? 'completetransaction' - : 'create' - ); - - $updated = civicrm_api3('Contribution', $api_action, $new_contribution_record); - - $new_contribution_record = reset($updated['values']); - - // The invoice_id does not seem to be recorded by - // Contribution.completetransaction, so let's update it directly. - if ($api_action === 'completetransaction') { - $updated = civicrm_api3('Contribution', 'create', [ - 'id' => $new_contribution_record['id'], - 'invoice_id' => $invoice_id, - ]); - $new_contribution_record = reset($updated['values']); - } - - if (count($responseErrors)) { - $note = new CRM_Core_BAO_Note(); - - $note->entity_table = 'civicrm_contribution'; - $note->contact_id = $contribution->contact_id; - $note->entity_id = $new_contribution_record['id']; - $note->subject = ts('Transaction Error'); - $note->note = implode("\n", $responseErrors); - - $note->save(); - } - - } catch (Exception $e) { - Civi::log()->warning("Processing payment {$contribution->id} for {$contribution->contact_id}: " . $e->getMessage()); - - // already talk to eway? then we need to check the payment status - if ($eWayResponse) { - $new_contribution_record['contribution_status_id'] = _contribution_status_id('Pending'); - } else { - $new_contribution_record['contribution_status_id'] = _contribution_status_id('Failed'); - } - - try { - $updated = civicrm_api3('Contribution', 'create', $new_contribution_record); - - $new_contribution_record = reset($updated['values']); - } catch (CiviCRM_API3_Exception $e) { - Civi::log()->warning("Recurring contribution - Unable to set status of new contribution: " . $e->getMessage(), $new_contribution_record); - } - - // CIVIEWAY-147 there is an unknown system error that happen after civi talks to eway - // It might be a cache cleaning task happening at the same time that break this task - // Defer the query later to update the contribution status - if ($eWayResponse) { - $ewayParams = [ - 'access_code' => $eWayResponse->TransactionID, - 'contribution_id' => $new_contribution_record['id'], - 'payment_processor_id' => $contribution->payment_processor_id, - ]; - civicrm_api3('EwayContributionTransactions', 'create', $ewayParams); - } - else { - // Just mark it failed when eWay have no info about this at all - _eWAYRecurring_mark_recurring_contribution_Failed($contribution); - } - - $note = new CRM_Core_BAO_Note(); - - $note->entity_table = 'civicrm_contribution'; - $note->contact_id = $contribution->contact_id; - $note->entity_id = $new_contribution_record['id']; - $note->subject = ts('Contribution Error'); - $note->note = $e->getMessage(); - - $note->save(); - } - - unset($eWayResponse); - - } - - $lock->release(); -} - -function _eWayRecurring_update_contribution_status($next_sched, $contribution) { - $d_now = new DateTime(); - if ($next_sched) { - CRM_Core_DAO::setFieldValue('CRM_Contribute_DAO_ContributionRecur', - $contribution->id, - 'next_sched_contribution_date', - CRM_Utils_Date::isoToMysql($next_sched)); - } - else { - CRM_Core_DAO::setFieldValue('CRM_Contribute_DAO_ContributionRecur', - $contribution->id, - 'contribution_status_id', - _contribution_status_id('Completed')); - CRM_Core_DAO::setFieldValue('CRM_Contribute_DAO_ContributionRecur', - $contribution->id, - 'end_date', - CRM_Utils_Date::isoToMysql($d_now)); - } -} - -function _eWAYRecurring_mark_recurring_contribution_Failed($contribution) { - $today = new DateTime(); - $retryDelayInDays = Civi::settings() - ->get('eway_recurring_contribution_retry_delay'); - $today->modify("+" . $retryDelayInDays . " days"); - $today->setTime(0, 0, 0); - - try { - civicrm_api3('Activity', 'create', [ - 'source_contact_id' => $contribution->contact_id, - 'activity_type_id' => 'eWay Transaction Failed', - 'source_record' => $contribution->id, - ]); - } - catch (CiviCRM_API3_Exception $e) { - /* Failing to create the failure activity should not prevent the - ContributionRecur entity from being updated. Log it and move on. */ - \Civi::log()->info('eWAY Recurring: Couldn\'t record failure activity: ' . $e->getMessage()); - } - - civicrm_api3('ContributionRecur', 'create', [ - 'id' => $contribution->id, - 'failure_count' => (++$contribution->failure_count), - 'failure_retry_date' => $today->format("Y-m-d H:i:s"), - // CIVIEWAY-125: Don't actually mark as failed, because that causes the UI - // to melt down. - // 'contribution_status_id' => _contribution_status_id('Failed'), - ]); -} - -/** - * get_scheduled_contributions - * - * Gets recurring contributions that are scheduled to be processed today - * - * @return array An array of contribtion_recur objects - */ -function get_scheduled_contributions($payment_processor) { - $scheduled_today = new CRM_Contribute_BAO_ContributionRecur(); - - // Only get contributions for the current processor - $scheduled_today->payment_processor_id = $payment_processor['id']; - - // Only get contribution that are on or past schedule - $scheduled_today->whereAdd("`next_sched_contribution_date` <= now()"); - - // Don't get cancelled or failed contributions - $status_ids = implode(', ', [ _contribution_status_id('In Progress'), _contribution_status_id('Pending') ]); - $scheduled_today->whereAdd("`contribution_status_id` IN ({$status_ids})"); - - // Ignore transactions that have a failure_retry_date, these are subject to different conditions - $scheduled_today->whereAdd("`failure_retry_date` IS NULL"); - - // CIVIEWAY-124: Exclude contributions that never completed - $t = $scheduled_today->tableName(); - $ct = CRM_Contribute_BAO_Contribution::getTableName(); - $scheduled_today->whereAdd("EXISTS (SELECT 1 FROM `{$ct}` WHERE `contribution_status_id` = 1 AND `{$t}`.id = `{$ct}`.`contribution_recur_id`)"); - - // Exclude contributions that have already been processed - $scheduled_today->whereAdd("NOT EXISTS (SELECT 1 FROM `{$ct}` WHERE `{$ct}`.`receive_date` >= `{$t}`.`next_sched_contribution_date` AND `{$t}`.id = `{$ct}`.`contribution_recur_id`)"); - - $scheduled_today->find(); - - $scheduled_contributions = []; - - while ($scheduled_today->fetch()) { - $scheduled_contributions[] = clone $scheduled_today; - } - - return $scheduled_contributions; -} - -/** - * get_scheduled_failed_contributions - * - * Gets recurring contributions that are failed and to be processed today - * - * @return array An array of contribtion_recur objects - */ -function get_scheduled_failed_contributions($payment_processor) { - $maxFailRetry = Civi::settings() - ->get('eway_recurring_contribution_max_retry'); - - $scheduled_today = new CRM_Contribute_BAO_ContributionRecur(); - - // Only get contributions for the current processor - $scheduled_today->payment_processor_id = $payment_processor['id']; - - $scheduled_today->whereAdd("`failure_retry_date` <= now()"); - - $scheduled_today->contribution_status_id = _contribution_status_id('In Progress'); - $scheduled_today->whereAdd("`failure_count` < " . $maxFailRetry); - $scheduled_today->whereAdd("`failure_count` > 0"); - - // CIVIEWAY-124: Exclude contributions that never completed - $t = $scheduled_today->tableName(); - $ct = CRM_Contribute_BAO_Contribution::getTableName(); - $scheduled_today->whereAdd("EXISTS (SELECT 1 FROM `{$ct}` WHERE `contribution_status_id` = 1 AND `{$t}`.id = `contribution_recur_id`)"); - - // Exclude contributions that have already been processed - $scheduled_today->whereAdd("NOT EXISTS (SELECT 1 FROM `{$ct}` WHERE `{$ct}`.`receive_date` >= `{$t}`.`failure_retry_date` AND `{$t}`.id = `{$ct}`.`contribution_recur_id`)"); - - $scheduled_today->find(); - - $scheduled_failed_contributions = []; - - while ($scheduled_today->fetch()) { - $scheduled_failed_contributions[] = clone $scheduled_today; - } - - return $scheduled_failed_contributions; -} - -/** - * process_eway_payment - * - * Processes an eWay token payment - * - * @param object $eWayClient An eWay client set up and ready to go - * @param string $managed_customer_id The eWay token ID for the credit card you - * want to process - * @param string $amount_in_cents The amount in cents to charge the customer - * @param string $invoice_reference InvoiceReference to send to eWay - * @param string $invoice_description InvoiceDescription to send to eWay - * - * @return object eWay response object - * @throws SoapFault exceptions - */ -function process_eway_payment($eWayClient, $managed_customer_id, $amount_in_cents, $invoice_reference, $invoice_description) { - - static $prev_response = NULL; - - $paymentTransaction = [ - 'Customer' => [ - 'TokenCustomerID' => substr($managed_customer_id,0,16) - ], - 'Payment' => [ - 'TotalAmount' => substr($amount_in_cents,0,10), - 'InvoiceDescription' => substr(trim($invoice_description), 0, 64), - 'InvoiceReference' => substr($invoice_reference,0,64), - ], - 'TransactionType' => \Eway\Rapid\Enum\TransactionType::MOTO - ]; - $eWayResponse = $eWayClient->createTransaction(\Eway\Rapid\Enum\ApiMethod::DIRECT, $paymentTransaction); - - if (isset($prev_response) && $prev_response->getAttribute('TransactionID') == $eWayResponse->getAttribute('TransactionID')) { - throw new Exception ( - 'eWay ProcessPayment returned duplicate transaction number: ' . - $prev_response->getAttribute('TransactionID') . ' vs ' . $eWayResponse->getAttribute('TransactionID') - ); - } - - $prev_response = &$eWayResponse; - - return $eWayResponse; -} - -/** - * send_receipt_email - * - * Sends a receipt for a contribution - * - * @param string $contribution_id The ID of the contribution to mark as complete - * @param CRM_Core_Payment $paymentObject CRM_Core_Payment object - * - * @return bool Success or failure - */ -function send_receipt_email($contribution_id, $paymentObject) { - $contribution = new CRM_Contribute_BAO_Contribution(); - $contribution->id = $contribution_id; - $contribution->find(TRUE); - - $is_email_receipt = civicrm_api3('ContributionPage', 'getvalue', [ - 'id' => $contribution->contribution_page_id, - 'return' => 'is_email_receipt', - ]); - - if (!$is_email_receipt) { - return NULL; - } - - list($name, $email) = CRM_Contact_BAO_Contact_Location::getEmailDetails($contribution->contact_id); - - $domainValues = CRM_Core_BAO_Domain::getNameAndEmail(); - $receiptFrom = "$domainValues[0] <$domainValues[1]>"; - $receiptFromEmail = $domainValues[1]; - - $params = [ - 'groupName' => 'msg_tpl_workflow_contribution', - 'valueName' => 'contribution_online_receipt', - 'contactId' => $contribution->contact_id, - 'tplParams' => [ - 'contributeMode' => 'directIPN', - // Tells the person to contact us for cancellations - 'receiptFromEmail' => $receiptFromEmail, - 'amount' => $contribution->total_amount, - 'title' => RECEIPT_SUBJECT_TITLE, - 'is_recur' => TRUE, - 'is_monetary' => TRUE, - 'is_pay_later' => FALSE, - 'billingName' => $name, - 'email' => $email, - 'trxn_id' => $contribution->trxn_id, - 'receive_date' => CRM_Utils_Date::format($contribution->receive_date), - 'updateSubscriptionBillingUrl' => $paymentObject->subscriptionURL($contribution_id, 'contribution', 'billing'), - ], - 'from' => $receiptFrom, - 'toName' => $name, - 'toEmail' => $email, - 'isTest' => $contribution->is_test, - ]; - - list($sent, $subject, $message, $html) = CRM_Core_BAO_MessageTemplate::sendTemplate($params); - - return $sent; -} diff --git a/info.xml b/info.xml index 5addec3..62ae8ed 100644 --- a/info.xml +++ b/info.xml @@ -15,8 +15,8 @@ support@agileware.com.au stable - 2023-05-19 - 2.5.0 + 2023-07-21 + 2.6.0 5.38 diff --git a/js/eway.js b/js/eway.js index 6d2a973..075f6bb 100644 --- a/js/eway.js +++ b/js/eway.js @@ -3,17 +3,20 @@ CRM.eway.paymentTokens = []; CRM.eway.updatedToken = 0; CRM.eway.selectedToken = 0; -CRM.eway.setPaymentTokenOptions = function () { - CRM.api4('PaymentToken', 'get', { - where: [ - [ 'contact_id', '=', CRM.eway.contact_id ], - [ 'expiry_date' , '>', 'now' ], - ], - orderBy: { expiry_date: 'DESC' } - }).then( - CRM.eway.updateOptions, - console.error - ); +CRM.eway.setPaymentTokenOptions = async function () { + try { + const options = await CRM.api4('PaymentToken', 'get', { + where: [ + ['contact_id', '=', CRM.eway.contact_id], + ['expiry_date', '>', 'now'], + ], + orderBy: {expiry_date: 'DESC'} + }); + + CRM.eway.updateOptions(options); + } catch (e) { + console.error(e); + } }; /**