diff --git a/conf/council-rutland_confirm.yml-example b/conf/council-rutland_confirm.yml-example new file mode 100644 index 000000000..8e26877b6 --- /dev/null +++ b/conf/council-rutland_confirm.yml-example @@ -0,0 +1,18 @@ +"logfile": "" +"min_log_level": "" + +"endpoint_url": "" +"username": "" +"password": "" +"tenant_id": "" + +"cutoff_enquiry_date": "" +"enquiry_method_code": "" +"point_of_contact_code": "" +"server_timezone": "" + +"service_whitelist": {} + +"forward_status_mapping": {} + +"reverse_status_mapping": {} diff --git a/conf/council-rutland.yml-example b/conf/council-rutland_salesforce.yml-example similarity index 100% rename from conf/council-rutland.yml-example rename to conf/council-rutland_salesforce.yml-example diff --git a/perllib/Integrations/Confirm.pm b/perllib/Integrations/Confirm.pm index 6e205d072..7e2692186 100644 --- a/perllib/Integrations/Confirm.pm +++ b/perllib/Integrations/Confirm.pm @@ -50,7 +50,7 @@ has ua => ( If the Confirm endpoint requires a particular EnquiryMethodCode for NewEnquiry requests, override this in the subclass. Valid values can be found by calling -the GetCustomerLookup method on the endpoint. +the GetCustomerLookups method on the endpoint. =cut @@ -64,7 +64,7 @@ has 'enquiry_method_code' => ( Similar to enquiry_method_code, if the Confirm endpoint requires a particular PointOfContactCode for NewEnquiry requests, override this in the subclass. -Valid values can be found by calling the GetCustomerLookup method on the endpoint. +Valid values can be found by calling the GetCustomerLookups method on the endpoint. =cut @@ -78,7 +78,7 @@ has 'point_of_contact_code' => ( Similar to enquiry_method_code/point_of_contact_code, if the Confirm endpoint requires a particular CustomerTypeCode for NewEnquiry requests, override this in the subclass. -Valid values can be found by calling the GetCustomerLookup method on the endpoint. +Valid values can be found by calling the GetCustomerLookups method on the endpoint. =cut @@ -874,6 +874,12 @@ sub GetEnquiries { return @enquiries; } +sub GetCustomerLookups { + my $self = shift; + my $lookups = $self->perform_request(\SOAP::Data->name('GetCustomerLookups')); + return $lookups; +} + sub GetEnquiryLookups { my $self = shift; diff --git a/perllib/Integrations/SalesForce/Rutland.pm b/perllib/Integrations/SalesForce/Rutland.pm index 71097ff2b..ef5070ce0 100644 --- a/perllib/Integrations/SalesForce/Rutland.pm +++ b/perllib/Integrations/SalesForce/Rutland.pm @@ -4,6 +4,7 @@ use Moo; extends 'Integrations::SalesForce::Base'; use JSON::MaybeXS; +use Try::Tiny; has 'requests_endpoint' => ( is => 'ro', @@ -118,7 +119,12 @@ sub get_services { $self->logger->debug("No memcached entry found for $key. Fetching services from Salesforce"); $services = []; - my $response = $self->get($self->services_endpoint . '?summary'); + my $response; + try { + $response = $self->get($self->services_endpoint . '?summary'); + } catch { + return (); + }; for my $service (@{ $response->{CategoryInformation} }) { push @$services, $service; } diff --git a/perllib/Open311/Endpoint.pm b/perllib/Open311/Endpoint.pm index f3141b5f2..9d7ac41e4 100644 --- a/perllib/Open311/Endpoint.pm +++ b/perllib/Open311/Endpoint.pm @@ -824,8 +824,13 @@ sub format_service_requests { ), ( map { - my $value = $request->$_->[0]; - $_ => $value || ''; + my $value; + if (scalar @{ $request->$_ } <= 1) { + $value = $request->$_->[0] || ''; + } else { + $value = $request->@{$_}; + } + $_ => $value; } qw/ media_url diff --git a/perllib/Open311/Endpoint/Integration/Confirm.pm b/perllib/Open311/Endpoint/Integration/Confirm.pm index fbc8dcb53..715e6122f 100644 --- a/perllib/Open311/Endpoint/Integration/Confirm.pm +++ b/perllib/Open311/Endpoint/Integration/Confirm.pm @@ -632,7 +632,7 @@ sub get_service_request_updates { my $job_photos = @{ $integ->enquiry_update_job_photo_statuses }; my $defect_photos = @{ $integ->enquiry_update_defect_photo_statuses }; - my $documents = 'documents { url documentName documentDate }'; + my $documents = 'documents { url documentName documentDate docTypeCode}'; my $job_documents = $job_photos ? $documents : ""; my $defect_documents = $defect_photos ? $documents : ""; @@ -1834,7 +1834,8 @@ sub _parse_graphql_docs { return map { { URL => $_->{url}, Name => $_->{documentName}, - Date => $self->date_parser->parse_datetime($_->{documentDate}) + Date => $self->date_parser->parse_datetime($_->{documentDate}), + ClassificationCode => $_->{docTypeCode} || '', } } @$docs; } diff --git a/perllib/Open311/Endpoint/Integration/Multi.pm b/perllib/Open311/Endpoint/Integration/Multi.pm index 04b23b24b..6bb7730a5 100644 --- a/perllib/Open311/Endpoint/Integration/Multi.pm +++ b/perllib/Open311/Endpoint/Integration/Multi.pm @@ -161,8 +161,6 @@ sub get_token { sub post_service_request_update { my ($self, $args) = @_; - # Cobrand needs to send the service_code through with updates - # (see Bexley's open311_munge_update_params in FMS for example) my ($integration, $service_code) = $self->_map_from_new_id($args->{service_code}, 'service'); my ($integration2, $service_request_id) = $self->_map_from_new_id($args->{service_request_id}, 'request'); die "$integration did not equal $integration2\n" if $integration ne $integration2; diff --git a/perllib/Open311/Endpoint/Integration/UK/Rutland.pm b/perllib/Open311/Endpoint/Integration/UK/Rutland.pm index 3f038cfa4..2181479ac 100644 --- a/perllib/Open311/Endpoint/Integration/UK/Rutland.pm +++ b/perllib/Open311/Endpoint/Integration/UK/Rutland.pm @@ -1,25 +1,32 @@ package Open311::Endpoint::Integration::UK::Rutland; -use parent 'Open311::Endpoint::Integration::SalesForce::Rutland'; use Moo; +extends 'Open311::Endpoint::Integration::Multi'; + +use Module::Pluggable + search_path => ['Open311::Endpoint::Integration::UK::Rutland'], + instantiate => 'new'; has jurisdiction_id => ( is => 'ro', default => 'rutland', ); -sub reverse_status_mapping { - my ($self, $status) = @_; +sub service_request_content { + '/open311/service_request_extended' +} - my %valid_status = map { my $no_spaces = $_; $no_spaces =~ s/\s+/_/g; $_ => $no_spaces; } ( - 'open', 'investigating', 'in progress', 'planned', 'action scheduled', - 'no further action', 'not councils responsibility', 'duplicate', 'internal referral', - 'fixed', 'closed', - ); +=pod - $valid_status{'not responsible'} = 'not_councils_responsibility'; +Rutland was previously only a Salesforce backend in open311-adapter, so we +maintain its categories/IDs without any backend prefix as any updates on pre-multi +reports will be looking for the id without the prefix - return $valid_status{lc($status)} || 'open'; -} +=cut + +has integration_without_prefix => ( + is => 'ro', + default => 'SalesForce', +); -1; +__PACKAGE__->run_if_script; diff --git a/perllib/Open311/Endpoint/Integration/UK/Rutland/Confirm.pm b/perllib/Open311/Endpoint/Integration/UK/Rutland/Confirm.pm new file mode 100644 index 000000000..a8ef7482e --- /dev/null +++ b/perllib/Open311/Endpoint/Integration/UK/Rutland/Confirm.pm @@ -0,0 +1,39 @@ +package Open311::Endpoint::Integration::UK::Rutland::Confirm; + +use Moo; +extends 'Open311::Endpoint::Integration::Confirm'; + +use Open311::Endpoint::Service::UKCouncil::Confirm; + +around BUILDARGS => sub { + my ($orig, $class, %args) = @_; + $args{jurisdiction_id} = 'rutland_confirm'; + $args{publish_service_update_text} = 1; + return $class->$orig(%args); +}; + +=head2 filter_photos_graphql + +Rutland want us to return photos with specific classification +tag. + +=cut + +around filter_photos_graphql => sub { + my ($orig, $self, @photos) = @_; + my @filtered = $self->$orig(@photos); + return grep { $_->{ClassificationCode} && $_->{ClassificationCode} eq 'DT20' } @filtered; +}; + +around _parse_enquiry_status_log => sub { + my ($orig, $self) = (shift, shift); + my $status_log = $_[0]; + + unless ($status_log->{EnquiryStatusCode} eq 'FMS') { + $status_log->{StatusLogNotes} = ''; + }; + + $self->$orig(@_); +}; + +1; diff --git a/perllib/Open311/Endpoint/Integration/SalesForce/Rutland.pm b/perllib/Open311/Endpoint/Integration/UK/Rutland/SalesForce.pm similarity index 84% rename from perllib/Open311/Endpoint/Integration/SalesForce/Rutland.pm rename to perllib/Open311/Endpoint/Integration/UK/Rutland/SalesForce.pm index 67563d34c..cf57d3ca2 100644 --- a/perllib/Open311/Endpoint/Integration/SalesForce/Rutland.pm +++ b/perllib/Open311/Endpoint/Integration/UK/Rutland/SalesForce.pm @@ -1,8 +1,4 @@ -package Open311::Endpoint::Integration::SalesForce::Rutland; - -use Moo; -extends 'Open311::Endpoint'; -with 'Open311::Endpoint::Role::mySociety'; +package Open311::Endpoint::Integration::UK::Rutland::SalesForce; use Open311::Endpoint::Service::UKCouncil::Rutland; use Open311::Endpoint::Service::Request::SalesForce; @@ -15,6 +11,34 @@ use Encode qw(encode_utf8); use Digest::MD5 qw(md5_hex); use DateTime::Format::Strptime; +use Moo; +extends 'Open311::Endpoint'; +with 'Open311::Endpoint::Role::mySociety'; + +has jurisdiction_id => ( + is => 'ro', + default => 'rutland_salesforce', +); + +has 'whitelist' => ( + is => 'ro', + is => 'lazy', + default => sub { shift->get_integration->config->{whitelist} || {} } +); + +sub reverse_status_mapping { + my ($self, $status) = @_; + + my %valid_status = map { my $no_spaces = $_; $no_spaces =~ s/\s+/_/g; $_ => $no_spaces; } ( + 'open', 'investigating', 'in progress', 'planned', 'action scheduled', + 'no further action', 'not councils responsibility', 'duplicate', 'internal referral', + 'fixed', 'closed', + ); + + $valid_status{'not responsible'} = 'not_councils_responsibility'; + return $valid_status{lc($status)} || 'open'; +} + sub service_request_content { '/open311/service_request_extended' } @@ -37,8 +61,6 @@ sub parse_datetime { return $strp->parse_datetime($time); } -sub reverse_status_mapping {} - has '+request_class' => ( is => 'ro', default => 'Open311::Endpoint::Service::Request::SalesForce', @@ -180,6 +202,7 @@ sub services { my ($self, $args) = @_; my @services = $self->get_integration->get_services($args); + @services = grep { $self->whitelist->{ $_->{name} } || $_->{hasChildren} eq 'true' } @services; my %service_lookup = map { $_->{serviceid} => $_ } @services; @@ -214,6 +237,7 @@ sub service { my $meta = $self->get_integration->get_service($id, $args); my @services = $self->get_integration->get_services($args); + @services = grep { $self->whitelist->{ $_->{name} } || $_->{hasChildren} eq 'true' } @services; my %service_lookup = map { $_->{serviceid} => $_ } @services; my $srv = $service_lookup{$id}; @@ -256,25 +280,24 @@ sub service { } my %options = ( + code => 'notice', required => 0, variable => 0, datatype => 'string', - automated => 'server_set', ); - push @{ $service->attributes }, Open311::Endpoint::Service::Attribute->new( - code => 'hint', - description => $hint, - %options, - ); - - push @{ $service->attributes }, Open311::Endpoint::Service::Attribute->new( - code => 'group_hint', - description => $group_hint, - %options, - ); + if ($hint || $group_hint) { + my $description = $group_hint ? '
' . $group_hint . '
' : ''; + $description .= $hint ? '' . $hint . '
' : ''; + if ($description) { + push @{ $service->attributes }, Open311::Endpoint::Service::Attribute->new( + description => $description, + %options, + ); + }; + }; return $service; } -__PACKAGE__->run_if_script; +1; diff --git a/perllib/Open311/Endpoint/Role/Photos.pm b/perllib/Open311/Endpoint/Role/Photos.pm index aecec1154..48c630b09 100644 --- a/perllib/Open311/Endpoint/Role/Photos.pm +++ b/perllib/Open311/Endpoint/Role/Photos.pm @@ -25,11 +25,25 @@ around dispatch_request => sub { ); }; +# Handle being given a multi's jurisdiction_id directly sub get_photo { my ($self, $args) = @_; - $self->_call('get_photo', $args->{jurisdiction_id}, $args) - or [ 400, [ 'Content-type', 'text/plain' ], [ 'Bad request' ] ]; + my $jurisdiction_id = $args->{jurisdiction_id}; + + foreach ($self->plugins) { + if ($_->jurisdiction_id eq $jurisdiction_id) { + return $_->get_photo($args); + } + if ($_->isa('Open311::Endpoint::Integration::Multi')) { + foreach ($_->plugins) { + if ($_->jurisdiction_id eq $jurisdiction_id) { + return $_->get_photo($args); + } + } + } + } + + [ 400, [ 'Content-type', 'text/plain' ], [ 'Bad request' ] ]; } - 1; diff --git a/perllib/Open311/Endpoint/Role/mySociety.pm b/perllib/Open311/Endpoint/Role/mySociety.pm index 016628224..988697bd4 100644 --- a/perllib/Open311/Endpoint/Role/mySociety.pm +++ b/perllib/Open311/Endpoint/Role/mySociety.pm @@ -286,8 +286,13 @@ sub format_updates { ), ( map { - my $value = $update->$_->[0]; - $_ => $value || ''; + my $value; + if (scalar @{ $update->$_ } <= 1) { + $value = $update->$_->[0] || ''; + } else { + $value = $update->@{$_}; + } + $_ => $value; } qw/ media_url @@ -326,6 +331,7 @@ sub learn_additional_types { my ($self, $schema) = @_; $schema->learn_type( 'tag:wiki.open311.org,GeoReport_v2:rx/status_extended', Open311::Endpoint::Schema->enum('//str', + 'unchanged', 'open', 'closed', 'fixed', @@ -346,6 +352,7 @@ sub learn_additional_types { ); $schema->learn_type( 'tag:wiki.open311.org,GeoReport_v2:rx/status_extended_upper', Open311::Endpoint::Schema->enum('//str', + 'UNCHANGED', 'OPEN', 'CLOSED', 'FIXED', @@ -373,7 +380,7 @@ sub learn_additional_types { status => '/open311/status_extended', updated_datetime => '/open311/datetime', description => '//str', - media_url => '//str', + media_url => { type => '//any', of => [ { type => '//str' }, { type => '//arr', contents => '//str' } ] }, }, optional => { external_status_code => '//str', @@ -401,7 +408,7 @@ sub learn_additional_types { zipcode => '//str', lat => '//num', long => '//num', - media_url => '//str', + media_url => { type => '//any', of => [ { type => '//str' }, { type => '//arr', contents => '//str' } ] }, }, optional => { title => '//str', diff --git a/perllib/Open311/Endpoint/Service/Request/Update/mySociety.pm b/perllib/Open311/Endpoint/Service/Request/Update/mySociety.pm index 388c5b7a9..6fc64c65f 100644 --- a/perllib/Open311/Endpoint/Service/Request/Update/mySociety.pm +++ b/perllib/Open311/Endpoint/Service/Request/Update/mySociety.pm @@ -8,6 +8,7 @@ extends 'Open311::Endpoint::Service::Request::Update'; has status => ( is => 'ro', isa => Enum[ + 'unchanged', # For when the update has no state 'open', 'closed', 'fixed', diff --git a/t/open311/endpoint/confirm_photos.t b/t/open311/endpoint/confirm_photos.t index 85358e3b5..eafafc84a 100644 --- a/t/open311/endpoint/confirm_photos.t +++ b/t/open311/endpoint/confirm_photos.t @@ -96,7 +96,11 @@ subtest "fetching of job photos for enquiry update" => sub { GET => '/servicerequestupdates.xml?start_date=2025-01-01T00:00:00Z&end_date=2025-01-01T01:00:00Z', ); ok $res->is_success, 'valid request' or diag $res->content; - contains_string $res->content, 'This is the group HTML hint
This is the category HTML hint
", + "datatype" => "string", + "datatype_description" => "", } ] }, 'correct json returned'; diff --git a/t/open311/endpoint/rutland_salesforce.yml b/t/open311/endpoint/rutland_salesforce.yml new file mode 100644 index 000000000..084fce70c --- /dev/null +++ b/t/open311/endpoint/rutland_salesforce.yml @@ -0,0 +1,3 @@ +whitelist: + Fly Tipping: 1 + diff --git a/t/open311/endpoint/uk.t b/t/open311/endpoint/uk.t index 8cef9c3d5..b2583cd48 100644 --- a/t/open311/endpoint/uk.t +++ b/t/open311/endpoint/uk.t @@ -18,7 +18,6 @@ test_multi(0, 'Open311::Endpoint::Integration::UK', 'Open311::Endpoint::Integration::UK::Kingston' => 'kingston_echo', 'Open311::Endpoint::Integration::UK::Lincolnshire' => 'lincolnshire_confirm', 'Open311::Endpoint::Integration::UK::NorthumberlandAlloy' => 'northumberland_alloy', - 'Open311::Endpoint::Integration::UK::Rutland' => 'rutland', 'Open311::Endpoint::Integration::UK::Shropshire' => 'shropshire_confirm', 'Open311::Endpoint::Integration::UK::Southwark' => 'southwark_confirm', 'Open311::Endpoint::Integration::UK::Surrey' => 'surrey_boomi', @@ -92,6 +91,12 @@ test_multi(0, 'Open311::Endpoint::Integration::UK::BANES', #'Open311::Endpoint::Integration::UK::Bristol::Passthrough' => 'www.banes.gov.uk', ); +test_multi(1, 'Open311::Endpoint::Integration::UK::Rutland', + 'Open311::Endpoint::Integration::UK::Rutland::SalesForce' => 'rutland_salesforce', + 'Open311::Endpoint::Integration::UK::Rutland::Confirm' => 'rutland_confirm', +); + + done_testing; sub test_multi {