]> git.agnieray.net Git - galette.git/blob - galette/lib/Galette/Entity/Contribution.php
Check for overlapping fee in global checks
[galette.git] / galette / lib / Galette / Entity / Contribution.php
1 <?php
2
3 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
4
5 /**
6 * Contribution class for galette
7 *
8 * PHP version 5
9 *
10 * Copyright © 2010-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 2010-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 * @version SVN: $Id$
34 * @link http://galette.tuxfamily.org
35 * @since Available since 0.7dev - 2010-03-11
36 */
37
38 namespace Galette\Entity;
39
40 use Analog\Analog;
41 use Zend\Db\Sql\Expression;
42 use Galette\Core\Db;
43 use Galette\Core\Login;
44 use Galette\IO\ExternalScript;
45 use Galette\IO\PdfContribution;
46
47 /**
48 * Contribution class for galette
49 *
50 * @category Entity
51 * @name Contribution
52 * @package Galette
53 * @author Johan Cwiklinski <johan@x-tnd.be>
54 * @copyright 2010-2014 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 Contribution
60 {
61 use DynamicsTrait;
62
63 const TABLE = 'cotisations';
64 const PK = 'id_cotis';
65
66 const PAYMENT_OTHER = 0;
67 const PAYMENT_CASH = 1;
68 const PAYMENT_CREDITCARD = 2;
69 const PAYMENT_CHECK = 3;
70 const PAYMENT_TRANSFER = 4;
71 const PAYMENT_PAYPAL = 5;
72
73 private $_id;
74 private $_date;
75 private $_member;
76 private $_type;
77 private $_amount;
78 private $_payment_type;
79 private $_orig_amount;
80 private $_info;
81 private $_begin_date;
82 private $_end_date;
83 private $_transaction = null;
84 private $_is_cotis;
85 private $_extension;
86
87 //fields list and their translation
88 private $_fields;
89
90 private $zdb;
91 private $login;
92
93 private $errors;
94
95 /**
96 * Default constructor
97 *
98 * @param Db $zdb Database
99 * @param Login $login Login instance
100 * @param null|int|ResultSet $args Either a ResultSet row to load
101 * a specific contribution, or an type id
102 * to just instanciate object
103 */
104 public function __construct(Db $zdb, Login $login, $args = null)
105 {
106 $this->zdb = $zdb;
107 $this->login = $login;
108
109 /*
110 * Fields configuration. Each field is an array and must reflect:
111 * array(
112 * (string)label,
113 * (string) propname
114 * )
115 *
116 * I'd prefer a static private variable for this...
117 * But call to the _T function does not seem to be allowed there :/
118 */
119 $this->_fields = array(
120 'id_cotis' => array(
121 'label' => null, //not a field in the form
122 'propname' => 'id'
123 ),
124 Adherent::PK => array(
125 'label' => _T("Contributor:"),
126 'propname' => 'member'
127 ),
128 ContributionsTypes::PK => array(
129 'label' => _T("Contribution type:"),
130 'propname' => 'type'
131 ),
132 'montant_cotis' => array(
133 'label' => _T("Amount:"),
134 'propname' => 'amount'
135 ),
136 'type_paiement_cotis' => array(
137 'label' => _T("Payment type:"),
138 'propname' => 'payment_type'
139 ),
140 'info_cotis' => array(
141 'label' => _T("Comments:"),
142 'propname' => 'info'
143 ),
144 'date_enreg' => array(
145 'label' => null, //not a field in the form
146 'propname' => 'date'
147 ),
148 'date_debut_cotis' => array(
149 'label' => _T("Date of contribution:"),
150 'cotlabel' => _T("Start date of membership:"), //if contribution is a cotisation, label differs
151 'propname' => 'begin_date'
152 ),
153 'date_fin_cotis' => array(
154 'label' => _T("End date of membership:"),
155 'propname' => 'end_date'
156 ),
157 Transaction::PK => array(
158 'label' => null, //not a field in the form
159 'propname' => 'transaction'
160 ),
161 //this one is not really a field, but is required in some cases...
162 //adding it here make simplier to check required fields
163 'duree_mois_cotis' => array(
164 'label' => _T("Membership extension:"),
165 'propname' => 'extension'
166 )
167 );
168 if (is_int($args)) {
169 $this->load($args);
170 } elseif (is_array($args)) {
171 $this->_date = date("Y-m-d");
172 if (isset($args['adh']) && $args['adh'] != '') {
173 $this->_member = (int)$args['adh'];
174 }
175 if (isset($args['trans'])) {
176 $this->_transaction = new Transaction($this->zdb, $this->login, (int)$args['trans']);
177 if (!isset($this->_member)) {
178 $this->_member = (int)$this->_transaction->member;
179 }
180 $this->_amount = $this->_transaction->getMissingAmount();
181 }
182 $this->type = (int)$args['type'];
183 //calculate begin date for cotisation
184 $this->_begin_date = $this->_date;
185 if ($this->_is_cotis) {
186 $curend = self::getDueDate($this->zdb, $this->_member);
187 if ($curend != '') {
188 $dend = new \DateTime($curend);
189 $now = date('Y-m-d');
190 $dnow = new \DateTime($now);
191 if ($dend < $dnow) {
192 // Member didn't renew on time
193 $this->_begin_date = $now;
194 } else {
195 $this->_begin_date = $curend;
196 }
197 }
198 $this->retrieveEndDate();
199 }
200 if (isset($args['payment_type'])) {
201 $this->_payment_type = $args['payment_type'];
202 }
203 } elseif (is_object($args)) {
204 $this->loadFromRS($args);
205 }
206
207 if ($this->id !== null) {
208 $this->loadDynamicFields();
209 }
210 }
211
212 /**
213 * Sets end contribution date
214 *
215 * @return void
216 */
217 private function retrieveEndDate()
218 {
219 global $preferences;
220
221 $bdate = new \DateTime($this->_begin_date);
222 if ($preferences->pref_beg_membership != '') {
223 //case beginning of membership
224 list($j, $m) = explode('/', $preferences->pref_beg_membership);
225 $edate = new \DateTime($bdate->format('Y') . '-' . $m . '-' . $j);
226 while ($edate <= $bdate) {
227 $edate->modify('+1 year');
228 }
229 $this->_end_date = $edate->format('Y-m-d');
230 } elseif ($preferences->pref_membership_ext != '') {
231 //case membership extension
232 $this->_extension = $preferences->pref_membership_ext;
233 $dext = new \DateInterval('P' . $this->_extension . 'M');
234 $edate = $bdate->add($dext);
235 $this->_end_date = $edate->format('Y-m-d');
236 } else {
237 throw new \RuntimeException(
238 'Unable to define end date; none of pref_beg_membership nor pref_membership_ext are defined!'
239 );
240 }
241 }
242
243 /**
244 * Loads a contribution from its id
245 *
246 * @param int $id the identifiant for the contribution to load
247 *
248 * @return bool true if query succeed, false otherwise
249 */
250 public function load($id)
251 {
252 try {
253 $select = $this->zdb->select(self::TABLE);
254 $select->where(self::PK . ' = ' . $id);
255 //restrict query on current member id if he's not admin nor staff member
256 if (!$this->login->isAdmin() && !$this->login->isStaff()) {
257 $select->where(Adherent::PK . ' = ' . $this->login->id);
258 }
259
260 $results = $this->zdb->execute($select);
261 $row = $results->current();
262 if ($row !== false) {
263 $this->loadFromRS($row);
264 return true;
265 } else {
266 throw new \Exception(
267 'No contribution #' . $id . ' (user ' .$this->login->id . ')'
268 );
269 }
270 } catch (\Exception $e) {
271 Analog::log(
272 'An error occured attempting to load contribution #' . $id .
273 $e->getMessage(),
274 Analog::ERROR
275 );
276 return false;
277 }
278 }
279
280 /**
281 * Populate object from a resultset row
282 *
283 * @param ResultSet $r the resultset row
284 *
285 * @return void
286 */
287 private function loadFromRS($r)
288 {
289 $pk = self::PK;
290 $this->_id = (int)$r->$pk;
291 $this->_date = $r->date_enreg;
292 $this->_amount = $r->montant_cotis;
293 //save original amount, we need it for transactions parts calulations
294 $this->_orig_amount = $r->montant_cotis;
295 $this->_payment_type = $r->type_paiement_cotis;
296 $this->_info = $r->info_cotis;
297 $this->_begin_date = $r->date_debut_cotis;
298 $enddate = $r->date_fin_cotis;
299 //do not work with knows bad dates...
300 //the one with BC comes from 0.63/pgsl demo... Why the hell a so
301 //strange date? dont know :(
302 if ($enddate !== '0000-00-00'
303 && $enddate !== '1901-01-01'
304 && $enddate !== '0001-01-01 BC'
305 ) {
306 $this->_end_date = $r->date_fin_cotis;
307 }
308 $adhpk = Adherent::PK;
309 $this->_member = (int)$r->$adhpk;
310
311 $transpk = Transaction::PK;
312 if ($r->$transpk != '') {
313 $this->_transaction = new Transaction($this->zdb, $this->login, (int)$r->$transpk);
314 }
315
316 $this->type = (int)$r->id_type_cotis;
317 }
318
319 /**
320 * Check posted values validity
321 *
322 * @param array $values All values to check, basically the $_POST array
323 * after sending the form
324 * @param array $required Array of required fields
325 * @param array $disabled Array of disabled fields
326 *
327 * @return true|array
328 */
329 public function check($values, $required, $disabled)
330 {
331 $this->errors = array();
332
333 $fields = array_keys($this->_fields);
334 foreach ($fields as $key) {
335 //first of all, let's sanitize values
336 $key = strtolower($key);
337 $prop = '_' . $this->_fields[$key]['propname'];
338
339 if (isset($values[$key])) {
340 $value = trim($values[$key]);
341 } else {
342 $value = '';
343 }
344
345 // if the field is enabled, check it
346 if (!isset($disabled[$key])) {
347 // fill up the adherent structure
348 //$this->$prop = stripslashes($value); //not relevant here!
349
350 // now, check validity
351 switch ($key) {
352 // dates
353 case 'date_enreg':
354 case 'date_debut_cotis':
355 case 'date_fin_cotis':
356 if ($value != '') {
357 try {
358 $d = \DateTime::createFromFormat(__("Y-m-d"), $value);
359 if ($d === false) {
360 throw new \Exception('Incorrect format');
361 }
362 $this->$prop = $d->format('Y-m-d');
363 } catch (\Exception $e) {
364 Analog::log(
365 'Wrong date format. field: ' . $key .
366 ', value: ' . $value . ', expected fmt: ' .
367 __("Y-m-d") . ' | ' . $e->getMessage(),
368 Analog::INFO
369 );
370 $this->errors[] = str_replace(
371 array(
372 '%date_format',
373 '%field'
374 ),
375 array(
376 __("Y-m-d"),
377 $this->_fields[$key]['label']
378 ),
379 _T("- Wrong date format (%date_format) for %field!")
380 );
381 }
382 }
383 break;
384 case Adherent::PK:
385 if ($value != '') {
386 $this->_member = $value;
387 }
388 break;
389 case ContributionsTypes::PK:
390 if ($value != '') {
391 $this->type = (int)$value;
392 }
393 break;
394 case 'montant_cotis':
395 $this->_amount = $value;
396 $value = strtr($value, ',', '.');
397 if (!is_numeric($value)) {
398 $this->errors[] = _T("- The amount must be an integer!");
399 }
400 break;
401 case 'type_paiement_cotis':
402 if ($value == self::PAYMENT_OTHER
403 || $value == self::PAYMENT_CASH
404 || $value == self::PAYMENT_CREDITCARD
405 || $value == self::PAYMENT_CHECK
406 || $value == self::PAYMENT_TRANSFER
407 || $value == self::PAYMENT_PAYPAL
408 ) {
409 $this->_payment_type = $value;
410 } else {
411 $this->errors[] = _T("- Unknown payment type");
412 }
413 break;
414 case 'info_cotis':
415 $this->_info = $value;
416 break;
417 case Transaction::PK:
418 if ($value != '') {
419 $this->_transaction = new Transaction($this->zdb, $this->login, (int)$value);
420 }
421 break;
422 case 'duree_mois_cotis':
423 if ($value != '') {
424 if (!is_numeric($value) || $value <= 0) {
425 $this->errors[] = _T("- The duration must be a positive integer!");
426 }
427 $this->$prop = $value;
428 $this->retrieveEndDate();
429 }
430 break;
431 }
432 }
433 }
434
435 // missing required fields?
436 foreach ($required as $key => $val) {
437 if ($val === 1) {
438 $prop = '_' . $this->_fields[$key]['propname'];
439 if (!isset($disabled[$key])
440 && (!isset($this->$prop)
441 || (!is_object($this->$prop) && trim($this->$prop) == '')
442 || (is_object($this->$prop) && trim($this->$prop->id) == ''))
443 ) {
444 $this->errors[] = _T("- Mandatory field empty: ") .
445 ' <a href="#' . $key . '">' . $this->getFieldLabel($key) .'</a>';
446 }
447 }
448 }
449
450 if ($this->_transaction != null && $this->_amount != null) {
451 $missing = $this->_transaction->getMissingAmount();
452 //calculate new missing amount
453 $missing = $missing + $this->_orig_amount - $this->_amount;
454 if ($missing < 0) {
455 $this->errors[] = _T("- Sum of all contributions exceed corresponding transaction amount.");
456 }
457 }
458
459 if ($this->isCotis() && count($this->errors) == 0) {
460 $overlap = $this->checkOverlap();
461 if ($overlap !== true) {
462 if ($overlap === false) {
463 $this->errors[] = _T("An error occured checking overlaping fees :(");
464 } else {
465 //method directly return error message
466 $this->errors[] = $overlap;
467 }
468 }
469 }
470
471 if (count($this->errors) > 0) {
472 Analog::log(
473 'Some errors has been throwed attempting to edit/store a contribution' .
474 print_r($this->errors, true),
475 Analog::ERROR
476 );
477 return $this->errors;
478 } else {
479 Analog::log(
480 'Contribution checked successfully.',
481 Analog::DEBUG
482 );
483 return true;
484 }
485 }
486
487 /**
488 * Check that membership fees does not overlap
489 *
490 * @return boolean|string True if all is ok, false if error,
491 * error message if overlap
492 */
493 public function checkOverlap()
494 {
495 try {
496 $select = $this->zdb->select(self::TABLE, 'c');
497 $select->columns(
498 array('date_debut_cotis', 'date_fin_cotis')
499 )->join(
500 array('ct' => PREFIX_DB . ContributionsTypes::TABLE),
501 'c.' . ContributionsTypes::PK . '=ct.' . ContributionsTypes::PK,
502 array()
503 )->where(Adherent::PK . ' = ' . $this->_member)
504 ->where(array('cotis_extension' => new Expression('true')))
505 ->where->nest->nest
506 ->greaterThanOrEqualTo('date_debut_cotis', $this->_begin_date)
507 ->lessThan('date_debut_cotis', $this->_end_date)
508 ->unnest
509 ->or->nest
510 ->greaterThan('date_fin_cotis', $this->_begin_date)
511 ->lessThanOrEqualTo('date_fin_cotis', $this->_end_date);
512
513 if ($this->id != '') {
514 $select->where(self::PK . ' != ' . $this->id);
515 }
516
517 $results = $this->zdb->execute($select);
518 if ($results->count() > 0) {
519 $result = $results->current();
520 $d = new \DateTime($result->date_debut_cotis);
521
522 return _T("- Membership period overlaps period starting at ") .
523 $d->format(__("Y-m-d"));
524 }
525 return true;
526 } catch (\Exception $e) {
527 Analog::log(
528 'An error occured checking overlaping fee. ' . $e->getMessage(),
529 Analog::ERROR
530 );
531 return false;
532 }
533 }
534
535 /**
536 * Store the contribution
537 *
538 * @return boolean
539 */
540 public function store()
541 {
542 global $hist;
543
544 try {
545 $this->zdb->connection->beginTransaction();
546 $values = array();
547 $fields = self::getDbFields($this->zdb);
548 foreach ($fields as $field) {
549 $prop = '_' . $this->_fields[$field]['propname'];
550 switch ($field) {
551 case ContributionsTypes::PK:
552 case Transaction::PK:
553 if (isset($this->$prop)) {
554 $values[$field] = $this->$prop->id;
555 }
556 break;
557 default:
558 $values[$field] = $this->$prop;
559 break;
560 }
561 }
562
563 //no end date, let's take database defaults
564 if (!$this->isCotis() && !$this->_end_date) {
565 unset($values['date_fin_cotis']);
566 }
567
568 if (!isset($this->_id) || $this->_id == '') {
569 //we're inserting a new contribution
570 unset($values[self::PK]);
571
572 $insert = $this->zdb->insert(self::TABLE);
573 $insert->values($values);
574 $add = $this->zdb->execute($insert);
575
576 if ($add->count() > 0) {
577 if ($this->zdb->isPostgres()) {
578 $this->_id = $this->zdb->driver->getLastGeneratedValue(
579 PREFIX_DB . 'cotisations_id_seq'
580 );
581 } else {
582 $this->_id = $this->zdb->driver->getLastGeneratedValue();
583 }
584
585 // logging
586 $hist->add(
587 _T("Contribution added"),
588 Adherent::getSName($this->zdb, $this->_member)
589 );
590 } else {
591 $hist->add(_T("Fail to add new contribution."));
592 throw new \Exception(
593 'An error occured inserting new contribution!'
594 );
595 }
596 } else {
597 //we're editing an existing contribution
598 $update = $this->zdb->update(self::TABLE);
599 $update->set($values)->where(
600 self::PK . '=' . $this->_id
601 );
602 $edit = $this->zdb->execute($update);
603
604 //edit == 0 does not mean there were an error, but that there
605 //were nothing to change
606 if ($edit->count() > 0) {
607 $hist->add(
608 _T("Contribution updated"),
609 Adherent::getSName($this->zdb, $this->_member)
610 );
611 } elseif ($edit === false) {
612 throw new \Exception(
613 'An error occured updating contribution # ' . $this->_id . '!'
614 );
615 }
616 }
617 //update deadline
618 if ($this->isCotis()) {
619 $deadline = $this->updateDeadline();
620 if ($deadline !== true) {
621 //if something went wrong, we rollback transaction
622 throw new \Exception('An error occured updating member\'s deadline');
623 }
624 }
625 $this->zdb->connection->commit();
626 $this->_orig_amount = $this->_amount;
627 return true;
628 } catch (\Exception $e) {
629 $this->zdb->connection->rollBack();
630 Analog::log(
631 'Something went wrong :\'( | ' . $e->getMessage() . "\n" .
632 $e->getTraceAsString(),
633 Analog::ERROR
634 );
635 return false;
636 }
637 }
638
639 /**
640 * Update member dead line
641 *
642 * @return boolean
643 */
644 private function updateDeadline()
645 {
646 try {
647 $due_date = self::getDueDate($this->zdb, $this->_member);
648
649 if ($due_date != '') {
650 $date_fin_update = $due_date;
651 } else {
652 $date_fin_update = new Expression('NULL');
653 }
654
655 $update = $this->zdb->update(Adherent::TABLE);
656 $update->set(
657 array('date_echeance' => $date_fin_update)
658 )->where(
659 Adherent::PK . '=' . $this->_member
660 );
661 $this->zdb->execute($update);
662 return true;
663 } catch (\Exception $e) {
664 Analog::log(
665 'An error occured updating member ' . $this->_member .
666 '\'s deadline |' .
667 $e->getMessage(),
668 Analog::ERROR
669 );
670 return false;
671 }
672 }
673
674 /**
675 * Remove contribution from database
676 *
677 * @param boolean $transaction Activate transaction mode (defaults to true)
678 *
679 * @return boolean
680 */
681 public function remove($transaction = true)
682 {
683 try {
684 if ($transaction) {
685 $this->zdb->connection->beginTransaction();
686 }
687
688 $delete = $this->zdb->delete(self::TABLE);
689 $delete->where(self::PK . ' = ' . $this->_id);
690 $del = $this->zdb->execute($delete);
691 if ($del->count() > 0) {
692 $this->updateDeadline();
693 }
694 if ($transaction) {
695 $this->zdb->connection->commit();
696 }
697 return true;
698 } catch (\Exception $e) {
699 if ($transaction) {
700 $this->zdb->connection->rollBack();
701 }
702 Analog::log(
703 'An error occured trying to remove contribution #' .
704 $this->_id . ' | ' . $e->getMessage(),
705 Analog::ERROR
706 );
707 return false;
708 }
709 }
710
711 /**
712 * Get field label
713 *
714 * @param string $field Field name
715 *
716 * @return string
717 */
718 public function getFieldLabel($field)
719 {
720 $label = $this->_fields[$field]['label'];
721 if ($this->isCotis() && $field == 'date_debut_cotis') {
722 $label = $this->_fields[$field]['cotlabel'];
723 }
724 //remove trailing ':' and then nbsp (for french at least)
725 $label = trim(trim($label, ':'), '&nbsp;');
726 return $label;
727 }
728
729 /**
730 * Retrieve fields from database
731 *
732 * @param Db $zdb Database instance
733 *
734 * @return array
735 */
736 public static function getDbFields(Db $zdb)
737 {
738 $columns = $zdb->getColumns(self::TABLE);
739 $fields = array();
740 foreach ($columns as $col) {
741 $fields[] = $col->getName();
742 }
743 return $fields;
744 }
745
746 /**
747 * Get the relevant CSS class for current contribution
748 *
749 * @return string current contribution row class
750 */
751 public function getRowClass()
752 {
753 return ( $this->_end_date != $this->_begin_date && $this->_is_cotis) ?
754 'cotis-normal' :
755 'cotis-give';
756 }
757
758 /**
759 * Retrieve member due date
760 *
761 * @param Db $zdb Database instance
762 * @param integer $member_id Member identifier
763 *
764 * @return date
765 */
766 public static function getDueDate(Db $zdb, $member_id)
767 {
768 try {
769 $select = $zdb->select(self::TABLE, 'c');
770 $select->columns(
771 array(
772 'max_date' => new Expression('MAX(date_fin_cotis)')
773 )
774 )->join(
775 array('ct' => PREFIX_DB . ContributionsTypes::TABLE),
776 'c.' . ContributionsTypes::PK . '=ct.' . ContributionsTypes::PK,
777 array()
778 )->where(
779 Adherent::PK . ' = ' . $member_id
780 )->where(
781 array('cotis_extension' => new Expression('true'))
782 );
783
784 $results = $zdb->execute($select);
785 $result = $results->current();
786 $due_date = $result->max_date;
787
788 //avoid bad dates in postgres and bad mysql return from zenddb
789 if ($due_date == '0001-01-01 BC' || $due_date == '1901-01-01') {
790 $due_date = '';
791 }
792 return $due_date;
793 } catch (\Exception $e) {
794 Analog::log(
795 'An error occured trying to retrieve member\'s due date',
796 Analog::ERROR
797 );
798 return false;
799 }
800 }
801
802 /**
803 * Detach a contribution from a transaction
804 *
805 * @param Db $zdb Database instance
806 * @param Login $login Login instance
807 * @param int $trans_id Transaction identifier
808 * @param int $contrib_id Contribution identifier
809 *
810 * @return boolean
811 */
812 public static function unsetTransactionPart(Db $zdb, Login $login, $trans_id, $contrib_id)
813 {
814 try {
815 //first, we check if contribution is part of transaction
816 $c = new Contribution($zdb, $login, (int)$contrib_id);
817 if ($c->isTransactionPartOf($trans_id)) {
818 $update = $zdb->update(self::TABLE);
819 $update->set(
820 array(Transaction::PK => null)
821 )->where(
822 self::PK . ' = ' . $contrib_id
823 );
824 $zdb->execute($update);
825 return true;
826 } else {
827 Analog::log(
828 'Contribution #' . $contrib_id .
829 ' is not actually part of transaction #' . $trans_id,
830 Analog::WARNING
831 );
832 return false;
833 }
834 } catch (\Exception $e) {
835 Analog::log(
836 'Unable to detach contribution #' . $contrib_id .
837 ' to transaction #' . $trans_id . ' | ' . $e->getMessage(),
838 Analog::ERROR
839 );
840 return false;
841 }
842 }
843
844 /**
845 * Set a contribution as a transaction part
846 *
847 * @param Db $zdb Database instance
848 * @param int $trans_id Transaction identifier
849 * @param int $contrib_id Contribution identifier
850 *
851 * @return boolean
852 */
853 public static function setTransactionPart(Db $zdb, $trans_id, $contrib_id)
854 {
855 try {
856 $update = $zdb->update(self::TABLE);
857 $update->set(
858 array(Transaction::PK => $trans_id)
859 )->where(self::PK . ' = ' . $contrib_id);
860
861 $zdb->execute($update);
862 return true;
863 } catch (\Exception $e) {
864 Analog::log(
865 'Unable to attach contribution #' . $contrib_id .
866 ' to transaction #' . $trans_id . ' | ' . $e->getMessage(),
867 Analog::ERROR
868 );
869 return false;
870 }
871 }
872
873 /**
874 * Is current contribution a cotisation
875 *
876 * @return boolean
877 */
878 public function isCotis()
879 {
880 return $this->_is_cotis;
881 }
882
883 /**
884 * Is current contribution part of specified transaction
885 *
886 * @param int $id Transaction identifier
887 *
888 * @return boolean
889 */
890 public function isTransactionPartOf($id)
891 {
892 if ($this->isTransactionPart()) {
893 return $id == $this->_transaction->id;
894 } else {
895 return false;
896 }
897 }
898
899 /**
900 * Is current contribution part of transaction
901 *
902 * @return boolean
903 */
904 public function isTransactionPart()
905 {
906 return $this->_transaction != null;
907 }
908
909 /**
910 * Execute post contribution script
911 *
912 * @param ExternalScript $es External script to execute
913 * @param array $extra Extra informations on contribution
914 * Defaults to null
915 * @param array $pextra Extra information on payment
916 * Defaults to null
917 *
918 * @return mixed Script return value on success, values and script output on fail
919 */
920 public function executePostScript(
921 ExternalScript $es,
922 $extra = null,
923 $pextra = null
924 ) {
925 global $preferences;
926
927 $payment = array(
928 'type' => $this->getPaymentType()
929 );
930
931 if ($pextra !== null && is_array($pextra)) {
932 $payment = array_merge($payment, $pextra);
933 }
934
935 if (!file_exists(GALETTE_CACHE_DIR . '/pdf_contribs')) {
936 @mkdir(GALETTE_CACHE_DIR . '/pdf_contribs');
937 }
938
939 $voucher_path = null;
940 if ($this->_id !== null) {
941 $voucher = new PdfContribution($this, $this->zdb, $preferences);
942 $voucher->store(GALETTE_CACHE_DIR . '/pdf_contribs');
943 $voucher_path = $voucher->getPath();
944 }
945
946 $contrib = array(
947 'date' => $this->_date,
948 'type' => $this->getRawType(),
949 'amount' => $this->amount,
950 'voucher' => $voucher_path,
951 'category' => array(
952 'id' => $this->type->id,
953 'label' => $this->type->libelle
954 ),
955 'payment' => $payment
956 );
957
958 if ($this->_member !== null) {
959 $m = new Adherent($this->zdb, (int)$this->_member);
960 $member = array(
961 'name' => $m->sfullname,
962 'email' => $m->email,
963 'organization' => ($m->isCompany() ? 1 : 0),
964 'status' => array(
965 'id' => $m->status,
966 'label' => $m->sstatus
967 ),
968 'country' => $m->country
969 );
970
971 if ($m->isCompany()) {
972 $member['organization_name'] = $m->company_name;
973 }
974
975 $contrib['member'] = $member;
976 }
977
978 if ($extra !== null && is_array($extra)) {
979 $contrib = array_merge($contrib, $extra);
980 }
981
982 $res = $es->send($contrib);
983
984 if ($res !== true) {
985 Analog::log(
986 'An error occured calling post contribution ' .
987 "script:\n" . $es->getOutput(),
988 Analog::ERROR
989 );
990 $res = _T("Contribution informations") . "\n";
991 $res .= print_r($contrib, true);
992 $res .= "\n\n" . _T("Script output") . "\n";
993 $res .= $es->getOutput();
994 }
995
996 return $res;
997 }
998 /**
999 * Get raw contribution type
1000 *
1001 * @return string
1002 */
1003 public function getRawType()
1004 {
1005 if ($this->isCotis()) {
1006 return 'membership';
1007 } else {
1008 return 'donation';
1009 }
1010 }
1011
1012 /**
1013 * Get contribution type label
1014 *
1015 * @return string
1016 */
1017 public function getTypeLabel()
1018 {
1019 if ($this->isCotis()) {
1020 return _T("Membership");
1021 } else {
1022 return _T("Donation");
1023 }
1024 }
1025
1026 /**
1027 * Get payent type label
1028 *
1029 * @return string
1030 */
1031 public function getPaymentType()
1032 {
1033 if ($this->_payment_type === null) {
1034 return '-';
1035 }
1036
1037 switch ($this->payment_type) {
1038 case Contribution::PAYMENT_CASH:
1039 return 'cash';
1040 break;
1041 case Contribution::PAYMENT_CREDITCARD:
1042 return 'credit_card';
1043 break;
1044 case Contribution::PAYMENT_CHECK:
1045 return 'check';
1046 break;
1047 case Contribution::PAYMENT_TRANSFER:
1048 return 'transfer';
1049 break;
1050 case Contribution::PAYMENT_PAYPAL:
1051 return 'paypal';
1052 break;
1053 case Contribution::PAYMENT_OTHER:
1054 return 'other';
1055 break;
1056 default:
1057 Analog::log(
1058 __METHOD__ . ' Unknonw payment type ' . $this->payment_type,
1059 Analog::ERROR
1060 );
1061 throw new \RuntimeException(
1062 'Unknonw payment type ' . $this->payment_type
1063 );
1064 }
1065 }
1066
1067 /**
1068 * Global getter method
1069 *
1070 * @param string $name name of the property we want to retrive
1071 *
1072 * @return false|object the called property
1073 */
1074 public function __get($name)
1075 {
1076
1077 $forbidden = array('is_cotis');
1078 $virtuals = array('duration', 'spayment_type', 'model', 'raw_date',
1079 'raw_begin_date', 'raw_end_date'
1080 );
1081
1082 $rname = '_' . $name;
1083
1084 if (in_array($name, $forbidden)) {
1085 Analog::log(
1086 "Call to __get for '$name' is forbidden!",
1087 Analog::WARNING
1088 );
1089
1090 switch ($name) {
1091 case 'is_cotis':
1092 return $this->isCotis();
1093 break;
1094 default:
1095 throw new \RuntimeException("Call to __get for '$name' is forbidden!");
1096 }
1097 } elseif (property_exists($this, $rname)
1098 || in_array($name, $virtuals)
1099 ) {
1100 switch ($name) {
1101 case 'raw_date':
1102 case 'raw_begin_date':
1103 case 'raw_end_date':
1104 $rname = '_' . substr($name, 4);
1105 if ($this->$rname != '') {
1106 try {
1107 $d = new \DateTime($this->$rname);
1108 return $d;
1109 } catch (\Exception $e) {
1110 //oops, we've got a bad date :/
1111 Analog::log(
1112 'Bad date (' . $this->$rname . ') | ' .
1113 $e->getMessage(),
1114 Analog::INFO
1115 );
1116 throw $e;
1117 }
1118 }
1119 break;
1120 case 'date':
1121 case 'begin_date':
1122 case 'end_date':
1123 if ($this->$rname != '') {
1124 try {
1125 $d = new \DateTime($this->$rname);
1126 return $d->format(__("Y-m-d"));
1127 } catch (\Exception $e) {
1128 //oops, we've got a bad date :/
1129 Analog::log(
1130 'Bad date (' . $this->$rname . ') | ' .
1131 $e->getMessage(),
1132 Analog::INFO
1133 );
1134 return $this->$rname;
1135 }
1136 }
1137 break;
1138 case 'duration':
1139 if ($this->_is_cotis) {
1140 $date_end = new \DateTime($this->_end_date);
1141 $date_start = new \DateTime($this->_begin_date);
1142 $diff = $date_end->diff($date_start);
1143 return $diff->format('%y') * 12 + $diff->format('%m');
1144 } else {
1145 return '';
1146 }
1147 break;
1148 case 'spayment_type':
1149 if ($this->_payment_type === null) {
1150 return '-';
1151 }
1152 switch ($this->_payment_type) {
1153 case self::PAYMENT_OTHER:
1154 return _T("Other");
1155 break;
1156 case self::PAYMENT_CASH:
1157 return _T("Cash");
1158 break;
1159 case self::PAYMENT_CREDITCARD:
1160 return _T("Credit card");
1161 break;
1162 case self::PAYMENT_CHECK:
1163 return _T("Check");
1164 break;
1165 case self::PAYMENT_TRANSFER:
1166 return _T("Transfer");
1167 break;
1168 case self::PAYMENT_PAYPAL:
1169 return _T("Paypal");
1170 break;
1171 default:
1172 Analog::log(
1173 'Unknown payment type ' . $this->_payment_type,
1174 Analog::WARNING
1175 );
1176 return '-';
1177 break;
1178 }
1179 break;
1180 case 'model':
1181 if ($this->_is_cotis === null) {
1182 return null;
1183 }
1184 return ($this->isCotis()) ?
1185 PdfModel::INVOICE_MODEL :
1186 PdfModel::RECEIPT_MODEL;
1187 break;
1188 default:
1189 return $this->$rname;
1190 break;
1191 }
1192 } else {
1193 Analog::log(
1194 "Unknown property '$rname'",
1195 Analog::WARNING
1196 );
1197 return null;
1198 }
1199 }
1200
1201 /**
1202 * Global setter method
1203 *
1204 * @param string $name name of the property we want to assign a value to
1205 * @param object $value a relevant value for the property
1206 *
1207 * @return void
1208 */
1209 public function __set($name, $value)
1210 {
1211 $forbidden = array('fields', 'is_cotis', 'end_date');
1212
1213 if (!in_array($name, $forbidden)) {
1214 $rname = '_' . $name;
1215 switch ($name) {
1216 case 'transaction':
1217 if (is_int($value)) {
1218 $this->$rname = new Transaction($this->zdb, $this->login, $value);
1219 } else {
1220 Analog::log(
1221 'Trying to set a transaction from an id that is not an integer.',
1222 Analog::WARNING
1223 );
1224 }
1225 break;
1226 case 'type':
1227 if (is_int($value)) {
1228 //set type
1229 $this->$rname = new ContributionsTypes($this->zdb, $value);
1230 //set is_cotis according to type
1231 if ($this->$rname->extension == 1) {
1232 $this->_is_cotis = true;
1233 } else {
1234 $this->_is_cotis = false;
1235 }
1236 } else {
1237 Analog::log(
1238 'Trying to set a type from an id that is not an integer.',
1239 Analog::WARNING
1240 );
1241 }
1242 break;
1243 case 'begin_date':
1244 try {
1245 $d = \DateTime::createFromFormat(__("Y-m-d"), $value);
1246 if ($d === false) {
1247 throw new \Exception('Incorrect format');
1248 }
1249 $this->_begin_date = $d->format('Y-m-d');
1250 } catch (\Exception $e) {
1251 Analog::log(
1252 'Wrong date format. field: ' . $key .
1253 ', value: ' . $value . ', expected fmt: ' .
1254 __("Y-m-d") . ' | ' . $e->getMessage(),
1255 Analog::INFO
1256 );
1257 $this->errors[] = str_replace(
1258 array(
1259 '%date_format',
1260 '%field'
1261 ),
1262 array(
1263 __("Y-m-d"),
1264 $this->_fields[$key]['label']
1265 ),
1266 _T("- Wrong date format (%date_format) for %field!")
1267 );
1268 }
1269 break;
1270 case 'amount':
1271 if (is_numeric($value) && $value > 0) {
1272 $this->$rname = $value;
1273 } else {
1274 Analog::log(
1275 'Trying to set an amount with a non numeric value, ' .
1276 'or with a zero value',
1277 Analog::WARNING
1278 );
1279 }
1280 break;
1281 case 'member':
1282 if (is_int($value)) {
1283 //set type
1284 $this->$rname = $value;
1285 }
1286 break;
1287 default:
1288 Analog::log(
1289 '[' . __CLASS__ . ']: Trying to set an unknown property (' .
1290 $name . ')',
1291 Analog::WARNING
1292 );
1293 break;
1294 }
1295 }
1296 }
1297 }