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