File Coverage

blib/lib/CMS/Drupal/Modules/MembershipEntity/Stats.pm
Criterion Covered Total %
statement 72 255 28.2
branch 14 78 17.9
condition 0 12 0.0
subroutine 15 27 55.5
pod 18 18 100.0
total 119 390 30.5


line stmt bran cond sub pod time code
1             package CMS::Drupal::Modules::MembershipEntity::Stats;
2             $CMS::Drupal::Modules::MembershipEntity::Stats::VERSION = '0.96';
3             # ABSTRACT: Generate statistics about MembershipEntity memberships on a Drupal site.
4              
5 2     2   11735 use strict;
  2         4  
  2         58  
6 2     2   11 use warnings;
  2         3  
  2         64  
7              
8 2     2   12 use Moo;
  2         4  
  2         16  
9 2     2   926 use Types::Standard qw/ :all /;
  2         4  
  2         28  
10 2     2   80725 use base 'Exporter::Tiny';
  2         5  
  2         303  
11             our @EXPORT = qw/
12             count_total_memberships
13             count_expired_memberships
14             count_active_memberships
15             count_cancelled_memberships
16             count_pending_memberships
17             count_set_were_renewal_memberships
18             count_daily_were_renewal_memberships
19             count_daily_total_memberships
20             count_daily_term_expirations
21             count_daily_term_activations
22             count_daily_new_memberships
23             count_daily_new_terms
24             count_daily_renewals
25             count_daily_active_memberships
26             build_date_range
27             time_plus_one_day
28             datetime_plus_one_day
29             report_yesterday
30             /;
31              
32 2     2   10 use Time::Local;
  2         6  
  2         140  
33 2     2   2143 use DateTime::Event::Recurrence;
  2         411869  
  2         76  
34              
35 2     2   24 use CMS::Drupal::Modules::MembershipEntity::Membership;
  2         5  
  2         71  
36              
37 2     2   10 use Carp qw/ carp croak /;
  2         5  
  2         5435  
38              
39             has dbh => ( is => 'ro', isa => InstanceOf['DBI::db'], required => 1 );
40             has prefix => ( is => 'ro', isa => Maybe[Str] );
41              
42              
43             sub count_total_memberships {
44 1     1 1 4221 my $self = shift;
45 1 50       6 if ($self->{'_memberships'}) {
46 1         3 $self->{'stats'}->{'_count_total_memberships'} = scalar keys %{ $self->{'_memberships'} };
  1         4  
47             } else {
48 0         0 my $sql = q{ SELECT COUNT(mid) FROM membership_entity };
49 0         0 $self->{'stats'}->{'_count_total_memberships'} = $self->{'dbh'}->selectrow_array( $sql );
50             }
51 1         8 return $self->{'stats'}->{'_count_total_memberships'};
52             }
53              
54              
55             sub count_expired_memberships {
56 1     1 1 3 my $self = shift;
57 1 50       4 if ($self->{'_memberships'}) {
58 1         2 my $count = 0;
59 1         2 while ( my ($mid, $mem) = each %{ $self->{'_memberships'} } ) {
  30         95  
60 29 100       73 $count++ if $mem->is_expired;
61             }
62 1         3 $self->{'_count_expired_memberships'} = $count;
63             } else {
64 0         0 my $sql = q{ select count(mid) from membership_entity where status = 0 };
65 0         0 $self->{'_count_expired_memberships'} = $self->{'dbh'}->selectrow_array( $sql );
66             }
67 1         5 return $self->{'_count_expired_memberships'};
68             }
69              
70              
71             sub count_active_memberships {
72 1     1 1 2 my $self = shift;
73 1 50       4 if ($self->{'_memberships'}) {
74 1         3 my $count = 0;
75 1         2 while ( my ($mid, $mem) = each %{ $self->{'_memberships'} } ) {
  30         79  
76 29 100       67 $count++ if $mem->is_active;
77             }
78 1         3 $self->{'_count_active_memberships'} = $count;
79             } else {
80 0         0 my $sql = q{ SELECT COUNT(mid) FROM membership_entity WHERE status = 1 };
81 0         0 $self->{'_count_active_memberships'} = $self->{'dbh'}->selectrow_array( $sql );
82             }
83 1         4 return $self->{'_count_active_memberships'};
84             }
85              
86              
87             sub count_cancelled_memberships {
88 1     1 1 2 my $self = shift;
89 1 50       6 if ($self->{'_memberships'}) {
90 1         2 my $count = 0;
91 1         2 while ( my ($mid, $mem) = each %{ $self->{'_memberships'} } ) {
  30         89  
92 29 50       71 $count++ if $mem->is_cancelled;
93             }
94 1         3 $self->{'_count_cancelled_memberships'} = $count;
95             } else {
96 0         0 my $sql = q{ SELECT COUNT(mid) FROM membership_entity WHERE status = 2 };
97 0         0 $self->{'_count_cancelled_memberships'} = $self->{'dbh'}->selectrow_array( $sql );
98             }
99 1         5 return $self->{'_count_cancelled_memberships'};
100             }
101              
102              
103             sub count_pending_memberships {
104 1     1 1 2 my $self = shift;
105 1 50       5 if ($self->{'_memberships'}) {
106 1         2 my $count = 0;
107 1         2 while ( my ($mid, $mem) = each %{ $self->{'_memberships'} } ) {
  30         78  
108 29 50       65 $count++ if $mem->is_pending;
109             }
110 1         3 $self->{'_count_pending_memberships'} = $count;
111             } else {
112 0         0 my $sql = q{ select count(mid) from membership_entity where status = 3 };
113 0         0 $self->{'_count_pending_memberships'} = $self->{'dbh'}->selectrow_array( $sql );
114             }
115 1         4 return $self->{'_count_pending_memberships'};
116             }
117              
118              
119             sub count_set_were_renewal_memberships {
120 1     1 1 3 my $self = shift;
121 1 50       4 if ($self->{'_memberships'}) {
122 1         4 $self->{'_count_were_renewal_memberships'} = 0;
123 1         2 while ( my ($mid, $mem) = each %{ $self->{'_memberships'} } ) {
  30         86  
124 29 100       68 if ( $mem->current_was_renewal ) {
125 2         5 $self->{'_count_were_renewal_memberships'}++;
126             }
127             }
128             } else {
129 0         0 croak qq/
130             Died.
131             count_were_renewal_memberships() must be called with a set of
132             Memberships. You probably forgot to call fetch_memberships()
133             on your MembershipEntity object before calling this method.
134             /;
135             }
136 1         5 return $self->{'_count_were_renewal_memberships'};
137             }
138              
139              
140             sub count_daily_were_renewal_memberships {
141 0     0 1   my $self = shift;
142 0           my @dates = @_;
143 0           my %counts;
144              
145 0 0         carp qq/
146             Warning!
147             Called a count_daily_ method with no dates. This will return nothing!
148             / if ! @dates;
149              
150             ## First get all the terms
151 0           my $sql = qq/
152             SELECT mid, id AS tid, start, end
153             FROM membership_entity_term
154             /;
155              
156 0           my %current_mids;
157             my %current_tids;
158 0           my %ordered_terms;
159              
160             ## get all the terms from the DB
161 0           my $terms = $self->{'dbh'}->selectall_hashref( $sql, 'tid' );
162            
163 0           foreach my $term ( values %{ $terms } ) {
  0            
164             # indexed by mid, but terms indexed by start time for easier sorting
165 0           $ordered_terms{ $term->{'mid'} }->{ $term->{'start'} } = $term->{'tid'};
166             }
167              
168             ## loop through the dates
169 0           foreach my $datetime ( @dates ) {
170              
171             ## find the ones that were current on the date
172 0           foreach my $term ( values %{ $terms } ) {
  0            
173 0 0 0       next unless $datetime gt $term->{'start'} and $term->{'end'} gt $datetime;
174            
175 0           $current_mids{ $term->{'mid'} }++;
176 0           $current_tids{ $term->{'tid'} }++;
177             }
178              
179             # Now process each mid
180 0           foreach my $mid ( keys %ordered_terms ) {
181            
182             # only keep it if it had a current term, i.e. was active
183 0 0         if ( ! exists $current_mids{ $mid } ) {
184 0           delete $ordered_terms{ $mid };
185 0           next;
186             }
187            
188             # only keep it if it has at least two terms
189 0 0         if ( scalar keys %{ $ordered_terms{ $mid } } < 2 ) {
  0            
190 0           delete $ordered_terms{ $mid };
191 0           next;
192             }
193             }
194            
195             # if the mem is still here, it has a current term and more than one term.
196             # shift the earliest one off; the rest are renewals; is one of them current?
197 0           my %were_renewal_memberships;
198              
199 0           foreach my $mid ( keys %ordered_terms ) {
200            
201 0           my $term_count = 0;
202            
203 0           foreach my $start (sort keys %{ $ordered_terms{ $mid } } ) {
  0            
204 0           $term_count++;
205 0 0         next if $term_count == 1;
206            
207 0 0         if ( exists $current_tids{ $ordered_terms{ $mid }->{ $start } } ) {
208 0           $were_renewal_memberships{ $mid } = 1;
209             }
210             }
211             }
212              
213 0           $counts{ $datetime } = scalar keys %were_renewal_memberships;
214             }
215              
216             return (scalar keys %counts == 1) ?
217 0 0         $counts{ $dates[0] } :
218             \%counts;
219              
220             } # end sub
221              
222              
223             sub count_daily_term_expirations {
224 0     0 1   my $self = shift;
225 0           my @dates = @_;
226 0           my %counts;
227              
228 0 0         carp qq/
229             Warning!
230             Called a count_daily_ method with no dates. This will return nothing!
231             / if ! @dates;
232              
233 0           my $sql = qq/
234             SELECT COUNT(DISTINCT mid)
235             FROM membership_entity_term
236             WHERE end >= ?
237             AND end < ?
238             AND status NOT IN (2,3)
239             /;
240            
241 0           my $sth = $self->{'dbh'}->prepare( $sql );
242              
243 0           foreach my $datetime (@dates) {
244 0           $sth->execute( $datetime, $self->datetime_plus_one_day( $datetime ) );
245 0           $counts{ $datetime } = $sth->fetchrow_array;
246             }
247              
248             return (scalar keys %counts == 1) ?
249 0 0         $counts{ $dates[0] } :
250             \%counts;
251            
252             } # end sub
253              
254              
255             sub count_daily_term_activations {
256 0     0 1   my $self = shift;
257 0           my @dates = @_;
258 0           my %counts;
259              
260 0 0         carp qq/
261             Warning!
262             Called a count_daily_ method with no dates. This will return nothing!
263             / if ! @dates;
264              
265 0           my $sql = qq/
266             SELECT COUNT(DISTINCT mid)
267             FROM membership_entity_term
268             WHERE start >= ?
269             AND start < ?
270             AND status NOT IN (2,3)
271             /;
272            
273 0           my $sth = $self->{'dbh'}->prepare( $sql );
274              
275 0           foreach my $datetime (@dates) {
276 0           $sth->execute( $datetime, $self->datetime_plus_one_day( $datetime ) );
277 0           $counts{ $datetime } = $sth->fetchrow_array;
278             }
279              
280             return (scalar keys %counts == 1) ?
281 0 0         $counts{ $dates[0] } :
282             \%counts;
283            
284             } # end sub
285              
286              
287             sub count_daily_new_memberships {
288 0     0 1   my $self = shift;
289 0           my @dates = @_;
290 0           my %counts;
291              
292 0 0         carp qq/
293             Warning!
294             Called a count_daily_ method with no dates. This will return nothing!
295             / if ! @dates;
296              
297 0           my $sql = qq/
298             SELECT COUNT(DISTINCT mid)
299             FROM membership_entity
300             WHERE created >= ?
301             AND created < ?
302             AND status NOT IN (3)
303             /;
304              
305 0           my $sth = $self->{'dbh'}->prepare( $sql );
306              
307 0           foreach my $datetime (@dates) {
308 0           my ( $start, $over ) = $self->time_plus_one_day( $datetime );
309 0           $sth->execute( $start, $over );
310 0           $counts{ $datetime } = $sth->fetchrow_array;
311             }
312              
313             return (scalar keys %counts == 1) ?
314 0 0         $counts{ $dates[0] } :
315             \%counts;
316              
317             } # end sub
318              
319              
320             sub count_daily_new_terms {
321 0     0 1   my $self = shift;
322 0           my @dates = @_;
323 0           my %counts;
324              
325 0 0         carp qq/
326             Warning!
327             Called a count_daily_ method with no dates. This will return nothing!
328             / if ! @dates;
329              
330 0           my $sql = qq/
331             SELECT COUNT(DISTINCT mid)
332             FROM membership_entity_term
333             WHERE created >= ?
334             AND created < ?
335             AND status NOT IN (3)
336             /;
337              
338 0           my $sth = $self->{'dbh'}->prepare( $sql );
339              
340 0           foreach my $datetime (@dates) {
341 0           my ( $start, $over ) = $self->time_plus_one_day( $datetime );
342 0           $sth->execute( $start, $over );
343 0           $counts{ $datetime } = $sth->fetchrow_array;
344             }
345              
346             return (scalar keys %counts == 1) ?
347 0 0         $counts{ $dates[0] } :
348             \%counts;
349              
350             } # end sub
351              
352              
353              
354             sub count_daily_renewals {
355 0     0 1   my $self = shift;
356 0           my @dates = @_;
357 0           my %counts;
358              
359 0 0         carp qq/
360             Warning!
361             Called a count_daily_ method with no dates. This will return nothing!
362             / if ! @dates;
363              
364 0           my $sql1 = qq/
365             SELECT id, mid
366             FROM membership_entity_term
367             WHERE created >= ?
368             AND created < ?
369             AND status NOT IN (3)
370             /;
371              
372 0           my $sth1 = $self->{'dbh'}->prepare( $sql1 );
373              
374 0           my $sql2 = qq/
375             SELECT id
376             FROM membership_entity_term
377             WHERE mid = ?
378             /;
379              
380 0           my $sth2 = $self->{'dbh'}->prepare( $sql2 );
381              
382 0           foreach my $datetime (@dates) {
383 0           $counts{ $datetime } = 0;
384 0           my ( $start, $over ) = $self->time_plus_one_day( $datetime );
385 0           $sth1->execute( $start, $over );
386 0           while ( my ($id, $mid) = $sth1->fetchrow_array ) {
387 0           my $was_renewal = 0;
388 0           $sth2->execute( $mid );
389 0           for ( $sth2->fetchrow_array ) {
390 0 0         $was_renewal = 1 if $_ < $id;
391             }
392 0           $counts{ $datetime } += $was_renewal;
393             }
394             }
395              
396             return (scalar keys %counts == 1) ?
397 0 0         $counts{ $dates[0] } :
398             \%counts;
399              
400             } # end sub
401              
402              
403             sub count_daily_active_memberships {
404 0     0 1   my $self = shift;
405 0           my @dates = @_;
406 0           my %counts;
407              
408 0 0         carp qq/
409             Warning!
410             Called a count_daily_ method with no dates. This will return nothing!
411             / if ! @dates;
412              
413 0           my $sql = qq/
414             SELECT COUNT(DISTINCT mid)
415             FROM membership_entity_term
416             WHERE start <= ? AND end > ?
417             AND status NOT IN (2,3)
418             /;
419              
420 0           my $sth = $self->{'dbh'}->prepare( $sql );
421              
422 0           foreach my $date (@dates) {
423 0           $sth->execute( $date, $date );
424 0           $counts{ $date } = $sth->fetchrow_array;
425             }
426              
427             return (scalar keys %counts == 1) ?
428 0 0         $counts{ $dates[0] } :
429             \%counts;
430              
431             } # end sub
432              
433              
434             sub count_daily_total_memberships {
435 0     0 1   my $self = shift;
436 0           my @dates = @_;
437 0           my %counts;
438              
439 0 0         carp qq/
440             Warning!
441             Called a count_daily_ method with no dates. This will return nothing!
442             / if ! @dates;
443              
444 0           my $sql = qq/
445             SELECT COUNT(mid)
446             FROM membership_entity
447             WHERE created <= ?
448             AND status NOT IN (3)
449             /;
450              
451 0           my $sth = $self->{'dbh'}->prepare( $sql );
452              
453 0           foreach my $datetime (@dates) {
454 0           my ( $start, $over ) = $self->time_plus_one_day( $datetime );
455 0           $sth->execute( $over );
456 0           $counts{ $datetime } = $sth->fetchrow_array;
457             }
458              
459             return (scalar keys %counts == 1) ?
460 0 0         $counts{ $dates[0] } :
461             \%counts;
462              
463             } # end sub
464              
465              
466             ##################################################
467             ##
468             ## Utility subs
469             ##
470              
471              
472              
473             sub build_date_range {
474              
475 0     0 1   my $self = shift;
476 0           my $opt_start = shift;
477 0           my $opt_end = shift;
478              
479 0           my $dt_start;
480 0 0 0       if ( ! $opt_start or $opt_start !~ m/[0-9]{4}-[0-9]{2}-[0-9]{2}/) {
481 0           croak qq/
482             Died.
483             build_date_range() requires a beginning date in format 'YYYY-MM-DD'.
484             /;
485             } else {
486 0           my ($y,$m,$d) = split( '-', $opt_start);
487 0           $dt_start = DateTime->new( year => $y, month => $m, day => $d );
488             }
489 0           $dt_start->set_time_zone('UTC');
490            
491 0           my $dt_end;
492 0 0         if ( $opt_end ) {
493 0 0         if ( $opt_end !~ m/[0-9]{4}-[0-9]{2}-[0-9]{2}/) {
494 0           croak qq/
495             Died.
496             build_date_range() requires that the end date,
497             if supplied, be in the format 'YYYY-MM-DD'.
498             /;
499             } else {
500 0           my ($y,$m,$d) = split( '-', $opt_end);
501 0           $dt_end = DateTime->new( year => $y, month => $m, day => $d );
502             }
503             } else {
504             # default to today;
505 0           $dt_end = DateTime->now;
506 0           $dt_end->set_hour('00');
507 0           $dt_end->set_minute('00');
508 0           $dt_end->set_second('00');
509             }
510 0           $dt_end->set_time_zone('UTC');
511              
512 0           my $set = DateTime::Event::Recurrence->daily();
513 0           my $itr = $set->iterator( start => $dt_start, end => $dt_end );
514            
515 0           my @dates;
516              
517 0           while ( my $dt = $itr->next ) {
518 0           push @dates, $dt->datetime;
519             }
520              
521 0           return \@dates;
522              
523             } # end sub
524              
525              
526             sub datetime_plus_one_day {
527 0     0 1   my $self = shift;
528 0           my $datetime = shift;
529              
530 0 0 0       if ( ! $datetime or $datetime !~ m/[0-9]{4}-[0-9]{2}-[0-9]{2}/ ) {
531 0           croak qq/
532             Died.
533             datetime_plus_one() requires a datetime
534             in ISO-ish format (YYYY-MM-DDTHH:MM:SS).
535             /;
536             }
537              
538 0           my ($y, $m, $d) = split /[-| |T|:]/, $datetime;
539 0           return DateTime->new( year => $y, month => $m, day => $d )
540             ->set_time_zone( 'UTC' )
541             ->clone()
542             ->add( days => 1 )
543             ->datetime();
544              
545             } # end sub
546              
547              
548              
549             sub time_plus_one_day {
550 0     0 1   my $self = shift;
551 0           my $datetime = shift;
552              
553 0 0 0       if ( ! $datetime or $datetime !~ m/[0-9]{4}-[0-9]{2}-[0-9]{2}/ ) {
554 0           croak qq/
555             Died.
556             time_plus_one() requires a datetime in
557             ISO-ish format (YYYY-MM-DDTHH:MM:SS).
558             /;
559             }
560              
561 0           my @dateparts = reverse ( split /[-| |T|:]/, $datetime );
562 0           $dateparts[4]--;
563 0           my $time = timelocal( @dateparts );
564 0           my $plus_one = ($time + (24*3600));
565              
566 0           return( $time, $plus_one );
567              
568             } # end sub
569              
570              
571             sub report_yesterday {
572 0     0 1   my $self = shift;
573 0           my %args = @_;
574            
575 0           my %methods = map { $_ => 1 } ( qw/
  0            
576             count_daily_total_memberships
577             count_daily_total_memberships
578             count_daily_term_expirations
579             count_daily_term_activations
580             count_daily_new_memberships
581             count_daily_new_terms
582             count_daily_active_memberships
583             count_daily_renewals
584             count_daily_were_renewal_memberships
585             /);
586              
587 0 0         if ( $args{'exclude'} ) {
588 0           delete $methods{ $_ } for @{ $args{'exclude'} };
  0            
589             }
590              
591 0           my $yesterday = DateTime->now()
592             ->clone()
593             ->subtract(days => 1)
594             ->set( hour => 0, minute => 0, second => 0 )
595             ->datetime();
596            
597 0           my %data;
598              
599 0           for ( keys %methods ) {
600 0           $data{ $_ } = $self->$_( $yesterday );
601             }
602            
603 0           $data{ 'date' } = $yesterday;
604              
605 0           return \%data;
606              
607             } # end sub
608              
609              
610             1; ## return true to end package CMS::Drupal::Modules::MembershipEntity
611              
612             __END__