From 7d309755c69d3b2230451b389c4ce82c7b4ac0f4 Mon Sep 17 00:00:00 2001 From: Finn Date: Tue, 12 Dec 2017 13:32:52 +0000 Subject: [PATCH 01/17] adding paths and placeholder for customer dash API --- lib/Pear/LocalLoop.pm | 4 + .../LocalLoop/Controller/Api/V1/Customer.pm | 21 ++ .../Controller/Api/V1/Customer/Graphs.pm | 193 ++++++++++++++++++ 3 files changed, 218 insertions(+) create mode 100644 lib/Pear/LocalLoop/Controller/Api/V1/Customer.pm create mode 100644 lib/Pear/LocalLoop/Controller/Api/V1/Customer/Graphs.pm diff --git a/lib/Pear/LocalLoop.pm b/lib/Pear/LocalLoop.pm index 1e13f09..ac596a0 100644 --- a/lib/Pear/LocalLoop.pm +++ b/lib/Pear/LocalLoop.pm @@ -171,6 +171,10 @@ 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_org->post('/graphs')->to('api-v1-customer-graphs#index'); + my $admin_routes = $r->under('/admin')->to('admin#under'); $admin_routes->get('/home')->to('admin#home'); 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..75a98b5 --- /dev/null +++ b/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Graphs.pm @@ -0,0 +1,193 @@ +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/ + customers_last_7_days + customers_last_30_days + sales_last_7_days + sales_last_30_days + purchases_last_7_days + purchases_last_30_days + customers_range + / ); + + 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_customers_range { + my $c = shift; + + my $validation = $c->validation; + $validation->input( $c->stash->{api_json} ); + $validation->required('start')->is_iso_date; + $validation->required('end')->is_iso_date; + + return $c->api_validation_error if $validation->has_error; + + my $entity = $c->stash->{api_user}->entity; + + my $data = { labels => [], data => [] }; + my $start = $c->parse_iso_date( $validation->param('start') ); + my $end = $c->parse_iso_date( $validation->param('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 } }, $c->format_iso_date( $start ); + push @{ $data->{ data } }, $transactions; + $start->add( days => 1 ); + } + + return $c->render( + json => { + success => Mojo::JSON->true, + graph => $data, + } + ); +} + +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 _customers_last_duration { + my ( $c, $duration ) = @_; + + my $entity = $c->stash->{api_user}->entity; + + my $data = { labels => [], data => [] }; + + my ( $start, $end ) = $c->_get_start_end_duration( $duration ); + + 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->{ data } }, $transactions; + $start->add( days => 1 ); + } + + return $c->render( + json => { + success => Mojo::JSON->true, + graph => $data, + } + ); +} + +sub graph_sales_last_7_days { return shift->_sales_last_duration( 7 ) } +sub graph_sales_last_30_days { return shift->_sales_last_duration( 30 ) } + +sub _sales_last_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 ); + + 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->{ data } }, $transactions / 100000; + $start->add( days => 1 ); + } + + return $c->render( + json => { + success => Mojo::JSON->true, + graph => $data, + } + ); +} + +sub graph_purchases_last_7_days { return shift->_purchases_last_duration( 7 ) } +sub graph_purchases_last_30_days { return shift->_purchases_last_duration( 30 ) } + +sub _purchases_last_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 ); + + 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->{ data } }, $transactions / 100000; + $start->add( days => 1 ); + } + + 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 ); +} + +1; From 1057eda445d9769ca0975af9c1d0ed9c49075cda Mon Sep 17 00:00:00 2001 From: Finn Date: Tue, 12 Dec 2017 17:21:32 +0000 Subject: [PATCH 02/17] added snippet endpoint and test --- lib/Pear/LocalLoop.pm | 3 +- .../Controller/Api/V1/Customer/Graphs.pm | 15 ++-- .../Controller/Api/V1/Customer/Snippets.pm | 31 ++++++++ t/api/v1/customer/snippets.t | 74 +++++++++++++++++++ 4 files changed, 115 insertions(+), 8 deletions(-) create mode 100644 lib/Pear/LocalLoop/Controller/Api/V1/Customer/Snippets.pm create mode 100644 t/api/v1/customer/snippets.t diff --git a/lib/Pear/LocalLoop.pm b/lib/Pear/LocalLoop.pm index ac596a0..f7f4ba4 100644 --- a/lib/Pear/LocalLoop.pm +++ b/lib/Pear/LocalLoop.pm @@ -173,7 +173,8 @@ sub startup { my $api_v1_cust = $api_v1->under('/customer')->to('api-v1-customer#auth'); - $api_v1_org->post('/graphs')->to('api-v1-customer-graphs#index'); + $api_v1_cust->post('/graphs')->to('api-v1-customer-graphs#index'); + $api_v1_cust->post('/snippets')->to('api-v1-customer-snippets#index'); my $admin_routes = $r->under('/admin')->to('admin#under'); diff --git a/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Graphs.pm b/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Graphs.pm index 75a98b5..b669dc0 100644 --- a/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Graphs.pm +++ b/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Graphs.pm @@ -16,13 +16,14 @@ sub index { my $validation = $c->validation; $validation->input( $c->stash->{api_json} ); $validation->required('graph')->in( qw/ - customers_last_7_days - customers_last_30_days - sales_last_7_days - sales_last_30_days - purchases_last_7_days - purchases_last_30_days - customers_range + total_today + avg_spend_today + total_last_week + avg_spend_last_week + total_last_month + avg_spend_last_month + total_user + avg_spend_user / ); return $c->api_validation_error if $validation->has_error; 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..c1846c9 --- /dev/null +++ b/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Snippets.pm @@ -0,0 +1,31 @@ +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' ); + 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/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; From 5049fa9eedce1f375dcfedc235e6e32f59e97e1f Mon Sep 17 00:00:00 2001 From: Finn Date: Tue, 12 Dec 2017 17:23:32 +0000 Subject: [PATCH 03/17] Placeholder test added for graphs --- t/api/v1/customer/graphs.t | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 t/api/v1/customer/graphs.t diff --git a/t/api/v1/customer/graphs.t b/t/api/v1/customer/graphs.t new file mode 100644 index 0000000..6f9a3d9 --- /dev/null +++ b/t/api/v1/customer/graphs.t @@ -0,0 +1,5 @@ +use Mojo::Base -strict; + +use Test::More skip_all => 'Test needs making!'; + +done_testing; From 536ab2155112bc8b0f928dcaf7703c4f2fc3df97 Mon Sep 17 00:00:00 2001 From: Finn Date: Tue, 12 Dec 2017 17:31:05 +0000 Subject: [PATCH 04/17] fixed snippets if no values to get --- lib/Pear/LocalLoop/Controller/Api/V1/Customer/Snippets.pm | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Snippets.pm b/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Snippets.pm index c1846c9..139b796 100644 --- a/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Snippets.pm +++ b/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Snippets.pm @@ -16,9 +16,10 @@ sub index { my $leaderboard_rs = $c->schema->resultset('Leaderboard'); my $monthly_board = $leaderboard_rs->get_latest( 'monthly_total' ); - my $monthly_values = $monthly_board->values; - $data->{ user_position } = $monthly_values ? $monthly_values->find({ entity_id => $entity->id })->position : 0; - + 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, From 5b745f666bb402fc1de5d170b06be3f4e1d94a66 Mon Sep 17 00:00:00 2001 From: Finn Date: Wed, 13 Dec 2017 13:33:29 +0000 Subject: [PATCH 05/17] made customer graphs work with passing test --- .../Controller/Api/V1/Customer/Graphs.pm | 166 +++++++++--------- t/api/v1/customer/graphs.t | 79 ++++++++- 2 files changed, 163 insertions(+), 82 deletions(-) diff --git a/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Graphs.pm b/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Graphs.pm index b669dc0..cfd97dd 100644 --- a/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Graphs.pm +++ b/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Graphs.pm @@ -16,14 +16,10 @@ sub index { my $validation = $c->validation; $validation->input( $c->stash->{api_json} ); $validation->required('graph')->in( qw/ - total_today - avg_spend_today total_last_week avg_spend_last_week total_last_month avg_spend_last_month - total_user - avg_spend_user / ); return $c->api_validation_error if $validation->has_error; @@ -45,6 +41,9 @@ sub index { return $c->$graph_sub; } + +# Replace with code for doughnut +=pod sub graph_customers_range { my $c = shift; @@ -78,22 +77,12 @@ sub graph_customers_range { } ); } +=cut -sub graph_customers_last_7_days { - my $c = shift; +sub graph_total_last_week { return shift->_purchases_total_duration( 7 ) } +sub graph_total_last_month { return shift->_purchases_total_duration( 30 ) } - 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 _customers_last_duration { +sub _purchases_total_duration { my ( $c, $duration ) = @_; my $entity = $c->stash->{api_user}->entity; @@ -102,69 +91,6 @@ sub _customers_last_duration { my ( $start, $end ) = $c->_get_start_end_duration( $duration ); - 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->{ data } }, $transactions; - $start->add( days => 1 ); - } - - return $c->render( - json => { - success => Mojo::JSON->true, - graph => $data, - } - ); -} - -sub graph_sales_last_7_days { return shift->_sales_last_duration( 7 ) } -sub graph_sales_last_30_days { return shift->_sales_last_duration( 30 ) } - -sub _sales_last_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 ); - - 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->{ data } }, $transactions / 100000; - $start->add( days => 1 ); - } - - return $c->render( - json => { - success => Mojo::JSON->true, - graph => $data, - } - ); -} - -sub graph_purchases_last_7_days { return shift->_purchases_last_duration( 7 ) } -sub graph_purchases_last_30_days { return shift->_purchases_last_duration( 30 ) } - -sub _purchases_last_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 ); - while ( $start < $end ) { my $next_end = $start->clone->add( days => 1 ); my $transactions = $entity->purchases @@ -184,6 +110,69 @@ sub _purchases_last_duration { ); } +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 ); + + 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), + ], + }, + }, + { + 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' }, + } + ); + +my $data = { + labels => [], + data => [], +}; + + for ( $transaction_rs->all ) { + my $quantised = $c->db_datetime_parser->parse_datetime($_->get_column('quantised')); + push @{ $data->{ labels } }, $quantised->day_name; + 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; @@ -191,4 +180,19 @@ sub _get_start_end_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/t/api/v1/customer/graphs.t b/t/api/v1/customer/graphs.t index 6f9a3d9..0e20778 100644 --- a/t/api/v1/customer/graphs.t +++ b/t/api/v1/customer/graphs.t @@ -1,5 +1,82 @@ use Mojo::Base -strict; -use Test::More skip_all => 'Test needs making!'; +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 => 'avg_spend_last_week', + }) + ->status_is(200)->or($framework->dump_error) + ->json_is('/graph', { + labels => [ map { $start->clone->subtract( days => $_ )->day_name } reverse ( 0 .. 6 ) ], + data => [ 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; From 3b191e5c76bce998c448d9bae5169df7804ba33b Mon Sep 17 00:00:00 2001 From: Finn Date: Wed, 13 Dec 2017 14:29:17 +0000 Subject: [PATCH 06/17] working graphs on frontend --- .../LocalLoop/Controller/Api/V1/Customer/Graphs.pm | 14 +++++--------- t/api/v1/customer/graphs.t | 4 +++- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Graphs.pm b/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Graphs.pm index cfd97dd..e8814cd 100644 --- a/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Graphs.pm +++ b/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Graphs.pm @@ -83,8 +83,9 @@ 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, $duration ) = @_; + my ( $c, $day_duration ) = @_; + my $duration = DateTime::Duration->new( days => $day_duration ); my $entity = $c->stash->{api_user}->entity; my $data = { labels => [], data => [] }; @@ -96,8 +97,8 @@ sub _purchases_total_duration { my $transactions = $entity->purchases ->search_between( $start, $next_end ) ->get_column('value') - ->sum || 0 + 0; - push @{ $data->{ labels } }, $start->day_name; + ->sum || 0 * 1; + push @{ $data->{ labels } }, $c->format_iso_datetime( $start ); push @{ $data->{ data } }, $transactions / 100000; $start->add( days => 1 ); } @@ -154,14 +155,9 @@ sub _purchases_avg_spend_duration { } ); -my $data = { - labels => [], - data => [], -}; - for ( $transaction_rs->all ) { my $quantised = $c->db_datetime_parser->parse_datetime($_->get_column('quantised')); - push @{ $data->{ labels } }, $quantised->day_name; + push @{ $data->{ labels } }, $c->format_iso_datetime( $quantised ); push @{ $data->{ data } }, ($_->get_column('average_value') || 0) / 100000; } diff --git a/t/api/v1/customer/graphs.t b/t/api/v1/customer/graphs.t index 0e20778..da9ec64 100644 --- a/t/api/v1/customer/graphs.t +++ b/t/api/v1/customer/graphs.t @@ -44,7 +44,9 @@ $t->post_ok('/api/v1/customer/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 ) ], data => [ 10, 10, 10, 10, 10, 10, 10 ], }); From a03d3f59773072b547f77b7db0a4902dde3354e7 Mon Sep 17 00:00:00 2001 From: Finn Date: Wed, 13 Dec 2017 15:29:35 +0000 Subject: [PATCH 07/17] Fixed admin interface for org users --- templates/admin/users/read.html.ep | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 { From 006cc7eb1e162626a73d2897857b7e26fbda7ada Mon Sep 17 00:00:00 2001 From: Finn Date: Thu, 14 Dec 2017 17:20:06 +0000 Subject: [PATCH 08/17] fixing graph code and added placeholder pie code --- lib/Pear/LocalLoop.pm | 1 + .../Controller/Api/V1/Customer/Graphs.pm | 48 ++++------------- .../Controller/Api/V1/Organisation/Graphs.pm | 39 ++++++++------ t/api/v1/customer/graphs.t | 54 ++++++++++++++++++- t/api/v1/organisation/graphs.t | 32 +++++++++-- 5 files changed, 114 insertions(+), 60 deletions(-) diff --git a/lib/Pear/LocalLoop.pm b/lib/Pear/LocalLoop.pm index f7f4ba4..bd5680b 100644 --- a/lib/Pear/LocalLoop.pm +++ b/lib/Pear/LocalLoop.pm @@ -175,6 +175,7 @@ sub startup { $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'); diff --git a/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Graphs.pm b/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Graphs.pm index e8814cd..06f45d0 100644 --- a/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Graphs.pm +++ b/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Graphs.pm @@ -41,44 +41,6 @@ sub index { return $c->$graph_sub; } - -# Replace with code for doughnut -=pod -sub graph_customers_range { - my $c = shift; - - my $validation = $c->validation; - $validation->input( $c->stash->{api_json} ); - $validation->required('start')->is_iso_date; - $validation->required('end')->is_iso_date; - - return $c->api_validation_error if $validation->has_error; - - my $entity = $c->stash->{api_user}->entity; - - my $data = { labels => [], data => [] }; - my $start = $c->parse_iso_date( $validation->param('start') ); - my $end = $c->parse_iso_date( $validation->param('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 } }, $c->format_iso_date( $start ); - push @{ $data->{ data } }, $transactions; - $start->add( days => 1 ); - } - - return $c->render( - json => { - success => Mojo::JSON->true, - graph => $data, - } - ); -} -=cut - sub graph_total_last_week { return shift->_purchases_total_duration( 7 ) } sub graph_total_last_month { return shift->_purchases_total_duration( 30 ) } @@ -92,6 +54,11 @@ sub _purchases_total_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 @@ -124,6 +91,11 @@ sub _purchases_avg_spend_duration { 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( diff --git a/lib/Pear/LocalLoop/Controller/Api/V1/Organisation/Graphs.pm b/lib/Pear/LocalLoop/Controller/Api/V1/Organisation/Graphs.pm index d1eddaf..5ce5f44 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/t/api/v1/customer/graphs.t b/t/api/v1/customer/graphs.t index da9ec64..580578c 100644 --- a/t/api/v1/customer/graphs.t +++ b/t/api/v1/customer/graphs.t @@ -40,16 +40,68 @@ my $session_key = $framework->login({ $t->post_ok('/api/v1/customer/graphs' => json => { session_key => $session_key, - graph => 'avg_spend_last_week', + 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 .. 29 ) ], + 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({ diff --git a/t/api/v1/organisation/graphs.t b/t/api/v1/organisation/graphs.t index 57b6cbb..5fd0da7 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 => 6 )->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 => 6 )->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 ], }); From 9594f9f7469154dbc0667acfef7f561e0df5b6ea Mon Sep 17 00:00:00 2001 From: Finn Date: Thu, 14 Dec 2017 17:22:09 +0000 Subject: [PATCH 09/17] widget graph test fixed --- t/api/v1/customer/graphs.t | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/t/api/v1/customer/graphs.t b/t/api/v1/customer/graphs.t index 580578c..a2e6010 100644 --- a/t/api/v1/customer/graphs.t +++ b/t/api/v1/customer/graphs.t @@ -62,7 +62,7 @@ $t->post_ok('/api/v1/customer/graphs' => json => { ->json_is('/graph', { labels => [ map { $t->app->format_iso_datetime( $start->clone->subtract( days => $_ )->subtract( hours => 12 ) - ) } reverse ( 0 .. 29 ) ], + ) } 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 )), From 12bd6b3ec961052e478df1441f3e076bf20684ab Mon Sep 17 00:00:00 2001 From: Finn Date: Thu, 14 Dec 2017 17:25:00 +0000 Subject: [PATCH 10/17] org graphs and relevant test fixed --- lib/Pear/LocalLoop/Controller/Api/V1/Organisation/Graphs.pm | 4 ++-- t/api/v1/organisation/graphs.t | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/Pear/LocalLoop/Controller/Api/V1/Organisation/Graphs.pm b/lib/Pear/LocalLoop/Controller/Api/V1/Organisation/Graphs.pm index 5ce5f44..a6f56eb 100644 --- a/lib/Pear/LocalLoop/Controller/Api/V1/Organisation/Graphs.pm +++ b/lib/Pear/LocalLoop/Controller/Api/V1/Organisation/Graphs.pm @@ -127,10 +127,10 @@ sub _sales_last_duration { my ( $start, $end ) = $c->_get_start_end_duration( $duration ); - $data->{bounds} = ( + $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 ); diff --git a/t/api/v1/organisation/graphs.t b/t/api/v1/organisation/graphs.t index 5fd0da7..3e438b4 100644 --- a/t/api/v1/organisation/graphs.t +++ b/t/api/v1/organisation/graphs.t @@ -64,7 +64,7 @@ $t->post_ok('/api/v1/organisation/graphs' => json => { $start->clone->subtract( days => $_ )->subtract( hours => 12 ) ) } reverse ( 0 .. 29 ) ], bounds => { - min => $t->app->format_iso_datetime($start->clone->subtract( days => 6 )->subtract( hours => 12 ) ), + 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 ], @@ -96,7 +96,7 @@ $t->post_ok('/api/v1/organisation/graphs' => json => { $start->clone->subtract( days => $_ )->subtract( hours => 12 ) ) } reverse ( 0 .. 29 ) ], bounds => { - min => $t->app->format_iso_datetime($start->clone->subtract( days => 6 )->subtract( hours => 12 ) ), + 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 ], From cb39b419d6fcc7b9df763ed1f9af3900e4499d27 Mon Sep 17 00:00:00 2001 From: Finn Date: Thu, 14 Dec 2017 20:30:44 +0000 Subject: [PATCH 11/17] Added new stats and fixed test --- lib/Pear/LocalLoop/Controller/Api/Stats.pm | 62 +++++----- .../Controller/Api/V1/Customer/Pies.pm | 28 +++++ .../Result/ViewQuantisedTransactionPg.pm | 3 +- .../Result/ViewQuantisedTransactionSQLite.pm | 3 +- t/api/stats.t | 117 ++++++------------ 5 files changed, 97 insertions(+), 116 deletions(-) create mode 100644 lib/Pear/LocalLoop/Controller/Api/V1/Customer/Pies.pm diff --git a/lib/Pear/LocalLoop/Controller/Api/Stats.pm b/lib/Pear/LocalLoop/Controller/Api/Stats.pm index ad984e2..d626c46 100644 --- a/lib/Pear/LocalLoop/Controller/Api/Stats.pm +++ b/lib/Pear/LocalLoop/Controller/Api/Stats.pm @@ -17,44 +17,42 @@ sub post_index { my $user = $c->stash->{api_user}->entity; - my $today_rs = $user->purchases->today_rs; - my $today_sum = $today_rs->get_column('value')->sum || 0; - my $today_count = $today_rs->count; + my $duration = DateTime::Duration->new( weeks => 7 ); + my $end = DateTime->today; + my $start = $end->clone->subtract_duration( $duration ); - my $week_rs = $user->purchases->week_rs; - my $week_sum = $week_rs->get_column('value')->sum || 0; - my $week_count = $week_rs->count; + my $data = { purchases => [] }; - my $month_rs = $user->purchases->month_rs; - my $month_sum = $month_rs->get_column('value')->sum || 0; - my $month_count = $month_rs->count; + 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), + ], + }, + }, + { + columns => [ + { + quantised => 'quantised_weeks', + count => \"COUNT(*)", + } + ], + group_by => 'quantised_weeks', + order_by => { '-asc' => 'quantised_weeks' }, + } + ); - my $user_rs = $user->purchases; - my $user_sum = $user_rs->get_column('value')->sum || 0; - my $user_count = $user_rs->count; - - my $global_rs = $c->schema->resultset('Transaction'); - my $global_sum = $global_rs->get_column('value')->sum || 0; - my $global_count = $global_rs->count; - - my $leaderboard_rs = $c->schema->resultset('Leaderboard'); - my $monthly_board = $leaderboard_rs->get_latest( 'monthly_total' ); - my $monthly_values = $monthly_board->values; - my $current_user_position = $monthly_values ? $monthly_values->find({ entity_id => $user->id }) : undef; + for ( $transaction_rs->all ) { + push @{ $data->{ purchases } }, ($_->get_column('count') || 0); + } return $c->render( json => { success => Mojo::JSON->true, - today_sum => $today_sum / 100000, - today_count => $today_count, - week_sum => $week_sum / 100000, - week_count => $week_count, - month_sum => $month_sum / 100000, - month_count => $month_count, - user_sum => $user_sum / 100000, - user_count => $user_count, - global_sum => $global_sum / 100000, - global_count => $global_count, - user_position => defined $current_user_position ? $current_user_position->position : 0, + data => $data, }); } 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..ae0340b --- /dev/null +++ b/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Pies.pm @@ -0,0 +1,28 @@ +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 $data = { data => [] }; + + my $data = { + 'Local shop local purchaser' => 20, + 'Local shop non-local purchaser' => 20, + 'Non-local shop local purchaser' => 20, + 'Non-local shop non-local purchaser' => 20, + }; + + #TODO insert code fetching numbers here + + return $c->render( + json => { + success => Mojo::JSON->true, + pie => $data, + } + ); + +} + +1; diff --git a/lib/Pear/LocalLoop/Schema/Result/ViewQuantisedTransactionPg.pm b/lib/Pear/LocalLoop/Schema/Result/ViewQuantisedTransactionPg.pm index fabbd38..68513ac 100644 --- a/lib/Pear/LocalLoop/Schema/Result/ViewQuantisedTransactionPg.pm +++ b/lib/Pear/LocalLoop/Schema/Result/ViewQuantisedTransactionPg.pm @@ -14,7 +14,8 @@ SELECT "value", "distance", "purchase_time", 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..649d6cd 100644 --- a/lib/Pear/LocalLoop/Schema/Result/ViewQuantisedTransactionSQLite.pm +++ b/lib/Pear/LocalLoop/Schema/Result/ViewQuantisedTransactionSQLite.pm @@ -14,7 +14,8 @@ SELECT "value", "distance", "purchase_time", 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..11a0a93 100644 --- a/t/api/stats.t +++ b/t/api/stats.t @@ -14,98 +14,51 @@ $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; +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' => 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('/data', { + purchases => [ 12, 19, 21, 20, 21, 20, 5 ], + }); -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; From 7ffdfc5bd34b1d6a5f6a1fcea1df03279cc21b72 Mon Sep 17 00:00:00 2001 From: Finn Date: Fri, 15 Dec 2017 14:59:38 +0000 Subject: [PATCH 12/17] pie code made functional and relevant distance code updated --- lib/Pear/LocalLoop/Controller/Admin/Users.pm | 7 +++ lib/Pear/LocalLoop/Controller/Api/Stats.pm | 8 +-- lib/Pear/LocalLoop/Controller/Api/Upload.pm | 6 +++ .../Controller/Api/V1/Customer/Pies.pm | 50 ++++++++++++++++--- 4 files changed, 60 insertions(+), 11 deletions(-) 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 d626c46..af36a67 100644 --- a/lib/Pear/LocalLoop/Controller/Api/Stats.pm +++ b/lib/Pear/LocalLoop/Controller/Api/Stats.pm @@ -21,7 +21,8 @@ sub post_index { my $end = DateTime->today; my $start = $end->clone->subtract_duration( $duration ); - my $data = { purchases => [] }; + my $weeks = { purchases => [] }; + my $sectors = { sectors => [], purchases => [] }; my $dtf = $c->schema->storage->datetime_parser; my $driver = $c->schema->storage->dbh->{Driver}->{Name}; @@ -47,12 +48,13 @@ sub post_index { ); for ( $transaction_rs->all ) { - push @{ $data->{ purchases } }, ($_->get_column('count') || 0); + push @{ $weeks->{ purchases } }, ($_->get_column('count') || 0); } return $c->render( json => { success => Mojo::JSON->true, - data => $data, + weeks => $weeks, + sectors => $sectors, }); } 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/Pies.pm b/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Pies.pm index ae0340b..06c57b1 100644 --- a/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Pies.pm +++ b/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Pies.pm @@ -5,17 +5,51 @@ sub index { my $c = shift; my $entity = $c->stash->{api_user}->entity; - my $data = { data => [] }; + + 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' => 20, - 'Local shop non-local purchaser' => 20, - 'Non-local shop local purchaser' => 20, - 'Non-local shop non-local purchaser' => 20, + '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, }; - - #TODO insert code fetching numbers here - + return $c->render( json => { success => Mojo::JSON->true, From 8c811d52bf9768c440f1df77d50b8ee82006d258 Mon Sep 17 00:00:00 2001 From: Finn Date: Fri, 15 Dec 2017 15:05:04 +0000 Subject: [PATCH 13/17] fixed stats test --- t/api/stats.t | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/t/api/stats.t b/t/api/stats.t index 11a0a93..9ef6397 100644 --- a/t/api/stats.t +++ b/t/api/stats.t @@ -42,8 +42,8 @@ $t->post_ok('/api/stats' => json => { session_key => $session_key, }) ->status_is(200)->or($framework->dump_error) - ->json_is('/data', { - purchases => [ 12, 19, 21, 20, 21, 20, 5 ], + ->json_is('/weeks', { + purchases => [ 8, 21, 19, 22, 20, 20, 8 ], }); sub create_random_transaction { From 2012e672733107af33c7a5923e011fc751cdc7f4 Mon Sep 17 00:00:00 2001 From: Finn Date: Fri, 15 Dec 2017 15:30:47 +0000 Subject: [PATCH 14/17] sectors code added --- lib/Pear/LocalLoop/Controller/Api/Stats.pm | 24 ++++++++++++++++--- .../Controller/Api/V1/Customer/Graphs.pm | 1 + .../Result/ViewQuantisedTransactionPg.pm | 2 ++ .../Result/ViewQuantisedTransactionSQLite.pm | 2 ++ 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/lib/Pear/LocalLoop/Controller/Api/Stats.pm b/lib/Pear/LocalLoop/Controller/Api/Stats.pm index af36a67..4e9b4ff 100644 --- a/lib/Pear/LocalLoop/Controller/Api/Stats.pm +++ b/lib/Pear/LocalLoop/Controller/Api/Stats.pm @@ -15,7 +15,7 @@ has error_messages => sub { sub post_index { my $c = shift; - my $user = $c->stash->{api_user}->entity; + my $entity = $c->stash->{api_user}->entity; my $duration = DateTime::Duration->new( weeks => 7 ); my $end = DateTime->today; @@ -26,7 +26,7 @@ sub post_index { my $dtf = $c->schema->storage->datetime_parser; my $driver = $c->schema->storage->dbh->{Driver}->{Name}; - my $transaction_rs = $c->schema->resultset('ViewQuantisedTransaction' . $driver)->search( + my $week_transaction_rs = $c->schema->resultset('ViewQuantisedTransaction' . $driver)->search( { purchase_time => { -between => [ @@ -34,6 +34,7 @@ sub post_index { $dtf->format_datetime($end), ], }, + buyer_id => $entity->id, }, { columns => [ @@ -47,10 +48,27 @@ sub post_index { } ); - for ( $transaction_rs->all ) { + for ( $week_transaction_rs->all ) { push @{ $weeks->{ purchases } }, ($_->get_column('count') || 0); } + my $sector_purchase_rs = $entity->purchases->search({}, + { + join => { 'seller' => 'organisation' }, + columns => { + sector => "organisation.sector", + count => \"COUNT(*)", + }, + group_by => "organisation.sector", + order_by => { '-desc' => "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, diff --git a/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Graphs.pm b/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Graphs.pm index 06f45d0..f48e52b 100644 --- a/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Graphs.pm +++ b/lib/Pear/LocalLoop/Controller/Api/V1/Customer/Graphs.pm @@ -106,6 +106,7 @@ sub _purchases_avg_spend_duration { $dtf->format_datetime($end), ], }, + buyer_id => $entity->id, }, { columns => [ diff --git a/lib/Pear/LocalLoop/Schema/Result/ViewQuantisedTransactionPg.pm b/lib/Pear/LocalLoop/Schema/Result/ViewQuantisedTransactionPg.pm index 68513ac..91ceede 100644 --- a/lib/Pear/LocalLoop/Schema/Result/ViewQuantisedTransactionPg.pm +++ b/lib/Pear/LocalLoop/Schema/Result/ViewQuantisedTransactionPg.pm @@ -13,6 +13,8 @@ __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('week', "purchase_time") AS "quantised_weeks" diff --git a/lib/Pear/LocalLoop/Schema/Result/ViewQuantisedTransactionSQLite.pm b/lib/Pear/LocalLoop/Schema/Result/ViewQuantisedTransactionSQLite.pm index 649d6cd..abf95dd 100644 --- a/lib/Pear/LocalLoop/Schema/Result/ViewQuantisedTransactionSQLite.pm +++ b/lib/Pear/LocalLoop/Schema/Result/ViewQuantisedTransactionSQLite.pm @@ -13,6 +13,8 @@ __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", 'weekday 1')) AS "quantised_weeks" From 9edf15b95b479817199d9ee930ec8527f8024e5f Mon Sep 17 00:00:00 2001 From: Finn Date: Fri, 15 Dec 2017 17:09:15 +0000 Subject: [PATCH 15/17] pie test added --- t/api/v1/customer/pies.t | 67 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 t/api/v1/customer/pies.t 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; From 7d044addeb344749c7a0d185aaa8f4cba24f570a Mon Sep 17 00:00:00 2001 From: Finn Date: Fri, 15 Dec 2017 17:52:02 +0000 Subject: [PATCH 16/17] tests fixed --- t/api/stats.t | 6 ++++++ t/etc/fixtures/config/users.pl | 2 +- t/etc/fixtures/data/users/organisations/1.fix | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/t/api/stats.t b/t/api/stats.t index 9ef6397..cad04ff 100644 --- a/t/api/stats.t +++ b/t/api/stats.t @@ -38,12 +38,18 @@ my $session_key = $framework->login({ password => 'abc123', }); +#TODO be able to define start and end below in request + $t->post_ok('/api/stats' => json => { session_key => $session_key, }) ->status_is(200)->or($framework->dump_error) ->json_is('/weeks', { purchases => [ 8, 21, 19, 22, 20, 20, 8 ], + }) + ->json_is('/sectors', { + sectors => ['A'], + purchases => [118], }); sub create_random_transaction { 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 From 767ed7323447e57048897206cde9344e65777b8d Mon Sep 17 00:00:00 2001 From: Finn Date: Fri, 15 Dec 2017 17:56:39 +0000 Subject: [PATCH 17/17] changelog updated --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54a0c26..1c15ebd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ a new tab * **Admin Feature** Ability to add ESTA to entity Added * Trail map code updated +* 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 Feature** Fixed org sector on user edit layout and text # v0.9.7