Merge pull request #14480 from JMAConsulting/core-553
[civicrm-core.git] / CRM / Pledge / BAO / PledgePayment.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 5 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2019 |
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
31 * @copyright CiviCRM LLC (c) 2004-2019
32 */
33 class CRM_Pledge_BAO_PledgePayment extends CRM_Pledge_DAO_PledgePayment {
34
35 /**
36 * Class constructor.
37 */
38 public function __construct() {
39 parent::__construct();
40 }
41
42 /**
43 * Get pledge payment details.
44 *
45 * @param int $pledgeId
46 * Pledge id.
47 *
48 * @return array
49 * associated array of pledge payment details
50 */
51 public static function getPledgePayments($pledgeId) {
52 $query = "
53 SELECT civicrm_pledge_payment.id id,
54 scheduled_amount,
55 scheduled_date,
56 reminder_date,
57 reminder_count,
58 actual_amount,
59 receive_date,
60 civicrm_pledge_payment.currency,
61 civicrm_option_value.name as status,
62 civicrm_option_value.label as label,
63 civicrm_contribution.id as contribution_id
64 FROM civicrm_pledge_payment
65
66 LEFT JOIN civicrm_contribution ON civicrm_pledge_payment.contribution_id = civicrm_contribution.id
67 LEFT JOIN civicrm_option_group ON ( civicrm_option_group.name = 'contribution_status' )
68 LEFT JOIN civicrm_option_value ON ( civicrm_pledge_payment.status_id = civicrm_option_value.value AND
69 civicrm_option_group.id = civicrm_option_value.option_group_id )
70 WHERE pledge_id = %1
71 ";
72
73 $params[1] = [$pledgeId, 'Integer'];
74 $payment = CRM_Core_DAO::executeQuery($query, $params);
75
76 $paymentDetails = [];
77 while ($payment->fetch()) {
78 $paymentDetails[$payment->id]['scheduled_amount'] = $payment->scheduled_amount;
79 $paymentDetails[$payment->id]['scheduled_date'] = $payment->scheduled_date;
80 $paymentDetails[$payment->id]['reminder_date'] = $payment->reminder_date;
81 $paymentDetails[$payment->id]['reminder_count'] = $payment->reminder_count;
82 $paymentDetails[$payment->id]['total_amount'] = $payment->actual_amount;
83 $paymentDetails[$payment->id]['receive_date'] = $payment->receive_date;
84 $paymentDetails[$payment->id]['status'] = $payment->status;
85 $paymentDetails[$payment->id]['label'] = $payment->label;
86 $paymentDetails[$payment->id]['id'] = $payment->id;
87 $paymentDetails[$payment->id]['contribution_id'] = $payment->contribution_id;
88 $paymentDetails[$payment->id]['currency'] = $payment->currency;
89 }
90
91 return $paymentDetails;
92 }
93
94 /**
95 * @param array $params
96 *
97 * @return pledge
98 */
99 public static function create($params) {
100 $transaction = new CRM_Core_Transaction();
101 $overdueStatusID = CRM_Core_PseudoConstant::getKey('CRM_Pledge_BAO_PledgePayment', 'status_id', 'Overdue');
102 $pendingStatusId = CRM_Core_PseudoConstant::getKey('CRM_Pledge_BAO_PledgePayment', 'status_id', 'Pending');
103
104 //calculate the scheduled date for every installment
105 $now = date('Ymd') . '000000';
106 $statues = $prevScheduledDate = [];
107 $prevScheduledDate[1] = CRM_Utils_Date::processDate($params['scheduled_date']);
108
109 if (CRM_Utils_Date::overdue($prevScheduledDate[1], $now)) {
110 $statues[1] = $overdueStatusID;
111 }
112 else {
113 $statues[1] = $pendingStatusId;
114 }
115
116 for ($i = 1; $i < $params['installments']; $i++) {
117 $prevScheduledDate[$i + 1] = self::calculateNextScheduledDate($params, $i);
118 if (CRM_Utils_Date::overdue($prevScheduledDate[$i + 1], $now)) {
119 $statues[$i + 1] = $overdueStatusID;
120 }
121 else {
122 $statues[$i + 1] = $pendingStatusId;
123 }
124 }
125
126 if ($params['installment_amount']) {
127 $params['scheduled_amount'] = $params['installment_amount'];
128 }
129 else {
130 $params['scheduled_amount'] = round(($params['amount'] / $params['installments']), 2);
131 }
132
133 for ($i = 1; $i <= $params['installments']; $i++) {
134 // calculate the scheduled amount for every installment.
135 if ($i == $params['installments']) {
136 $params['scheduled_amount'] = $params['amount'] - ($i - 1) * $params['scheduled_amount'];
137 }
138 if (!isset($params['contribution_id']) && $params['installments'] > 1) {
139 $params['status_id'] = $statues[$i];
140 }
141
142 $params['scheduled_date'] = $prevScheduledDate[$i];
143 $payment = self::add($params);
144 if (is_a($payment, 'CRM_Core_Error')) {
145 $transaction->rollback();
146 return $payment;
147 }
148
149 // we should add contribution id to only first payment record
150 if (isset($params['contribution_id'])) {
151 unset($params['contribution_id']);
152 unset($params['actual_amount']);
153 }
154 }
155
156 // update pledge status
157 self::updatePledgePaymentStatus($params['pledge_id']);
158
159 $transaction->commit();
160 return $payment;
161 }
162
163 /**
164 * Add pledge payment.
165 *
166 * @param array $params
167 * Associate array of field.
168 *
169 * @return CRM_Pledge_DAO_PledgePayment
170 * pledge payment id
171 */
172 public static function add($params) {
173 if (!empty($params['id'])) {
174 CRM_Utils_Hook::pre('edit', 'PledgePayment', $params['id'], $params);
175 }
176 else {
177 CRM_Utils_Hook::pre('create', 'PledgePayment', NULL, $params);
178 }
179
180 $payment = new CRM_Pledge_DAO_PledgePayment();
181 $payment->copyValues($params);
182
183 // set currency for CRM-1496
184 if (!isset($payment->currency)) {
185 $payment->currency = CRM_Core_Config::singleton()->defaultCurrency;
186 }
187
188 $result = $payment->save();
189
190 if (!empty($params['id'])) {
191 CRM_Utils_Hook::post('edit', 'PledgePayment', $payment->id, $payment);
192 }
193 else {
194 CRM_Utils_Hook::post('create', 'PledgePayment', $payment->id, $payment);
195 }
196
197 return $result;
198 }
199
200 /**
201 * Retrieve DB object based on input parameters.
202 *
203 * It also stores all the retrieved values in the default array.
204 *
205 * @param array $params
206 * (reference ) an assoc array of name/value pairs.
207 * @param array $defaults
208 * (reference ) an assoc array to hold the flattened values.
209 *
210 * @return CRM_Pledge_BAO_PledgePayment
211 */
212 public static function retrieve(&$params, &$defaults) {
213 $payment = new CRM_Pledge_BAO_PledgePayment();
214 $payment->copyValues($params);
215 if ($payment->find(TRUE)) {
216 CRM_Core_DAO::storeValues($payment, $defaults);
217 return $payment;
218 }
219 return NULL;
220 }
221
222 /**
223 * Delete pledge payment.
224 *
225 * @param int $id
226 *
227 * @return int
228 * pledge payment id
229 */
230 public static function del($id) {
231 $payment = new CRM_Pledge_DAO_PledgePayment();
232 $payment->id = $id;
233 if ($payment->find()) {
234 $payment->fetch();
235
236 CRM_Utils_Hook::pre('delete', 'PledgePayment', $id, $payment);
237
238 $result = $payment->delete();
239
240 CRM_Utils_Hook::post('delete', 'PledgePayment', $id, $payment);
241
242 return $result;
243 }
244 else {
245 return FALSE;
246 }
247 }
248
249 /**
250 * Delete all pledge payments.
251 *
252 * @param int $id
253 * Pledge id.
254 *
255 * @return bool
256 */
257 public static function deletePayments($id) {
258 if (!CRM_Utils_Rule::positiveInteger($id)) {
259 return FALSE;
260 }
261
262 $transaction = new CRM_Core_Transaction();
263
264 $payment = new CRM_Pledge_DAO_PledgePayment();
265 $payment->pledge_id = $id;
266
267 if ($payment->find()) {
268 while ($payment->fetch()) {
269 //also delete associated contribution.
270 if ($payment->contribution_id) {
271 CRM_Contribute_BAO_Contribution::deleteContribution($payment->contribution_id);
272 }
273 self::del($payment->id);
274 }
275 }
276
277 $transaction->commit();
278
279 return TRUE;
280 }
281
282 /**
283 * On delete contribution record update associated pledge payment and pledge.
284 *
285 * @param int $contributionID
286 * Contribution id.
287 *
288 * @return bool
289 */
290 public static function resetPledgePayment($contributionID) {
291 $transaction = new CRM_Core_Transaction();
292
293 $payment = new CRM_Pledge_DAO_PledgePayment();
294 $payment->contribution_id = $contributionID;
295 if ($payment->find(TRUE)) {
296 $payment->contribution_id = 'null';
297 $payment->status_id = CRM_Core_PseudoConstant::getKey('CRM_Pledge_BAO_Pledge', 'status_id', 'Pending');
298 $payment->scheduled_date = NULL;
299 $payment->reminder_date = NULL;
300 $payment->scheduled_amount = $payment->actual_amount;
301 $payment->actual_amount = 'null';
302 $payment->save();
303
304 //update pledge status.
305 $pledgeID = $payment->pledge_id;
306 $pledgeStatusID = self::calculatePledgeStatus($pledgeID);
307 CRM_Core_DAO::setFieldValue('CRM_Pledge_DAO_Pledge', $pledgeID, 'status_id', $pledgeStatusID);
308
309 }
310
311 $transaction->commit();
312 return TRUE;
313 }
314
315 /**
316 * Update Pledge Payment Status.
317 *
318 * @param int $pledgeID
319 * Id of pledge.
320 * @param array $paymentIDs
321 * Ids of pledge payment(s) to update.
322 * @param int $paymentStatusID
323 * Payment status to set.
324 * @param int $pledgeStatusID
325 * Pledge status to change (if needed).
326 * @param float|int $actualAmount , actual amount being paid
327 * @param bool $adjustTotalAmount
328 * Is amount being paid different from scheduled amount?.
329 * @param bool $isScriptUpdate
330 * Is function being called from bin script?.
331 *
332 * @return int
333 * $newStatus, updated status id (or 0)
334 */
335 public static function updatePledgePaymentStatus(
336 $pledgeID,
337 $paymentIDs = NULL,
338 $paymentStatusID = NULL,
339 $pledgeStatusID = NULL,
340 $actualAmount = 0,
341 $adjustTotalAmount = FALSE,
342 $isScriptUpdate = FALSE
343 ) {
344 $totalAmountClause = '';
345 $paymentContributionId = NULL;
346 $editScheduled = FALSE;
347
348 // get all statuses
349 $allStatus = CRM_Core_OptionGroup::values('pledge_status',
350 FALSE, FALSE, FALSE, NULL, 'name', TRUE
351 );
352
353 // if we get do not get contribution id means we are editing the scheduled payment.
354 if (!empty($paymentIDs)) {
355 $editScheduled = FALSE;
356 $payments = implode(',', $paymentIDs);
357 $paymentContributionId = CRM_Core_DAO::getFieldValue('CRM_Pledge_DAO_PledgePayment',
358 $payments,
359 'contribution_id',
360 'id'
361 );
362
363 if (!$paymentContributionId) {
364 $editScheduled = TRUE;
365 }
366 }
367
368 // if payment ids are passed, we update payment table first, since payments statuses are not dependent on pledge status
369 $pledgeStatusName = CRM_Core_PseudoConstant::getName('CRM_Pledge_BAO_Pledge', 'status_id', $pledgeStatusID);
370 if ((!empty($paymentIDs) || $pledgeStatusName == 'Cancelled') && (!$editScheduled || $isScriptUpdate)) {
371 if ($pledgeStatusName == 'Cancelled') {
372 $paymentStatusID = $pledgeStatusID;
373 }
374
375 self::updatePledgePayments($pledgeID, $paymentStatusID, $paymentIDs, $actualAmount, $paymentContributionId, $isScriptUpdate);
376 }
377 if (!empty($paymentIDs) && $actualAmount) {
378 $payments = implode(',', $paymentIDs);
379 $pledgeScheduledAmount = CRM_Core_DAO::getFieldValue('CRM_Pledge_DAO_PledgePayment',
380 $payments,
381 'scheduled_amount',
382 'id'
383 );
384
385 $pledgeStatusId = self::calculatePledgeStatus($pledgeID);
386 // Actual Pledge Amount
387 $actualPledgeAmount = CRM_Core_DAO::getFieldValue('CRM_Pledge_DAO_Pledge',
388 $pledgeID,
389 'amount',
390 'id'
391 );
392 // while editing scheduled we need to check if we are editing last pending
393 $lastPending = FALSE;
394 if (!$paymentContributionId) {
395 $checkPendingCount = self::getOldestPledgePayment($pledgeID, 2);
396 if ($checkPendingCount['count'] == 1) {
397 $lastPending = TRUE;
398 }
399 }
400
401 // check if this is the last payment and adjust the actual amount.
402 if ($pledgeStatusId && $pledgeStatusId == array_search('Completed', $allStatus) || $lastPending) {
403 // last scheduled payment
404 if ($actualAmount >= $pledgeScheduledAmount) {
405 $adjustTotalAmount = TRUE;
406 }
407 elseif (!$adjustTotalAmount) {
408 // actual amount is less than the scheduled amount, so enter new pledge payment record
409 $pledgeFrequencyUnit = CRM_Core_DAO::getFieldValue('CRM_Pledge_DAO_Pledge', $pledgeID, 'frequency_unit', 'id');
410 $pledgeFrequencyInterval = CRM_Core_DAO::getFieldValue('CRM_Pledge_DAO_Pledge', $pledgeID, 'frequency_interval', 'id');
411 $pledgeScheduledDate = CRM_Core_DAO::getFieldValue('CRM_Pledge_DAO_PledgePayment', $payments, 'scheduled_date', 'id');
412 $scheduled_date = CRM_Utils_Date::processDate($pledgeScheduledDate);
413 $date['year'] = (int) substr($scheduled_date, 0, 4);
414 $date['month'] = (int) substr($scheduled_date, 4, 2);
415 $date['day'] = (int) substr($scheduled_date, 6, 2);
416 $newDate = date('YmdHis', mktime(0, 0, 0, $date['month'], $date['day'], $date['year']));
417 $ScheduledDate = CRM_Utils_Date::format(CRM_Utils_Date::intervalAdd($pledgeFrequencyUnit,
418 $pledgeFrequencyInterval, $newDate
419 ));
420 $pledgeParams = [
421 'status_id' => array_search('Pending', $allStatus),
422 'pledge_id' => $pledgeID,
423 'scheduled_amount' => ($pledgeScheduledAmount - $actualAmount),
424 'scheduled_date' => $ScheduledDate,
425 ];
426 $payment = self::add($pledgeParams);
427 // while editing schedule, after adding a new pledge payemnt update the scheduled amount of the current payment
428 if (!$paymentContributionId) {
429 CRM_Core_DAO::setFieldValue('CRM_Pledge_DAO_PledgePayment', $payments, 'scheduled_amount', $actualAmount);
430 }
431 }
432 }
433 elseif (!$adjustTotalAmount) {
434 // not last schedule amount and also not selected to adjust Total
435 $paymentContributionId = CRM_Core_DAO::getFieldValue('CRM_Pledge_DAO_PledgePayment',
436 $payments,
437 'contribution_id',
438 'id'
439 );
440 self::adjustPledgePayment($pledgeID, $actualAmount, $pledgeScheduledAmount, $paymentContributionId, $payments, $paymentStatusID);
441 // while editing schedule, after adding a new pledge payemnt update the scheduled amount of the current payment
442 if (!$paymentContributionId) {
443 CRM_Core_DAO::setFieldValue('CRM_Pledge_DAO_PledgePayment', $payments, 'scheduled_amount', $actualAmount);
444 }
445 // after adjusting all payments check if the actual amount was greater than the actual remaining amount , if so then update the total pledge amount.
446 $pledgeStatusId = self::calculatePledgeStatus($pledgeID);
447 $balanceQuery = "
448 SELECT sum( civicrm_pledge_payment.actual_amount )
449 FROM civicrm_pledge_payment
450 WHERE civicrm_pledge_payment.pledge_id = %1
451 AND civicrm_pledge_payment.status_id = 1
452 ";
453 $totalPaidParams = [1 => [$pledgeID, 'Integer']];
454 $totalPaidAmount = CRM_Core_DAO::singleValueQuery($balanceQuery, $totalPaidParams);
455 $remainingTotalAmount = ($actualPledgeAmount - $totalPaidAmount);
456 if (($pledgeStatusId && $pledgeStatusId == array_search('Completed', $allStatus)) && (($actualAmount > $remainingTotalAmount) || ($actualAmount >= $actualPledgeAmount))) {
457 $totalAmountClause = ", civicrm_pledge.amount = {$totalPaidAmount}";
458 }
459 }
460 if ($adjustTotalAmount) {
461 $newTotalAmount = ($actualPledgeAmount + ($actualAmount - $pledgeScheduledAmount));
462 $totalAmountClause = ", civicrm_pledge.amount = {$newTotalAmount}";
463 if (!$paymentContributionId) {
464 CRM_Core_DAO::setFieldValue('CRM_Pledge_DAO_PledgePayment', $payments, 'scheduled_amount', $actualAmount);
465 }
466 }
467 }
468
469 $cancelDateClause = $endDateClause = NULL;
470 // update pledge and payment status if status is Completed/Cancelled.
471 if ($pledgeStatusID && $pledgeStatusID == array_search('Cancelled', $allStatus)) {
472 $paymentStatusID = $pledgeStatusID;
473 $cancelDateClause = ", civicrm_pledge.cancel_date = CURRENT_TIMESTAMP ";
474 }
475 else {
476 // get pledge status
477 $pledgeStatusID = self::calculatePledgeStatus($pledgeID);
478 }
479
480 if ($pledgeStatusID == array_search('Completed', $allStatus)) {
481 $endDateClause = ", civicrm_pledge.end_date = CURRENT_TIMESTAMP ";
482 }
483
484 // update pledge status
485 $query = "
486 UPDATE civicrm_pledge
487 SET civicrm_pledge.status_id = %1
488 {$cancelDateClause} {$endDateClause} {$totalAmountClause}
489 WHERE civicrm_pledge.id = %2
490 ";
491
492 $params = [
493 1 => [$pledgeStatusID, 'Integer'],
494 2 => [$pledgeID, 'Integer'],
495 ];
496
497 CRM_Core_DAO::executeQuery($query, $params);
498
499 return $pledgeStatusID;
500 }
501
502 /**
503 * Calculate the base scheduled date. This function effectively 'rounds' the $params['scheduled_date'] value
504 * to the first payment date with respect to the frequency day - ie. if payments are on the 15th of the month the date returned
505 * will be the 15th of the relevant month. Then to calculate the payments you can use intervalAdd ie.
506 * CRM_Utils_Date::intervalAdd( $params['frequency_unit'], $i * ($params['frequency_interval']) , calculateBaseScheduledDate( &$params )))
507 *
508 * @param array $params
509 *
510 * @return array
511 * Next scheduled date as an array
512 */
513 public static function calculateBaseScheduleDate(&$params) {
514 $date = [];
515 $scheduled_date = CRM_Utils_Date::processDate($params['scheduled_date']);
516 $date['year'] = (int) substr($scheduled_date, 0, 4);
517 $date['month'] = (int) substr($scheduled_date, 4, 2);
518 $date['day'] = (int) substr($scheduled_date, 6, 2);
519 // calculation of schedule date according to frequency day of period
520 // frequency day is not applicable for daily installments
521 if ($params['frequency_unit'] != 'day' && $params['frequency_unit'] != 'year') {
522 if ($params['frequency_unit'] != 'week') {
523 // CRM-18316: To calculate pledge scheduled dates at the end of a month.
524 $date['day'] = $params['frequency_day'];
525 $lastDayOfMonth = date('t', mktime(0, 0, 0, $date['month'], 1, $date['year']));
526 if ($lastDayOfMonth < $date['day']) {
527 $date['day'] = $lastDayOfMonth;
528 }
529 }
530 elseif ($params['frequency_unit'] == 'week') {
531
532 // for week calculate day of week ie. Sunday,Monday etc. as next payment date
533 $dayOfWeek = date('w', mktime(0, 0, 0, $date['month'], $date['day'], $date['year']));
534 $frequencyDay = $params['frequency_day'] - $dayOfWeek;
535
536 $scheduleDate = explode("-", date('n-j-Y', mktime(0, 0, 0, $date['month'],
537 $date['day'] + $frequencyDay, $date['year']
538 )));
539 $date['month'] = $scheduleDate[0];
540 $date['day'] = $scheduleDate[1];
541 $date['year'] = $scheduleDate[2];
542 }
543 }
544 $newdate = date('YmdHis', mktime(0, 0, 0, $date['month'], $date['day'], $date['year']));
545 return $newdate;
546 }
547
548 /**
549 * Calculate next scheduled pledge payment date. Function calculates next pledge payment date.
550 *
551 * @param array $params
552 * must include frequency unit & frequency interval
553 * @param int $paymentNo
554 * number of payment in sequence (e.g. 1 for first calculated payment (treat initial payment as 0)
555 * @param string $basePaymentDate
556 * date to calculate payments from. This would normally be the
557 * first day of the pledge (default) & is calculated off the 'scheduled date' param. Returned date will
558 * be equal to basePaymentDate normalised to fit the 'pledge pattern' + number of installments
559 *
560 * @return string
561 * formatted date
562 */
563 public static function calculateNextScheduledDate(&$params, $paymentNo, $basePaymentDate = NULL) {
564 $interval = $paymentNo * ($params['frequency_interval']);
565 if (!$basePaymentDate) {
566 $basePaymentDate = self::calculateBaseScheduleDate($params);
567 }
568
569 //CRM-18316 - change $basePaymentDate for the end dates of the month eg: 29, 30 or 31.
570 if ($params['frequency_unit'] == 'month' && in_array($params['frequency_day'], [29, 30, 31])) {
571 $frequency = $params['frequency_day'];
572 extract(date_parse($basePaymentDate));
573 $lastDayOfMonth = date('t', mktime($hour, $minute, $second, $month + $interval, 1, $year));
574 // Take the last day in case the current month is Feb or frequency_day is set to 31.
575 if (in_array($lastDayOfMonth, [28, 29]) || $frequency == 31) {
576 $frequency = 0;
577 $interval++;
578 }
579 $basePaymentDate = [
580 'M' => $month,
581 'd' => $frequency,
582 'Y' => $year,
583 ];
584 }
585
586 return CRM_Utils_Date::format(
587 CRM_Utils_Date::intervalAdd(
588 $params['frequency_unit'],
589 $interval,
590 $basePaymentDate
591 )
592 );
593 }
594
595 /**
596 * Calculate the pledge status.
597 *
598 * @param int $pledgeId
599 * Pledge id.
600 *
601 * @return int
602 * $statusId calculated status id of pledge
603 */
604 public static function calculatePledgeStatus($pledgeId) {
605 $paymentStatusTypes = CRM_Contribute_PseudoConstant::contributionStatus(NULL, 'name');
606 $pledgeStatusTypes = CRM_Pledge_BAO_Pledge::buildOptions('status_id', 'validate');
607
608 //return if the pledge is cancelled.
609 $currentPledgeStatusId = CRM_Core_DAO::getFieldValue('CRM_Pledge_DAO_Pledge', $pledgeId, 'status_id', 'id', TRUE);
610 if ($currentPledgeStatusId == array_search('Cancelled', $pledgeStatusTypes)) {
611 return $currentPledgeStatusId;
612 }
613
614 // retrieve all pledge payments for this particular pledge
615 $allPledgePayments = $allStatus = [];
616 $returnProperties = ['status_id'];
617 CRM_Core_DAO::commonRetrieveAll('CRM_Pledge_DAO_PledgePayment', 'pledge_id', $pledgeId, $allPledgePayments, $returnProperties);
618
619 // build pledge payment statuses
620 foreach ($allPledgePayments as $key => $value) {
621 $allStatus[$value['id']] = $paymentStatusTypes[$value['status_id']];
622 }
623
624 if (array_search('Overdue', $allStatus)) {
625 $statusId = array_search('Overdue', $pledgeStatusTypes);
626 }
627 elseif (array_search('Completed', $allStatus)) {
628 if (count(array_count_values($allStatus)) == 1) {
629 $statusId = array_search('Completed', $pledgeStatusTypes);
630 }
631 else {
632 $statusId = array_search('In Progress', $pledgeStatusTypes);
633 }
634 }
635 else {
636 $statusId = array_search('Pending', $pledgeStatusTypes);
637 }
638
639 return $statusId;
640 }
641
642 /**
643 * Update pledge payment table.
644 *
645 * @param int $pledgeId
646 * Pledge id.
647 * @param int $paymentStatusId
648 * Payment status id to set.
649 * @param array $paymentIds
650 * Payment ids to be updated.
651 * @param float|int $actualAmount , actual amount being paid
652 * @param int $contributionId
653 * , Id of associated contribution when payment is recorded.
654 * @param bool $isScriptUpdate
655 * , is function being called from bin script?.
656 *
657 */
658 public static function updatePledgePayments(
659 $pledgeId,
660 $paymentStatusId,
661 $paymentIds = NULL,
662 $actualAmount = 0,
663 $contributionId = NULL,
664 $isScriptUpdate = FALSE
665 ) {
666 $allStatus = CRM_Contribute_PseudoConstant::contributionStatus(NULL, 'name');
667 $paymentClause = NULL;
668 if (!empty($paymentIds)) {
669 $payments = implode(',', $paymentIds);
670 $paymentClause = " AND civicrm_pledge_payment.id IN ( {$payments} )";
671 }
672 elseif ($paymentStatusId == array_search('Cancelled', $allStatus)) {
673 $completedStatus = array_search('Completed', $allStatus);
674 $paymentClause = " AND civicrm_pledge_payment.status_id != {$completedStatus}";
675 }
676 $actualAmountClause = NULL;
677 $contributionIdClause = NULL;
678 if (isset($contributionId) && !$isScriptUpdate) {
679 $contributionIdClause = ", civicrm_pledge_payment.contribution_id = {$contributionId}";
680 $actualAmountClause = ", civicrm_pledge_payment.actual_amount = {$actualAmount}";
681 }
682
683 $query = "
684 UPDATE civicrm_pledge_payment
685 SET civicrm_pledge_payment.status_id = {$paymentStatusId}
686 {$actualAmountClause} {$contributionIdClause}
687 WHERE civicrm_pledge_payment.pledge_id = %1
688 {$paymentClause}
689 ";
690
691 CRM_Core_DAO::executeQuery($query, [1 => [$pledgeId, 'Integer']]);
692 }
693
694 /**
695 * Update pledge payment table when reminder is sent.
696 *
697 * @param int $paymentId
698 * Payment id.
699 */
700 public static function updateReminderDetails($paymentId) {
701 $query = "
702 UPDATE civicrm_pledge_payment
703 SET civicrm_pledge_payment.reminder_date = CURRENT_TIMESTAMP,
704 civicrm_pledge_payment.reminder_count = civicrm_pledge_payment.reminder_count + 1
705 WHERE civicrm_pledge_payment.id = {$paymentId}
706 ";
707 $dao = CRM_Core_DAO::executeQuery($query);
708 }
709
710 /**
711 * Get oldest pending or in progress pledge payments.
712 *
713 * @param int $pledgeID
714 * Pledge id.
715 *
716 * @param int $limit
717 *
718 * @return array
719 * associated array of pledge details
720 */
721 public static function getOldestPledgePayment($pledgeID, $limit = 1) {
722 // get pending / overdue statuses
723 $pledgeStatuses = CRM_Core_OptionGroup::values('pledge_status',
724 FALSE, FALSE, FALSE, NULL, 'name'
725 );
726
727 // get pending and overdue payments
728 $status[] = array_search('Pending', $pledgeStatuses);
729 $status[] = array_search('Overdue', $pledgeStatuses);
730
731 $statusClause = " IN (" . implode(',', $status) . ")";
732
733 $query = "
734 SELECT civicrm_pledge_payment.id id, civicrm_pledge_payment.scheduled_amount amount, civicrm_pledge_payment.currency, civicrm_pledge_payment.scheduled_date,civicrm_pledge.financial_type_id
735 FROM civicrm_pledge, civicrm_pledge_payment
736 WHERE civicrm_pledge.id = civicrm_pledge_payment.pledge_id
737 AND civicrm_pledge_payment.status_id {$statusClause}
738 AND civicrm_pledge.id = %1
739 ORDER BY civicrm_pledge_payment.scheduled_date ASC
740 LIMIT 0, %2
741 ";
742
743 $params[1] = [$pledgeID, 'Integer'];
744 $params[2] = [$limit, 'Integer'];
745 $payment = CRM_Core_DAO::executeQuery($query, $params);
746 $count = 1;
747 $paymentDetails = [];
748 while ($payment->fetch()) {
749 $paymentDetails[] = [
750 'id' => $payment->id,
751 'amount' => $payment->amount,
752 'currency' => $payment->currency,
753 'schedule_date' => $payment->scheduled_date,
754 'financial_type_id' => $payment->financial_type_id,
755 'count' => $count,
756 ];
757 $count++;
758 }
759 return end($paymentDetails);
760 }
761
762 /**
763 * @param int $pledgeID
764 * @param $actualAmount
765 * @param $pledgeScheduledAmount
766 * @param int $paymentContributionId
767 * @param int $pPaymentId
768 * @param int $paymentStatusID
769 */
770 public static function adjustPledgePayment($pledgeID, $actualAmount, $pledgeScheduledAmount, $paymentContributionId = NULL, $pPaymentId = NULL, $paymentStatusID = NULL) {
771 $allStatus = CRM_Contribute_PseudoConstant::contributionStatus(NULL, 'name');
772 $paymentStatusName = CRM_Core_PseudoConstant::getName('CRM_Pledge_BAO_PledgePayment', 'status_id', $paymentStatusID);
773 if ($paymentStatusName == 'Cancelled'|| $paymentStatusName == 'Refunded') {
774 $query = "
775 SELECT civicrm_pledge_payment.id id
776 FROM civicrm_pledge_payment
777 WHERE civicrm_pledge_payment.contribution_id = {$paymentContributionId}
778 ";
779 $paymentsAffected = CRM_Core_DAO::executeQuery($query);
780 $paymentIDs = [];
781 while ($paymentsAffected->fetch()) {
782 $paymentIDs[] = $paymentsAffected->id;
783 }
784 // Reset the affected values by the amount paid more than the scheduled amount
785 foreach ($paymentIDs as $key => $value) {
786 $payment = new CRM_Pledge_DAO_PledgePayment();
787 $payment->id = $value;
788 if ($payment->find(TRUE)) {
789 $payment->contribution_id = 'null';
790 $payment->status_id = array_search('Pending', $allStatus);
791 $payment->scheduled_date = NULL;
792 $payment->reminder_date = NULL;
793 $payment->scheduled_amount = $pledgeScheduledAmount;
794 $payment->actual_amount = 'null';
795 $payment->save();
796 }
797 }
798
799 // Cancel the initial paid amount
800 CRM_Core_DAO::setFieldValue('CRM_Pledge_DAO_PledgePayment', reset($paymentIDs), 'status_id', $paymentStatusID, 'id');
801 CRM_Core_DAO::setFieldValue('CRM_Pledge_DAO_PledgePayment', reset($paymentIDs), 'actual_amount', $actualAmount, 'id');
802
803 // Add new payment after the last payment for the pledge
804 $allPayments = self::getPledgePayments($pledgeID);
805 $lastPayment = array_pop($allPayments);
806
807 $pledgeFrequencyUnit = CRM_Core_DAO::getFieldValue('CRM_Pledge_DAO_Pledge', $pledgeID, 'frequency_unit', 'id');
808 $pledgeFrequencyInterval = CRM_Core_DAO::getFieldValue('CRM_Pledge_DAO_Pledge', $pledgeID, 'frequency_interval', 'id');
809 $pledgeScheduledDate = $lastPayment['scheduled_date'];
810 $scheduled_date = CRM_Utils_Date::processDate($pledgeScheduledDate);
811 $date['year'] = (int) substr($scheduled_date, 0, 4);
812 $date['month'] = (int) substr($scheduled_date, 4, 2);
813 $date['day'] = (int) substr($scheduled_date, 6, 2);
814 $newDate = date('YmdHis', mktime(0, 0, 0, $date['month'], $date['day'], $date['year']));
815 $ScheduledDate = CRM_Utils_Date::format(CRM_Utils_Date::intervalAdd($pledgeFrequencyUnit, $pledgeFrequencyInterval, $newDate));
816 $pledgeParams = [
817 'status_id' => array_search('Pending', $allStatus),
818 'pledge_id' => $pledgeID,
819 'scheduled_amount' => $pledgeScheduledAmount,
820 'scheduled_date' => $ScheduledDate,
821 ];
822 $payment = self::add($pledgeParams);
823 }
824 else {
825 $nextPledgeInstallmentDue = self::getOldestPledgePayment($pledgeID);
826 if (!$paymentContributionId) {
827 // means we are editing payment scheduled payment, so get the second pending to update.
828 $nextPledgeInstallmentDue = self::getOldestPledgePayment($pledgeID, 2);
829 if (($nextPledgeInstallmentDue['count'] != 1) && ($nextPledgeInstallmentDue['id'] == $pPaymentId)) {
830 $nextPledgeInstallmentDue = self::getOldestPledgePayment($pledgeID);
831 }
832 }
833
834 if ($nextPledgeInstallmentDue) {
835 // not the last scheduled payment and the actual amount is less than the expected , add it to oldest pending.
836 if (($actualAmount != $pledgeScheduledAmount) && (($actualAmount < $pledgeScheduledAmount) || (($actualAmount - $pledgeScheduledAmount) < $nextPledgeInstallmentDue['amount']))) {
837 $oldScheduledAmount = $nextPledgeInstallmentDue['amount'];
838 $newScheduledAmount = $oldScheduledAmount + ($pledgeScheduledAmount - $actualAmount);
839 // store new amount in oldest pending payment record.
840 CRM_Core_DAO::setFieldValue('CRM_Pledge_DAO_PledgePayment',
841 $nextPledgeInstallmentDue['id'],
842 'scheduled_amount',
843 $newScheduledAmount
844 );
845 if (CRM_Core_DAO::getFieldValue('CRM_Pledge_DAO_PledgePayment', $nextPledgeInstallmentDue['id'], 'contribution_id', 'id')) {
846 CRM_Core_DAO::setFieldValue('CRM_Pledge_DAO_PledgePayment',
847 $nextPledgeInstallmentDue['id'],
848 'contribution_id',
849 $paymentContributionId
850 );
851 }
852 }
853 elseif (($actualAmount > $pledgeScheduledAmount) && (($actualAmount - $pledgeScheduledAmount) >= $nextPledgeInstallmentDue['amount'])) {
854 // here the actual amount is greater than expected and also greater than the next installment amount, so update the next installment as complete and again add it to next subsequent pending payment
855 // set the actual amount of the next pending to '0', set contribution Id to current contribution Id and status as completed
856 $paymentId = [$nextPledgeInstallmentDue['id']];
857 self::updatePledgePayments($pledgeID, array_search('Completed', $allStatus), $paymentId, 0, $paymentContributionId);
858 CRM_Core_DAO::setFieldValue('CRM_Pledge_DAO_PledgePayment', $nextPledgeInstallmentDue['id'], 'scheduled_amount', 0, 'id');
859 if (!$paymentContributionId) {
860 // means we are editing payment scheduled payment.
861 $oldestPaymentAmount = self::getOldestPledgePayment($pledgeID, 2);
862 }
863 $newActualAmount = round(($actualAmount - $pledgeScheduledAmount), CRM_Utils_Money::getCurrencyPrecision());
864 $newPledgeScheduledAmount = $nextPledgeInstallmentDue['amount'];
865 if (!$paymentContributionId) {
866 $newActualAmount = ($actualAmount - $pledgeScheduledAmount);
867 $newPledgeScheduledAmount = $oldestPaymentAmount['amount'];
868 // means we are editing payment scheduled payment, so update scheduled amount.
869 CRM_Core_DAO::setFieldValue('CRM_Pledge_DAO_PledgePayment',
870 $oldestPaymentAmount['id'],
871 'scheduled_amount',
872 $newActualAmount
873 );
874 }
875 if ($newActualAmount > 0) {
876 self::adjustPledgePayment($pledgeID, $newActualAmount, $newPledgeScheduledAmount, $paymentContributionId);
877 }
878 }
879 }
880 }
881 }
882
883 /**
884 * Override buildOptions to hack out some statuses.
885 *
886 * @todo instead of using & hacking the shared optionGroup contribution_status use a separate one.
887 *
888 * @param string $fieldName
889 * @param string $context
890 * @param array $props
891 *
892 * @return array|bool
893 */
894 public static function buildOptions($fieldName, $context = NULL, $props = []) {
895 $result = parent::buildOptions($fieldName, $context, $props);
896 if ($fieldName == 'status_id') {
897 $result = CRM_Pledge_BAO_Pledge::buildOptions($fieldName, $context, $props);
898 $result = array_diff($result, ['Failed', 'In Progress']);
899 }
900 return $result;
901 }
902
903 }