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