diff --git a/config/config.exs b/config/config.exs index 9bc80908..a34344e9 100644 --- a/config/config.exs +++ b/config/config.exs @@ -83,6 +83,11 @@ config :safira, Safira.Mailer, recv_timeout: :timer.minutes(1) ] +config :elixir, :time_zone_database, Tzdata.TimeZoneDatabase + +# Cron-like jobs +config :quantum, Safira.JobScheduler, timezone: "Europe/Lisbon" + # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{Mix.env()}.exs" diff --git a/lib/mix/tasks/gift.all_gold.badge.ex b/lib/mix/tasks/gift.all_gold.badge.ex deleted file mode 100644 index 08b16dc6..00000000 --- a/lib/mix/tasks/gift.all_gold.badge.ex +++ /dev/null @@ -1,53 +0,0 @@ -defmodule Mix.Tasks.Gift.AllGold.Badge do - @moduledoc """ - Task to give a badge to a list of users which have all company gold badges and exclusive badges - """ - use Mix.Task - - import Ecto.Query, warn: false - - alias Safira.Accounts.Attendee - alias Safira.Accounts.Company - - alias Safira.Contest - alias Safira.Contest.Badge - alias Safira.Contest.Redeem - - alias Safira.Repo - - def run(args) when length(args) == 1 do - Mix.Task.run("app.start") - - args |> List.first() |> String.to_integer() |> create() - end - - def run(_args) do - Mix.shell().error("You must provide a badge_id") - end - - def create(badge_id) do - companies = - Badge - |> where([b], b.type == 4) - |> join(:inner, [b], c in Company, on: c.badge_id == b.id) - |> where([b, c], c.sponsorship in ["Gold", "Exclusive"]) - |> Repo.aggregate(:count) - - attendees = - Badge - |> where([b], b.type == ^4) - |> join(:inner, [b], c in Company, on: c.badge_id == b.id) - |> where([b, c], c.sponsorship in ["Gold", "Exclusive"]) - |> join(:inner, [b, c], r in Redeem, on: b.id == r.badge_id) - |> join(:inner, [b, c, r], a in Attendee, on: a.id == r.attendee_id) - |> group_by([b, c, r, a], a.id) - |> having([b, c, r], count(r.id) == ^companies) - |> select([b, c, r, a], a.id) - |> Repo.all() - - attendees - |> Enum.each(fn id -> - Contest.create_redeem(%{attendee_id: id, badge_id: badge_id, staff_id: 1}, :admin) - end) - end -end diff --git a/lib/mix/tasks/gift.badge.full.participation.ex b/lib/mix/tasks/gift.badge.full.participation.ex deleted file mode 100644 index 2abf2874..00000000 --- a/lib/mix/tasks/gift.badge.full.participation.ex +++ /dev/null @@ -1,53 +0,0 @@ -defmodule Mix.Tasks.Gift.Badge.Full.Participation do - @moduledoc """ - Task to gift all badges to an attendee - """ - use Mix.Task - - alias Safira.Accounts - alias Safira.Contest - - def run(args) do - if Enum.empty?(args) do - Mix.shell().info("Needs badge_id.") - else - args |> List.first() |> String.to_integer() |> create() - end - end - - defp create(badge_id) do - Mix.Task.run("app.start") - - b1 = Contest.get_badge_name!("Dia 1") - b2 = Contest.get_badge_name!("Dia 2") - b3 = Contest.get_badge_name!("Dia 3") - b4 = Contest.get_badge_name!("Dia 4") - - lb = [b1, b2, b3, b4] - - Enum.each( - Accounts.list_active_attendees(), - fn a -> gift_badge(badge_id, lb, a.id) end - ) - end - - defp gift_badge(badge_id, list_badges, attendee_id) do - give = - Enum.map( - list_badges, - fn l -> !is_nil(Contest.get_keys_redeem(attendee_id, l.id)) end - ) - |> Enum.reduce(fn x, acc -> x && acc end) - - if give do - Contest.create_redeem( - %{ - attendee_id: attendee_id, - staff_id: 1, - badge_id: badge_id - }, - :admin - ) - end - end -end diff --git a/lib/mix/tasks/gift.badge.participation.ex b/lib/mix/tasks/gift.badge.participation.ex deleted file mode 100644 index e55fc4ad..00000000 --- a/lib/mix/tasks/gift.badge.participation.ex +++ /dev/null @@ -1,56 +0,0 @@ -defmodule Mix.Tasks.Gift.Badge.Participation do - @moduledoc """ - Task to give a badge to all attendees that completed at least a given number of days of the contest - """ - use Mix.Task - - alias Safira.Accounts - alias Safira.Contest - - def run(args) do - if length(args) != 2 do - Mix.shell().info("Needs badge_id and number of days") - else - badge_id = String.to_integer(Enum.at(args, 0)) - days = String.to_integer(Enum.at(args, 1)) - - create(badge_id, days) - end - end - - defp create(badge_id, days) do - Mix.Task.run("app.start") - - b1 = Contest.get_badge_name!("Dia 1") - b2 = Contest.get_badge_name!("Dia 2") - b3 = Contest.get_badge_name!("Dia 3") - b4 = Contest.get_badge_name!("Dia 4") - - lb = [b1, b2, b3, b4] - - Accounts.list_active_attendees() - |> Enum.map(fn a -> gift_badge(badge_id, lb, a.id, days) end) - end - - defp gift_badge(badge_id, list_badges, attendee_id, days) do - give = - Enum.map( - list_badges, - fn l -> !is_nil(Contest.get_keys_redeem(attendee_id, l.id)) end - ) - |> Enum.filter(fn x -> x end) - |> Enum.count() - |> then(fn x -> x >= days end) - - if give do - Contest.create_redeem( - %{ - attendee_id: attendee_id, - staff_id: 1, - badge_id: badge_id - }, - :admin - ) - end - end -end diff --git a/lib/mix/tasks/gift.checkpoint.badge.ex b/lib/mix/tasks/gift.checkpoint.badge.ex deleted file mode 100644 index 91b4893f..00000000 --- a/lib/mix/tasks/gift.checkpoint.badge.ex +++ /dev/null @@ -1,112 +0,0 @@ -defmodule Mix.Tasks.Gift.Company.Checkpoint.Badge do - @shortdoc "Gives checkpoint badge to attendees that reach that checkpoint" - - @moduledoc """ - This task needs to receive: - - badge_id: Badge's ID to give - - badge_count: Number of Badges thar an attendee must have - to receive the checkpoint basge - - entries: Number of entries that an attendee receives for - having this badge - - badge_type: type of badge to confirm - """ - use Mix.Task - - import Ecto.Query, warn: false - - alias Ecto.Multi - - alias Safira.Accounts.Attendee - alias Safira.Contest.Badge - alias Safira.Contest.Redeem - - alias Safira.Repo - - def run(args) do - if length(args) != 4 do - Mix.shell().info("Needs to receive badge_id, badge_count, entries and badge_type.") - else - args |> create() - end - end - - defp create(args) do - Mix.Task.run("app.start") - - args - |> validate_args() - |> map_args() - |> gift() - end - - defp validate_args(args) do - args - |> Enum.map(fn x -> Integer.parse(x) |> elem(0) end) - rescue - ArgumentError -> - Mix.shell().info("All arguments should be integers") - end - - defp map_args(args) do - %{ - badge_id: Enum.at(args, 0), - badge_count: Enum.at(args, 1), - entries: Enum.at(args, 2), - badge_type: Enum.at(args, 3) - } - end - - defp gift(args) do - case Repo.get(Badge, Map.get(args, :badge_id)) do - %Badge{} = badge -> - args = Map.put(args, :badge, badge) - attendees = get_attendees_company_badges(args) - give_checkpoint_badge(args, attendees) - - nil -> - Mix.shell().error("Badge_id needs to be valid.") - end - end - - defp get_attendees_company_badges(args) do - Repo.all( - from a in Attendee, - join: r in Redeem, - on: a.id == r.attendee_id, - join: b in Badge, - on: r.badge_id == b.id, - where: b.type == ^Map.get(args, :badge_type), - preload: [badges: b] - ) - |> Enum.map(fn a -> Map.put(a, :badge_count, length(a.badges)) end) - |> Enum.filter(fn x -> x.badge_count >= Map.get(args, :badge_count) end) - end - - defp give_checkpoint_badge(args, attendees) do - attendees - |> Enum.each(fn a -> - %{ - attendee_id: a.id, - badge_id: Map.get(args, :badge_id), - staff_id: 1 - } - |> create_redeem(args, a, Map.get(args, :badge)) - end) - end - - defp create_redeem(redeem_attrs, args, attendee, badge) do - Multi.new() - |> Multi.insert(:redeem, Redeem.changeset(%Redeem{}, redeem_attrs, :admin)) - |> Multi.update( - :attendee, - Attendee.update_on_redeem_changeset( - attendee, - %{ - token_balance: attendee.token_balance + badge.tokens, - entries: attendee.entries + Map.get(args, :entries) - } - ) - ) - |> Repo.transaction() - end -end diff --git a/lib/mix/tasks/gift.checkpoint.badge.quantity.ex b/lib/mix/tasks/gift.checkpoint.badge.quantity.ex deleted file mode 100644 index 5f5586d8..00000000 --- a/lib/mix/tasks/gift.checkpoint.badge.quantity.ex +++ /dev/null @@ -1,109 +0,0 @@ -defmodule Mix.Tasks.Gift.Quantity.Checkpoint.Badge do - @shortdoc "Gives checkpoint badge to attendees that reach that checkpoint" - - @moduledoc """ - This task needs to receive: - - badge_id: Badge's ID to give - - badge_count: Number of Badges thar an attendee must have - to receive the checkpoint basge - - entries: Number of entries that an attendee receives for - having this badge - """ - use Mix.Task - - import Ecto.Query, warn: false - - alias Ecto.Multi - - alias Safira.Accounts.Attendee - alias Safira.Contest.Badge - alias Safira.Contest.Redeem - - alias Safira.Repo - - def run(args) do - if length(args) != 3 do - Mix.shell().info("Needs to receive badge_id, badge_count, entries.") - else - args |> create() - end - end - - defp create(args) do - Mix.Task.run("app.start") - - args - |> validate_args() - |> map_args() - |> gift() - end - - defp validate_args(args) do - args - |> Enum.map(fn x -> Integer.parse(x) |> elem(0) end) - rescue - ArgumentError -> - Mix.shell().info("All arguments should be integers") - end - - defp map_args(args) do - %{ - badge_id: Enum.at(args, 0), - badge_count: Enum.at(args, 1), - entries: Enum.at(args, 2) - } - end - - defp gift(args) do - case Repo.get(Badge, Map.get(args, :badge_id)) do - %Badge{} = badge -> - args = Map.put(args, :badge, badge) - attendees = get_attendees_company_badges(args) - give_checkpoint_badge(args, attendees) - - nil -> - Mix.shell().error("Badge_id needs to be valid.") - end - end - - defp get_attendees_company_badges(args) do - Repo.all( - from a in Attendee, - join: r in Redeem, - on: a.id == r.attendee_id, - join: b in Badge, - on: r.badge_id == b.id, - preload: [badges: b] - ) - |> Enum.map(fn a -> Map.put(a, :badge_count, length(a.badges)) end) - |> Enum.filter(fn x -> x.badge_count >= Map.get(args, :badge_count) end) - end - - defp give_checkpoint_badge(args, attendees) do - attendees - |> Enum.each(fn a -> - %{ - attendee_id: a.id, - badge_id: Map.get(args, :badge_id), - staff_id: 1 - } - |> create_redeem(args, a, Map.get(args, :badge)) - end) - end - - defp create_redeem(redeem_attrs, args, attendee, badge) do - Multi.new() - |> Multi.insert(:redeem, Redeem.changeset(%Redeem{}, redeem_attrs, :admin)) - |> Multi.update( - :attendee, - Attendee.update_on_redeem_changeset( - attendee, - %{ - token_balance: attendee.token_balance + badge.tokens, - entries: attendee.entries + Map.get(args, :entries) - } - ) - ) - |> Repo.transaction() - end -end diff --git a/lib/mix/tasks/gift.checkpoint.badge.with.redeemable.ex b/lib/mix/tasks/gift.checkpoint.badge.with.redeemable.ex deleted file mode 100644 index cd976dd6..00000000 --- a/lib/mix/tasks/gift.checkpoint.badge.with.redeemable.ex +++ /dev/null @@ -1,128 +0,0 @@ -defmodule Mix.Tasks.Gift.Company.Checkpoint.Badge.With.Redeemable do - @shortdoc "Gives checkpoint badge to attendees that reach that checkpoint" - - @moduledoc """ - This task needs to receive: - - badge_id: Badge's ID to give - - badge_count: Number of Badges thar an attendee must have - to receive the checkpoint basge - - entries: Number of entries that an attendee receives for - having this badge - - badge_type: type of badge to confirm - - redeemable_id: Redeemable's ID that is associated with the given badge - """ - use Mix.Task - - import Ecto.Query, warn: false - - alias Ecto.Multi - - alias Safira.Accounts.Attendee - alias Safira.Contest.Badge - alias Safira.Contest.Redeem - - alias Safira.Repo - alias Safira.Store - - def run(args) do - if length(args) != 5 do - Mix.shell().info( - "Needs to receive badge_id, badge_count, entries ,badge_type and redeemable_id." - ) - else - create(args) - end - end - - defp create(args) do - Mix.Task.run("app.start") - - args - |> validate_args() - |> map_args() - |> gift_redeemable_badge() - end - - defp validate_args(args) do - args - |> Enum.map(fn x -> Integer.parse(x) |> elem(0) end) - rescue - ArgumentError -> - Mix.shell().info("All arguments should be integers") - end - - defp map_args(args) do - %{ - badge_id: Enum.at(args, 0), - badge_count: Enum.at(args, 1), - entries: Enum.at(args, 2), - badge_type: Enum.at(args, 3), - redeemable_id: Enum.at(args, 4) - } - end - - defp gift_redeemable_badge(args) do - case Repo.get(Badge, Map.get(args, :badge_id)) do - %Badge{} = badge -> - args = Map.put(args, :badge, badge) - attendees = get_attendees_company_badges(args) - give_checkpoint_badge(args, attendees) - give_checkpoint_redeemable(args, attendees) - - nil -> - Mix.shell().error("Badge_id needs to be valid.") - end - end - - defp get_attendees_company_badges(args) do - Repo.all( - from a in Attendee, - join: r in Redeem, - on: a.id == r.attendee_id, - join: b in Badge, - on: r.badge_id == b.id, - where: b.type == ^Map.get(args, :badge_type), - preload: [badges: b] - ) - |> Enum.map(fn a -> Map.put(a, :badge_count, length(a.badges)) end) - |> Enum.filter(fn x -> x.badge_count >= Map.get(args, :badge_count) end) - end - - defp give_checkpoint_badge(args, attendees) do - attendees - |> Enum.each(fn a -> - %{ - attendee_id: a.id, - badge_id: Map.get(args, :badge_id), - staff_id: 1 - } - |> create_redeem(args, a, Map.get(args, :badge)) - end) - end - - defp create_redeem(redeem_attrs, args, attendee, badge) do - Multi.new() - |> Multi.insert(:redeem, Redeem.changeset(%Redeem{}, redeem_attrs, :admin)) - |> Multi.update( - :attendee, - Attendee.update_on_redeem_changeset( - attendee, - %{ - token_balance: attendee.token_balance + badge.tokens, - entries: attendee.entries + Map.get(args, :entries) - } - ) - ) - |> Repo.transaction() - end - - defp give_checkpoint_redeemable(args, attendees) do - attendees - |> Enum.each(fn a -> - Store.buy_redeemable(Map.get(args, :redeemable_id), a) - end) - - # this needs to return args - args - end -end diff --git a/lib/mix/tasks/gift.daily.badge.ex b/lib/mix/tasks/gift.daily.badge.ex deleted file mode 100644 index cda277b5..00000000 --- a/lib/mix/tasks/gift.daily.badge.ex +++ /dev/null @@ -1,53 +0,0 @@ -defmodule Mix.Tasks.Gift.Daily.Badge do - @moduledoc """ - Task to gift the daily badge to all attendees - """ - use Mix.Task - - import Ecto.Query, warn: false - - alias Safira.Accounts.Attendee - - alias Safira.Contest - alias Safira.Contest.Badge - alias Safira.Contest.Redeem - - alias Safira.Repo - - # Expects a date in yyyy-mm-dd format - def run(args) do - Mix.Task.run("app.start") - - if length(args) != 2 do - Mix.shell().info("Needs to receive badge_id and the date of the day.") - else - args |> create() - end - end - - def create(args) do - {badge_id, date} = {args |> Enum.at(0), args |> Enum.at(1) |> Date.from_iso8601!()} - - Repo.all( - from a in Attendee, - join: r in Redeem, - on: a.id == r.attendee_id, - join: b in Badge, - on: r.badge_id == b.id, - where: not is_nil(a.user_id) and fragment("?::date", r.inserted_at) == ^date, - preload: [badges: b] - ) - |> Enum.map(fn a -> Map.put(a, :badge_count, length(a.badges)) end) - |> Enum.filter(fn a -> a.badge_count > 0 end) - |> Enum.each(fn a -> - Contest.create_redeem( - %{ - attendee_id: a.id, - staff_id: 1, - badge_id: badge_id - }, - :admin - ) - end) - end -end diff --git a/lib/safira/application.ex b/lib/safira/application.ex index a04372f1..52577f0e 100644 --- a/lib/safira/application.ex +++ b/lib/safira/application.ex @@ -13,7 +13,9 @@ defmodule Safira.Application do # Start the PubSub system {Phoenix.PubSub, name: Safira.PubSub}, # Start the endpoint when the application starts - SafiraWeb.Endpoint + SafiraWeb.Endpoint, + # Start the cron-like job scheduler system + Safira.JobScheduler # Starts a worker by calling: Safira.Worker.start_link(arg) # {Safira.Worker, arg}, ] diff --git a/lib/safira/job_scheduler.ex b/lib/safira/job_scheduler.ex new file mode 100644 index 00000000..ee5cbd84 --- /dev/null +++ b/lib/safira/job_scheduler.ex @@ -0,0 +1,64 @@ +defmodule Safira.JobScheduler do + @moduledoc false + use Quantum, otp_app: :safira + + import Crontab.CronExpression + + alias Safira.Jobs + + def init(opts) do + Keyword.put(opts, :jobs, load_jobs()) + end + + # FIXME: These are only examples. Should be updated to the real jobs, with the correct parameters + # and should be set to active. + defp load_jobs do + [ + create_job( + :daily_badge, + ~e[0 * * * *], + {Jobs.DailyBadge, :run, [123, "2023-12-01"]}, + :inactive + ), + create_job(:all_gold_badge, ~e[5 * * * *], {Jobs.AllGoldBadge, :run, [123]}, :inactive), + create_job( + :full_participation_badge, + ~e[10 * * * *], + {Jobs.FullParticipationBadge, :run, [123]}, + :inactive + ), + create_job( + :participate_in_two_days, + ~e[15 * * * *], + {Jobs.ParticipationBadge, :run, [123, 2]}, + :inactive + ), + create_job( + :redeem_fifty_badges, + ~e[25 * * * *], + {Jobs.CheckpointBadge, :run, [123, 50, 0]}, + :inactive + ), + create_job( + :attend_three_workshops, + ~e[30 * * * *], + {Jobs.CheckpointBadge, :run, [123, 3, 7]}, + :inactive + ), + create_job( + :visit_twenty_booths, + ~e[20 * * * *], + {Jobs.CheckpointBadgeWithRedeemable, :run, [123, 20, 4, 30, 456]}, + :inactive + ) + ] + end + + defp create_job(name, schedule, task, state \\ :active) do + Safira.JobScheduler.new_job() + |> Quantum.Job.set_name(name) + |> Quantum.Job.set_schedule(schedule) + |> Quantum.Job.set_task(task) + |> Quantum.Job.set_state(state) + end +end diff --git a/lib/safira/jobs/all_gold_badge.ex b/lib/safira/jobs/all_gold_badge.ex new file mode 100644 index 00000000..7dc1bd6a --- /dev/null +++ b/lib/safira/jobs/all_gold_badge.ex @@ -0,0 +1,51 @@ +defmodule Safira.Jobs.AllGoldBadge do + @moduledoc """ + Job to gift the all gold badge to all eligible attendees. + Eligible attendees are those that visited all gold and exclusive sponsors. + + Receives the badge_id. + """ + import Ecto.Query, warn: false + + alias Safira.Accounts.{Attendee, Company} + alias Safira.Contest + alias Safira.Contest.{Badge, Redeem} + alias Safira.Repo + + @spec run(integer()) :: :ok + def run(badge_id) do + list_eligible_attendees() + |> Enum.each(&create_redeem(&1.id, badge_id)) + end + + defp list_eligible_attendees do + companies_count = + Badge + |> where([b], b.type == ^4) + |> join(:inner, [b], c in Company, on: c.badge_id == b.id) + |> where([b, c], c.sponsorship in ["Gold", "Exclusive"]) + |> Repo.aggregate(:count) + + Badge + |> where([b], b.type == ^4) + |> join(:inner, [b], c in Company, on: c.badge_id == b.id) + |> where([b, c], c.sponsorship in ["Gold", "Exclusive"]) + |> join(:inner, [b, c], r in Redeem, on: b.id == r.badge_id) + |> join(:inner, [b, c, r], a in Attendee, on: a.id == r.attendee_id) + |> group_by([b, c, r, a], a.id) + |> having([b, c, r], count(r.id) == ^companies_count) + |> select([b, c, r, a], a.id) + |> Repo.all() + end + + defp create_redeem(attendee_id, badge_id) do + Contest.create_redeem( + %{ + attendee_id: attendee_id, + staff_id: 1, + badge_id: badge_id + }, + :admin + ) + end +end diff --git a/lib/safira/jobs/checkpoint_badge.ex b/lib/safira/jobs/checkpoint_badge.ex new file mode 100644 index 00000000..725dda13 --- /dev/null +++ b/lib/safira/jobs/checkpoint_badge.ex @@ -0,0 +1,73 @@ +defmodule Safira.Jobs.CheckpointBadge do + @moduledoc """ + Job to gift a badge to all eligible attendees. + Eligible attendees are those that have at least the required number of badges. + If a badge_type is provided, only attendees with at least the required number of badges of that type will be eligible. + + Receives the badge_id, badge_count, an optional badge_type and a number of entries to the final draw. + """ + import Ecto.Query, warn: false + + alias Ecto.Multi + alias Safira.Accounts.Attendee + alias Safira.Contest.Badge + alias Safira.Contest.Redeem + alias Safira.Repo + + @spec run(integer(), integer(), integer() | nil, integer()) :: :ok + def run(badge_id, badge_count, badge_type \\ nil, entries) do + badge = Repo.get(Badge, badge_id) + + case badge_type do + nil -> + list_eligible_attendees(badge_count) + |> Enum.each(&create_redeem(&1, badge, entries)) + + _ -> + list_eligible_attendees_with_badge_type(badge_count, badge_type) + |> Enum.each(&create_redeem(&1, badge, entries)) + end + end + + defp list_eligible_attendees(badge_count) do + Attendee + |> join(:inner, [a], r in Redeem, on: a.id == r.attendee_id) + |> join(:inner, [a, r], b in Badge, on: r.badge_id == b.id) + |> preload([a, r, b], badges: b) + |> Repo.all() + |> Enum.map(fn a -> Map.put(a, :badge_count, length(a.badges)) end) + |> Enum.filter(fn a -> a.badge_count >= badge_count end) + end + + defp list_eligible_attendees_with_badge_type(badge_count, badge_type) do + Attendee + |> join(:inner, [a], r in Redeem, on: a.id == r.attendee_id) + |> join(:inner, [a, r], b in Badge, on: r.badge_id == b.id) + |> where([a, r, b], b.type == ^badge_type) + |> preload([a, r, b], badges: b) + |> Repo.all() + |> Enum.map(fn a -> Map.put(a, :badge_count, length(a.badges)) end) + |> Enum.filter(fn a -> a.badge_count >= badge_count end) + end + + defp create_redeem(attendee, badge, entries) do + redeem_attrs = %{ + attendee_id: attendee.id, + badge_id: badge.id, + staff_id: 1 + } + + Multi.new() + |> Multi.insert(:redeem, Redeem.changeset(%Redeem{}, redeem_attrs, :admin)) + |> Multi.update( + :attendee, + Attendee.update_on_redeem_changeset( + attendee, + %{ + token_balance: attendee.token_balance + badge.tokens, + entries: attendee.entries + entries + } + ) + ) + end +end diff --git a/lib/safira/jobs/checkpoint_badge_with_redeemable.ex b/lib/safira/jobs/checkpoint_badge_with_redeemable.ex new file mode 100644 index 00000000..62697ff0 --- /dev/null +++ b/lib/safira/jobs/checkpoint_badge_with_redeemable.ex @@ -0,0 +1,61 @@ +defmodule Safira.Jobs.CheckpointBadgeWithRedeemable do + @moduledoc """ + Job to gift a badge and a redeemable to all eligible attendees. + Eligible attendees are those that have at least the required number of badges from a specific type. + + Receives the badge_id, badge_count, badge_type, number of entries to the final draw and a redeemable_id. + """ + import Ecto.Query, warn: false + + alias Ecto.Multi + alias Safira.Accounts.Attendee + alias Safira.Contest.Badge + alias Safira.Contest.Redeem + alias Safira.Repo + alias Safira.Store + + @spec run(integer(), integer(), integer(), integer(), integer()) :: :ok + def run(badge_id, badge_count, badge_type, entries, redeemable_id) do + attendees = list_eligible_attendees(badge_count, badge_type) + badge = Repo.get(Badge, badge_id) + + Enum.each(attendees, &create_redeem(&1, badge, entries)) + Enum.each(attendees, &gift_redeemable(&1, redeemable_id)) + end + + defp list_eligible_attendees(badge_count, badge_type) do + Attendee + |> join(:inner, [a], r in Redeem, on: a.id == r.attendee_id) + |> join(:inner, [a, r], b in Badge, on: r.badge_id == b.id) + |> where([a, r, b], b.type == ^badge_type) + |> preload([a, r, b], badges: b) + |> Repo.all() + |> Enum.map(fn a -> Map.put(a, :badge_count, length(a.badges)) end) + |> Enum.filter(fn a -> a.badge_count >= badge_count end) + end + + defp create_redeem(attendee, badge, entries) do + redeem_attrs = %{ + attendee_id: attendee.id, + badge_id: badge.id, + staff_id: 1 + } + + Multi.new() + |> Multi.insert(:redeem, Redeem.changeset(%Redeem{}, redeem_attrs, :admin)) + |> Multi.update( + :attendee, + Attendee.update_on_redeem_changeset( + attendee, + %{ + token_balance: attendee.token_balance + badge.tokens, + entries: attendee.entries + entries + } + ) + ) + end + + defp gift_redeemable(attendee, redeemable_id) do + Store.buy_redeemable(redeemable_id, attendee) + end +end diff --git a/lib/safira/jobs/daily_badge.ex b/lib/safira/jobs/daily_badge.ex new file mode 100644 index 00000000..580e1f49 --- /dev/null +++ b/lib/safira/jobs/daily_badge.ex @@ -0,0 +1,46 @@ +defmodule Safira.Jobs.DailyBadge do + @moduledoc """ + Job to gift the daily badge to all eligible attendees. + Eligible attendees are those that have at least one redeem on the given date. + + Receives the badge_id and the date of the day, in yyyy-mm-dd format. + """ + import Ecto.Query, warn: false + + require Logger + + alias Safira.Accounts.Attendee + alias Safira.Contest + alias Safira.Contest.{Badge, Redeem} + alias Safira.Repo + + @spec run(integer(), String.t()) :: :ok | no_return() + def run(badge_id, date) do + date = date |> Date.from_iso8601!() + + list_eligible_attendees(date) + |> Enum.each(&create_redeem(&1.id, badge_id)) + end + + defp list_eligible_attendees(date) do + Attendee + |> join(:inner, [a], r in Redeem, on: a.id == r.attendee_id) + |> join(:inner, [a, r], b in Badge, on: r.badge_id == b.id) + |> where([a, r, b], not is_nil(a.user_id) and fragment("?::date", r.inserted_at) == ^date) + |> preload([a, r, b], badges: b) + |> Repo.all() + |> Enum.map(&Map.put(&1, :badge_count, length(&1.badges))) + |> Enum.filter(&(&1.badge_count > 0)) + end + + defp create_redeem(attendee_id, badge_id) do + Contest.create_redeem( + %{ + attendee_id: attendee_id, + staff_id: 1, + badge_id: badge_id + }, + :admin + ) + end +end diff --git a/lib/safira/jobs/full_participation_badge.ex b/lib/safira/jobs/full_participation_badge.ex new file mode 100644 index 00000000..e720ec42 --- /dev/null +++ b/lib/safira/jobs/full_participation_badge.ex @@ -0,0 +1,55 @@ +defmodule Safira.Jobs.FullParticipationBadge do + @moduledoc """ + Job to gift the full participation badge to all eligible attendees. + Eligible attendees are those that completed all days of the contest. + + Receives the badge_id. + """ + import Ecto.Query, warn: false + + alias Safira.Accounts + alias Safira.Contest + + @spec run(integer()) :: :ok + def run(badge_id) do + list = [ + get_badge_by_name("Dia 1"), + get_badge_by_name("Dia 2"), + get_badge_by_name("Dia 3"), + get_badge_by_name("Dia 4") + ] + + Accounts.list_active_attendees() + |> Enum.each(&maybe_gift_badge(&1.id, badge_id, list)) + end + + defp maybe_gift_badge(attendee_id, badge_id, list) do + give? = + Enum.map(list, &attendee_has_badge?(&1, attendee_id)) + # Checks that all values are true (attendee redeemed all daily badges) + |> Enum.reduce(fn x, acc -> x && acc end) + + if give? do + create_redeem(attendee_id, badge_id) + end + end + + defp attendee_has_badge?(badge, attendee_id) do + !is_nil(Contest.get_keys_redeem(attendee_id, badge.id)) + end + + defp create_redeem(attendee_id, badge_id) do + Contest.create_redeem( + %{ + attendee_id: attendee_id, + staff_id: 1, + badge_id: badge_id + }, + :admin + ) + end + + defp get_badge_by_name(name) do + Contest.get_badge_name!(name) + end +end diff --git a/lib/safira/jobs/participation_badge.ex b/lib/safira/jobs/participation_badge.ex new file mode 100644 index 00000000..4f2d5041 --- /dev/null +++ b/lib/safira/jobs/participation_badge.ex @@ -0,0 +1,58 @@ +defmodule Safira.Jobs.ParticipationBadge do + @moduledoc """ + Job to gift the participation badge to all eligible attendees. + Eligible attendees are those that completed at least a given number of days of the contest. + + Receives the badge_id and the number of days of participation. + """ + import Ecto.Query, warn: false + + alias Safira.Accounts + alias Safira.Contest + + @spec run(integer(), integer()) :: :ok + def run(badge_id, days) do + list = [ + get_badge_by_name("Dia 1"), + get_badge_by_name("Dia 2"), + get_badge_by_name("Dia 3"), + get_badge_by_name("Dia 4") + ] + + Accounts.list_active_attendees() + |> Enum.each(&maybe_gift_badge(&1.id, badge_id, list, days)) + end + + defp maybe_gift_badge(attendee_id, badge_id, list, days) do + give? = + Enum.map(list, &attendee_has_badge?(&1, attendee_id)) + # Filters for true values (badges redeemed by the attendee) + # and then checks if their count is greater than or equal to the number of days + |> Enum.filter(fn x -> x end) + |> Enum.count() + |> then(fn x -> x >= days end) + + if give? do + create_redeem(attendee_id, badge_id) + end + end + + defp attendee_has_badge?(badge, attendee_id) do + !is_nil(Contest.get_keys_redeem(attendee_id, badge.id)) + end + + defp create_redeem(attendee_id, badge_id) do + Contest.create_redeem( + %{ + attendee_id: attendee_id, + staff_id: 1, + badge_id: badge_id + }, + :admin + ) + end + + defp get_badge_by_name(name) do + Contest.get_badge_name!(name) + end +end diff --git a/mix.exs b/mix.exs index 6a971abb..7686aa96 100644 --- a/mix.exs +++ b/mix.exs @@ -77,7 +77,9 @@ defmodule Safira.MixProject do {:bureaucrat, "~> 0.2.9", only: :test}, {:credo, "~> 1.6.7", only: [:dev, :test], runtime: false}, {:http_stream, "~> 1.0.0", git: "https://github.com/coders51/http_stream"}, - {:zstream, "~> 0.6"} + {:zstream, "~> 0.6"}, + {:quantum, "~> 3.0"}, + {:tzdata, "~> 1.1"} ] end diff --git a/mix.lock b/mix.lock index 704290a9..a36e3a63 100644 --- a/mix.lock +++ b/mix.lock @@ -14,6 +14,7 @@ "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"}, "credo": {:hex, :credo, "1.6.7", "323f5734350fd23a456f2688b9430e7d517afb313fbd38671b8a4449798a7854", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "41e110bfb007f7eda7f897c10bf019ceab9a0b269ce79f015d54b0dcf4fc7dd3"}, + "crontab": {:hex, :crontab, "1.1.13", "3bad04f050b9f7f1c237809e42223999c150656a6b2afbbfef597d56df2144c5", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "d67441bec989640e3afb94e123f45a2bc42d76e02988c9613885dc3d01cf7085"}, "db_connection": {:hex, :db_connection, "2.5.0", "bb6d4f30d35ded97b29fe80d8bd6f928a1912ca1ff110831edcd238a1973652c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c92d5ba26cd69ead1ff7582dbb860adeedfff39774105a4f1c92cbb654b55aa2"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "ecto": {:hex, :ecto, "3.10.3", "eb2ae2eecd210b4eb8bece1217b297ad4ff824b4384c0e3fdd28aaf96edd6135", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "44bec74e2364d491d70f7e42cd0d690922659d329f6465e89feb8a34e8cd3433"}, @@ -25,6 +26,7 @@ "faker": {:hex, :faker, "0.17.0", "671019d0652f63aefd8723b72167ecdb284baf7d47ad3a82a15e9b8a6df5d1fa", [:mix], [], "hexpm", "a7d4ad84a93fd25c5f5303510753789fc2433ff241bf3b4144d3f6f291658a6a"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, "filtrex": {:hex, :filtrex, "0.4.3", "d59e496d385b19df7e3a613ad74deca4ac5ef1666352e9e435e8442fc9ae4c70", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:timex, "~> 3.1", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "a374999d00174c3a6c617def9c9d199a2bc5928d49a227d1de622ccbadbff1d0"}, + "gen_stage": {:hex, :gen_stage, "1.2.1", "19d8b5e9a5996d813b8245338a28246307fd8b9c99d1237de199d21efc4c76a1", [:mix], [], "hexpm", "83e8be657fa05b992ffa6ac1e3af6d57aa50aace8f691fcf696ff02f8335b001"}, "gettext": {:hex, :gettext, "0.20.0", "75ad71de05f2ef56991dbae224d35c68b098dd0e26918def5bb45591d5c8d429", [:mix], [], "hexpm", "1c03b177435e93a47441d7f681a7040bd2a816ece9e2666d1c9001035121eb3d"}, "guardian": {:hex, :guardian, "2.3.1", "2b2d78dc399a7df182d739ddc0e566d88723299bfac20be36255e2d052fd215d", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "bbe241f9ca1b09fad916ad42d6049d2600bbc688aba5b3c4a6c82592a54274c3"}, "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~> 2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, @@ -56,6 +58,7 @@ "pow": {:hex, :pow, "1.0.31", "5787b9520af180535f05cef4e04ad64f3c1be3b6d407ef9b1a64e9eba5bc66fb", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.3.0 and < 1.8.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, ">= 2.0.0 and < 4.0.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, ">= 0.18.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, ">= 1.5.0 and < 2.0.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "988d9af08b7380c5b95fc65d1325642dd96defde3f0a7a103c52d5e2390d888f"}, "qr_code_svg": {:git, "https://github.com/ondrej-tucek/qr-code-svg", "febda672fcde25cc071ee84b556f650237751361", [tag: "v1.2.0"]}, "qrcode": {:git, "https://github.com/sunboshan/qrcode.git", "f6855c378b0d11e2aef6fb8c071280f367350643", [tag: nil]}, + "quantum": {:hex, :quantum, "3.5.0", "8d2c5ba68c55991e8975aca368e3ab844ba01f4b87c4185a7403280e2c99cf34", [:mix], [{:crontab, "~> 1.1", [hex: :crontab, repo: "hexpm", optional: false]}, {:gen_stage, "~> 0.14 or ~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_registry, "~> 0.2", [hex: :telemetry_registry, repo: "hexpm", optional: false]}], "hexpm", "cab737d1d9779f43cb1d701f46dd05ea58146fd96238d91c9e0da662c1982bb6"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "scrivener": {:hex, :scrivener, "2.7.2", "1d913c965ec352650a7f864ad7fd8d80462f76a32f33d57d1e48bc5e9d40aba2", [:mix], [], "hexpm", "7866a0ec4d40274efbee1db8bead13a995ea4926ecd8203345af8f90d2b620d9"}, "scrivener_ecto": {:hex, :scrivener_ecto, "2.7.0", "cf64b8cb8a96cd131cdbcecf64e7fd395e21aaa1cb0236c42a7c2e34b0dca580", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:scrivener, "~> 2.4", [hex: :scrivener, repo: "hexpm", optional: false]}], "hexpm", "e809f171687806b0031129034352f5ae44849720c48dd839200adeaf0ac3e260"}, @@ -63,6 +66,7 @@ "sweet_xml": {:hex, :sweet_xml, "0.7.3", "debb256781c75ff6a8c5cbf7981146312b66f044a2898f453709a53e5031b45b", [:mix], [], "hexpm", "e110c867a1b3fe74bfc7dd9893aa851f0eed5518d0d7cad76d7baafd30e4f5ba"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"}, + "telemetry_registry": {:hex, :telemetry_registry, "0.3.1", "14a3319a7d9027bdbff7ebcacf1a438f5f5c903057b93aee484cca26f05bdcba", [:mix, :rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6d0ca77b691cf854ed074b459a93b87f4c7f5512f8f7743c635ca83da81f939e"}, "timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"}, "torch": {:hex, :torch, "4.3.2", "736f4ff91a2c4ceab8fceff06e5a9c740cf86e40e6171cb594dc4252f82f8966", [:mix], [{:filtrex, "~> 0.4.1", [hex: :filtrex, repo: "hexpm", optional: false]}, {:gettext, "~> 0.16", [hex: :gettext, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.6.0 and < 1.8.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:scrivener_ecto, "~> 2.7.0", [hex: :scrivener_ecto, repo: "hexpm", optional: false]}], "hexpm", "56dbb223418125361e8d5c827e8d685701d4eb0a2dfee6ea9ddfe319ddac8ce6"}, "tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"},