976526aeaa7bbb837604fc54dcc54ca054a7d0d4
[memberdb.git] / include / fees.php
1 <?php
2
3 use MemberDB\Config\Config;
4
5 // HELPER functions FOR FEES
6
7 // build an empty structure for holding monthly information
8 function _fees_build_month_array($start_date, $end_date, $preset = array())
9 {
10     $start_info = getdate($start_date);
11     $end_info = getdate($end_date);
12
13     if ($start_info['year'] > $end_info['year']) {
14         return $preset;
15     }
16     if ($start_info['year'] == $end_info['year'] && $start_info['mon'] > $end_info['mon']) {
17         return $preset;
18     }
19
20     $ret = $preset;
21     for ($year = $start_info['year']; $year <= $end_info['year']; $year++) {
22         if (!isset($ret[$year])) {
23             $ret[$year] = array();
24         }
25         for (
26             $month = (($year == $start_info['year']) ? $start_info['mon'] : 1);
27             $month <= (($year == $end_info['year']) ? $end_info['mon'] : 12);
28             $month++
29         ) {
30             if (isset($ret[$year][$month])) {
31                 continue;
32             }
33             $ret[$year][$month] = array(
34                 'is_member'        => null,
35                 'member_type'      => null,
36                 'fee'              => null,
37                 'payment_interval' => null
38             );
39         }
40     }
41     return $ret;
42 }
43
44 function _fees_apply_event_information(&$info, $events)
45 {
46     $config = Config::getInstance();
47     if (!empty($events)) {
48
49         foreach ($events as $event) {
50
51             $timestamp = db_date2unixtime($event['event_date']);
52             if ($timestamp < $config->get('founding_date')) {
53                 $timestamp = $config->get('founding_date');
54             }
55             $date_info = getdate($timestamp);
56
57             if (!isset($info[$date_info['year']])) {
58                 continue;
59             }
60             if (!isset($info[$date_info['year']][$date_info['mon']])) {
61                 continue;
62             }
63
64             if ($event['fee'] !== null) {
65                 $info[$date_info['year']][$date_info['mon']]['fee'] = $event['fee'];
66             }
67             if ($event['member_type'] !== null) {
68                 $info[$date_info['year']][$date_info['mon']]['member_type'] = $event['member_type'];
69             }
70             if ($event['event_type'] !== 'changed') {
71                 $info[$date_info['year']][$date_info['mon']]['is_member'] = ($event['event_type'] == 'joined' ? 1 : 0);
72             }
73             if ($event['payment_interval'] !== null) {
74                 $info[$date_info['year']][$date_info['mon']]['payment_interval'] = $event['payment_interval'];
75             }
76         }
77     }
78
79     $fee = 0;
80     $member_type = null;
81     $is_member = false;
82     $payment_interval = null;
83
84     foreach (array_keys($info) as $year) {
85         foreach (array_keys($info[$year]) as $month) {
86             if (!isset($info[$year][$month]['fee'])) {
87                 $info[$year][$month]['fee'] = $fee;
88             } else {
89                 $fee = $info[$year][$month]['fee'];
90             }
91
92             if (!isset($info[$year][$month]['member_type'])) {
93                 $info[$year][$month]['member_type'] = $member_type;
94             } else {
95                 $member_type = $info[$year][$month]['member_type'];
96             }
97
98             if (!isset($info[$year][$month]['is_member'])) {
99                 $info[$year][$month]['is_member'] = $is_member;
100             } else {
101                 $is_member = $info[$year][$month]['is_member'];
102             }
103
104             if (!isset($info[$year][$month]['payment_interval'])) {
105                 $info[$year][$month]['payment_interval'] = $payment_interval;
106             } else {
107                 $payment_interval = $info[$year][$month]['payment_interval'];
108             }
109         }
110     }
111     return;
112 }
113
114 function fees_get_list_for_member($member_id, $end_date)
115 {
116     static $cache = array();
117     $config = Config::getInstance();
118
119     $end_date = mktime(0, 0, 0, date('m', $end_date) + 1, 0, date('Y', $end_date)); // last day of given month
120
121     if (isset($cache[$member_id][$end_date])) {
122         return $cache[$member_id][$end_date];
123     }
124     if (isset($cache[$member_id])) {
125         foreach (array_reverse(array_keys($cache[$member_id])) as $cache_date) {
126             if ($cache_date <= $end_date) {
127                 $ret = _fees_build_month_array($cache_date /* XXX einen Monat später wäre an dieser Stelle richtiger*/,
128                     $end_date, $cache[$member_id][$cache_date]);
129                 _fees_apply_event_information($ret, db_get_events_for_member($member_id, $cache_date, $end_date));
130                 $cache[$member_id][$end_date] = $ret;
131                 return $ret;
132             }
133         }
134     }
135
136     $ret = _fees_build_month_array($config->get('founding_date'), $end_date);
137     if (empty($ret)) {
138         return null;
139     }
140
141     _fees_apply_event_information($ret, db_get_events_for_member($member_id));
142
143     $cache[$member_id][$end_date] = $ret;
144     return $ret;
145 }
146
147 function fees_sum_for_member($member_id, $end_date)
148 {
149     $membership_info = fees_get_list_for_member($member_id, $end_date);
150
151     $total = '0';
152
153     foreach ($membership_info as $year => $months) {
154         foreach ($months as $month => $info) {
155             if ($info['is_member']) {
156                 $total = bcadd($total, $info['fee']);
157             }
158         }
159     }
160     return $total;
161 }
162
163 function fees_for_member_at_date($member_id, $end_date)
164 {
165     $membership_info = fees_get_list_for_member($member_id, $end_date);
166
167     $this_year = array_pop($membership_info);
168     $this_month = array_pop($this_year);
169     if ($this_month['is_member']) {
170         return $this_month['fee'];
171     }
172     return null;
173 }
174
175 function fees_info_for_member($member_id, $end_date)
176 {
177     $membership_info = fees_get_list_for_member($member_id, $end_date);
178
179     $this_year = array_pop($membership_info);
180     return array_pop($this_year);
181 }
182
183 function fees_sum_by_month($end_date)
184 {
185     $members = db_get_members();
186     $fees = array();
187     if (empty($members)) {
188         return array();
189     }
190     foreach ($members as $member) {
191         $membership_info = fees_get_list_for_member($member['id'], $end_date);
192         foreach ($membership_info as $year => $months) {
193             foreach ($months as $month => $info) {
194                 if (!isset($fees[$year][$month])) {
195                     $fees[$year][$month] = '0';
196                 }
197                 if ($info['is_member']) {
198                     $fees[$year][$month] = bcadd($fees[$year][$month], $info['fee']);
199                 }
200             }
201         }
202     }
203     return $fees;
204 }
205
206 function fees_get_list_for_month($year, $month)
207 {
208     $members = db_get_members();
209     $fees = array();
210     foreach ($members as $member) {
211         $membership_info = fees_get_list_for_member($member['id'], mktime(0, 0, 0, $month, 1, $year));
212
213         if (empty($membership_info)) {
214             continue;
215         }
216         $member['fee'] = $membership_info[$year][$month]['fee'];
217         $member['is_member'] = $membership_info[$year][$month]['is_member'];
218         $fees[] = $member;
219     }
220     return $fees;
221 }
222
223 function fee_next_directdebit_for_member($member_id, $max_date = null)
224 {
225     $config = Config::getInstance();
226     $member = db_get_member_with_id($member_id);
227     if (!$member['directdebit']) {
228         return null;
229     }
230
231     /** @var DateTime $direct_debit_date */
232     $direct_debit_date = $config->get('direct_debit')['date'];
233     $sum_old_fees = fees_sum_for_member($member_id, $direct_debit_date->getTimestamp() - 86400);
234     $sum_new_paid = finance_get_paid_fees_for_member($member_id);
235     $year = $direct_debit_date->format('Y');
236     $month = $direct_debit_date->format('n');
237     $day = 1;
238
239
240     while (true) {
241         $start_date = mktime(0, 0, 0, $month, $day, $year);
242         if (isset($max_date) && $start_date > $max_date) {
243             return null;
244         }
245
246         // check if fee is zero at the moment and skip to next event
247         // quit searching if theres no event in future
248         $current_fee = fees_for_member_at_date($member_id, $start_date);
249         if (empty($current_fee)) {
250             $events = db_get_events_for_member($member_id, $start_date + 86400);
251             if (empty($events)) {
252                 return null;
253             }
254             $start_date = db_date2unixtime($events[0]['event_date']);
255             $day = date('j', $start_date);
256             $month = date('n', $start_date);
257             $year = date('Y', $start_date);
258             continue;
259         }
260
261         $sum_fees = fees_sum_for_member($member_id, $start_date);
262         $sum_new_fees = bcsub($sum_fees, $sum_old_fees);
263         if (bccomp($sum_new_fees, $sum_new_paid) == 1) {
264             $info = fees_get_list_for_member($member_id, $start_date);
265             $months = 1;
266             $ret = array(
267                 'date'  => $start_date,
268                 'value' => bcsub($sum_new_fees, $sum_new_paid),
269                 'info'  => '',
270             );
271             switch ($info[$year][$month]['payment_interval']) {
272                 case 'monthly'   :
273                     $months = 1;
274                     break;
275                 case 'quarterly' :
276                     $months = 3;
277                     break;
278                 case 'halfyearly':
279                     $months = 6;
280                     break;
281                 case 'yearly'    :
282                     $months = 12;
283                     break;
284             }
285             if ($months == 1) {
286                 $ret['info'] = dtaus_string(sprintf('CCCFFM %d, %s', $member['number'], format_month($start_date)));
287                 return $ret;
288             }
289             $end_date = mktime(0, 0, 0, $month + $months - 1, 1, $year);
290             $sum_fee_end = fees_sum_for_member($member_id, $end_date);
291             $ret['value'] = bcadd($ret['value'], bcsub($sum_fee_end, $sum_fees));
292             $ret['info'] = dtaus_string(sprintf('CCCFFM %d, %s-%s', $member['number'], format_month($start_date),
293                 format_month($end_date)));
294             return $ret;
295         }
296         $day = 1;
297         $month++;
298         if ($month == 13) {
299             $month = 1;
300             $year++;
301         }
302     }
303
304     return null;
305 }
306
307 function action_fees()
308 {
309     if (isset($_REQUEST['member_id'])) {
310         render_fees_for_member($_REQUEST['member_id']);
311         return;
312     }
313     if (isset($_REQUEST['year']) && isset($_REQUEST['month'])) {
314         render_accrued_fees_for_month($_REQUEST['year'], $_REQUEST['month']);
315         return;
316     }
317
318     render_fees_by_member();
319     render_accrued_fees_by_month();
320     render_next_direct_debit();
321     render_future_fees();
322 }
323
324 function render_fees_by_member()
325 {
326     $members = db_get_members();
327     $config = Config::getInstance();
328     ?>
329     <h2>Mitgliedsbeitr&auml;ge nach Mitglied</h2>
330     <table>
331         <tr>
332             <th>Mitgliedsnummer</th>
333             <th>Nickname</th>
334             <th style="text-align: right;">Angefallene Beitr&auml;ge</th>
335             <th style="text-align: right;">Aktueller Beitrag</th>
336             <th style="text-align: right;">Offener Beitrag</th>
337         </tr>
338         <?php if (empty($members)) {
339             $members = array();
340         } ?>
341         <?php foreach ($members as $member) : ?>
342             <?php
343             $current_fee = fees_for_member_at_date($member['id'], time());
344
345             $sum_fees = fees_sum_for_member($member['id'], time());
346             /** @var DateTime $direct_debit */
347             $direct_debit = $config->get('direct_debit')['date'];
348             $sum_old_fees = fees_sum_for_member($member['id'], $direct_debit->getTimestamp() - 86400);
349             $sum_old_paid = finance_get_paid_fees_for_member($member['id'], true);
350             $sum_new_paid = finance_get_paid_fees_for_member($member['id']);
351             $sum_new_fees = bcsub($sum_fees, $sum_old_fees);
352             $open_fees = bcadd(bcsub($sum_old_fees, $sum_old_paid), max(bcsub($sum_new_fees, $sum_new_paid), 0));
353             ?>
354             <tr>
355                 <td><a href="<?php echo html_escape(link_to('fees',
356                         array('member_id' => $member['id']))); ?>"><?php echo html_escape($member['number']); ?></a>
357                 </td>
358                 <td><?php echo html_escape($member['nickname']); ?></td>
359                 <td style="text-align: right;"><?php echo format_money($sum_fees); ?></td>
360                 <td style="text-align: right;"><?php echo isset($current_fee) ? format_money($current_fee) : '-'; ?></td>
361                 <td style="text-align: right;"><?php echo $open_fees > 0 ? format_money($open_fees) : '-'; ?></td>
362             </tr>
363         <?php endforeach ?>
364     </table>
365     <?php
366 }
367
368 function render_future_fees()
369 {
370     $total_paid = finance_get_total_paid_fees();
371     $this_year = date('Y');
372     $this_month = date('m');
373     $fees = fees_sum_by_month(mktime(0, 0, 0, date('m') + 6, date('d'), date('Y') + 1));
374     $total = 0;
375     foreach ($fees as $year => $months) {
376         foreach ($months as $month => $fee) {
377             $total = bcadd($total, $fee);
378             $fees[$year][$month] = array('total' => $total, 'fee' => $fee);
379         }
380     }
381     $fees = array_reverse($fees, true);
382     ?>
383     <h2>Beitragsprognose nach Monat</h2>
384     <table>
385         <tr>
386             <th>Monat</th>
387             <th style="text-align: right;">Mitgliedsbeitr&auml;ge</th>
388             <th style="text-align: right;">kummuliert</th>
389             <th style="text-align: right;">eingenommen</th>
390             <th style="text-align: right;"><strong>offen</strong></th>
391         </tr>
392         <?php foreach ($fees as $year => $months) : ?>
393             <?php $months = array_reverse($months, true); ?>
394             <?php foreach ($months as $month => $data) : ?>
395                 <tr<?php if ($year == $this_year && $month == $this_month) : ?> class="current"<?php endif ?>>
396                     <td><a href="<?php echo html_escape(link_to('fees',
397                             array('year' => $year, 'month' => $month))); ?>"><?php echo html_escape(format_month($year,
398                                 $month)); ?></a></td>
399                     <td style="text-align: right;"><?php echo html_escape(format_money($data['fee'])); ?></td>
400                     <td style="text-align: right;"><?php echo html_escape(format_money($data['total'])); ?></td>
401                     <td style="text-align: right;">
402                         <?php if ($year == $this_year && $month == $this_month) : ?>
403                             <?php echo html_escape(format_money($total_paid)); ?>
404                         <?php endif ?>
405                     </td>
406                     <td style="text-align: right;">
407                         <?php if ($year > $this_year || ($year >= $this_year && $month >= $this_month)) : ?>
408                             <?php echo html_escape(format_money(bcsub($data['total'], $total_paid))); ?>
409                         <?php endif ?>
410                     </td>
411                 </tr>
412             <?php endforeach ?>
413         <?php endforeach ?>
414     </table>
415     <?php
416 }
417
418 function render_accrued_fees_by_month()
419 {
420     $fees = fees_sum_by_month(time());
421     $fees = array_reverse($fees, true);
422     ?>
423     <h2>Angefallene Mitgliedsbeitr&auml;ge nach Monat</h2>
424     <table>
425         <tr>
426             <th>Monat</th>
427             <th style="text-align: right;">Mitgliedsbeitrag</th>
428         </tr>
429         <?php foreach ($fees as $year => $months) : ?>
430             <?php $months = array_reverse($months, true); ?>
431             <?php foreach ($months as $month => $fee) : ?>
432                 <tr>
433                     <td><a href="<?php echo html_escape(link_to('fees',
434                             array('year' => $year, 'month' => $month))); ?>"><?php echo html_escape(format_month($year,
435                                 $month)) ?></a></td>
436                     <td style="text-align: right;"><?php echo html_escape(format_money($fee)) ?></td>
437                 </tr>
438             <?php endforeach ?>
439         <?php endforeach ?>
440     </table>
441     <?php
442 }
443
444 function render_accrued_fees_for_month($year, $month)
445 {
446     $fees = fees_get_list_for_month($year, $month);
447     ?>
448     <h2>Angefallene Mitgliedsbeitr&auml;ge f&uuml;r <?php echo format_month($year, $month) ?></h2>
449     <table>
450         <tr>
451             <th>Mitgliedsnummer</th>
452             <th>Nickname</th>
453             <th style="text-align: right;">Mitgliedsbeitrag</th>
454         </tr>
455         <?php foreach ($fees as $info) : ?>
456             <tr>
457                 <td><a href="<?php echo html_escape(link_to('fees',
458                         array('member_id' => $info['id']))) ?>"><?php echo html_escape($info['number']) ?></a></td>
459                 <td><?php echo html_escape($info['nickname']) ?></td>
460                 <td style="text-align: right;"><?php echo html_escape($info['is_member'] ? format_money($info['fee']) : '-') ?></td>
461             </tr>
462         <?php endforeach ?>
463     </table>
464     <p><a href="<?php echo html_escape(link_to('fees')) ?>">Alle angefallenen Mitgliedsbeitr&auml;ge</a></p>
465     <?php
466 }
467
468 function render_next_direct_debit()
469 {
470     $members = db_get_members();
471     ?>
472     <h2>Nächste Abbuchungen nach Mitglied</h2>
473     <table>
474         <tr>
475             <th>Mitgliedsnummer</th>
476             <th>Nickname</th>
477             <th style="text-align: right;">Verwendungszweck</th>
478             <th style="text-align: right;">Betrag</th>
479         </tr>
480         <?php if (empty($members)) {
481             $members = array();
482         } ?>
483         <?php foreach ($members as $member) : ?>
484             <?php $next_debit = fee_next_directdebit_for_member($member['id']); ?>
485             <tr>
486                 <td><a href="<?php echo html_escape(link_to('fees',
487                         array('member_id' => $member['id']))) ?>"><?php echo html_escape($member['number']) ?></a></td>
488                 <td><?php echo html_escape($member['nickname']) ?></td>
489                 <?php if (empty($next_debit)) : ?>
490                     <td>-</td>
491                     <td style="text-align: right;">-</td>
492                 <?php else : ?>
493                     <td><?php echo html_escape($next_debit['info']) ?></td>
494                     <td style="text-align: right;"><?php echo format_money($next_debit['value']) ?></td>
495                 <?php endif ?>
496             </tr>
497         <?php endforeach ?>
498     </table>
499     <?php
500 }
501
502 function render_fees_for_member($member_id)
503 {
504     global $MEMBER_TYPES, $EARNING_TYPES, $EXPENSE_TYPES;
505     $config = Config::getInstance();
506     /** @var DateTime $direct_debit */
507     $direct_debit = $config->get('direct_debit')['date'];
508
509     $member = db_get_member_with_id($member_id);
510     if (!isset($member)) {
511         redirect(link_to('fees'));
512     }
513
514     $membership_info = fees_get_list_for_member($member_id, time());
515     $membership_info = array_reverse($membership_info, true);
516
517     $paid_fees = finance_list_paid_fees_for_member($member_id);
518
519     $sum_new_paid = finance_get_paid_fees_for_member($member_id);
520     $sum_old_paid = finance_get_paid_fees_for_member($member_id, true);
521     $sum_old_fees = fees_sum_for_member($member_id, $direct_debit->getTimestamp() - 86400);
522     $sum_fees = fees_sum_for_member($member_id, time());
523     $sum_new_fees = bcsub($sum_fees, $sum_old_fees);
524
525     $state = '';
526     $new_open = 0;
527     $old_open = 0;
528     if (bccomp($sum_new_fees, $sum_new_paid) == 1) {
529         $new_open = 1;
530     }
531     if (bccomp($sum_old_fees, $sum_old_paid) == 1) {
532         $old_open = 1;
533     }
534
535     if ($new_open && $old_open) {
536         $state = sprintf('Es sind noch %1$s Mitgliedsbeitrag offen, davon %2$s für die Zeit vor dem %3$s und %4$s für danach.',
537             format_money(bcadd(bcsub($sum_old_fees, $sum_old_paid), bcsub($sum_new_fees, $sum_new_paid))),
538             format_money(bcsub($sum_old_fees, $sum_old_paid)),
539             format_date($direct_debit->getTimestamp()),
540             format_money(bcsub($sum_new_fees, $sum_new_paid))
541         );
542     } elseif ($new_open) {
543         $state = sprintf('Es sind noch %1$s Mitgliedsbeitrag offen.',
544             format_money(bcsub($sum_new_fees, $sum_new_paid)));
545     } elseif ($old_open) {
546         $state = sprintf(
547             'Für die Zeit vor dem %1$s sind noch %2$s Mitgliedsbeitrag offen.',
548             format_date($direct_debit->getTimestamp()), format_money(bcsub($sum_old_fees, $sum_old_paid))
549         );
550     }
551
552     $next_debit = fee_next_directdebit_for_member($member_id);
553
554     ?>
555     <h2>Mitgliedsbeitr&auml;ge
556         von <?php echo html_escape(!empty($member['nickname']) ? $member['nickname'] : sprintf('Mitglied Nr. %d',
557             $member['number'])) ?></h2>
558     <h3>Mitgliedsdetails</h3>
559     <table>
560         <tr>
561             <th>Mitgliedsnummer</th>
562             <th>Nickname</th>
563             <th>Status</th>
564         </tr>
565         <tr>
566             <td><a href="<?php echo html_escape(link_to('view_member',
567                     array('id' => $member['id']))) ?>"><?php echo html_escape($member['number']) ?></a></td>
568             <td><?php echo html_escape($member['nickname']) ?></strong></p></td>
569             <td>
570                 <?php if (empty($state)) : ?>
571                     Kein Beitragsrückstand
572                 <?php else : ?>
573                     <?php echo wordwrap(html_escape($state), 70, '<br/>') ?>
574                 <?php endif ?>
575             </td>
576         </tr>
577     </table>
578     <div style="float: left">
579         <h3>Angefallene Mitgliedsbeitr&auml;ge</h3>
580         <table>
581             <tr>
582                 <th>Monat</th>
583                 <th>Mitgliedsart</th>
584                 <th style="text-align: right;">Mitgliedsbeitrag</th>
585             </tr>
586             <?php foreach ($membership_info as $year => $months) : ?>
587                 <?php $months = array_reverse($months, true); ?>
588                 <?php foreach ($months as $month => $info) : ?>
589                     <tr>
590                         <td><?php echo html_escape(format_month($year, $month)) ?></td>
591                         <td><?php echo html_escape($info['is_member'] ? $MEMBER_TYPES[$info['member_type']] : 'Kein Mitglied') ?></td>
592                         <td style="text-align: right;"><?php echo html_escape($info['is_member'] ? format_money($info['fee']) : '-') ?></td>
593                     </tr>
594                 <?php endforeach ?>
595             <?php endforeach ?>
596         </table>
597         <p><a href="<?php echo html_escape(link_to('fees')) ?>">Alle angefallenen Mitgliedsbeitr&auml;ge</a></p>
598     </div>
599     <div style="float: left; margin-left: 1em;">
600         <h3>Nächste Abbuchung</h3>
601         <table>
602             <tr>
603                 <th>Verwendungszweck</th>
604                 <th style="text-align: right;">Betrag</th>
605             </tr>
606             <?php if (empty($next_debit)) : ?>
607                 <td>-</td>
608                 <td style="text-align: right;">-</td>
609             <?php else : ?>
610                 <td><?php echo html_escape($next_debit['info']) ?></td>
611                 <td style="text-align: right;"><?php echo format_money($next_debit['value']) ?></td>
612             <?php endif ?>
613         </table>
614         <h3>Bezahlte Mitgliedsbeitr&auml;ge</h3>
615         <table>
616             <tr>
617                 <th>Monat</th>
618                 <th style="text-align: right;">Typ</th>
619                 <th style="text-align: right;">Betrag</th>
620             </tr>
621             <?php foreach ($paid_fees as $payment) : ?>
622                 <tr>
623                     <td><?php echo html_escape(format_date(db_date2unixtime($payment['date']))) ?></td>
624                     <td><?php echo ($payment['value'] < 0) ? $EXPENSE_TYPES[$payment['type']] : $EARNING_TYPES[$payment['type']] ?></td>
625                     <td style="text-align: right;"><?php echo format_money($payment['value']) ?></td>
626                 </tr>
627             <?php endforeach ?>
628         </table>
629     </div>
630     <br style="clear: left;"/>
631     <?php
632 }