Skip to content
Merged
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
28 changes: 27 additions & 1 deletion app/services/event_registration_services/public_registration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ class PublicRegistration
# ce_credit_requested flag. Kept here so the seed, service, and specs agree.
CE_CREDIT_INTEREST_IDENTIFIER = "ce_credit_interest".freeze

# Well-known field_identifier of the "Additional forms" multi-select question.
# Checking "Invoice" / "W-9" toggles the registration's invoice_requested /
# w9_requested flags, which the digital ticket reads to surface those downloads.
# The option labels below must match the seeded answer options exactly.
ADDITIONAL_FORMS_IDENTIFIER = "additional_forms".freeze
ADDITIONAL_FORMS_INVOICE = "Invoice".freeze
ADDITIONAL_FORMS_W9 = "W-9".freeze

def self.call(event:, form:, form_params:, scholarship_requested: false, person: nil)
new(event:, form:, form_params:, scholarship_requested:, person:).call
end
Expand Down Expand Up @@ -37,6 +45,8 @@ def call
if existing
existing.update!(scholarship_requested: true) if @scholarship_requested
existing.update!(ce_credit_requested: true) if ce_credit_requested?
existing.update!(w9_requested: true) if w9_requested?
existing.update!(invoice_requested: true) if invoice_requested?
if existing.status == "cancelled"
existing.update!(status: "registered")
send_notifications(existing)
Expand Down Expand Up @@ -255,7 +265,9 @@ def create_event_registration(person)
@event.event_registrations.create!(
registrant: person,
scholarship_requested: @scholarship_requested,
ce_credit_requested: ce_credit_requested?
ce_credit_requested: ce_credit_requested?,
w9_requested: w9_requested?,
invoice_requested: invoice_requested?
)
end

Expand All @@ -264,6 +276,20 @@ def ce_credit_requested?
field_value(CE_CREDIT_INTEREST_IDENTIFIER).to_s.strip.casecmp?("yes")
end

# The "Additional forms" question is a multi-select, so its submitted value is
# an array of the checked option labels (e.g. [ "Invoice", "W-9" ]).
def additional_forms_selections

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 From Claude: "Additional forms" is a multi-select, so its submitted value is an array of checked labels (e.g. ["Invoice", "W-9"]). Array(...) normalizes the nil/single/array cases before the per-flag casecmp? checks below.

Array(field_value(ADDITIONAL_FORMS_IDENTIFIER)).map { |value| value.to_s.strip }
end

def w9_requested?
additional_forms_selections.any? { |value| value.casecmp?(ADDITIONAL_FORMS_W9) }
end

def invoice_requested?
additional_forms_selections.any? { |value| value.casecmp?(ADDITIONAL_FORMS_INVOICE) }
end

def create_form_submission(person)
submission = FormSubmission.create!(person: person, form: @form, event: @event)
save_form_answers(submission)
Expand Down
16 changes: 16 additions & 0 deletions app/views/event_registrations/_ticket.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,22 @@
<% end %>
<% end %>

<!-- Additional forms the registrant asked for during registration -->
<% if event_registration.w9_requested? %>
<%= link_to "/documents/awbw-w9.pdf",

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 From Claude: Links to a stable, undigested public path on purpose — the W-9 lives at public/documents/awbw-w9.pdf, not the asset pipeline (which would fingerprint the filename and break this URL). Shown only when w9_requested?.

target: "_blank", rel: "noopener",
class: "flex items-center gap-3 rounded-xl border-2 border-blue-200 bg-blue-50 px-4 py-3 hover:bg-blue-100 transition-colors" do %>
<i class="fa-solid fa-file-pdf text-blue-500 text-xl shrink-0"></i>

<div class="flex-1 min-w-0 text-left">
<p class="font-semibold text-blue-900">Download W-9</p>
<p class="text-sm text-blue-700">AWBW's W-9 tax form for your records</p>
</div>

<i class="fa-solid fa-download text-blue-500 shrink-0"></i>
<% end %>
<% end %>

<!-- Divider -->
<div class="border-t border-gray-200"></div>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
class AddW9AndInvoiceRequestedToEventRegistrations < ActiveRecord::Migration[8.1]
def up
unless column_exists?(:event_registrations, :w9_requested)
add_column :event_registrations, :w9_requested, :boolean, default: false, null: false
end

unless column_exists?(:event_registrations, :invoice_requested)
add_column :event_registrations, :invoice_requested, :boolean, default: false, null: false
end
end

def down
remove_column :event_registrations, :w9_requested, if_exists: true
remove_column :event_registrations, :invoice_requested, if_exists: true
end
end
4 changes: 3 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[8.1].define(version: 2026_06_16_130000) do
ActiveRecord::Schema[8.1].define(version: 2026_06_17_180000) do
create_table "action_text_mentions", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
t.bigint "action_text_rich_text_id", null: false
t.datetime "created_at", null: false
Expand Down Expand Up @@ -451,12 +451,14 @@
t.string "checkout_session_id"
t.datetime "created_at", null: false
t.bigint "event_id"
t.boolean "invoice_requested", default: false, null: false
t.boolean "payment_unresolved"
t.bigint "registrant_id", null: false
t.boolean "scholarship_requested", default: false, null: false
t.string "slug"
t.string "status", default: "registered", null: false
t.datetime "updated_at", null: false
t.boolean "w9_requested", default: false, null: false
t.index ["checkout_session_id"], name: "index_event_registrations_on_checkout_session_id"
t.index ["event_id"], name: "index_event_registrations_on_event_id"
t.index ["payment_unresolved"], name: "index_event_registrations_on_payment_unresolved"
Expand Down
40 changes: 40 additions & 0 deletions db/seeds/dev/events_management.rb
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,46 @@
end
end

# The "Additional forms" question: a multi-select whose checked options drive the
# resulting registration's invoice_requested / w9_requested flags (see
# EventRegistrationServices::PublicRegistration). The digital ticket reads those
# flags to surface the matching downloads. Seeded onto its own section, like the
# CE question above, so the form builder's add/remove-section logic leaves it
# alone, and carrying the well-known field_identifier the service keys off. The
# answer-option names must match the service's ADDITIONAL_FORMS_* constants.
additional_forms_identifier = EventRegistrationServices::PublicRegistration::ADDITIONAL_FORMS_IDENTIFIER
if registration_form.form_fields.where(field_identifier: additional_forms_identifier).none?
next_position = (registration_form.form_fields.maximum(:position) || 0) + 1
registration_form.form_fields.create!(
name: "Additional forms",
answer_type: :group_header,
status: :active,
position: next_position,
required: false,
section: "additional_forms",
visibility: :always_ask
)
additional_forms_field = registration_form.form_fields.create!(
name: "Do you need either of the following?",

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 From Claude: Deliberately required: false (set on the field below): "Do you need either of the following?" must allow "neither," matching the CE magic-question precedent — despite the red * in the original mock. Flag if you actually want it required.

answer_type: :multi_select_checkbox,
status: :active,
position: next_position + 1,
required: false,
field_identifier: additional_forms_identifier,
section: "additional_forms",
visibility: :always_ask,
width: :full,
hint_text: "If selected, these will be available on your digital registration ticket."
)
[
EventRegistrationServices::PublicRegistration::ADDITIONAL_FORMS_INVOICE,
EventRegistrationServices::PublicRegistration::ADDITIONAL_FORMS_W9
].each_with_index do |opt, idx|
ao = AnswerOption.find_or_create_by!(name: opt) { |a| a.position = idx }
additional_forms_field.form_field_answer_options.create!(answer_option: ao)
end
end

# Each entry: [title, form_type, cost_cents, scholarship?, visibility, span_days]
# form_type: :long, :short, or :none. span_days (optional) makes a multi-day event.
dev_events = [
Expand Down
12 changes: 12 additions & 0 deletions public/documents/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Static documents

Files here are served directly at `/documents/<filename>` (no asset digest), so
links to them stay stable.

## awbw-w9.pdf

The registration ticket links to `/documents/awbw-w9.pdf` when a registrant checks
"W-9" on the "Additional forms" registration question. It's a single static file
shared by every registrant (see `EventRegistrationServices::PublicRegistration` and
`app/views/event_registrations/_ticket.html.erb`). Replace `awbw-w9.pdf` here when
AWBW issues an updated W-9.
Binary file added public/documents/awbw-w9.pdf
Binary file not shown.
14 changes: 14 additions & 0 deletions spec/requests/events/registrations_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,20 @@
end
end

context "W-9 download link" do
it "shows the W-9 download link when w9_requested" do
registration.update!(w9_requested: true)
get registration_ticket_path(registration.slug)
expect(response.body).to include("/documents/awbw-w9.pdf")
expect(response.body).to include("Download W-9")
end

it "hides the W-9 download link when not requested" do
get registration_ticket_path(registration.slug)
expect(response.body).not_to include("/documents/awbw-w9.pdf")
end
end

context "with an invalid slug" do
it "returns 404" do
get registration_ticket_path("nonexistent-slug")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,68 @@ def register_with_ce(answer)
end
end

describe "Additional forms (multi-select magic question)" do
let!(:additional_forms_field) do
field = form.form_fields.create!(
name: "Do you need either of the following?",
answer_type: :multi_select_checkbox,
status: :active,
position: (form.form_fields.maximum(:position) || 0) + 1,
required: false,
field_identifier: described_class::ADDITIONAL_FORMS_IDENTIFIER,
section: "additional_forms",
visibility: :always_ask
)
[ described_class::ADDITIONAL_FORMS_INVOICE, described_class::ADDITIONAL_FORMS_W9 ].each_with_index do |opt, idx|
ao = AnswerOption.find_or_create_by!(name: opt) { |a| a.position = idx }
field.form_field_answer_options.create!(answer_option: ao)
end
field
end

def register_with_additional_forms(selections)
params = base_form_params(first_name: "Wendy", last_name: "Nein", email: "wendy@example.com")
params = params.merge(additional_forms_field.id.to_s => selections) unless selections.nil?
described_class.call(event: event, form: form, form_params: params)
end

it "sets both flags when both options are checked" do
registration = register_with_additional_forms([ "Invoice", "W-9" ]).event_registration
expect(registration.invoice_requested).to be true
expect(registration.w9_requested).to be true
end

it "sets only w9_requested when only W-9 is checked" do
registration = register_with_additional_forms([ "W-9" ]).event_registration
expect(registration.w9_requested).to be true
expect(registration.invoice_requested).to be false
end

it "sets only invoice_requested when only Invoice is checked" do
registration = register_with_additional_forms([ "Invoice" ]).event_registration
expect(registration.invoice_requested).to be true
expect(registration.w9_requested).to be false
end

it "leaves both flags off when nothing is checked" do
registration = register_with_additional_forms(nil).event_registration
expect(registration.w9_requested).to be false
expect(registration.invoice_requested).to be false
end

it "turns the flags on for an existing registration that now checks the options" do
person = create(:person, first_name: "Wendy", last_name: "Nein", email: "wendy@example.com")
existing = create(:event_registration, event: event, registrant: person,
w9_requested: false, invoice_requested: false)

result = register_with_additional_forms([ "Invoice", "W-9" ])

expect(result.event_registration).to eq(existing)
expect(existing.reload.w9_requested).to be true
expect(existing.reload.invoice_requested).to be true
end
end

describe "re-registration after cancellation" do
let(:person) { create(:person, first_name: "Jane", last_name: "Doe", email: "jane@example.com") }
let!(:cancelled_registration) do
Expand Down