]> git.agnieray.net Git - galette.git/blob - galette/lib/Galette/Entity/ScheduledPayment.php
Add DateHelper for setting/getting dates
[galette.git] / galette / lib / Galette / Entity / ScheduledPayment.php
1 <?php
2
3 /**
4 * Copyright © 2003-2024 The Galette Team
5 *
6 * This file is part of Galette (https://galette.eu).
7 *
8 * Galette is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * Galette is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with Galette. If not, see <http://www.gnu.org/licenses/>.
20 */
21
22 namespace Galette\Entity;
23
24 use ArrayObject;
25 use DateTime;
26 use Galette\Helpers\EntityHelper;
27 use Laminas\Db\Sql\Expression;
28 use Laminas\Db\Sql\Predicate\IsNull;
29 use Laminas\Db\Sql\Predicate\Operator;
30 use Laminas\Db\Sql\Predicate\PredicateSet;
31 use Throwable;
32 use Galette\Core\Db;
33 use Analog\Analog;
34
35 /**
36 * Scheduled payment
37 *
38 * @author Johan Cwiklinski <johan@x-tnd.be>
39 */
40
41 class ScheduledPayment
42 {
43 use EntityHelper;
44
45 public const TABLE = 'payments_schedules';
46 public const PK = 'id_schedule';
47 private Db $zdb;
48 private int $id;
49 private Contribution $contribution;
50 private PaymentType $payment_type;
51 private string $creation_date;
52 private string $scheduled_date;
53 private float $amount;
54 private bool $is_paid = false;
55 private ?string $comment = null;
56 /** @var string[] */
57 private array $errors = [];
58
59 /**
60 * Main constructor
61 *
62 * @param Db $zdb Database instance
63 * @param ArrayObject<string,int|string>|int|null $args Arguments
64 */
65 public function __construct(Db $zdb, ArrayObject|int $args = null)
66 {
67 $this->zdb = $zdb;
68 $now = new DateTime();
69 $this->creation_date = $now->format('Y-m-d');
70 $this->scheduled_date = $now->format('Y-m-d');
71
72 $this->setFields();
73
74 if (is_int($args)) {
75 $this->load($args);
76 } elseif ($args instanceof ArrayObject) {
77 $this->loadFromRs($args);
78 }
79 }
80
81 /**
82 * Load a scheduled payment from its identifier
83 *
84 * @param integer $id Identifier
85 *
86 * @return bool
87 */
88 public function load(int $id): bool
89 {
90 try {
91 $select = $this->zdb->select(self::TABLE);
92 $select->limit(1)->where([self::PK => $id]);
93
94 $results = $this->zdb->execute($select);
95 $rs = $results->current();
96
97 if (!$rs) {
98 return false;
99 }
100 $this->loadFromRs($rs);
101 return true;
102 } catch (Throwable $e) {
103 Analog::log(
104 'An error occurred loading scheduled payment #' . $id . "Message:\n" .
105 $e->getMessage(),
106 Analog::ERROR
107 );
108 throw $e;
109 }
110 }
111
112 /**
113 * Load scheduled payment from a db ResultSet
114 *
115 * @param ArrayObject<string, int|string> $rs ResultSet
116 *
117 * @return void
118 */
119 private function loadFromRs(ArrayObject $rs): void
120 {
121 global $login;
122
123 $pk = self::PK;
124 $this->id = $rs->$pk;
125 $this->contribution = new Contribution($this->zdb, $login, $rs->{Contribution::PK});
126 $this->payment_type = new PaymentType($this->zdb, $rs->id_paymenttype);
127 $this->creation_date = $rs->creation_date;
128 $this->scheduled_date = $rs->scheduled_date;
129 $this->amount = $rs->amount;
130 $this->is_paid = (bool)$rs->paid;
131 $this->comment = $rs->comment;
132 }
133
134 /**
135 * Check data
136 *
137 * @param array<string,mixed> $data Data
138 *
139 * @return boolean
140 */
141 public function check(array $data): bool
142 {
143 global $login;
144
145 $this->errors = [];
146 $this->contribution = new Contribution($this->zdb, $login);
147
148 if (!isset($data[Contribution::PK]) || !is_numeric($data[Contribution::PK])) {
149 $this->errors[] = _T('Contribution is required');
150 } else {
151 if (!$this->contribution->load($data[Contribution::PK])) {
152 $this->errors[] = _T('Unable to load contribution');
153 } else {
154 if (isset($data['amount'])) {
155 //Amount is not required (will defaults to contribution amount)
156 if (!is_numeric($data['amount']) || $data['amount'] <= 0) {
157 $this->errors[] = _T('Amount must be a positive number');
158 } else {
159 $not_allocated = $this->contribution->amount - $this->getAllocation($this->contribution->id);
160 if (isset($this->id)) {
161 $not_allocated += $this->amount;
162 }
163 if ($data['amount'] > $not_allocated) {
164 $this->errors[] = _T('Amount cannot be greater than non allocated amount');
165 }
166 }
167 }
168 if ($this->contribution->payment_type !== PaymentType::SCHEDULED) {
169 $this->errors[] = _T('Payment type for contribution must be set to scheduled');
170 }
171 }
172 }
173
174 if (!isset($data['id_paymenttype']) || !is_numeric($data['id_paymenttype'])) {
175 $this->errors[] = _T('Payment type is required');
176 } else {
177 //no schedule inception allowed!
178 if ($data['id_paymenttype'] === PaymentType::SCHEDULED) {
179 $this->errors[] = _T('Cannot schedule a scheduled payment!');
180 } else {
181 $this->payment_type = new PaymentType($this->zdb, $data['id_paymenttype']);
182 }
183 }
184
185 if (!isset($data['scheduled_date'])) {
186 $this->errors[] = _T('Scheduled date is required');
187 }
188
189 if (count($this->errors) > 0) {
190 return false;
191 }
192
193 $this
194 ->setContribution($data[Contribution::PK])
195 ->setPaymentType($data['id_paymenttype'])
196 ->setCreationDate($data['creation_date'] ?? date('Y-m-d'))
197 ->setScheduledDate($data['scheduled_date'])
198 ->setAmount($data['amount'] ?? $this->contribution->amount)
199 ->setPaid($data['is_paid'] ?? false)
200 ->setComment($data['comment'] ?? null);
201
202 return count($this->errors) === 0;
203 }
204
205 /**
206 * Store scheduled payment in database
207 *
208 * @return boolean
209 */
210 public function store(): bool
211 {
212 $data = array(
213 Contribution::PK => $this->contribution->id,
214 'id_paymenttype' => $this->payment_type->id,
215 'scheduled_date' => $this->scheduled_date,
216 'amount' => $this->amount,
217 'paid' => ($this->is_paid ? true : ($this->zdb->isPostgres() ? 'false' : 0)),
218 'comment' => $this->comment
219 );
220 try {
221 if (isset($this->id) && $this->id > 0) {
222 $update = $this->zdb->update(self::TABLE);
223 $update->set($data)->where([self::PK => $this->id]);
224 $this->zdb->execute($update);
225 } else {
226 $data['creation_date'] = $this->creation_date;
227 $insert = $this->zdb->insert(self::TABLE);
228 $insert->values($data);
229 $add = $this->zdb->execute($insert);
230 if (!$add->count() > 0) {
231 Analog::log('Not stored!', Analog::ERROR);
232 return false;
233 }
234
235 $this->id = $this->zdb->getLastGeneratedValue($this);
236 }
237 return true;
238 } catch (Throwable $e) {
239 Analog::log(
240 'An error occurred storing shceduled payment: ' . $e->getMessage() .
241 "\n" . print_r($data, true),
242 Analog::ERROR
243 );
244 throw $e;
245 }
246 }
247
248 /**
249 * Remove current
250 *
251 * @return boolean
252 */
253 public function remove(): bool
254 {
255 $id = $this->id;
256
257 try {
258 $delete = $this->zdb->delete(self::TABLE);
259 $delete->where([self::PK => $id]);
260 $this->zdb->execute($delete);
261 Analog::log(
262 'Scheduled Payment #' . $id . ' deleted successfully.',
263 Analog::INFO
264 );
265 return true;
266 } catch (Throwable $e) {
267 Analog::log(
268 'Unable to delete scheduled payment ' . $id . ' | ' . $e->getMessage(),
269 Analog::ERROR
270 );
271 throw $e;
272 }
273 }
274
275 /**
276 * Get identifier
277 *
278 * @return ?int
279 */
280 public function getId(): ?int
281 {
282 return $this->id ?? null;
283 }
284
285 /**
286 * Get contribution
287 *
288 * @return Contribution
289 */
290 public function getContribution(): Contribution
291 {
292 return $this->contribution;
293 }
294
295 /**
296 * Set contribution
297 *
298 * @param int|Contribution $contribution Contribution instance or id
299 *
300 * @return self
301 */
302 public function setContribution(int|Contribution $contribution): self
303 {
304 if (is_int($contribution)) {
305 global $login;
306 try {
307 $contrib = new Contribution($this->zdb, $login);
308 if ($contrib->load($contribution)) {
309 $this->contribution = $contrib;
310 } else {
311 throw new \RuntimeException('Cannot load contribution #' . $contribution);
312 }
313 } catch (Throwable $e) {
314 Analog::log(
315 'Unable to load contribution #' . $contribution . ' | ' . $e->getMessage(),
316 Analog::ERROR
317 );
318 $this->errors[] = _T('Unable to load contribution');
319 }
320 } else {
321 $this->contribution = $contribution;
322 }
323 return $this;
324 }
325
326 /**
327 * Get payment type
328 *
329 * @return PaymentType
330 */
331 public function getPaymentType(): PaymentType
332 {
333 global $preferences;
334
335 return $this->payment_type ?? new PaymentType($this->zdb, $preferences->pref_default_paymenttype);
336 }
337
338 /**
339 * Set payment type
340 *
341 * @param int|PaymentType $payment_type Payment type instance or id
342 *
343 * @return self
344 */
345 public function setPaymentType(int|PaymentType $payment_type): self
346 {
347 if (is_int($payment_type)) {
348 try {
349 $ptype = new PaymentType($this->zdb);
350 if ($ptype->load($payment_type)) {
351 $this->payment_type = $ptype;
352 } else {
353 throw new \RuntimeException('Cannot load payment type #' . $payment_type);
354 }
355 } catch (Throwable $e) {
356 Analog::log(
357 'Unable to load payment type #' . $payment_type . ' | ' . $e->getMessage(),
358 Analog::ERROR
359 );
360 $this->errors[] = _T('Unable to load payment type');
361 }
362 } else {
363 $this->payment_type = $payment_type;
364 }
365
366 return $this;
367 }
368
369 /**
370 * Get creation date
371 *
372 * @param bool $formatted Get formatted date, or DateTime object
373 *
374 * @return string|DateTime|null
375 */
376 public function getCreationDate(bool $formatted = true): string|DateTime|null
377 {
378 return $this->getDate('creation_date', $formatted);
379 }
380
381 /**
382 * Set creation date
383 *
384 * @param string $creation_date Creation date
385 *
386 * @return self
387 */
388 public function setCreationDate(string $creation_date): self
389 {
390 $this->setDate('creation_date', $creation_date);
391 return $this;
392 }
393
394 /**
395 * Get scheduled date
396 *
397 * @param bool $formatted Get formatted date, or DateTime object
398 *
399 * @return string|DateTime|null
400 */
401 public function getScheduledDate(bool $formatted = true): string|DateTime|null
402 {
403 return $this->getDate('scheduled_date', $formatted);
404 }
405
406 /**
407 * Set scheduled date
408 *
409 * @param string $scheduled_date Scheduled date
410 *
411 * @return self
412 */
413 public function setScheduledDate(string $scheduled_date): self
414 {
415 $this->setDate('scheduled_date', $scheduled_date);
416 return $this;
417 }
418
419 /**
420 * Get amount
421 *
422 * @return float
423 */
424 public function getAmount(): ?float
425 {
426 return $this->amount ?? null;
427 }
428
429 /**
430 * Set amount
431 *
432 * @param float $amount Amount
433 *
434 * @return self
435 */
436 public function setAmount(float $amount): self
437 {
438 $this->amount = $amount;
439 return $this;
440 }
441
442 /**
443 * Is payment done?
444 *
445 * @return bool
446 */
447 public function isPaid(): bool
448 {
449 return $this->is_paid;
450 }
451
452 /**
453 * Set paid
454 *
455 * @param bool $is_paid Paid status
456 *
457 * @return self
458 */
459 public function setPaid(bool $is_paid = true): self
460 {
461 $this->is_paid = $is_paid;
462 return $this;
463 }
464
465 /**
466 * Get comment
467 *
468 * @return string
469 */
470 public function getComment(): ?string
471 {
472 return $this->comment;
473 }
474
475 /**
476 * Set comment
477 *
478 * @param ?string $comment Comment
479 *
480 * @return self
481 */
482 public function setComment(?string $comment): self
483 {
484 $this->comment = $comment;
485 return $this;
486 }
487
488 /**
489 * Is a contribution handled from a scheduled payment?
490 *
491 * @param int $id_cotis Contribution identifier
492 *
493 * @return bool
494 * @throws Throwable
495 */
496 public function isContributionHandled(int $id_cotis): bool
497 {
498 $select = $this->zdb->select(self::TABLE);
499 $select->limit(1)->where([Contribution::PK => $id_cotis]);
500
501 $results = $this->zdb->execute($select);
502 return ($results->count() > 0);
503 }
504
505 /**
506 * Get allocated amount
507 *
508 * @param int $id_cotis Contribution identifier
509 *
510 * @return float
511 * @throws Throwable
512 */
513 public function getAllocation(int $id_cotis): float
514 {
515 $select = $this->zdb->select(self::TABLE);
516 $select->columns(['allocation' => new Expression('SUM(amount)')]);
517 $select->where([Contribution::PK => $id_cotis]);
518
519 $results = $this->zdb->execute($select);
520 $result = $results->current();
521 return $result->allocation ?? 0;
522 }
523
524 /**
525 * Get allocated amount for current contribution
526 *
527 * @return float
528 * @throws Throwable
529 */
530 public function getAllocated(): float
531 {
532 return $this->getAllocation($this->contribution->id);
533 }
534
535 /**
536 * Get missing amount
537 *
538 * @return float
539 */
540 public function getMissingAmount(): float
541 {
542 return $this->contribution->amount - $this->getAllocated();
543 }
544
545 /**
546 * Is scheduled payment fully allocated?
547 *
548 * @param Contribution $contrib Contribution
549 *
550 * @return bool
551 */
552 public function isFullyAllocated(Contribution $contrib): bool
553 {
554 return !($this->getAllocation($contrib->id) < $contrib->amount);
555 }
556
557 /**
558 * Get not fully allocated scheduled payments
559 *
560 * @return Contribution[]
561 */
562 public function getNotFullyAllocated(): array
563 {
564 $select = $this->zdb->select(Contribution::TABLE, 'c');
565 $select->columns([Contribution::PK, 'montant_cotis']);
566 $select->quantifier('DISTINCT');
567
568 $select->join(
569 array('s' => PREFIX_DB . self::TABLE),
570 //$on,
571 'c.' . Contribution::PK . '=s.' . Contribution::PK,
572 array('allocated' => new Expression('SUM(s.amount)')),
573 $select::JOIN_LEFT
574 );
575
576 $select->group('c.' . Contribution::PK);
577 $select->where(['c.type_paiement_cotis' => PaymentType::SCHEDULED]);
578 $select->having([
579 new PredicateSet(
580 array(
581 new Operator(
582 /** @phpstan-ignore-next-line */
583 new \Laminas\Db\Sql\Predicate\Expression('SUM(s.amount)'),
584 '<',
585 new \Laminas\Db\Sql\Predicate\Expression('c.montant_cotis')
586 ),
587 /** @phpstan-ignore-next-line */
588 new IsNull(new \Laminas\Db\Sql\Predicate\Expression('SUM(s.amount)'))
589 ),
590 PredicateSet::OP_OR
591 )
592 ]);
593
594 $results = $this->zdb->execute($select);
595
596 return $results->toArray();
597 }
598
599 /**
600 * Get errors
601 *
602 * @return string[]
603 */
604 public function getErrors(): array
605 {
606 return $this->errors;
607 }
608
609 /**
610 * Set fields, must populate $this->fields
611 *
612 * @return self
613 */
614 protected function setFields(): self
615 {
616 $this->fields = array(
617 self::PK => array(
618 'label' => _T('Scheduled payment ID'), //not a field in the form
619 'propname' => 'id'
620 ),
621 Contribution::PK => array(
622 'label' => _T('Contribution ID'), //not a field in the form
623 'propname' => 'contribution'
624 ),
625 'id_paymenttype' => array(
626 'label' => _T('Payment type'),
627 'propname' => 'payment_type'
628 ),
629 'creation_date' => array(
630 'label' => _T('Creation date'),
631 'propname' => 'creation_date'
632 ),
633 'scheduled_date' => array(
634 'label' => _T('Scheduled date'),
635 'propname' => 'scheduled_date'
636 ),
637 'amount' => array(
638 'label' => _T('Amount'),
639 'propname' => 'amount'
640 ),
641 'paid' => array(
642 'label' => _T('Paid'),
643 'propname' => 'is_paid'
644 ),
645 'comment' => array(
646 'label' => _T('Comment'),
647 'propname' => 'comment'
648 )
649 );
650
651 return $this;
652 }
653 }