]> git.agnieray.net Git - galette.git/blob - galette/lib/Galette/Entity/Transaction.php
a9199b7c4dd4bf767d011695a5cc002f525674eb
[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 class Transaction
60 {
61 use DynamicsTrait;
62
63 public const TABLE = 'transactions';
64 public const PK = 'trans_id';
65
66 private $_id;
67 private $_date;
68 private $_amount;
69 private $_description;
70 private $_member;
71
72 //fields list and their translation
73 private $_fields;
74
75 private $zdb;
76 private $login;
77
78 private $errors;
79
80 /**
81 * Default constructor
82 *
83 * @param Db $zdb Database instance
84 * @param Login $login Login instance
85 * @param null|int|ResultSet $args Either a ResultSet row or its id for to load
86 * a specific transaction, or null to just
87 * instanciate object
88 */
89 public function __construct(Db $zdb, Login $login, $args = null)
90 {
91 $this->zdb = $zdb;
92 $this->login = $login;
93
94 /*
95 * Fields configuration. Each field is an array and must reflect:
96 * array(
97 * (string)label,
98 * (string) propname
99 * )
100 *
101 * I'd prefer a static private variable for this...
102 * But call to the _T function does not seem to be allowed there :/
103 */
104 $this->_fields = array(
105 self::PK => array(
106 'label' => null, //not a field in the form
107 'propname' => 'id'
108 ),
109 'trans_date' => array(
110 'label' => _T("Date:"), //not a field in the form
111 'propname' => 'date'
112 ),
113 'trans_amount' => array(
114 'label' => _T("Amount:"),
115 'propname' => 'amount'
116 ),
117 'trans_desc' => array(
118 'label' => _T("Description:"),
119 'propname' => 'description'
120 ),
121 Adherent::PK => array(
122 'label' => _T("Originator:"),
123 'propname' => 'member'
124 )
125 );
126 if ($args == null || is_int($args)) {
127 $this->_date = date("Y-m-d");
128
129 if (is_int($args) && $args > 0) {
130 $this->load($args);
131 }
132 } elseif (is_object($args)) {
133 $this->loadFromRS($args);
134 }
135
136 $this->loadDynamicFields();
137 }
138
139 /**
140 * Loads a transaction from its id
141 *
142 * @param int $id the identifier for the transaction to load
143 *
144 * @return bool true if query succeed, false otherwise
145 */
146 public function load($id)
147 {
148 try {
149 $select = $this->zdb->select(self::TABLE);
150 $select->where(self::PK . ' = ' . $id);
151
152 $results = $this->zdb->execute($select);
153 $result = $results->current();
154 if ($result) {
155 $this->loadFromRS($result);
156 return true;
157 } else {
158 throw new \Exception();
159 }
160 } catch (Throwable $e) {
161 Analog::log(
162 'Cannot load transaction form id `' . $id . '` | ' .
163 $e->getMessage(),
164 Analog::WARNING
165 );
166 return false;
167 }
168 }
169
170 /**
171 * Remove transaction (and all associated contributions) from database
172 *
173 * @param History $hist History
174 * @param boolean $transaction Activate transaction mode (defaults to true)
175 *
176 * @return boolean
177 */
178 public function remove(History $hist, $transaction = true)
179 {
180 global $emitter;
181
182 try {
183 if ($transaction) {
184 $this->zdb->connection->beginTransaction();
185 }
186
187 //remove associated contributions if needeed
188 if ($this->getDispatchedAmount() > 0) {
189 $c = new Contributions($this->zdb, $this->login);
190 $clist = $c->getListFromTransaction($this->_id);
191 $cids = array();
192 foreach ($clist as $cid) {
193 $cids[] = $cid->id;
194 }
195 $rem = $c->remove($cids, $hist, false);
196 }
197
198 //remove transaction itself
199 $delete = $this->zdb->delete(self::TABLE);
200 $delete->where(
201 self::PK . ' = ' . $this->_id
202 );
203 $this->zdb->execute($delete);
204
205 $this->dynamicsRemove(true);
206
207 if ($transaction) {
208 $this->zdb->connection->commit();
209 }
210
211 $emitter->emit('transaction.remove', $this);
212 return true;
213 } catch (Throwable $e) {
214 if ($transaction) {
215 $this->zdb->connection->rollBack();
216 }
217 Analog::log(
218 'An error occurred trying to remove transaction #' .
219 $this->_id . ' | ' . $e->getMessage(),
220 Analog::ERROR
221 );
222 return false;
223 }
224 }
225
226 /**
227 * Populate object from a resultset row
228 *
229 * @param ResultSet $r the resultset row
230 *
231 * @return void
232 */
233 private function loadFromRS($r)
234 {
235 $pk = self::PK;
236 $this->_id = $r->$pk;
237 $this->_date = $r->trans_date;
238 $this->_amount = $r->trans_amount;
239 $this->_description = $r->trans_desc;
240 $adhpk = Adherent::PK;
241 $this->_member = (int)$r->$adhpk;
242
243 $this->loadDynamicFields();
244 }
245
246 /**
247 * Check posted values validity
248 *
249 * @param array $values All values to check, basically the $_POST array
250 * after sending the form
251 * @param array $required Array of required fields
252 * @param array $disabled Array of disabled fields
253 *
254 * @return true|array
255 */
256 public function check($values, $required, $disabled)
257 {
258 $this->errors = array();
259
260 $fields = array_keys($this->_fields);
261 foreach ($fields as $key) {
262 //first of all, let's sanitize values
263 $key = strtolower($key);
264 $prop = '_' . $this->_fields[$key]['propname'];
265
266 if (isset($values[$key])) {
267 $value = trim($values[$key]);
268 } else {
269 $value = '';
270 }
271
272 // if the field is enabled, check it
273 if (!isset($disabled[$key])) {
274 // now, check validity
275 if ($value != '') {
276 switch ($key) {
277 // dates
278 case 'trans_date':
279 try {
280 $d = \DateTime::createFromFormat(__("Y-m-d"), $value);
281 if ($d === false) {
282 throw new \Exception('Incorrect format');
283 }
284 $this->$prop = $d->format('Y-m-d');
285 } catch (Throwable $e) {
286 Analog::log(
287 'Wrong date format. field: ' . $key .
288 ', value: ' . $value . ', expected fmt: ' .
289 __("Y-m-d") . ' | ' . $e->getMessage(),
290 Analog::INFO
291 );
292 $this->errors[] = str_replace(
293 array(
294 '%date_format',
295 '%field'
296 ),
297 array(
298 __("Y-m-d"),
299 $this->_fields[$key]['label']
300 ),
301 _T("- Wrong date format (%date_format) for %field!")
302 );
303 }
304 break;
305 case Adherent::PK:
306 $this->_member = (int)$value;
307 break;
308 case 'trans_amount':
309 $this->_amount = $value;
310 $value = strtr($value, ',', '.');
311 if (!is_numeric($value)) {
312 $this->errors[] = _T("- The amount must be an integer!");
313 }
314 break;
315 case 'trans_desc':
316 /** TODO: retrieve field length from database and check that */
317 $this->_description = $value;
318 if (trim($value) == '') {
319 $this->errors[] = _T("- Empty transaction description!");
320 } elseif (mb_strlen($value) > 150) {
321 $this->errors[] = _T("- Transaction description must be 150 characters long maximum.");
322 }
323 break;
324 }
325 }
326 }
327 }
328
329 // missing required fields?
330 foreach ($required as $key => $val) {
331 if ($val === 1) {
332 $prop = '_' . $this->_fields[$key]['propname'];
333 if (!isset($disabled[$key]) && !isset($this->$prop)) {
334 $this->errors[] = str_replace(
335 '%field',
336 '<a href="#' . $key . '">' . $this->getFieldLabel($key) . '</a>',
337 _T("- Mandatory field %field empty.")
338 );
339 }
340 }
341 }
342
343 if ($this->_id != '') {
344 $dispatched = $this->getDispatchedAmount();
345 if ($dispatched > $this->_amount) {
346 $this->errors[] = _T("- Sum of all contributions exceed corresponding transaction amount.");
347 }
348 }
349
350 $this->dynamicsCheck($values, $required, $disabled);
351
352 if (count($this->errors) > 0) {
353 Analog::log(
354 'Some errors has been thew attempting to edit/store a transaction' .
355 print_r($this->errors, true),
356 Analog::DEBUG
357 );
358 return $this->errors;
359 } else {
360 Analog::log(
361 'Transaction checked successfully.',
362 Analog::DEBUG
363 );
364 return true;
365 }
366 }
367
368 /**
369 * Store the transaction
370 *
371 * @param History $hist History
372 *
373 * @return boolean
374 */
375 public function store(History $hist)
376 {
377 global $emitter;
378
379 $event = null;
380
381 try {
382 $this->zdb->connection->beginTransaction();
383 $values = array();
384 $fields = $this->getDbFields($this->zdb);
385 /** FIXME: quote? */
386 foreach ($fields as $field) {
387 $prop = '_' . $this->_fields[$field]['propname'];
388 $values[$field] = $this->$prop;
389 }
390
391 $success = false;
392 if (!isset($this->_id) || $this->_id == '') {
393 //we're inserting a new transaction
394 unset($values[self::PK]);
395 $insert = $this->zdb->insert(self::TABLE);
396 $insert->values($values);
397 $add = $this->zdb->execute($insert);
398 if ($add->count() > 0) {
399 $this->_id = $this->zdb->getLastGeneratedValue($this);
400
401 // logging
402 $hist->add(
403 _T("Transaction added"),
404 Adherent::getSName($this->zdb, $this->_member)
405 );
406 $success = true;
407 $event = 'transaction.add';
408 } else {
409 $hist->add(_T("Fail to add new transaction."));
410 throw new \RuntimeException(
411 'An error occurred inserting new transaction!'
412 );
413 }
414 } else {
415 //we're editing an existing transaction
416 $update = $this->zdb->update(self::TABLE);
417 $update->set($values)->where(
418 self::PK . '=' . $this->_id
419 );
420 $edit = $this->zdb->execute($update);
421 //edit == 0 does not mean there were an error, but that there
422 //were nothing to change
423 if ($edit->count() > 0) {
424 $hist->add(
425 _T("Transaction updated"),
426 Adherent::getSName($this->zdb, $this->_member)
427 );
428 }
429 $success = true;
430 $event = 'transaction.edit';
431 }
432
433 //dynamic fields
434 if ($success) {
435 $success = $this->dynamicsStore(true);
436 }
437
438 $this->zdb->connection->commit();
439
440 //send event at the end of process, once all has been stored
441 if ($event !== null) {
442 $emitter->emit($event, $this);
443 }
444
445 return true;
446 } catch (Throwable $e) {
447 $this->zdb->connection->rollBack();
448 Analog::log(
449 'Something went wrong :\'( | ' . $e->getMessage() . "\n" .
450 $e->getTraceAsString(),
451 Analog::ERROR
452 );
453 return false;
454 }
455 }
456
457 /**
458 * Retrieve amount that has already been dispatched into contributions
459 *
460 * @return double
461 */
462 public function getDispatchedAmount()
463 {
464 try {
465 $select = $this->zdb->select(Contribution::TABLE);
466 $select->columns(
467 array(
468 'sum' => new Expression('SUM(montant_cotis)')
469 )
470 )->where(self::PK . ' = ' . $this->_id);
471
472 $results = $this->zdb->execute($select);
473 $result = $results->current();
474 $dispatched_amount = $result->sum;
475 return (double)$dispatched_amount;
476 } catch (Throwable $e) {
477 Analog::log(
478 'An error occurred retrieving dispatched amounts | ' .
479 $e->getMessage(),
480 Analog::ERROR
481 );
482 }
483 }
484
485 /**
486 * Retrieve amount that has not yet been dispatched into contributions
487 *
488 * @return double
489 */
490 public function getMissingAmount()
491 {
492 try {
493 $select = $this->zdb->select(Contribution::TABLE);
494 $select->columns(
495 array(
496 'sum' => new Expression('SUM(montant_cotis)')
497 )
498 )->where(self::PK . ' = ' . $this->_id);
499
500 $results = $this->zdb->execute($select);
501 $result = $results->current();
502 $dispatched_amount = $result->sum;
503 return (double)$this->_amount - (double)$dispatched_amount;
504 } catch (Throwable $e) {
505 Analog::log(
506 'An error occurred retrieving missing amounts | ' .
507 $e->getMessage(),
508 Analog::ERROR
509 );
510 }
511 }
512
513 /**
514 * Retrieve fields from database
515 *
516 * @param Db $zdb Database instance
517 *
518 * @return array
519 */
520 public function getDbFields(Db $zdb)
521 {
522 $columns = $zdb->getColumns(self::TABLE);
523 $fields = array();
524 foreach ($columns as $col) {
525 $fields[] = $col->getName();
526 }
527 return $fields;
528 }
529
530 /**
531 * Get the relevant CSS class for current transaction
532 *
533 * @return string current transaction row class
534 */
535 public function getRowClass()
536 {
537 return ($this->getMissingAmount() == 0) ?
538 'transaction-normal' : 'transaction-uncomplete';
539 }
540
541 /**
542 * Global getter method
543 *
544 * @param string $name name of the property we want to retrive
545 *
546 * @return false|object the called property
547 */
548 public function __get($name)
549 {
550 $forbidden = array();
551
552 $rname = '_' . $name;
553 if (!in_array($name, $forbidden) && isset($this->$rname)) {
554 switch ($name) {
555 case 'date':
556 if ($this->$rname != '') {
557 try {
558 $d = new \DateTime($this->$rname);
559 return $d->format(__("Y-m-d"));
560 } catch (Throwable $e) {
561 //oops, we've got a bad date :/
562 Analog::log(
563 'Bad date (' . $this->$rname . ') | ' .
564 $e->getMessage(),
565 Analog::INFO
566 );
567 return $this->$rname;
568 }
569 }
570 break;
571 default:
572 return $this->$rname;
573 break;
574 }
575 } else {
576 return false;
577 }
578 }
579
580 /**
581 * Global setter method
582 *
583 * @param string $name name of the property we want to assign a value to
584 * @param object $value a relevant value for the property
585 *
586 * @return void
587 */
588 public function __set($name, $value)
589 {
590 /*$forbidden = array('fields');*/
591 /** TODO: What to do ? :-) */
592 }
593
594 /**
595 * Get field label
596 *
597 * @param string $field Field name
598 *
599 * @return string
600 */
601 private function getFieldLabel($field)
602 {
603 $label = $this->_fields[$field]['label'];
604 //remove trailing ':' and then nbsp (for french at least)
605 $label = trim(trim($label, ':'), '&nbsp;');
606 return $label;
607 }
608
609 /**
610 * Handle files (dynamics files)
611 *
612 * @param array $files Files sent
613 *
614 * @return array|true
615 */
616 public function handleFiles($files)
617 {
618 $this->errors = [];
619
620 $this->dynamicsFiles($files);
621
622 if (count($this->errors) > 0) {
623 Analog::log(
624 'Some errors has been thew attempting to edit/store a transaction files' . "\n" .
625 print_r($this->errors, true),
626 Analog::ERROR
627 );
628 return $this->errors;
629 } else {
630 return true;
631 }
632 }
633 }