aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStruan Donald <struan@exo.org.uk>2020-09-10 14:49:42 +0100
committerM Somerville <matthew-github@dracos.co.uk>2020-10-14 09:23:30 +0100
commit98cc29c15b98a26f75adabb1c07acdd9513b4446 (patch)
tree088f82776e23748645ef59e3d8635712cb3cce1b
parentae686e9f8f16841d2cfcf00dd674503756d69536 (diff)
[TfL] script to automatically close reports
Closes reports that meet the following criteria: * status is action scheduled * in a category with a fixed - council auto response template * over n days in action scheduled state (n is an argument) Reports matching these criteria are marked as fixed - council and the relevant response template text is added as a comment. Fixes mysociety/fixmystreet-commercial#1955
-rwxr-xr-xbin/tfl/auto-close-reports48
-rw-r--r--perllib/FixMyStreet/Script/TfL/AutoClose.pm129
-rw-r--r--t/script/tfl/autoclose.t216
3 files changed, 393 insertions, 0 deletions
diff --git a/bin/tfl/auto-close-reports b/bin/tfl/auto-close-reports
new file mode 100755
index 000000000..00d310811
--- /dev/null
+++ b/bin/tfl/auto-close-reports
@@ -0,0 +1,48 @@
+#!/usr/bin/env perl
+
+# Closes reports that meet the following criteria:
+# * status is action scheduled
+# * in a category with a fixed - council auto response template
+# * over n days in action scheduled state (n is an argument)
+#
+# Reports matching these criteria are marked as fixed - council and the
+# relevant response template text is added as a comment.
+
+use warnings;
+use v5.14;
+use utf8;
+
+BEGIN {
+ use File::Basename qw(dirname);
+ use File::Spec;
+ my $d = dirname(File::Spec->rel2abs($0));
+ require "$d/../../setenv.pl";
+}
+
+use FixMyStreet;
+use Getopt::Long;
+use FixMyStreet::Script::TfL::AutoClose;
+
+my %h;
+GetOptions(\%h, 'verbose|v', 'days|d', 'help|h', 'commit|c');
+pod2usage(0) if $h{help};
+
+FixMyStreet::Script::TfL::AutoClose->new(%h)->close;
+
+__END__
+
+=head1 NAME
+
+auto-close-reports - set action_scheduled reports to fixed
+
+=head1 SYNOPSIS
+
+auto-close-reports --commit
+
+ Options:
+ --commit Actually close any reports
+ --days Number of days before autoclosing
+ --verbose Output how many reports have been closed
+ --help This help message
+
+=cut
diff --git a/perllib/FixMyStreet/Script/TfL/AutoClose.pm b/perllib/FixMyStreet/Script/TfL/AutoClose.pm
new file mode 100644
index 000000000..687a29e7f
--- /dev/null
+++ b/perllib/FixMyStreet/Script/TfL/AutoClose.pm
@@ -0,0 +1,129 @@
+package FixMyStreet::Script::TfL::AutoClose;
+
+use v5.14;
+
+use Moo;
+use CronFns;
+use FixMyStreet;
+use FixMyStreet::Cobrand;
+use FixMyStreet::DB;
+use Types::Standard qw(InstanceOf Maybe);
+
+has commit => ( is => 'ro', default => 0 );
+
+has verbose => ( is => 'ro', default => 0 );
+
+has body => (
+ is => 'lazy',
+ isa => Maybe[InstanceOf['FixMyStreet::DB::Result::Body']],
+ default => sub {
+ my $body = FixMyStreet::DB->resultset('Body')->find({ name => 'TfL' });
+ return $body;
+ }
+);
+
+has days => (
+ is => 'ro',
+ default => 28
+);
+
+sub close {
+ my $self = shift;
+
+ die "Can't find body\n" unless $self->body;
+ warn "DRY RUN: use --commit to close reports\n" unless $self->commit;
+ my $categories = $self->categories;
+ $self->close_reports($categories);
+}
+
+has newest => (
+ is => 'lazy',
+ isa => InstanceOf['DateTime'],
+ default => sub {
+ my $self = shift;
+ my $days = $self->days * -1;
+ my $date = DateTime->now->add( days => $days )->truncate( to => 'day' );
+ return $date;
+ }
+);
+
+# get list of cateories that have a response template for the fixed
+# state marked as auto-response.
+sub categories {
+ my $self = shift;
+
+ my $templates = FixMyStreet::DB->resultset('ResponseTemplate')->search({
+ state => 'fixed - council',
+ auto_response => 1,
+ body_id => $self->body->id,
+ });
+
+ my %categories;
+ for my $template ( $templates->all ) {
+ map { $categories{$_->category} = $template; } $template->contacts->all;
+ }
+
+ return \%categories;
+}
+
+# find reports in relevant categories that have been set to action
+# scheduled for 30 days.
+sub close_reports {
+ my ($self, $categories) = @_;
+
+ my $dtf = FixMyStreet::DB->schema->storage->datetime_parser;
+
+ my $reports = FixMyStreet::DB->resultset('Problem')->search({
+ category => { -in => [ keys %$categories ] },
+ 'me.state' => 'action scheduled',
+ bodies_str => $self->body->id,
+ 'comments.state' => 'confirmed',
+ 'comments.problem_state' => 'action scheduled',
+ },
+ {
+ group_by => 'me.id',
+ join => [ 'comments' ],
+ having => \[ 'MIN(comments.confirmed) < ?', $dtf->format_datetime($self->newest) ]
+ });
+
+ my $count = 0;
+ for my $r ( $reports->all ) {
+ my $comments = FixMyStreet::DB->resultset('Comment')->search(
+ { problem_id => $r->id },
+ { order_by => 'confirmed' }
+ );
+ my $earliest;
+ while ( my $c = $comments->next ) {
+ if ( $c->problem_state ne 'action scheduled' ) {
+ $earliest = undef;
+ next;
+ }
+ $earliest = $c->confirmed unless defined $earliest;
+ }
+ next unless defined $earliest && $earliest < $self->newest;
+ if ($self->commit) {
+ $r->update({
+ state => 'fixed - council',
+ lastupdate => \'current_timestamp',
+ });
+ my $c = FixMyStreet::DB->resultset('Comment')->new(
+ {
+ problem => $r,
+ text => $categories->{$r->category}->text,
+ state => 'confirmed',
+ problem_state => 'fixed - council',
+ user => $self->body->comment_user,
+ confirmed => \'current_timestamp'
+ }
+ );
+ $c->insert;
+ }
+ $count++;
+ }
+
+ say "$count reports closed" if $self->verbose;
+
+ return 1;
+}
+
+1;
diff --git a/t/script/tfl/autoclose.t b/t/script/tfl/autoclose.t
new file mode 100644
index 000000000..91f46867c
--- /dev/null
+++ b/t/script/tfl/autoclose.t
@@ -0,0 +1,216 @@
+use FixMyStreet::TestMech;
+use DateTime;
+use Test::Output;
+
+use_ok 'FixMyStreet::Script::TfL::AutoClose';
+
+my $close = FixMyStreet::Script::TfL::AutoClose->new( commit => 1 );
+my $no_commit = FixMyStreet::Script::TfL::AutoClose->new();
+my $mech = FixMyStreet::TestMech->new;
+
+
+my $area_id = 2651;
+my $body = $mech->create_body_ok($area_id, 'TfL');
+my $body_user = $mech->create_user_ok('tfl@example.com', name => 'TfL', from_body => $body);
+$body->update( { comment_user_id => $body_user->id } );
+my $c1 = $mech->create_contact_ok(category => 'Potholes', body_id => $body->id, email => 'p');
+my $c2 = $mech->create_contact_ok(category => 'Graffiti', body_id => $body->id, email => 'g');
+my $c3 = $mech->create_contact_ok(category => 'Flytipping', body_id => $body->id, email => 'f');
+my $t1 = FixMyStreet::DB->resultset('ResponseTemplate')->create({
+ body_id => $body->id, title => "Not auto closed", text => "Text 1 ⛄", state => "fixed - council" }
+);
+my $t2 = FixMyStreet::DB->resultset('ResponseTemplate')->create({
+ body_id => $body->id, title => "Auto closed", text => "Text 2", state => "fixed - council", auto_response => 1
+});
+my $t3 = FixMyStreet::DB->resultset('ResponseTemplate')->create({
+ body_id => $body->id, title => "Investigating", text => "Text 3", state => "investigating", auto_response => 1
+});
+$t1->add_to_contacts($c1);
+$t2->add_to_contacts($c2);
+$t3->add_to_contacts($c3);
+
+is_deeply keys %{ $close->categories }, ("Graffiti"), "fetches correct category list";
+
+my $now = DateTime->now;
+
+my %problems;
+for my $p (
+ {
+ category => 'Grafitti',
+ state => 'in progress',
+ test => 'category_not_state',
+ date => $now->clone->add( days => -30 ),
+ },
+ {
+ category => 'Grafitti',
+ state => 'action scheduled',
+ test => 'category_state',
+ date => $now->clone->add( days => -30 ),
+ },
+ {
+ category => 'Potholes',
+ state => 'action scheduled',
+ test => 'not_category_state',
+ date => $now->clone->add( days => -30 ),
+ },
+ {
+ category => 'Grafitti',
+ state => 'action scheduled',
+ test => 'category_state_not_old',
+ date => $now->clone->add( days => -20 ),
+ }
+) {
+ my $k = delete $p->{test};
+ my $d = delete $p->{date};
+ $p->{confirmed} = $d;
+ $p->{lastupdate} = $d;
+ ($problems{$k}) = $mech->create_problems_for_body( 1, $body->id, 'Title', $p);
+ my $c = FixMyStreet::DB->resultset('Comment')->create({
+ problem => $problems{$k},
+ text => 'comment',
+ state => 'confirmed',
+ problem_state => $p->{state},
+ user => $body_user,
+ confirmed => $p->{lastupdate}
+ });
+ is $problems{$k}->comments->count, 1, "comment added";
+}
+
+subtest "check that nothing saved without commit arg" => sub {
+ ok $no_commit->close_reports({ 'Grafitti' => $t2 }), "close reports ran";
+ $_->discard_changes for values %problems;
+
+ is $problems{category_not_state}->state, 'in progress', 'ignores incorrect state';
+ is $problems{category_state}->state, 'action scheduled', 'not updated to fixed';
+ is $problems{not_category_state}->state, 'action scheduled', 'ignores incorrect category';
+ is $problems{category_state_not_old}->state, 'action scheduled', 'ignores newer reports';
+
+ is $problems{category_state}->comments->count, 1, "no comment added";
+};
+
+subtest "check that reports are updated" => sub {
+ ok $close->close_reports({ 'Grafitti' => $t2 }), "close reports ran";
+
+ $_->discard_changes for values %problems;
+
+ is $problems{category_not_state}->state, 'in progress', 'ignores incorrect state';
+ is $problems{category_state}->state, 'fixed - council', 'updates to fixed';
+ is $problems{not_category_state}->state, 'action scheduled', 'ignores incorrect category';
+ is $problems{category_state_not_old}->state, 'action scheduled', 'ignores newer reports';
+
+ my $comment = ( $problems{category_state}->comments->search({}, { order_by => 'id' })->all )[-1];
+ is $comment->text, "Text 2", "correct template used when closing";
+};
+
+subtest "check that days argument works" => sub {
+ my $close_newer = FixMyStreet::Script::TfL::AutoClose->new( days => 19, commit => 1 );
+ ok $close_newer->close_reports({ 'Grafitti' => $t2 }), "close reports ran";
+
+ $problems{category_state_not_old}->discard_changes;
+ is $problems{category_state_not_old}->state, 'fixed - council', 'updates to fixed';
+};
+
+subtest 'check that uses oldest comment for time' => sub {
+ my $latest = $now->clone->add( days => -20 );
+ my $oldest = $now->clone->add( days => -30 );
+ my ($p) = $mech->create_problems_for_body( 1, $body->id, 'Title', {
+ category => 'Grafitti',
+ state => 'action scheduled',
+ user => $body_user,
+ lastupdate => $latest,
+ confirmed => $latest,
+ });
+
+ my $first = FixMyStreet::DB->resultset('Comment')->create({
+ problem => $p,
+ problem_state => 'investigating',
+ text => 'comment',
+ state => 'confirmed',
+ user => $body_user,
+ confirmed => $oldest
+ });
+
+ FixMyStreet::DB->resultset('Comment')->create({
+ problem => $p,
+ text => 'comment',
+ state => 'confirmed',
+ problem_state => 'action scheduled',
+ user => $body_user,
+ confirmed => $latest
+ });
+
+ ok $close->close_reports({ 'Grafitti' => $t2 }), "close reports ran";
+
+ $p->discard_changes;
+ is $p->state, 'action scheduled', 'ignores comments with wrong state';
+
+
+ $first->update( { problem_state => 'action scheduled' });
+ ok $close->close_reports({ 'Grafitti' => $t2 }), "close reports ran";
+
+ $p->discard_changes;
+ is $p->state, 'fixed - council', 'updates to fixed';
+};
+
+subtest 'check that changing state accounted for' => sub {
+ my $latest = $now->clone->add( days => -20 );
+ my $oldest = $now->clone->add( days => -30 );
+ my ($p) = $mech->create_problems_for_body( 1, $body->id, 'Title', {
+ category => 'Grafitti',
+ state => 'action scheduled',
+ user => $body_user,
+ lastupdate => $latest,
+ confirmed => $latest,
+ });
+
+ my $investigating = FixMyStreet::DB->resultset('Comment')->create({
+ problem => $p,
+ problem_state => 'investigating',
+ text => 'comment',
+ state => 'confirmed',
+ user => $body_user,
+ confirmed => $latest
+ });
+
+ FixMyStreet::DB->resultset('Comment')->create({
+ problem => $p,
+ text => 'comment',
+ state => 'confirmed',
+ problem_state => 'action scheduled',
+ user => $body_user,
+ confirmed => $oldest
+ });
+
+ FixMyStreet::DB->resultset('Comment')->create({
+ problem => $p,
+ text => 'comment',
+ state => 'confirmed',
+ problem_state => 'action scheduled',
+ user => $body_user,
+ confirmed => $now
+ });
+
+ ok $close->close_reports({ 'Grafitti' => $t2 }), "close reports ran";
+
+ $p->discard_changes;
+ is $p->state, 'action scheduled', 'change of state resets time';
+
+
+ $investigating->update( { problem_state => 'action scheduled' });
+ ok $close->close_reports({ 'Grafitti' => $t2 }), "close reports ran";
+
+ $p->discard_changes;
+ is $p->state, 'fixed - council', 'updates to fixed';
+};
+subtest 'check verbose works' => sub {
+ my $verbose = FixMyStreet::Script::TfL::AutoClose->new( commit => 1, verbose => 1 );
+ stdout_is { $close->close_reports({ 'Grafitti' => $t2 }) } "", "No message displayed with verbose";
+ stdout_is { $verbose->close_reports({ 'Grafitti' => $t2 }) } "0 reports closed\n", "Message displayed with verbose";
+};
+
+subtest 'check dry run warning displayed' => sub {
+ stderr_is { $no_commit->close() } "DRY RUN: use --commit to close reports\n", "Dry run warning message displayed without commit";
+ stderr_is { $close->close() } "", "No warning displayed with commit";
+};
+
+done_testing;