4 * Copyright © 2003-2024 The Galette Team
6 * This file is part of Galette (https://galette.eu).
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.
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.
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/>.
22 namespace Galette\Entity
;
26 use Galette\Features\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
;
38 * @author Johan Cwiklinski <johan@x-tnd.be>
41 class ScheduledPayment
45 public const TABLE
= 'payments_schedules';
46 public const PK
= 'id_schedule';
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;
57 private array $errors = [];
62 * @param Db $zdb Database instance
63 * @param ArrayObject<string,int|string>|int|null $args Arguments
65 public function __construct(Db
$zdb, ArrayObject|
int $args = null)
68 $now = new DateTime();
69 $this->creation_date
= $now->format('Y-m-d');
70 $this->scheduled_date
= $now->format('Y-m-d');
76 } elseif ($args instanceof ArrayObject
) {
77 $this->loadFromRs($args);
82 * Load a scheduled payment from its identifier
84 * @param integer $id Identifier
88 public function load(int $id): bool
91 $select = $this->zdb
->select(self
::TABLE
);
92 $select->limit(1)->where([self
::PK
=> $id]);
94 $results = $this->zdb
->execute($select);
95 $rs = $results->current();
100 $this->loadFromRs($rs);
102 } catch (Throwable
$e) {
104 'An error occurred loading scheduled payment #' . $id . "Message:\n" .
113 * Load scheduled payment from a db ResultSet
115 * @param ArrayObject<string, int|string> $rs ResultSet
119 private function loadFromRs(ArrayObject
$rs): void
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
;
137 * @param array<string,mixed> $data Data
141 public function check(array $data): bool
146 $this->contribution
= new Contribution($this->zdb
, $login);
148 if (!isset($data[Contribution
::PK
]) ||
!is_numeric($data[Contribution
::PK
])) {
149 $this->errors
[] = _T('Contribution is required');
151 if (!$this->contribution
->load($data[Contribution
::PK
])) {
152 $this->errors
[] = _T('Unable to load contribution');
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');
159 $not_allocated = $this->contribution
->amount
- $this->getAllocation($this->contribution
->id
);
160 if (isset($this->id
)) {
161 $not_allocated +
= $this->amount
;
163 if ($data['amount'] > $not_allocated) {
164 $this->errors
[] = _T('Amount cannot be greater than non allocated amount');
168 if ($this->contribution
->payment_type
!== PaymentType
::SCHEDULED
) {
169 $this->errors
[] = _T('Payment type for contribution must be set to scheduled');
174 if (!isset($data['id_paymenttype']) ||
!is_numeric($data['id_paymenttype'])) {
175 $this->errors
[] = _T('Payment type is required');
177 //no schedule inception allowed!
178 if ($data['id_paymenttype'] === PaymentType
::SCHEDULED
) {
179 $this->errors
[] = _T('Cannot schedule a scheduled payment!');
181 $this->payment_type
= new PaymentType($this->zdb
, $data['id_paymenttype']);
185 if (!isset($data['scheduled_date'])) {
186 $this->errors
[] = _T('Scheduled date is required');
189 if (count($this->errors
) > 0) {
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);
202 return count($this->errors
) === 0;
206 * Store scheduled payment in database
210 public function store(): bool
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
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);
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
);
235 $this->id
= $this->zdb
->getLastGeneratedValue($this);
238 } catch (Throwable
$e) {
240 'An error occurred storing shceduled payment: ' . $e->getMessage() .
241 "\n" . print_r($data, true),
253 public function remove(): bool
258 $delete = $this->zdb
->delete(self
::TABLE
);
259 $delete->where([self
::PK
=> $id]);
260 $this->zdb
->execute($delete);
262 'Scheduled Payment #' . $id . ' deleted successfully.',
266 } catch (Throwable
$e) {
268 'Unable to delete scheduled payment ' . $id . ' | ' . $e->getMessage(),
280 public function getId(): ?
int
282 return $this->id ??
null;
288 * @return Contribution
290 public function getContribution(): Contribution
292 return $this->contribution
;
298 * @param int|Contribution $contribution Contribution instance or id
302 public function setContribution(int|Contribution
$contribution): self
304 if (is_int($contribution)) {
307 $contrib = new Contribution($this->zdb
, $login);
308 if ($contrib->load($contribution)) {
309 $this->contribution
= $contrib;
311 throw new \
RuntimeException('Cannot load contribution #' . $contribution);
313 } catch (Throwable
$e) {
315 'Unable to load contribution #' . $contribution . ' | ' . $e->getMessage(),
318 $this->errors
[] = _T('Unable to load contribution');
321 $this->contribution
= $contribution;
329 * @return PaymentType
331 public function getPaymentType(): PaymentType
335 return $this->payment_type ??
new PaymentType($this->zdb
, $preferences->pref_default_paymenttype
);
341 * @param int|PaymentType $payment_type Payment type instance or id
345 public function setPaymentType(int|PaymentType
$payment_type): self
347 if (is_int($payment_type)) {
349 $ptype = new PaymentType($this->zdb
);
350 if ($ptype->load($payment_type)) {
351 $this->payment_type
= $ptype;
353 throw new \
RuntimeException('Cannot load payment type #' . $payment_type);
355 } catch (Throwable
$e) {
357 'Unable to load payment type #' . $payment_type . ' | ' . $e->getMessage(),
360 $this->errors
[] = _T('Unable to load payment type');
363 $this->payment_type
= $payment_type;
372 * @param bool $formatted Get formatted date, or DateTime object
374 * @return string|DateTime|null
376 public function getCreationDate(bool $formatted = true): string|DateTime|
null
378 return $this->getDate('creation_date', $formatted);
384 * @param string $creation_date Creation date
388 public function setCreationDate(string $creation_date): self
390 $this->setDate('creation_date', $creation_date);
397 * @param bool $formatted Get formatted date, or DateTime object
399 * @return string|DateTime|null
401 public function getScheduledDate(bool $formatted = true): string|DateTime|
null
403 return $this->getDate('scheduled_date', $formatted);
409 * @param string $scheduled_date Scheduled date
413 public function setScheduledDate(string $scheduled_date): self
415 $this->setDate('scheduled_date', $scheduled_date);
424 public function getAmount(): ?
float
426 return $this->amount ??
null;
432 * @param float $amount Amount
436 public function setAmount(float $amount): self
438 $this->amount
= $amount;
447 public function isPaid(): bool
449 return $this->is_paid
;
455 * @param bool $is_paid Paid status
459 public function setPaid(bool $is_paid = true): self
461 $this->is_paid
= $is_paid;
470 public function getComment(): ?
string
472 return $this->comment
;
478 * @param ?string $comment Comment
482 public function setComment(?
string $comment): self
484 $this->comment
= $comment;
489 * Is a contribution handled from a scheduled payment?
491 * @param int $id_cotis Contribution identifier
496 public function isContributionHandled(int $id_cotis): bool
498 $select = $this->zdb
->select(self
::TABLE
);
499 $select->limit(1)->where([Contribution
::PK
=> $id_cotis]);
501 $results = $this->zdb
->execute($select);
502 return ($results->count() > 0);
506 * Get allocated amount
508 * @param int $id_cotis Contribution identifier
513 public function getAllocation(int $id_cotis): float
515 $select = $this->zdb
->select(self
::TABLE
);
516 $select->columns(['allocation' => new Expression('SUM(amount)')]);
517 $select->where([Contribution
::PK
=> $id_cotis]);
519 $results = $this->zdb
->execute($select);
520 $result = $results->current();
521 return $result->allocation ??
0;
525 * Get allocated amount for current contribution
530 public function getAllocated(): float
532 return $this->getAllocation($this->contribution
->id
);
540 public function getMissingAmount(): float
542 return $this->contribution
->amount
- $this->getAllocated();
546 * Is scheduled payment fully allocated?
548 * @param Contribution $contrib Contribution
552 public function isFullyAllocated(Contribution
$contrib): bool
554 return !($this->getAllocation($contrib->id
) < $contrib->amount
);
558 * Get not fully allocated scheduled payments
560 * @return Contribution[]
562 public function getNotFullyAllocated(): array
564 $select = $this->zdb
->select(Contribution
::TABLE
, 'c');
565 $select->columns([Contribution
::PK
, 'montant_cotis']);
566 $select->quantifier('DISTINCT');
569 array('s' => PREFIX_DB
. self
::TABLE
),
571 'c.' . Contribution
::PK
. '=s.' . Contribution
::PK
,
572 array('allocated' => new Expression('SUM(s.amount)')),
576 $select->group('c.' . Contribution
::PK
);
577 $select->where(['c.type_paiement_cotis' => PaymentType
::SCHEDULED
]);
582 /** @phpstan-ignore-next-line */
583 new \Laminas\Db\Sql\Predicate\
Expression('SUM(s.amount)'),
585 new \Laminas\Db\Sql\Predicate\
Expression('c.montant_cotis')
587 /** @phpstan-ignore-next-line */
588 new IsNull(new \Laminas\Db\Sql\Predicate\
Expression('SUM(s.amount)'))
594 $results = $this->zdb
->execute($select);
596 return $results->toArray();
604 public function getErrors(): array
606 return $this->errors
;
610 * Set fields, must populate $this->fields
614 protected function setFields(): self
616 $this->fields
= array(
618 'label' => _T('Scheduled payment ID'), //not a field in the form
621 Contribution
::PK
=> array(
622 'label' => _T('Contribution ID'), //not a field in the form
623 'propname' => 'contribution'
625 'id_paymenttype' => array(
626 'label' => _T('Payment type'),
627 'propname' => 'payment_type'
629 'creation_date' => array(
630 'label' => _T('Creation date'),
631 'propname' => 'creation_date'
633 'scheduled_date' => array(
634 'label' => _T('Scheduled date'),
635 'propname' => 'scheduled_date'
638 'label' => _T('Amount'),
639 'propname' => 'amount'
642 'label' => _T('Paid'),
643 'propname' => 'is_paid'
646 'label' => _T('Comment'),
647 'propname' => 'comment'