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