defmodule Microwaveprop.Radio.Contact do @moduledoc false use Ecto.Schema import Ecto.Changeset alias Microwaveprop.Radio.Maidenhead @primary_key {:id, :binary_id, autogenerate: true} @foreign_key_type :binary_id schema "contacts" do field :station1, :string field :station2, :string field :qso_timestamp, :utc_datetime field :grid1, :string field :grid2, :string field :pos1, :map field :pos2, :map field :mode, :string field :band, :decimal field :distance_km, :decimal field :hrrr_status, Ecto.Enum, values: [:pending, :queued, :processing, :complete, :failed, :unavailable], default: :pending field :weather_status, Ecto.Enum, values: [:pending, :queued, :processing, :complete, :failed, :unavailable], default: :pending field :terrain_status, Ecto.Enum, values: [:pending, :queued, :processing, :complete, :failed, :unavailable], default: :pending field :iemre_status, Ecto.Enum, values: [:pending, :queued, :processing, :complete, :failed, :unavailable], default: :pending field :user_submitted, :boolean, default: false field :submitter_email, :string field :flagged_invalid, :boolean, default: false timestamps(type: :utc_datetime) end @required_fields ~w(station1 station2 qso_timestamp mode band)a @optional_fields ~w(grid1 grid2 pos1 pos2 distance_km)a def changeset(contact, attrs) do contact |> cast(attrs, @required_fields ++ @optional_fields) |> validate_required(@required_fields) end @submission_fields ~w(station1 station2 qso_timestamp mode band grid1 grid2 submitter_email)a @allowed_modes ~w(CW SSB FM FT8 FT4) @allowed_bands Enum.map(~w(1296 2304 3456 5760 10000 24000 47000 68000 75000 122000 134000 241000), &Decimal.new/1) def submission_changeset(contact, attrs) do contact |> cast(attrs, @submission_fields) |> validate_required(@submission_fields) |> sanitize_callsign(:station1) |> sanitize_callsign(:station2) |> sanitize_grid(:grid1) |> sanitize_grid(:grid2) |> validate_callsign(:station1) |> validate_callsign(:station2) |> validate_grid_format(:grid1) |> validate_grid_format(:grid2) |> validate_inclusion(:mode, @allowed_modes) |> validate_inclusion(:band, @allowed_bands) |> validate_format(:submitter_email, ~r/^[^\s<>]+@[^\s<>]+\.[^\s<>]+$/) |> validate_length(:submitter_email, max: 254) |> validate_length(:station1, max: 20) |> validate_length(:station2, max: 20) end # Strip whitespace and upcase callsigns defp sanitize_callsign(changeset, field) do case get_change(changeset, field) do nil -> changeset val -> put_change(changeset, field, val |> String.trim() |> String.upcase()) end end # Upcase grid squares (Maidenhead uses uppercase letters + digits) defp sanitize_grid(changeset, field) do case get_change(changeset, field) do nil -> changeset val -> put_change(changeset, field, val |> String.trim() |> String.upcase()) end end # Callsigns: letters, digits, and / only (e.g. W5XD, KG5CCI/P, VE3/W5XD) defp validate_callsign(changeset, field) do validate_format(changeset, field, ~r/^[A-Z0-9\/]+$/, message: "must contain only letters, digits, and /") end defp validate_grid_format(changeset, field) do validate_change(changeset, field, fn _, value -> if Maidenhead.valid?(value) do [] else [{field, "is not a valid Maidenhead grid square"}] end end) end end