習作: Rails: イベントの案内メール

イベントの開催日時の1日前に案内メールを送る。
イベントの開催日時を変更できる。

外観

環境

ruby 3.2.2
Rails 7.1.3.2
sqlite3 (1.7.3)
solid_queue (0.3.0)

リンク

https://note.com/usutani/n/n0cd86869ff3c
https://github.com/usutani/event_information_email

手順

準備

rails new event_information_email
cd event_information_email
bundle add solid_queue
bin/rails generate solid_queue:install
bin/rails db:migrate

config/environments/development.rb
config/environments/test.rb
config/environments/production.rb

config.active_job.queue_adapter = :solid_queue
config.solid_queue.use_skip_locked = false

config/puma.rb

plugin :solid_queue
bin/rails generate scaffold member email
bin/rails generate scaffold event name started_at:datetime
bin/rails generate scaffold notification job_id event:belongs_to member:belongs_to
bin/rails db:migrate
bin/rails generate job notification
bin/rails generate mailer member notify

モデル

app/models/notification.rb

class Notification < ApplicationRecord
  belongs_to :event
  belongs_to :member

  def self.create_or_update_and_notify(event:, member:)
    job_id = nil
    Notification.transaction do
      notification = Notification.transaction do
        Notification.find_or_create_by(event: event, member: member)
      end
      discard_job_by(notification.job_id)
      job_id = job_one_day_before(event.started_at).perform_later(notification).job_id
      notification.update!(job_id: job_id)
      notification
    end
  rescue
    discard_job_by(job_id)
    nil
  end

  def job
    Notification.find_job_by(job_id)
  end

  private
    def self.find_job_by(job_id)
      SolidQueue::Job.find_by(active_job_id: job_id)
    end

    def self.discard_job_by(job_id)
      find_job_by(job_id)&.discard
    end

    def self.job_one_day_before(started_at)
      NotificationJob.set(wait_until: started_at - 1.day)
    end
end

test/models/notification_test.rb

require "test_helper"

class NotificationTest < ActiveSupport::TestCase
  test "Create - create_or_update_and_notify" do
    started_at = Time.current.since(1.week)
    event = Event.create! name: "イベント1", started_at: started_at
    member = Member.create! email: "member1@example.com"

    assert_difference "Notification.count" do "Notificationは1件だけ生成されること"
      assert_difference "SolidQueue::Job.count" do "SolidQueue::Jobは1件だけ生成されること"
        notification = Notification.create_or_update_and_notify(event: event, member: member)
        assert notification, "生成に成功すること"
        notifications = Notification.where(event: event, member: member)
        assert_equal notification.event, event, "イベントは一致すること"
        assert_equal notification.member, member, "メンバーは一致すること"
        assert_in_delta started_at - 1.day, notification.job.scheduled_at, 1, "ジョブの開始は開始日時の1日前(誤差1秒)"
      end
    end
  end

  test "Update - create_or_update_and_notify" do
    started_at_old = Time.current.since(1.week)
    event = Event.create! name: "イベント1", started_at: started_at_old
    member = Member.create! email: "member1@example.com"
    Notification.create_or_update_and_notify(event: event, member: member)

    # イベントの開始日時を変更する
    started_at_new = Time.current
    event.update! started_at: started_at_new

    assert_no_difference "Notification.count" do "Notificationの件数は変わらないこと"
      assert_no_difference "SolidQueue::Job.count" do "SolidQueue::Jobの件数は変わらないこと"
        notification = Notification.create_or_update_and_notify(event: event, member: member)
        assert notification, "更新に成功すること"
        notifications = Notification.where(event: event, member: member)
        assert_equal notification.event, event, "イベントは一致すること"
        assert_equal notification.member, member, "メンバーは一致すること"
        assert_in_delta started_at_new - 1.day, notification.job.scheduled_at, 1, "ジョブの開始は変更した開始日時の1日前(誤差1秒)"
      end
    end
  end

  test "job" do
    started_at = Time.current.since(1.week)
    event = Event.create! name: "イベント1", started_at: started_at
    member = Member.create! email: "member1@example.com"

    assert_difference "Notification.count" do "Notificationは1件だけ生成されること"
      assert_difference "SolidQueue::Job.count" do "SolidQueue::Jobは1件だけ生成されること"
        Notification.create_or_update_and_notify(event: event, member: member)
        assert job_id = Notification.find_by(event: event, member: member).job_id, "ジョブIDが設定されていること"
        assert job = SolidQueue::Job.find_by(active_job_id: job_id), "ジョブが生成されていること"
        assert_in_delta started_at - 1.day, job.scheduled_at, 1, "ジョブの開始は開始日時の1日前(誤差1秒)"
      end
    end
  end
end

以上です。

この記事が気に入ったらサポートをしてみませんか?