Packages
phoenix_kit
1.7.71
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/mix/tasks/phoenix_kit.fix_missing_events.ex
defmodule Mix.Tasks.PhoenixKit.FixMissingEvents do
@moduledoc """
Finds and fixes email logs with missing bounce/complaint/reject events.
This task scans the email logs table for records that have a bounce/complaint/reject
status but are missing the corresponding EmailEvent record in the timeline.
## Usage
# Fix all missing events
mix phoenix_kit.fix_missing_events
# Dry run (show what would be fixed without making changes)
mix phoenix_kit.fix_missing_events --dry-run
# Fix specific log by UUID
mix phoenix_kit.fix_missing_events --log-uuid 018f1234-5678-7890-abcd-ef1234567890
## Options
* `--dry-run` - Show missing events without creating them
* `--log-uuid` - Fix specific log UUID only
* `--help` - Show this help
## Examples
# Find and fix all missing events
mix phoenix_kit.fix_missing_events
# Check what would be fixed
mix phoenix_kit.fix_missing_events --dry-run
# Fix specific email log
mix phoenix_kit.fix_missing_events --log-uuid 018f1234-5678-7890-abcd-ef1234567890
"""
use Mix.Task
import Ecto.Query
alias PhoenixKit.Modules.Emails
alias PhoenixKit.Modules.Emails.Log
alias PhoenixKit.Utils.Date, as: UtilsDate
@shortdoc "Fix email logs with missing bounce/complaint/reject events"
@switches [
dry_run: :boolean,
log_uuid: :string,
help: :boolean
]
@aliases [
d: :dry_run,
l: :log_uuid,
h: :help
]
@impl Mix.Task
def run(args) do
{opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
cond do
opts[:help] ->
show_help()
opts[:log_uuid] ->
Mix.Task.run("app.start")
fix_single_log(opts[:log_uuid], opts[:dry_run] || false)
true ->
Mix.Task.run("app.start")
fix_all_logs(opts[:dry_run] || false)
end
end
defp show_help do
Mix.shell().info(@moduledoc)
end
defp fix_single_log(log_uuid, dry_run) do
Mix.shell().info("\n=== Checking Log UUID #{log_uuid} ===\n")
log = repo().get_by!(Log, uuid: log_uuid) |> repo().preload(:events)
case find_missing_event_type(log) do
nil ->
Mix.shell().info("✅ Log #{log_uuid} has all required events")
event_type ->
Mix.shell().info("❌ Missing #{event_type} event")
if dry_run do
Mix.shell().info(" [DRY RUN] Would create #{event_type} event")
else
create_missing_event(log, event_type)
end
end
end
defp fix_all_logs(dry_run) do
Mix.shell().info("\n=== Scanning for Missing Events ===\n")
# Find logs with bounce status but no bounce event
bounced_logs =
repo().all(
from l in Log,
where: l.status in ["hard_bounced", "soft_bounced", "bounced"],
preload: :events
)
# Find logs with complaint status but no complaint event
complaint_logs =
repo().all(
from l in Log,
where: l.status == "complaint",
preload: :events
)
# Find logs with rejected status but no reject event
rejected_logs =
repo().all(
from l in Log,
where: l.status == "rejected",
preload: :events
)
all_logs = bounced_logs ++ complaint_logs ++ rejected_logs
missing_events =
Enum.filter(all_logs, fn log ->
find_missing_event_type(log) != nil
end)
if Enum.empty?(missing_events) do
Mix.shell().info("✅ No missing events found")
else
Mix.shell().info("Found #{length(missing_events)} logs with missing events:\n")
Enum.each(missing_events, fn log ->
event_type = find_missing_event_type(log)
Mix.shell().info(" Log #{log.id}: Missing #{event_type} event (status: #{log.status})")
if dry_run do
Mix.shell().info(" [DRY RUN] Would create #{event_type} event\n")
else
create_missing_event(log, event_type)
Mix.shell().info(" ✅ Created #{event_type} event\n")
end
end)
if dry_run do
Mix.shell().info("\nRun without --dry-run to fix these events")
else
Mix.shell().info("\n✅ Fixed #{length(missing_events)} missing events")
end
end
end
defp find_missing_event_type(log) do
event_types = Enum.map(log.events, & &1.event_type)
cond do
log.status in ["hard_bounced", "soft_bounced", "bounced"] and
"bounce" not in event_types ->
"bounce"
log.status == "complaint" and "complaint" not in event_types ->
"complaint"
log.status == "rejected" and "reject" not in event_types ->
"reject"
true ->
nil
end
end
defp create_missing_event(log, "bounce") do
bounce_type =
case log.status do
"hard_bounced" -> "hard"
"soft_bounced" -> "soft"
_ -> "hard"
end
event_attrs = %{
email_log_uuid: log.uuid,
event_type: "bounce",
occurred_at: log.bounced_at || UtilsDate.utc_now(),
bounce_type: bounce_type,
event_data: %{
bounceType: bounce_type,
timestamp: DateTime.to_iso8601(log.bounced_at || UtilsDate.utc_now()),
diagnosticCode: log.error_message
}
}
case Emails.create_event(event_attrs) do
{:ok, event} ->
Mix.shell().info(" ✅ Created bounce event (ID: #{event.id})")
{:error, changeset} ->
Mix.shell().error(" ❌ Failed to create event: #{inspect(changeset.errors)}")
end
end
defp create_missing_event(log, "complaint") do
event_attrs = %{
email_log_uuid: log.uuid,
event_type: "complaint",
occurred_at: log.complained_at || UtilsDate.utc_now(),
complaint_type: "abuse",
event_data: %{
complaintFeedbackType: "abuse",
timestamp: DateTime.to_iso8601(log.complained_at || UtilsDate.utc_now())
}
}
case Emails.create_event(event_attrs) do
{:ok, event} ->
Mix.shell().info(" ✅ Created complaint event (ID: #{event.id})")
{:error, changeset} ->
Mix.shell().error(" ❌ Failed to create event: #{inspect(changeset.errors)}")
end
end
defp create_missing_event(log, "reject") do
event_attrs = %{
email_log_uuid: log.uuid,
event_type: "reject",
occurred_at: log.rejected_at || UtilsDate.utc_now(),
reject_reason: log.error_message,
event_data: %{
reason: log.error_message,
timestamp: DateTime.to_iso8601(log.rejected_at || UtilsDate.utc_now())
}
}
case Emails.create_event(event_attrs) do
{:ok, event} ->
Mix.shell().info(" ✅ Created reject event (ID: #{event.id})")
{:error, changeset} ->
Mix.shell().error(" ❌ Failed to create event: #{inspect(changeset.errors)}")
end
end
defp repo do
PhoenixKit.RepoHelper.repo()
end
end