Queue bounded support and legal email
This commit is contained in:
parent
346b03695f
commit
d2fb194d56
|
|
@ -77,7 +77,9 @@ local Codex CLI authenticated with their ChatGPT subscription.
|
|||
verified-contact status links, verification-gated support alerts, and audited
|
||||
staff queues. Support sends one initial response email and later public-status
|
||||
changes through the dedicated Oban `mail` queue; ordinary conversation
|
||||
messages stay in the private inbox and optional push.
|
||||
messages stay in the private inbox and optional push. Content-removal
|
||||
confirmation, receipt, and public decision updates use the same bounded queue,
|
||||
while internal assignment-only changes do not email the submitter.
|
||||
Authenticated users can download an allow-listed JSON data export that omits
|
||||
password/session/push credentials and counterpart message bodies. A
|
||||
moderator-only deletion preflight reports active workflows without performing
|
||||
|
|
|
|||
|
|
@ -1047,11 +1047,13 @@ The worker role consumes `maintenance`, `push`, and `mail`. Configure their
|
|||
per-worker limits with `OBAN_MAINTENANCE_CONCURRENCY`,
|
||||
`OBAN_PUSH_CONCURRENCY`, and `OBAN_MAIL_CONCURRENCY`; multiplying a value by
|
||||
the number of worker replicas gives the configured cluster-wide concurrency
|
||||
for that queue. Authentication and mandatory legal-intake confirmations remain
|
||||
synchronous. Ordinary support-update email runs in `mail`, so SMTP latency does
|
||||
not occupy a web request or the `push` queue. Optional new-case operator alerts
|
||||
also run in `mail`; requester conversation messages do not generate operator
|
||||
email.
|
||||
for that queue. Authentication links remain synchronous because their database
|
||||
token is committed only when delivery succeeds. Support-contact verification,
|
||||
support updates, content-removal confirmation/receipt/decision messages, and
|
||||
optional new-case operator alerts run in `mail`, so their SMTP latency does not
|
||||
occupy a web request or the `push` queue. Requester and staff conversation
|
||||
messages do not each generate email, and internal assignment-only legal updates
|
||||
do not notify the submitter.
|
||||
|
||||
After starting the isolated load project, run an explicitly sized experiment:
|
||||
|
||||
|
|
|
|||
|
|
@ -115,6 +115,14 @@ specific review.
|
|||
public support contact is verified. Content-removal notification behavior is
|
||||
kept separate because those notices can require prompt legal or safety review.
|
||||
|
||||
All support-contact and content-removal confirmation, receipt, decision, and
|
||||
optional operator-alert messages run through the dedicated Oban `mail` queue.
|
||||
Authentication email remains a separate immediate path. A legal operator's
|
||||
assignment or other internal-only change does not email the submitter; a public
|
||||
status or decision-text change does. Queue uniqueness coalesces an undelivered
|
||||
update to the latest persisted case state instead of sending one message for
|
||||
each rapid edit.
|
||||
|
||||
The alert intentionally excludes the free-text report and reported URLs. The
|
||||
full record remains in the protected database queue. If the variable is empty,
|
||||
intake and reporter acknowledgement still work, but no operator inbox alert is
|
||||
|
|
|
|||
|
|
@ -30,11 +30,14 @@ results from product limits and unknown production properties.
|
|||
response or a later public-status change, while authenticated requesters also
|
||||
receive an in-app support update. Ordinary requester and staff conversation
|
||||
messages do not each generate email.
|
||||
- Optional support email runs through the dedicated Oban `mail` queue, whose
|
||||
default concurrency is one per worker. Fixed-purpose delivery telemetry
|
||||
records only the allow-listed purpose and outcome, without email addresses or
|
||||
message content. Authentication and content-removal confirmations remain
|
||||
transactional email paths by design.
|
||||
- Support-contact verification, optional support updates, content-removal
|
||||
confirmation/receipt/decision messages, and optional operator alerts run
|
||||
through the dedicated Oban `mail` queue, whose default concurrency is one per
|
||||
worker. Internal legal assignment-only changes do not email the submitter.
|
||||
Fixed-purpose delivery telemetry records only the allow-listed purpose and
|
||||
outcome, without email addresses or message content. Authentication links
|
||||
remain synchronous by design because their database token is committed only
|
||||
when delivery succeeds.
|
||||
- Exact-name inspection after completion found no container or temporary image
|
||||
belonging to quality scope `20260812133255-3403320` /
|
||||
`wnh_quality_202608121332553403320`.
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ defmodule WhoNeedHelp.ContentRemoval do
|
|||
alias WhoNeedHelp.Accounts
|
||||
alias WhoNeedHelp.Accounts.{Scope, User}
|
||||
alias WhoNeedHelp.ContentRemoval.{Notice, Notifier}
|
||||
alias WhoNeedHelp.Mail.ContentRemovalEmailWorker
|
||||
alias WhoNeedHelp.Pagination
|
||||
alias WhoNeedHelp.PublicAccess
|
||||
alias WhoNeedHelp.Repo
|
||||
|
|
@ -63,11 +64,11 @@ defmodule WhoNeedHelp.ContentRemoval do
|
|||
"regime" => to_string(notice.regime),
|
||||
"status" => to_string(notice.status)
|
||||
}
|
||||
) do
|
||||
),
|
||||
{:ok, _jobs} <- enqueue_created_emails(notice) do
|
||||
{:ok, notice}
|
||||
end
|
||||
end)
|
||||
|> notify_received()
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -198,7 +199,13 @@ defmodule WhoNeedHelp.ContentRemoval do
|
|||
|> Repo.one()
|
||||
|
||||
if notice do
|
||||
with {:ok, notice} <- notice |> Notice.moderation_changeset(attrs) |> Repo.update(),
|
||||
changeset = Notice.moderation_changeset(notice, attrs)
|
||||
|
||||
requester_update? =
|
||||
Ecto.Changeset.changed?(changeset, :status) or
|
||||
Ecto.Changeset.changed?(changeset, :resolution_note)
|
||||
|
||||
with {:ok, notice} <- Repo.update(changeset),
|
||||
{:ok, _audit} <-
|
||||
Trust.audit(
|
||||
moderator.id,
|
||||
|
|
@ -209,14 +216,15 @@ defmodule WhoNeedHelp.ContentRemoval do
|
|||
"status" => to_string(notice.status),
|
||||
"assigned_to_id" => notice.assigned_to_id
|
||||
}
|
||||
) do
|
||||
{:ok, Repo.preload(notice, :assigned_to, force: true)}
|
||||
),
|
||||
{:ok, _job} <- maybe_enqueue_update(notice, requester_update?) do
|
||||
{:ok, {Repo.preload(notice, :assigned_to, force: true), requester_update?}}
|
||||
end
|
||||
else
|
||||
{:error, :not_found}
|
||||
end
|
||||
end)
|
||||
|> notify_decision()
|
||||
|> normalize_moderation_result()
|
||||
end
|
||||
else
|
||||
false -> {:error, :forbidden}
|
||||
|
|
@ -224,52 +232,8 @@ defmodule WhoNeedHelp.ContentRemoval do
|
|||
end
|
||||
end
|
||||
|
||||
defp notify_received({:ok, %Notice{contact_email: email, contact_verified_at: nil} = notice})
|
||||
when is_binary(email) and email != "" do
|
||||
case Notifier.deliver_confirmation(notice, confirmation_url(notice)) do
|
||||
{:ok, _metadata} -> mark_acknowledgement_sent(notice)
|
||||
_error -> {:ok, notice}
|
||||
end
|
||||
end
|
||||
|
||||
defp notify_received({:ok, %Notice{contact_email: email} = notice}) when email in [nil, ""],
|
||||
do: notify_operator(notice)
|
||||
|
||||
defp notify_received({:ok, notice}) do
|
||||
result =
|
||||
case Notifier.deliver_received(notice, status_url(notice)) do
|
||||
{:ok, _metadata} -> mark_acknowledgement_sent(notice)
|
||||
_error -> {:ok, notice}
|
||||
end
|
||||
|
||||
case result do
|
||||
{:ok, current} -> notify_operator(current)
|
||||
error -> error
|
||||
end
|
||||
end
|
||||
|
||||
defp notify_received(result), do: result
|
||||
|
||||
defp notify_operator(notice) do
|
||||
_ = Notifier.deliver_operator_alert(notice)
|
||||
{:ok, notice}
|
||||
end
|
||||
|
||||
defp notify_decision({:ok, %Notice{contact_verified_at: nil} = notice}), do: {:ok, notice}
|
||||
|
||||
defp notify_decision({:ok, notice}) do
|
||||
case Notifier.deliver_decision(notice, status_url(notice)) do
|
||||
{:ok, _metadata} ->
|
||||
notice
|
||||
|> Ecto.Changeset.change(decision_sent_at: DateTime.utc_now(:second))
|
||||
|> Repo.update()
|
||||
|
||||
_error ->
|
||||
{:ok, notice}
|
||||
end
|
||||
end
|
||||
|
||||
defp notify_decision(result), do: result
|
||||
defp normalize_moderation_result({:ok, {notice, _requester_update?}}), do: {:ok, notice}
|
||||
defp normalize_moderation_result(result), do: result
|
||||
|
||||
defp verify_contact(%Notice{contact_verified_at: nil} = notice) do
|
||||
Repo.transact(fn ->
|
||||
|
|
@ -287,13 +251,13 @@ defmodule WhoNeedHelp.ContentRemoval do
|
|||
{:ok, {current, :already_verified}}
|
||||
|
||||
true ->
|
||||
with {:ok, verified} <-
|
||||
current
|
||||
|> Ecto.Changeset.change(contact_verified_at: DateTime.utc_now(:second))
|
||||
|> Repo.update()
|
||||
|> then(fn
|
||||
{:ok, verified} -> {:ok, {verified, :newly_verified}}
|
||||
error -> error
|
||||
end)
|
||||
|> Repo.update(),
|
||||
{:ok, _jobs} <- enqueue_verified_emails(verified) do
|
||||
{:ok, {verified, :newly_verified}}
|
||||
end
|
||||
end
|
||||
end)
|
||||
|> notify_verified_notice()
|
||||
|
|
@ -302,8 +266,6 @@ defmodule WhoNeedHelp.ContentRemoval do
|
|||
defp verify_contact(%Notice{} = notice), do: {:ok, notice}
|
||||
|
||||
defp notify_verified_notice({:ok, {notice, :newly_verified}}) do
|
||||
_ = Notifier.deliver_received(notice, status_url(notice))
|
||||
_ = Notifier.deliver_operator_alert(notice)
|
||||
{:ok, notice}
|
||||
end
|
||||
|
||||
|
|
@ -325,12 +287,46 @@ defmodule WhoNeedHelp.ContentRemoval do
|
|||
Application.fetch_env!(:who_need_help, :public_case_access_max_age_seconds)
|
||||
end
|
||||
|
||||
defp mark_acknowledgement_sent(notice) do
|
||||
notice
|
||||
|> Ecto.Changeset.change(acknowledgement_sent_at: DateTime.utc_now(:second))
|
||||
|> Repo.update()
|
||||
defp enqueue_email(notice, kind) do
|
||||
%{notice_id: notice.id, kind: to_string(kind)}
|
||||
|> ContentRemovalEmailWorker.new()
|
||||
|> Oban.insert()
|
||||
end
|
||||
|
||||
defp enqueue_created_emails(%Notice{contact_email: email, contact_verified_at: nil} = notice)
|
||||
when is_binary(email) and email != "" do
|
||||
with {:ok, confirmation} <- enqueue_email(notice, :confirmation) do
|
||||
{:ok, [confirmation]}
|
||||
end
|
||||
end
|
||||
|
||||
defp enqueue_created_emails(%Notice{} = notice), do: enqueue_verified_emails(notice)
|
||||
|
||||
defp enqueue_verified_emails(%Notice{} = notice) do
|
||||
with {:ok, received} <- maybe_enqueue_received(notice),
|
||||
{:ok, operator} <- maybe_enqueue_operator(notice) do
|
||||
{:ok, [received, operator]}
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_enqueue_received(%Notice{contact_email: email} = notice)
|
||||
when is_binary(email) and email != "",
|
||||
do: enqueue_email(notice, :received)
|
||||
|
||||
defp maybe_enqueue_received(%Notice{}), do: {:ok, :no_contact_email}
|
||||
|
||||
defp maybe_enqueue_operator(%Notice{} = notice) do
|
||||
if Notifier.operator_alerts_enabled?(),
|
||||
do: enqueue_email(notice, :operator),
|
||||
else: {:ok, :disabled}
|
||||
end
|
||||
|
||||
defp maybe_enqueue_update(%Notice{contact_verified_at: nil}, _requester_update?),
|
||||
do: {:ok, :contact_not_verified}
|
||||
|
||||
defp maybe_enqueue_update(%Notice{} = notice, true), do: enqueue_email(notice, :update)
|
||||
defp maybe_enqueue_update(%Notice{}, false), do: {:ok, :not_needed}
|
||||
|
||||
defp visible_to_staff(query) do
|
||||
where(
|
||||
query,
|
||||
|
|
|
|||
|
|
@ -93,6 +93,8 @@ defmodule WhoNeedHelp.ContentRemoval.Notifier do
|
|||
end
|
||||
end
|
||||
|
||||
def operator_alerts_enabled?, do: match?({:ok, _address}, operator_alert_address())
|
||||
|
||||
defp deliver(recipient, subject, body, kind) do
|
||||
from = Application.fetch_env!(:who_need_help, :mailer_from)
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ defmodule WhoNeedHelp.EmailDelivery do
|
|||
:content_removal_operator,
|
||||
:content_removal_received,
|
||||
:content_removal_update,
|
||||
:nearby_alert,
|
||||
:support_confirmation,
|
||||
:support_operator,
|
||||
:support_update
|
||||
|
|
|
|||
74
lib/who_need_help/mail/content_removal_email_worker.ex
Normal file
74
lib/who_need_help/mail/content_removal_email_worker.ex
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
defmodule WhoNeedHelp.Mail.ContentRemovalEmailWorker do
|
||||
@moduledoc false
|
||||
|
||||
use Oban.Worker,
|
||||
queue: :mail,
|
||||
max_attempts: 8,
|
||||
unique: [
|
||||
period: :infinity,
|
||||
fields: [:args, :worker],
|
||||
keys: [:notice_id, :kind],
|
||||
states: :incomplete
|
||||
]
|
||||
|
||||
alias WhoNeedHelp.ContentRemoval
|
||||
alias WhoNeedHelp.ContentRemoval.{Notice, Notifier}
|
||||
alias WhoNeedHelp.Repo
|
||||
|
||||
@impl Oban.Worker
|
||||
def perform(%Oban.Job{args: %{"notice_id" => notice_id, "kind" => kind}}) do
|
||||
case Repo.get(Notice, notice_id) do
|
||||
nil ->
|
||||
{:cancel, :content_removal_notice_missing}
|
||||
|
||||
%Notice{} = notice ->
|
||||
deliver(notice, kind)
|
||||
end
|
||||
end
|
||||
|
||||
defp deliver(%Notice{contact_verified_at: verified_at}, "confirmation")
|
||||
when not is_nil(verified_at),
|
||||
do: {:cancel, :contact_already_verified}
|
||||
|
||||
defp deliver(%Notice{} = notice, "confirmation") do
|
||||
notice
|
||||
|> Notifier.deliver_confirmation(ContentRemoval.confirmation_url(notice))
|
||||
|> mark_sent(notice, :acknowledgement_sent_at)
|
||||
end
|
||||
|
||||
defp deliver(%Notice{contact_verified_at: nil}, kind) when kind in ["received", "update"],
|
||||
do: {:cancel, :contact_not_verified}
|
||||
|
||||
defp deliver(%Notice{} = notice, "received") do
|
||||
notice
|
||||
|> Notifier.deliver_received(ContentRemoval.status_url(notice))
|
||||
|> mark_sent(notice, :acknowledgement_sent_at)
|
||||
end
|
||||
|
||||
defp deliver(%Notice{} = notice, "update") do
|
||||
notice
|
||||
|> Notifier.deliver_decision(ContentRemoval.status_url(notice))
|
||||
|> mark_sent(notice, :decision_sent_at)
|
||||
end
|
||||
|
||||
defp deliver(%Notice{} = notice, "operator") do
|
||||
case Notifier.deliver_operator_alert(notice) do
|
||||
{:ok, _metadata} -> :ok
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp deliver(%Notice{}, _kind), do: {:cancel, :unknown_content_removal_email_kind}
|
||||
|
||||
defp mark_sent({:ok, _metadata}, notice, field) do
|
||||
notice
|
||||
|> Ecto.Changeset.change([{field, DateTime.utc_now(:second)}])
|
||||
|> Repo.update()
|
||||
|> case do
|
||||
{:ok, _notice} -> :ok
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp mark_sent({:error, reason}, _notice, _field), do: {:error, reason}
|
||||
end
|
||||
34
lib/who_need_help/mail/support_confirmation_worker.ex
Normal file
34
lib/who_need_help/mail/support_confirmation_worker.ex
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
defmodule WhoNeedHelp.Mail.SupportConfirmationWorker do
|
||||
@moduledoc false
|
||||
|
||||
use Oban.Worker,
|
||||
queue: :mail,
|
||||
max_attempts: 8,
|
||||
unique: [
|
||||
period: :infinity,
|
||||
fields: [:args, :worker],
|
||||
keys: [:request_id],
|
||||
states: :incomplete
|
||||
]
|
||||
|
||||
alias WhoNeedHelp.Repo
|
||||
alias WhoNeedHelp.Support
|
||||
alias WhoNeedHelp.Support.{Notifier, SupportRequest}
|
||||
|
||||
@impl Oban.Worker
|
||||
def perform(%Oban.Job{args: %{"request_id" => request_id}}) do
|
||||
case Repo.get(SupportRequest, request_id) do
|
||||
nil ->
|
||||
{:cancel, :support_request_missing}
|
||||
|
||||
%SupportRequest{contact_verified_at: verified_at} when not is_nil(verified_at) ->
|
||||
{:cancel, :contact_already_verified}
|
||||
|
||||
%SupportRequest{} = request ->
|
||||
case Notifier.deliver_confirmation(request, Support.confirmation_url(request)) do
|
||||
{:ok, _metadata} -> :ok
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -6,7 +6,13 @@ defmodule WhoNeedHelp.Support do
|
|||
alias WhoNeedHelp.Accounts
|
||||
alias WhoNeedHelp.Accounts.DataLifecycle
|
||||
alias WhoNeedHelp.Accounts.{Scope, User}
|
||||
alias WhoNeedHelp.Mail.{SupportOperatorAlertWorker, SupportUpdateWorker}
|
||||
|
||||
alias WhoNeedHelp.Mail.{
|
||||
SupportConfirmationWorker,
|
||||
SupportOperatorAlertWorker,
|
||||
SupportUpdateWorker
|
||||
}
|
||||
|
||||
alias WhoNeedHelp.Notifications
|
||||
alias WhoNeedHelp.Pagination
|
||||
alias WhoNeedHelp.PublicAccess
|
||||
|
|
@ -70,14 +76,14 @@ defmodule WhoNeedHelp.Support do
|
|||
request.status,
|
||||
user && user.id,
|
||||
:requester
|
||||
) do
|
||||
),
|
||||
{:ok, _job} <- enqueue_created_email(request) do
|
||||
{:ok, request}
|
||||
end
|
||||
end)
|
||||
|
||||
result
|
||||
|> resolve_duplicate_submission(fingerprint)
|
||||
|> notify_created()
|
||||
|> broadcast_request_update()
|
||||
end
|
||||
end
|
||||
|
|
@ -257,14 +263,14 @@ defmodule WhoNeedHelp.Support do
|
|||
"status" => to_string(request.status),
|
||||
"assigned_to_id" => request.assigned_to_id
|
||||
}
|
||||
) do
|
||||
{:ok,
|
||||
{preload_conversation(request),
|
||||
%{
|
||||
),
|
||||
update = %{
|
||||
audit_id: audit.id,
|
||||
message_added: match?(%ConversationMessage{}, message),
|
||||
status_changed: previous_status != request.status
|
||||
}}}
|
||||
},
|
||||
{:ok, _job} <- maybe_enqueue_support_email(request, update) do
|
||||
{:ok, {preload_conversation(request), update}}
|
||||
end
|
||||
else
|
||||
{:error, :not_found}
|
||||
|
|
@ -406,18 +412,13 @@ defmodule WhoNeedHelp.Support do
|
|||
end
|
||||
end
|
||||
|
||||
defp notify_created({:ok, {request, :pending_verification}}) do
|
||||
_ = Notifier.deliver_confirmation(request, confirmation_url(request))
|
||||
{:ok, request}
|
||||
defp enqueue_created_email(%SupportRequest{contact_verified_at: nil} = request) do
|
||||
request.id
|
||||
|> then(&SupportConfirmationWorker.new(%{request_id: &1}))
|
||||
|> Oban.insert()
|
||||
end
|
||||
|
||||
defp notify_created({:ok, {request, :verified}}) do
|
||||
_ = enqueue_operator_alert(request)
|
||||
{:ok, request}
|
||||
end
|
||||
|
||||
defp notify_created({:ok, {request, :duplicate}}), do: {:ok, request}
|
||||
defp notify_created(result), do: result
|
||||
defp enqueue_created_email(%SupportRequest{} = request), do: enqueue_operator_alert(request)
|
||||
|
||||
defp notify_decision({:ok, {%SupportRequest{contact_verified_at: nil} = request, _update}}),
|
||||
do: {:ok, request}
|
||||
|
|
@ -427,10 +428,6 @@ defmodule WhoNeedHelp.Support do
|
|||
notify_requester_in_app(request, update)
|
||||
end
|
||||
|
||||
if support_email_needed?(request, update) do
|
||||
_ = enqueue_support_email(request)
|
||||
end
|
||||
|
||||
{:ok, request}
|
||||
end
|
||||
|
||||
|
|
@ -493,7 +490,8 @@ defmodule WhoNeedHelp.Support do
|
|||
"support_request.contact_verified",
|
||||
"support_request",
|
||||
verified.id
|
||||
) do
|
||||
),
|
||||
{:ok, _job} <- enqueue_operator_alert(verified) do
|
||||
{:ok, {verified, :newly_verified}}
|
||||
end
|
||||
end
|
||||
|
|
@ -504,8 +502,6 @@ defmodule WhoNeedHelp.Support do
|
|||
defp verify_contact(%SupportRequest{} = request), do: {:ok, request}
|
||||
|
||||
defp notify_verified_request({:ok, {request, :newly_verified}}) do
|
||||
_ = enqueue_operator_alert(request)
|
||||
|
||||
event = {:support_request_updated, request.id}
|
||||
Phoenix.PubSub.broadcast(WhoNeedHelp.PubSub, request_topic(request.id), event)
|
||||
Phoenix.PubSub.broadcast(WhoNeedHelp.PubSub, @staff_topic, event)
|
||||
|
|
@ -538,6 +534,14 @@ defmodule WhoNeedHelp.Support do
|
|||
update.status_changed or (update.message_added and is_nil(request.response_sent_at))
|
||||
end
|
||||
|
||||
defp maybe_enqueue_support_email(request, update) do
|
||||
if support_email_needed?(request, update) do
|
||||
enqueue_support_email(request)
|
||||
else
|
||||
{:ok, :not_needed}
|
||||
end
|
||||
end
|
||||
|
||||
defp enqueue_support_email(request) do
|
||||
request.id
|
||||
|> then(&SupportUpdateWorker.new(%{request_id: &1}))
|
||||
|
|
@ -757,15 +761,14 @@ defmodule WhoNeedHelp.Support do
|
|||
end
|
||||
|
||||
defp resolve_duplicate_submission({:ok, request}, _fingerprint) do
|
||||
outcome = if request.contact_verified_at, do: :verified, else: :pending_verification
|
||||
{:ok, {request, outcome}}
|
||||
{:ok, request}
|
||||
end
|
||||
|
||||
defp resolve_duplicate_submission({:error, %Ecto.Changeset{} = changeset} = error, fingerprint)
|
||||
when is_binary(fingerprint) do
|
||||
if duplicate_fingerprint_error?(changeset) do
|
||||
case Repo.get_by(SupportRequest, public_submission_fingerprint: fingerprint) do
|
||||
%SupportRequest{} = request -> {:ok, {request, :duplicate}}
|
||||
%SupportRequest{} = request -> {:ok, request}
|
||||
nil -> error
|
||||
end
|
||||
else
|
||||
|
|
|
|||
|
|
@ -7,7 +7,14 @@ defmodule WhoNeedHelp.SupportAndContentRemovalTest do
|
|||
|
||||
alias WhoNeedHelp.{Catalog, ContentRemoval, Help, Repo, Support}
|
||||
alias WhoNeedHelp.ContentRemoval.Notice
|
||||
alias WhoNeedHelp.Mail.{SupportOperatorAlertWorker, SupportUpdateWorker}
|
||||
|
||||
alias WhoNeedHelp.Mail.{
|
||||
ContentRemovalEmailWorker,
|
||||
SupportConfirmationWorker,
|
||||
SupportOperatorAlertWorker,
|
||||
SupportUpdateWorker
|
||||
}
|
||||
|
||||
alias WhoNeedHelp.Notifications.Notification
|
||||
alias WhoNeedHelp.Support.{ConversationMessage, StatusEvent, SupportRequest}
|
||||
alias WhoNeedHelp.Trust.AuditEvent
|
||||
|
|
@ -27,6 +34,14 @@ defmodule WhoNeedHelp.SupportAndContentRemovalTest do
|
|||
assert is_binary(request.public_submission_fingerprint)
|
||||
assert is_nil(request.contact_verified_at)
|
||||
|
||||
assert_enqueued(
|
||||
worker: SupportConfirmationWorker,
|
||||
queue: :mail,
|
||||
args: %{"request_id" => request.id}
|
||||
)
|
||||
|
||||
assert :ok = perform_job(SupportConfirmationWorker, %{"request_id" => request.id})
|
||||
|
||||
test_pid = self()
|
||||
|
||||
assert_email_sent(fn email ->
|
||||
|
|
@ -103,6 +118,8 @@ defmodule WhoNeedHelp.SupportAndContentRemovalTest do
|
|||
"Please review the moderation decision identified in my account notice."
|
||||
})
|
||||
|
||||
assert :ok = perform_job(SupportConfirmationWorker, %{"request_id" => request.id})
|
||||
|
||||
assert_email_sent(fn email ->
|
||||
email.to == [{"", "appeal@example.com"}] and
|
||||
email.reply_to == {"", "support@example.com"} and
|
||||
|
|
@ -179,6 +196,7 @@ defmodule WhoNeedHelp.SupportAndContentRemovalTest do
|
|||
}
|
||||
|
||||
assert {:ok, first} = Support.create_request(nil, attrs)
|
||||
assert :ok = perform_job(SupportConfirmationWorker, %{"request_id" => first.id})
|
||||
assert_email_sent()
|
||||
|
||||
assert {:ok, duplicate} =
|
||||
|
|
@ -386,11 +404,98 @@ defmodule WhoNeedHelp.SupportAndContentRemovalTest do
|
|||
|
||||
assert {:ok, assigned_notice} =
|
||||
ContentRemoval.moderate(operator_scope, notice.id, %{
|
||||
"status" => "reviewing",
|
||||
"status" => "open",
|
||||
"assigned_to_id" => legal_assignee.id
|
||||
})
|
||||
|
||||
assert assigned_notice.assigned_to_id == legal_assignee.id
|
||||
|
||||
refute_enqueued(
|
||||
worker: ContentRemovalEmailWorker,
|
||||
args: %{"notice_id" => notice.id, "kind" => "update"}
|
||||
)
|
||||
end
|
||||
|
||||
test "content-removal mail is queued and internal assignment does not notify the submitter" do
|
||||
requester = user_fixture()
|
||||
assert_email_sent()
|
||||
legal_operator = staff_user_fixture([:legal], display_name: "Legal operator")
|
||||
assert_email_sent()
|
||||
operator_scope = user_scope_fixture(legal_operator)
|
||||
|
||||
assert {:ok, notice} =
|
||||
ContentRemoval.create_notice(user_scope_fixture(requester), :general, %{
|
||||
"category" => "privacy_violation",
|
||||
"submitter_name" => "Verified submitter",
|
||||
"relationship" => "self",
|
||||
"content_locations" => "https://example.test/requests/queued-legal-mail",
|
||||
"explanation" =>
|
||||
"This notice verifies bounded asynchronous legal-notice email delivery.",
|
||||
"electronic_signature" => "Verified submitter",
|
||||
"good_faith" => "true",
|
||||
"accurate_complete" => "true"
|
||||
})
|
||||
|
||||
assert_enqueued(
|
||||
worker: ContentRemovalEmailWorker,
|
||||
queue: :mail,
|
||||
args: %{"notice_id" => notice.id, "kind" => "received"}
|
||||
)
|
||||
|
||||
refute_receive {:email, _synchronous_received_email}, 50
|
||||
|
||||
assert :ok =
|
||||
perform_job(ContentRemovalEmailWorker, %{
|
||||
"notice_id" => notice.id,
|
||||
"kind" => "received"
|
||||
})
|
||||
|
||||
assert Repo.reload!(notice).acknowledgement_sent_at
|
||||
|
||||
assert_email_sent(fn email ->
|
||||
email.subject =~ notice.reference and email.text_body =~ "We received removal notice"
|
||||
end)
|
||||
|
||||
assert {:ok, assigned} =
|
||||
ContentRemoval.moderate(operator_scope, notice.id, %{
|
||||
"status" => "open",
|
||||
"assigned_to_id" => legal_operator.id
|
||||
})
|
||||
|
||||
assert assigned.assigned_to_id == legal_operator.id
|
||||
|
||||
refute_enqueued(
|
||||
worker: ContentRemovalEmailWorker,
|
||||
args: %{"notice_id" => notice.id, "kind" => "update"}
|
||||
)
|
||||
|
||||
assert {:ok, updated} =
|
||||
ContentRemoval.moderate(operator_scope, notice.id, %{
|
||||
"status" => "needs_information",
|
||||
"assigned_to_id" => legal_operator.id,
|
||||
"resolution_note" => "Please provide one exact public URL for review."
|
||||
})
|
||||
|
||||
assert updated.status == :needs_information
|
||||
|
||||
assert_enqueued(
|
||||
worker: ContentRemovalEmailWorker,
|
||||
queue: :mail,
|
||||
args: %{"notice_id" => notice.id, "kind" => "update"}
|
||||
)
|
||||
|
||||
assert :ok =
|
||||
perform_job(ContentRemovalEmailWorker, %{
|
||||
"notice_id" => notice.id,
|
||||
"kind" => "update"
|
||||
})
|
||||
|
||||
assert Repo.reload!(updated).decision_sent_at
|
||||
|
||||
assert_email_sent(fn email ->
|
||||
email.subject =~ "removal notice update" and
|
||||
email.text_body =~ "Please provide one exact public URL for review."
|
||||
end)
|
||||
end
|
||||
|
||||
test "requester replies reopen a finished case and preserve the full conversation history" do
|
||||
|
|
@ -515,6 +620,18 @@ defmodule WhoNeedHelp.SupportAndContentRemovalTest do
|
|||
assert notice.response_due_at
|
||||
assert is_nil(notice.contact_verified_at)
|
||||
|
||||
assert_enqueued(
|
||||
worker: ContentRemovalEmailWorker,
|
||||
queue: :mail,
|
||||
args: %{"notice_id" => notice.id, "kind" => "confirmation"}
|
||||
)
|
||||
|
||||
assert :ok =
|
||||
perform_job(ContentRemovalEmailWorker, %{
|
||||
"notice_id" => notice.id,
|
||||
"kind" => "confirmation"
|
||||
})
|
||||
|
||||
assert_email_sent(fn email ->
|
||||
email.subject =~ "Confirm Who Need Help removal notice" and
|
||||
email.text_body =~ notice.reference and
|
||||
|
|
@ -538,6 +655,12 @@ defmodule WhoNeedHelp.SupportAndContentRemovalTest do
|
|||
|
||||
assert verified.contact_verified_at
|
||||
|
||||
assert_enqueued(
|
||||
worker: ContentRemovalEmailWorker,
|
||||
queue: :mail,
|
||||
args: %{"notice_id" => notice.id, "kind" => "received"}
|
||||
)
|
||||
|
||||
assert Enum.map(ContentRemoval.paginate_for_staff(moderator_scope).entries, & &1.id) == [
|
||||
notice.id
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
defmodule WhoNeedHelpWeb.SupportControllerTest do
|
||||
use WhoNeedHelpWeb.ConnCase, async: false
|
||||
use Oban.Testing, repo: WhoNeedHelp.Repo
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
import WhoNeedHelp.AccountsFixtures
|
||||
import Swoosh.TestAssertions
|
||||
|
||||
alias WhoNeedHelp.Repo
|
||||
alias WhoNeedHelp.Mail.{ContentRemovalEmailWorker, SupportConfirmationWorker}
|
||||
alias WhoNeedHelp.Support.SupportRequest
|
||||
|
||||
describe "public support and removal intake" do
|
||||
|
|
@ -66,6 +68,18 @@ defmodule WhoNeedHelpWeb.SupportControllerTest do
|
|||
assert location =~ "/support/received?reference=SUP-"
|
||||
refute location =~ "token="
|
||||
|
||||
request = Repo.get_by!(SupportRequest, contact_email: "person@example.com")
|
||||
|
||||
assert_enqueued(
|
||||
worker: SupportConfirmationWorker,
|
||||
queue: :mail,
|
||||
args: %{"request_id" => request.id}
|
||||
)
|
||||
|
||||
refute_receive {:email, _synchronous_confirmation}, 50
|
||||
|
||||
assert :ok = perform_job(SupportConfirmationWorker, %{"request_id" => request.id})
|
||||
|
||||
assert_email_sent(fn email ->
|
||||
email.to == [{"", "person@example.com"}] and
|
||||
email.subject =~ "Confirm Who Need Help support request"
|
||||
|
|
@ -134,6 +148,26 @@ defmodule WhoNeedHelpWeb.SupportControllerTest do
|
|||
location = redirected_to(conn)
|
||||
assert location =~ "/legal/content-removal/received?reference=REM-"
|
||||
refute location =~ "token="
|
||||
|
||||
notice =
|
||||
Repo.get_by!(WhoNeedHelp.ContentRemoval.Notice,
|
||||
contact_email: "affected@example.com"
|
||||
)
|
||||
|
||||
assert_enqueued(
|
||||
worker: ContentRemovalEmailWorker,
|
||||
queue: :mail,
|
||||
args: %{"notice_id" => notice.id, "kind" => "confirmation"}
|
||||
)
|
||||
|
||||
refute_receive {:email, _synchronous_confirmation}, 50
|
||||
|
||||
assert :ok =
|
||||
perform_job(ContentRemovalEmailWorker, %{
|
||||
"notice_id" => notice.id,
|
||||
"kind" => "confirmation"
|
||||
})
|
||||
|
||||
assert_email_sent()
|
||||
end
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user