]> git.agnieray.net Git - galette.git/blob - galette/lib/Galette/Entity/Transaction.php
Fix transaction loading query
[galette.git] / galette / lib / Galette / Entity / Transaction.php
1 <?php
2
3 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
4
5 /**
6 * Transaction class for galette
7 *
8 * PHP version 5
9 *
10 * Copyright © 2011-2021 The Galette Team
11 *
12 * This file is part of Galette (http://galette.tuxfamily.org).
13 *
14 * Galette is free software: you can redistribute it and/or modify
15 * it under the terms of the GNU General Public License as published by
16 * the Free Software Foundation, either version 3 of the License, or
17 * (at your option) any later version.
18 *
19 * Galette is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
23 *
24 * You should have received a copy of the GNU General Public License
25 * along with Galette. If not, see <http://www.gnu.org/licenses/>.
26 *
27 * @category Entity
28 * @package Galette
29 *
30 * @author Johan Cwiklinski <johan@x-tnd.be>
31 * @copyright 2011-2021 The Galette Team
32 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
33 * @link http://galette.tuxfamily.org
34 * @since Available since 0.7dev - 2011-07-31
35 */
36
37 namespace Galette\Entity;
38
39 use Throwable;
40 use Analog\Analog;
41 use Laminas\Db\Sql\Expression;
42 use Galette\Repository\Contributions;
43 use Galette\Core\Db;
44 use Galette\Core\History;
45 use Galette\Core\Login;
46
47 /**
48 * Transaction class for galette
49 *
50 * @category Entity
51 * @name Transaction
52 * @package Galette
53 * @author Johan Cwiklinski <johan@x-tnd.be>
54 * @copyright 2010-2021 The Galette Team
55 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
56 * @link http://galette.tuxfamily.org
57 * @since Available since 0.7dev - 2010-03-11
58 *
59 * @property integer $id
60 * @property date $date
61 * @property integer $amount
62 * @property string $description
63 * @property integer $member
64 */
65 class Transaction
66 {
67 use DynamicsTrait;
68
69 public const TABLE = 'transactions';
70 public const PK = 'trans_id';
71
72 private $_id;
73 private $_date;
74 private $_amount;
75 private $_description;
76 private $_member;
77
78 //fields list and their translation
79 private $_fields;
80
81 private $zdb;
82 private $login;
83
84 private $errors;
85
86 /**
87 * Default constructor
88 *
89 * @param Db $zdb Database instance
90 * @param Login $login Login instance
91 * @param null|int|ResultSet $args Either a ResultSet row or its id for to load
92 * a specific transaction, or null to just
93 * instantiate object
94 */
95 public function __construct(Db $zdb, Login $login, $args = null)
96 {
97 $this->zdb = $zdb;
98 $this->login = $login;
99
100 /*
101 * Fields configuration. Each field is an array and must reflect:
102 * array(
103 * (string)label,
104 * (string) propname
105 * )
106 *
107 * I'd prefer a static private variable for this...
108 * But call to the _T function does not seem to be allowed there :/
109 */
110 $this->_fields = array(
111 self::PK => array(
112 'label' => null, //not a field in the form
113 'propname' => 'id'
114 ),
115 'trans_date' => array(
116 'label' => _T("Date:"), //not a field in the form
117 'propname' => 'date'
118 ),
119 'trans_amount' => array(
120 'label' => _T("Amount:"),
121 'propname' => 'amount'
122 ),
123 'trans_desc' => array(
124 'label' => _T("Description:"),
125 'propname' => 'description'
126 ),
127 Adherent::PK => array(
128 'label' => _T("Originator:"),
129 'propname' => 'member'
130 )
131 );
132 if ($args == null || is_int($args)) {
133 $this->_date = date("Y-m-d");
134
135 if (is_int($args) && $args > 0) {
136 $this->load($args);
137 }
138 } elseif (is_object($args)) {
139 $this->loadFromRS($args);
140 }
141
142 $this->loadDynamicFields();
143 }
144
145 /**
146 * Loads a transaction from its id
147 *
148 * @param int $id the identifier for the transaction to load
149 *
150 * @return bool true if query succeed, false otherwise
151 */
152 public function load($id)
153 {
154 try {
155 $select = $this->zdb->select(self::TABLE, 't');
156 $select->where(self::PK . ' = ' . $id);
157 $select->join(
158 array('a' => PREFIX_DB . Adherent::TABLE),
159 't.' . Adherent::PK . '=a.' . Adherent::PK,
160 array()
161 );
162
163 //restrict query on current member id if he's not admin nor staff member
164 if (!$this->login->isAdmin() && !$this->login->isStaff() && !$this->login->isGroupManager()) {
165 $select->where
166 ->nest()
167 ->equalTo('a.' . Adherent::PK, $this->login->id)
168 ->or
169 ->equalTo('a.parent_id', $this->login->id)
170 ->unnest()
171 ->and
172 ->equalTo('t.' . self::PK, $id)
173 ;
174 } else {
175 $select->where->equalTo(self::PK, $id);
176 }
177
178 $results = $this->zdb->execute($select);
179 $result = $results->current();
180 if ($result) {
181 $this->loadFromRS($result);
182 return true;
183 } else {
184 throw new \Exception();
185 }
186 } catch (Throwable $e) {
187 Analog::log(
188 'Cannot load transaction form id `' . $id . '` | ' .
189 $e->getMessage(),
190 Analog::WARNING
191 );
192 return false;
193 }
194 }
195
196 /**
197 * Remove transaction (and all associated contributions) from database
198 *
199 * @param History $hist History
200 * @param boolean $transaction Activate transaction mode (defaults to true)
201 *
202 * @return boolean
203 */
204 public function remove(History $hist, $transaction = true)
205 {
206 global $emitter;
207
208 try {
209 if ($transaction) {
210 $this->zdb->connection->beginTransaction();
211 }
212
213 //remove associated contributions if needeed
214 if ($this->getDispatchedAmount() > 0) {
215 $c = new Contributions($this->zdb, $this->login);
216 $clist = $c->getListFromTransaction($this->_id);
217 $cids = array();
218 foreach ($clist as $cid) {
219 $cids[] = $cid->id;
220 }
221 $rem = $c->remove($cids, $hist, false);
222 }
223
224 //remove transaction itself
225 $delete = $this->zdb->delete(self::TABLE);
226 $delete->where(
227 self::PK . ' = ' . $this->_id
228 );
229 $this->zdb->execute($delete);
230
231 $this->dynamicsRemove(true);
232
233 if ($transaction) {
234 $this->zdb->connection->commit();
235 }
236
237 $emitter->emit('transaction.remove', $this);
238 return true;
239 } catch (Throwable $e) {
240 if ($transaction) {
241 $this->zdb->connection->rollBack();
242 }
243 Analog::log(
244 'An error occurred trying to remove transaction #' .
245 $this->_id . ' | ' . $e->getMessage(),
246 Analog::ERROR
247 );
248 return false;
249 }
250 }
251
252 /**
253 * Populate object from a resultset row
254 *
255 * @param ResultSet $r the resultset row
256 *
257 * @return void
258 */
259 private function loadFromRS($r)
260 {
261 $pk = self::PK;
262 $this->_id = $r->$pk;
263 $this->_date = $r->trans_date;
264 $this->_amount = $r->trans_amount;
265 $this->_description = $r->trans_desc;
266 $adhpk = Adherent::PK;
267 $this->_member = (int)$r->$adhpk;
268
269 $this->loadDynamicFields();
270 }
271
272 /**
273 * Check posted values validity
274 *
275 * @param array $values All values to check, basically the $_POST array
276 * after sending the form
277 * @param array $required Array of required fields
278 * @param array $disabled Array of disabled fields
279 *
280 * @return true|array
281 */
282 public function check($values, $required, $disabled)
283 {
284 $this->errors = array();
285
286 $fields = array_keys($this->_fields);
287 foreach ($fields as $key) {
288 //first of all, let's sanitize values
289 $key = strtolower($key);
290 $prop = '_' . $this->_fields[$key]['propname'];
291
292 if (isset($values[$key])) {
293 $value = trim($values[$key]);
294 } else {
295 $value = '';
296 }
297
298 // if the field is enabled, check it
299 if (!isset($disabled[$key])) {
300 // now, check validity
301 if ($value != '') {
302 switch ($key) {
303 // dates
304 case 'trans_date':
305 try {
306 $d = \DateTime::createFromFormat(__("Y-m-d"), $value);
307 if ($d === false) {
308 throw new \Exception('Incorrect format');
309 }
310 $this->$prop = $d->format('Y-m-d');
311 } catch (Throwable $e) {
312 Analog::log(
313 'Wrong date format. field: ' . $key .
314 ', value: ' . $value . ', expected fmt: ' .
315 __("Y-m-d") . ' | ' . $e->getMessage(),
316 Analog::INFO
317 );
318 $this->errors[] = str_replace(
319 array(
320 '%date_format',
321 '%field'
322 ),
323 array(
324 __("Y-m-d"),
325 $this->_fields[$key]['label']
326 ),
327 _T("- Wrong date format (%date_format) for %field!")
328 );
329 }
330 break;
331 case Adherent::PK:
332 $this->_member = (int)$value;
333 break;
334 case 'trans_amount':
335 $this->_amount = $value;
336 $value = strtr($value, ',', '.');
337 if (!is_numeric($value)) {
338 $this->errors[] = _T("- The amount must be an integer!");
339 }
340 break;
341 case 'trans_desc':
342 /** TODO: retrieve field length from database and check that */
343 $this->_description = $value;
344 if (trim($value) == '') {
345 $this->errors[] = _T("- Empty transaction description!");
346 } elseif (mb_strlen($value) > 150) {
347 $this->errors[] = _T("- Transaction description must be 150 characters long maximum.");
348 }
349 break;
350 }
351 }
352 }
353 }
354
355 // missing required fields?
356 foreach ($required as $key => $val) {
357 if ($val === 1) {
358 $prop = '_' . $this->_fields[$key]['propname'];
359 if (!isset($disabled[$key]) && !isset($this->$prop)) {
360 $this->errors[] = str_replace(
361 '%field',
362 '<a href="#' . $key . '">' . $this->getFieldLabel($key) . '</a>',
363 _T("- Mandatory field %field empty.")
364 );
365 }
366 }
367 }
368
369 if ($this->_id != '') {
370 $dispatched = $this->getDispatchedAmount();
371 if ($dispatched > $this->_amount) {
372 $this->errors[] = _T("- Sum of all contributions exceed corresponding transaction amount.");
373 }
374 }
375
376 $this->dynamicsCheck($values, $required, $disabled);
377
378 if (count($this->errors) > 0) {
379 Analog::log(
380 'Some errors has been thew attempting to edit/store a transaction' .
381 print_r($this->errors, true),
382 Analog::DEBUG
383 );
384 return $this->errors;
385 } else {
386 Analog::log(
387 'Transaction checked successfully.',
388 Analog::DEBUG
389 );
390 return true;
391 }
392 }
393
394 /**
395 * Store the transaction
396 *
397 * @param History $hist History
398 *
399 * @return boolean
400 */
401 public function store(History $hist)
402 {
403 global $emitter;
404
405 $event = null;
406
407 try {
408 $this->zdb->connection->beginTransaction();
409 $values = array();
410 $fields = $this->getDbFields($this->zdb);
411 /** FIXME: quote? */
412 foreach ($fields as $field) {
413 $prop = '_' . $this->_fields[$field]['propname'];
414 $values[$field] = $this->$prop;
415 }
416
417 $success = false;
418 if (!isset($this->_id) || $this->_id == '') {
419 //we're inserting a new transaction
420 unset($values[self::PK]);
421 $insert = $this->zdb->insert(self::TABLE);
422 $insert->values($values);
423 $add = $this->zdb->execute($insert);
424 if ($add->count() > 0) {
425 $this->_id = $this->zdb->getLastGeneratedValue($this);
426
427 // logging
428 $hist->add(
429 _T("Transaction added"),
430 Adherent::getSName($this->zdb, $this->_member)
431 );
432 $success = true;
433 $event = 'transaction.add';
434 } else {
435 $hist->add(_T("Fail to add new transaction."));
436 throw new \RuntimeException(
437 'An error occurred inserting new transaction!'
438 );
439 }
440 } else {
441 //we're editing an existing transaction
442 $update = $this->zdb->update(self::TABLE);
443 $update->set($values)->where(
444 self::PK . '=' . $this->_id
445 );
446 $edit = $this->zdb->execute($update);
447 //edit == 0 does not mean there were an error, but that there
448 //were nothing to change
449 if ($edit->count() > 0) {
450 $hist->add(
451 _T("Transaction updated"),
452 Adherent::getSName($this->zdb, $this->_member)
453 );
454 }
455 $success = true;
456 $event = 'transaction.edit';
457 }
458
459 //dynamic fields
460 if ($success) {
461 $success = $this->dynamicsStore(true);
462 }
463
464 $this->zdb->connection->commit();
465
466 //send event at the end of process, once all has been stored
467 if ($event !== null) {
468 $emitter->emit($event, $this);
469 }
470
471 return true;
472 } catch (Throwable $e) {
473 $this->zdb->connection->rollBack();
474 Analog::log(
475 'Something went wrong :\'( | ' . $e->getMessage() . "\n" .
476 $e->getTraceAsString(),
477 Analog::ERROR
478 );
479 return false;
480 }
481 }
482
483 /**
484 * Retrieve amount that has already been dispatched into contributions
485 *
486 * @return double
487 */
488 public function getDispatchedAmount()
489 {
490 try {
491 $select = $this->zdb->select(Contribution::TABLE);
492 $select->columns(
493 array(
494 'sum' => new Expression('SUM(montant_cotis)')
495 )
496 )->where(self::PK . ' = ' . $this->_id);
497
498 $results = $this->zdb->execute($select);
499 $result = $results->current();
500 $dispatched_amount = $result->sum;
501 return (double)$dispatched_amount;
502 } catch (Throwable $e) {
503 Analog::log(
504 'An error occurred retrieving dispatched amounts | ' .
505 $e->getMessage(),
506 Analog::ERROR
507 );
508 }
509 }
510
511 /**
512 * Retrieve amount that has not yet been dispatched into contributions
513 *
514 * @return double
515 */
516 public function getMissingAmount()
517 {
518 try {
519 $select = $this->zdb->select(Contribution::TABLE);
520 $select->columns(
521 array(
522 'sum' => new Expression('SUM(montant_cotis)')
523 )
524 )->where(self::PK . ' = ' . $this->_id);
525
526 $results = $this->zdb->execute($select);
527 $result = $results->current();
528 $dispatched_amount = $result->sum;
529 return (double)$this->_amount - (double)$dispatched_amount;
530 } catch (Throwable $e) {
531 Analog::log(
532 'An error occurred retrieving missing amounts | ' .
533 $e->getMessage(),
534 Analog::ERROR
535 );
536 }
537 }
538
539 /**
540 * Retrieve fields from database
541 *
542 * @param Db $zdb Database instance
543 *
544 * @return array
545 */
546 public function getDbFields(Db $zdb)
547 {
548 $columns = $zdb->getColumns(self::TABLE);
549 $fields = array();
550 foreach ($columns as $col) {
551 $fields[] = $col->getName();
552 }
553 return $fields;
554 }
555
556 /**
557 * Get the relevant CSS class for current transaction
558 *
559 * @return string current transaction row class
560 */
561 public function getRowClass()
562 {
563 return ($this->getMissingAmount() == 0) ?
564 'transaction-normal' : 'transaction-uncomplete';
565 }
566
567 /**
568 * Global getter method
569 *
570 * @param string $name name of the property we want to retrive
571 *
572 * @return false|object the called property
573 */
574 public function __get($name)
575 {
576 $forbidden = array();
577
578 $rname = '_' . $name;
579 if (!in_array($name, $forbidden) && isset($this->$rname)) {
580 switch ($name) {
581 case 'date':
582 if ($this->$rname != '') {
583 try {
584 $d = new \DateTime($this->$rname);
585 return $d->format(__("Y-m-d"));
586 } catch (Throwable $e) {
587 //oops, we've got a bad date :/
588 Analog::log(
589 'Bad date (' . $this->$rname . ') | ' .
590 $e->getMessage(),
591 Analog::INFO
592 );
593 return $this->$rname;
594 }
595 }
596 break;
597 default:
598 return $this->$rname;
599 break;
600 }
601 } else {
602 return false;
603 }
604 }
605
606 /**
607 * Global setter method
608 *
609 * @param string $name name of the property we want to assign a value to
610 * @param object $value a relevant value for the property
611 *
612 * @return void
613 */
614 public function __set($name, $value)
615 {
616 /*$forbidden = array('fields');*/
617 /** TODO: What to do ? :-) */
618 }
619
620 /**
621 * Get field label
622 *
623 * @param string $field Field name
624 *
625 * @return string
626 */
627 private function getFieldLabel($field)
628 {
629 $label = $this->_fields[$field]['label'];
630 //remove trailing ':' and then nbsp (for french at least)
631 $label = trim(trim($label, ':'), '&nbsp;');
632 return $label;
633 }
634
635 /**
636 * Handle files (dynamics files)
637 *
638 * @param array $files Files sent
639 *
640 * @return array|true
641 */
642 public function handleFiles($files)
643 {
644 $this->errors = [];
645
646 $this->dynamicsFiles($files);
647
648 if (count($this->errors) > 0) {
649 Analog::log(
650 'Some errors has been thew attempting to edit/store a transaction files' . "\n" .
651 print_r($this->errors, true),
652 Analog::ERROR
653 );
654 return $this->errors;
655 } else {
656 return true;
657 }
658 }
659
660 /**
661 * Can current logged-in user display transaction
662 *
663 * @param Login $login Login instance
664 *
665 * @return boolean
666 */
667 public function canShow(Login $login): bool
668 {
669 //admin and staff users can edit, as well as member itself
670 if (!$this->id || $this->id && $login->id == $this->_member || $login->isAdmin() || $login->isStaff()) {
671 return true;
672 }
673
674 //parent can see their children transactions
675 $parent = new Adherent($this->zdb);
676 $parent
677 ->disableAllDeps()
678 ->enableDep('children')
679 ->load($this->login->id);
680 if ($parent->hasChildren()) {
681 foreach ($parent->children as $child) {
682 if ($child->id === $this->_member) {
683 return true;
684 }
685 }
686 return false;
687 }
688
689 return false;
690 }
691 }