Merge pull request #15281 from mattwire/formatpaymentparams_useprepare
[civicrm-core.git] / CRM / Pledge / BAO / PledgePayment.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
fee14197 4 | CiviCRM version 5 |
6a488035 5 +--------------------------------------------------------------------+
6b83d5bd 6 | Copyright CiviCRM LLC (c) 2004-2019 |
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 +--------------------------------------------------------------------+
d25dd0ee 26 */
6a488035
TO
27
28/**
29 *
30 * @package CRM
6b83d5bd 31 * @copyright CiviCRM LLC (c) 2004-2019
6a488035
TO
32 */
33class CRM_Pledge_BAO_PledgePayment extends CRM_Pledge_DAO_PledgePayment {
34
35 /**
fe482240 36 * Class constructor.
6a488035 37 */
00be9182 38 public function __construct() {
6a488035
TO
39 parent::__construct();
40 }
41
42 /**
fe482240 43 * Get pledge payment details.
6a488035 44 *
3a1617b6
TO
45 * @param int $pledgeId
46 * Pledge id.
6a488035 47 *
a6c01b45
CW
48 * @return array
49 * associated array of pledge payment details
6a488035 50 */
00be9182 51 public static function getPledgePayments($pledgeId) {
6a488035
TO
52 $query = "
53SELECT civicrm_pledge_payment.id id,
54 scheduled_amount,
55 scheduled_date,
56 reminder_date,
57 reminder_count,
58 actual_amount,
59 receive_date,
a9ae3b2c 60 civicrm_pledge_payment.currency,
6a488035
TO
61 civicrm_option_value.name as status,
62 civicrm_option_value.label as label,
63 civicrm_contribution.id as contribution_id
64FROM civicrm_pledge_payment
65
66LEFT JOIN civicrm_contribution ON civicrm_pledge_payment.contribution_id = civicrm_contribution.id
67LEFT JOIN civicrm_option_group ON ( civicrm_option_group.name = 'contribution_status' )
68LEFT 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 )
70WHERE pledge_id = %1
71";
72
be2fb01f 73 $params[1] = [$pledgeId, 'Integer'];
6a488035
TO
74 $payment = CRM_Core_DAO::executeQuery($query, $params);
75
be2fb01f 76 $paymentDetails = [];
6a488035
TO
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
ffd93213 94 /**
c490a46a 95 * @param array $params
ffd93213
EM
96 *
97 * @return pledge
98 */
00be9182 99 public static function create($params) {
6a488035 100 $transaction = new CRM_Core_Transaction();
74edda99 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');
6a488035
TO
103
104 //calculate the scheduled date for every installment
105 $now = date('Ymd') . '000000';
be2fb01f 106 $statues = $prevScheduledDate = [];
6a488035
TO
107 $prevScheduledDate[1] = CRM_Utils_Date::processDate($params['scheduled_date']);
108
109 if (CRM_Utils_Date::overdue($prevScheduledDate[1], $now)) {
74edda99 110 $statues[1] = $overdueStatusID;
6a488035
TO
111 }
112 else {
74edda99 113 $statues[1] = $pendingStatusId;
6a488035
TO
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)) {
74edda99 119 $statues[$i + 1] = $overdueStatusID;
6a488035
TO
120 }
121 else {
74edda99 122 $statues[$i + 1] = $pendingStatusId;
6a488035
TO
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++) {
cc28438b 134 // calculate the scheduled amount for every installment.
6a488035
TO
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
cc28438b 156 // update pledge status
6a488035
TO
157 self::updatePledgePaymentStatus($params['pledge_id']);
158
159 $transaction->commit();
160 return $payment;
161 }
162
163 /**
fe482240 164 * Add pledge payment.
6a488035 165 *
3a1617b6
TO
166 * @param array $params
167 * Associate array of field.
6a488035 168 *
906e6120 169 * @return CRM_Pledge_DAO_PledgePayment
72b3a70c 170 * pledge payment id
6a488035 171 */
00be9182 172 public static function add($params) {
a7488080 173 if (!empty($params['id'])) {
6a488035
TO
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)) {
906e6120 185 $payment->currency = CRM_Core_Config::singleton()->defaultCurrency;
6a488035
TO
186 }
187
188 $result = $payment->save();
189
a7488080 190 if (!empty($params['id'])) {
6a488035
TO
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
6a488035
TO
197 return $result;
198 }
199
200 /**
fe482240
EM
201 * Retrieve DB object based on input parameters.
202 *
203 * It also stores all the retrieved values in the default array.
6a488035 204 *
3a1617b6
TO
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.
6a488035 209 *
16b10e64 210 * @return CRM_Pledge_BAO_PledgePayment
6a488035 211 */
00be9182 212 public static function retrieve(&$params, &$defaults) {
317fceb4 213 $payment = new CRM_Pledge_BAO_PledgePayment();
6a488035
TO
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 /**
fe482240 223 * Delete pledge payment.
6a488035 224 *
100fef9d 225 * @param int $id
6a488035 226 *
a6c01b45
CW
227 * @return int
228 * pledge payment id
6a488035 229 */
00be9182 230 public static function del($id) {
6a488035
TO
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 /**
fe482240 250 * Delete all pledge payments.
6a488035 251 *
3a1617b6
TO
252 * @param int $id
253 * Pledge id.
6a488035 254 *
77b97be7 255 * @return bool
6a488035 256 */
00be9182 257 public static function deletePayments($id) {
6a488035
TO
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 }
7eb6e796 273 self::del($payment->id);
6a488035
TO
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 *
3a1617b6
TO
285 * @param int $contributionID
286 * Contribution id.
6a488035 287 *
77b97be7 288 * @return bool
6a488035 289 */
00be9182 290 public static function resetPledgePayment($contributionID) {
6a488035
TO
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';
51a4603b 297 $payment->status_id = CRM_Core_PseudoConstant::getKey('CRM_Pledge_BAO_Pledge', 'status_id', 'Pending');
6a488035
TO
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
6a488035
TO
309 }
310
311 $transaction->commit();
312 return TRUE;
313 }
314
315 /**
fe482240 316 * Update Pledge Payment Status.
6a488035 317 *
3a1617b6 318 * @param int $pledgeID
51a4603b 319 * Id of pledge.
3a1617b6 320 * @param array $paymentIDs
51a4603b 321 * Ids of pledge payment(s) to update.
3a1617b6 322 * @param int $paymentStatusID
51a4603b 323 * Payment status to set.
3a1617b6
TO
324 * @param int $pledgeStatusID
325 * Pledge status to change (if needed).
fd31fa4c 326 * @param float|int $actualAmount , actual amount being paid
3a1617b6 327 * @param bool $adjustTotalAmount
51a4603b 328 * Is amount being paid different from scheduled amount?.
3a1617b6 329 * @param bool $isScriptUpdate
51a4603b 330 * Is function being called from bin script?.
6a488035 331 *
a6c01b45
CW
332 * @return int
333 * $newStatus, updated status id (or 0)
6a488035 334 */
317fceb4 335 public static function updatePledgePaymentStatus(
6a488035 336 $pledgeID,
64041b14 337 $paymentIDs = NULL,
338 $paymentStatusID = NULL,
339 $pledgeStatusID = NULL,
340 $actualAmount = 0,
6a488035 341 $adjustTotalAmount = FALSE,
64041b14 342 $isScriptUpdate = FALSE
6a488035
TO
343 ) {
344 $totalAmountClause = '';
345 $paymentContributionId = NULL;
346 $editScheduled = FALSE;
347
cc28438b 348 // get all statuses
e7212d86
JP
349 $allStatus = CRM_Core_OptionGroup::values('pledge_status',
350 FALSE, FALSE, FALSE, NULL, 'name', TRUE
351 );
6a488035
TO
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
7ce5b644 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') {
6a488035
TO
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 );
cc28438b 392 // while editing scheduled we need to check if we are editing last pending
6a488035
TO
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) {
64041b14 405 $adjustTotalAmount = TRUE;
406 }
6a488035
TO
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,
64041b14 418 $pledgeFrequencyInterval, $newDate
419 ));
be2fb01f 420 $pledgeParams = [
6a488035
TO
421 'status_id' => array_search('Pending', $allStatus),
422 'pledge_id' => $pledgeID,
423 'scheduled_amount' => ($pledgeScheduledAmount - $actualAmount),
424 'scheduled_date' => $ScheduledDate,
be2fb01f 425 ];
6a488035
TO
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 }
64041b14 432 }
6a488035
TO
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 );
a9ae3b2c 440 self::adjustPledgePayment($pledgeID, $actualAmount, $pledgeScheduledAmount, $paymentContributionId, $payments, $paymentStatusID);
6a488035
TO
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 ";
be2fb01f 453 $totalPaidParams = [1 => [$pledgeID, 'Integer']];
6a488035
TO
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;
cc28438b 470 // update pledge and payment status if status is Completed/Cancelled.
6a488035
TO
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
cc28438b 484 // update pledge status
6a488035
TO
485 $query = "
486UPDATE civicrm_pledge
487 SET civicrm_pledge.status_id = %1
488 {$cancelDateClause} {$endDateClause} {$totalAmountClause}
489WHERE civicrm_pledge.id = %2
490";
491
be2fb01f
CW
492 $params = [
493 1 => [$pledgeStatusID, 'Integer'],
494 2 => [$pledgeID, 'Integer'],
495 ];
6a488035 496
c8ab305b 497 CRM_Core_DAO::executeQuery($query, $params);
6a488035
TO
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 *
6a488035
TO
508 * @param array $params
509 *
a6c01b45
CW
510 * @return array
511 * Next scheduled date as an array
6a488035 512 */
7e66081a 513 public static function calculateBaseScheduleDate(&$params) {
be2fb01f 514 $date = [];
6a488035 515 $scheduled_date = CRM_Utils_Date::processDate($params['scheduled_date']);
64041b14 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);
cc28438b
SB
519 // calculation of schedule date according to frequency day of period
520 // frequency day is not applicable for daily installments
6a488035
TO
521 if ($params['frequency_unit'] != 'day' && $params['frequency_unit'] != 'year') {
522 if ($params['frequency_unit'] != 'week') {
921bb3a9 523 // CRM-18316: To calculate pledge scheduled dates at the end of a month.
6a488035 524 $date['day'] = $params['frequency_day'];
7e66081a 525 $lastDayOfMonth = date('t', mktime(0, 0, 0, $date['month'], 1, $date['year']));
921bb3a9
WA
526 if ($lastDayOfMonth < $date['day']) {
527 $date['day'] = $lastDayOfMonth;
528 }
6a488035
TO
529 }
530 elseif ($params['frequency_unit'] == 'week') {
531
cc28438b 532 // for week calculate day of week ie. Sunday,Monday etc. as next payment date
6a488035
TO
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'],
64041b14 537 $date['day'] + $frequencyDay, $date['year']
538 )));
6a488035 539 $date['month'] = $scheduleDate[0];
64041b14 540 $date['day'] = $scheduleDate[1];
541 $date['year'] = $scheduleDate[2];
6a488035
TO
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 *
72b3a70c
CW
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
6a488035 559 *
72b3a70c
CW
560 * @return string
561 * formatted date
6a488035 562 */
00be9182 563 public static function calculateNextScheduledDate(&$params, $paymentNo, $basePaymentDate = NULL) {
7e66081a 564 $interval = $paymentNo * ($params['frequency_interval']);
6a488035 565 if (!$basePaymentDate) {
7e66081a 566 $basePaymentDate = self::calculateBaseScheduleDate($params);
567 }
568
569 //CRM-18316 - change $basePaymentDate for the end dates of the month eg: 29, 30 or 31.
be2fb01f 570 if ($params['frequency_unit'] == 'month' && in_array($params['frequency_day'], [29, 30, 31])) {
7e66081a 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.
be2fb01f 575 if (in_array($lastDayOfMonth, [28, 29]) || $frequency == 31) {
7e66081a 576 $frequency = 0;
577 $interval++;
578 }
be2fb01f 579 $basePaymentDate = [
7e66081a 580 'M' => $month,
581 'd' => $frequency,
582 'Y' => $year,
be2fb01f 583 ];
6a488035 584 }
7e66081a 585
6a488035
TO
586 return CRM_Utils_Date::format(
587 CRM_Utils_Date::intervalAdd(
588 $params['frequency_unit'],
7e66081a 589 $interval,
6a488035
TO
590 $basePaymentDate
591 )
592 );
593 }
594
595 /**
fe482240 596 * Calculate the pledge status.
6a488035 597 *
3a1617b6
TO
598 * @param int $pledgeId
599 * Pledge id.
6a488035 600 *
a6c01b45
CW
601 * @return int
602 * $statusId calculated status id of pledge
6a488035 603 */
00be9182 604 public static function calculatePledgeStatus($pledgeId) {
771b9eff
JP
605 $paymentStatusTypes = CRM_Contribute_PseudoConstant::contributionStatus(NULL, 'name');
606 $pledgeStatusTypes = CRM_Pledge_BAO_Pledge::buildOptions('status_id', 'validate');
6a488035 607
2c0f8c67 608 //return if the pledge is cancelled.
74edda99 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;
2c0f8c67
JP
612 }
613
cc28438b 614 // retrieve all pledge payments for this particular pledge
be2fb01f
CW
615 $allPledgePayments = $allStatus = [];
616 $returnProperties = ['status_id'];
6a488035
TO
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)) {
01dac399 625 $statusId = array_search('Overdue', $pledgeStatusTypes);
6a488035
TO
626 }
627 elseif (array_search('Completed', $allStatus)) {
628 if (count(array_count_values($allStatus)) == 1) {
01dac399 629 $statusId = array_search('Completed', $pledgeStatusTypes);
6a488035
TO
630 }
631 else {
01dac399 632 $statusId = array_search('In Progress', $pledgeStatusTypes);
6a488035
TO
633 }
634 }
635 else {
01dac399 636 $statusId = array_search('Pending', $pledgeStatusTypes);
6a488035
TO
637 }
638
639 return $statusId;
640 }
641
642 /**
fe482240 643 * Update pledge payment table.
6a488035 644 *
3a1617b6
TO
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.
77b97be7 651 * @param float|int $actualAmount , actual amount being paid
3a1617b6
TO
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?.
77b97be7 656 *
6a488035 657 */
317fceb4 658 public static function updatePledgePayments(
3295515a 659 $pledgeId,
353ffa53
TO
660 $paymentStatusId,
661 $paymentIds = NULL,
662 $actualAmount = 0,
663 $contributionId = NULL,
664 $isScriptUpdate = FALSE
6a488035
TO
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 }
762dc4cf
JP
672 elseif ($paymentStatusId == array_search('Cancelled', $allStatus)) {
673 $completedStatus = array_search('Completed', $allStatus);
674 $paymentClause = " AND civicrm_pledge_payment.status_id != {$completedStatus}";
675 }
6a488035
TO
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 = "
684UPDATE civicrm_pledge_payment
685SET civicrm_pledge_payment.status_id = {$paymentStatusId}
686 {$actualAmountClause} {$contributionIdClause}
687WHERE civicrm_pledge_payment.pledge_id = %1
688 {$paymentClause}
689";
690
be2fb01f 691 CRM_Core_DAO::executeQuery($query, [1 => [$pledgeId, 'Integer']]);
6a488035
TO
692 }
693
694 /**
fe482240 695 * Update pledge payment table when reminder is sent.
6a488035 696 *
3a1617b6
TO
697 * @param int $paymentId
698 * Payment id.
6a488035 699 */
00be9182 700 public static function updateReminderDetails($paymentId) {
6a488035
TO
701 $query = "
702UPDATE civicrm_pledge_payment
703SET civicrm_pledge_payment.reminder_date = CURRENT_TIMESTAMP,
704 civicrm_pledge_payment.reminder_count = civicrm_pledge_payment.reminder_count + 1
705WHERE civicrm_pledge_payment.id = {$paymentId}
706";
707 $dao = CRM_Core_DAO::executeQuery($query);
708 }
709
710 /**
fe482240 711 * Get oldest pending or in progress pledge payments.
6a488035 712 *
3a1617b6
TO
713 * @param int $pledgeID
714 * Pledge id.
6a488035 715 *
2a6da8d7
EM
716 * @param int $limit
717 *
a6c01b45
CW
718 * @return array
719 * associated array of pledge details
6a488035 720 */
00be9182 721 public static function getOldestPledgePayment($pledgeID, $limit = 1) {
cc28438b 722 // get pending / overdue statuses
01dac399
JP
723 $pledgeStatuses = CRM_Core_OptionGroup::values('pledge_status',
724 FALSE, FALSE, FALSE, NULL, 'name'
725 );
6a488035 726
cc28438b 727 // get pending and overdue payments
6a488035
TO
728 $status[] = array_search('Pending', $pledgeStatuses);
729 $status[] = array_search('Overdue', $pledgeStatuses);
730
731 $statusClause = " IN (" . implode(',', $status) . ")";
732
733 $query = "
5542b7c2 734SELECT 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
6a488035
TO
735FROM civicrm_pledge, civicrm_pledge_payment
736WHERE civicrm_pledge.id = civicrm_pledge_payment.pledge_id
737 AND civicrm_pledge_payment.status_id {$statusClause}
738 AND civicrm_pledge.id = %1
739ORDER BY civicrm_pledge_payment.scheduled_date ASC
740LIMIT 0, %2
741";
742
be2fb01f
CW
743 $params[1] = [$pledgeID, 'Integer'];
744 $params[2] = [$limit, 'Integer'];
64041b14 745 $payment = CRM_Core_DAO::executeQuery($query, $params);
746 $count = 1;
be2fb01f 747 $paymentDetails = [];
6a488035 748 while ($payment->fetch()) {
be2fb01f 749 $paymentDetails[] = [
6a488035
TO
750 'id' => $payment->id,
751 'amount' => $payment->amount,
752 'currency' => $payment->currency,
9fa00ed1 753 'schedule_date' => $payment->scheduled_date,
5542b7c2 754 'financial_type_id' => $payment->financial_type_id,
6a488035 755 'count' => $count,
be2fb01f 756 ];
6a488035
TO
757 $count++;
758 }
759 return end($paymentDetails);
760 }
761
ffd93213 762 /**
100fef9d 763 * @param int $pledgeID
ffd93213
EM
764 * @param $actualAmount
765 * @param $pledgeScheduledAmount
100fef9d
CW
766 * @param int $paymentContributionId
767 * @param int $pPaymentId
768 * @param int $paymentStatusID
ffd93213 769 */
00be9182 770 public static function adjustPledgePayment($pledgeID, $actualAmount, $pledgeScheduledAmount, $paymentContributionId = NULL, $pPaymentId = NULL, $paymentStatusID = NULL) {
6a488035 771 $allStatus = CRM_Contribute_PseudoConstant::contributionStatus(NULL, 'name');
36f46ce7 772 $paymentStatusName = CRM_Core_PseudoConstant::getName('CRM_Pledge_BAO_PledgePayment', 'status_id', $paymentStatusID);
773 if ($paymentStatusName == 'Cancelled'|| $paymentStatusName == 'Refunded') {
a9ae3b2c
DG
774 $query = "
775SELECT civicrm_pledge_payment.id id
776FROM civicrm_pledge_payment
777WHERE civicrm_pledge_payment.contribution_id = {$paymentContributionId}
778";
779 $paymentsAffected = CRM_Core_DAO::executeQuery($query);
be2fb01f 780 $paymentIDs = [];
a9ae3b2c
DG
781 while ($paymentsAffected->fetch()) {
782 $paymentIDs[] = $paymentsAffected->id;
783 }
2efcf0c2 784 // Reset the affected values by the amount paid more than the scheduled amount
64041b14 785 foreach ($paymentIDs as $key => $value) {
a9ae3b2c
DG
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 }
2efcf0c2 798
cc28438b 799 // Cancel the initial paid amount
a9ae3b2c
DG
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');
2efcf0c2 802
cc28438b 803 // Add new payment after the last payment for the pledge
a9ae3b2c
DG
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');
64041b14 809 $pledgeScheduledDate = $lastPayment['scheduled_date'];
a9ae3b2c
DG
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));
be2fb01f 816 $pledgeParams = [
a9ae3b2c
DG
817 'status_id' => array_search('Pending', $allStatus),
818 'pledge_id' => $pledgeID,
819 'scheduled_amount' => $pledgeScheduledAmount,
820 'scheduled_date' => $ScheduledDate,
be2fb01f 821 ];
a9ae3b2c
DG
822 $payment = self::add($pledgeParams);
823 }
824 else {
c8ab305b 825 $nextPledgeInstallmentDue = self::getOldestPledgePayment($pledgeID);
64041b14 826 if (!$paymentContributionId) {
827 // means we are editing payment scheduled payment, so get the second pending to update.
c8ab305b 828 $nextPledgeInstallmentDue = self::getOldestPledgePayment($pledgeID, 2);
829 if (($nextPledgeInstallmentDue['count'] != 1) && ($nextPledgeInstallmentDue['id'] == $pPaymentId)) {
830 $nextPledgeInstallmentDue = self::getOldestPledgePayment($pledgeID);
64041b14 831 }
6a488035 832 }
6a488035 833
c8ab305b 834 if ($nextPledgeInstallmentDue) {
64041b14 835 // not the last scheduled payment and the actual amount is less than the expected , add it to oldest pending.
c8ab305b 836 if (($actualAmount != $pledgeScheduledAmount) && (($actualAmount < $pledgeScheduledAmount) || (($actualAmount - $pledgeScheduledAmount) < $nextPledgeInstallmentDue['amount']))) {
837 $oldScheduledAmount = $nextPledgeInstallmentDue['amount'];
64041b14 838 $newScheduledAmount = $oldScheduledAmount + ($pledgeScheduledAmount - $actualAmount);
cc28438b 839 // store new amount in oldest pending payment record.
7ca3c666 840 CRM_Core_DAO::setFieldValue('CRM_Pledge_DAO_PledgePayment',
c8ab305b 841 $nextPledgeInstallmentDue['id'],
7ca3c666 842 'scheduled_amount',
843 $newScheduledAmount
844 );
c8ab305b 845 if (CRM_Core_DAO::getFieldValue('CRM_Pledge_DAO_PledgePayment', $nextPledgeInstallmentDue['id'], 'contribution_id', 'id')) {
7ca3c666 846 CRM_Core_DAO::setFieldValue('CRM_Pledge_DAO_PledgePayment',
c8ab305b 847 $nextPledgeInstallmentDue['id'],
7ca3c666 848 'contribution_id',
849 $paymentContributionId
850 );
851 }
6a488035 852 }
c8ab305b 853 elseif (($actualAmount > $pledgeScheduledAmount) && (($actualAmount - $pledgeScheduledAmount) >= $nextPledgeInstallmentDue['amount'])) {
64041b14 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
be2fb01f 856 $paymentId = [$nextPledgeInstallmentDue['id']];
64041b14 857 self::updatePledgePayments($pledgeID, array_search('Completed', $allStatus), $paymentId, 0, $paymentContributionId);
c8ab305b 858 CRM_Core_DAO::setFieldValue('CRM_Pledge_DAO_PledgePayment', $nextPledgeInstallmentDue['id'], 'scheduled_amount', 0, 'id');
64041b14 859 if (!$paymentContributionId) {
860 // means we are editing payment scheduled payment.
861 $oldestPaymentAmount = self::getOldestPledgePayment($pledgeID, 2);
862 }
31f5f5e4 863 $newActualAmount = round(($actualAmount - $pledgeScheduledAmount), CRM_Utils_Money::getCurrencyPrecision());
c8ab305b 864 $newPledgeScheduledAmount = $nextPledgeInstallmentDue['amount'];
64041b14 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 }
6a488035
TO
878 }
879 }
880 }
881 }
96025800 882
ab6ba136 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 */
be2fb01f 894 public static function buildOptions($fieldName, $context = NULL, $props = []) {
ab6ba136 895 $result = parent::buildOptions($fieldName, $context, $props);
896 if ($fieldName == 'status_id') {
74edda99 897 $result = CRM_Pledge_BAO_Pledge::buildOptions($fieldName, $context, $props);
be2fb01f 898 $result = array_diff($result, ['Failed', 'In Progress']);
ab6ba136 899 }
900 return $result;
901 }
902
6a488035 903}