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
;
25 use Galette\Events\GaletteEvent
;
26 use Galette\Repository\PaymentTypes
;
29 use Laminas\Db\Sql\Expression
;
30 use Galette\Repository\Contributions
;
32 use Galette\Core\History
;
33 use Galette\Core\Login
;
34 use Galette\Features\Dynamics
;
37 * Transaction class for galette
39 * @author Johan Cwiklinski <johan@x-tnd.be>
41 * @property integer $id
42 * @property date $date
43 * @property integer $amount
44 * @property ?string $description
45 * @property ?integer $member
46 * @property ?integer $payment_type
52 public const TABLE
= 'transactions';
53 public const PK
= 'trans_id';
56 private string $_date;
57 private float $_amount;
58 private ?
string $_description = null;
59 private ?
int $_member = null;
60 private ?
int $_payment_type = null;
63 * fields list and their translation
65 * @var array<string, array<string, string>>
67 private array $_fields;
72 /** @var array<string> */
73 private array $errors;
78 * @param Db $zdb Database instance
79 * @param Login $login Login instance
80 * @param null|int|ArrayObject<string, int|string> $args Either a ResultSet row or its id for to load
81 * a specific transaction, or null to just
84 public function __construct(Db
$zdb, Login
$login, ArrayObject|
int|
null $args = null)
87 $this->login
= $login;
90 * Fields configuration. Each field is an array and must reflect:
96 * I'd prefer a static private variable for this...
97 * But call to the _T function does not seem to be allowed there :/
99 $this->_fields
= array(
101 'label' => null, //not a field in the form
104 'trans_date' => array(
105 'label' => _T("Date:"), //not a field in the form
108 'trans_amount' => array(
109 'label' => _T("Amount:"),
110 'propname' => 'amount'
112 'trans_desc' => array(
113 'label' => _T("Description:"),
114 'propname' => 'description'
116 Adherent
::PK
=> array(
117 'label' => _T("Originator:"),
118 'propname' => 'member'
120 'type_paiement_trans' => array(
121 'label' => _T("Payment type:"),
122 'propname' => 'payment_type'
125 if ($args === null ||
is_int($args)) {
126 $this->_date
= date("Y-m-d");
128 if (is_int($args) && $args > 0) {
131 } elseif ($args instanceof ArrayObject
) {
132 $this->loadFromRS($args);
135 $this->loadDynamicFields();
139 * Loads a transaction from its id
141 * @param int $id the identifier for the transaction to load
143 * @return bool true if query succeed, false otherwise
145 public function load(int $id): bool
147 if (!$this->login
->isLogged()) {
149 'Non-logged-in users cannot load transaction id `' . $id,
156 $select = $this->zdb
->select(self
::TABLE
, 't');
157 $select->where([self
::PK
=> $id]);
159 array('a' => PREFIX_DB
. Adherent
::TABLE
),
160 't.' . Adherent
::PK
. '=a.' . Adherent
::PK
,
164 //restrict query on current member id if he's not admin nor staff member
165 if (!$this->login
->isAdmin() && !$this->login
->isStaff()) {
168 ->equalTo('a.' . Adherent
::PK
, $this->login
->id
)
170 ->equalTo('a.parent_id', $this->login
->id
)
173 ->equalTo('t.' . self
::PK
, $id)
176 $select->where
->equalTo(self
::PK
, $id);
179 $results = $this->zdb
->execute($select);
180 if ($results->count() > 0) {
181 /** @var ArrayObject<string, int|string> $result */
182 $result = $results->current();
183 $this->loadFromRS($result);
187 'Transaction id `' . $id . '` does not exists',
192 } catch (Throwable
$e) {
194 'Cannot load transaction form id `' . $id . '` | ' .
203 * Remove transaction (and all associated contributions) from database
205 * @param History $hist History
206 * @param boolean $transaction Activate transaction mode (defaults to true)
210 public function remove(History
$hist, bool $transaction = true): bool
216 $this->zdb
->connection
->beginTransaction();
219 //remove associated contributions if needeed
220 if ($this->getDispatchedAmount() > 0) {
221 $c = new Contributions($this->zdb
, $this->login
);
222 $clist = $c->getListFromTransaction($this->_id
);
224 foreach ($clist as $cid) {
227 $c->remove($cids, $hist, false);
230 //remove transaction itself
231 $delete = $this->zdb
->delete(self
::TABLE
);
232 $delete->where([self
::PK
=> $this->_id
]);
233 $del = $this->zdb
->execute($delete);
234 if ($del->count() > 0) {
235 $this->dynamicsRemove(true);
238 'Transaction has not been removed!',
245 $this->zdb
->connection
->commit();
248 $emitter->dispatch(new GaletteEvent('transaction.remove', $this));
250 } catch (Throwable
$e) {
252 $this->zdb
->connection
->rollBack();
255 'An error occurred trying to remove transaction #' .
256 $this->_id
. ' | ' . $e->getMessage(),
264 * Populate object from a resultset row
266 * @param ArrayObject<string,int|string> $r the resultset row
270 private function loadFromRS(ArrayObject
$r): void
273 $this->_id
= $r->$pk;
274 $this->_date
= $r->trans_date
;
275 $this->_amount
= $r->trans_amount
;
276 $this->_description
= $r->trans_desc
;
277 $adhpk = Adherent
::PK
;
278 $this->_member
= (int)$r->$adhpk;
279 $this->_payment_type
= $r->type_paiement_trans
;
281 $this->loadDynamicFields();
285 * Check posted values validity
287 * @param array<string,mixed> $values All values to check, basically the $_POST array
288 * after sending the form
289 * @param array<string,int> $required Array of required fields
290 * @param array<string> $disabled Array of disabled fields
292 * @return true|array<string>
294 public function check(array $values, array $required, array $disabled): bool|
array
297 $this->errors
= array();
299 $fields = array_keys($this->_fields
);
300 foreach ($fields as $key) {
301 //first, let's sanitize values
302 $key = strtolower($key);
303 $prop = '_' . $this->_fields
[$key]['propname'];
305 if (isset($values[$key])) {
306 $value = trim($values[$key]);
311 // if the field is enabled, check it
312 if (!isset($disabled[$key])) {
313 // now, check validity
319 $d = \DateTime
::createFromFormat(__("Y-m-d"), $value);
321 //try with non localized date
322 $d = \DateTime
::createFromFormat("Y-m-d", $value);
324 throw new \
Exception('Incorrect format');
327 $this->$prop = $d->format('Y-m-d');
328 } catch (Throwable
$e) {
330 'Wrong date format. field: ' . $key .
331 ', value: ' . $value . ', expected fmt: ' .
332 __("Y-m-d") . ' | ' . $e->getMessage(),
335 $this->errors
[] = sprintf(
336 //TRANS: %1$s is the date format, %2$s is the field name
337 _T('- Wrong date format (%1$s) for %2$s!'),
339 $this->getFieldLabel($key)
344 $this->_member
= (int)$value;
347 $value = strtr($value, ',', '.');
348 $this->_amount
= (double)$value;
349 if (!is_numeric($value)) {
350 $this->errors
[] = _T("- The amount must be an integer!");
354 /** TODO: retrieve field length from database and check that */
355 $this->_description
= strip_tags($value);
356 if (mb_strlen($value) > 150) {
357 $this->errors
[] = _T("- Transaction description must be 150 characters long maximum.");
360 case 'type_paiement_trans':
364 $ptypes = new PaymentTypes(
369 $ptlist = $ptypes->getList();
370 if (isset($ptlist[$value])) {
371 $this->_payment_type
= (int)$value;
373 $this->errors
[] = _T("- Unknown payment type");
381 // missing required fields?
382 foreach ($required as $key => $val) {
384 $prop = '_' . $this->_fields
[$key]['propname'];
385 if (!isset($disabled[$key]) && !isset($this->$prop)) {
386 $this->errors
[] = str_replace(
388 '<a href="#' . $key . '">' . $this->getFieldLabel($key) . '</a>',
389 _T("- Mandatory field %field empty.")
395 if (isset($this->_id
)) {
396 $dispatched = $this->getDispatchedAmount();
397 if ($dispatched > $this->_amount
) {
398 $this->errors
[] = _T("- Sum of all contributions exceed corresponding transaction amount.");
402 $this->dynamicsCheck($values, $required, $disabled);
404 if (count($this->errors
) > 0) {
406 'Some errors has been thew attempting to edit/store a transaction' .
407 print_r($this->errors
, true),
410 return $this->errors
;
413 'Transaction checked successfully.',
421 * Store the transaction
423 * @param History $hist History
427 public function store(History
$hist): bool
434 $this->zdb
->connection
->beginTransaction();
436 $fields = $this->getDbFields($this->zdb
);
438 foreach ($fields as $field) {
439 $prop = '_' . $this->_fields
[$field]['propname'];
440 if (isset($this->$prop)) {
441 $values[$field] = $this->$prop;
445 if (!isset($this->_id
) ||
$this->_id
== '') {
446 //we're inserting a new transaction
447 unset($values[self
::PK
]);
448 $insert = $this->zdb
->insert(self
::TABLE
);
449 $insert->values($values);
450 $add = $this->zdb
->execute($insert);
451 if ($add->count() > 0) {
452 $this->_id
= $this->zdb
->getLastGeneratedValue($this);
456 _T("Transaction added"),
457 Adherent
::getSName($this->zdb
, $this->_member
)
459 $event = 'transaction.add';
461 $hist->add(_T("Fail to add new transaction."));
462 throw new \
RuntimeException(
463 'An error occurred inserting new transaction!'
467 //we're editing an existing transaction
468 $update = $this->zdb
->update(self
::TABLE
);
469 $update->set($values)->where([self
::PK
=> $this->_id
]);
470 $edit = $this->zdb
->execute($update);
471 //edit == 0 does not mean there were an error, but that there
472 //were nothing to change
473 if ($edit->count() > 0) {
475 _T("Transaction updated"),
476 Adherent
::getSName($this->zdb
, $this->_member
)
479 $event = 'transaction.edit';
483 $this->dynamicsStore(true);
485 $this->zdb
->connection
->commit();
487 //send event at the end of process, once all has been stored
488 if ($event !== null) {
489 $emitter->dispatch(new GaletteEvent($event, $this));
493 } catch (Throwable
$e) {
494 $this->zdb
->connection
->rollBack();
496 'Something went wrong :\'( | ' . $e->getMessage() . "\n" .
497 $e->getTraceAsString(),
505 * Retrieve amount that has already been dispatched into contributions
509 public function getDispatchedAmount(): float
511 if (empty($this->_id
)) {
516 $select = $this->zdb
->select(Contribution
::TABLE
);
519 'sum' => new Expression('SUM(montant_cotis)')
521 )->where([self
::PK
=> $this->_id
]);
523 $results = $this->zdb
->execute($select);
524 $result = $results->current();
525 $dispatched_amount = $result->sum
;
526 return (double)$dispatched_amount;
527 } catch (Throwable
$e) {
529 'An error occurred retrieving dispatched amounts | ' .
538 * Retrieve amount that has not yet been dispatched into contributions
542 public function getMissingAmount(): float
544 if (empty($this->_id
)) {
545 return (double)$this->amount
;
549 $select = $this->zdb
->select(Contribution
::TABLE
);
552 'sum' => new Expression('SUM(montant_cotis)')
554 )->where([self
::PK
=> $this->_id
]);
556 $results = $this->zdb
->execute($select);
557 $result = $results->current();
558 $dispatched_amount = $result->sum
;
559 return (double)$this->_amount
- (double)$dispatched_amount;
560 } catch (Throwable
$e) {
562 'An error occurred retrieving missing amounts | ' .
571 * Get payment type label
575 public function getPaymentType(): string
577 if ($this->_payment_type
=== null) {
581 $ptype = new PaymentType($this->zdb
, $this->payment_type
);
582 return $ptype->getName();
586 * Retrieve fields from database
588 * @param Db $zdb Database instance
590 * @return array<string>
592 public function getDbFields(Db
$zdb): array
594 $columns = $zdb->getColumns(self
::TABLE
);
596 foreach ($columns as $col) {
597 $fields[] = $col->getName();
603 * Get the relevant CSS class for current transaction
605 * @return string current transaction row class
607 public function getRowClass(): string
609 return ($this->getMissingAmount() == 0) ?
610 'transaction-normal' : 'transaction-uncomplete';
614 * Global getter method
616 * @param string $name name of the property we want to retrieve
618 * @return mixed the called property
620 public function __get(string $name)
622 $forbidden = array();
624 $rname = '_' . $name;
625 if (!in_array($name, $forbidden) && property_exists($this, $rname)) {
628 if ($this->$rname != '') {
630 $d = new \
DateTime($this->$rname);
631 return $d->format(__("Y-m-d"));
632 } catch (Throwable
$e) {
633 //oops, we've got a bad date :/
635 'Bad date (' . $this->$rname . ') | ' .
639 return $this->$rname;
644 if (isset($this->$rname) && $this->$rname !== null) {
645 return (int)$this->$rname;
649 if (isset($this->$rname)) {
650 return (double)$this->$rname;
654 return $this->$rname;
659 'Property %1$s does not exists for transaction',
669 * Global isset method
670 * Required for twig to access properties via __get
672 * @param string $name name of the property we want to retrieve
676 public function __isset(string $name): bool
678 $forbidden = array();
680 $rname = '_' . $name;
681 if (!in_array($name, $forbidden) && property_exists($this, $rname)) {
691 * @param string $field Field name
695 public function getFieldLabel(string $field): string
697 $label = $this->_fields
[$field]['label'];
699 $label = str_replace(' ', ' ', $label);
700 //remove trailing ':' and then trim
701 $label = trim(trim($label, ':'));
706 * Handle files (dynamics files)
708 * @param array<string,mixed> $files Files sent
710 * @return array<string>|true
712 public function handleFiles(array $files)
716 $this->dynamicsFiles($files);
718 if (count($this->errors
) > 0) {
720 'Some errors has been thew attempting to edit/store a transaction files' . "\n" .
721 print_r($this->errors
, true),
724 return $this->errors
;
731 * Can current logged-in user display transaction
733 * @param Login $login Login instance
737 public function canShow(Login
$login): bool
739 //non-logged-in members cannot show contributions
740 if (!$login->isLogged()) {
744 //admin and staff users can edit, as well as member itself
745 if (!$this->id ||
$login->id
== $this->_member ||
$login->isAdmin() ||
$login->isStaff()) {
749 //parent can see their children transactions
750 $parent = new Adherent($this->zdb
);
753 ->enableDep('children')
754 ->load($this->login
->id
);
755 if ($parent->hasChildren()) {
756 foreach ($parent->children
as $child) {
757 if ($child->id
=== $this->_member
) {