Merge pull request #4578 from pradpnayak/CRM-10206
[civicrm-core.git] / CRM / Core / Payment / BaseIPN.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
06b69b18 4 | CiviCRM version 4.5 |
6a488035 5 +--------------------------------------------------------------------+
06b69b18 6 | Copyright CiviCRM LLC (c) 2004-2014 |
6a488035
TO
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13 | |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
26*/
27
28/**
29 *
30 * @package CRM
06b69b18 31 * @copyright CiviCRM LLC (c) 2004-2014
6a488035
TO
32 * $Id$
33 *
34 */
35class CRM_Core_Payment_BaseIPN {
36
37 static $_now = NULL;
8196c759 38
c8aa607b 39 /**
40 * Input parameters from payment processor. Store these so that
41 * the code does not need to keep retrieving from the http request
42 * @var array
43 */
44 protected $_inputParameters = array();
45
8196c759 46 /**
47 * Constructor
48 */
6a488035
TO
49 function __construct() {
50 self::$_now = date('YmdHis');
51 }
52
c8aa607b 53 /**
54 * Store input array on the class
77b97be7 55 *
c8aa607b 56 * @param array $parameters
77b97be7
EM
57 *
58 * @throws CRM_Core_Exception
c8aa607b 59 */
60 function setInputParameters($parameters) {
61 if(!is_array($parameters)) {
cc0c30cc 62 throw new CRM_Core_Exception('Invalid input parameters');
c8aa607b 63 }
64 $this->_inputParameters = $parameters;
65 }
8196c759 66 /**
67 * Validate incoming data. This function is intended to ensure that incoming data matches
68 * It provides a form of pseudo-authentication - by checking the calling fn already knows
69 * the correct contact id & contribution id (this can be problematic when that has changed in
70 * the meantime for transactions that are delayed & contacts are merged in-between. e.g
71 * Paypal allows you to resend Instant Payment Notifications if you, for example, moved site
72 * and didn't update your IPN URL.
73 *
74 * @param array $input interpreted values from the values returned through the IPN
75 * @param array $ids more interpreted values (ids) from the values returned through the IPN
76 * @param array $objects an empty array that will be populated with loaded object
77 * @param boolean $required boolean Return FALSE if the relevant objects don't exist
78 * @param integer $paymentProcessorID Id of the payment processor ID in use
79 * @return boolean
80 */
6a488035
TO
81 function validateData(&$input, &$ids, &$objects, $required = TRUE, $paymentProcessorID = NULL) {
82
83 // make sure contact exists and is valid
5a9c68ac 84 $contact = new CRM_Contact_BAO_Contact();
6a488035
TO
85 $contact->id = $ids['contact'];
86 if (!$contact->find(TRUE)) {
87 CRM_Core_Error::debug_log_message("Could not find contact record: {$ids['contact']} in IPN request: ".print_r($input, TRUE));
88 echo "Failure: Could not find contact record: {$ids['contact']}<p>";
89 return FALSE;
90 }
91
92 // make sure contribution exists and is valid
5a9c68ac 93 $contribution = new CRM_Contribute_BAO_Contribution();
6a488035
TO
94 $contribution->id = $ids['contribution'];
95 if (!$contribution->find(TRUE)) {
96 CRM_Core_Error::debug_log_message("Could not find contribution record: {$contribution->id} in IPN request: ".print_r($input, TRUE));
97 echo "Failure: Could not find contribution record for {$contribution->id}<p>";
98 return FALSE;
99 }
100 $contribution->receive_date = CRM_Utils_Date::isoToMysql($contribution->receive_date);
101
102 $objects['contact'] = &$contact;
103 $objects['contribution'] = &$contribution;
104 if (!$this->loadObjects($input, $ids, $objects, $required, $paymentProcessorID)) {
105 return FALSE;
106 }
ac891698
DG
107 //the process is that the loadObjects is kind of hacked by loading the objects for the original contribution and then somewhat inconsistently using them for the
108 //current contribution. Here we ensure that the original contribution is available to the complete transaction function
109 //we don't want to fix this in the payment processor classes because we would have to fix all of them - so better to fix somewhere central
110 if (isset($objects['contributionRecur'])) {
111 $objects['first_contribution'] = $objects['contribution'];
112 }
6a488035
TO
113 return TRUE;
114 }
115
8196c759 116 /**
6a488035
TO
117 * Load objects related to contribution
118 *
119 * @input array information from Payment processor
dd244018
EM
120 *
121 * @param $input
8196c759 122 * @param array $ids
123 * @param array $objects
124 * @param boolean $required
125 * @param integer $paymentProcessorID
126 * @param array $error_handling
dd244018 127 *
8196c759 128 * @return multitype:number NULL |boolean
6a488035
TO
129 */
130 function loadObjects(&$input, &$ids, &$objects, $required, $paymentProcessorID, $error_handling = NULL) {
131 if (empty($error_handling)) {
132 // default options are that we log an error & echo it out
133 // note that we should refactor this error handling into error code @ some point
134 // but for now setting up enough separation so we can do unit tests
135 $error_handling = array(
136 'log_error' => 1,
137 'echo_error' => 1,
138 );
139 }
140 $ids['paymentProcessor'] = $paymentProcessorID;
141 if (is_a($objects['contribution'], 'CRM_Contribute_BAO_Contribution')) {
142 $contribution = &$objects['contribution'];
143 }
144 else {
145 //legacy support - functions are 'used' to be able to pass in a DAO
146 $contribution = new CRM_Contribute_BAO_Contribution();
147 $contribution->id = CRM_Utils_Array::value('contribution', $ids);
148 $contribution->find(TRUE);
149 $objects['contribution'] = &$contribution;
150 }
151 try {
152 $success = $contribution->loadRelatedObjects($input, $ids, $required);
153 }
c8aa607b 154 catch(Exception $e) {
cc0c30cc 155 $success = FALSE;
a7488080 156 if (!empty($error_handling['log_error'])) {
6a488035
TO
157 CRM_Core_Error::debug_log_message($e->getMessage());
158 }
a7488080 159 if (!empty($error_handling['echo_error'])) {
6a488035
TO
160 echo ($e->getMessage());
161 }
a7488080 162 if (!empty($error_handling['return_error'])) {
6a488035
TO
163 return array(
164 'is_error' => 1,
165 'error_message' => ($e->getMessage()),
166 );
167 }
168 }
169 $objects = array_merge($objects, $contribution->_relatedObjects);
170 return $success;
171 }
172
8196c759 173 /**
174 * Set contribution to failed
175 * @param array $objects
176 * @param object $transaction
177 * @param array $input
178 * @return boolean
179 */
6a488035
TO
180 function failed(&$objects, &$transaction, $input = array()) {
181 $contribution = &$objects['contribution'];
182 $memberships = array();
a7488080 183 if (!empty($objects['membership'])) {
6a488035
TO
184 $memberships = &$objects['membership'];
185 if (is_numeric($memberships)) {
186 $memberships = array($objects['membership']);
187 }
188 }
189
190 $addLineItems = FALSE;
191 if (empty($contribution->id)) {
192 $addLineItems = TRUE;
193 }
194 $participant = &$objects['participant'];
195
196 $contributionStatus = CRM_Contribute_PseudoConstant::contributionStatus(NULL, 'name');
5a9c68ac
PJ
197 $contribution->receive_date = CRM_Utils_Date::isoToMysql($contribution->receive_date);
198 $contribution->receipt_date = CRM_Utils_Date::isoToMysql($contribution->receipt_date);
199 $contribution->thankyou_date = CRM_Utils_Date::isoToMysql($contribution->thankyou_date);
6a488035
TO
200 $contribution->contribution_status_id = array_search('Failed', $contributionStatus);
201 $contribution->save();
202
203 //add lineitems for recurring payments
a7488080 204 if (!empty($objects['contributionRecur']) && $objects['contributionRecur']->id && $addLineItems) {
bf45dbe8 205 $this->addrecurLineItems($objects['contributionRecur']->id, $contribution->id, CRM_Core_DAO::$_nullArray);
6a488035
TO
206 }
207
8381af80 208 //add new soft credit against current contribution id and
6357981e 209 //copy initial contribution custom fields for recurring contributions
a7488080 210 if (!empty($objects['contributionRecur']) && $objects['contributionRecur']->id) {
8381af80 211 $this->addrecurSoftCredit($objects['contributionRecur']->id, $contribution->id);
6357981e
PJ
212 $this->copyCustomValues($objects['contributionRecur']->id, $contribution->id);
213 }
214
a7488080 215 if (empty($input['skipComponentSync'])) {
6a488035 216 if (!empty($memberships)) {
5968aa47
C
217 // if transaction is failed then set "Cancelled" as membership status
218 $cancelStatusId = array_search('Cancelled', CRM_Member_PseudoConstant::membershipStatus());
6a488035
TO
219 foreach ($memberships as $membership) {
220 if ($membership) {
5968aa47 221 $membership->status_id = $cancelStatusId;
6a488035 222 $membership->save();
d63f4fc3 223
6a488035 224 //update related Memberships.
5968aa47 225 $params = array('status_id' => $cancelStatusId);
6a488035
TO
226 CRM_Member_BAO_Membership::updateRelatedMemberships($membership->id, $params);
227 }
228 }
229 }
d63f4fc3 230
6a488035
TO
231 if ($participant) {
232 $participant->status_id = 4;
233 $participant->save();
234 }
235 }
236
237 $transaction->commit();
238 CRM_Core_Error::debug_log_message("Setting contribution status to failed");
239 //echo "Success: Setting contribution status to failed<p>";
240 return TRUE;
241 }
242
8196c759 243 /**
244 * Handled pending contribution status
245 * @param array $objects
246 * @param object $transaction
247 * @return boolean
248 */
6a488035
TO
249 function pending(&$objects, &$transaction) {
250 $transaction->commit();
251 CRM_Core_Error::debug_log_message("returning since contribution status is pending");
252 echo "Success: Returning since contribution status is pending<p>";
253 return TRUE;
254 }
255
6c786a9b
EM
256 /**
257 * @param $objects
258 * @param $transaction
259 * @param array $input
260 *
261 * @return bool
262 */
6a488035
TO
263 function cancelled(&$objects, &$transaction, $input = array()) {
264 $contribution = &$objects['contribution'];
265 $memberships = &$objects['membership'];
266 if (is_numeric($memberships)) {
267 $memberships = array($objects['membership']);
268 }
269
270 $participant = &$objects['participant'];
271 $addLineItems = FALSE;
272 if (empty($contribution->id)) {
273 $addLineItems = TRUE;
274 }
275 $contribution->contribution_status_id = 3;
276 $contribution->cancel_date = self::$_now;
277 $contribution->cancel_reason = CRM_Utils_Array::value('reasonCode', $input);
278 $contribution->receive_date = CRM_Utils_Date::isoToMysql($contribution->receive_date);
279 $contribution->receipt_date = CRM_Utils_Date::isoToMysql($contribution->receipt_date);
280 $contribution->thankyou_date = CRM_Utils_Date::isoToMysql($contribution->thankyou_date);
281 $contribution->save();
282
283 //add lineitems for recurring payments
a7488080 284 if (!empty($objects['contributionRecur']) && $objects['contributionRecur']->id && $addLineItems) {
bf45dbe8 285 $this->addrecurLineItems($objects['contributionRecur']->id, $contribution->id, CRM_Core_DAO::$_nullArray);
6a488035
TO
286 }
287
8381af80 288 //add new soft credit against current $contribution and
6357981e 289 //copy initial contribution custom fields for recurring contributions
a7488080 290 if (!empty($objects['contributionRecur']) && $objects['contributionRecur']->id) {
8381af80 291 $this->addrecurSoftCredit($objects['contributionRecur']->id, $contribution->id);
6357981e
PJ
292 $this->copyCustomValues($objects['contributionRecur']->id, $contribution->id);
293 }
294
a7488080 295 if (empty($input['skipComponentSync'])) {
6a488035
TO
296 if (!empty($memberships)) {
297 foreach ($memberships as $membership) {
298 if ($membership) {
299 $membership->status_id = 6;
300 $membership->save();
d63f4fc3 301
6a488035
TO
302 //update related Memberships.
303 $params = array('status_id' => 6);
304 CRM_Member_BAO_Membership::updateRelatedMemberships($membership->id, $params);
305 }
306 }
307 }
d63f4fc3 308
6a488035
TO
309 if ($participant) {
310 $participant->status_id = 4;
311 $participant->save();
312 }
313 }
314 $transaction->commit();
315 CRM_Core_Error::debug_log_message("Setting contribution status to cancelled");
316 //echo "Success: Setting contribution status to cancelled<p>";
317 return TRUE;
318 }
319
6c786a9b
EM
320 /**
321 * @param $objects
322 * @param $transaction
323 *
324 * @return bool
325 */
6a488035
TO
326 function unhandled(&$objects, &$transaction) {
327 $transaction->rollback();
328 // we dont handle this as yet
329 CRM_Core_Error::debug_log_message("returning since contribution status: $status is not handled");
330 echo "Failure: contribution status $status is not handled<p>";
331 return FALSE;
332 }
333
6c786a9b
EM
334 /**
335 * @param $input
336 * @param $ids
337 * @param $objects
338 * @param $transaction
339 * @param bool $recur
340 */
6a488035
TO
341 function completeTransaction(&$input, &$ids, &$objects, &$transaction, $recur = FALSE) {
342 $contribution = &$objects['contribution'];
ac891698
DG
343
344 $primaryContributionID = isset($contribution->id) ? $contribution->id : $objects['first_contribution']->id;
345
6a488035
TO
346 $memberships = &$objects['membership'];
347 if (is_numeric($memberships)) {
348 $memberships = array($objects['membership']);
349 }
350 $participant = &$objects['participant'];
351 $event = &$objects['event'];
352 $changeToday = CRM_Utils_Array::value('trxn_date', $input, self::$_now);
353 $recurContrib = &$objects['contributionRecur'];
354
355 $values = array();
356 $source = NULL;
357 if ($input['component'] == 'contribute') {
358 if ($contribution->contribution_page_id) {
359 CRM_Contribute_BAO_ContributionPage::setValues($contribution->contribution_page_id, $values);
360 $source = ts('Online Contribution') . ': ' . $values['title'];
361 }
362 elseif ($recurContrib && $recurContrib->id) {
363 $contribution->contribution_page_id = NULL;
364 $values['amount'] = $recurContrib->amount;
365 $values['financial_type_id'] = $objects['contributionType']->id;
366 $values['title'] = $source = ts('Offline Recurring Contribution');
6a488035
TO
367 $domainValues = CRM_Core_BAO_Domain::getNameAndEmail();
368 $values['receipt_from_name'] = $domainValues[0];
369 $values['receipt_from_email'] = $domainValues[1];
370 }
ef3a8cf0 371 if($recurContrib && $recurContrib->id){
2b5b0279 372 //CRM-13273 - is_email_receipt setting on recurring contribution should take precedence over contribution page setting
373 $values['is_email_receipt'] = $recurContrib->is_email_receipt;
374 }
6a488035
TO
375
376 $contribution->source = $source;
a7488080 377 if (!empty($values['is_email_receipt'])) {
6a488035
TO
378 $contribution->receipt_date = self::$_now;
379 }
380
381 if (!empty($memberships)) {
382 $membershipsUpdate = array( );
383 foreach ($memberships as $membershipTypeIdKey => $membership) {
384 if ($membership) {
385 $format = '%Y%m%d';
386
387 $currentMembership = CRM_Member_BAO_Membership::getContactMembership($membership->contact_id,
388 $membership->membership_type_id,
389 $membership->is_test, $membership->id
390 );
391
392 // CRM-8141 update the membership type with the value recorded in log when membership created/renewed
393 // this picks up membership type changes during renewals
394 $sql = "
395SELECT membership_type_id
396FROM civicrm_membership_log
397WHERE membership_id=$membership->id
398ORDER BY id DESC
399LIMIT 1;";
400 $dao = new CRM_Core_DAO;
401 $dao->query($sql);
402 if ($dao->fetch()) {
403 if (!empty($dao->membership_type_id)) {
404 $membership->membership_type_id = $dao->membership_type_id;
405 $membership->save();
406 }
407 // else fall back to using current membership type
408 }
409 // else fall back to using current membership type
410 $dao->free();
411
ac891698 412 $num_terms = $contribution->getNumTermsByContributionAndMembershipType($membership->membership_type_id, $primaryContributionID);
6a488035
TO
413 if ($currentMembership) {
414 /*
415 * Fixed FOR CRM-4433
416 * In BAO/Membership.php(renewMembership function), we skip the extend membership date and status
417 * when Contribution mode is notify and membership is for renewal )
418 */
419 CRM_Member_BAO_Membership::fixMembershipStatusBeforeRenew($currentMembership, $changeToday);
420
2243fe93
EM
421 // @todo - we should pass membership_type_id instead of null here but not
422 // adding as not sure of testing
6a488035 423 $dates = CRM_Member_BAO_MembershipType::getRenewalDatesForMembershipType($membership->id,
2243fe93 424 $changeToday, NULL, $num_terms
6a488035 425 );
2243fe93 426
6a488035
TO
427 $dates['join_date'] = CRM_Utils_Date::customFormat($currentMembership['join_date'], $format);
428 }
429 else {
29347f3d 430 $dates = CRM_Member_BAO_MembershipType::getDatesForMembershipType($membership->membership_type_id, NULL, NULL, NULL, $num_terms);
6a488035
TO
431 }
432
433 //get the status for membership.
434 $calcStatus = CRM_Member_BAO_MembershipStatus::getMembershipStatusByDate($dates['start_date'],
435 $dates['end_date'],
436 $dates['join_date'],
437 'today',
5f11bbcc
EM
438 TRUE,
439 $membership->membership_type_id,
440 (array) $membership
6a488035
TO
441 );
442
443 $formatedParams = array('status_id' => CRM_Utils_Array::value('id', $calcStatus, 2),
444 'join_date' => CRM_Utils_Date::customFormat(CRM_Utils_Array::value('join_date', $dates), $format),
445 'start_date' => CRM_Utils_Date::customFormat(CRM_Utils_Array::value('start_date', $dates), $format),
446 'end_date' => CRM_Utils_Date::customFormat(CRM_Utils_Array::value('end_date', $dates), $format),
447 );
448 //we might be renewing membership,
449 //so make status override false.
450 $formatedParams['is_override'] = FALSE;
451 $membership->copyValues($formatedParams);
452 $membership->save();
453
454 //updating the membership log
455 $membershipLog = array();
456 $membershipLog = $formatedParams;
457
458 $logStartDate = $formatedParams['start_date'];
a7488080 459 if (!empty($dates['log_start_date'])) {
6a488035
TO
460 $logStartDate = CRM_Utils_Date::customFormat($dates['log_start_date'], $format);
461 $logStartDate = CRM_Utils_Date::isoToMysql($logStartDate);
462 }
463
464 $membershipLog['start_date'] = $logStartDate;
465 $membershipLog['membership_id'] = $membership->id;
466 $membershipLog['modified_id'] = $membership->contact_id;
467 $membershipLog['modified_date'] = date('Ymd');
468 $membershipLog['membership_type_id'] = $membership->membership_type_id;
469
470 CRM_Member_BAO_MembershipLog::add($membershipLog, CRM_Core_DAO::$_nullArray);
471
472 //update related Memberships.
473 CRM_Member_BAO_Membership::updateRelatedMemberships($membership->id, $formatedParams);
d63f4fc3 474
6a488035
TO
475 //update the membership type key of membership relatedObjects array
476 //if it has changed after membership update
477 if ($membershipTypeIdKey != $membership->membership_type_id) {
478 $membershipsUpdate[$membership->membership_type_id] = $membership;
479 $contribution->_relatedObjects['membership'][$membership->membership_type_id] = $membership;
480 unset($contribution->_relatedObjects['membership'][$membershipTypeIdKey]);
481 unset($memberships[$membershipTypeIdKey]);
482 }
483 }
484 }
485 //update the memberships object with updated membershipTypeId data
486 //if membershipTypeId has changed after membership update
487 if (!empty($membershipsUpdate)) {
488 $memberships = $memberships + $membershipsUpdate;
489 }
490 }
491 }
492 else {
493 // event
494 $eventParams = array('id' => $objects['event']->id);
495 $values['event'] = array();
496
497 CRM_Event_BAO_Event::retrieve($eventParams, $values['event']);
498
499 //get location details
500 $locationParams = array('entity_id' => $objects['event']->id, 'entity_table' => 'civicrm_event');
501 $values['location'] = CRM_Core_BAO_Location::getValues($locationParams);
502
503 $ufJoinParams = array(
504 'entity_table' => 'civicrm_event',
505 'entity_id' => $ids['event'],
506 'module' => 'CiviEvent',
507 );
d63f4fc3 508
6a488035
TO
509 list($custom_pre_id,
510 $custom_post_ids
511 ) = CRM_Core_BAO_UFJoin::getUFGroupIds($ufJoinParams);
d63f4fc3 512
6a488035
TO
513 $values['custom_pre_id'] = $custom_pre_id;
514 $values['custom_post_id'] = $custom_post_ids;
e1eb31b5
RN
515 //for tasks 'Change Participant Status' and 'Batch Update Participants Via Profile' case
516 //and cases involving status updation through ipn
517 $values['totalAmount'] = $input['amount'];
6a488035
TO
518
519 $contribution->source = ts('Online Event Registration') . ': ' . $values['event']['title'];
520
521 if ($values['event']['is_email_confirm']) {
522 $contribution->receipt_date = self::$_now;
523 $values['is_email_receipt'] = 1;
524 }
a7488080 525 if (empty($input['skipComponentSync'])) {
6a488035
TO
526 $participant->status_id = 1;
527 }
528 $participant->save();
529 }
530
531 if (CRM_Utils_Array::value('net_amount', $input, 0) == 0 &&
532 CRM_Utils_Array::value('fee_amount', $input, 0) != 0
533 ) {
534 $input['net_amount'] = $input['amount'] - $input['fee_amount'];
535 }
536 $addLineItems = FALSE;
537 if (empty($contribution->id)) {
538 $addLineItems = TRUE;
539 }
d63f4fc3 540
6a488035
TO
541 $contribution->contribution_status_id = 1;
542 $contribution->is_test = $input['is_test'];
543 $contribution->fee_amount = CRM_Utils_Array::value('fee_amount', $input, 0);
544 $contribution->net_amount = CRM_Utils_Array::value('net_amount', $input, 0);
545 $contribution->trxn_id = $input['trxn_id'];
546 $contribution->receive_date = CRM_Utils_Date::isoToMysql($contribution->receive_date);
547 $contribution->thankyou_date = CRM_Utils_Date::isoToMysql($contribution->thankyou_date);
46fa5206 548 $contribution->receipt_date = CRM_Utils_Date::isoToMysql($contribution->receipt_date);
6a488035
TO
549 $contribution->cancel_date = 'null';
550
a7488080 551 if (!empty($input['check_number'])) {
6a488035
TO
552 $contribution->check_number = $input['check_number'];
553 }
554
a7488080 555 if (!empty($input['payment_instrument_id'])) {
6a488035
TO
556 $contribution->payment_instrument_id = $input['payment_instrument_id'];
557 }
d63f4fc3 558
6a488035
TO
559 if ($contribution->id) {
560 $contributionId['id'] = $contribution->id;
561 $input['prevContribution'] = CRM_Contribute_BAO_Contribution::getValues($contributionId, CRM_Core_DAO::$_nullArray, CRM_Core_DAO::$_nullArray);
562 }
563 $contribution->save();
564
8381af80 565 //add new soft credit against current $contribution and
566 if (CRM_Utils_Array::value('contributionRecur', $objects) && $objects['contributionRecur']->id) {
567 $this->addrecurSoftCredit($objects['contributionRecur']->id, $contribution->id);
568 }
569
6a488035 570 //add lineitems for recurring payments
a7488080 571 if (!empty($objects['contributionRecur']) && $objects['contributionRecur']->id && $addLineItems) {
bf45dbe8 572 $this->addrecurLineItems($objects['contributionRecur']->id, $contribution->id, $input);
6a488035
TO
573 }
574
6357981e
PJ
575 //copy initial contribution custom fields for recurring contributions
576 if ($recurContrib && $recurContrib->id) {
577 $this->copyCustomValues($recurContrib->id, $contribution->id);
578 }
579
6a488035
TO
580 // next create the transaction record
581 $paymentProcessor = $paymentProcessorId = '';
582 if (isset($objects['paymentProcessor'])) {
583 if (is_array($objects['paymentProcessor'])) {
584 $paymentProcessor = $objects['paymentProcessor']['payment_processor_type'];
585 $paymentProcessorId = $objects['paymentProcessor']['id'];
586 }
587 else {
588 $paymentProcessor = $objects['paymentProcessor']->payment_processor_type;
589 $paymentProcessorId = $objects['paymentProcessor']->id;
590 }
591 }
4d34aefa 592 //it's hard to see how it could reach this point without a contributon id as it is saved in line 511 above
593 // which raised the question as to whether this check preceded line 511 & if so whether something could be broken
594 // From a lot of code reading /debugging I'm still not sure the intent WRT first & subsequent payments in this code
595 // it would be good if someone added some comments or refactored this
6a488035
TO
596 if ($contribution->id) {
597 $contributionStatuses = CRM_Contribute_PseudoConstant::contributionStatus(NULL, 'name');
cc757ab9 598 if ((empty($input['prevContribution']) && $paymentProcessorId) || (!$input['prevContribution']->is_pay_later &&
599- $input['prevContribution']->contribution_status_id == array_search('Pending', $contributionStatuses))) {
600 $input['payment_processor'] = $paymentProcessorId;
6a488035 601 }
78b79549 602 $input['contribution_status_id'] = array_search('Completed', $contributionStatuses);
6a488035
TO
603 $input['total_amount'] = $input['amount'];
604 $input['contribution'] = $contribution;
a1f5ffcc 605 $input['financial_type_id'] = $contribution->financial_type_id;
606
a7488080 607 if (!empty($contribution->_relatedObjects['participant'])) {
6a488035
TO
608 $input['contribution_mode'] = 'participant';
609 $input['participant_id'] = $contribution->_relatedObjects['participant']->id;
0ba0addf 610 $input['skipLineItem'] = 1;
f92fd420 611 }
d2035566
PN
612 elseif (!empty($contribution->_relatedObjects['membership'])) {
613 $input['skipLineItem'] = TRUE;
614 $input['contribution_mode'] = 'membership';
615 }
4d34aefa 616 //@todo writing a unit test I was unable to create a scenario where this line did not fatal on second
617 // and subsequent payments. In this case the line items are created at $this->addrecurLineItems
618 // and since the contribution is saved prior to this line there is always a contribution-id,
619 // however there is never a prevContribution (which appears to mean original contribution not previous
620 // contribution - or preUpdateContributionObject most accurately)
621 // so, this is always called & only appears to succeed when prevContribution exists - which appears
622 // to mean "are we updating an exisitng pending contribution"
623 //I was able to make the unit test complete as fataling here doesn't prevent
624 // the contribution being created - but activities would not be created or emails sent
6a488035
TO
625 CRM_Contribute_BAO_Contribution::recordFinancialAccounts($input, NULL);
626 }
627
628 self::updateRecurLinkedPledge($contribution);
629
630 // create an activity record
631 if ($input['component'] == 'contribute') {
632 //CRM-4027
633 $targetContactID = NULL;
a7488080 634 if (!empty($ids['related_contact'])) {
6a488035
TO
635 $targetContactID = $contribution->contact_id;
636 $contribution->contact_id = $ids['related_contact'];
637 }
638 CRM_Activity_BAO_Activity::addActivity($contribution, NULL, $targetContactID);
639 // event
640 }
641 else {
642 CRM_Activity_BAO_Activity::addActivity($participant);
643 }
644
645 CRM_Core_Error::debug_log_message("Contribution record updated successfully");
646 $transaction->commit();
647
648 // CRM-9132 legacy behaviour was that receipts were sent out in all instances. Still sending
649 // when array_key 'is_email_receipt doesn't exist in case some instances where is needs setting haven't been set
650 if (!array_key_exists('is_email_receipt', $values) ||
651 $values['is_email_receipt'] == 1
652 ) {
653 self::sendMail($input, $ids, $objects, $values, $recur, FALSE);
bf014492 654 CRM_Core_Error::debug_log_message("Receipt sent");
6a488035
TO
655 }
656
bf014492 657 CRM_Core_Error::debug_log_message("Success: Database updated");
6a488035
TO
658 }
659
6c786a9b
EM
660 /**
661 * @param $ids
662 *
663 * @return bool
664 */
6a488035
TO
665 function getBillingID(&$ids) {
666 // get the billing location type
180409a4 667 $locationTypes = CRM_Core_PseudoConstant::get('CRM_Core_DAO_Address', 'location_type_id', array(), 'validate');
6a488035
TO
668 // CRM-8108 remove the ts around the Billing locationtype
669 //$ids['billing'] = array_search( ts('Billing'), $locationTypes );
670 $ids['billing'] = array_search('Billing', $locationTypes);
671 if (!$ids['billing']) {
672 CRM_Core_Error::debug_log_message(ts('Please set a location type of %1', array(1 => 'Billing')));
673 echo "Failure: Could not find billing location type<p>";
674 return FALSE;
675 }
676 return TRUE;
677 }
678
679 /*
680 * Send receipt from contribution. Note that the compose message part has been moved to contribution
681 * In general LoadObjects is called first to get the objects but the composeMessageArray function now calls it
682 *
683 * @params array $input Incoming data from Payment processor
684 * @params array $ids Related object IDs
685 * @params array $values values related to objects that have already been loaded
686 * @params bool $recur is it part of a recurring contribution
687 * @params bool $returnMessageText Should text be returned instead of sent. This
688 * is because the function is also used to generate pdfs
689 */
6c786a9b
EM
690 /**
691 * @param $input
692 * @param $ids
693 * @param $objects
694 * @param $values
695 * @param bool $recur
696 * @param bool $returnMessageText
697 *
698 * @return mixed
699 */
6a488035
TO
700 function sendMail(&$input, &$ids, &$objects, &$values, $recur = FALSE, $returnMessageText = FALSE) {
701 $contribution = &$objects['contribution'];
702 $input['is_recur'] = $recur;
703 // set receipt from e-mail and name in value
704 if (!$returnMessageText) {
705 $session = CRM_Core_Session::singleton();
706 $userID = $session->get('userID');
707 if (!empty($userID)) {
708 list($userName, $userEmail) = CRM_Contact_BAO_Contact_Location::getEmailDetails($userID);
709 $values['receipt_from_email'] = $userEmail;
710 $values['receipt_from_name'] = $userName;
711 }
712 }
713 return $contribution->composeMessageArray($input, $ids, $values, $recur, $returnMessageText);
714 }
715
8196c759 716 /**
717 * Update contribution status - this is only called from one place in the code &
718 * it is unclear whether it is a function on the way in or on the way out
719 *
720 * @param unknown_type $params
721 * @return void|Ambigous <value, unknown, array>
722 */
6a488035
TO
723 function updateContributionStatus(&$params) {
724 // get minimum required values.
725 $statusId = CRM_Utils_Array::value('contribution_status_id', $params);
726 $componentId = CRM_Utils_Array::value('component_id', $params);
727 $componentName = CRM_Utils_Array::value('componentName', $params);
728 $contributionId = CRM_Utils_Array::value('contribution_id', $params);
729
730 if (!$contributionId || !$componentId || !$componentName || !$statusId) {
731 return;
732 }
733
734 $input = $ids = $objects = array();
735
736 //get the required ids.
737 $ids['contribution'] = $contributionId;
738
739 if (!$ids['contact'] = CRM_Utils_Array::value('contact_id', $params)) {
740 $ids['contact'] = CRM_Core_DAO::getFieldValue('CRM_Contribute_DAO_Contribution',
741 $contributionId,
742 'contact_id'
743 );
744 }
745
746 if ($componentName == 'Event') {
747 $name = 'event';
748 $ids['participant'] = $componentId;
749
750 if (!$ids['event'] = CRM_Utils_Array::value('event_id', $params)) {
751 $ids['event'] = CRM_Core_DAO::getFieldValue('CRM_Event_DAO_Participant',
752 $componentId,
753 'event_id'
754 );
755 }
756 }
757
758 if ($componentName == 'Membership') {
759 $name = 'contribute';
760 $ids['membership'] = $componentId;
761 }
762 $ids['contributionPage'] = NULL;
763 $ids['contributionRecur'] = NULL;
764 $input['component'] = $name;
765
766 $baseIPN = new CRM_Core_Payment_BaseIPN();
767 $transaction = new CRM_Core_Transaction();
768
769 // reset template values.
770 $template = CRM_Core_Smarty::singleton();
771 $template->clearTemplateVars();
772
773 if (!$baseIPN->validateData($input, $ids, $objects, FALSE)) {
774 CRM_Core_Error::fatal();
775 }
776
777 $contribution = &$objects['contribution'];
778
779 $contributionStatuses = CRM_Contribute_PseudoConstant::contributionStatus(NULL, 'name');
780 $input['skipComponentSync'] = CRM_Utils_Array::value('skipComponentSync', $params);
781 if ($statusId == array_search('Cancelled', $contributionStatuses)) {
782 $baseIPN->cancelled($objects, $transaction, $input);
783 $transaction->commit();
784 return $statusId;
785 }
786 elseif ($statusId == array_search('Failed', $contributionStatuses)) {
787 $baseIPN->failed($objects, $transaction, $input);
788 $transaction->commit();
789 return $statusId;
790 }
791
792 // status is not pending
793 if ($contribution->contribution_status_id != array_search('Pending', $contributionStatuses)) {
794 $transaction->commit();
795 return;
796 }
797
798 //set values for ipn code.
799 foreach (array(
800 'fee_amount', 'check_number', 'payment_instrument_id') as $field) {
801 if (!$input[$field] = CRM_Utils_Array::value($field, $params)) {
802 $input[$field] = $contribution->$field;
803 }
804 }
805 if (!$input['trxn_id'] = CRM_Utils_Array::value('trxn_id', $params)) {
806 $input['trxn_id'] = $contribution->invoice_id;
807 }
808 if (!$input['amount'] = CRM_Utils_Array::value('total_amount', $params)) {
809 $input['amount'] = $contribution->total_amount;
810 }
811 $input['is_test'] = $contribution->is_test;
812 $input['net_amount'] = $contribution->net_amount;
8cc574cf 813 if (!empty($input['fee_amount']) && !empty($input['amount'])) {
6a488035
TO
814 $input['net_amount'] = $input['amount'] - $input['fee_amount'];
815 }
816
817 //complete the contribution.
818 $baseIPN->completeTransaction($input, $ids, $objects, $transaction, FALSE);
819
820 // reset template values before processing next transactions
821 $template->clearTemplateVars();
822
823 return $statusId;
824 }
825
826 /*
827 * Update pledge associated with a recurring contribution
828 *
829 * If the contribution has a pledge_payment record pledge, then update the pledge_payment record & pledge based on that linkage.
830 *
831 * If a previous contribution in the recurring contribution sequence is linked with a pledge then we assume this contribution
832 * should be linked with the same pledge also. Currently only back-office users can apply a recurring payment to a pledge &
833 * it should be assumed they
834 * do so with the intention that all payments will be linked
835 *
836 * The pledge payment record should already exist & will need to be updated with the new contribution ID.
837 * If not the contribution will also need to be linked to the pledge
838 */
6c786a9b
EM
839 /**
840 * @param $contribution
841 */
6a488035
TO
842 function updateRecurLinkedPledge(&$contribution) {
843 $returnProperties = array('id', 'pledge_id');
844 $paymentDetails = $paymentIDs = array();
845
846 if (CRM_Core_DAO::commonRetrieveAll('CRM_Pledge_DAO_PledgePayment', 'contribution_id', $contribution->id,
847 $paymentDetails, $returnProperties
848 )) {
849 foreach ($paymentDetails as $key => $value) {
850 $paymentIDs[] = $value['id'];
851 $pledgeId = $value['pledge_id'];
852 }
853 }
854 else {
855 //payment is not already linked - if it is linked with a pledge we need to create a link.
856 // return if it is not recurring contribution
857 if (!$contribution->contribution_recur_id) {
858 return;
859 }
860
861 $relatedContributions = new CRM_Contribute_DAO_Contribution();
862 $relatedContributions->contribution_recur_id = $contribution->contribution_recur_id;
863 $relatedContributions->find();
864
865 while ($relatedContributions->fetch()) {
866 CRM_Core_DAO::commonRetrieveAll('CRM_Pledge_DAO_PledgePayment', 'contribution_id', $relatedContributions->id,
867 $paymentDetails, $returnProperties
868 );
869 }
870
871 if (empty($paymentDetails)) {
872 // payment is not linked with a pledge and neither are any other contributions on this
873 return;
874 }
875
876 foreach ($paymentDetails as $key => $value) {
877 $pledgeId = $value['pledge_id'];
878 }
879
880 // we have a pledge now we need to get the oldest unpaid payment
881 $paymentDetails = CRM_Pledge_BAO_PledgePayment::getOldestPledgePayment($pledgeId);
e3f3156b 882 if(empty($paymentDetails['id'])){
883 // we can assume this pledge is now completed
884 // return now so we don't create a core error & roll back
885 return;
886 }
6a488035
TO
887 $paymentDetails['contribution_id'] = $contribution->id;
888 $paymentDetails['status_id'] = $contribution->contribution_status_id;
889 $paymentDetails['actual_amount'] = $contribution->total_amount;
890
891 // put contribution against it
892 $payment = CRM_Pledge_BAO_PledgePayment::add($paymentDetails);
893 $paymentIDs[] = $payment->id;
894 }
895
896 // update pledge and corresponding payment statuses
897 CRM_Pledge_BAO_PledgePayment::updatePledgePaymentStatus($pledgeId, $paymentIDs, $contribution->contribution_status_id,
898 NULL, $contribution->total_amount
899 );
900 }
901
6c786a9b
EM
902 /**
903 * @param $recurId
904 * @param $contributionId
905 * @param $input
906 */
bf45dbe8 907 function addrecurLineItems($recurId, $contributionId, &$input) {
6a488035
TO
908 $lineSets = $lineItems = array();
909
910 //Get the first contribution id with recur id
911 if ($recurId) {
912 $contriID = CRM_Core_DAO::getFieldValue('CRM_Contribute_DAO_Contribution', $recurId, 'id', 'contribution_recur_id');
913 $lineItems = CRM_Price_BAO_LineItem::getLineItems($contriID, 'contribution');
914 if (!empty($lineItems)) {
915 foreach ($lineItems as $key => $value) {
9da8dc8c 916 $pricesetID = new CRM_Price_DAO_PriceField();
6a488035
TO
917 $pricesetID->id = $value['price_field_id'];
918 $pricesetID->find(TRUE);
919 $lineSets[$pricesetID->price_set_id][] = $value;
920 }
921 }
bf45dbe8
PN
922 if (!empty($input)) {
923 $input['line_item'] = $lineSets;
924 }
925 else {
926 CRM_Price_BAO_LineItem::processPriceSet($contributionId, $lineSets);
927 }
6a488035
TO
928 }
929 }
6a488035 930
6357981e
PJ
931 // function to copy custom data of the
932 // initial contribution into its recurring contributions
6c786a9b
EM
933 /**
934 * @param $recurId
935 * @param $targetContributionId
936 */
6357981e
PJ
937 function copyCustomValues($recurId, $targetContributionId) {
938 if ($recurId && $targetContributionId) {
939 // get the initial contribution id of recur id
940 $sourceContributionId = CRM_Core_DAO::getFieldValue('CRM_Contribute_DAO_Contribution', $recurId, 'id', 'contribution_recur_id');
941
942 // if the same contribution is being proccessed then return
943 if ($sourceContributionId == $targetContributionId) {
944 return;
945 }
946 // check if proper recurring contribution record is being processed
947 $targetConRecurId = CRM_Core_DAO::getFieldValue('CRM_Contribute_DAO_Contribution', $targetContributionId, 'contribution_recur_id');
948 if ($targetConRecurId != $recurId) {
949 return;
950 }
951
952 // copy custom data
953 $extends = array('Contribution');
954 $groupTree = CRM_Core_BAO_CustomGroup::getGroupDetail(NULL, NULL, $extends);
955 if ($groupTree) {
956 foreach ($groupTree as $groupID => $group) {
957 $table[$groupTree[$groupID]['table_name']] = array('entity_id');
958 foreach ($group['fields'] as $fieldID => $field) {
959 $table[$groupTree[$groupID]['table_name']][] = $groupTree[$groupID]['fields'][$fieldID]['column_name'];
960 }
961 }
962
963 foreach ($table as $tableName => $tableColumns) {
964 $insert = 'INSERT INTO ' . $tableName . ' (' . implode(', ', $tableColumns) . ') ';
965 $tableColumns[0] = $targetContributionId;
966 $select = 'SELECT ' . implode(', ', $tableColumns);
967 $from = ' FROM ' . $tableName;
968 $where = " WHERE {$tableName}.entity_id = {$sourceContributionId}";
969 $query = $insert . $select . $from . $where;
970 $dao = CRM_Core_DAO::executeQuery($query, CRM_Core_DAO::$_nullArray);
971 }
972 }
973 }
974 }
8381af80 975
976 // function to copy soft credit record of first recurring contribution
977 // and add new soft credit against $targetContributionId
6c786a9b
EM
978 /**
979 * @param $recurId
980 * @param $targetContributionId
981 */
8381af80 982 function addrecurSoftCredit($recurId, $targetContributionId) {
983 $contriID = CRM_Core_DAO::getFieldValue('CRM_Contribute_DAO_Contribution', $recurId, 'id', 'contribution_recur_id');
984
985 $soft_contribution = new CRM_Contribute_DAO_ContributionSoft();
986 $soft_contribution->contribution_id = $contriID;
987
988 //check if first recurring contribution has any associated soft credit
989 if ($soft_contribution->find(TRUE)) {
990 $soft_contribution->contribution_id = $targetContributionId;
991 unset($soft_contribution->id);
992 $soft_contribution->save();
993 }
994 }
b2b0530a 995}