diff --git a/perllib/Open311/Endpoint/Integration/AlloyV2.pm b/perllib/Open311/Endpoint/Integration/AlloyV2.pm index a942c421f..0cc197390 100644 --- a/perllib/Open311/Endpoint/Integration/AlloyV2.pm +++ b/perllib/Open311/Endpoint/Integration/AlloyV2.pm @@ -1773,6 +1773,59 @@ sub _get_attachments { return @photos; } +=head2 get_photo + +Fetch a photo from Alloy by its file item ID. + +=cut + +sub get_photo { + my ($self, $args) = @_; + + my $item_id = $args->{item}; + unless ($item_id) { + $self->logger->error("get_photo called without item parameter"); + return [ 400, [ 'Content-Type', 'text/plain' ], [ 'Missing item parameter' ] ]; + } + + # Fetch the file from Alloy + my $content; + my $content_type = 'image/jpeg'; # default + + # First, get the file metadata to determine content type + try { + my $file_item = $self->alloy->api_call(call => "item/$item_id"); + if ($file_item && $file_item->{item}) { + my $attrs = $self->alloy->attributes_to_hash($file_item->{item}); + my $filename = $attrs->{attributes_filesOriginalName} || ''; + if ($filename =~ /\.png$/i) { + $content_type = 'image/png'; + } elsif ($filename =~ /\.gif$/i) { + $content_type = 'image/gif'; + } elsif ($filename =~ /\.jpe?g$/i) { + $content_type = 'image/jpeg'; + } + } + } catch { + $self->logger->warn("Failed to fetch file metadata for $item_id: $_"); + }; + + # Now fetch the actual file content + try { + $content = $self->alloy->api_call( + call => "file/$item_id", + raw => 1, + ); + } catch { + $self->logger->error("Failed to fetch photo $item_id: $_"); + return [ 404, [ 'Content-Type', 'text/plain' ], [ 'Photo not found' ] ]; + }; + + return [ 404, [ 'Content-Type', 'text/plain' ], [ 'Photo not found' ] ] unless $content; + return [ 200, [ 'Content-Type', $content_type ], [ $content ] ]; +} + + sub upload_media { my ($self, $args) = @_; diff --git a/perllib/Open311/Endpoint/Integration/UK/Bristol/Alloy.pm b/perllib/Open311/Endpoint/Integration/UK/Bristol/Alloy.pm index bf2ec133c..b25932825 100644 --- a/perllib/Open311/Endpoint/Integration/UK/Bristol/Alloy.pm +++ b/perllib/Open311/Endpoint/Integration/UK/Bristol/Alloy.pm @@ -174,5 +174,39 @@ sub _get_inspection_status { return ($status, $ext_code); } +=head2 _get_inspection_updates_design + +Override to add media_url support for photos attached to the item. + +=cut + +sub _get_inspection_updates_design { + my ($self, $design, $args) = @_; + + # Call parent to get the base updates and the raw items + my ($updates_ref, $items_by_id) = $self->SUPER::_get_inspection_updates_design($design, $args); + my @updates = @$updates_ref; + + # Build attachment cache once for all updates + # This avoids making individual API calls for each attachment + # This also filters out photos that seem to have originated from FMS (based on filename) + my $cache = $self->_build_attachment_cache($args); + + # For each update, use the already-fetched resource to add media URLs + for my $update (@updates) { + my $service_request_id = $update->service_request_id; + + next unless $update->status eq 'fixed'; # only show photos for fixed updates + + # Use the item already fetched by the parent method + my $report = $items_by_id->{$service_request_id}; + + if ($report) { + $update->{media_url} = $self->_media_urls_for_item({item => $report}, $cache, $args); + } + } + + return (\@updates, $items_by_id); +} 1; diff --git a/perllib/Open311/Endpoint/Integration/UK/Dumfries.pm b/perllib/Open311/Endpoint/Integration/UK/Dumfries.pm index 783ccaf77..7abe568af 100644 --- a/perllib/Open311/Endpoint/Integration/UK/Dumfries.pm +++ b/perllib/Open311/Endpoint/Integration/UK/Dumfries.pm @@ -331,58 +331,6 @@ sub _get_inspection_updates_design { return (\@updates, $items_by_id); } -=head2 get_photo - -Fetch a photo from Alloy by its file item ID. - -=cut - -sub get_photo { - my ($self, $args) = @_; - - my $item_id = $args->{item}; - unless ($item_id) { - $self->logger->error("get_photo called without item parameter"); - return [ 400, [ 'Content-Type', 'text/plain' ], [ 'Missing item parameter' ] ]; - } - - # Fetch the file from Alloy - my $content; - my $content_type = 'image/jpeg'; # default - - # First, get the file metadata to determine content type - try { - my $file_item = $self->alloy->api_call(call => "item/$item_id"); - if ($file_item && $file_item->{item}) { - my $attrs = $self->alloy->attributes_to_hash($file_item->{item}); - my $filename = $attrs->{attributes_filesOriginalName} || ''; - if ($filename =~ /\.png$/i) { - $content_type = 'image/png'; - } elsif ($filename =~ /\.gif$/i) { - $content_type = 'image/gif'; - } elsif ($filename =~ /\.jpe?g$/i) { - $content_type = 'image/jpeg'; - } - } - } catch { - $self->logger->warn("Failed to fetch file metadata for $item_id: $_"); - }; - - # Now fetch the actual file content - try { - $content = $self->alloy->api_call( - call => "file/$item_id", - raw => 1, - ); - } catch { - $self->logger->error("Failed to fetch photo $item_id: $_"); - return [ 404, [ 'Content-Type', 'text/plain' ], [ 'Photo not found' ] ]; - }; - - return [ 404, [ 'Content-Type', 'text/plain' ], [ 'Photo not found' ] ] unless $content; - return [ 200, [ 'Content-Type', $content_type ], [ $content ] ]; -} - =head2 _append_attachments_to_defect Appends new attachment IDs to a defect's existing attachments. diff --git a/t/open311/endpoint/bristol_alloy.t b/t/open311/endpoint/bristol_alloy.t index 17d65db61..850cd5ff6 100644 --- a/t/open311/endpoint/bristol_alloy.t +++ b/t/open311/endpoint/bristol_alloy.t @@ -466,4 +466,80 @@ subtest "check fetch updates" => sub { +subtest "check fetch updates includes media_url for fixed status" => sub { + $integration->mock('search', sub { + my ($self, $body) = @_; + if ($body->{properties}{dodiCode} eq 'designs_files') { + return [ + { + itemId => '63e0f01cdce34826965f3039', + createdDate => '2023-02-16T13:50:00.000Z', + attributes => [ + { attributeCode => 'attributes_filesOriginalName', value => 'photo.jpg' }, # BCC inspector photo + ], + }, + { + itemId => '631cdce34826965f3039e0f0', + createdDate => '2023-02-16T13:50:00.000Z', + attributes => [ + { attributeCode => 'attributes_filesOriginalName', value => '123456.0.full.jpeg' }, # FMS report photo + ], + }, + ]; + } + return [ + { # update in fixed state but with FMS photo as well as inspector photo + itemId => '63ee34826965f30390f01cdc', + designCode => 'designs_bWCSCStreetCleansingDefect_5e21a98bca315003e0983035', + attributes => [ + { attributeCode => 'attributes_defectsStatus', value => ['5c8bdfc88ae862230019dc22'] }, + { attributeCode => 'attributes_filesAttachableAttachments', value => ['63e0f01cdce34826965f3039', '631cdce34826965f3039e0f0'] }, + ], + }, + { # update in action scheduled state with inspector photo + itemId => '63ee34826965f30390f01cda', + designCode => 'designs_bWCSCStreetCleansingDefect_5e21a98bca315003e0983035', + attributes => [ + { attributeCode => 'attributes_defectsStatus', value => ['5c8bdfb58ae862230019dc1f'] }, + { attributeCode => 'attributes_filesAttachableAttachments', value => ['63e0f01cdce34826965f3039'] }, + ], + }, + ]; + }); + + my $res = $endpoint->run_test_request( + GET => '/servicerequestupdates.json?jurisdiction_id=dummy&start_date=2023-02-16T07:43:46Z&end_date=2023-02-16T19:43:46Z', + ); + ok $res->is_success, 'valid request' or diag $res->content; + + my $updates = decode_json($res->content); + + is_deeply $updates, [ + { + description => '', + extras => { + latest_data_only => 1 + }, + media_url => 'http://localhost/photos?jurisdiction_id=bristol_alloy&item=63e0f01cdce34826965f3039', # FMS photo is not included + service_request_id => '63ee34826965f30390f01cdc', + status => 'fixed', + update_id => '63ee34826965f30390f01cdc_20230216135008792', + updated_datetime => '2023-02-16T13:50:08Z', + }, + { + description => '', + extras => { + latest_data_only => 1 + }, + media_url => '', # no photos for action scheduled update + service_request_id => '63ee34826965f30390f01cda', + status => 'action_scheduled', + update_id => '63ee34826965f30390f01cda_20230216135008792', + updated_datetime => '2023-02-16T13:50:08Z', + } + ]; + + $integration->unmock('search'); +}; + done_testing; diff --git a/t/open311/endpoint/bristol_alloy.yml b/t/open311/endpoint/bristol_alloy.yml index de2853464..d629d8187 100644 --- a/t/open311/endpoint/bristol_alloy.yml +++ b/t/open311/endpoint/bristol_alloy.yml @@ -1,6 +1,7 @@ { "api_key": "api_key", "api_url": "http://localhost/api/", + "base_url": "http://localhost/", "rfs_design": { "SC-Fly-Post Defect": 'designs_bWCSCFlyPostDefect_5e203bd4ca315009b4e5c714', diff --git a/t/open311/endpoint/json/alloyv2/bristol_item_log_63ee34826965f30390f01cdc.json b/t/open311/endpoint/json/alloyv2/bristol_item_log_63ee34826965f30390f01cdc.json new file mode 100644 index 000000000..079691a02 --- /dev/null +++ b/t/open311/endpoint/json/alloyv2/bristol_item_log_63ee34826965f30390f01cdc.json @@ -0,0 +1,19 @@ +{ + "page": 1, + "pageSize": 20, + "results": [ + { + "itemId": "63ee34826965f30390f01cdc", + "designCode": "designs_bWCSCStreetCleansingDefect_5e21a98bca315003e0983035", + "action": "Edit", + "causes": [ + { + "workflowRunId": "63ee348be5b28203ae91632d", + "discriminator": "ItemChangeCauseWorkflowWebModel" + } + ], + "date": "2023-02-16T13:50:08.792Z", + "username": "alloybot" + } + ] +}