diff --git a/CHANGELOG.md b/CHANGELOG.md index def0371..c2c0eb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ # Next Release +* 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 + +* 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/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'; 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 822087d..fc8ed0b 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,11 @@ sub post_register { return $c->api_validation_error if $validation->has_error; + my $location = $c->get_location_from_postcode( + $validation->param('postcode'), + $usertype, + ); + if ($usertype eq 'customer'){ $c->schema->txn_do( sub { @@ -94,6 +101,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 +126,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/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/Controller/Api/User.pm b/lib/Pear/LocalLoop/Controller/Api/User.pm index d85e2d8..c500164 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( @@ -135,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 { @@ -142,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'), @@ -159,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..ee7b15a --- /dev/null +++ b/lib/Pear/LocalLoop/Plugin/Postcodes.pm @@ -0,0 +1,58 @@ +package Pear::LocalLoop::Plugin::Postcodes; +use Mojo::Base 'Mojolicious::Plugin'; + +use Geo::UK::Postcode::Regex; +use GIS::Distance; + +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; + }); + + $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/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/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/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/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; 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({ 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;