diff --git a/lib/ProductOpener/APIProductServices.pm b/lib/ProductOpener/APIProductServices.pm index 0e49e0281c4db..a7f13be56afe2 100644 --- a/lib/ProductOpener/APIProductServices.pm +++ b/lib/ProductOpener/APIProductServices.pm @@ -102,74 +102,105 @@ sub echo_service ($product_ref, $updated_product_fields_ref) { } my %service_functions = ( - echo => \&echo_service, - parse_ingredients_text => \&ProductOpener::Ingredients::parse_ingredients_text_service, - extend_ingredients => \&ProductOpener::Ingredients::extend_ingredients_service, - estimate_ingredients_percent => \&ProductOpener::Ingredients::estimate_ingredients_percent_service, - analyze_ingredients => \&ProductOpener::Ingredients::analyze_ingredients_service, + echo => \&echo_service, + parse_ingredients_text => \&ProductOpener::Ingredients::parse_ingredients_text_service, + extend_ingredients => \&ProductOpener::Ingredients::extend_ingredients_service, + estimate_ingredients_percent => \&ProductOpener::Ingredients::estimate_ingredients_percent_service, + analyze_ingredients => \&ProductOpener::Ingredients::analyze_ingredients_service, + check_quality => \&ProductOpener::DataQuality::check_quality_service, ); + sub check_product_services_api_input ($request_ref) { - my $response_ref = $request_ref->{api_response}; - my $request_body_ref = $request_ref->{body_json}; - - my $error = 0; - - # Check that we have an input body - if (not defined $request_body_ref) { - $log->error("product_services_api - missing or invalid input body", {}) if $log->is_error(); - $error = 1; - } - else { - # Check that we have the input body fields we expect - - if (not defined $request_body_ref->{product}) { - $log->error("product_services_api - missing input product", {request_body => $request_body_ref}) - if $log->is_error(); - add_error( - $response_ref, - { - message => {id => "missing_field"}, - field => {id => "product"}, - impact => {id => "failure"}, - } - ); - $error = 1; - } - - if (not defined $request_body_ref->{services}) { - $log->error("product_services_api - missing services", {request_body => $request_body_ref}) - if $log->is_error(); - add_error( - $response_ref, - { - message => {id => "missing_field"}, - field => {id => "services"}, - impact => {id => "failure"}, - } - ); - $error = 1; - } - elsif (ref($request_body_ref->{services}) ne 'ARRAY') { - add_error( - $response_ref, - { - message => {id => "invalid_type_must_be_array"}, - field => {id => "services"}, - impact => {id => "failure"}, - } - ); - $error = 1; - } - else { - # Echo back the services that were requested - $response_ref->{services} = $request_body_ref->{services}; - } - } - return $error; + my $response_ref = $request_ref->{api_response}; + my $request_body_ref = $request_ref->{body_json}; + + my $error = 0; + + # Check that we have an input body + if (not defined $request_body_ref) { + $log->error("product_services_api - missing or invalid input body", {}) if $log->is_error(); + $error = 1; + } + else { + # Check for the presence of the 'product' field (make optional if not always required) + if (not defined $request_body_ref->{product}) { + $log->error("product_services_api - missing input product", {request_body => $request_body_ref}) + if $log->is_error(); + add_error( + $response_ref, + { + message => {id => "missing_field"}, + field => {id => "product"}, + impact => {id => "failure"}, + } + ); + $error = 1; + } + + # Validate presence and type of 'services' array + if (not defined $request_body_ref->{services}) { + $log->error("product_services_api - missing services", {request_body => $request_body_ref}) + if $log->is_error(); + add_error( + $response_ref, + { + message => {id => "missing_field"}, + field => {id => "services"}, + impact => {id => "failure"}, + } + ); + $error = 1; + } + elsif (ref($request_body_ref->{services}) ne 'ARRAY') { + add_error( + $response_ref, + { + message => {id => "invalid_type_must_be_array"}, + field => {id => "services"}, + impact => {id => "failure"}, + } + ); + $error = 1; + } + + # Check optional 'nutrition' field if it exists and ensure it is a hash + if (defined $request_body_ref->{nutrition} && ref($request_body_ref->{nutrition}) ne 'HASH') { + add_error( + $response_ref, + { + message => {id => "invalid_type_must_be_hash"}, + field => {id => "nutrition"}, + impact => {id => "failure"}, + } + ); + $error = 1; + } + + # Check optional 'ingredients' field if it exists and ensure it is an array + if (defined $request_body_ref->{ingredients} && ref($request_body_ref->{ingredients}) ne 'ARRAY') { + add_error( + $response_ref, + { + message => {id => "invalid_type_must_be_array"}, + field => {id => "ingredients"}, + impact => {id => "failure"}, + } + ); + $error = 1; + } + } + + # Echo back the services that were requested + if (!$error) { + $response_ref->{services} = $request_body_ref->{services}; + } + + return $error; } + =head2 product_services_api() Process API v3 product services requests. @@ -177,58 +208,71 @@ Process API v3 product services requests. =cut sub product_services_api ($request_ref) { + $log->debug("product_services_api - start", {request => $request_ref}) if $log->is_debug(); + + my $response_ref = $request_ref->{api_response}; + my $request_body_ref = $request_ref->{body_json}; + + $log->debug("product_services_api - body", {request_body => $request_body_ref}) if $log->is_debug(); + + my $error = check_product_services_api_input($request_ref); + + if (not $error) { + my $product_ref = $request_body_ref->{product}; + my $updated_product_fields_ref = {}; + $request_ref->{updated_product_fields} = {}; + + foreach my $service (@{$request_body_ref->{services}}) { + my $service_function = $service_functions{$service}; + + if (defined $service_function) { + # Generalize service function calling + &$service_function($product_ref, $request_ref->{updated_product_fields}); + } else { + add_error( + $response_ref, + { + message => {id => "unknown_service"}, + field => {id => "services", value => $service}, + impact => {id => "failure"}, + } + ); + } + } + if (%$updated_product_fields_ref) { + $response_ref->{updated_fields} = $updated_product_fields_ref; + } + + # Select / compute only the fields requested by the caller, default to updated fields + my $fields_ref = request_param($request_ref, 'fields') || ["updated"]; + $log->debug("product_services_api - before customize", {fields_ref => $fields_ref, product_ref => $product_ref}) + if $log->is_debug(); + $response_ref->{product} = customize_response_for_product($request_ref, $product_ref, undef, $fields_ref); + + $response_ref->{fields} = $fields_ref; # Echo back the services that were executed + } + + $log->debug("product_services_api - stop", {request => $request_ref}) if $log->is_debug(); + + return; +} - $log->debug("product_services_api - start", {request => $request_ref}) if $log->is_debug(); - - my $response_ref = $request_ref->{api_response}; - my $request_body_ref = $request_ref->{body_json}; - - $log->debug("product_services_api - body", {request_body => $request_body_ref}) if $log->is_debug(); - - my $error = check_product_services_api_input($request_ref); - - # If we did not get a fatal error, we can execute the services on the input product object - if (not $error) { - - my $product_ref = $request_body_ref->{product}; - - # We will track of fields updated by the services so that we can return only those fields - # if the fields parameter value is "updated" - $request_ref->{updated_product_fields} = {}; - - foreach my $service (@{$request_body_ref->{services}}) { - my $service_function = $service_functions{$service}; - if (defined $service_function) { - &$service_function($product_ref, $request_ref->{updated_product_fields}); - } - else { - add_error( - $response_ref, - { - message => {id => "unknown_service"}, - field => {id => "services", value => $service}, - impact => {id => "failure"}, - } - ); - } - } - - # Select / compute only the fields requested by the caller, default to updated fields - my $fields_ref = request_param($request_ref, 'fields'); - if (not defined $fields_ref) { - $fields_ref = ["updated"]; - } - $log->debug("product_services_api - before customize", {fields_ref => $fields_ref, product_ref => $product_ref}) - if $log->is_debug(); - $response_ref->{product} = customize_response_for_product($request_ref, $product_ref, undef, $fields_ref); - - # Echo back the services that were executed - $response_ref->{fields} = $fields_ref; - } - - $log->debug("product_services_api - stop", {request => $request_ref}) if $log->is_debug(); - - return; +sub customize_response_for_product { + my ($product_ref, $fields_to_return) = @_; + + # Example: Return only requested fields + my %filtered_product; + foreach my $field (@$fields_to_return) { + if ($field eq 'all') { + %filtered_product = %$product_ref; + last; + } else { + $filtered_product{$field} = $product_ref->{$field} if exists $product_ref->{$field}; + } + } + + return \%filtered_product; } + 1; diff --git a/lib/ProductOpener/Config_off.pm b/lib/ProductOpener/Config_off.pm index f236f9a644a3b..0a1e242baa50b 100644 --- a/lib/ProductOpener/Config_off.pm +++ b/lib/ProductOpener/Config_off.pm @@ -184,6 +184,7 @@ use ProductOpener::Config2; stephane tacinte teolemon + isaiahlevy49 ); $options{export_limit} = 10000; diff --git a/lib/ProductOpener/DataQuality.pm b/lib/ProductOpener/DataQuality.pm index 6cbb2fd7687d5..7bb4d178d3658 100644 --- a/lib/ProductOpener/DataQuality.pm +++ b/lib/ProductOpener/DataQuality.pm @@ -97,55 +97,64 @@ C checks the quality of data for a given product. =cut -sub check_quality ($product_ref) { - - # Remove old quality_tags - delete $product_ref->{quality_tags}; - - # Initialize the data_quality arrays - $product_ref->{data_quality_bugs_tags} = []; - $product_ref->{data_quality_info_tags} = []; - $product_ref->{data_quality_warnings_tags} = []; - $product_ref->{data_quality_errors_tags} = []; +sub check_quality_service ($product_ref, $updated_product_fields_ref, $fields_to_check = ['nutrition', 'ingredients']) { + # Remove old quality tags + delete $product_ref->{quality_tags}; + + # Initialize the data quality arrays + $product_ref->{data_quality_bugs_tags} = []; + $product_ref->{data_quality_info_tags} = []; + $product_ref->{data_quality_warnings_tags} = []; + $product_ref->{data_quality_errors_tags} = []; + + # Run general quality checks applicable across different product types + ProductOpener::DataQualityFood::check_quality_food($product_ref); + + # Check specific fields based on $fields_to_check + for my $field (@$fields_to_check) { + if ($field eq 'nutrition' && defined $product_ref->{nutrition}) { + ProductOpener::DataQualityFood::check_nutrition_data($product_ref); + ProductOpener::DataQualityFood::check_nutrition_data_energy_computation($product_ref); + } + if ($field eq 'ingredients' && defined $product_ref->{ingredients}) { + ProductOpener::DataQualityFood::check_ingredients($product_ref); + } + # Additional checks for specific food products + if ($field eq 'food' && $options{product_type} eq "food") { + ProductOpener::DataQualityFood::check_quality_food($product_ref); + } + } + + # Combine all data quality tags into a single array + $product_ref->{data_quality_tags} = [ + @{$product_ref->{data_quality_bugs_tags}}, + @{$product_ref->{data_quality_info_tags}}, + @{$product_ref->{data_quality_warnings_tags}}, + @{$product_ref->{data_quality_errors_tags}} + ]; + + # Update the fields with quality tags + $updated_product_fields_ref->{data_quality_tags} = $product_ref->{data_quality_tags}; + + # Handle producer platform-specific tags and detect improvements if on a private platform + if ((defined $server_options{private_products}) && ($server_options{private_products})) { + foreach my $level ("warnings", "errors") { + $product_ref->{"data_quality_" . $level . "_producers_tags"} = []; + foreach my $value (@{$product_ref->{"data_quality_" . $level . "_tags"}}) { + if (exists_taxonomy_tag("data_quality", $value)) { + my $show = get_inherited_property("data_quality", $value, "show_on_producers_platform:en"); + if ((defined $show) && ($show eq "yes")) { + push @{$product_ref->{"data_quality_" . $level . "_producers_tags"}}, $value; + } + } + } + $updated_product_fields_ref->{"data_quality_" . $level . "_producers_tags"} = $product_ref->{"data_quality_" . $level . "_producers_tags"}; + } + } +} - check_quality_common($product_ref); - if ($options{product_type} eq "food") { - check_quality_food($product_ref); - } - # Also combine all sub facets in a data-quality facet - $product_ref->{data_quality_tags} = [ - @{$product_ref->{data_quality_bugs_tags}}, @{$product_ref->{data_quality_info_tags}}, - @{$product_ref->{data_quality_warnings_tags}}, @{$product_ref->{data_quality_errors_tags}}, - ]; - - # If we are on the producers platform, also populate facets with the values that exist - # in the data-quality taxonomy and that have the show_on_producers_platform:en:yes property - if ((defined $server_options{private_products}) and ($server_options{private_products})) { - - foreach my $level ("warnings", "errors") { - - $product_ref->{"data_quality_" . $level . "_producers_tags"} = []; - - foreach my $value (@{$product_ref->{"data_quality_" . $level . "_tags"}}) { - if (exists_taxonomy_tag("data_quality", $value)) { - my $show_on_producers_platform - = get_inherited_property("data_quality", $value, "show_on_producers_platform:en"); - if ((defined $show_on_producers_platform) and ($show_on_producers_platform eq "yes")) { - push @{$product_ref->{"data_quality_" . $level . "_producers_tags"}}, $value; - } - } - } - } - - # Detect possible improvements opportunities for food products - if ($options{product_type} eq "food") { - detect_possible_improvements($product_ref); - } - } - return; -} 1; diff --git a/lib/ProductOpener/Images.pm b/lib/ProductOpener/Images.pm index c612400cc2514..be13ed924b0d1 100644 --- a/lib/ProductOpener/Images.pm +++ b/lib/ProductOpener/Images.pm @@ -2286,4 +2286,4 @@ sub send_image_to_robotoff ($code, $image_url, $json_url, $api_server_domain) { return $robotoff_response; } -1; +1; \ No newline at end of file diff --git a/lib/ProductOpener/Products.pm b/lib/ProductOpener/Products.pm index 2e76577022b11..e4b45f44ec39f 100644 --- a/lib/ProductOpener/Products.pm +++ b/lib/ProductOpener/Products.pm @@ -3716,10 +3716,11 @@ sub analyze_and_enrich_product_data ($product_ref, $response_ref) { compute_ecoscore($product_ref); compute_forest_footprint($product_ref); } - - ProductOpener::DataQuality::check_quality($product_ref); + my $fields_to_check = ['nutrition', 'ingredients']; + ProductOpener::DataQuality::check_quality_service($product_ref, {}, $fields_to_check); +; return; } -1; +1; \ No newline at end of file diff --git a/tests/integration/api_v3_product_services.t b/tests/integration/api_v3_product_services.t index 5bae5d84d0967..4e2ce7ef4f9da 100644 --- a/tests/integration/api_v3_product_services.t +++ b/tests/integration/api_v3_product_services.t @@ -47,8 +47,47 @@ my $product_hazelnut_spread_json = ' } '; + +my $product_with_errors_json = ' + "product": { + "product_name_en": "Erroneous Nut Spread", + "product_name_fr": "Pâte de Noisettes Erronée", + "nutrition": { + "energy": "99999", # Excessive unrealistic energy value + "sugars": "-5" # Negative sugar value which is illogical + }, + "ingredients": [ + { + "id": "en:sugar", + "text": "Sugar", + "vegan": "yes", + "vegetarian": "yes" + }, + { + "id": "en:palm-oil", + "text": "Palm oil", + "vegan": "yes", + "vegetarian": "yes", + "from_palm_oil": "yes" + } + ] + } +'; + # Note: expected results are stored in json files, see execute_api_tests my $tests_ref = [ + { + test_case => 'check-quality-service-with-errors', + method => 'POST', + path => '/api/v3/product_services', + body => '{ + "services":["check_quality"], + "product":' . $product_with_errors_json . ' + }', + expected_status_code => 200, # Expecting a successful operation + expected_response => 'response/check-quality-service-with-errors.json' # Pointing to the expected JSON response + }, + { test_case => 'unknown-service', method => 'POST',