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