diff options
author | Matthew Somerville <matthew@mysociety.org> | 2020-06-23 17:29:04 +0100 |
---|---|---|
committer | M Somerville <matthew-github@dracos.co.uk> | 2020-11-11 10:29:20 +0000 |
commit | cabc4f91d55b952ab2521ec85ec745de4c354d8c (patch) | |
tree | 5ba392d8dd688f1c1475816abe5b1a7cf06e0616 | |
parent | 09209f4168fed34837d11fb828aec33523b71737 (diff) |
[Bromley] Script to update open waste reports.
-rwxr-xr-x | bin/fixmystreet.com/bromley-fetch-waste | 24 | ||||
-rw-r--r-- | perllib/FixMyStreet/Cobrand/Bromley.pm | 149 | ||||
-rw-r--r-- | perllib/Integrations/Echo.pm | 18 | ||||
-rw-r--r-- | t/cobrand/bromley.t | 124 |
4 files changed, 311 insertions, 4 deletions
diff --git a/bin/fixmystreet.com/bromley-fetch-waste b/bin/fixmystreet.com/bromley-fetch-waste new file mode 100755 index 000000000..392905c83 --- /dev/null +++ b/bin/fixmystreet.com/bromley-fetch-waste @@ -0,0 +1,24 @@ +#!/usr/bin/env perl + +use v5.14; +use warnings; + +BEGIN { + use File::Basename qw(dirname); + use File::Spec; + my $d = dirname(File::Spec->rel2abs($0)); + require "$d/../../setenv.pl"; +} + +use Getopt::Long::Descriptive; +use FixMyStreet::Cobrand::Bromley; + +my ($opts, $usage) = describe_options( + '%c %o', + ['verbose|v', 'more verbose output'], + ['help|h', "print usage message and exit" ], +); +$usage->die if $opts->help; + +my $cobrand = FixMyStreet::Cobrand::Bromley->new; +$cobrand->waste_fetch_events($opts->verbose); diff --git a/perllib/FixMyStreet/Cobrand/Bromley.pm b/perllib/FixMyStreet/Cobrand/Bromley.pm index ff2a80b21..1e1212d7b 100644 --- a/perllib/FixMyStreet/Cobrand/Bromley.pm +++ b/perllib/FixMyStreet/Cobrand/Bromley.pm @@ -11,6 +11,7 @@ use Sort::Key::Natural qw(natkeysort_inplace); use Try::Tiny; use FixMyStreet::DateRange; use FixMyStreet::WorkingDays; +use Open311::GetServiceRequestUpdates; use Memcached; sub council_area_id { return 2482; } @@ -668,8 +669,6 @@ Given a DateTime object and a number, return true if today is less than or equal to that number of working days (excluding weekends and bank holidays) after the date. -=back - =cut sub within_working_days { @@ -680,4 +679,150 @@ sub within_working_days { return $today le $dt; } +=item waste_fetch_events + +Loop through all open waste events to see if there have been any updates + +=back + +=cut + +sub waste_fetch_events { + my ($self, $verbose) = @_; + + my $body = $self->body; + my @contacts = $body->contacts->search({ + send_method => 'Open311', + endpoint => { '!=', '' }, + })->all; + die "Could not find any devolved contacts\n" unless @contacts; + + my %open311_conf = ( + endpoint => $contacts[0]->endpoint || '', + api_key => $contacts[0]->api_key || '', + jurisdiction => $contacts[0]->jurisdiction || '', + extended_statuses => $body->send_extended_statuses, + ); + my $cobrand = $body->get_cobrand_handler; + $cobrand->call_hook(open311_config_updates => \%open311_conf) + if $cobrand; + my $open311 = Open311->new(%open311_conf); + + my $updates = Open311::GetServiceRequestUpdates->new( + current_open311 => $open311, + current_body => $body, + system_user => $body->comment_user, + suppress_alerts => 0, + blank_updates_permitted => $body->blank_updates_permitted, + ); + + my $echo = $self->feature('echo'); + $echo = Integrations::Echo->new(%$echo); + + my $cfg = { + verbose => $verbose, + updates => $updates, + echo => $echo, + event_types => {}, + }; + + my $reports = $self->problems->search({ + external_id => { '!=', '' }, + state => [ FixMyStreet::DB::Result::Problem->open_states() ], + category => [ map { $_->category } @contacts ], + }); + + while (my $report = $reports->next) { + print 'Fetching data for report ' . $report->id . "\n" if $verbose; + + my $event = $cfg->{echo}->GetEvent($report->external_id); + my $request = $self->construct_waste_open311_update($cfg, $event) or next; + + next if !$request->{status} || $request->{status} eq 'confirmed'; # Still in initial state + next unless $self->waste_check_last_update( + $cfg, $report, $request->{status}, $request->{external_status_code}); + + my $last_updated = construct_bin_date($event->{LastUpdatedDate}); + $request->{comment_time} = $last_updated; + + print " Updating report to state $request->{status}, $request->{description} ($request->{external_status_code})\n" if $cfg->{verbose}; + $cfg->{updates}->process_update($request, $report); + } +} + +sub construct_waste_open311_update { + my ($self, $cfg, $event) = @_; + + my $event_type = $cfg->{event_types}{$event->{EventTypeId}} ||= $self->waste_get_event_type($cfg, $event->{EventTypeId}); + my $state_id = $event->{EventStateId}; + my $resolution_id = $event->{ResolutionCodeId} || ''; + my $status = $event_type->{states}{$state_id}{state}; + my $description = $event_type->{resolution}{$resolution_id} || $event_type->{states}{$state_id}{name}; + return { + description => $description, + status => $status, + update_id => 'waste', + external_status_code => $resolution_id, + }; +} + +sub waste_get_event_type { + my ($self, $cfg, $id) = @_; + + my $event_type = $cfg->{echo}->GetEventType($id); + + my $state_map = { + New => { New => 'confirmed' }, + Pending => { + Unallocated => 'investigating', + 'Allocated to Crew' => 'action scheduled', + }, + Closed => { + Completed => 'fixed - council', + 'Not Completed' => 'unable to fix', + Rejected => 'closed', + }, + }; + + my $states = $event_type->{Workflow}->{States}->{State}; + my $data; + foreach (@$states) { + my $core = $_->{CoreState}; # New/Pending/Closed + my $name = $_->{Name}; # New : Unallocated/Allocated to Crew : Completed/Not Completed/Rejected + $data->{states}{$_->{Id}} = { + core => $core, + name => $name, + state => $state_map->{$core}{$name}, + }; + my $codes = Integrations::Echo::force_arrayref($_->{ResolutionCodes}, 'StateResolutionCode'); + foreach (@$codes) { + my $name = $_->{Name}; + my $id = $_->{ResolutionCodeId}; + $data->{resolution}{$id} = $name; + } + } + return $data; +} + +# We only have the report's current state, no history, so must check current +# against latest received update to see if state the same, and skip if so +sub waste_check_last_update { + my ($self, $cfg, $report, $status, $resolution_id) = @_; + + my $latest = $report->comments->search( + { external_id => 'waste', }, + { order_by => { -desc => 'id' } } + )->first; + + if ($latest) { + my $state = $cfg->{updates}->current_open311->map_state($status); + my $code = $latest->get_extra_metadata('external_status_code') || ''; + if ($latest->problem_state eq $state && $code eq $resolution_id) { + print " Latest update matches fetched state, skipping\n" if $cfg->{verbose}; + return; + } + } + return 1; +} + 1; diff --git a/perllib/Integrations/Echo.pm b/perllib/Integrations/Echo.pm index 48a0edc6c..9a5b65ec2 100644 --- a/perllib/Integrations/Echo.pm +++ b/perllib/Integrations/Echo.pm @@ -291,6 +291,24 @@ sub GetServiceTaskInstances { return force_arrayref($res, 'ServiceTaskInstances'); } +sub GetEvent { + my ($self, $guid) = @_; + $self->call('GetEvent', ref => ixhash( + Key => 'Guid', + Type => 'Event', + Value => { 'msArray:anyType' => $guid }, + )); +} + +sub GetEventType { + my ($self, $id) = @_; + $self->call('GetEventType', ref => ixhash( + Key => 'Id', + Type => 'EventType', + Value => { 'msArray:anyType' => $id }, + )); +} + sub GetEventsForObject { my ($self, $id, $type) = @_; my $from = DateTime->now->set_time_zone(FixMyStreet->local_time_zone)->subtract(months => 3); diff --git a/t/cobrand/bromley.t b/t/cobrand/bromley.t index b31908f7d..3f3e8ed5d 100644 --- a/t/cobrand/bromley.t +++ b/t/cobrand/bromley.t @@ -1,6 +1,7 @@ use CGI::Simple; use Test::MockModule; use Test::MockTime qw(:all); +use Test::Output; use FixMyStreet::TestMech; use FixMyStreet::Script::Reports; my $mech = FixMyStreet::TestMech->new; @@ -11,7 +12,8 @@ $uk->mock('_fetch_url', sub { '{}' }); # Create test data my $user = $mech->create_user_ok( 'bromley@example.com', name => 'Bromley' ); -my $body = $mech->create_body_ok( 2482, 'Bromley Council'); +my $body = $mech->create_body_ok( 2482, 'Bromley Council', + { can_be_devolved => 1, comment_user => $user }); my $contact = $mech->create_contact_ok( body_id => $body->id, category => 'Other', @@ -32,7 +34,13 @@ $mech->create_contact_ok( email => 'tfl@example.org', ); -my $waste = $mech->create_contact_ok(body => $body, category => 'Report missed collection', email => 'missed'); +my $waste = $mech->create_contact_ok( + body => $body, + category => 'Report missed collection', + email => 'missed', + send_method => 'Open311', + endpoint => 'waste-endpoint', +); $waste->set_extra_metadata(group => ['Waste']); $waste->update; @@ -289,4 +297,116 @@ subtest 'test waste max-per-day' => sub { }; +package SOAP::Result; +sub result { return $_[0]->{result}; } +sub new { my $c = shift; bless { @_ }, $c; } + +package main; + +subtest 'updating of waste reports' => sub { + my $integ = Test::MockModule->new('SOAP::Lite'); + $integ->mock(call => sub { + my ($cls, @args) = @_; + my $method = $args[0]->name; + if ($method eq 'GetEvent') { + my ($key, $type, $value) = ${$args[3]->value}->value; + my $external_id = ${$value->value}->value->value; + my ($waste, $event_state_id, $resolution_code) = split /-/, $external_id; + return SOAP::Result->new(result => { + EventStateId => $event_state_id, + EventTypeId => '2104', + LastUpdatedDate => { OffsetMinutes => 60, DateTime => '2020-06-24T14:00:00Z' }, + ResolutionCodeId => $resolution_code, + }); + } elsif ($method eq 'GetEventType') { + return SOAP::Result->new(result => { + Workflow => { States => { State => [ + { CoreState => 'New', Name => 'New', Id => 15001 }, + { CoreState => 'Pending', Name => 'Unallocated', Id => 15002 }, + { CoreState => 'Pending', Name => 'Allocated to Crew', Id => 15003 }, + { CoreState => 'Closed', Name => 'Completed', Id => 15004, + ResolutionCodes => { StateResolutionCode => [ + { ResolutionCodeId => 201, Name => '' }, + { ResolutionCodeId => 202, Name => 'Spillage on Arrival' }, + ] } }, + { CoreState => 'Closed', Name => 'Not Completed', Id => 15005, + ResolutionCodes => { StateResolutionCode => [ + { ResolutionCodeId => 203, Name => 'Nothing Found' }, + { ResolutionCodeId => 204, Name => 'Too Heavy' }, + { ResolutionCodeId => 205, Name => 'Inclement Weather' }, + ] } }, + { CoreState => 'Closed', Name => 'Rejected', Id => 15006, + ResolutionCodes => { StateResolutionCode => [ + { ResolutionCodeId => 206, Name => 'Out of Time' }, + { ResolutionCodeId => 207, Name => 'Duplicate' }, + ] } }, + ] } }, + }); + } else { + is $method, 'UNKNOWN'; + } + }); + + FixMyStreet::override_config { + ALLOWED_COBRANDS => 'bromley', + COBRAND_FEATURES => { + echo => { bromley => { url => 'https://www.example.org/' } }, + waste => { bromley => 1 } + }, + }, sub { + @reports = $mech->create_problems_for_body(2, $body->id, 'Report missed collection', { + category => 'Report missed collection', + cobrand_data => 'waste', + }); + $reports[1]->update({ external_id => 'something-else' }); # To test loop + $report = $reports[0]; + my $cobrand = FixMyStreet::Cobrand::Bromley->new; + + $report->update({ external_id => 'waste-15001-' }); + stdout_like { + $cobrand->waste_fetch_events(1); + } qr/Fetching data for report/; + $report->discard_changes; + is $report->comments->count, 0, 'No new update'; + is $report->state, 'confirmed', 'No state change'; + + $report->update({ external_id => 'waste-15003-' }); + stdout_like { + $cobrand->waste_fetch_events(1); + } qr/Updating report to state action scheduled, Allocated to Crew/; + $report->discard_changes; + is $report->comments->count, 1, 'A new update'; + is $report->state, 'action scheduled', 'A state change'; + + $report->update({ external_id => 'waste-15003-' }); + stdout_like { + $cobrand->waste_fetch_events(1); + } qr/Latest update matches fetched state/; + $report->discard_changes; + is $report->comments->count, 1, 'No new update'; + is $report->state, 'action scheduled', 'State unchanged'; + + $report->update({ external_id => 'waste-15004-201' }); + stdout_like { + $cobrand->waste_fetch_events(1); + } qr/Updating report to state fixed - council, Completed/; + $report->discard_changes; + is $report->comments->count, 2, 'A new update'; + is $report->state, 'fixed - council', 'Changed to fixed'; + + $reports[1]->update({ state => 'fixed - council' }); + stdout_like { + $cobrand->waste_fetch_events(1); + } qr/^$/, 'No open reports'; + + $report->update({ external_id => 'waste-15005-205', state => 'confirmed' }); + stdout_like { + $cobrand->waste_fetch_events(1); + } qr/Updating report to state unable to fix, Inclement Weather/; + $report->discard_changes; + is $report->comments->count, 3, 'A new update'; + is $report->state, 'unable to fix', 'A state change'; + }; +}; + done_testing(); |