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