diff --git a/CHANGELOG.md b/CHANGELOG.md index b3360e3..6eaefaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ # Next Release +# v0.10.1 + +* Added API for customer graphs +* Revamped graphs code +* Added API for customer local purchase pie charts +* Added API for customer snippets +* Added API for sector purchase list for customer dashboard +* **Admin Fix** Fixed org sector on user edit layout and text +* **Admin Feature** Added Sector U + # v0.10.0 * **API Change** Updated API for story trail maps diff --git a/cpanfile b/cpanfile index 1916f82..5ef4b1f 100644 --- a/cpanfile +++ b/cpanfile @@ -6,7 +6,6 @@ requires 'Mojo::JSON'; requires 'Email::Valid'; requires 'Geo::UK::Postcode::Regex' => '0.017'; requires 'Authen::Passphrase::BlowfishCrypt'; -requires 'Time::Fake'; requires 'Scalar::Util'; requires 'DBIx::Class'; requires 'DBIx::Class::PassphraseColumn'; @@ -25,6 +24,11 @@ requires 'GIS::Distance'; requires 'Text::CSV'; requires 'Try::Tiny'; +on 'test' => sub { + requires 'Test::More'; + requires 'Test::MockTime'; +}; + feature 'schema-graph', 'Draw diagrams of Schema' => sub { requires 'GraphViz'; requires 'SQL::Translator'; @@ -38,4 +42,3 @@ feature 'postgres', 'PostgreSQL Support' => sub { feature 'codepoint-open', 'Code Point Open manipulation' => sub { requires 'Geo::UK::Postcode::CodePointOpen'; }; - diff --git a/lib/Pear/LocalLoop.pm b/lib/Pear/LocalLoop.pm index 1e13f09..6ab7fa8 100644 --- a/lib/Pear/LocalLoop.pm +++ b/lib/Pear/LocalLoop.pm @@ -148,6 +148,7 @@ sub startup { $api->post('/user/account')->to('api-user#post_account_update'); $api->post('/user-history')->to('api-user#post_user_history'); $api->post('/stats')->to('api-stats#post_index'); + $api->post('/stats/customer')->to('api-stats#post_customer'); $api->post('/stats/leaderboard')->to('api-stats#post_leaderboards'); $api->post('/stats/leaderboard/paged')->to('api-stats#post_leaderboards_paged'); $api->post('/outgoing-transactions')->to('api-transactions#post_transaction_list_purchases'); @@ -171,6 +172,12 @@ sub startup { $api_v1_org->post('/employee')->to('api-organisation#post_employee_read'); $api_v1_org->post('/employee/add')->to('api-organisation#post_employee_add'); + my $api_v1_cust = $api_v1->under('/customer')->to('api-v1-customer#auth'); + + $api_v1_cust->post('/graphs')->to('api-v1-customer-graphs#index'); + $api_v1_cust->post('/snippets')->to('api-v1-customer-snippets#index'); + $api_v1_cust->post('/pies')->to('api-v1-customer-pies#index'); + my $admin_routes = $r->under('/admin')->to('admin#under'); $admin_routes->get('/home')->to('admin#home'); diff --git a/lib/Pear/LocalLoop/Controller/Admin/Users.pm b/lib/Pear/LocalLoop/Controller/Admin/Users.pm index e731d7b..a1d4aef 100644 --- a/lib/Pear/LocalLoop/Controller/Admin/Users.pm +++ b/lib/Pear/LocalLoop/Controller/Admin/Users.pm @@ -92,6 +92,11 @@ sub update { return $c->redirect_to( '/admin/users/' . $id ); } + my $location = $c->get_location_from_postcode( + $validation->param('postcode'), + $user->type, + ); + if ( $user->type eq 'customer' ){ try { @@ -100,6 +105,7 @@ sub update { full_name => $validation->param('full_name'), display_name => $validation->param('display_name'), postcode => $validation->param('postcode'), + ( defined $location ? ( %$location ) : ( latitude => undef, longitude => undef ) ), }); $user->update({ email => $validation->param('email'), @@ -125,6 +131,7 @@ sub update { town => $validation->param('town'), sector => $validation->param('sector'), postcode => $validation->param('postcode'), + ( defined $location ? ( %$location ) : ( latitude => undef, longitude => undef ) ), }); $user->update({ email => $validation->param('email'), diff --git a/lib/Pear/LocalLoop/Controller/Api/Stats.pm b/lib/Pear/LocalLoop/Controller/Api/Stats.pm index ad984e2..8545406 100644 --- a/lib/Pear/LocalLoop/Controller/Api/Stats.pm +++ b/lib/Pear/LocalLoop/Controller/Api/Stats.pm @@ -1,7 +1,7 @@ package Pear::LocalLoop::Controller::Api::Stats; use Mojo::Base 'Mojolicious::Controller'; -use List::Util qw/ first /; +use List::Util qw/ max sum /; has error_messages => sub { return { @@ -58,6 +58,80 @@ sub post_index { }); } +sub post_customer { + my $c = shift; + + my $entity = $c->stash->{api_user}->entity; + + my $duration = DateTime::Duration->new( weeks => 7 ); + my $end = DateTime->today; + my $start = $end->clone->subtract_duration( $duration ); + + my $dtf = $c->schema->storage->datetime_parser; + my $driver = $c->schema->storage->dbh->{Driver}->{Name}; + my $week_transaction_rs = $c->schema->resultset('ViewQuantisedTransaction' . $driver)->search( + { + purchase_time => { + -between => [ + $dtf->format_datetime($start), + $dtf->format_datetime($end), + ], + }, + buyer_id => $entity->id, + }, + { + columns => [ + { + quantised => 'quantised_weeks', + count => \"COUNT(*)", + } + ], + group_by => 'quantised_weeks', + order_by => { '-asc' => 'quantised_weeks' }, + } + ); + + my @all_weeks = $week_transaction_rs->all; + my $first = $all_weeks[0]->get_column('count') || 0; + my $second = $all_weeks[1]->get_column('count') || 0; + my $max = max( map { $_->get_column('count') } @all_weeks ); + my $sum = sum( map { $_->get_column('count') } @all_weeks ); + my $count = $week_transaction_rs->count; + + my $weeks = { + first => $first, + second => $second, + max => $max, + sum => $sum, + count => $count, + }; + + my $sectors = { sectors => [], purchases => [] }; + + my $sector_purchase_rs = $entity->purchases->search({}, + { + join => { 'seller' => 'organisation' }, + columns => { + sector => "organisation.sector", + count => \"COUNT(*)", + }, + group_by => "organisation.sector", + order_by => { '-desc' => $c->pg_or_sqlite('count',"COUNT(*)",)}, + } + ); + + for ( $sector_purchase_rs->all ) { + push @{ $sectors->{ sectors } }, $_->get_column('sector'); + push @{ $sectors->{ purchases } }, ($_->get_column('count') || 0); + } + + return $c->render( json => { + success => Mojo::JSON->true, + weeks => $weeks, + sectors => $sectors, + }); +} + sub post_leaderboards { my $c = shift; @@ -125,53 +199,74 @@ sub post_leaderboards_paged { my $page = 1; my $today_board = $leaderboard_rs->get_latest( $validation->param('type') ); + my @leaderboard_array; + my $current_user_position; + my $values_count = 0; + if ( defined $today_board ) { - if ( !defined $validation->param('page') || $validation->param('page') < 1 ) { - my $user_position = $today_board->values->find({ entity_id => $c->stash->{api_user}->entity->id }); - $page = int(defined $user_position ? $user_position->{position} : 0 / 10) + 1; - } else { - $page = $validation->param('page'); - } + if ( !defined $validation->param('page') || $validation->param('page') < 1 ) { + my $user_position = $today_board->values->find({ entity_id => $c->stash->{api_user}->entity->id }); + $page = int(defined $user_position ? $user_position->{position} : 0 / 10) + 1; + } else { + $page = $validation->param('page'); + } - my $today_values = $today_board->values->search( - {}, - { - page => $page, - rows => 10, - order_by => { -asc => 'me.position' }, - columns => [ - qw/ - me.value - me.trend - me.position - /, - { display_name => 'customer.display_name' }, - ], - join => { entity => 'customer' }, - }, - ); - $today_values->result_class( 'DBIx::Class::ResultClass::HashRefInflator' ); - - my @leaderboard_array = $today_values->all; - - if ( $validation->param('type') =~ /total$/ ) { - @leaderboard_array = (map { + my $today_values = $today_board->values->search( + {}, { - %$_, - value => $_->{value} / 100000, - } - } @leaderboard_array); + page => $page, + rows => 10, + order_by => { -asc => 'me.position' }, + columns => [ + qw/ + me.value + me.trend + me.position + /, + { display_name => 'customer.display_name' }, + ], + join => { entity => 'customer' }, + }, + ); + $today_values->result_class( 'DBIx::Class::ResultClass::HashRefInflator' ); + + @leaderboard_array = $today_values->all; + + $values_count = $today_values->pager->total_entries; + + if ( $validation->param('type') =~ /total$/ ) { + @leaderboard_array = (map { + { + %$_, + value => $_->{value} / 100000, + } + } @leaderboard_array); + } + + $current_user_position = $today_values->find({ entity_id => $c->stash->{api_user}->entity->id }); } - - my $current_user_position = $today_values->find({ entity_id => $c->stash->{api_user}->entity->id }); - return $c->render( json => { success => Mojo::JSON->true, leaderboard => [ @leaderboard_array ], user_position => defined $current_user_position ? $current_user_position->{position} : 0, page => $page, - count => $today_values->pager->total_entries, + count => $values_count, }); } +sub pg_or_sqlite { + my ( $c, $pg_sql, $sqlite_sql ) = @_; + + my $driver = $c->schema->storage->dbh->{Driver}->{Name}; + + if ( $driver eq 'Pg' ) { + return \$pg_sql; + } elsif ( $driver eq 'SQLite' ) { + return \$sqlite_sql; + } else { + $c->app->log->warn('Unknown Driver Used'); + return undef; + } +} + 1; diff --git a/lib/Pear/LocalLoop/Controller/Api/Upload.pm b/lib/Pear/LocalLoop/Controller/Api/Upload.pm index 15fa500..3783d7f 100644 --- a/lib/Pear/LocalLoop/Controller/Api/Upload.pm +++ b/lib/Pear/LocalLoop/Controller/Api/Upload.pm @@ -146,12 +146,18 @@ sub post_upload { return $c->api_validation_error if $validation->has_error; + my $location = $c->get_location_from_postcode( + $validation->param('postcode'), + 'organisation', + ); + my $entity = $c->schema->resultset('Entity')->create_org({ submitted_by_id => $user->id, name => $validation->param('organisation_name'), street_name => $validation->param('street_name'), town => $validation->param('town'), postcode => $validation->param('postcode'), + ( defined $location ? ( %$location ) : ( latitude => undef, longitude => undef ) ), pending => 1, }); $organisation = $entity->organisation; diff --git a/lib/Pear/LocalLoop/Controller/Api/V1/Customer.pm b/lib/Pear/LocalLoop/Controller/Api/V1/Customer.pm new file mode 100644 index 0000000..bb5623c --- /dev/null +++ b/lib/Pear/LocalLoop/Controller/Api/V1/Customer.pm @@ -0,0 +1,21 @@ +package Pear::LocalLoop::Controller::Api::V1::Customer; +use Mojo::Base 'Mojolicious::Controller'; + +sub auth { + my $c = shift; + + return 1 if $c->stash->{api_user}->type eq 'customer'; + + $c->render( + json => { + success => Mojo::JSON->false, + message => 'Not an Customer', + error => 'user_not_cust', + }, + status => 403, + ); + + return 0; +} + +1; diff --git a/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Graphs.pm b/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Graphs.pm new file mode 100644 index 0000000..f48e52b --- /dev/null +++ b/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Graphs.pm @@ -0,0 +1,167 @@ +package Pear::LocalLoop::Controller::Api::V1::Customer::Graphs; +use Mojo::Base 'Mojolicious::Controller'; + +has error_messages => sub { + return { + graph => { + required => { message => 'Must request graph type', status => 400 }, + in => { message => 'Unrecognised graph type', status => 400 }, + }, + }; +}; + +sub index { + my $c = shift; + + my $validation = $c->validation; + $validation->input( $c->stash->{api_json} ); + $validation->required('graph')->in( qw/ + total_last_week + avg_spend_last_week + total_last_month + avg_spend_last_month + / ); + + return $c->api_validation_error if $validation->has_error; + + my $graph_sub = "graph_" . $validation->param('graph'); + + unless ( $c->can($graph_sub) ) { + # Secondary catch in case a mistake has been made + return $c->render( + json => { + success => Mojo::JSON->false, + message => $c->error_messages->{graph}->{in}->{message}, + error => 'in', + }, + status => $c->error_messages->{graph}->{in}->{status}, + ); + } + + return $c->$graph_sub; +} + +sub graph_total_last_week { return shift->_purchases_total_duration( 7 ) } +sub graph_total_last_month { return shift->_purchases_total_duration( 30 ) } + +sub _purchases_total_duration { + my ( $c, $day_duration ) = @_; + + my $duration = DateTime::Duration->new( days => $day_duration ); + my $entity = $c->stash->{api_user}->entity; + + my $data = { labels => [], data => [] }; + + my ( $start, $end ) = $c->_get_start_end_duration( $duration ); + + $data->{bounds} = { + min => $c->format_iso_datetime( $start ), + max => $c->format_iso_datetime( $end ), + }; + + while ( $start < $end ) { + my $next_end = $start->clone->add( days => 1 ); + my $transactions = $entity->purchases + ->search_between( $start, $next_end ) + ->get_column('value') + ->sum || 0 * 1; + push @{ $data->{ labels } }, $c->format_iso_datetime( $start ); + push @{ $data->{ data } }, $transactions / 100000; + $start->add( days => 1 ); + } + + return $c->render( + json => { + success => Mojo::JSON->true, + graph => $data, + } + ); +} + +sub graph_avg_spend_last_week { return shift->_purchases_avg_spend_duration( 7 ) } +sub graph_avg_spend_last_month { return shift->_purchases_avg_spend_duration( 30 ) } + +sub _purchases_avg_spend_duration { + my ( $c, $day_duration ) = @_; + + my $duration = DateTime::Duration->new( days => $day_duration ); + my $entity = $c->stash->{api_user}->entity; + + my $data = { labels => [], data => [] }; + + my ( $start, $end ) = $c->_get_start_end_duration( $duration ); + + $data->{bounds} = { + min => $c->format_iso_datetime( $start ), + max => $c->format_iso_datetime( $end ), + }; + + my $dtf = $c->schema->storage->datetime_parser; + my $driver = $c->schema->storage->dbh->{Driver}->{Name}; + my $transaction_rs = $c->schema->resultset('ViewQuantisedTransaction' . $driver)->search( + { + purchase_time => { + -between => [ + $dtf->format_datetime($start), + $dtf->format_datetime($end), + ], + }, + buyer_id => $entity->id, + }, + { + columns => [ + { + quantised => 'quantised_days', + count => \"COUNT(*)", + sum_value => $c->pg_or_sqlite( + 'SUM("me"."value")', + 'SUM("me"."value")', + ), + average_value => $c->pg_or_sqlite( + 'AVG("me"."value")', + 'AVG("me"."value")', + ), + } + ], + group_by => 'quantised_days', + order_by => { '-asc' => 'quantised_days' }, + } + ); + + for ( $transaction_rs->all ) { + my $quantised = $c->db_datetime_parser->parse_datetime($_->get_column('quantised')); + push @{ $data->{ labels } }, $c->format_iso_datetime( $quantised ); + push @{ $data->{ data } }, ($_->get_column('average_value') || 0) / 100000; + } + + return $c->render( + json => { + success => Mojo::JSON->true, + graph => $data, + } + ); +} + +sub _get_start_end_duration { + my ( $c, $duration ) = @_; + my $end = DateTime->today; + my $start = $end->clone->subtract_duration( $duration ); + return ( $start, $end ); +} + +sub pg_or_sqlite { + my ( $c, $pg_sql, $sqlite_sql ) = @_; + + my $driver = $c->schema->storage->dbh->{Driver}->{Name}; + + if ( $driver eq 'Pg' ) { + return \$pg_sql; + } elsif ( $driver eq 'SQLite' ) { + return \$sqlite_sql; + } else { + $c->app->log->warn('Unknown Driver Used'); + return undef; + } +} + +1; diff --git a/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Pies.pm b/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Pies.pm new file mode 100644 index 0000000..06c57b1 --- /dev/null +++ b/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Pies.pm @@ -0,0 +1,62 @@ +package Pear::LocalLoop::Controller::Api::V1::Customer::Pies; +use Mojo::Base 'Mojolicious::Controller'; + +sub index { + my $c = shift; + + my $entity = $c->stash->{api_user}->entity; + + my $purchase_rs = $entity->purchases; + my $local_org_local_purchase = $purchase_rs->search({ + "me.distance" => { '<', 20000 }, + 'organisation.is_local' => 1, + }, + { + join => { 'seller' => 'organisation' }, + } + ); + + my $local_org_non_local_purchase = $purchase_rs->search({ + "me.distance" => { '>=', 20000 }, + 'organisation.is_local' => 1, + }, + { + join => { 'seller' => 'organisation' }, + } + ); + + my $non_local_org_local_purchase = $purchase_rs->search({ + "me.distance" => { '<', 20000 }, + 'organisation.is_local' => 0, + }, + { + join => { 'seller' => 'organisation' }, + } + ); + + my $non_local_org_non_local_purchase = $purchase_rs->search({ + "me.distance" => { '>=', 20000 }, + 'organisation.is_local' => 0, + }, + { + join => { 'seller' => 'organisation' }, + } + ); + + my $data = { + 'Local shop local purchaser' => $local_org_local_purchase->count, + 'Local shop non-local purchaser' => $local_org_non_local_purchase->count, + 'Non-local shop local purchaser' => $non_local_org_local_purchase->count, + 'Non-local shop non-local purchaser' => $non_local_org_non_local_purchase->count, + }; + + return $c->render( + json => { + success => Mojo::JSON->true, + pie => $data, + } + ); + +} + +1; diff --git a/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Snippets.pm b/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Snippets.pm new file mode 100644 index 0000000..139b796 --- /dev/null +++ b/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Snippets.pm @@ -0,0 +1,32 @@ +package Pear::LocalLoop::Controller::Api::V1::Customer::Snippets; +use Mojo::Base 'Mojolicious::Controller'; + +sub index { + my $c = shift; + + my $entity = $c->stash->{api_user}->entity; + my $data = { + user_sum => 0, + user_position => 0, + }; + + my $user_rs = $entity->purchases; + $data->{ user_sum } = $user_rs->get_column('value')->sum || 0; + $data->{ user_sum } /= 100000; + + my $leaderboard_rs = $c->schema->resultset('Leaderboard'); + my $monthly_board = $leaderboard_rs->get_latest( 'monthly_total' ); + if (defined $monthly_board) { + my $monthly_values = $monthly_board->values; + $data->{ user_position } = $monthly_values ? $monthly_values->find({ entity_id => $entity->id })->position : 0; + } + return $c->render( + json => { + success => Mojo::JSON->true, + snippets => $data, + } + ); + +} + +1; diff --git a/lib/Pear/LocalLoop/Controller/Api/V1/Organisation/Graphs.pm b/lib/Pear/LocalLoop/Controller/Api/V1/Organisation/Graphs.pm index d1eddaf..a6f56eb 100644 --- a/lib/Pear/LocalLoop/Controller/Api/V1/Organisation/Graphs.pm +++ b/lib/Pear/LocalLoop/Controller/Api/V1/Organisation/Graphs.pm @@ -78,35 +78,30 @@ sub graph_customers_range { ); } -sub graph_customers_last_7_days { - my $c = shift; - - my $duration = DateTime::Duration->new( days => 7 ); - return $c->_customers_last_duration( $duration ); -} - -sub graph_customers_last_30_days { - my $c = shift; - - my $duration = DateTime::Duration->new( days => 30 ); - return $c->_customers_last_duration( $duration ); -} +sub graph_customers_last_7_days { return shift->_customers_last_duration( 7 ) } +sub graph_customers_last_30_days { return shift->_customers_last_duration( 30 ) } sub _customers_last_duration { - my ( $c, $duration ) = @_; + my ( $c, $day_duration ) = @_; + my $duration = DateTime::Duration->new( days => $day_duration ); my $entity = $c->stash->{api_user}->entity; my $data = { labels => [], data => [] }; my ( $start, $end ) = $c->_get_start_end_duration( $duration ); + $data->{bounds} = { + min => $c->format_iso_datetime( $start ), + max => $c->format_iso_datetime( $end ), + }; + while ( $start < $end ) { my $next_end = $start->clone->add( days => 1 ); my $transactions = $entity->sales ->search_between( $start, $next_end ) ->count; - push @{ $data->{ labels } }, $start->day_name; + push @{ $data->{ labels } }, $c->format_iso_datetime( $start ); push @{ $data->{ data } }, $transactions; $start->add( days => 1 ); } @@ -132,13 +127,18 @@ sub _sales_last_duration { my ( $start, $end ) = $c->_get_start_end_duration( $duration ); + $data->{bounds} = { + min => $c->format_iso_datetime( $start ), + max => $c->format_iso_datetime( $end ), + }; + while ( $start < $end ) { my $next_end = $start->clone->add( days => 1 ); my $transactions = $entity->sales ->search_between( $start, $next_end ) ->get_column('value') ->sum || 0 + 0; - push @{ $data->{ labels } }, $start->day_name; + push @{ $data->{ labels } }, $c->format_iso_datetime( $start ); push @{ $data->{ data } }, $transactions / 100000; $start->add( days => 1 ); } @@ -164,13 +164,18 @@ sub _purchases_last_duration { my ( $start, $end ) = $c->_get_start_end_duration( $duration ); + $data->{bounds} = { + min => $c->format_iso_datetime( $start ), + max => $c->format_iso_datetime( $end ), + }; + while ( $start < $end ) { my $next_end = $start->clone->add( days => 1 ); my $transactions = $entity->purchases ->search_between( $start, $next_end ) ->get_column('value') ->sum || 0 + 0; - push @{ $data->{ labels } }, $start->day_name; + push @{ $data->{ labels } }, $c->format_iso_datetime( $start ); push @{ $data->{ data } }, $transactions / 100000; $start->add( days => 1 ); } diff --git a/lib/Pear/LocalLoop/Schema/Result/ViewQuantisedTransactionPg.pm b/lib/Pear/LocalLoop/Schema/Result/ViewQuantisedTransactionPg.pm index fabbd38..91ceede 100644 --- a/lib/Pear/LocalLoop/Schema/Result/ViewQuantisedTransactionPg.pm +++ b/lib/Pear/LocalLoop/Schema/Result/ViewQuantisedTransactionPg.pm @@ -13,8 +13,11 @@ __PACKAGE__->result_source_instance->view_definition( qq/ SELECT "value", "distance", "purchase_time", + "buyer_id", + "seller_id", DATE_TRUNC('hour', "purchase_time") AS "quantised_hours", - DATE_TRUNC('day', "purchase_time") AS "quantised_days" + DATE_TRUNC('day', "purchase_time") AS "quantised_days", + DATE_TRUNC('week', "purchase_time") AS "quantised_weeks" FROM "transactions" /); diff --git a/lib/Pear/LocalLoop/Schema/Result/ViewQuantisedTransactionSQLite.pm b/lib/Pear/LocalLoop/Schema/Result/ViewQuantisedTransactionSQLite.pm index 2ce3aac..abf95dd 100644 --- a/lib/Pear/LocalLoop/Schema/Result/ViewQuantisedTransactionSQLite.pm +++ b/lib/Pear/LocalLoop/Schema/Result/ViewQuantisedTransactionSQLite.pm @@ -13,8 +13,11 @@ __PACKAGE__->result_source_instance->view_definition( qq/ SELECT "value", "distance", "purchase_time", + "buyer_id", + "seller_id", DATETIME(STRFTIME('%Y-%m-%d %H:00:00',"purchase_time")) AS "quantised_hours", - DATETIME(STRFTIME('%Y-%m-%d 00:00:00',"purchase_time")) AS "quantised_days" + DATETIME(STRFTIME('%Y-%m-%d 00:00:00',"purchase_time")) AS "quantised_days", + DATETIME(STRFTIME('%Y-%m-%d 00:00:00',"purchase_time", 'weekday 1')) AS "quantised_weeks" FROM "transactions" /); diff --git a/t/api/stats.t b/t/api/stats.t index 0484664..3df45c8 100644 --- a/t/api/stats.t +++ b/t/api/stats.t @@ -1,5 +1,9 @@ use Mojo::Base -strict; +BEGIN { + use Test::MockTime qw/ set_absolute_time /; +} + use FindBin qw/ $Bin /; use Test::More; @@ -14,98 +18,61 @@ $framework->install_fixtures('users'); my $t = $framework->framework; my $schema = $t->app->schema; -my $dtf = $schema->storage->datetime_parser; -my $org_result = $schema->resultset('Organisation')->find({ name => 'Test Org' })->entity; -my $user_result = $schema->resultset('User')->find({ email => 'test1@example.com' })->entity; +set_absolute_time('2017-01-01T00:00:00Z'); + +my $start = DateTime->today->subtract( hours => 12 ); + +# create 40 days worth of data +for my $count ( 0 .. 40 ) { + my $trans_day = $start->clone->subtract( days => $count ); + + create_random_transaction( 'test1@example.com', $trans_day ); + if ( $count % 2 ) { + create_random_transaction( 'test1@example.com', $trans_day ); + } + if ( $count % 3 ) { + create_random_transaction( 'test1@example.com', $trans_day ); + } + if ( $count % 4 ) { + create_random_transaction( 'test1@example.com', $trans_day ); + } +} my $session_key = $framework->login({ - email => 'test1@example.com', + email => 'test1@example.com', password => 'abc123', }); -$t->app->schema->resultset('Leaderboard')->create_new( 'monthly_total', DateTime->now->truncate(to => 'month' )->subtract( months => 1) ); - -$t->post_ok('/api/stats' => json => { session_key => $session_key } ) +$t->post_ok('/api/stats/customer' => json => { + session_key => $session_key, + }) ->status_is(200)->or($framework->dump_error) - ->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); + ->json_is('/weeks', { + first => 2, + second => 21, + max => 22, + sum => 118, + count => 7, + }) + ->json_is('/sectors', { + sectors => ['A'], + purchases => [118], + }); -for ( 1 .. 10 ) { - $user_result->create_related( 'purchases', { - seller_id => $org_result->id, - value => $_ * 100000, +sub create_random_transaction { + my $buyer = shift; + my $time = shift; + + my $buyer_result = $schema->resultset('User')->find({ email => $buyer })->entity; + my $seller_result = $schema->resultset('Organisation')->find({ name => 'Test Org' })->entity; + $schema->resultset('Transaction')->create({ + buyer => $buyer_result, + seller => $seller_result, + value => 10 * 100000, proof_image => 'a', + purchase_time => $time, }); } -for ( 11 .. 20 ) { - $user_result->create_related( 'purchases', { - seller_id => $org_result->id, - value => $_ * 100000, - proof_image => 'a', - purchase_time => $dtf->format_datetime(DateTime->today()->subtract( days => 5 )), - }); -} - -for ( 21 .. 30 ) { - $user_result->create_related( 'purchases', { - seller_id => $org_result->id, - value => $_ * 100000, - proof_image => 'a', - purchase_time => $dtf->format_datetime(DateTime->today()->subtract( days => 25 )), - }); -} - -for ( 31 .. 40 ) { - $user_result->create_related( 'purchases', { - seller_id => $org_result->id, - value => $_ * 100000, - proof_image => 'a', - purchase_time => $dtf->format_datetime(DateTime->today()->subtract( days => 50 )), - }); -} - -for ( 41 .. 50 ) { - $org_result->create_related( 'purchases', { - seller_id => $org_result->id, - value => $_ * 100000, - proof_image => 'a', - purchase_time => $dtf->format_datetime(DateTime->today()->subtract( days => 50 )), - }); -} - -is $user_result->purchases->search({ - purchase_time => { - -between => [ - $dtf->format_datetime(DateTime->today()), - $dtf->format_datetime(DateTime->today()->add( days => 1 )), - ], - }, -})->get_column('value')->sum, 5500000, 'Got correct sum'; -is $user_result->purchases->today_rs->get_column('value')->sum, 5500000, '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/v1/customer/graphs.t b/t/api/v1/customer/graphs.t new file mode 100644 index 0000000..a2e6010 --- /dev/null +++ b/t/api/v1/customer/graphs.t @@ -0,0 +1,136 @@ +use Mojo::Base -strict; + +use FindBin qw/ $Bin /; + +use Test::More; +use Mojo::JSON; +use Test::Pear::LocalLoop; +use DateTime; + +my $framework = Test::Pear::LocalLoop->new( + etc_dir => "$Bin/../../../etc", +); +$framework->install_fixtures('users'); + +my $t = $framework->framework; +my $schema = $t->app->schema; + +my $start = DateTime->today->subtract( hours => 12 ); + +# create 30 days worth of data +for my $count ( 0 .. 29 ) { + my $trans_day = $start->clone->subtract( days => $count ); + + create_random_transaction( 'test1@example.com', $trans_day ); + if ( $count % 2 ) { + create_random_transaction( 'test1@example.com', $trans_day ); + } + if ( $count % 3 ) { + create_random_transaction( 'test1@example.com', $trans_day ); + } + if ( $count % 4 ) { + create_random_transaction( 'test1@example.com', $trans_day ); + } +} + +my $session_key = $framework->login({ + email => 'test1@example.com', + password => 'abc123', +}); + +$t->post_ok('/api/v1/customer/graphs' => json => { + session_key => $session_key, + graph => 'total_last_week', + }) + ->status_is(200)->or($framework->dump_error) + ->json_is('/graph', { + labels => [ map { $t->app->format_iso_datetime( + $start->clone->subtract( days => $_ )->subtract( hours => 12 ) + ) } reverse ( 0 .. 6 ) ], + bounds => { + min => $t->app->format_iso_datetime($start->clone->subtract( days => 6 )->subtract( hours => 12 ) ), + max => $t->app->format_iso_datetime($start->clone->add( hours => 12 )), + }, + data => [ 20, 40, 20, 30, 30, 40, 10 ], + }); + +$t->post_ok('/api/v1/customer/graphs' => json => { + session_key => $session_key, + graph => 'avg_spend_last_week', + }) + ->status_is(200)->or($framework->dump_error) + ->json_is('/graph', { + labels => [ map { $t->app->format_iso_datetime( + $start->clone->subtract( days => $_ )->subtract( hours => 12 ) + ) } reverse ( 0 .. 6 ) ], + bounds => { + min => $t->app->format_iso_datetime($start->clone->subtract( days => 6 )->subtract( hours => 12 ) ), + max => $t->app->format_iso_datetime($start->clone->add( hours => 12 )), + }, + data => [ 10, 10, 10, 10, 10, 10, 10 ], + }); + +$t->post_ok('/api/v1/customer/graphs' => json => { + session_key => $session_key, + graph => 'total_last_month', + }) + ->status_is(200)->or($framework->dump_error) + ->json_is('/graph', { + labels => [ map { $t->app->format_iso_datetime( + $start->clone->subtract( days => $_ )->subtract( hours => 12 ) + ) } reverse ( 0 .. 29 ) ], + bounds => { + min => $t->app->format_iso_datetime($start->clone->subtract( days => 29 )->subtract( hours => 12 ) ), + max => $t->app->format_iso_datetime($start->clone->add( hours => 12 )), + }, + data => [ 40, 20, 30, 30, 40, 10, 40, 30, 30, 20, 40, 20, 40, 20, 30, 30, 40, 10, 40, 30, 30, 20, 40, 20, 40, 20, 30, 30, 40, 10 ], + }); + +$t->post_ok('/api/v1/customer/graphs' => json => { + session_key => $session_key, + graph => 'avg_spend_last_month', + }) + ->status_is(200)->or($framework->dump_error) + ->json_is('/graph', { + labels => [ map { $t->app->format_iso_datetime( + $start->clone->subtract( days => $_ )->subtract( hours => 12 ) + ) } reverse ( 0 .. 29 ) ], + bounds => { + min => $t->app->format_iso_datetime($start->clone->subtract( days => 29 )->subtract( hours => 12 ) ), + max => $t->app->format_iso_datetime($start->clone->add( hours => 12 )), + }, + data => [ 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10 ], + }); + +$framework->logout( $session_key ); + +$session_key = $framework->login({ + email => 'org@example.com', + password => 'abc123', +}); + +$t->post_ok('/api/v1/customer/graphs' => json => { + session_key => $session_key, + graph => 'avg_spend_last_week', + }) + ->status_is(403) + ->json_is('/success', Mojo::JSON->false) + ->json_is('/error', 'user_not_cust'); + + +sub create_random_transaction { + my $buyer = shift; + my $time = shift; + + my $buyer_result = $schema->resultset('User')->find({ email => $buyer })->entity; + my $seller_result = $schema->resultset('Organisation')->find({ name => 'Test Org' })->entity; + $schema->resultset('Transaction')->create({ + buyer => $buyer_result, + seller => $seller_result, + value => 10 * 100000, + proof_image => 'a', + purchase_time => $time, + }); +} + +done_testing; diff --git a/t/api/v1/customer/pies.t b/t/api/v1/customer/pies.t new file mode 100644 index 0000000..53fdd8f --- /dev/null +++ b/t/api/v1/customer/pies.t @@ -0,0 +1,67 @@ +use Mojo::Base -strict; + +use FindBin qw/ $Bin /; + +use Test::More; +use Mojo::JSON; +use Test::Pear::LocalLoop; +use DateTime; + +my $framework = Test::Pear::LocalLoop->new( + etc_dir => "$Bin/../../../etc", +); +$framework->install_fixtures('users'); + +my $t = $framework->framework; +my $schema = $t->app->schema; + +my $start = DateTime->today->subtract( hours => 12 ); + +# create 30 days worth of data +for my $count ( 0 .. 29 ) { + my $trans_day = $start->clone->subtract( days => $count ); + + create_random_transaction( 'test1@example.com', $trans_day ); + if ( $count % 2 ) { + create_random_transaction( 'test1@example.com', $trans_day ); + } + if ( $count % 3 ) { + create_random_transaction( 'test1@example.com', $trans_day ); + } + if ( $count % 4 ) { + create_random_transaction( 'test1@example.com', $trans_day ); + } +} + +my $session_key = $framework->login({ + email => 'test1@example.com', + password => 'abc123', +}); + +$t->post_ok('/api/v1/customer/pies' => json => { + session_key => $session_key, + }) + ->status_is(200)->or($framework->dump_error) + ->json_is('/pie', { + 'Local shop local purchaser' => 0, + 'Local shop non-local purchaser' => 0, + 'Non-local shop local purchaser' => 0, + 'Non-local shop non-local purchaser' => 0, + }); + +sub create_random_transaction { + my $buyer = shift; + my $time = shift; + + my $buyer_result = $schema->resultset('User')->find({ email => $buyer })->entity; + my $seller_result = $schema->resultset('Organisation')->find({ name => 'Test Org' })->entity; + $schema->resultset('Transaction')->create({ + buyer => $buyer_result, + seller => $seller_result, + value => 10 * 100000, + proof_image => 'a', + purchase_time => $time, + }); +} + +done_testing; diff --git a/t/api/v1/customer/snippets.t b/t/api/v1/customer/snippets.t new file mode 100644 index 0000000..4a3ae8c --- /dev/null +++ b/t/api/v1/customer/snippets.t @@ -0,0 +1,74 @@ +use Mojo::Base -strict; + +use FindBin qw/ $Bin /; + +use Test::More; +use Mojo::JSON; +use Test::Pear::LocalLoop; +use DateTime; + +my $framework = Test::Pear::LocalLoop->new( + etc_dir => "$Bin/../../../etc", +); +$framework->install_fixtures('users'); + +my $t = $framework->framework; +my $schema = $t->app->schema; + +$t->app->schema->resultset('Leaderboard')->create_new( 'monthly_total', DateTime->now->truncate(to => 'month' )->subtract( months => 1) ); + +my $start = DateTime->today->subtract( hours => 12 ); + +# create 30 days worth of data +for my $count ( 0 .. 60 ) { + my $trans_day = $start->clone->subtract( days => $count ); + + create_random_transaction( 'test1@example.com', $trans_day ); + if ( $count % 2 ) { + create_random_transaction( 'test2@example.com', $trans_day ); + } + if ( $count % 3 ) { + create_random_transaction( 'test3@example.com', $trans_day ); + } + if ( $count % 4 ) { + create_random_transaction( 'test4@example.com', $trans_day ); + } +} + +my $session_key = $framework->login({ + email => 'test1@example.com', + password => 'abc123', +}); + +$t->post_ok('/api/v1/customer/snippets' => json => { + session_key => $session_key, + }) + ->status_is(200)->or($framework->dump_error) + ->json_is('/snippets', { + user_sum => 610, + user_position => 1, + }); + +$framework->logout( $session_key ); + +$session_key = $framework->login({ + email => 'test1@example.com', + password => 'abc123', +}); + +sub create_random_transaction { + my $buyer = shift; + my $time = shift; + + my $buyer_result = $schema->resultset('User')->find({ email => $buyer })->entity; + my $seller_result = $schema->resultset('Organisation')->find({ name => 'Test Org' })->entity; + $schema->resultset('Transaction')->create({ + buyer => $buyer_result, + seller => $seller_result, + value => 10 * 100000, + proof_image => 'a', + purchase_time => $time, + }); +} + +done_testing; diff --git a/t/api/v1/organisation/graphs.t b/t/api/v1/organisation/graphs.t index 57b6cbb..3e438b4 100644 --- a/t/api/v1/organisation/graphs.t +++ b/t/api/v1/organisation/graphs.t @@ -44,7 +44,13 @@ $t->post_ok('/api/v1/organisation/graphs' => json => { }) ->status_is(200)->or($framework->dump_error) ->json_is('/graph', { - labels => [ map { $start->clone->subtract( days => $_ )->day_name } reverse ( 0 .. 6 ) ], + labels => [ map { $t->app->format_iso_datetime( + $start->clone->subtract( days => $_ )->subtract( hours => 12 ) + ) } reverse ( 0 .. 6 ) ], + bounds => { + min => $t->app->format_iso_datetime($start->clone->subtract( days => 6 )->subtract( hours => 12 ) ), + max => $t->app->format_iso_datetime($start->clone->add( hours => 12 )), + }, data => [ 2, 4, 2, 3, 3, 4, 1 ], }); @@ -54,7 +60,13 @@ $t->post_ok('/api/v1/organisation/graphs' => json => { }) ->status_is(200)->or($framework->dump_error) ->json_is('/graph', { - labels => [ map { $start->clone->subtract( days => $_ )->day_name } reverse ( 0 .. 29 ) ], + labels => [ map { $t->app->format_iso_datetime( + $start->clone->subtract( days => $_ )->subtract( hours => 12 ) + ) } reverse ( 0 .. 29 ) ], + bounds => { + min => $t->app->format_iso_datetime($start->clone->subtract( days => 29 )->subtract( hours => 12 ) ), + max => $t->app->format_iso_datetime($start->clone->add( hours => 12 )), + }, data => [ 4, 2, 3, 3, 4, 1, 4, 3, 3, 2, 4, 2, 4, 2, 3, 3, 4, 1, 4, 3, 3, 2, 4, 2, 4, 2, 3, 3, 4, 1 ], }); @@ -64,7 +76,13 @@ $t->post_ok('/api/v1/organisation/graphs' => json => { }) ->status_is(200)->or($framework->dump_error) ->json_is('/graph', { - labels => [ map { $start->clone->subtract( days => $_ )->day_name } reverse ( 0 .. 6 ) ], + labels => [ map { $t->app->format_iso_datetime( + $start->clone->subtract( days => $_ )->subtract( hours => 12 ) + ) } reverse ( 0 .. 6 ) ], + bounds => { + min => $t->app->format_iso_datetime($start->clone->subtract( days => 6 )->subtract( hours => 12 ) ), + max => $t->app->format_iso_datetime($start->clone->add( hours => 12 )), + }, data => [ 20, 40, 20, 30, 30, 40, 10 ], }); @@ -74,7 +92,13 @@ $t->post_ok('/api/v1/organisation/graphs' => json => { }) ->status_is(200)->or($framework->dump_error) ->json_is('/graph', { - labels => [ map { $start->clone->subtract( days => $_ )->day_name } reverse ( 0 .. 29 ) ], + labels => [ map { $t->app->format_iso_datetime( + $start->clone->subtract( days => $_ )->subtract( hours => 12 ) + ) } reverse ( 0 .. 29 ) ], + bounds => { + min => $t->app->format_iso_datetime($start->clone->subtract( days => 29 )->subtract( hours => 12 ) ), + max => $t->app->format_iso_datetime($start->clone->add( hours => 12 )), + }, data => [ 40, 20, 30, 30, 40, 10, 40, 30, 30, 20, 40, 20, 40, 20, 30, 30, 40, 10, 40, 30, 30, 20, 40, 20, 40, 20, 30, 30, 40, 10 ], }); diff --git a/t/etc/fixtures/config/users.pl b/t/etc/fixtures/config/users.pl index 9a7f5ca..0341077 100644 --- a/t/etc/fixtures/config/users.pl +++ b/t/etc/fixtures/config/users.pl @@ -91,6 +91,7 @@ my $entity5 = { street_name => 'Test Street', town => 'Lancaster', postcode => 'LA1 1AA', + sector => 'A', }, user => { email => 'org@example.com', @@ -124,4 +125,3 @@ $fixtures->dump({ schema => $schema, directory => "$Bin/../data/" . $data_set, }); - diff --git a/t/etc/fixtures/data/users/organisations/1.fix b/t/etc/fixtures/data/users/organisations/1.fix index b761450..72165ed 100644 --- a/t/etc/fixtures/data/users/organisations/1.fix +++ b/t/etc/fixtures/data/users/organisations/1.fix @@ -7,7 +7,7 @@ $HASH1 = { pending => 0, postcode => 'LA1 1AA', - sector => undef, + sector => 'A', street_name => 'Test Street', submitted_by_id diff --git a/templates/admin/users/read.html.ep b/templates/admin/users/read.html.ep index 7c93f1f..0a63324 100644 --- a/templates/admin/users/read.html.ep +++ b/templates/admin/users/read.html.ep @@ -81,8 +81,10 @@
- - + +
% } else { diff --git a/templates/partials/sector_options.html.ep b/templates/partials/sector_options.html.ep index 625e89f..0c84578 100644 --- a/templates/partials/sector_options.html.ep +++ b/templates/partials/sector_options.html.ep @@ -19,3 +19,4 @@ +