From f1e37560752cb727f9b58aa7542e917535839e2e Mon Sep 17 00:00:00 2001 From: Tom Bloor Date: Tue, 3 Oct 2017 15:27:05 +0100 Subject: [PATCH 1/4] updated Geo::UK::Postcode::Regex --- cpanfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpanfile b/cpanfile index c707f19..f3ab731 100644 --- a/cpanfile +++ b/cpanfile @@ -4,7 +4,7 @@ requires 'Data::UUID'; requires 'Devel::Dwarn'; requires 'Mojo::JSON'; requires 'Email::Valid'; -requires 'Geo::UK::Postcode::Regex'; +requires 'Geo::UK::Postcode::Regex' => '0.017'; requires 'Authen::Passphrase::BlowfishCrypt'; requires 'Time::Fake'; requires 'Scalar::Util'; From 202deb9178f560cb26be8473a583b6c768935f43 Mon Sep 17 00:00:00 2001 From: Tom Bloor Date: Tue, 3 Oct 2017 15:33:43 +0100 Subject: [PATCH 2/4] Added output of location on User endpoint, and setting lat/long on user registration --- CHANGELOG.md | 8 ++ lib/Pear/LocalLoop/Controller/Api/Register.pm | 32 ++++++ lib/Pear/LocalLoop/Controller/Api/User.pm | 26 +++-- lib/Test/Pear/LocalLoop.pm | 2 + t/api/register/location.t | 108 ++++++++++++++++++ t/api/user.t | 14 +++ 6 files changed, 182 insertions(+), 8 deletions(-) create mode 100644 t/api/register/location.t diff --git a/CHANGELOG.md b/CHANGELOG.md index def0371..687ee1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ # Next Release +* Location is now updated on registration. Customers location is truncated to 2 + decimal places based on their postcode. + +## Bug Fixes + +* Updated Geo::UK::Postcode::Regex dependency to latest version. Fixes postcode + validation errors + # v0.9.4 * **Admin Feature:** Report of transaction data graphs diff --git a/lib/Pear/LocalLoop/Controller/Api/Register.pm b/lib/Pear/LocalLoop/Controller/Api/Register.pm index 822087d..9de39d9 100644 --- a/lib/Pear/LocalLoop/Controller/Api/Register.pm +++ b/lib/Pear/LocalLoop/Controller/Api/Register.pm @@ -2,6 +2,8 @@ package Pear::LocalLoop::Controller::Api::Register; use Mojo::Base 'Mojolicious::Controller'; use DateTime; +use Geo::UK::Postcode::Regex; + has error_messages => sub { return { token => { @@ -80,6 +82,34 @@ sub post_register { return $c->api_validation_error if $validation->has_error; + my $postcode_obj = Geo::UK::Postcode::Regex->parse( + $validation->param('postcode') + ); + + my $location; + + unless ( defined $postcode_obj && $postcode_obj->{non_geographical} ) { + my $pc_result = $c->schema->resultset('GbPostcode')->find({ + incode => $postcode_obj->{incode}, + outcode => $postcode_obj->{outcode}, + }); + if ( defined $pc_result ) { + # Force truncation here as SQLite is stupid + $location = { + latitude => ( + $usertype eq 'customer' + ? int($pc_result->latitude * 100 ) / 100 + : $pc_result->latitude + ), + longitude => ( + $usertype eq 'customer' + ? int($pc_result->longitude * 100 ) / 100 + : $pc_result->longitude + ), + }; + } + } + if ($usertype eq 'customer'){ $c->schema->txn_do( sub { @@ -94,6 +124,7 @@ sub post_register { display_name => $validation->param('display_name'), year_of_birth => $validation->param('year_of_birth'), postcode => $validation->param('postcode'), + ( defined $location ? ( %$location ) : () ), }, user => { email => $validation->param('email'), @@ -118,6 +149,7 @@ sub post_register { town => $validation->param('town'), sector => $validation->param('sector'), postcode => $validation->param('postcode'), + ( defined $location ? ( %$location ) : () ), }, user => { email => $validation->param('email'), diff --git a/lib/Pear/LocalLoop/Controller/Api/User.pm b/lib/Pear/LocalLoop/Controller/Api/User.pm index d85e2d8..85d2be6 100644 --- a/lib/Pear/LocalLoop/Controller/Api/User.pm +++ b/lib/Pear/LocalLoop/Controller/Api/User.pm @@ -49,22 +49,28 @@ sub post_account { my $email = $user_result->email; if ( $user_result->type eq 'customer' ) { - my $full_name = $user_result->entity->customer->full_name; - my $display_name = $user_result->entity->customer->display_name; - my $postcode = $user_result->entity->customer->postcode; + my $customer = $user_result->entity->customer; + my $full_name = $customer->full_name; + my $display_name = $customer->display_name; + my $postcode = $customer->postcode; return $c->render( json => { success => Mojo::JSON->true, full_name => $full_name, display_name => $display_name, email => $email, postcode => $postcode, + location => { + latitude => (defined $customer->latitude ? $customer->latitude * 1 : undef), + longitude => (defined $customer->longitude ? $customer->longitude * 1 : undef), + }, }); } elsif ( $user_result->type eq 'organisation' ) { - my $name = $user_result->entity->organisation->name; - my $postcode = $user_result->entity->organisation->postcode; - my $street_name = $user_result->entity->organisation->street_name; - my $town = $user_result->entity->organisation->town; - my $sector = $user_result->entity->organisation->sector; + my $organisation = $user_result->entity->organisation; + my $name = $organisation->name; + my $postcode = $organisation->postcode; + my $street_name = $organisation->street_name; + my $town = $organisation->town; + my $sector = $organisation->sector; return $c->render( json => { success => Mojo::JSON->true, town => $town, @@ -73,6 +79,10 @@ sub post_account { street_name => $street_name, email => $email, postcode => $postcode, + location => { + latitude => (defined $organisation->latitude ? $organisation->latitude * 1 : undef), + longitude => (defined $organisation->longitude ? $organisation->longitude * 1 : undef), + }, }); } else { return $c->render( diff --git a/lib/Test/Pear/LocalLoop.pm b/lib/Test/Pear/LocalLoop.pm index 961fd55..490aa1a 100644 --- a/lib/Test/Pear/LocalLoop.pm +++ b/lib/Test/Pear/LocalLoop.pm @@ -219,6 +219,8 @@ sub install_fixtures { { entities => 'entities_id_seq', organisations => 'organisations_id_seq', + users => 'users_id_seq', + customers => 'customers_id_seq', } ); } diff --git a/t/api/register/location.t b/t/api/register/location.t new file mode 100644 index 0000000..d01ff1f --- /dev/null +++ b/t/api/register/location.t @@ -0,0 +1,108 @@ +use Mojo::Base -strict; + +use FindBin qw/ $Bin /; + +use Test::More; +use Mojo::JSON; +use Test::Pear::LocalLoop; + +my $framework = Test::Pear::LocalLoop->new( + etc_dir => "$Bin/../../etc", +); +$framework->install_fixtures('full'); + +my $t = $framework->framework; +my $schema = $t->app->schema; + +$schema->resultset('AccountToken')->populate([ + {name => 'test1'}, + {name => 'test2'}, + {name => 'test3'}, +]); + +$t->post_ok('/api/register', + json => { + token => 'test1', + usertype => 'customer', + full_name => 'New Test User', + display_name => 'Testing User New', + email => 'newtest@example.com', + postcode => 'LA2 0AD', + year_of_birth => 2001, + password => 'abc123', + }) + ->status_is(200)->or($framework->dump_error) + ->json_is('/success', Mojo::JSON->true); + +my $session_key = $framework->login({ + email => 'newtest@example.com', + password => 'abc123', +}); + +$t->post_ok('/api/user', json => { session_key => $session_key }) + ->status_is(200) + ->json_is('/success', Mojo::JSON->true) + ->json_is('/location', { + latitude => 54.02, + longitude => -2.80, + } + ); + +$t->post_ok('/api/register', + json => { + token => 'test2', + usertype => 'organisation', + email => 'neworg@example.com', + password => 'abc123', + postcode => 'LA2 0AD', + name => 'New Org', + street_name => '18 Test Road', + town => 'Lancaster', + sector => 'A', + }) + ->status_is(200) + ->json_is('/success', Mojo::JSON->true); + +$session_key = $framework->login({ + email => 'neworg@example.com', + password => 'abc123', +}); + +$t->post_ok('/api/user', json => { session_key => $session_key }) + ->status_is(200) + ->json_is('/success', Mojo::JSON->true) + ->json_is('/location', { + latitude => 54.02432, + longitude => -2.80635, + } + ); + +$t->post_ok('/api/register', + json => { + token => 'test3', + usertype => 'customer', + full_name => 'New Test User', + display_name => 'Testing User New', + email => 'newtest2@example.com', + postcode => 'BX1 1AA', + year_of_birth => 2001, + password => 'abc123', + }) + ->status_is(200)->or($framework->dump_error) + ->json_is('/success', Mojo::JSON->true); + +$session_key = $framework->login({ + email => 'newtest2@example.com', + password => 'abc123', +}); + +$t->post_ok('/api/user', json => { session_key => $session_key }) + ->status_is(200) + ->json_is('/success', Mojo::JSON->true) + ->json_is('/location', { + latitude => undef, + longitude => undef, + } + ); + +done_testing; diff --git a/t/api/user.t b/t/api/user.t index 643165a..acedf1e 100644 --- a/t/api/user.t +++ b/t/api/user.t @@ -40,6 +40,10 @@ $t->post_ok('/api/user', json => { session_key => $session_key }) display_name => 'Testing User', email => $email, postcode => 'LA1 1AA', + location => { + latitude => undef, + longitude => undef, + }, }); #with wrong password @@ -80,6 +84,11 @@ $t->post_ok('/api/user', json => { session_key => $session_key }) display_name => 'Testing User 2', email => 'test50@example.com', postcode => 'LA1 1AB', + location => { + latitude => undef, + longitude => undef, + }, + }); $t->post_ok('/api/user/account', json => { @@ -105,6 +114,11 @@ $t->post_ok('/api/user', json => { session_key => $session_key }) display_name => 'Testing User 3', email => 'test60@example.com', postcode => 'LA1 1AD', + location => { + latitude => undef, + longitude => undef, + }, + }); $session_key = $framework->login({ From 37e8f0b46a193ba992034b11216d3df0fd91407f Mon Sep 17 00:00:00 2001 From: Tom Bloor Date: Tue, 3 Oct 2017 15:47:05 +0100 Subject: [PATCH 3/4] Updated to allow for user updates to change location by postcode --- CHANGELOG.md | 1 + lib/Pear/LocalLoop.pm | 1 + lib/Pear/LocalLoop/Controller/Api/Register.pm | 29 ++-------- lib/Pear/LocalLoop/Controller/Api/User.pm | 7 +++ lib/Pear/LocalLoop/Plugin/Postcodes.pm | 40 ++++++++++++++ t/api/user/postcode.t | 53 +++++++++++++++++++ 6 files changed, 105 insertions(+), 26 deletions(-) create mode 100644 lib/Pear/LocalLoop/Plugin/Postcodes.pm create mode 100644 t/api/user/postcode.t diff --git a/CHANGELOG.md b/CHANGELOG.md index 687ee1e..bdbc69c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * Location is now updated on registration. Customers location is truncated to 2 decimal places based on their postcode. +* Location is also updated on changing a users postcode ## Bug Fixes diff --git a/lib/Pear/LocalLoop.pm b/lib/Pear/LocalLoop.pm index b6f6fcf..9890811 100644 --- a/lib/Pear/LocalLoop.pm +++ b/lib/Pear/LocalLoop.pm @@ -39,6 +39,7 @@ sub startup { $self->plugin('Pear::LocalLoop::Plugin::BootstrapPagination', { bootstrap4 => 1 } ); $self->plugin('Pear::LocalLoop::Plugin::Validators'); $self->plugin('Pear::LocalLoop::Plugin::Datetime'); + $self->plugin('Pear::LocalLoop::Plugin::Postcodes'); $self->plugin('Pear::LocalLoop::Plugin::TemplateHelpers'); $self->plugin('Authentication' => { diff --git a/lib/Pear/LocalLoop/Controller/Api/Register.pm b/lib/Pear/LocalLoop/Controller/Api/Register.pm index 9de39d9..fc8ed0b 100644 --- a/lib/Pear/LocalLoop/Controller/Api/Register.pm +++ b/lib/Pear/LocalLoop/Controller/Api/Register.pm @@ -82,34 +82,11 @@ sub post_register { return $c->api_validation_error if $validation->has_error; - my $postcode_obj = Geo::UK::Postcode::Regex->parse( - $validation->param('postcode') + my $location = $c->get_location_from_postcode( + $validation->param('postcode'), + $usertype, ); - my $location; - - unless ( defined $postcode_obj && $postcode_obj->{non_geographical} ) { - my $pc_result = $c->schema->resultset('GbPostcode')->find({ - incode => $postcode_obj->{incode}, - outcode => $postcode_obj->{outcode}, - }); - if ( defined $pc_result ) { - # Force truncation here as SQLite is stupid - $location = { - latitude => ( - $usertype eq 'customer' - ? int($pc_result->latitude * 100 ) / 100 - : $pc_result->latitude - ), - longitude => ( - $usertype eq 'customer' - ? int($pc_result->longitude * 100 ) / 100 - : $pc_result->longitude - ), - }; - } - } - if ($usertype eq 'customer'){ $c->schema->txn_do( sub { diff --git a/lib/Pear/LocalLoop/Controller/Api/User.pm b/lib/Pear/LocalLoop/Controller/Api/User.pm index 85d2be6..c500164 100644 --- a/lib/Pear/LocalLoop/Controller/Api/User.pm +++ b/lib/Pear/LocalLoop/Controller/Api/User.pm @@ -145,6 +145,11 @@ sub post_account_update { return $c->api_validation_error if $validation->has_error; + my $location = $c->get_location_from_postcode( + $validation->param('postcode'), + $user->type, + ); + if ( $user->type eq 'customer' ){ $c->schema->txn_do( sub { @@ -152,6 +157,7 @@ sub post_account_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'), @@ -169,6 +175,7 @@ sub post_account_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/Plugin/Postcodes.pm b/lib/Pear/LocalLoop/Plugin/Postcodes.pm new file mode 100644 index 0000000..2f6e0c9 --- /dev/null +++ b/lib/Pear/LocalLoop/Plugin/Postcodes.pm @@ -0,0 +1,40 @@ +package Pear::LocalLoop::Plugin::Postcodes; +use Mojo::Base 'Mojolicious::Plugin'; + +use DateTime::Format::Strptime; + +sub register { + my ( $plugin, $app, $conf ) = @_; + + $app->helper( get_location_from_postcode => sub { + my ( $c, $postcode, $usertype ) = @_; + my $postcode_obj = Geo::UK::Postcode::Regex->parse( $postcode ); + + my $location; + + unless ( defined $postcode_obj && $postcode_obj->{non_geographical} ) { + my $pc_result = $c->schema->resultset('GbPostcode')->find({ + incode => $postcode_obj->{incode}, + outcode => $postcode_obj->{outcode}, + }); + if ( defined $pc_result ) { + # Force truncation here as SQLite is stupid + $location = { + latitude => ( + $usertype eq 'customer' + ? int($pc_result->latitude * 100 ) / 100 + : $pc_result->latitude + ), + longitude => ( + $usertype eq 'customer' + ? int($pc_result->longitude * 100 ) / 100 + : $pc_result->longitude + ), + }; + } + } + return $location; + }); +} + +1; diff --git a/t/api/user/postcode.t b/t/api/user/postcode.t new file mode 100644 index 0000000..851922c --- /dev/null +++ b/t/api/user/postcode.t @@ -0,0 +1,53 @@ +use Mojo::Base -strict; + +use FindBin qw/ $Bin /; + +use Test::More; +use Mojo::JSON; +use Test::Pear::LocalLoop; + +my $framework = Test::Pear::LocalLoop->new( + etc_dir => "$Bin/../../etc", +); +$framework->install_fixtures('full'); + +my $t = $framework->framework; +my $schema = $t->app->schema; + +my $session_key = $framework->login({ + email => 'test1@example.com', + password => 'abc123', +}); + +$t->post_ok('/api/user', json => { session_key => $session_key }) + ->status_is(200) + ->json_is('/success', Mojo::JSON->true) + ->json_is('/postcode', 'LA1 1AA') + ->json_is('/location', { + latitude => 54.04, + longitude => -2.80, + } + ); + +$t->post_ok('/api/user/account', json => { + session_key => $session_key, + full_name => 'Test User1', + display_name => 'Testing User1', + email => 'test1@example.com', + postcode => 'LA2 0AR', + password => 'abc123', + }) + ->status_is(200)->or($framework->dump_error) + ->json_is('/success', Mojo::JSON->true); + +$t->post_ok('/api/user', json => { session_key => $session_key }) + ->status_is(200) + ->json_is('/success', Mojo::JSON->true) + ->json_is('/postcode', 'LA2 0AR') + ->json_is('/location', { + latitude => 53.99, + longitude => -2.84, + } + ); + +done_testing; From 62881a0edab987457d76e07ba9538d9e4dfdf2f4 Mon Sep 17 00:00:00 2001 From: Tom Bloor Date: Tue, 3 Oct 2017 18:08:30 +0100 Subject: [PATCH 4/4] Added transaction distance calculation --- CHANGELOG.md | 1 + lib/Pear/LocalLoop/Controller/Api/Upload.pm | 2 + lib/Pear/LocalLoop/Plugin/Postcodes.pm | 20 +++++++- t/admin/reports/transactions.t | 4 +- t/api/upload/distance.t | 51 +++++++++++++++++++++ 5 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 t/api/upload/distance.t diff --git a/CHANGELOG.md b/CHANGELOG.md index bdbc69c..c2c0eb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * Location is now updated on registration. Customers location is truncated to 2 decimal places based on their postcode. * Location is also updated on changing a users postcode +* Distance is now calculated when a transaction is submitted ## Bug Fixes diff --git a/lib/Pear/LocalLoop/Controller/Api/Upload.pm b/lib/Pear/LocalLoop/Controller/Api/Upload.pm index 879c128..15fa500 100644 --- a/lib/Pear/LocalLoop/Controller/Api/Upload.pm +++ b/lib/Pear/LocalLoop/Controller/Api/Upload.pm @@ -173,6 +173,7 @@ sub post_upload { my $purchase_time = $c->parse_iso_datetime($validation->param('purchase_time') || ''); $purchase_time ||= DateTime->now(); my $file = defined $upload ? $c->store_file_from_upload( $upload ) : undef; + my $distance = $c->get_distance_from_coords( $user->entity->type_object, $organisation ); my $new_transaction = $organisation->entity->create_related( 'sales', @@ -181,6 +182,7 @@ sub post_upload { value => $transaction_value * 100000, ( defined $file ? ( proof_image => $file ) : () ), purchase_time => $c->format_db_datetime($purchase_time), + distance => $distance, } ); diff --git a/lib/Pear/LocalLoop/Plugin/Postcodes.pm b/lib/Pear/LocalLoop/Plugin/Postcodes.pm index 2f6e0c9..ee7b15a 100644 --- a/lib/Pear/LocalLoop/Plugin/Postcodes.pm +++ b/lib/Pear/LocalLoop/Plugin/Postcodes.pm @@ -1,7 +1,8 @@ package Pear::LocalLoop::Plugin::Postcodes; use Mojo::Base 'Mojolicious::Plugin'; -use DateTime::Format::Strptime; +use Geo::UK::Postcode::Regex; +use GIS::Distance; sub register { my ( $plugin, $app, $conf ) = @_; @@ -35,6 +36,23 @@ sub register { } return $location; }); + + $app->helper( get_distance_from_coords => sub { + my ( $c, $buyer, $seller ) = @_; + + my $gis = GIS::Distance->new(); + + my $buyer_lat = $buyer->latitude; + my $buyer_long = $buyer->longitude; + my $seller_lat = $seller->latitude; + my $seller_long = $seller->longitude; + + if ( $buyer_lat && $buyer_long + && $seller_lat && $seller_long ) { + return int( $gis->distance( $buyer_lat, $buyer_long => $seller_lat, $seller_long )->meters ); + } + return; + }); } 1; diff --git a/t/admin/reports/transactions.t b/t/admin/reports/transactions.t index 8269c5c..f741a32 100644 --- a/t/admin/reports/transactions.t +++ b/t/admin/reports/transactions.t @@ -40,7 +40,7 @@ my $expected_hours = {}; sub increment_day { my ( $value, $day, $distance ) = @_; $value *= 100000; - $distance //= 0; + $distance //= 845; $expected_days->{$day} = { quantised => $day, sum_value => ($expected_days->{$day}->{sum_value} || 0) + $value, @@ -52,7 +52,7 @@ sub increment_day { sub increment_hour { my ( $value, $day, $distance ) = @_; $value *= 100000; - $distance //= 0; + $distance //= 845; $expected_hours->{$day} = { quantised => $day, sum_value => ($expected_hours->{$day}->{sum_value} || 0) + $value, diff --git a/t/api/upload/distance.t b/t/api/upload/distance.t new file mode 100644 index 0000000..6a4ab60 --- /dev/null +++ b/t/api/upload/distance.t @@ -0,0 +1,51 @@ +use Mojo::Base -strict; + +use FindBin qw/ $Bin /; + +use Test::More; +use Mojo::JSON; +use Test::Pear::LocalLoop; +use GIS::Distance; + +my $framework = Test::Pear::LocalLoop->new( + etc_dir => "$Bin/../../etc", +); +$framework->install_fixtures('full'); + +my $t = $framework->framework; +my $schema = $t->app->schema; + +my $session_key = $framework->login({ + email => 'test1@example.com', + password => 'abc123', +}); + +my $test_purchase_time = "2017-08-14T11:29:07.965+01:00"; + +$t->post_ok('/api/upload' => json => { + transaction_value => 10, + transaction_type => 1, + purchase_time => $test_purchase_time, + organisation_id => 1, + session_key => $session_key, + }) + ->status_is(200) + ->or($framework->dump_error) + ->json_is('/success', Mojo::JSON->true) + ->json_like('/message', qr/Upload Successful/); + +is $schema->resultset('Transaction')->count, 1, "1 transaction"; + +my $transaction = $schema->resultset('Transaction')->first; + +my $gis = GIS::Distance->new(); +my $expected_distance = int( $gis->distance( + # Buyer + 54.04, -2.8, + # Seller + 54.04725, -2.79611, +)->meters ); + +is $transaction->distance, $expected_distance, 'Transaction Distance Correct'; + +done_testing;