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