aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatthew Somerville <matthew@mysociety.org>2020-06-23 17:29:04 +0100
committerM Somerville <matthew-github@dracos.co.uk>2020-11-11 10:29:20 +0000
commitcabc4f91d55b952ab2521ec85ec745de4c354d8c (patch)
tree5ba392d8dd688f1c1475816abe5b1a7cf06e0616
parent09209f4168fed34837d11fb828aec33523b71737 (diff)
[Bromley] Script to update open waste reports.
-rwxr-xr-xbin/fixmystreet.com/bromley-fetch-waste24
-rw-r--r--perllib/FixMyStreet/Cobrand/Bromley.pm149
-rw-r--r--perllib/Integrations/Echo.pm18
-rw-r--r--t/cobrand/bromley.t124
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();