]> git.agnieray.net Git - galette.git/blob - galette/lib/Galette/Repository/Contributions.php
1f418d9986968cb39af06dcda14c3dc3ceb4a589
[galette.git] / galette / lib / Galette / Repository / Contributions.php
1 <?php
2
3 /**
4 * Copyright © 2003-2024 The Galette Team
5 *
6 * This file is part of Galette (https://galette.eu).
7 *
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.
12 *
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.
17 *
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/>.
20 */
21
22 namespace Galette\Repository;
23
24 use ArrayObject;
25 use Laminas\Db\ResultSet\ResultSet;
26 use Throwable;
27 use Analog\Analog;
28 use Laminas\Db\Sql\Expression;
29 use Galette\Core\Db;
30 use Galette\Core\Login;
31 use Galette\Core\History;
32 use Galette\Entity\Contribution;
33 use Galette\Entity\Adherent;
34 use Galette\Entity\Transaction;
35 use Galette\Entity\ContributionsTypes;
36 use Galette\Filters\ContributionsList;
37 use Laminas\Db\Sql\Select;
38
39 /**
40 * Contributions class for galette
41 *
42 * @author Johan Cwiklinski <johan@x-tnd.be>
43 */
44 class Contributions
45 {
46 public const TABLE = Contribution::TABLE;
47 public const PK = Contribution::PK;
48
49 private ContributionsList $filters;
50 private int $count = 0;
51
52 private Db $zdb;
53 private Login $login;
54 private float $sum = 0;
55 /** @var array<int> */
56 private array $current_selection;
57
58 /**
59 * Default constructor
60 *
61 * @param Db $zdb Database
62 * @param Login $login Login
63 * @param ?ContributionsList $filters Filtering
64 */
65 public function __construct(Db $zdb, Login $login, ?ContributionsList $filters = null)
66 {
67 $this->zdb = $zdb;
68 $this->login = $login;
69
70 if ($filters === null) {
71 $this->filters = new ContributionsList();
72 } else {
73 $this->filters = $filters;
74 }
75 }
76
77 /**
78 * Get contributions list for a specific transaction
79 *
80 * @param int $trans_id Transaction identifier
81 *
82 * @return Contribution[]
83 */
84 public function getListFromTransaction(int $trans_id): array
85 {
86 $this->filters->from_transaction = $trans_id;
87 return $this->getList(true);
88 }
89
90 /**
91 * Get contributions list for a specific transaction
92 *
93 * @param array<int> $ids an array of members id that has been selected
94 * @param bool $as_contrib return the results as an array of
95 * @param ?array<string> $fields field(s) name(s) to get. Should be a string or
96 * an array. If null, all fields will be returned
97 *
98 * @return array<int, Contribution>|false
99 */
100 public function getArrayList(array $ids, bool $as_contrib = false, ?array $fields = null): array|false
101 {
102 if (count($ids) < 1) {
103 Analog::log('No contribution selected.', Analog::INFO);
104 return false;
105 }
106
107 $this->current_selection = $ids;
108 $list = $this->getList($as_contrib, $fields);
109 $array_list = [];
110 foreach ($list as $entry) {
111 $array_list[] = $entry;
112 }
113 return $array_list;
114 }
115
116 /**
117 * Get contributions list
118 *
119 * @param bool $as_contrib return the results as an array of
120 * Contribution object.
121 * @param ?array<string> $fields field(s) name(s) to get. Should be a string or
122 * an array. If null, all fields will be returned
123 *
124 * @return array<int, Contribution>|ResultSet
125 */
126 public function getList(bool $as_contrib = false, ?array $fields = null): array|ResultSet
127 {
128 try {
129 $select = $this->buildSelect($fields);
130
131 $this->filters->setLimits($select);
132
133 $contributions = array();
134 $results = $this->zdb->execute($select);
135 if ($as_contrib) {
136 foreach ($results as $row) {
137 $contributions[] = new Contribution($this->zdb, $this->login, $row);
138 }
139 } else {
140 $contributions = $results;
141 }
142 return $contributions;
143 } catch (Throwable $e) {
144 Analog::log(
145 'Cannot list contributions | ' . $e->getMessage(),
146 Analog::WARNING
147 );
148 throw $e;
149 }
150 }
151
152 /**
153 * Builds the SELECT statement
154 *
155 * @param ?array<string> $fields fields list to retrieve
156 *
157 * @return Select SELECT statement
158 */
159 private function buildSelect(?array $fields): Select
160 {
161 try {
162 $fieldsList = ['*'];
163 if (is_array($fields) && count($fields)) {
164 $fieldsList = $fields;
165 }
166
167 $select = $this->zdb->select(self::TABLE, 'a');
168 $select->columns($fieldsList);
169
170 $select->join(
171 array('p' => PREFIX_DB . Adherent::TABLE),
172 'a.' . Adherent::PK . '= p.' . Adherent::PK,
173 array()
174 );
175
176 $this->buildWhereClause($select);
177 $select->order(self::buildOrderClause());
178
179 $this->calculateSum($select);
180
181 $this->proceedCount($select);
182
183 return $select;
184 } catch (Throwable $e) {
185 Analog::log(
186 'Cannot build SELECT clause for contributions | ' . $e->getMessage(),
187 Analog::WARNING
188 );
189 throw $e;
190 }
191 }
192
193 /**
194 * Count contributions from the query
195 *
196 * @param Select $select Original select
197 *
198 * @return void
199 */
200 private function proceedCount(Select $select): void
201 {
202 try {
203 $countSelect = clone $select;
204 $countSelect->reset($countSelect::COLUMNS);
205 $countSelect->reset($countSelect::JOINS);
206 $countSelect->reset($countSelect::ORDER);
207 $countSelect->columns(
208 array(
209 self::PK => new Expression('COUNT(' . self::PK . ')')
210 )
211 );
212
213 $results = $this->zdb->execute($countSelect);
214 $result = $results->current();
215
216 $k = self::PK;
217 $this->count = $result->$k;
218 $this->filters->setCounter($this->count);
219 } catch (Throwable $e) {
220 Analog::log(
221 'Cannot count contributions | ' . $e->getMessage(),
222 Analog::WARNING
223 );
224 throw $e;
225 }
226 }
227
228 /**
229 * Calculate sum of all selected contributions
230 *
231 * @param Select $select Original select
232 *
233 * @return void
234 */
235 private function calculateSum(Select $select): void
236 {
237 try {
238 $sumSelect = clone $select;
239 $sumSelect->reset($sumSelect::COLUMNS);
240 $sumSelect->reset($sumSelect::JOINS);
241 $sumSelect->reset($sumSelect::ORDER);
242 $sumSelect->columns(
243 array(
244 'contribsum' => new Expression('SUM(montant_cotis)')
245 )
246 );
247
248 $results = $this->zdb->execute($sumSelect);
249 $result = $results->current();
250 if ($result->contribsum) {
251 $this->sum = round($result->contribsum, 2);
252 }
253 } catch (Throwable $e) {
254 Analog::log(
255 'Cannot calculate contributions sum | ' . $e->getMessage(),
256 Analog::WARNING
257 );
258 throw $e;
259 }
260 }
261
262 /**
263 * Builds the order clause
264 *
265 * @return array<string> SQL ORDER clauses
266 */
267 private function buildOrderClause(): array
268 {
269 $order = array();
270
271 switch ($this->filters->orderby) {
272 case ContributionsList::ORDERBY_ID:
273 $order[] = Contribution::PK . ' ' . $this->filters->ordered;
274 break;
275 case ContributionsList::ORDERBY_DATE:
276 $order[] = 'date_enreg ' . $this->filters->ordered;
277 break;
278 case ContributionsList::ORDERBY_BEGIN_DATE:
279 $order[] = 'date_debut_cotis ' . $this->filters->ordered;
280 break;
281 case ContributionsList::ORDERBY_END_DATE:
282 $order[] = 'date_fin_cotis ' . $this->filters->ordered;
283 break;
284 case ContributionsList::ORDERBY_MEMBER:
285 $order[] = 'nom_adh ' . $this->filters->ordered;
286 $order[] = 'prenom_adh ' . $this->filters->ordered;
287 break;
288 case ContributionsList::ORDERBY_TYPE:
289 $order[] = ContributionsTypes::PK;
290 break;
291 case ContributionsList::ORDERBY_AMOUNT:
292 $order[] = 'montant_cotis ' . $this->filters->ordered;
293 break;
294 /*
295 Hum... I really do not know how to sort a query with a value that
296 is calculated code side :/
297 case ContributionsList::ORDERBY_DURATION:
298 break;*/
299 case ContributionsList::ORDERBY_PAYMENT_TYPE:
300 $order[] = 'type_paiement_cotis ' . $this->filters->ordered;
301 break;
302 default:
303 $order[] = $this->filters->orderby . ' ' . $this->filters->ordered;
304 break;
305 }
306
307 return $order;
308 }
309
310 /**
311 * Builds where clause, for filtering on simple list mode
312 *
313 * @param Select $select Original select
314 *
315 * @return void
316 */
317 private function buildWhereClause(Select $select): void
318 {
319 $field = 'date_debut_cotis';
320
321 switch ($this->filters->date_field) {
322 case ContributionsList::DATE_RECORD:
323 $field = 'date_enreg';
324 break;
325 case ContributionsList::DATE_END:
326 $field = 'date_fin_cotis';
327 break;
328 case ContributionsList::DATE_BEGIN:
329 default:
330 $field = 'date_debut_cotis';
331 break;
332 }
333
334 if (isset($this->current_selection)) {
335 $select->where->in('a.' . self::PK, $this->current_selection);
336 }
337
338 try {
339 if ($this->filters->start_date_filter != null) {
340 $d = new \DateTime($this->filters->rstart_date_filter);
341 $select->where->greaterThanOrEqualTo(
342 $field,
343 $d->format('Y-m-d')
344 );
345 }
346
347 if ($this->filters->end_date_filter != null) {
348 $d = new \DateTime($this->filters->rend_date_filter);
349 $select->where->lessThanOrEqualTo(
350 $field,
351 $d->format('Y-m-d')
352 );
353 }
354
355 if ($this->filters->payment_type_filter !== null) {
356 $select->where->equalTo(
357 'type_paiement_cotis',
358 $this->filters->payment_type_filter
359 );
360 }
361
362 if ($this->filters->from_transaction !== false) {
363 $select->where->equalTo(
364 Transaction::PK,
365 $this->filters->from_transaction
366 );
367 }
368
369 if ($this->filters->max_amount !== null) {
370 $select->where(
371 '(montant_cotis <= ' . $this->filters->max_amount .
372 ' OR montant_cotis IS NULL)'
373 );
374 }
375
376 $member_clause = null;
377 if ($this->filters->filtre_cotis_adh != null) {
378 $member_clause = [$this->filters->filtre_cotis_adh];
379 if (!$this->login->isAdmin() && !$this->login->isStaff() && $this->filters->filtre_cotis_adh != $this->login->id) {
380 $member = new Adherent(
381 $this->zdb,
382 (int)$this->filters->filtre_cotis_adh,
383 [
384 'picture' => false,
385 'groups' => false,
386 'dues' => false,
387 'parent' => true
388 ]
389 );
390 if (
391 !$member->hasParent() ||
392 $member->parent->id != $this->login->id
393 ) {
394 Analog::log(
395 'Trying to display contributions for member #' . $member->id .
396 ' without appropriate ACLs',
397 Analog::WARNING
398 );
399 $this->filters->filtre_cotis_adh = $this->login->id;
400 $member_clause = [$this->login->id];
401 }
402 }
403 } elseif ($this->filters->filtre_cotis_children !== false) {
404 $member_clause = [$this->login->id];
405 $member = new Adherent(
406 $this->zdb,
407 (int)$this->filters->filtre_cotis_children,
408 [
409 'picture' => false,
410 'groups' => false,
411 'dues' => false,
412 'children' => true
413 ]
414 );
415 foreach ($member->children as $child) {
416 $member_clause[] = $child->id;
417 }
418 } elseif (!$this->login->isAdmin() && !$this->login->isStaff()) {
419 //non staff members can only view their own contributions
420 $member_clause = $this->login->id;
421 }
422
423 if ($member_clause !== null) {
424 $select->where(
425 array(
426 'a.' . Adherent::PK => $member_clause
427 )
428 );
429 }
430
431 if ($this->filters->filtre_transactions === true) {
432 $select->where('a.trans_id IS NULL');
433 }
434 } catch (Throwable $e) {
435 Analog::log(
436 __METHOD__ . ' | ' . $e->getMessage(),
437 Analog::WARNING
438 );
439 throw $e;
440 }
441 }
442
443 /**
444 * Get count for current query
445 *
446 * @return int
447 */
448 public function getCount(): int
449 {
450 return $this->count;
451 }
452
453 /**
454 * Get sum
455 *
456 * @return float
457 */
458 public function getSum(): float
459 {
460 return $this->sum;
461 }
462
463 /**
464 * Remove specified contributions
465 *
466 * @param integer|array<int> $ids Contributions identifiers to delete
467 * @param History $hist History
468 * @param boolean $transaction True to begin a database transaction
469 *
470 * @return boolean
471 */
472 public function remove(int|array $ids, History $hist, bool $transaction = true): bool
473 {
474 $list = array();
475 if (is_array($ids)) {
476 $list = $ids;
477 } elseif (is_numeric($ids)) {
478 $list = [(int)$ids];
479 } else {
480 //not numeric and not an array: incorrect.
481 Analog::log(
482 'Asking to remove contribution, but without providing an array or a single numeric value.',
483 Analog::WARNING
484 );
485 return false;
486 }
487
488 try {
489 if ($transaction) {
490 $this->zdb->connection->beginTransaction();
491 }
492 $select = $this->zdb->select(self::TABLE);
493 $select->where->in(self::PK, $list);
494 $contributions = $this->zdb->execute($select);
495 foreach ($contributions as $contribution) {
496 $c = new Contribution($this->zdb, $this->login, $contribution);
497 $res = $c->remove(false);
498 if ($res === false) {
499 throw new \Exception();
500 }
501 }
502 if ($transaction) {
503 $this->zdb->connection->commit();
504 }
505 $hist->add(
506 str_replace(
507 '%list',
508 print_r($list, true),
509 _T("Contributions deleted (%list)")
510 )
511 );
512 return true;
513 } catch (Throwable $e) {
514 if ($transaction) {
515 $this->zdb->connection->rollBack();
516 }
517 Analog::log(
518 'An error occurred trying to remove contributions | ' .
519 $e->getMessage(),
520 Analog::ERROR
521 );
522 throw $e;
523 }
524 }
525 }