4 * Copyright © 2003-2024 The Galette Team
6 * This file is part of Galette (https://galette.eu).
8 * Galette is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
13 * Galette is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
18 * You should have received a copy of the GNU General Public License
19 * along with Galette. If not, see <http://www.gnu.org/licenses/>.
22 namespace Galette\Entity
;
26 use Galette\Events\GaletteEvent
;
27 use Galette\Features\HasEvent
;
30 use Laminas\Db\Sql\Expression
;
32 use Galette\Core\Login
;
33 use Galette\IO\ExternalScript
;
34 use Galette\IO\PdfContribution
;
35 use Galette\Repository\PaymentTypes
;
36 use Galette\Features\Dynamics
;
37 use Galette\Features\EntityHelper
;
40 * Contribution class for galette
41 * Manage membership fees and donations.
43 * @author Johan Cwiklinski <johan@x-tnd.be>
45 * @property integer $id
46 * @property string $date
47 * @property DateTime $raw_date
48 * @property integer $member
49 * @property ContributionsTypes $type
50 * @property double $amount
51 * @property integer $payment_type
52 * @property double $orig_amount
53 * @property string $info
54 * @property string $begin_date
55 * @property DateTime $raw_begin_date
56 * @property string $end_date
57 * @property DateTime $raw_end_date
58 * @property Transaction|null $transaction
59 * @property integer $extension
60 * @property integer $duration
61 * @property integer $model
62 * @property array<string, array<string, string>> $fields
69 getFieldLabel
as protected trait_getFieldLabel
;
70 __isset
as protected trait___isset
;
73 public const TABLE
= 'cotisations';
74 public const PK
= 'id_cotis';
76 public const TYPE_FEE
= 'fee';
77 public const TYPE_DONATION
= 'donation';
79 public const STATUS_NEVER
= -1;
80 public const STATUS_UNKNOWN
= 0;
81 public const STATUS_UPTODATE
= 1;
82 public const STATUS_DUEFREE
= 2;
83 public const STATUS_IMPENDING
= 3;
84 public const STATUS_LATE
= 4;
85 public const STATUS_OLD
= 5;
88 private ?
string $_date = null;
89 private ?
int $_member = null;
90 private ?ContributionsTypes
$_type = null;
91 private ?
float $_amount = null;
92 private ?
int $_payment_type;
93 private ?
float $_orig_amount = null;
94 private ?
string $_info = null;
95 private ?
string $_begin_date = null;
96 private ?
string $_end_date = null;
97 private ?Transaction
$_transaction = null;
98 private bool $_is_cotis;
99 private ?
int $_extension = null;
104 private Login
$login;
105 /** @var array<string> */
106 protected array $errors = [];
108 private bool $sendmail = false;
111 protected array $forbidden_fields = ['is_cotis'];
114 protected array $virtual_fields = [
123 * Default constructor
125 * @param Db $zdb Database
126 * @param Login $login Login instance
127 * @param null|int|array<string,mixed>|ArrayObject<string,int|string> $args Either a ResultSet row to load
128 * a specific contribution, or a type id
129 * to just instantiate object
131 public function __construct(Db
$zdb, Login
$login, int|
array|ArrayObject
$args = null)
134 $this->login
= $login;
137 $this->_payment_type
= $preferences->pref_default_paymenttype
;
143 ->withoutDeleteEvent()
149 } elseif (is_array($args)) {
150 $this->_date
= date("Y-m-d");
151 if (isset($args['adh']) && $args['adh'] != '') {
152 $this->_member
= (int)$args['adh'];
154 if (isset($args['trans'])) {
155 $this->_transaction
= new Transaction($this->zdb
, $this->login
, (int)$args['trans']);
156 if (!isset($this->_member
)) {
157 $this->_member
= $this->_transaction
->member
;
159 $this->_amount
= $this->_transaction
->getMissingAmount();
161 $this->setContributionType((int)$args['type']);
162 //calculate begin date for membership fee
163 $this->_begin_date
= $this->_date
;
164 if ($this->_is_cotis
) {
165 $due_date = self
::getDueDate($this->zdb
, $this->_member
);
166 if ($due_date != '') {
167 $now = new \
DateTime();
168 $due_date = new \
DateTime($due_date);
169 if ($due_date < $now) {
170 // Member didn't renew on time
171 $this->_begin_date
= $now->format('Y-m-d');
173 // Caution : the next_begin_date is the day after the due_date.
174 $next_begin_date = clone $due_date;
175 $next_begin_date->add(new \
DateInterval('P1D'));
176 $this->_begin_date
= $next_begin_date->format('Y-m-d');
179 $this->retrieveEndDate();
181 if (isset($args['payment_type'])) {
182 $this->_payment_type
= $args['payment_type'];
184 } elseif (is_object($args)) {
185 $this->loadFromRS($args);
188 $this->loadDynamicFields();
192 * Set fields, must populate $this->fields
196 protected function setFields(): self
198 $this->fields
= array(
200 'label' => _T('Contribution id'), //not a field in the form
203 Adherent
::PK
=> array(
204 'label' => _T("Contributor:"),
205 'propname' => 'member'
207 ContributionsTypes
::PK
=> array(
208 'label' => _T("Contribution type:"),
211 'montant_cotis' => array(
212 'label' => _T("Amount:"),
213 'propname' => 'amount'
215 'type_paiement_cotis' => array(
216 'label' => _T("Payment type:"),
217 'propname' => 'payment_type'
219 'info_cotis' => array(
220 'label' => _T("Comments:"),
223 'date_enreg' => array(
224 'label' => _T('Date'), //not a field in the form
227 'date_debut_cotis' => array(
228 'label' => _T("Date of contribution:"),
229 'cotlabel' => _T("Start date of membership:"), //if contribution is a membership fee, label differs
230 'propname' => 'begin_date'
232 'date_fin_cotis' => array(
233 'label' => _T("End date of membership:"),
234 'propname' => 'end_date'
236 Transaction
::PK
=> array(
237 'label' => _T('Transaction ID'), //not a field in the form
238 'propname' => 'transaction'
240 //this one is not really a field, but is required in some cases...
241 //adding it here make more simple to check required fields
242 'duree_mois_cotis' => array(
243 'label' => _T("Membership extension:"),
244 'propname' => 'extension'
253 * Sets end contribution date
257 private function retrieveEndDate(): void
261 $now = new \
DateTime();
262 $begin_date = new \
DateTime($this->_begin_date
);
263 if ($preferences->pref_beg_membership
!= '') {
264 //case beginning of membership
265 list($j, $m) = explode('/', $preferences->pref_beg_membership
);
266 $next_begin_date = new \
DateTime($begin_date->format('Y') . '-' . $m . '-' . $j);
267 while ($next_begin_date <= $begin_date) {
268 $next_begin_date->add(new \
DateInterval('P1Y'));
271 if ($preferences->pref_membership_offermonths
> 0) {
272 //count days until next membership begin date
273 $diff1 = (int)$now->diff($next_begin_date)->format('%a');
275 //count days between next membership begin date and offered months
276 $tdate = clone $next_begin_date;
277 $tdate->sub(new \
DateInterval('P' . $preferences->pref_membership_offermonths
. 'M'));
278 $diff2 = (int)$next_begin_date->diff($tdate)->format('%a');
280 //when number of days until next membership begin date is less than or equal to the offered months, it's free :)
281 if ($diff1 <= $diff2) {
282 $next_begin_date->add(new \
DateInterval('P1Y'));
286 // Caution : the end_date to retrieve is the day before the next_begin_date.
287 $end_date = clone $next_begin_date;
288 $end_date->sub(new \
DateInterval('P1D'));
289 $this->_end_date
= $end_date->format('Y-m-d');
290 } elseif ($preferences->pref_membership_ext
!= '') {
291 //case membership extension
292 if ($this->_extension
== null) {
293 $this->_extension
= $preferences->pref_membership_ext
;
295 $dext = new \
DateInterval('P' . $this->_extension
. 'M');
296 // Caution : the end_date to retrieve is the day before the next_begin_date.
297 $next_begin_date = $begin_date->add($dext);
298 $end_date = clone $next_begin_date;
299 $end_date->sub(new \
DateInterval('P1D'));
300 $this->_end_date
= $end_date->format('Y-m-d');
302 throw new \
RuntimeException(
303 'Unable to define end date; none of pref_beg_membership nor pref_membership_ext are defined!'
309 * Loads a contribution from its id
311 * @param int $id the identifier for the contribution to load
313 * @return bool true if query succeed, false otherwise
315 public function load(int $id): bool
317 if (!$this->login
->isLogged() && $this->login
->id
== '') {
322 $select = $this->zdb
->select(self
::TABLE
, 'c');
324 array('a' => PREFIX_DB
. Adherent
::TABLE
),
325 'c.' . Adherent
::PK
. '=a.' . Adherent
::PK
,
328 //restrict query on current member id if he's not admin nor staff member
329 if (!$this->login
->isAdmin() && !$this->login
->isStaff()) {
332 ->equalTo('a.' . Adherent
::PK
, $this->login
->id
)
334 ->equalTo('a.parent_id', $this->login
->id
)
337 ->equalTo('c.' . self
::PK
, $id)
340 $select->where
->equalTo(self
::PK
, $id);
343 $results = $this->zdb
->execute($select);
344 if ($results->count() > 0) {
345 $row = $results->current();
346 $this->loadFromRS($row);
350 'No contribution #' . $id . ' (user ' . $this->login
->id
. ')',
355 } catch (Throwable
$e) {
357 'An error occurred attempting to load contribution #' . $id .
366 * Populate object from a resultset row
368 * @param ArrayObject<string, int|string> $r the resultset row
372 private function loadFromRS(ArrayObject
$r): void
375 $this->_id
= (int)$r->$pk;
376 $this->_date
= $r->date_enreg
;
377 $this->_amount
= (double)$r->montant_cotis
;
378 //save original amount, we need it for transactions parts calculations
379 $this->_orig_amount
= (double)$r->montant_cotis
;
380 $this->_payment_type
= $r->type_paiement_cotis
;
381 $this->_info
= $r->info_cotis
;
382 $this->_begin_date
= $r->date_debut_cotis
;
383 $end_date = $r->date_fin_cotis
;
384 //do not work with knows bad dates...
385 //the one with BC comes from 0.63/pgsql demo... Why the hell a so
386 //strange date? don't know :(
388 $end_date !== '0000-00-00'
389 && $end_date !== '1901-01-01'
390 && $end_date !== '0001-01-01 BC'
392 $this->_end_date
= $r->date_fin_cotis
;
394 $adhpk = Adherent
::PK
;
395 $this->_member
= (int)$r->$adhpk;
397 $transpk = Transaction
::PK
;
398 if ($r->$transpk != '') {
399 $this->_transaction
= new Transaction($this->zdb
, $this->login
, (int)$r->$transpk);
402 $this->setContributionType((int)$r->id_type_cotis
);
403 $this->loadDynamicFields();
407 * Check posted values validity
409 * @param array<string,mixed> $values All values to check, basically the $_POST array
410 * after sending the form
411 * @param array<string,int> $required Array of required fields
412 * @param array<string> $disabled Array of disabled fields
414 * @return true|array<string>
416 public function check(array $values, array $required, array $disabled): bool|
array
419 $this->errors
= array();
421 $fields = array_keys($this->fields
);
422 foreach ($fields as $key) {
423 //first, let's sanitize values
424 $key = strtolower($key);
425 $prop = '_' . $this->fields
[$key]['propname'];
427 if (isset($values[$key])) {
428 $value = trim($values[$key]);
433 // if the field is enabled, check it
434 if (!isset($disabled[$key])) {
435 // fill up the adherent structure
436 //$this->$prop = stripslashes($value); //not relevant here!
438 // now, check validity
442 case 'date_debut_cotis':
443 case 'date_fin_cotis':
445 $this->setDate($key, $value);
450 $this->_member
= (int)$value;
453 case ContributionsTypes
::PK
:
455 $this->setContributionType((int)$value);
458 case 'montant_cotis':
459 $value = strtr($value, ',', '.');
460 if (!empty($value) ||
$value === '0') {
461 $this->_amount
= (double)$value;
463 if (!is_numeric($value) && $value !== '') {
464 $this->errors
[] = _T("- The amount must be an integer!");
467 case 'type_paiement_cotis':
468 $ptypes = new PaymentTypes(
473 $ptlist = $ptypes->getList();
474 if (isset($ptlist[$value])) {
475 $this->_payment_type
= (int)$value;
477 $this->errors
[] = _T("- Unknown payment type");
481 $this->_info
= $value;
483 case Transaction
::PK
:
485 $this->_transaction
= new Transaction($this->zdb
, $this->login
, (int)$value);
488 case 'duree_mois_cotis':
490 if (!is_numeric($value) ||
$value <= 0) {
491 $this->errors
[] = _T("- The duration must be a positive integer!");
493 $this->$prop = $value;
494 $this->retrieveEndDate();
501 // missing required fields?
502 foreach ($required as $key => $val) {
504 $prop = '_' . $this->fields
[$key]['propname'];
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
) == ''))
511 $this->errors
[] = str_replace(
513 '<a href="#' . $key . '">' . $this->getFieldLabel($key) . '</a>',
514 _T("- Mandatory field %field empty.")
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
;
525 $this->errors
[] = _T("- Sum of all contributions exceed corresponding transaction amount.");
529 if ($this->isFee() && count($this->errors
) == 0) {
530 $overlap = $this->checkOverlap();
531 if ($overlap !== true) {
532 //method directly return error message
533 $this->errors
[] = $overlap;
537 $this->dynamicsCheck($values, $required, $disabled);
539 if (count($this->errors
) > 0) {
541 'Some errors has been threw attempting to edit/store a contribution' .
542 print_r($this->errors
, true),
545 return $this->errors
;
548 'Contribution checked successfully.',
556 * Check that membership fees does not overlap
558 * @return boolean|string True if all is ok, false if error,
559 * error message if overlap
561 public function checkOverlap(): bool|
string
564 $select = $this->zdb
->select(self
::TABLE
, 'c');
565 //@phpstan-ignore-next-line
567 array('date_debut_cotis', 'date_fin_cotis')
569 array('ct' => PREFIX_DB
. ContributionsTypes
::TABLE
),
570 'c.' . ContributionsTypes
::PK
. '=ct.' . ContributionsTypes
::PK
,
572 )->where([Adherent
::PK
=> $this->_member
])
573 ->where(array('cotis_extension' => new Expression('true')))
575 ->greaterThanOrEqualTo('date_debut_cotis', $this->_begin_date
)
576 ->lessThanOrEqualTo('date_debut_cotis', $this->_end_date
)
579 ->greaterThanOrEqualTo('date_fin_cotis', $this->_begin_date
)
580 ->lessThanOrEqualTo('date_fin_cotis', $this->_end_date
);
582 if ($this->id
!= '') {
583 $select->where
->notEqualTo(self
::PK
, $this->id
);
586 $results = $this->zdb
->execute($select);
587 if ($results->count() > 0) {
588 $result = $results->current();
590 $d_begin = new \
DateTime($result->date_debut_cotis
);
591 $d_end = new \
DateTime($result->date_fin_cotis
);
593 if ($d_begin->format('m-d') == $d_end->format('m-d') && $result->date_fin_cotis
== $this->_begin_date
) {
594 //see https://bugs.galette.eu/issues/1762
598 return _T("- Membership period overlaps period starting at ") .
599 $d_begin->format(__("Y-m-d"));
602 } catch (Throwable
$e) {
604 'An error occurred checking overlapping fee. ' . $e->getMessage(),
612 * Store the contribution
616 public function store(): bool
618 global $hist, $emitter;
622 if (count($this->errors
) > 0) {
623 throw new \
RuntimeException(
624 'Existing errors prevents storing contribution: ' .
625 print_r($this->errors
, true)
630 $this->zdb
->connection
->beginTransaction();
632 $fields = self
::getDbFields($this->zdb
);
633 foreach ($fields as $field) {
634 $prop = '_' . $this->fields
[$field]['propname'];
635 if (!isset($this->$prop)) {
639 case ContributionsTypes
::PK
:
640 case Transaction
::PK
:
641 $values[$field] = $this->$prop->id
;
644 $values[$field] = $this->$prop;
649 //no end date, let's take database defaults
650 if (!$this->isFee() && !$this->_end_date
) {
651 unset($values['date_fin_cotis']);
654 if (!isset($this->_id
) ||
$this->_id
== '') {
655 //we're inserting a new contribution
656 unset($values[self
::PK
]);
658 $insert = $this->zdb
->insert(self
::TABLE
);
659 $insert->values($values);
660 $add = $this->zdb
->execute($insert);
662 if ($add->count() > 0) {
663 $this->_id
= $this->zdb
->getLastGeneratedValue($this);
667 _T("Contribution added"),
668 Adherent
::getSName($this->zdb
, $this->_member
)
670 $event = $this->getAddEventName();
672 $hist->add(_T("Fail to add new contribution."));
673 throw new \
Exception(
674 'An error occurred inserting new contribution!'
678 //we're editing an existing contribution
679 $update = $this->zdb
->update(self
::TABLE
);
680 $update->set($values)->where([self
::PK
=> $this->_id
]);
681 $edit = $this->zdb
->execute($update);
683 //edit == 0 does not mean there were an error, but that there
684 //were nothing to change
685 if ($edit->count() > 0) {
687 _T("Contribution updated"),
688 Adherent
::getSName($this->zdb
, $this->_member
)
692 $event = $this->getEditEventName();
695 if ($this->isFee()) {
696 $this->updateDeadline();
700 $this->dynamicsStore(true);
702 $this->zdb
->connection
->commit();
703 $this->_orig_amount
= $this->_amount
;
705 //send event at the end of process, once all has been stored
706 if ($event !== null && $this->areEventsEnabled()) {
707 $emitter->dispatch(new GaletteEvent($event, $this));
711 } catch (Throwable
$e) {
712 if ($this->zdb
->connection
->inTransaction()) {
713 $this->zdb
->connection
->rollBack();
720 * Update member deadline
724 private function updateDeadline(): bool
727 $due_date = self
::getDueDate($this->zdb
, $this->_member
);
729 if ($due_date != '') {
730 $due_date_update = $due_date;
732 $due_date_update = new Expression('NULL');
735 $update = $this->zdb
->update(Adherent
::TABLE
);
737 array('date_echeance' => $due_date_update)
739 [Adherent
::PK
=> $this->_member
]
741 $this->zdb
->execute($update);
743 } catch (Throwable
$e) {
745 'An error occurred updating member ' . $this->_member
.
755 * Remove contribution from database
757 * @param boolean $transaction Activate transaction mode (defaults to true)
761 public function remove(bool $transaction = true): bool
767 $this->zdb
->connection
->beginTransaction();
770 $delete = $this->zdb
->delete(self
::TABLE
);
771 $delete->where([self
::PK
=> $this->_id
]);
772 $del = $this->zdb
->execute($delete);
773 if ($del->count() > 0) {
774 $this->updateDeadline();
775 $this->dynamicsRemove(true);
778 'Contribution has not been removed!',
784 $this->zdb
->connection
->commit();
786 $emitter->dispatch(new GaletteEvent('contribution.remove', $this));
788 } catch (Throwable
$e) {
790 $this->zdb
->connection
->rollBack();
793 'An error occurred trying to remove contribution #' .
794 $this->_id
. ' | ' . $e->getMessage(),
804 * @param string $field Field name
805 * @param string $entry Array entry to use (defaults to "label")
809 public function getFieldLabel(string $field, string $entry = 'label'): string
811 if ($field == 'date_debut_cotis' && !empty($this->_is_cotis
) && $this->isFee()) {
814 return $this->trait_getFieldLabel($field, $entry);
818 * Retrieve fields from database
820 * @param Db $zdb Database instance
822 * @return array<string>
824 public static function getDbFields(Db
$zdb): array
826 $columns = $zdb->getColumns(self
::TABLE
);
828 foreach ($columns as $col) {
829 $fields[] = $col->getName();
835 * Get the relevant CSS class for current contribution
837 * @return string current contribution row class
839 public function getRowClass(): string
841 return ($this->_end_date
!= $this->_begin_date
&& $this->_is_cotis
) ?
842 'cotis-normal' : 'cotis-give';
846 * Retrieve member due date
848 * @param Db $zdb Database instance
849 * @param ?integer $member_id Member identifier
851 * @return string|null
853 public static function getDueDate(Db
$zdb, ?
int $member_id): ?
string
859 $select = $zdb->select(self
::TABLE
, 'c');
862 'max_date' => new Expression('MAX(date_fin_cotis)')
865 array('ct' => PREFIX_DB
. ContributionsTypes
::TABLE
),
866 'c.' . ContributionsTypes
::PK
. '=ct.' . ContributionsTypes
::PK
,
869 [Adherent
::PK
=> $member_id]
871 array('cotis_extension' => new Expression('true'))
874 $results = $zdb->execute($select);
875 $result = $results->current();
876 $due_date = $result->max_date
;
878 //avoid bad dates in postgres and bad mysql return from zenddb
879 if ($due_date == '0001-01-01 BC' ||
$due_date == '1901-01-01') {
883 } catch (Throwable
$e) {
885 'An error occurred trying to retrieve member\'s due date',
893 * Detach a contribution from a transaction
895 * @param Db $zdb Database instance
896 * @param Login $login Login instance
897 * @param int $trans_id Transaction identifier
898 * @param int $contrib_id Contribution identifier
902 public static function unsetTransactionPart(Db
$zdb, Login
$login, int $trans_id, int $contrib_id): bool
905 //first, we check if contribution is part of transaction
906 $c = new Contribution($zdb, $login, (int)$contrib_id);
907 if ($c->isTransactionPartOf($trans_id)) {
908 $update = $zdb->update(self
::TABLE
);
910 array(Transaction
::PK
=> null)
912 [self
::PK
=> $contrib_id]
914 $zdb->execute($update);
918 'Contribution #' . $contrib_id .
919 ' is not actually part of transaction #' . $trans_id,
924 } catch (Throwable
$e) {
926 'Unable to detach contribution #' . $contrib_id .
927 ' to transaction #' . $trans_id . ' | ' . $e->getMessage(),
935 * Set a contribution as a transaction part
937 * @param Db $zdb Database instance
938 * @param int $trans_id Transaction identifier
939 * @param int $contrib_id Contribution identifier
943 public static function setTransactionPart(Db
$zdb, int $trans_id, int $contrib_id): bool
946 $update = $zdb->update(self
::TABLE
);
948 array(Transaction
::PK
=> $trans_id)
949 )->where([self
::PK
=> $contrib_id]);
951 $zdb->execute($update);
953 } catch (Throwable
$e) {
955 'Unable to attach contribution #' . $contrib_id .
956 ' to transaction #' . $trans_id . ' | ' . $e->getMessage(),
964 * Is current contribution a membership fee
968 public function isFee(): bool
970 return $this->_is_cotis
;
974 * Is current contribution part of specified transaction
976 * @param int $id Transaction identifier
980 public function isTransactionPartOf(int $id): bool
982 if ($this->isTransactionPart()) {
983 return $id == $this->_transaction
->id
;
990 * Is current contribution part of transaction
994 public function isTransactionPart(): bool
996 return $this->_transaction
!= null;
1000 * Execute post contribution script
1002 * @param ExternalScript $es External script to execute
1003 * @param ?array<string,mixed> $extra Extra information on contribution
1005 * @param ?array<string,mixed> $pextra Extra information on payment
1008 * @return string|bool Script return value on success, values and script output on fail
1010 public function executePostScript(
1012 array $extra = null,
1013 array $pextra = null
1015 global $preferences;
1018 'type' => $this->getPaymentType()
1021 if ($pextra !== null) {
1022 $payment = array_merge($payment, $pextra);
1025 if (!file_exists(GALETTE_CACHE_DIR
. '/pdf_contribs')) {
1026 @mkdir
(GALETTE_CACHE_DIR
. '/pdf_contribs');
1029 $voucher_path = null;
1030 if (isset($this->_id
)) {
1031 $voucher = new PdfContribution($this, $this->zdb
, $preferences);
1032 $voucher->store(GALETTE_CACHE_DIR
. '/pdf_contribs');
1033 $voucher_path = $voucher->getPath();
1038 'date' => $this->_date
,
1039 'type' => $this->getRawType(),
1040 'amount' => $this->amount
,
1041 'voucher' => $voucher_path,
1042 'category' => array(
1043 'id' => $this->type
->id
,
1044 'label' => $this->type
->libelle
1046 'payment' => $payment
1049 if ($this->_member
!== null) {
1050 $m = new Adherent($this->zdb
, (int)$this->_member
);
1052 'id' => (int)$this->_member
,
1053 'name' => $m->sfullname
,
1054 'email' => $m->email
,
1055 'organization' => ($m->isCompany() ?
1 : 0),
1058 'label' => $m->sstatus
1060 'country' => $m->country
1063 if ($m->isCompany()) {
1064 $member['organization_name'] = $m->company_name
;
1067 $contrib['member'] = $member;
1070 if ($extra !== null) {
1071 $contrib = array_merge($contrib, $extra);
1074 $res = $es->send($contrib);
1076 if ($res !== true) {
1078 'An error occurred calling post contribution ' .
1079 "script:\n" . $es->getOutput(),
1082 $res = _T("Contribution information") . "\n";
1083 $res .= print_r($contrib, true);
1084 $res .= "\n\n" . _T("Script output") . "\n";
1085 $res .= $es->getOutput();
1091 * Get raw contribution type
1095 public function getRawType(): string
1097 if ($this->isFee()) {
1098 return 'membership';
1105 * Get contribution type label
1109 public function getTypeLabel(): string
1111 if ($this->isFee()) {
1112 return _T("Membership");
1114 return _T("Donation");
1119 * Get payment type label
1123 public function getPaymentType(): string
1125 if ($this->_payment_type
=== null) {
1129 $ptype = new PaymentType($this->zdb
, $this->payment_type
);
1130 return $ptype->getName();
1134 * Global getter method
1136 * @param string $name name of the property we want to retrieve
1138 * @return mixed the called property
1140 public function __get(string $name)
1142 $rname = '_' . $name;
1144 if (in_array($name, $this->forbidden_fields
)) {
1146 "Call to __get for '$name' is forbidden!",
1152 return $this->isFee();
1154 throw new \
RuntimeException("Call to __get for '$name' is forbidden!");
1157 property_exists($this, $rname)
1158 ||
property_exists($this, $name)
1159 ||
in_array($name, $this->virtual_fields
)
1163 case 'raw_begin_date':
1164 case 'raw_end_date':
1165 $rname = '_' . substr($name, 4);
1166 return $this->getDate($rname, false);
1170 return $this->getDate($rname);
1172 if (isset($this->_is_cotis
)) {
1173 // Caution : the end_date stored is actually the due date.
1174 // Adding a day to compute the next_begin_date is required
1175 // to return the right number of months.
1176 $next_begin_date = new \
DateTime($this->_end_date ??
$this->_begin_date
);
1177 $next_begin_date->add(new \
DateInterval('P1D'));
1178 $begin_date = new \
DateTime($this->_begin_date
);
1179 $diff = $next_begin_date->diff($begin_date);
1180 return (int)$diff->format('%y') * 12 +
(int)$diff->format('%m');
1185 if (!isset($this->_is_cotis
)) {
1188 return ($this->isFee()) ?
1189 PdfModel
::INVOICE_MODEL
: PdfModel
::RECEIPT_MODEL
;
1191 return $this->fields
;
1193 if (property_exists($this, $rname)) {
1194 if (isset($this->$rname)) {
1195 return $this->$rname;
1198 throw new \
LogicException("Property '" . __CLASS__
. "::$rname' does not exist!");
1203 "Unknown property '$rname'",
1211 * Global isset method
1212 * Required for twig to access properties via __get
1214 * @param string $name name of the property we want to retrieve
1218 public function __isset(string $name): bool
1220 return $this->trait___isset('_' . $name) ||
$this->trait___isset($name);
1225 * Global setter method
1227 * @param string $name name of the property we want to assign a value to
1228 * @param mixed $value a relevant value for the property
1232 public function __set(string $name, $value): void
1234 global $preferences;
1236 $forbidden = array('fields', 'is_cotis', 'end_date');
1238 if (!in_array($name, $forbidden)) {
1239 $rname = '_' . $name;
1242 if (is_int($value)) {
1243 $this->$rname = new Transaction($this->zdb
, $this->login
, $value);
1246 'Trying to set a transaction from an id that is not an integer.',
1252 $this->setContributionType($value);
1255 $this->setDate($rname, $value);
1258 if (is_numeric($value) && $value > 0) {
1259 $this->$rname = $value;
1262 'Trying to set an amount with a non numeric value, ' .
1263 'or with a zero value',
1269 if (is_int($value)) {
1271 $this->$rname = $value;
1274 case 'payment_type':
1275 $ptypes = new PaymentTypes(
1280 $list = $ptypes->getList();
1281 if (isset($list[$value])) {
1282 $this->_payment_type
= $value;
1285 'Unknown payment type ' . $value,
1292 '[' . __CLASS__
. ']: Trying to set an unknown property (' .
1302 * Flag creation mail sending
1304 * @param boolean $send True (default) to send creation email
1308 public function setSendmail(bool $send = true): self
1310 $this->sendmail
= $send;
1315 * Should we send administrative emails to member?
1319 public function sendEMail(): bool
1321 return $this->sendmail
;
1325 * Handle files (dynamics files)
1327 * @param array<string, mixed> $files Files sent
1329 * @return array<string>|true
1331 public function handleFiles(array $files): bool|
array
1335 $this->dynamicsFiles($files);
1337 if (count($this->errors
) > 0) {
1339 'Some errors has been threw attempting to edit/store a contribution files' . "\n" .
1340 print_r($this->errors
, true),
1343 return $this->errors
;
1350 * Get required fields list
1352 * @return array<string, int>
1354 public function getRequired(): array
1357 'id_type_cotis' => 1,
1360 'date_debut_cotis' => 1,
1361 'date_fin_cotis' => $this->isFee() ?
1 : 0,
1362 'montant_cotis' => $this->isFee() ?
1 : 0
1367 * Can current logged-in user display contribution
1369 * @param Login $login Login instance
1373 public function canShow(Login
$login): bool
1375 //non-logged-in members cannot show contributions
1376 if (!$login->isLogged()) {
1380 //admin and staff users can edit, as well as member itself
1381 if (!$this->id ||
$login->id
== $this->_member ||
$login->isAdmin() ||
$login->isStaff()) {
1385 //parent can see their children contributions
1386 $parent = new Adherent($this->zdb
);
1389 ->enableDep('children')
1390 ->load($this->login
->id
);
1391 if ($parent->hasChildren()) {
1392 foreach ($parent->children
as $child) {
1393 if ($child->id
=== $this->_member
) {
1404 * Set contribution type and determine if it is a contribution or a donation
1406 * @param int|null $type Type
1410 public function setContributionType(?
int $type): self
1412 if (is_int($type)) {
1414 $this->_type
= new ContributionsTypes($this->zdb
, $type);
1415 //set is_cotis according to type
1416 if ($this->_type
->extension
== 1) {
1417 $this->_is_cotis
= true;
1419 $this->_is_cotis
= false;
1423 'Trying to set a type from an id that is not an integer.',
1432 * Get prefix for events
1436 protected function getEventsPrefix(): string
1438 return 'contribution';