diff --git a/lib/Pear/LocalLoop.pm b/lib/Pear/LocalLoop.pm index aae42c2..540f1a2 100644 --- a/lib/Pear/LocalLoop.pm +++ b/lib/Pear/LocalLoop.pm @@ -147,6 +147,8 @@ sub startup { $api->post('/edit')->to('api-api#post_edit'); $api->post('/fetchuser')->to('api-api#post_fetchuser'); $api->post('/user-history')->to('api-user#post_user_history'); + $api->post('/stats')->to('api-stats#post_index'); + $api->post('/stats/leaderboard')->to('api-stats#post_leaderboards'); my $api_admin = $api->under('/')->to('api-admin#auth'); diff --git a/lib/Pear/LocalLoop/Controller/Api/Stats.pm b/lib/Pear/LocalLoop/Controller/Api/Stats.pm new file mode 100644 index 0000000..357562b --- /dev/null +++ b/lib/Pear/LocalLoop/Controller/Api/Stats.pm @@ -0,0 +1,100 @@ +package Pear::LocalLoop::Controller::Api::Stats; +use Mojo::Base 'Mojolicious::Controller'; + +use List::Util qw/ first /; + +has error_messages => sub { + return { + type => { + required => { message => 'Type of Leaderboard Required', status => 400 }, + in_resultset => { message => 'Unrecognised Leaderboard Type', status => 400 }, + }, + }; +}; + +sub post_index { + my $c = shift; + + my $user = $c->stash->{api_user}; + + my $today_rs = $user->transactions->today_rs; + my $today_sum = $today_rs->get_column('value')->sum; + my $today_count = $today_rs->count; + + my $week_rs = $user->transactions->week_rs; + my $week_sum = $week_rs->get_column('value')->sum; + my $week_count = $week_rs->count; + + my $month_rs = $user->transactions->month_rs; + my $month_sum = $month_rs->get_column('value')->sum; + my $month_count = $month_rs->count; + + my $user_rs = $user->transactions; + my $user_sum = $user_rs->get_column('value')->sum; + my $user_count = $user_rs->count; + + my $global_rs = $c->schema->resultset('Transaction'); + my $global_sum = $global_rs->get_column('value')->sum; + my $global_count = $global_rs->count; + + return $c->render( json => { + success => Mojo::JSON->true, + today_sum => $today_sum || 0, + today_count => $today_count, + week_sum => $week_sum || 0, + week_count => $week_count, + month_sum => $month_sum || 0, + month_count => $month_count, + user_sum => $user_sum || 0, + user_count => $user_count, + global_sum => $global_sum || 0, + global_count => $global_count, + }); +} + +sub post_leaderboards { + my $c = shift; + + my $validation = $c->validation; + $validation->input( $c->stash->{api_json} ); + + my $leaderboard_rs = $c->schema->resultset('Leaderboard'); + + $validation->required('type')->in_resultset( 'type', $leaderboard_rs ); + + return $c->api_validation_error if $validation->has_error; + + my $today_board = $leaderboard_rs->get_latest( $validation->param('type') ); + + my $today_values = $today_board->values->search( + {}, + { + order_by => { -desc => 'me.value' }, + columns => [ + qw/ + me.value + me.trend + me.user_id + /, + { display_name => 'customer.display_name' }, + ], + join => { user => 'customer' }, + }, + ); + $today_values->result_class( 'DBIx::Class::ResultClass::HashRefInflator' ); + + my @leaderboard_array = $today_values->all; + + my $current_user_index = first { $leaderboard_array[$_]->{user_id} == $c->stash->{api_user}->id } 0..$#leaderboard_array; + + # Dont leak user id's + map { delete $_->{user_id} } @leaderboard_array; + + return $c->render( json => { + success => Mojo::JSON->true, + leaderboard => [ @leaderboard_array ], + user_position => $current_user_index, + }); +} + +1; diff --git a/lib/Pear/LocalLoop/Controller/Api/Upload.pm b/lib/Pear/LocalLoop/Controller/Api/Upload.pm index c376129..5d2eb0c 100644 --- a/lib/Pear/LocalLoop/Controller/Api/Upload.pm +++ b/lib/Pear/LocalLoop/Controller/Api/Upload.pm @@ -70,6 +70,10 @@ has error_messages => sub { search_name => { required => { message => 'search_name is missing', status => 400 }, }, + postcode => { + required => { message => 'postcode is missing', status => 400 }, + postcode => { message => 'postcode must be valid', status => 400 }, + }, }; }; diff --git a/lib/Pear/LocalLoop/Schema/Result/Leaderboard.pm b/lib/Pear/LocalLoop/Schema/Result/Leaderboard.pm new file mode 100644 index 0000000..1105806 --- /dev/null +++ b/lib/Pear/LocalLoop/Schema/Result/Leaderboard.pm @@ -0,0 +1,300 @@ +package Pear::LocalLoop::Schema::Result::Leaderboard; + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +use DateTime; + +__PACKAGE__->table("leaderboards"); + +__PACKAGE__->add_columns( + "id" => { + data_type => "integer", + is_auto_increment => 1, + is_nullable => 0, + }, + "name" => { + data_type => "varchar", + size => 255, + is_nullable => 0, + }, + "type" => { + data_type => "varchar", + size => 255, + is_nullable => 0, + }, +); + +__PACKAGE__->set_primary_key("id"); + +__PACKAGE__->add_unique_constraint(["type"]); + +__PACKAGE__->has_many( + "sets", + "Pear::LocalLoop::Schema::Result::LeaderboardSet", + { "foreign.leaderboard_id" => "self.id" }, + { cascade_copy => 0, cascade_delete => 0 }, +); + +sub create_new { + my $self = shift; + my $start = shift; + + my $type = $self->type; + + if ( $type eq 'daily_total' ) { + return $self->_create_total_set( $start, $start->clone->add( days => 1 ) ); + } elsif ( $type eq 'weekly_total' ) { + return $self->_create_total_set( $start, $start->clone->add( days => 7 ) ); + } elsif ( $type eq 'monthly_total' ) { + return $self->_create_total_set( $start, $start->clone->add( months => 1 ) ); + } elsif ( $type eq 'all_time_total' ) { + return $self->_create_total_all_time( $start ); + } elsif ( $type eq 'daily_count' ) { + return $self->_create_count_set( $start, $start->clone->add( days => 1 ) ); + } elsif ( $type eq 'weekly_count' ) { + return $self->_create_count_set( $start, $start->clone->add( days => 7 ) ); + } elsif ( $type eq 'monthly_count' ) { + return $self->_create_count_set( $start, $start->clone->add( months => 1 ) ); + } elsif ( $type eq 'all_time_count' ) { + return $self->_create_count_all_time( $start ); + } + warn "Unrecognised type"; + return $self; +} + +sub _get_customer_rs { + my $self = shift; + return $self->result_source->schema->resultset('User')->search({ + organisation_id => undef, + }); +} + +sub _create_total_set { + my ( $self, $start, $end ) = @_; + + my $user_rs = $self->_get_customer_rs; + + my @leaderboard; + + my $previous_board = $self->get_latest; + + if ( defined $previous_board ) { + $previous_board = $previous_board->values; + } + + while ( my $user_result = $user_rs->next ) { + my $transaction_rs = $user_result->transactions->search_between( $start, $end ); + + my $transaction_sum = $transaction_rs->get_column('value')->sum; + + my $previous_value; + + if ( defined $previous_board ) { + $previous_value = $previous_board->find({ user_id => $user_result->id }); + } + + my $trend; + + if ( ! defined $previous_value ) { + $trend = 0; + } elsif ( $previous_value->value > $transaction_sum ) { + $trend = -1; + } elsif ( $previous_value->value < $transaction_sum ) { + $trend = 1; + } else { + $trend = 0; + } + + push @leaderboard, { + user_id => $user_result->id, + value => $transaction_sum || 0, + trend => $trend, + }; + } + + $self->create_related( + 'sets', + { + date => $start, + values => \@leaderboard, + }, + ); + + return $self; +} + +sub _create_count_set { + my ( $self, $start, $end ) = @_; + + my $user_rs = $self->_get_customer_rs; + + my @leaderboard; + + my $previous_board = $self->get_latest; + + if ( defined $previous_board ) { + $previous_board = $previous_board->values; + } + + while ( my $user_result = $user_rs->next ) { + my $transaction_rs = $user_result->transactions->search_between( $start, $end ); + + my $transaction_count = $transaction_rs->count; + + my $previous_value; + + if ( defined $previous_board ) { + $previous_value = $previous_board->find({ user_id => $user_result->id }); + } + + my $trend; + + if ( ! defined $previous_value ) { + $trend = 0; + } elsif ( $previous_value->value > $transaction_count ) { + $trend = -1; + } elsif ( $previous_value->value < $transaction_count ) { + $trend = 1; + } else { + $trend = 0; + } + + push @leaderboard, { + user_id => $user_result->id, + value => $transaction_count || 0, + trend => $trend, + }; + } + + $self->create_related( + 'sets', + { + date => $start, + values => \@leaderboard, + }, + ); + + return $self; +} + +sub _create_total_all_time { + my ( $self, $end ) = @_; + + my $user_rs = $self->_get_customer_rs; + + my @leaderboard; + + my $previous_board = $self->get_latest; + + if ( defined $previous_board ) { + $previous_board = $previous_board->values; + } + + while ( my $user_result = $user_rs->next ) { + my $transaction_rs = $user_result->transactions; + + my $transaction_sum = $transaction_rs->get_column('value')->sum; + + my $previous_value; + + if ( defined $previous_board ) { + $previous_value = $previous_board->find({ user_id => $user_result->id }); + } + + my $trend; + + if ( ! defined $previous_value ) { + $trend = 0; + } elsif ( $previous_value->value > $transaction_sum ) { + $trend = -1; + } elsif ( $previous_value->value < $transaction_sum ) { + $trend = 1; + } else { + $trend = 0; + } + + push @leaderboard, { + user_id => $user_result->id, + value => $transaction_sum || 0, + }; + } + + $self->create_related( + 'sets', + { + date => $end, + values => \@leaderboard, + }, + ); + + return $self; +} + +sub _create_count_all_time { + my ( $self, $end ) = @_; + + my $user_rs = $self->_get_customer_rs; + + my @leaderboard; + + my $previous_board = $self->get_latest; + + if ( defined $previous_board ) { + $previous_board = $previous_board->values; + } + + while ( my $user_result = $user_rs->next ) { + my $transaction_rs = $user_result->transactions; + + my $transaction_count = $transaction_rs->count; + + my $previous_value; + + if ( defined $previous_board ) { + $previous_value = $previous_board->find({ user_id => $user_result->id }); + } + + my $trend; + + if ( ! defined $previous_value ) { + $trend = 0; + } elsif ( $previous_value->value > $transaction_count ) { + $trend = -1; + } elsif ( $previous_value->value < $transaction_count ) { + $trend = 1; + } else { + $trend = 0; + } + + push @leaderboard, { + user_id => $user_result->id, + value => $transaction_count || 0, + trend => $trend, + }; + } + + $self->create_related( + 'sets', + { + date => $end, + values => \@leaderboard, + }, + ); + + return $self; +} + +sub get_latest { + my $self = shift; + + my $latest = $self->search_related('sets', {}, { + order_by => { -desc => 'date' }, + })->first; + + return $latest; +} + +1; diff --git a/lib/Pear/LocalLoop/Schema/Result/LeaderboardSet.pm b/lib/Pear/LocalLoop/Schema/Result/LeaderboardSet.pm new file mode 100644 index 0000000..f1a072f --- /dev/null +++ b/lib/Pear/LocalLoop/Schema/Result/LeaderboardSet.pm @@ -0,0 +1,52 @@ +package Pear::LocalLoop::Schema::Result::LeaderboardSet; + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +__PACKAGE__->load_components( qw/ + InflateColumn::DateTime +/); + +__PACKAGE__->table("leaderboard_sets"); + +__PACKAGE__->add_columns( + "id" => { + data_type => "integer", + is_auto_increment => 1, + is_nullable => 0, + }, + "leaderboard_id" => { + data_type => "integer", + is_foreign_key => 1, + is_nullable => 0, + }, + "date" => { + data_type => "datetime", + is_nullable => 0, + }, +); + +__PACKAGE__->set_primary_key("id"); + +__PACKAGE__->belongs_to( + "leaderboard", + "Pear::LocalLoop::Schema::Result::Leaderboard", + { "foreign.id" => "self.leaderboard_id" }, + { + is_deferrable => 0, + join_type => "LEFT", + on_delete => "NO ACTION", + on_update => "NO ACTION", + }, +); + +__PACKAGE__->has_many( + "values", + "Pear::LocalLoop::Schema::Result::LeaderboardValue", + { "foreign.set_id" => "self.id" }, + { cascade_copy => 0, cascade_delete => 0 }, +); + +1; diff --git a/lib/Pear/LocalLoop/Schema/Result/LeaderboardValue.pm b/lib/Pear/LocalLoop/Schema/Result/LeaderboardValue.pm new file mode 100644 index 0000000..4beaed4 --- /dev/null +++ b/lib/Pear/LocalLoop/Schema/Result/LeaderboardValue.pm @@ -0,0 +1,65 @@ +package Pear::LocalLoop::Schema::Result::LeaderboardValue; + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +__PACKAGE__->table("leaderboard_values"); + +__PACKAGE__->add_columns( + "id" => { + data_type => "integer", + is_auto_increment => 1, + is_nullable => 0, + }, + "user_id" => { + data_type => "integer", + is_foreign_key => 1, + is_nullable => 0, + }, + "set_id" => { + data_type => "integer", + is_foreign_key => 1, + is_nullable => 0, + }, + "value" => { + data_type => "decimal", + size => [ 16, 2 ], + is_nullable => 0, + }, + "trend" => { + data_type => "integer", + default_value => 0, + }, +); + +__PACKAGE__->set_primary_key("id"); + +__PACKAGE__->add_unique_constraint([qw/ user_id set_id /]); + +__PACKAGE__->belongs_to( + "set", + "Pear::LocalLoop::Schema::Result::LeaderboardSet", + { "foreign.id" => "self.set_id" }, + { + is_deferrable => 0, + join_type => "LEFT", + on_delete => "NO ACTION", + on_update => "NO ACTION", + }, +); + +__PACKAGE__->belongs_to( + "user", + "Pear::LocalLoop::Schema::Result::User", + { "foreign.id" => "self.user_id" }, + { + is_deferrable => 0, + join_type => "LEFT", + on_delete => "NO ACTION", + on_update => "NO ACTION", + }, +); + +1; diff --git a/lib/Pear/LocalLoop/Schema/Result/User.pm b/lib/Pear/LocalLoop/Schema/Result/User.pm index 6974cb4..259a03b 100644 --- a/lib/Pear/LocalLoop/Schema/Result/User.pm +++ b/lib/Pear/LocalLoop/Schema/Result/User.pm @@ -138,7 +138,7 @@ sub name { my $self = shift; if ( defined $self->customer_id ) { - return $self->customer->name; + return $self->customer->display_name; } elsif ( defined $self->organisation_id ) { return $self->organisation->name; } else { diff --git a/lib/Pear/LocalLoop/Schema/ResultSet/Leaderboard.pm b/lib/Pear/LocalLoop/Schema/ResultSet/Leaderboard.pm new file mode 100644 index 0000000..d8fd9c6 --- /dev/null +++ b/lib/Pear/LocalLoop/Schema/ResultSet/Leaderboard.pm @@ -0,0 +1,42 @@ +package Pear::LocalLoop::Schema::ResultSet::Leaderboard; + +use strict; +use warnings; + +use base 'DBIx::Class::ResultSet'; + +sub get_latest { + my $self = shift; + my $type = shift; + + my $type_result = $self->find_by_type( $type ); + + return undef unless defined $type_result; + + my $latest = $type_result->search_related('sets', {}, { + order_by => { -desc => 'date' }, + })->first; + + return $latest; +} + +sub create_new { + my $self = shift; + my $type = shift; + my $date = shift; + + my $type_result = $self->find_by_type($type); + + return undef unless $type_result; + + return $type_result->create_new($date); +} + +sub find_by_type { + my $self = shift; + my $type = shift; + + return $self->find({ type => $type }); +} + +1; diff --git a/lib/Pear/LocalLoop/Schema/ResultSet/Transaction.pm b/lib/Pear/LocalLoop/Schema/ResultSet/Transaction.pm new file mode 100644 index 0000000..4345cf4 --- /dev/null +++ b/lib/Pear/LocalLoop/Schema/ResultSet/Transaction.pm @@ -0,0 +1,45 @@ +package Pear::LocalLoop::Schema::ResultSet::Transaction; + +use strict; +use warnings; + +use base 'DBIx::Class::ResultSet'; + +use DateTime; + +sub search_between { + my ( $self, $from, $to ) = @_; + + my $dtf = $self->result_source->schema->storage->datetime_parser; + return $self->search({ + submitted_at => { + -between => [ + $dtf->format_datetime($from), + $dtf->format_datetime($to), + ], + }, + }); +} + +sub today_rs { + my ( $self ) = @_; + + my $today = DateTime->today(); + return $self->search_between( $today, $today->clone->add( days => 1 ) ); +} + +sub week_rs { + my ( $self ) = @_; + + my $today = DateTime->today(); + return $self->search_between( $today->clone->subtract( days => 7 ), $today ); +} + +sub month_rs { + my ( $self ) = @_; + + my $today = DateTime->today(); + return $self->search_between( $today->clone->subtract( days => 30 ), $today ); +} + +1; diff --git a/lib/Test/Pear/LocalLoop.pm b/lib/Test/Pear/LocalLoop.pm index 606f109..9581b02 100644 --- a/lib/Test/Pear/LocalLoop.pm +++ b/lib/Test/Pear/LocalLoop.pm @@ -37,6 +37,18 @@ has framework => sub { [ '50+' ], ]); + $schema->resultset('Leaderboard')->populate([ + [ qw/ name type / ], + [ 'Daily Total', 'daily_total' ], + [ 'Daily Count', 'daily_count' ], + [ 'Weekly Total', 'weekly_total' ], + [ 'Weekly Count', 'weekly_count' ], + [ 'Monthly Total', 'monthly_total' ], + [ 'Monthly Count', 'monthly_count' ], + [ 'All Time Total', 'all_time_total' ], + [ 'All Time Count', 'all_time_count' ], + ]); + return $t; }; @@ -65,6 +77,16 @@ sub register_customer { ->json_is('/success', Mojo::JSON->true)->or($self->dump_error); } +sub register_organisation { + my ( $self, $args ) = @_; + + $args->{usertype} = 'organisation'; + + $self->framework->post_ok('/api/register' => json => $args) + ->status_is(200)->or($self->dump_error) + ->json_is('/success', Mojo::JSON->true)->or($self->dump_error); +} + sub login { my $self = shift; my $args = shift; @@ -76,4 +98,19 @@ sub login { return $self->framework->tx->res->json->{session_key}; } +sub gen_upload { + my ( $self, $args ) = @_; + + my $file = { + content => '', + filename => 'text.jpg', + 'Content-Type' => 'image/jpeg', + }; + + return { + json => Mojo::JSON::encode_json($args), + file => $file, + }; +} + 1; diff --git a/script/deploy_db b/script/deploy_db index 9354a6d..d0e4c19 100755 --- a/script/deploy_db +++ b/script/deploy_db @@ -21,6 +21,18 @@ $schema->resultset('AgeRange')->populate([ [ '50+' ], ]); +$schema->resultset('Leaderboard')->populate([ + [ qw/ name type / ], + [ 'Daily Total', 'daily_total' ], + [ 'Daily Count', 'daily_count' ], + [ 'Weekly Total', 'weekly_total' ], + [ 'Weekly Count', 'weekly_count' ], + [ 'Monthly Total', 'monthly_total' ], + [ 'Monthly Count', 'monthly_count' ], + [ 'All Time Total', 'all_time_total' ], + [ 'All Time Count', 'all_time_count' ], +]); + if (defined $ENV{MOJO_MODE} && $ENV{MOJO_MODE} eq 'development' ) { $schema->resultset('User')->create({ diff --git a/t/api/stats.t b/t/api/stats.t new file mode 100644 index 0000000..180a91f --- /dev/null +++ b/t/api/stats.t @@ -0,0 +1,129 @@ +use Mojo::Base -strict; + +use Test::More; +use Mojo::JSON; +use Test::Pear::LocalLoop; +use DateTime; + +my $framework = Test::Pear::LocalLoop->new; +my $t = $framework->framework; +my $schema = $t->app->schema; +my $dtf = $schema->storage->datetime_parser; + +my $user = { + token => 'a', + full_name => 'Test User', + display_name => 'Test User', + email => 'test@example.com', + postcode => 'LA1 1AA', + password => 'abc123', + age_range => 1, +}; + +my $org = { + token => 'b', + email => 'test2@example.com', + name => 'Test Org', + street_name => 'Test Street', + town => 'Lancaster', + postcode => 'LA1 1AA', + password => 'abc123', +}; + +$schema->resultset('AccountToken')->create({ name => $user->{token} }); +$schema->resultset('AccountToken')->create({ name => $org->{token} }); + +$framework->register_customer($user); +$framework->register_organisation($org); + +my $org_result = $schema->resultset('Organisation')->find({ name => $org->{name} }); +my $user_result = $schema->resultset('User')->find({ email => $user->{email} }); + +my $session_key = $framework->login({ + email => $user->{email}, + password => $user->{password}, +}); + +$t->post_ok('/api/stats' => json => { session_key => $session_key } ) + ->status_is(200) + ->json_is('/success', Mojo::JSON->true) + ->json_is('/today_sum', 0) + ->json_is('/today_count', 0) + ->json_is('/week_sum', 0) + ->json_is('/week_count', 0) + ->json_is('/month_sum', 0) + ->json_is('/month_count', 0) + ->json_is('/user_sum', 0) + ->json_is('/user_count', 0) + ->json_is('/global_sum', 0) + ->json_is('/global_count', 0); + +for ( 1 .. 10 ) { + $user_result->create_related( 'transactions', { + seller_id => $org_result->id, + value => $_, + proof_image => 'a', + }); +} + +for ( 11 .. 20 ) { + $user_result->create_related( 'transactions', { + seller_id => $org_result->id, + value => $_, + proof_image => 'a', + submitted_at => $dtf->format_datetime(DateTime->today()->subtract( days => 5 )), + }); +} + +for ( 21 .. 30 ) { + $user_result->create_related( 'transactions', { + seller_id => $org_result->id, + value => $_, + proof_image => 'a', + submitted_at => $dtf->format_datetime(DateTime->today()->subtract( days => 25 )), + }); +} + +for ( 31 .. 40 ) { + $user_result->create_related( 'transactions', { + seller_id => $org_result->id, + value => $_, + proof_image => 'a', + submitted_at => $dtf->format_datetime(DateTime->today()->subtract( days => 50 )), + }); +} + +for ( 41 .. 50 ) { + $org_result->user->create_related( 'transactions', { + seller_id => $org_result->id, + value => $_, + proof_image => 'a', + submitted_at => $dtf->format_datetime(DateTime->today()->subtract( days => 50 )), + }); +} + +is $user_result->transactions->search({ + submitted_at => { + -between => [ + $dtf->format_datetime(DateTime->today()), + $dtf->format_datetime(DateTime->today()->add( days => 1 )), + ], + }, +})->get_column('value')->sum, 55, 'Got correct sum'; +is $user_result->transactions->today_rs->get_column('value')->sum, 55, 'Got correct sum through rs'; + +$t->post_ok('/api/stats' => json => { session_key => $session_key } ) + ->status_is(200) + ->json_is('/success', Mojo::JSON->true) + ->json_is('/today_sum', 55) + ->json_is('/today_count', 10) + ->json_is('/week_sum', 155) + ->json_is('/week_count', 10) + ->json_is('/month_sum', 410) + ->json_is('/month_count', 20) + ->json_is('/user_sum', 820) + ->json_is('/user_count', 40) + ->json_is('/global_sum', 1275) + ->json_is('/global_count', 50); + +done_testing; diff --git a/t/api/stats_leaderboards.t b/t/api/stats_leaderboards.t new file mode 100644 index 0000000..e8bf855 --- /dev/null +++ b/t/api/stats_leaderboards.t @@ -0,0 +1,198 @@ +use Mojo::Base -strict; + +use Test::More; +use Mojo::JSON; +use Test::Pear::LocalLoop; +use DateTime; + +my $framework = Test::Pear::LocalLoop->new; +my $t = $framework->framework; +my $schema = $t->app->schema; +my $dtf = $schema->storage->datetime_parser; + +my $user1 = { + token => 'a', + full_name => 'Test User1', + display_name => 'Test User1', + email => 'test1@example.com', + postcode => 'LA1 1AA', + password => 'abc123', + age_range => 1, +}; + +my $user2 = { + token => 'b', + full_name => 'Test User2', + display_name => 'Test User2', + email => 'test2@example.com', + postcode => 'LA1 1AA', + password => 'abc123', + age_range => 1, +}; + +my $user3 = { + token => 'c', + full_name => 'Test User3', + display_name => 'Test User3', + email => 'test3@example.com', + postcode => 'LA1 1AA', + password => 'abc123', + age_range => 1, +}; + +my $user4 = { + token => 'd', + full_name => 'Test User4', + display_name => 'Test User4', + email => 'test4@example.com', + postcode => 'LA1 1AA', + password => 'abc123', + age_range => 1, +}; + +my $org = { + token => 'e', + email => 'test5@example.com', + name => 'Test Org', + street_name => 'Test Street', + town => 'Lancaster', + postcode => 'LA1 1AA', + password => 'abc123', +}; + +$schema->resultset('AccountToken')->create({ name => $_->{token} }) + for ( $user1, $user2, $user3, $user4, $org ); + +$framework->register_customer($_) + for ( $user1, $user2, $user3, $user4 ); + +$framework->register_organisation($org); + +my $org_result = $schema->resultset('Organisation')->find({ name => $org->{name} }); + +my $tweak = 0; + +my $now = DateTime->today(); + +{ + my $user_result = $schema->resultset('User')->find({ email => $user1->{email} }); + + $user_result->create_related( 'transactions', { + seller_id => $org_result->id, + value => 1, + proof_image => 'a', + }); + + $user_result->create_related( 'transactions', { + seller_id => $org_result->id, + value => 9, + proof_image => 'a', + submitted_at => $dtf->format_datetime($now->clone->subtract( days => 1 )), + }); +} + +{ + my $user_result = $schema->resultset('User')->find({ email => $user2->{email} }); + + $user_result->create_related( 'transactions', { + seller_id => $org_result->id, + value => 3, + proof_image => 'a', + }); + + $user_result->create_related( 'transactions', { + seller_id => $org_result->id, + value => 1, + proof_image => 'a', + submitted_at => $dtf->format_datetime($now->clone->subtract( days => 1 )), + }); +} + +{ + my $user_result = $schema->resultset('User')->find({ email => $user3->{email} }); + + $user_result->create_related( 'transactions', { + seller_id => $org_result->id, + value => 5, + proof_image => 'a', + }); + + $user_result->create_related( 'transactions', { + seller_id => $org_result->id, + value => 5, + proof_image => 'a', + submitted_at => $dtf->format_datetime($now->clone->subtract( days => 1 )), + }); +} + +{ + my $user_result = $schema->resultset('User')->find({ email => $user4->{email} }); + + $user_result->create_related( 'transactions', { + seller_id => $org_result->id, + value => 9, + proof_image => 'a', + }); + + $user_result->create_related( 'transactions', { + seller_id => $org_result->id, + value => 3, + proof_image => 'a', + submitted_at => $dtf->format_datetime($now->clone->subtract( days => 1 )), + }); +} + +my $session_key = $framework->login({ + email => $user1->{email}, + password => $user1->{password}, +}); + +sub test_leaderboard { + my ( $title, $name, $date, $expected, $user_place ) = @_; + + subtest $title => sub { + my $leaderboard_rs = $schema->resultset('Leaderboard'); + + $leaderboard_rs->create_new( $name, $date ); + + $t->post_ok('/api/stats/leaderboard' => json => { session_key => $session_key, type => $name } ) + ->status_is(200) + ->or($framework->dump_error) + ->json_is('/success', Mojo::JSON->true) + ->or($framework->dump_error) + ->json_is('/leaderboard', $expected) + ->or($framework->dump_error) + ->json_is('/user_position', $user_place) + ->or($framework->dump_error); + }; +} + +$schema->resultset('Leaderboard')->create_new( 'daily_total', $now->clone->subtract( days => 1 ) ); + +test_leaderboard( + 'Daily Total', + 'daily_total', + $now, + [ + { display_name => 'Test User4', value => 9, trend => 1 }, + { display_name => 'Test User3', value => 5, trend => 0 }, + { display_name => 'Test User2', value => 3, trend => 1 }, + { display_name => 'Test User1', value => 1, trend => -1}, + ], + 3 +); + +test_leaderboard( + 'Daily Count', + 'daily_count', + $now, + [ + { display_name => 'Test User1', value => 1, trend => 0 }, + { display_name => 'Test User2', value => 1, trend => 0 }, + { display_name => 'Test User3', value => 1, trend => 0 }, + { display_name => 'Test User4', value => 1, trend => 0 }, + ], + 0 +); + +done_testing; diff --git a/t/schema/leaderboard.t b/t/schema/leaderboard.t new file mode 100644 index 0000000..b03106a --- /dev/null +++ b/t/schema/leaderboard.t @@ -0,0 +1,268 @@ +use Mojo::Base -strict; + +use Test::More; +use Mojo::JSON; +use Test::Pear::LocalLoop; +use DateTime; + +my $framework = Test::Pear::LocalLoop->new; +my $t = $framework->framework; +my $schema = $t->app->schema; +my $dtf = $schema->storage->datetime_parser; + +my $user1 = { + token => 'a', + full_name => 'Test User1', + display_name => 'Test User1', + email => 'test1@example.com', + postcode => 'LA1 1AA', + password => 'abc123', + age_range => 1, +}; + +my $user2 = { + token => 'b', + full_name => 'Test User2', + display_name => 'Test User2', + email => 'test2@example.com', + postcode => 'LA1 1AA', + password => 'abc123', + age_range => 1, +}; + +my $user3 = { + token => 'c', + full_name => 'Test User3', + display_name => 'Test User3', + email => 'test3@example.com', + postcode => 'LA1 1AA', + password => 'abc123', + age_range => 1, +}; + +my $user4 = { + token => 'd', + full_name => 'Test User4', + display_name => 'Test User4', + email => 'test4@example.com', + postcode => 'LA1 1AA', + password => 'abc123', + age_range => 1, +}; + +my $org = { + token => 'e', + email => 'test5@example.com', + name => 'Test Org', + street_name => 'Test Street', + town => 'Lancaster', + postcode => 'LA1 1AA', + password => 'abc123', +}; + +$schema->resultset('AccountToken')->create({ name => $_->{token} }) + for ( $user1, $user2, $user3, $user4, $org ); + +$framework->register_customer($_) + for ( $user1, $user2, $user3, $user4 ); + +$framework->register_organisation($org); + +my $org_result = $schema->resultset('Organisation')->find({ name => $org->{name} }); + +my $tweak = 0; + +my $now = DateTime->today(); + +for my $user ( $user1, $user2, $user3, $user4 ) { + $tweak ++; + my $user_result = $schema->resultset('User')->find({ email => $user->{email} }); + for ( 1 .. 10 ) { + $user_result->create_related( 'transactions', { + seller_id => $org_result->id, + value => $_ + $tweak, + proof_image => 'a', + }); + } + + for ( 11 .. 20 ) { + $user_result->create_related( 'transactions', { + seller_id => $org_result->id, + value => $_ + $tweak, + proof_image => 'a', + submitted_at => $dtf->format_datetime($now->clone->subtract( days => 5 )), + }); + } + + for ( 21 .. 30 ) { + $user_result->create_related( 'transactions', { + seller_id => $org_result->id, + value => $_ + $tweak, + proof_image => 'a', + submitted_at => $dtf->format_datetime($now->clone->subtract( days => 25 )), + }); + } + + for ( 31 .. 40 ) { + $user_result->create_related( 'transactions', { + seller_id => $org_result->id, + value => $_ + $tweak, + proof_image => 'a', + submitted_at => $dtf->format_datetime($now->clone->subtract( days => 50 )), + }); + } + + is $user_result->transactions->count, 40, 'correct count for user' . $tweak; +} + +sub test_leaderboard { + my ( $title, $name, $date, $expected ) = @_; + + subtest $title => sub { + my $leaderboard_rs = $schema->resultset('Leaderboard'); + + my $today_board = $leaderboard_rs->find({ type => $name })->create_new($date)->get_latest; + + is $today_board->values->count, 4, 'correct value count'; + + my $today_values = $today_board->values->search( + {}, + { + order_by => { -desc => 'value' }, + columns => [ qw/ user_id value / ], + }, + ); + $today_values->result_class( 'DBIx::Class::ResultClass::HashRefInflator' ); + + is_deeply [ $today_values->all ], $expected, 'array as expected'; + }; +} + +test_leaderboard( + 'Daily Total', + 'daily_total', + $now, + [ + { user_id => 4, value => 95 }, + { user_id => 3, value => 85 }, + { user_id => 2, value => 75 }, + { user_id => 1, value => 65 }, + ] +); + +test_leaderboard( + 'Daily Count', + 'daily_count', + $now, + [ + { user_id => 1, value => 10 }, + { user_id => 2, value => 10 }, + { user_id => 3, value => 10 }, + { user_id => 4, value => 10 }, + ] +); + +test_leaderboard( + 'Weekly Total', + 'weekly_total', + $now->clone->subtract( days => 7 ), + [ + { user_id => 4, value => 195 }, + { user_id => 3, value => 185 }, + { user_id => 2, value => 175 }, + { user_id => 1, value => 165 }, + ] +); + +test_leaderboard( + 'Weekly Count', + 'weekly_count', + $now->clone->subtract( days => 7 ), + [ + { user_id => 1, value => 10 }, + { user_id => 2, value => 10 }, + { user_id => 3, value => 10 }, + { user_id => 4, value => 10 }, + ] +); + +test_leaderboard( + 'Monthly Total', + 'monthly_total', + $now->clone->subtract( months => 1 ), + [ + { user_id => 4, value => 490 }, + { user_id => 3, value => 470 }, + { user_id => 2, value => 450 }, + { user_id => 1, value => 430 }, + ] +); + +test_leaderboard( + 'Monthly Count', + 'monthly_count', + $now->clone->subtract( months => 1 ), + [ + { user_id => 1, value => 20 }, + { user_id => 2, value => 20 }, + { user_id => 3, value => 20 }, + { user_id => 4, value => 20 }, + ] +); + +test_leaderboard( + 'All Time Total', + 'all_time_total', + $now, + [ + { user_id => 4, value => 980 }, + { user_id => 3, value => 940 }, + { user_id => 2, value => 900 }, + { user_id => 1, value => 860 }, + ] +); + +test_leaderboard( + 'All Time Count', + 'all_time_count', + $now, + [ + { user_id => 1, value => 40 }, + { user_id => 2, value => 40 }, + { user_id => 3, value => 40 }, + { user_id => 4, value => 40 }, + ] +); + +subtest 'get_latest' => sub { + my $leaderboard_rs = $schema->resultset('Leaderboard'); + $leaderboard_rs->find({ type => 'daily_total' })->create_new($now->clone->subtract(days => 5)); + $leaderboard_rs->find({ type => 'daily_total' })->create_new($now->clone->subtract(days => 25)); + $leaderboard_rs->find({ type => 'daily_total' })->create_new($now->clone->subtract(days => 50)); + + my $today_board = $leaderboard_rs->find({ type => 'daily_total' })->get_latest; + + is $today_board->values->count, 4, 'correct value count'; + + my $today_values = $today_board->values->search( + {}, + { + order_by => { -desc => 'value' }, + columns => [ qw/ user_id value / ], + }, + ); + $today_values->result_class( 'DBIx::Class::ResultClass::HashRefInflator' ); + + my $expected = [ + { user_id => 4, value => 95 }, + { user_id => 3, value => 85 }, + { user_id => 2, value => 75 }, + { user_id => 1, value => 65 }, + ]; + + is_deeply [ $today_values->all ], $expected, 'array as expected'; + + is $leaderboard_rs->find({ type => 'daily_total' })->sets->count, 4, 'correct leaderboard count'; +}; + +done_testing; diff --git a/t/schema/leaderboard_trend.t b/t/schema/leaderboard_trend.t new file mode 100644 index 0000000..f8b370a --- /dev/null +++ b/t/schema/leaderboard_trend.t @@ -0,0 +1,199 @@ +use Mojo::Base -strict; + +use Test::More; +use Mojo::JSON; +use Test::Pear::LocalLoop; +use DateTime; + +my $framework = Test::Pear::LocalLoop->new; +my $t = $framework->framework; +my $schema = $t->app->schema; +my $dtf = $schema->storage->datetime_parser; + +my $user1 = { + token => 'a', + full_name => 'Test User1', + display_name => 'Test User1', + email => 'test1@example.com', + postcode => 'LA1 1AA', + password => 'abc123', + age_range => 1, +}; + +my $user2 = { + token => 'b', + full_name => 'Test User2', + display_name => 'Test User2', + email => 'test2@example.com', + postcode => 'LA1 1AA', + password => 'abc123', + age_range => 1, +}; + +my $user3 = { + token => 'c', + full_name => 'Test User3', + display_name => 'Test User3', + email => 'test3@example.com', + postcode => 'LA1 1AA', + password => 'abc123', + age_range => 1, +}; + +my $user4 = { + token => 'd', + full_name => 'Test User4', + display_name => 'Test User4', + email => 'test4@example.com', + postcode => 'LA1 1AA', + password => 'abc123', + age_range => 1, +}; + +my $org = { + token => 'e', + email => 'test5@example.com', + name => 'Test Org', + street_name => 'Test Street', + town => 'Lancaster', + postcode => 'LA1 1AA', + password => 'abc123', +}; + +$schema->resultset('AccountToken')->create({ name => $_->{token} }) + for ( $user1, $user2, $user3, $user4, $org ); + +$framework->register_customer($_) + for ( $user1, $user2, $user3, $user4 ); + +$framework->register_organisation($org); + +my $org_result = $schema->resultset('Organisation')->find({ name => $org->{name} }); + +my $tweak = 0; + +my $now = DateTime->today(); + +{ + my $user_result = $schema->resultset('User')->find({ email => $user1->{email} }); + + $user_result->create_related( 'transactions', { + seller_id => $org_result->id, + value => 1, + proof_image => 'a', + }); + + $user_result->create_related( 'transactions', { + seller_id => $org_result->id, + value => 9, + proof_image => 'a', + submitted_at => $dtf->format_datetime($now->clone->subtract( days => 1 )), + }); +} + +{ + my $user_result = $schema->resultset('User')->find({ email => $user2->{email} }); + + $user_result->create_related( 'transactions', { + seller_id => $org_result->id, + value => 3, + proof_image => 'a', + }); + + $user_result->create_related( 'transactions', { + seller_id => $org_result->id, + value => 1, + proof_image => 'a', + submitted_at => $dtf->format_datetime($now->clone->subtract( days => 1 )), + }); +} + +{ + my $user_result = $schema->resultset('User')->find({ email => $user3->{email} }); + + $user_result->create_related( 'transactions', { + seller_id => $org_result->id, + value => 5, + proof_image => 'a', + }); + + $user_result->create_related( 'transactions', { + seller_id => $org_result->id, + value => 5, + proof_image => 'a', + submitted_at => $dtf->format_datetime($now->clone->subtract( days => 1 )), + }); +} + +{ + my $user_result = $schema->resultset('User')->find({ email => $user4->{email} }); + + $user_result->create_related( 'transactions', { + seller_id => $org_result->id, + value => 9, + proof_image => 'a', + }); + + $user_result->create_related( 'transactions', { + seller_id => $org_result->id, + value => 3, + proof_image => 'a', + submitted_at => $dtf->format_datetime($now->clone->subtract( days => 1 )), + }); +} + +my $session_key = $framework->login({ + email => $user1->{email}, + password => $user1->{password}, +}); + +sub test_leaderboard { + my ( $title, $name, $date, $expected ) = @_; + + subtest $title => sub { + my $leaderboard_rs = $schema->resultset('Leaderboard'); + + my $today_board = $leaderboard_rs->find({ type => $name })->create_new($date)->get_latest; + + is $today_board->values->count, 4, 'correct value count'; + + my $today_values = $today_board->values->search( + {}, + { + order_by => { -desc => 'value' }, + columns => [ qw/ user_id value trend / ], + }, + ); + $today_values->result_class( 'DBIx::Class::ResultClass::HashRefInflator' ); + + is_deeply [ $today_values->all ], $expected, 'array as expected'; + }; +} + +$schema->resultset('Leaderboard')->find({ type => 'daily_total' })->create_new( $now->clone->subtract( days => 1 ) ); + +test_leaderboard( + 'Daily Total', + 'daily_total', + $now, + [ + { user_id => 4, value => 9, trend => 1 }, + { user_id => 3, value => 5, trend => 0 }, + { user_id => 2, value => 3, trend => 1 }, + { user_id => 1, value => 1, trend => -1}, + ] +); + +test_leaderboard( + 'Daily Count', + 'daily_count', + $now, + [ + { user_id => 1, value => 1, trend => 0 }, + { user_id => 2, value => 1, trend => 0 }, + { user_id => 3, value => 1, trend => 0 }, + { user_id => 4, value => 1, trend => 0 }, + ] +); + +done_testing; diff --git a/t/schema/resultset_leaderboard.t b/t/schema/resultset_leaderboard.t new file mode 100644 index 0000000..76d622e --- /dev/null +++ b/t/schema/resultset_leaderboard.t @@ -0,0 +1,270 @@ +use Mojo::Base -strict; + +use Test::More; +use Mojo::JSON; +use Test::Pear::LocalLoop; +use DateTime; + +my $framework = Test::Pear::LocalLoop->new; +my $t = $framework->framework; +my $schema = $t->app->schema; +my $dtf = $schema->storage->datetime_parser; + +my $user1 = { + token => 'a', + full_name => 'Test User1', + display_name => 'Test User1', + email => 'test1@example.com', + postcode => 'LA1 1AA', + password => 'abc123', + age_range => 1, +}; + +my $user2 = { + token => 'b', + full_name => 'Test User2', + display_name => 'Test User2', + email => 'test2@example.com', + postcode => 'LA1 1AA', + password => 'abc123', + age_range => 1, +}; + +my $user3 = { + token => 'c', + full_name => 'Test User3', + display_name => 'Test User3', + email => 'test3@example.com', + postcode => 'LA1 1AA', + password => 'abc123', + age_range => 1, +}; + +my $user4 = { + token => 'd', + full_name => 'Test User4', + display_name => 'Test User4', + email => 'test4@example.com', + postcode => 'LA1 1AA', + password => 'abc123', + age_range => 1, +}; + +my $org = { + token => 'e', + email => 'test5@example.com', + name => 'Test Org', + street_name => 'Test Street', + town => 'Lancaster', + postcode => 'LA1 1AA', + password => 'abc123', +}; + +$schema->resultset('AccountToken')->create({ name => $_->{token} }) + for ( $user1, $user2, $user3, $user4, $org ); + +$framework->register_customer($_) + for ( $user1, $user2, $user3, $user4 ); + +$framework->register_organisation($org); + +my $org_result = $schema->resultset('Organisation')->find({ name => $org->{name} }); + +my $tweak = 0; + +my $now = DateTime->today(); + +for my $user ( $user1, $user2, $user3, $user4 ) { + $tweak ++; + my $user_result = $schema->resultset('User')->find({ email => $user->{email} }); + for ( 1 .. 10 ) { + $user_result->create_related( 'transactions', { + seller_id => $org_result->id, + value => $_ + $tweak, + proof_image => 'a', + }); + } + + for ( 11 .. 20 ) { + $user_result->create_related( 'transactions', { + seller_id => $org_result->id, + value => $_ + $tweak, + proof_image => 'a', + submitted_at => $dtf->format_datetime($now->clone->subtract( days => 5 )), + }); + } + + for ( 21 .. 30 ) { + $user_result->create_related( 'transactions', { + seller_id => $org_result->id, + value => $_ + $tweak, + proof_image => 'a', + submitted_at => $dtf->format_datetime($now->clone->subtract( days => 25 )), + }); + } + + for ( 31 .. 40 ) { + $user_result->create_related( 'transactions', { + seller_id => $org_result->id, + value => $_ + $tweak, + proof_image => 'a', + submitted_at => $dtf->format_datetime($now->clone->subtract( days => 50 )), + }); + } + + is $user_result->transactions->count, 40, 'correct count for user' . $tweak; +} + +sub test_leaderboard { + my ( $title, $name, $date, $expected ) = @_; + + subtest $title => sub { + my $leaderboard_rs = $schema->resultset('Leaderboard'); + + $leaderboard_rs->create_new( $name, $date ); + + my $today_board = $leaderboard_rs->get_latest( $name ); + + is $today_board->values->count, 4, 'correct value count'; + + my $today_values = $today_board->values->search( + {}, + { + order_by => { -desc => 'value' }, + columns => [ qw/ user_id value / ], + }, + ); + $today_values->result_class( 'DBIx::Class::ResultClass::HashRefInflator' ); + + is_deeply [ $today_values->all ], $expected, 'array as expected'; + }; +} + +test_leaderboard( + 'Daily Total', + 'daily_total', + $now, + [ + { user_id => 4, value => 95 }, + { user_id => 3, value => 85 }, + { user_id => 2, value => 75 }, + { user_id => 1, value => 65 }, + ] +); + +test_leaderboard( + 'Daily Count', + 'daily_count', + $now, + [ + { user_id => 1, value => 10 }, + { user_id => 2, value => 10 }, + { user_id => 3, value => 10 }, + { user_id => 4, value => 10 }, + ] +); + +test_leaderboard( + 'Weekly Total', + 'weekly_total', + $now->clone->subtract( days => 7 ), + [ + { user_id => 4, value => 195 }, + { user_id => 3, value => 185 }, + { user_id => 2, value => 175 }, + { user_id => 1, value => 165 }, + ] +); + +test_leaderboard( + 'Weekly Count', + 'weekly_count', + $now->clone->subtract( days => 7 ), + [ + { user_id => 1, value => 10 }, + { user_id => 2, value => 10 }, + { user_id => 3, value => 10 }, + { user_id => 4, value => 10 }, + ] +); + +test_leaderboard( + 'Monthly Total', + 'monthly_total', + $now->clone->subtract( months => 1 ), + [ + { user_id => 4, value => 490 }, + { user_id => 3, value => 470 }, + { user_id => 2, value => 450 }, + { user_id => 1, value => 430 }, + ] +); + +test_leaderboard( + 'Monthly Count', + 'monthly_count', + $now->clone->subtract( months => 1 ), + [ + { user_id => 1, value => 20 }, + { user_id => 2, value => 20 }, + { user_id => 3, value => 20 }, + { user_id => 4, value => 20 }, + ] +); + +test_leaderboard( + 'All Time Total', + 'all_time_total', + $now, + [ + { user_id => 4, value => 980 }, + { user_id => 3, value => 940 }, + { user_id => 2, value => 900 }, + { user_id => 1, value => 860 }, + ] +); + +test_leaderboard( + 'All Time Count', + 'all_time_count', + $now, + [ + { user_id => 1, value => 40 }, + { user_id => 2, value => 40 }, + { user_id => 3, value => 40 }, + { user_id => 4, value => 40 }, + ] +); + +subtest 'get_latest' => sub { + my $leaderboard_rs = $schema->resultset('Leaderboard'); + $leaderboard_rs->create_new( 'daily_total', $now->clone->subtract(days => 5)); + $leaderboard_rs->create_new( 'daily_total', $now->clone->subtract(days => 25)); + $leaderboard_rs->create_new( 'daily_total', $now->clone->subtract(days => 50)); + + my $today_board = $leaderboard_rs->get_latest( 'daily_total' ); + + is $today_board->values->count, 4, 'correct value count'; + + my $today_values = $today_board->values->search( + {}, + { + order_by => { -desc => 'value' }, + columns => [ qw/ user_id value / ], + }, + ); + $today_values->result_class( 'DBIx::Class::ResultClass::HashRefInflator' ); + + my $expected = [ + { user_id => 4, value => 95 }, + { user_id => 3, value => 85 }, + { user_id => 2, value => 75 }, + { user_id => 1, value => 65 }, + ]; + + is_deeply [ $today_values->all ], $expected, 'array as expected'; + + is $leaderboard_rs->find_by_type( 'daily_total' )->sets->count, 4, 'correct leaderboard count'; +}; + +done_testing;