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