diff --git a/apps/transport/lib/db/notification_subscription.ex b/apps/transport/lib/db/notification_subscription.ex index 3dabcbc87e..739fb2fcf6 100644 --- a/apps/transport/lib/db/notification_subscription.ex +++ b/apps/transport/lib/db/notification_subscription.ex @@ -14,14 +14,18 @@ defmodule DB.NotificationSubscription do @hidden_reasons_related_to_datasets [:dataset_now_on_nap, :resources_changed] # These notification reasons are *not* linked to a specific dataset, `dataset_id` should be nil @platform_wide_reasons [:new_dataset, :datasets_switching_climate_resilience_bill, :daily_new_comments] + @possible_roles [:producer, :reuser] + @type role :: :producer | :reuser typed_schema "notification_subscription" do field(:reason, Ecto.Enum, values: @reasons_related_to_datasets ++ @platform_wide_reasons ++ @hidden_reasons_related_to_datasets ) + # Useful to know if the subscription has been created by an admin + # from the backoffice (`:admin`) or by the user (`:user`) field(:source, Ecto.Enum, values: [:admin, :user]) - field(:role, Ecto.Enum, values: [:producer, :reuser]) + field(:role, Ecto.Enum, values: @possible_roles) belongs_to(:contact, DB.Contact) belongs_to(:dataset, DB.Dataset) @@ -83,6 +87,26 @@ defmodule DB.NotificationSubscription do |> DB.Repo.all() end + @spec subscriptions_for_reason_dataset_and_role(atom(), DB.Dataset.t(), role()) :: [__MODULE__.t()] + def subscriptions_for_reason_dataset_and_role(reason, %DB.Dataset{id: dataset_id}, role) + when role in @possible_roles do + base_query() + |> preload([:contact]) + |> where( + [notification_subscription: ns], + ns.reason == ^reason and ns.dataset_id == ^dataset_id and ns.role == ^role + ) + |> DB.Repo.all() + end + + @spec subscriptions_for_reason_and_role(atom(), role()) :: [__MODULE__.t()] + def subscriptions_for_reason_and_role(reason, role) when role in @possible_roles do + base_query() + |> preload([:contact]) + |> where([notification_subscription: ns], ns.reason == ^reason and is_nil(ns.dataset_id) and ns.role == ^role) + |> DB.Repo.all() + end + @spec subscriptions_for_dataset(DB.Dataset.t()) :: [__MODULE__.t()] def subscriptions_for_dataset(%DB.Dataset{id: dataset_id}) do base_query() @@ -91,6 +115,14 @@ defmodule DB.NotificationSubscription do |> DB.Repo.all() end + @spec subscriptions_for_dataset_and_role(DB.Dataset.t(), role()) :: [__MODULE__.t()] + def subscriptions_for_dataset_and_role(%DB.Dataset{id: dataset_id}, role) when role in @possible_roles do + base_query() + |> preload([:contact]) + |> where([notification_subscription: ns], ns.dataset_id == ^dataset_id and ns.role == ^role) + |> DB.Repo.all() + end + @spec subscriptions_to_emails([__MODULE__.t()]) :: [binary()] def subscriptions_to_emails(subscriptions) do subscriptions |> Enum.map(& &1.contact.email) diff --git a/apps/transport/lib/jobs/multi_validation_with_error_notification_job.ex b/apps/transport/lib/jobs/multi_validation_with_error_notification_job.ex index 31f70eaa6d..49b4cbf18a 100644 --- a/apps/transport/lib/jobs/multi_validation_with_error_notification_job.ex +++ b/apps/transport/lib/jobs/multi_validation_with_error_notification_job.ex @@ -26,40 +26,42 @@ defmodule Transport.Jobs.MultiValidationWithErrorNotificationJob do inserted_at |> relevant_validations() |> Enum.each(fn {%DB.Dataset{} = dataset, multi_validations} -> - dataset - |> emails_list() - |> MapSet.difference(notifications_sent_recently(dataset)) - |> Enum.each(fn email -> - Transport.EmailSender.impl().send_mail( - "transport.data.gouv.fr", - Application.get_env(:transport, :contact_email), - email, - Application.get_env(:transport, :contact_email), - "Erreurs détectées dans le jeu de données #{dataset.custom_title}", - """ - Bonjour, + producer_emails = dataset |> emails_list(:producer) + send_to_producers(producer_emails, dataset, multi_validations) - Des erreurs bloquantes ont été détectées dans votre jeu de données #{dataset_url(dataset)}. Ces erreurs empêchent la réutilisation de vos données. - - Nous vous invitons à les corriger en vous appuyant sur les rapports de validation suivants : - #{Enum.map_join(multi_validations, "\n", &resource_link/1)} - - Nous restons disponible pour vous accompagner si besoin. - - Merci par avance pour votre action, + reuser_emails = dataset |> emails_list(:reuser) + send_to_reusers(reuser_emails, dataset, producer_warned: not Enum.empty?(producer_emails)) + end) + end - À bientôt, + defp send_to_reusers(emails, %DB.Dataset{} = dataset, producer_warned: producer_warned) do + Enum.each(emails, &send_mail(&1, :reuser, dataset: dataset, producer_warned: producer_warned)) + end - L'équipe du PAN - """, - "" - ) + defp send_to_producers(emails, %DB.Dataset{} = dataset, multi_validations) do + Enum.each( + emails, + &send_mail(&1, :producer, + dataset: dataset, + resources: Enum.map(multi_validations, fn mv -> mv.resource_history.resource end) + ) + ) + end - save_notification(dataset, email) - end) - end) + defp send_mail(email, role, args) do + dataset = Keyword.fetch!(args, :dataset) + + Transport.EmailSender.impl().send_mail( + "transport.data.gouv.fr", + Application.get_env(:transport, :contact_email), + email, + Application.get_env(:transport, :contact_email), + "Erreurs détectées dans le jeu de données #{dataset.custom_title}", + "", + Phoenix.View.render_to_string(TransportWeb.EmailView, "#{@notification_reason}_#{role}.html", args) + ) - :ok + save_notification(dataset, email) end defp notifications_sent_recently(%DB.Dataset{id: dataset_id}) do @@ -91,24 +93,11 @@ defmodule Transport.Jobs.MultiValidationWithErrorNotificationJob do |> Enum.group_by(& &1.resource_history.resource.dataset) end - defp emails_list(%DB.Dataset{} = dataset) do + defp emails_list(%DB.Dataset{} = dataset, role) do @notification_reason - |> DB.NotificationSubscription.subscriptions_for_reason(dataset) + |> DB.NotificationSubscription.subscriptions_for_reason_dataset_and_role(dataset, role) |> DB.NotificationSubscription.subscriptions_to_emails() |> MapSet.new() - end - - defp dataset_url(%DB.Dataset{slug: slug, custom_title: custom_title}) do - url = TransportWeb.Router.Helpers.dataset_url(TransportWeb.Endpoint, :details, slug) - - "#{custom_title} — #{url}" - end - - defp resource_link(%DB.MultiValidation{ - resource_history: %DB.ResourceHistory{resource: %DB.Resource{id: id, title: title}} - }) do - url = TransportWeb.Router.Helpers.resource_url(TransportWeb.Endpoint, :details, id) <> "#validation-report" - - "* #{title} — #{url}" + |> MapSet.difference(notifications_sent_recently(dataset)) end end diff --git a/apps/transport/lib/jobs/resource_unavailable_notification_job.ex b/apps/transport/lib/jobs/resource_unavailable_notification_job.ex index eae5dd97e1..f3c029c895 100644 --- a/apps/transport/lib/jobs/resource_unavailable_notification_job.ex +++ b/apps/transport/lib/jobs/resource_unavailable_notification_job.ex @@ -21,30 +21,50 @@ defmodule Transport.Jobs.ResourceUnavailableNotificationJob do inserted_at |> relevant_unavailabilities() |> Enum.each(fn {%DB.Dataset{} = dataset, unavailabilities} -> - dataset - |> emails_list() - |> MapSet.difference(notifications_sent_recently(dataset)) - |> Enum.each(fn email -> - Transport.EmailSender.impl().send_mail( - "transport.data.gouv.fr", - Application.get_env(:transport, :contact_email), - email, - Application.get_env(:transport, :contact_email), - "Ressources indisponibles dans le jeu de données #{dataset.custom_title}", - "", - Phoenix.View.render_to_string(TransportWeb.EmailView, "resource_unavailable.html", - dataset: dataset, - hours_consecutive_downtime: @hours_consecutive_downtime, - deleted_recreated_on_datagouv: deleted_and_recreated_resource_hosted_on_datagouv(dataset, unavailabilities), - resource_titles: Enum.map_join(unavailabilities, ", ", &resource_title/1) - ) - ) - - save_notification(dataset, email) - end) + producer_emails = emails_list(dataset, :producer) + send_to_producers(producer_emails, dataset, unavailabilities) + + reuser_emails = emails_list(dataset, :reuser) + send_to_reusers(reuser_emails, dataset, unavailabilities, producer_warned: not Enum.empty?(producer_emails)) + end) + end + + defp send_to_reusers(emails, %DB.Dataset{} = dataset, unavailabilities, producer_warned: producer_warned) do + Enum.each(emails, fn email -> + send_mail(email, :reuser, + dataset: dataset, + hours_consecutive_downtime: @hours_consecutive_downtime, + producer_warned: producer_warned, + resource_titles: Enum.map_join(unavailabilities, ", ", &resource_title/1) + ) + end) + end + + defp send_to_producers(emails, dataset, unavailabilities) do + Enum.each(emails, fn email -> + send_mail(email, :producer, + dataset: dataset, + hours_consecutive_downtime: @hours_consecutive_downtime, + deleted_recreated_on_datagouv: deleted_and_recreated_resource_hosted_on_datagouv(dataset, unavailabilities), + resource_titles: Enum.map_join(unavailabilities, ", ", &resource_title/1) + ) end) + end + + defp send_mail(email, role, args) do + dataset = Keyword.fetch!(args, :dataset) + + Transport.EmailSender.impl().send_mail( + "transport.data.gouv.fr", + Application.get_env(:transport, :contact_email), + email, + Application.get_env(:transport, :contact_email), + "Ressources indisponibles dans le jeu de données #{dataset.custom_title}", + "", + Phoenix.View.render_to_string(TransportWeb.EmailView, "#{@notification_reason}_#{role}.html", args) + ) - :ok + save_notification(dataset, email) end defp notifications_sent_recently(%DB.Dataset{id: dataset_id}) do @@ -105,11 +125,12 @@ defmodule Transport.Jobs.ResourceUnavailableNotificationJob do DB.Notification.insert!(@notification_reason, dataset, email) end - defp emails_list(%DB.Dataset{} = dataset) do + defp emails_list(%DB.Dataset{} = dataset, role) do @notification_reason - |> DB.NotificationSubscription.subscriptions_for_reason(dataset) + |> DB.NotificationSubscription.subscriptions_for_reason_dataset_and_role(dataset, role) |> DB.NotificationSubscription.subscriptions_to_emails() |> MapSet.new() + |> MapSet.difference(notifications_sent_recently(dataset)) end defp resource_title(%DB.ResourceUnavailability{resource: %DB.Resource{title: title}}) do diff --git a/apps/transport/lib/transport_web/templates/email/dataset_with_error_producer.html.md b/apps/transport/lib/transport_web/templates/email/dataset_with_error_producer.html.md new file mode 100644 index 0000000000..d5c04469c4 --- /dev/null +++ b/apps/transport/lib/transport_web/templates/email/dataset_with_error_producer.html.md @@ -0,0 +1,16 @@ +Bonjour, + +Des erreurs bloquantes ont été détectées dans votre jeu de données <%= link_for_dataset(@dataset, :heex) %>. Ces erreurs empêchent la réutilisation de vos données. + +Nous vous invitons à les corriger en vous appuyant sur les rapports de validation suivants : +<%= for resource <- @resources do %> +<%= link_for_resource(resource) %> +<% end %> + +Nous restons disponible pour vous accompagner si besoin. + +Merci par avance pour votre action, + +À bientôt, + +L'équipe du PAN diff --git a/apps/transport/lib/transport_web/templates/email/dataset_with_error_reuser.html.md b/apps/transport/lib/transport_web/templates/email/dataset_with_error_reuser.html.md new file mode 100644 index 0000000000..14f22e7f5f --- /dev/null +++ b/apps/transport/lib/transport_web/templates/email/dataset_with_error_reuser.html.md @@ -0,0 +1,9 @@ +Bonjour, + +Des erreurs bloquantes ont été détectées dans le jeu de données <%= link_for_dataset(@dataset, :heex) %> que vous réutilisez. + +<%= if @producer_warned do %> +Le producteur de ces données a été informé de ces erreurs. +<% end %> + +L’équipe du PAN diff --git a/apps/transport/lib/transport_web/templates/email/resource_unavailable.html.md b/apps/transport/lib/transport_web/templates/email/resource_unavailable_producer.html.md similarity index 100% rename from apps/transport/lib/transport_web/templates/email/resource_unavailable.html.md rename to apps/transport/lib/transport_web/templates/email/resource_unavailable_producer.html.md diff --git a/apps/transport/lib/transport_web/templates/email/resource_unavailable_reuser.html.md b/apps/transport/lib/transport_web/templates/email/resource_unavailable_reuser.html.md new file mode 100644 index 0000000000..575a240626 --- /dev/null +++ b/apps/transport/lib/transport_web/templates/email/resource_unavailable_reuser.html.md @@ -0,0 +1,9 @@ +Bonjour, + +Les ressources <%= @resource_titles %> du jeu de données <%= link_for_dataset(@dataset, :heex) %> que vous réutilisez ne sont plus disponibles au téléchargement depuis plus de <%= @hours_consecutive_downtime %>h. + +<%= if @producer_warned do %> +Le producteur de ces données a été informé de cette indisponibilité. +<% end %> + +L’équipe du PAN diff --git a/apps/transport/lib/transport_web/views/email_view.ex b/apps/transport/lib/transport_web/views/email_view.ex index 3ba95101b7..7cf2268096 100644 --- a/apps/transport/lib/transport_web/views/email_view.ex +++ b/apps/transport/lib/transport_web/views/email_view.ex @@ -9,4 +9,9 @@ defmodule TransportWeb.EmailView do :heex -> link(custom_title, to: url) end end + + def link_for_resource(%DB.Resource{id: id, title: title}) do + url = TransportWeb.Router.Helpers.resource_url(TransportWeb.Endpoint, :details, id) + link(title, to: url) + end end diff --git a/apps/transport/test/transport/jobs/multi_validation_with_error_notification_job_test.exs b/apps/transport/test/transport/jobs/multi_validation_with_error_notification_job_test.exs index 91bab44c85..088174a933 100644 --- a/apps/transport/test/transport/jobs/multi_validation_with_error_notification_job_test.exs +++ b/apps/transport/test/transport/jobs/multi_validation_with_error_notification_job_test.exs @@ -91,7 +91,9 @@ defmodule Transport.Test.Transport.Jobs.MultiValidationWithErrorNotificationJobT %DB.Contact{id: already_sent_contact_id} = insert_contact(%{email: already_sent_email}) %DB.Contact{id: foo_contact_id} = insert_contact(%{email: "foo@example.com"}) + %DB.Contact{id: reuser_contact_id} = insert_contact(%{email: reuser_email = "reuser@example.com"}) + # Subscriptions for a contact who was already warned, a producer and a reuser insert(:notification_subscription, %{ reason: :dataset_with_error, source: :admin, @@ -108,6 +110,15 @@ defmodule Transport.Test.Transport.Jobs.MultiValidationWithErrorNotificationJobT dataset_id: dataset.id }) + insert(:notification_subscription, %{ + reason: :dataset_with_error, + source: :user, + role: :reuser, + contact_id: reuser_contact_id, + dataset_id: dataset.id + }) + + # Contact + subscription for another dataset %DB.Contact{id: bar_contact_id} = insert_contact(%{email: "bar@example.com"}) insert(:notification_subscription, %{ @@ -124,18 +135,36 @@ defmodule Transport.Test.Transport.Jobs.MultiValidationWithErrorNotificationJobT "foo@example.com" = _to, "contact@transport.beta.gouv.fr", subject, - plain_text_body, - "" = _html_part -> + "", + html -> assert subject == "Erreurs détectées dans le jeu de données #{dataset.custom_title}" - assert plain_text_body =~ - "Des erreurs bloquantes ont été détectées dans votre jeu de données #{dataset.custom_title}" + assert html =~ + ~s(Des erreurs bloquantes ont été détectées dans votre jeu de données #{dataset.custom_title}) + + assert html =~ + ~s(#{resource_1.title}) + + assert html =~ + ~s(#{resource_2.title}) + + :ok + end) + + Transport.EmailSender.Mock + |> expect(:send_mail, fn "transport.data.gouv.fr", + "contact@transport.beta.gouv.fr", + ^reuser_email, + "contact@transport.beta.gouv.fr", + subject, + "", + html -> + assert subject == "Erreurs détectées dans le jeu de données #{dataset.custom_title}" - assert plain_text_body =~ - "#{resource_1.title} — http://127.0.0.1:5100/resources/#{resource_1.id}#validation-report" + assert html =~ + ~s(Des erreurs bloquantes ont été détectées dans le jeu de données #{dataset.custom_title} que vous réutilisez.) - assert plain_text_body =~ - "#{resource_2.title} — http://127.0.0.1:5100/resources/#{resource_2.id}#validation-report" + assert html =~ "Le producteur de ces données a été informé de ces erreurs." :ok end) @@ -146,15 +175,15 @@ defmodule Transport.Test.Transport.Jobs.MultiValidationWithErrorNotificationJobT "bar@example.com" = _to, "contact@transport.beta.gouv.fr", subject, - plain_text_body, - "" = _html_part -> + "", + html -> assert subject == "Erreurs détectées dans le jeu de données #{gtfs_dataset.custom_title}" - assert plain_text_body =~ - "Des erreurs bloquantes ont été détectées dans votre jeu de données #{gtfs_dataset.custom_title}" + assert html =~ + ~s(Des erreurs bloquantes ont été détectées dans votre jeu de données #{gtfs_dataset.custom_title}) - assert plain_text_body =~ - "#{resource_gtfs.title} — http://127.0.0.1:5100/resources/#{resource_gtfs.id}#validation-report" + assert html =~ + ~s(#{resource_gtfs.title}) :ok end) @@ -172,6 +201,14 @@ defmodule Transport.Test.Transport.Jobs.MultiValidationWithErrorNotificationJobT ) |> DB.Repo.exists?() + assert DB.Notification + |> where( + [n], + n.email_hash == ^reuser_email and n.dataset_id == ^dataset_id and n.inserted_at >= ^recent_dt and + n.reason == :dataset_with_error + ) + |> DB.Repo.exists?() + assert DB.Notification |> where( [n], diff --git a/apps/transport/test/transport/jobs/resource_unavailable_notification_job_test.exs b/apps/transport/test/transport/jobs/resource_unavailable_notification_job_test.exs index 8de6267a3f..8bf55351c1 100644 --- a/apps/transport/test/transport/jobs/resource_unavailable_notification_job_test.exs +++ b/apps/transport/test/transport/jobs/resource_unavailable_notification_job_test.exs @@ -101,6 +101,7 @@ defmodule Transport.Test.Transport.Jobs.ResourceUnavailableNotificationJobTest d %DB.Contact{id: already_sent_contact_id} = insert_contact(%{email: already_sent_email}) %DB.Contact{id: foo_contact_id} = insert_contact(%{email: "foo@example.com"}) + %DB.Contact{id: reuser_contact_id} = insert_contact(%{email: reuser_email = "reuser@example.com"}) insert(:notification_subscription, %{ reason: :resource_unavailable, @@ -118,6 +119,14 @@ defmodule Transport.Test.Transport.Jobs.ResourceUnavailableNotificationJobTest d dataset_id: dataset.id }) + insert(:notification_subscription, %{ + reason: :resource_unavailable, + source: :user, + role: :reuser, + contact_id: reuser_contact_id, + dataset_id: dataset.id + }) + %DB.Contact{id: bar_contact_id} = insert_contact(%{email: "bar@example.com"}) insert(:notification_subscription, %{ @@ -147,6 +156,24 @@ defmodule Transport.Test.Transport.Jobs.ResourceUnavailableNotificationJobTest d :ok end) + Transport.EmailSender.Mock + |> expect(:send_mail, fn "transport.data.gouv.fr", + "contact@transport.beta.gouv.fr", + ^reuser_email = _to, + "contact@transport.beta.gouv.fr", + subject, + _plain_text_body, + html_part -> + assert subject == "Ressources indisponibles dans le jeu de données #{dataset.custom_title}" + + assert html_part =~ + ~s(Les ressources #{resource_1.title}, #{resource_2.title} du jeu de données #{dataset.custom_title} que vous réutilisez ne sont plus disponibles au téléchargement depuis plus de 6h.) + + assert html_part =~ "Le producteur de ces données a été informé de cette indisponibilité." + + :ok + end) + Transport.EmailSender.Mock |> expect(:send_mail, fn "transport.data.gouv.fr", "contact@transport.beta.gouv.fr", @@ -171,6 +198,8 @@ defmodule Transport.Test.Transport.Jobs.ResourceUnavailableNotificationJobTest d # Logs have been saved recent_dt = DateTime.utc_now() |> DateTime.add(-1, :second) + assert DB.Notification |> DB.Repo.aggregate(:count) == 7 + assert DB.Notification |> where( [n], @@ -179,6 +208,14 @@ defmodule Transport.Test.Transport.Jobs.ResourceUnavailableNotificationJobTest d ) |> DB.Repo.exists?() + assert DB.Notification + |> where( + [n], + n.email_hash == ^reuser_email and n.dataset_id == ^dataset_id and n.inserted_at >= ^recent_dt and + n.reason == :resource_unavailable + ) + |> DB.Repo.exists?() + assert DB.Notification |> where( [n],