From 374f787da684256c2d9da075dcd70adce7dd206a Mon Sep 17 00:00:00 2001 From: "jonathan.kerr" <3410350+jonodrew@users.noreply.github.com> Date: Thu, 29 Jan 2026 11:53:01 +0000 Subject: [PATCH 1/6] Add Active Job adapter This starts us down the path of using Active Job, which is the preferred mechanism for jobs in Rails 7.1. Signed-off-by: jonathan.kerr <3410350+jonodrew@users.noreply.github.com> --- config/application.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/application.rb b/config/application.rb index a8af5e442..e84652355 100644 --- a/config/application.rb +++ b/config/application.rb @@ -33,6 +33,9 @@ class Application < Rails::Application config.active_record.belongs_to_required_by_default = true + # let's start using Active Job! + config.active_job.queue_adapter = :delayed_job + if ENV["RAILS_LOG_TO_STDOUT"].present? $stdout.sync = true config.rails_semantic_logger.add_file_appender = false From f44a78fd581475b2a683900c1a64d846b8b1e25f Mon Sep 17 00:00:00 2001 From: "jonathan.kerr" <3410350+jonodrew@users.noreply.github.com> Date: Thu, 29 Jan 2026 12:21:21 +0000 Subject: [PATCH 2/6] Add chaser email copy I've taken the copy from Kimberly and put it into HTML, as we do with the rest of our emails Signed-off-by: jonathan.kerr <3410350+jonodrew@users.noreply.github.com> --- .../three_month_chaser.html.haml | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 app/views/member_mailer/three_month_chaser.html.haml diff --git a/app/views/member_mailer/three_month_chaser.html.haml b/app/views/member_mailer/three_month_chaser.html.haml new file mode 100644 index 000000000..18221575e --- /dev/null +++ b/app/views/member_mailer/three_month_chaser.html.haml @@ -0,0 +1,21 @@ +%h1 Hi #{@member.name}, + +%p +We’ve noticed you haven’t been to a codebar workshop in a little while, and we just wanted to check in. We know life gets busy, but we’d love to understand how things are going for you and whether there’s anything we can do to make it easier or more valuable for you to join again. +%p +If you have a minute, could you please share your thoughts in this short form? 👉 https://forms.gle/tEETvC3zYP9mcLar7 + +%p +Or, if you’re thinking about coming back soon, we’ve got some great upcoming workshops and events you might like to join 👉https://codebar.io/events/ + +%p +Your feedback really helps us make codebar more welcoming and useful for everyone in our community 💜 + +%p +We’d love to see you again soon! + +%p +#{"-- "} +%br +Warmly, +The Codebar Team From fabb71efecbaccc0bb74e87f49d22c214b620adf Mon Sep 17 00:00:00 2001 From: "jonathan.kerr" <3410350+jonodrew@users.noreply.github.com> Date: Thu, 29 Jan 2026 17:01:49 +0000 Subject: [PATCH 3/6] Add EmailDelivery concern and test This adds an additional method to the MemberMailer class. It's currently called #chaser and it sends...the chaser email. In the next commit I'll call it from a Job, or maybe a Service that's called from a Job Signed-off-by: jonathan.kerr <3410350+jonodrew@users.noreply.github.com> --- app/mailers/concerns/email_delivery.rb | 19 +++++++++++++++++++ app/mailers/member_mailer.rb | 11 +++++++++++ app/models/member_email_delivery.rb | 3 +++ spec/mailers/member_mailer_spec.rb | 17 +++++++++++++++++ 4 files changed, 50 insertions(+) create mode 100644 app/mailers/concerns/email_delivery.rb create mode 100644 app/models/member_email_delivery.rb diff --git a/app/mailers/concerns/email_delivery.rb b/app/mailers/concerns/email_delivery.rb new file mode 100644 index 000000000..f39bf581a --- /dev/null +++ b/app/mailers/concerns/email_delivery.rb @@ -0,0 +1,19 @@ +module EmailDelivery + extend ActiveSupport::Concern + + private + + def log_sent_email + member = params[:member] + return unless member + + MemberEmailDelivery.create!( + member: member, + subject: mail.subject, + body: mail.body.to_s, + to: mail.to, + cc: mail.cc, + bcc: mail.bcc + ) + end +end diff --git a/app/mailers/member_mailer.rb b/app/mailers/member_mailer.rb index ebc5e4850..c1d5ee7a0 100644 --- a/app/mailers/member_mailer.rb +++ b/app/mailers/member_mailer.rb @@ -1,5 +1,16 @@ class MemberMailer < ApplicationMailer include EmailHeaderHelper + include EmailDelivery + + after_action :log_sent_email, only: [:chaser] + + def chaser + @member = params[:member] + subject = "It’s been a while, how are you doing? ♥️" + mail mail_args(@member, subject, 'hello@codebar.io', 'hello@codebar.io') do |format| + format.html {render 'three_month_chaser'} + end + end def welcome(member) if member.student? diff --git a/app/models/member_email_delivery.rb b/app/models/member_email_delivery.rb new file mode 100644 index 000000000..f6d836129 --- /dev/null +++ b/app/models/member_email_delivery.rb @@ -0,0 +1,3 @@ +class MemberEmailDelivery < ApplicationRecord + belongs_to :member, polymorphic: true, optional: true +end diff --git a/spec/mailers/member_mailer_spec.rb b/spec/mailers/member_mailer_spec.rb index b09ed32f6..77c6566ff 100644 --- a/spec/mailers/member_mailer_spec.rb +++ b/spec/mailers/member_mailer_spec.rb @@ -115,4 +115,21 @@ end.to change { ActionMailer::Base.deliveries.count }.by 1 end end + + describe "#chaser" do + it "logs the sent email" do + expect do + MemberMailer + .with(member: member) + .chaser + .deliver_now + end.to change(MemberEmailDelivery, :count).by(1) + + log = MemberEmailDelivery.last! + + expect(log.member).to eq(member) + expect(log.subject).to eq("It’s been a while, how are you doing? ♥️") + expect(log.to).to eq([member.email]) + end + end end From daf785da0486186b23715d73f90a4d5daf39432d Mon Sep 17 00:00:00 2001 From: "jonathan.kerr" <3410350+jonodrew@users.noreply.github.com> Date: Wed, 4 Feb 2026 17:03:27 +0000 Subject: [PATCH 4/6] Update test runner to use the queue This is the setup required for using ActiveJob and its queue, and checking if things were enqueued. Signed-off-by: jonathan.kerr <3410350+jonodrew@users.noreply.github.com> --- config/environments/test.rb | 3 +++ spec/spec_helper.rb | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/config/environments/test.rb b/config/environments/test.rb index 7b18fa6e0..3afe8b20f 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -1,5 +1,6 @@ require "active_support/core_ext/integer/time" require "timecop" +require "active_job/test_helper" # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that @@ -55,6 +56,8 @@ # Raise error when a before_action's only/except options reference missing actions. config.action_controller.raise_on_missing_callback_actions = true + config.active_job.queue_adapter = :test + # Fake omniauth for testing OmniAuth.config.test_mode = true diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 3ee48ccc4..ce9f6ee41 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -57,6 +57,7 @@ def self.branch_coverage? ActiveRecord::Migration.check_all_pending! if defined?(ActiveRecord::Migration) RSpec.configure do |config| + config.include ActiveJob::TestHelper config.include ApplicationHelper config.include LoginHelpers config.include ActiveSupport::Testing::TimeHelpers @@ -95,6 +96,9 @@ def self.branch_coverage? to_return(status: 200, body: '{"status":"active","segments":[]}', headers: { 'Content-Type' => 'application/json' }) DatabaseCleaner.strategy = :transaction + + clear_enqueued_jobs + clear_performed_jobs end # Driver is using an external browser with an app From cba8b9fb3ffd95b2181708ef14db8b1dca1ae327 Mon Sep 17 00:00:00 2001 From: "jonathan.kerr" <3410350+jonodrew@users.noreply.github.com> Date: Wed, 4 Feb 2026 17:04:05 +0000 Subject: [PATCH 5/6] Add :member_email_deliveries relation to Member This was an oversight when creating the related table, and fixes it Signed-off-by: jonathan.kerr <3410350+jonodrew@users.noreply.github.com> --- app/models/member.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/member.rb b/app/models/member.rb index b6be0e8a3..13e36fd70 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -22,6 +22,7 @@ class Member < ApplicationRecord has_many :chapters, -> { distinct }, through: :groups has_many :announcements, -> { distinct }, through: :groups has_many :meeting_invitations + has_many :member_email_deliveries validates :auth_services, presence: true validates :name, :surname, :email, :about_you, presence: true, if: :can_log_in? From 8c8f601a7fab71eac048832e2a3d6393a6ddd2f6 Mon Sep 17 00:00:00 2001 From: "jonathan.kerr" <3410350+jonodrew@users.noreply.github.com> Date: Wed, 4 Feb 2026 17:05:40 +0000 Subject: [PATCH 6/6] Create Job and Service for sending the chaser This outlines a single test for sending the chaser email. It also creates the relevant Job and Service. When users haven't attended a workshop they were invited to in the last three months _and_ haven't already been chased, they'll be sent a follow up email Signed-off-by: jonathan.kerr <3410350+jonodrew@users.noreply.github.com> --- app/jobs/send_three_month_email_job.rb | 9 ++++ app/services/three_month_email_service.rb | 16 +++++++ .../member_email_delivery_fabricator.rb | 6 +++ .../three_month_email_service_spec.rb | 43 +++++++++++++++++++ 4 files changed, 74 insertions(+) create mode 100644 app/jobs/send_three_month_email_job.rb create mode 100644 app/services/three_month_email_service.rb create mode 100644 spec/fabricators/member_email_delivery_fabricator.rb create mode 100644 spec/services/three_month_email_service_spec.rb diff --git a/app/jobs/send_three_month_email_job.rb b/app/jobs/send_three_month_email_job.rb new file mode 100644 index 000000000..29c250500 --- /dev/null +++ b/app/jobs/send_three_month_email_job.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class SendThreeMonthEmailJob < ApplicationJob + queue_as :default + + def perform + ThreeMonthEmailService.send_chaser + end +end diff --git a/app/services/three_month_email_service.rb b/app/services/three_month_email_service.rb new file mode 100644 index 000000000..dd3639415 --- /dev/null +++ b/app/services/three_month_email_service.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class ThreeMonthEmailService + def self.send_chaser + members = Member.joins(:workshop_invitations) + .left_joins(:member_email_deliveries) + .where(workshop_invitations: { attended: false }) + .where("workshop_invitations.created_at >= ?", 3.months.ago.beginning_of_day) + .where(member_email_deliveries: { id: nil }) + .distinct + return if members.empty? + members.each do |member| + MemberMailer.with(member: member).chaser.deliver_later + end + end +end diff --git a/spec/fabricators/member_email_delivery_fabricator.rb b/spec/fabricators/member_email_delivery_fabricator.rb new file mode 100644 index 000000000..f633b7a35 --- /dev/null +++ b/spec/fabricators/member_email_delivery_fabricator.rb @@ -0,0 +1,6 @@ +Fabricator(:member_email_delivery) do + member(fabricator: :member) + subject("Chaser") + body("Lorem ipsum") + to(["test_email@address"]) +end diff --git a/spec/services/three_month_email_service_spec.rb b/spec/services/three_month_email_service_spec.rb new file mode 100644 index 000000000..3d2b9b6b7 --- /dev/null +++ b/spec/services/three_month_email_service_spec.rb @@ -0,0 +1,43 @@ +RSpec.describe ThreeMonthEmailService, type: :service do + describe "#send_chaser" do + subject(:call) { described_class.send_chaser } + + let!(:eligible_member) { Fabricate(:member) } + let!(:emailed_member) { Fabricate(:member) } + let!(:old_invite_member) { Fabricate(:member) } + + before do + # Eligible: recent invite, no email delivery + Fabricate( + :workshop_invitation, + member: eligible_member, + created_at: 3.months.ago, + attended: false + ) + + # Already emailed: recent invite, but has email delivery + Fabricate( + :workshop_invitation, + member: emailed_member, + created_at: 2.months.ago + ) + Fabricate( + :member_email_delivery, + member: emailed_member + ) + + # Old invite: more than 3 months ago + Fabricate( + :workshop_invitation, + member: old_invite_member, + created_at: 4.months.ago + ) + end + + it "enqueues chaser emails only for eligible members" do + expect { + call + }.to have_enqueued_mail(MemberMailer, :chaser).once + end + end +end