Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true
class RemindDraftEventProposalsJob < ApplicationJob
class SendRemindersJob < ApplicationJob
def perform
RemindDraftEventProposals.new.call!
RemindQueueWithoutTicket.new.call!
end
end
1 change: 1 addition & 0 deletions app/models/user_con_profile.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
# needs_update :boolean default(FALSE), not null
# nickname :string
# preferred_contact :string
# queue_no_ticket_reminded_at :datetime
# ranked_choice_fallback_action :text default("waitlist"), not null
# ranked_choice_ordering_boost :integer
# receive_whos_free_emails :boolean default(TRUE), not null
Expand Down
3 changes: 3 additions & 0 deletions app/notifiers/notifier/dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module Notifier::Dsl
event_proposal_owner: Notifier::DynamicDestinations::EventProposalOwnerEvaluator,
event_team_members: Notifier::DynamicDestinations::EventTeamMembersEvaluator,
order_user_con_profile: Notifier::DynamicDestinations::OrderUserConProfileEvaluator,
signup_ranked_choice_user_con_profile: Notifier::DynamicDestinations::SignupRankedChoiceUserConProfileEvaluator,
signup_request_user_con_profile: Notifier::DynamicDestinations::SignupRequestUserConProfileEvaluator,
signup_user_con_profile: Notifier::DynamicDestinations::SignupUserConProfileEvaluator,
ticket_user_con_profile: Notifier::DynamicDestinations::TicketUserConProfileEvaluator,
Expand Down Expand Up @@ -63,8 +64,10 @@ def condition_evaluators
self.class.allowed_conditions.index_with { |condition| self.class.evaluator_for_condition(condition, self) }
end

# rubocop:disable Naming/PredicateMethod
def evaluate_condition(condition_type, condition_value)
@condition_evaluators ||= condition_evaluators
@condition_evaluators.fetch(condition_type.to_sym).matches?(condition_value)
end
# rubocop:enable Naming/PredicateMethod
end
13 changes: 13 additions & 0 deletions app/notifiers/notifier/dynamic_destinations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,17 @@ def user_con_profiles
user_activity_alert.notification_destinations.flat_map { |destination| destination.user_con_profiles(notifier) }
end
end

class SignupRankedChoiceUserConProfileEvaluator < Evaluator
attr_reader :signup_ranked_choice

def initialize(notifier:, signup_ranked_choice:)
super(notifier:)
@signup_ranked_choice = signup_ranked_choice
end

def user_con_profiles
[signup_ranked_choice.user_con_profile]
end
end
end
11 changes: 9 additions & 2 deletions app/notifiers/notifier_preview_factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def parameters
# This is super not worth refactoring
def synthesize_parameter_value(parameter_name) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
case parameter_name
when :alert_user_con_profile
when :alert_user_con_profile, :user_con_profile
UserConProfile.new(convention: convention)
when :changes
[]
Expand All @@ -50,6 +50,11 @@ def synthesize_parameter_value(parameter_name) # rubocop:disable Metrics/AbcSize
Order.new(user_con_profile: UserConProfile.new(convention: convention))
when :signup
Signup.new(run: Run.new(event: Event.new(convention: convention, title: "Event Title")))
when :signup_ranked_choice
SignupRankedChoice.new(
user_con_profile: UserConProfile.new(convention: convention),
target_run: Run.new(event: Event.new(convention: convention, title: "Event Title"))
)
when :signup_request
SignupRequest.new(target_run: Run.new(event: Event.new(convention: convention, title: "Event Title")))
when :ticket
Expand All @@ -64,7 +69,7 @@ def synthesize_parameter_value(parameter_name) # rubocop:disable Metrics/AbcSize
# This is super not worth refactoring
def find_parameter_value(parameter_name) # rubocop:disable Metrics
case parameter_name
when :alert_user_con_profile
when :alert_user_con_profile, :user_con_profile
convention.user_con_profiles.first
when :changes
[
Expand All @@ -89,6 +94,8 @@ def find_parameter_value(parameter_name) # rubocop:disable Metrics
"refund-abc123"
when :signup
convention.signups.first
when :signup_ranked_choice
SignupRankedChoice.where(user_con_profile: convention.user_con_profiles.select(:id)).first
when :signup_request
convention.signup_requests.first
when :ticket
Expand Down
30 changes: 30 additions & 0 deletions app/notifiers/signup_queue/no_ticket_reminder_notifier.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# frozen_string_literal: true
class SignupQueue::NoTicketReminderNotifier < Notifier
attr_reader :signup_ranked_choice
delegate :user_con_profile, to: :signup_ranked_choice

dynamic_destination :signup_ranked_choice_user_con_profile do
{ signup_ranked_choice: }
end

def initialize(signup_ranked_choice:)
@signup_ranked_choice = signup_ranked_choice
super(convention: user_con_profile.convention, event_key: "signup_queue/no_ticket_reminder")
end

def initializer_options
{ signup_ranked_choice: }
end

def liquid_assigns
super.merge(
"user_con_profile" => user_con_profile,
"ticket_name" => convention.ticket_name,
"queue_items" => user_con_profile.signup_ranked_choices.where(state: "pending")
)
end

def self.build_default_destinations(notification_template:)
[notification_template.notification_destinations.new(dynamic_destination: :signup_ranked_choice_user_con_profile)]
end
end
40 changes: 40 additions & 0 deletions app/services/remind_queue_without_ticket.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# frozen_string_literal: true
class RemindQueueWithoutTicket < CivilService::Service
private

def inner_call
signup_ranked_choices_to_remind.each do |signup_ranked_choice|
SignupQueue::NoTicketReminderNotifier.new(signup_ranked_choice: signup_ranked_choice).deliver_later
signup_ranked_choice.user_con_profile.update_columns(queue_no_ticket_reminded_at: Time.zone.now)
end

success
end

# rubocop:disable Metrics/MethodLength
def signup_ranked_choices_to_remind
reminder_window_start = 1.week.from_now.beginning_of_day
reminder_window_end = 1.week.from_now.end_of_day

@signup_ranked_choices_to_remind ||=
SignupRankedChoice
.joins(:user_con_profile)
.joins(user_con_profile: :convention)
.joins("LEFT JOIN tickets ON tickets.user_con_profile_id = user_con_profiles.id")
.joins("INNER JOIN signup_rounds ON signup_rounds.convention_id = conventions.id")
.where(state: "pending")
.where(tickets: { id: nil })
.where(signup_rounds: { automation_action: "execute_ranked_choice", executed_at: nil })
.where("signup_rounds.start BETWEEN ? AND ?", reminder_window_start, reminder_window_end)
.where(
conventions: {
ticket_mode: %w[required_for_signup ticket_per_event],
signup_automation_mode: "ranked_choice"
}
)
.where(user_con_profiles: { queue_no_ticket_reminded_at: nil })
.select("DISTINCT ON (user_con_profiles.id) signup_ranked_choices.*")
.includes(user_con_profile: :convention)
end
# rubocop:enable Metrics/MethodLength
end
2 changes: 1 addition & 1 deletion app/services/run_notifications_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ class RunNotificationsService < CivilService::Service
private

def inner_call
[NotifyEventProposalChangesJob, NotifyEventChangesJob, RemindDraftEventProposalsJob].each(&:perform_later)
[NotifyEventProposalChangesJob, NotifyEventChangesJob, SendRemindersJob].each(&:perform_later)
success
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
subject: |
[{{ convention.name }}] Don't forget to get your {{ ticket_name }}!
body_text: |-
Hi {{ user_con_profile.name_without_nickname }},

You have {{ queue_items.size }} event{% if queue_items.size != 1 %}s{% endif %} in your signup queue for {{ convention.name }}, but you don't have a {{ ticket_name }} yet. To be eligible for signup when your queue processes, you'll need to purchase a {{ ticket_name }}.

Your queue includes:
{%- for item in queue_items limit: 10 %}
- {{ item.target_run.event.title }}{% if item.target_run.title_suffix %} ({{ item.target_run.title_suffix }}){% endif %}
{%- endfor %}

{% if queue_items.size > 10 %}...and {{ queue_items.size | minus: 10 }} more{% endif %}

Get your {{ ticket_name }} now: {{ convention.url | append: '/ticket/new' | absolute_url }}

We hope to see you at {{ convention.name }}!
---
<h1>Don't forget to get your {{ ticket_name }}!</h1>

<p>Hi {{ user_con_profile.name_without_nickname }},</p>

<p>
You have <strong>{{ queue_items.size }} event{% if queue_items.size != 1 %}s{% endif %}</strong>
in your signup queue for {{ convention.name }}, but you don't have a {{ ticket_name }} yet.
To be eligible for signup when your queue processes, you'll need to purchase a {{ ticket_name }}.
</p>

<h2>Your queue includes:</h2>
<ul>
{%- for item in queue_items limit: 10 %}
<li>{{ item.target_run.event.title }}{% if item.target_run.title_suffix %} ({{ item.target_run.title_suffix }}){% endif %}</li>
{%- endfor %}
</ul>

{% if queue_items.size > 10 %}
<p><em>...and {{ queue_items.size | minus: 10 }} more</em></p>
{% endif %}

<p>
<a href="{{ convention.url | append: '/ticket/new' | absolute_url }}" style="display: inline-block; padding: 10px 20px; background-color: #007bff; color: white; text-decoration: none; border-radius: 5px;">
Get your {{ ticket_name }} now
</a>
</p>

<p>We hope to see you at {{ convention.name }}!</p>
11 changes: 11 additions & 0 deletions config/notifications.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,17 @@
}
]
},
{
"key": "signup_queue",
"name": "Signup queue",
"events": [
{
"key": "no_ticket_reminder",
"name": "Reminder: queue items without ticket",
"destination_description": "Attendee with queue items"
}
]
},
{
"key": "signup_requests",
"name": "Event signup requests",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddQueueNoTicketRemindedAtToUserConProfiles < ActiveRecord::Migration[8.1]
def change
add_column :user_con_profiles, :queue_no_ticket_reminded_at, :datetime
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
class LoadSignupQueueNoTicketReminderTemplate < ActiveRecord::Migration[7.2]
def up
Convention.find_each do |convention|
say "Loading signup_queue/no_ticket_reminder notification template for #{convention.name}"
adapter = CmsContentStorageAdapters::NotificationTemplates.new(convention, CmsContentSet.new(name: "base"))

item = adapter.all_items_from_disk.find { |i| i.identifier == "signup_queue/no_ticket_reminder" }
next unless item

attrs = adapter.read_item_attrs(item)
template = convention.notification_templates.find_or_initialize_by(event_key: "signup_queue/no_ticket_reminder")
template.assign_attributes(**attrs)
template.save!

# Build default destinations if this is a new template
if template.notification_destinations.empty?
SignupQueue::NoTicketReminderNotifier.build_default_destinations(notification_template: template).each(&:save!)
end
end
end

def down
NotificationTemplate.where(event_key: "signup_queue/no_ticket_reminder").find_each(&:destroy!)
end
end
5 changes: 4 additions & 1 deletion db/structure.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2955,7 +2955,8 @@ CREATE TABLE public.user_con_profiles (
allow_sms boolean DEFAULT true NOT NULL,
lottery_number integer NOT NULL,
ranked_choice_ordering_boost integer,
ranked_choice_fallback_action text DEFAULT 'waitlist'::text NOT NULL
ranked_choice_fallback_action text DEFAULT 'waitlist'::text NOT NULL,
queue_no_ticket_reminded_at timestamp(6) without time zone
);


Expand Down Expand Up @@ -6135,6 +6136,8 @@ ALTER TABLE ONLY public.cms_files_pages
SET search_path TO "$user", public;

INSERT INTO "schema_migrations" (version) VALUES
('20260214191626'),
('20260214190735'),
('20251210230514'),
('20251109200750'),
('20251001173716'),
Expand Down
1 change: 1 addition & 0 deletions test/factories/user_con_profiles.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
# needs_update :boolean default(FALSE), not null
# nickname :string
# preferred_contact :string
# queue_no_ticket_reminded_at :datetime
# ranked_choice_fallback_action :text default("waitlist"), not null
# ranked_choice_ordering_boost :integer
# receive_whos_free_emails :boolean default(TRUE), not null
Expand Down
9 changes: 5 additions & 4 deletions test/models/user_con_profile_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
# needs_update :boolean default(FALSE), not null
# nickname :string
# preferred_contact :string
# queue_no_ticket_reminded_at :datetime
# ranked_choice_fallback_action :text default("waitlist"), not null
# ranked_choice_ordering_boost :integer
# receive_whos_free_emails :boolean default(TRUE), not null
Expand All @@ -47,11 +48,11 @@
# fk_rails_... (user_id => users.id)
#
# rubocop:enable Layout/LineLength, Lint/RedundantCopDisableDirective
require 'test_helper'
require "test_helper"

class UserConProfileTest < ActiveSupport::TestCase
describe 'is_team_member' do
it 'finds a user who is a team member for an event' do
describe "is_team_member" do
it "finds a user who is a team member for an event" do
team_member = create(:team_member)
assert UserConProfile.is_team_member.to_a.include?(team_member.user_con_profile)
end
Expand All @@ -61,7 +62,7 @@ class UserConProfileTest < ActiveSupport::TestCase
refute UserConProfile.is_team_member.to_a.include?(user_con_profile)
end

it 'scopes correctly by convention' do
it "scopes correctly by convention" do
team_member = create(:team_member)
other_convention = create(:convention)

Expand Down
Loading
Loading