Skip to content

Commit 5691399

Browse files
maebealeclaude
andauthored
Add W-9 download to the registration ticket via an Additional forms question (#1707)
Registrants who need AWBW's W-9 (or an invoice) had no way to ask for it during registration, and staff had no signal to act on. Add an "Additional forms" multi-select question whose selections drive new w9_requested / invoice_requested flags on the registration — mirroring the existing ce_credit "magic question" so the seed, service, and ticket stay consistent. The digital ticket now surfaces a W-9 download (static public PDF) when w9_requested is set. The invoice flag is wired up but its download page is intentionally left for a follow-up PR, which only needs to consume the existing flag. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c599e52 commit 5691399

9 files changed

Lines changed: 190 additions & 2 deletions

File tree

app/services/event_registration_services/public_registration.rb

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ class PublicRegistration
77
# ce_credit_requested flag. Kept here so the seed, service, and specs agree.
88
CE_CREDIT_INTEREST_IDENTIFIER = "ce_credit_interest".freeze
99

10+
# Well-known field_identifier of the "Additional forms" multi-select question.
11+
# Checking "Invoice" / "W-9" toggles the registration's invoice_requested /
12+
# w9_requested flags, which the digital ticket reads to surface those downloads.
13+
# The option labels below must match the seeded answer options exactly.
14+
ADDITIONAL_FORMS_IDENTIFIER = "additional_forms".freeze
15+
ADDITIONAL_FORMS_INVOICE = "Invoice".freeze
16+
ADDITIONAL_FORMS_W9 = "W-9".freeze
17+
1018
def self.call(event:, form:, form_params:, scholarship_requested: false, person: nil)
1119
new(event:, form:, form_params:, scholarship_requested:, person:).call
1220
end
@@ -37,6 +45,8 @@ def call
3745
if existing
3846
existing.update!(scholarship_requested: true) if @scholarship_requested
3947
existing.update!(ce_credit_requested: true) if ce_credit_requested?
48+
existing.update!(w9_requested: true) if w9_requested?
49+
existing.update!(invoice_requested: true) if invoice_requested?
4050
if existing.status == "cancelled"
4151
existing.update!(status: "registered")
4252
send_notifications(existing)
@@ -255,7 +265,9 @@ def create_event_registration(person)
255265
@event.event_registrations.create!(
256266
registrant: person,
257267
scholarship_requested: @scholarship_requested,
258-
ce_credit_requested: ce_credit_requested?
268+
ce_credit_requested: ce_credit_requested?,
269+
w9_requested: w9_requested?,
270+
invoice_requested: invoice_requested?
259271
)
260272
end
261273

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

279+
# The "Additional forms" question is a multi-select, so its submitted value is
280+
# an array of the checked option labels (e.g. [ "Invoice", "W-9" ]).
281+
def additional_forms_selections
282+
Array(field_value(ADDITIONAL_FORMS_IDENTIFIER)).map { |value| value.to_s.strip }
283+
end
284+
285+
def w9_requested?
286+
additional_forms_selections.any? { |value| value.casecmp?(ADDITIONAL_FORMS_W9) }
287+
end
288+
289+
def invoice_requested?
290+
additional_forms_selections.any? { |value| value.casecmp?(ADDITIONAL_FORMS_INVOICE) }
291+
end
292+
267293
def create_form_submission(person)
268294
submission = FormSubmission.create!(person: person, form: @form, event: @event)
269295
save_form_answers(submission)

app/views/event_registrations/_ticket.html.erb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,22 @@
109109
<% end %>
110110
<% end %>
111111

112+
<!-- Additional forms the registrant asked for during registration -->
113+
<% if event_registration.w9_requested? %>
114+
<%= link_to "/documents/awbw-w9.pdf",
115+
target: "_blank", rel: "noopener",
116+
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 %>
117+
<i class="fa-solid fa-file-pdf text-blue-500 text-xl shrink-0"></i>
118+
119+
<div class="flex-1 min-w-0 text-left">
120+
<p class="font-semibold text-blue-900">Download W-9</p>
121+
<p class="text-sm text-blue-700">AWBW's W-9 tax form for your records</p>
122+
</div>
123+
124+
<i class="fa-solid fa-download text-blue-500 shrink-0"></i>
125+
<% end %>
126+
<% end %>
127+
112128
<!-- Divider -->
113129
<div class="border-t border-gray-200"></div>
114130

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
class AddW9AndInvoiceRequestedToEventRegistrations < ActiveRecord::Migration[8.1]
2+
def up
3+
unless column_exists?(:event_registrations, :w9_requested)
4+
add_column :event_registrations, :w9_requested, :boolean, default: false, null: false
5+
end
6+
7+
unless column_exists?(:event_registrations, :invoice_requested)
8+
add_column :event_registrations, :invoice_requested, :boolean, default: false, null: false
9+
end
10+
end
11+
12+
def down
13+
remove_column :event_registrations, :w9_requested, if_exists: true
14+
remove_column :event_registrations, :invoice_requested, if_exists: true
15+
end
16+
end

db/schema.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
#
1111
# It's strongly recommended that you check this file into your version control system.
1212

13-
ActiveRecord::Schema[8.1].define(version: 2026_06_16_130000) do
13+
ActiveRecord::Schema[8.1].define(version: 2026_06_17_180000) do
1414
create_table "action_text_mentions", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
1515
t.bigint "action_text_rich_text_id", null: false
1616
t.datetime "created_at", null: false
@@ -451,12 +451,14 @@
451451
t.string "checkout_session_id"
452452
t.datetime "created_at", null: false
453453
t.bigint "event_id"
454+
t.boolean "invoice_requested", default: false, null: false
454455
t.boolean "payment_unresolved"
455456
t.bigint "registrant_id", null: false
456457
t.boolean "scholarship_requested", default: false, null: false
457458
t.string "slug"
458459
t.string "status", default: "registered", null: false
459460
t.datetime "updated_at", null: false
461+
t.boolean "w9_requested", default: false, null: false
460462
t.index ["checkout_session_id"], name: "index_event_registrations_on_checkout_session_id"
461463
t.index ["event_id"], name: "index_event_registrations_on_event_id"
462464
t.index ["payment_unresolved"], name: "index_event_registrations_on_payment_unresolved"

db/seeds/dev/events_management.rb

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,46 @@
129129
end
130130
end
131131

132+
# The "Additional forms" question: a multi-select whose checked options drive the
133+
# resulting registration's invoice_requested / w9_requested flags (see
134+
# EventRegistrationServices::PublicRegistration). The digital ticket reads those
135+
# flags to surface the matching downloads. Seeded onto its own section, like the
136+
# CE question above, so the form builder's add/remove-section logic leaves it
137+
# alone, and carrying the well-known field_identifier the service keys off. The
138+
# answer-option names must match the service's ADDITIONAL_FORMS_* constants.
139+
additional_forms_identifier = EventRegistrationServices::PublicRegistration::ADDITIONAL_FORMS_IDENTIFIER
140+
if registration_form.form_fields.where(field_identifier: additional_forms_identifier).none?
141+
next_position = (registration_form.form_fields.maximum(:position) || 0) + 1
142+
registration_form.form_fields.create!(
143+
name: "Additional forms",
144+
answer_type: :group_header,
145+
status: :active,
146+
position: next_position,
147+
required: false,
148+
section: "additional_forms",
149+
visibility: :always_ask
150+
)
151+
additional_forms_field = registration_form.form_fields.create!(
152+
name: "Do you need either of the following?",
153+
answer_type: :multi_select_checkbox,
154+
status: :active,
155+
position: next_position + 1,
156+
required: false,
157+
field_identifier: additional_forms_identifier,
158+
section: "additional_forms",
159+
visibility: :always_ask,
160+
width: :full,
161+
hint_text: "If selected, these will be available on your digital registration ticket."
162+
)
163+
[
164+
EventRegistrationServices::PublicRegistration::ADDITIONAL_FORMS_INVOICE,
165+
EventRegistrationServices::PublicRegistration::ADDITIONAL_FORMS_W9
166+
].each_with_index do |opt, idx|
167+
ao = AnswerOption.find_or_create_by!(name: opt) { |a| a.position = idx }
168+
additional_forms_field.form_field_answer_options.create!(answer_option: ao)
169+
end
170+
end
171+
132172
# Each entry: [title, form_type, cost_cents, scholarship?, visibility, span_days]
133173
# form_type: :long, :short, or :none. span_days (optional) makes a multi-day event.
134174
dev_events = [

public/documents/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Static documents
2+
3+
Files here are served directly at `/documents/<filename>` (no asset digest), so
4+
links to them stay stable.
5+
6+
## awbw-w9.pdf
7+
8+
The registration ticket links to `/documents/awbw-w9.pdf` when a registrant checks
9+
"W-9" on the "Additional forms" registration question. It's a single static file
10+
shared by every registrant (see `EventRegistrationServices::PublicRegistration` and
11+
`app/views/event_registrations/_ticket.html.erb`). Replace `awbw-w9.pdf` here when
12+
AWBW issues an updated W-9.

public/documents/awbw-w9.pdf

1.04 MB
Binary file not shown.

spec/requests/events/registrations_spec.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,20 @@
4747
end
4848
end
4949

50+
context "W-9 download link" do
51+
it "shows the W-9 download link when w9_requested" do
52+
registration.update!(w9_requested: true)
53+
get registration_ticket_path(registration.slug)
54+
expect(response.body).to include("/documents/awbw-w9.pdf")
55+
expect(response.body).to include("Download W-9")
56+
end
57+
58+
it "hides the W-9 download link when not requested" do
59+
get registration_ticket_path(registration.slug)
60+
expect(response.body).not_to include("/documents/awbw-w9.pdf")
61+
end
62+
end
63+
5064
context "with an invalid slug" do
5165
it "returns 404" do
5266
get registration_ticket_path("nonexistent-slug")

spec/services/event_registration_services/public_registration_spec.rb

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,68 @@ def register_with_ce(answer)
110110
end
111111
end
112112

113+
describe "Additional forms (multi-select magic question)" do
114+
let!(:additional_forms_field) do
115+
field = form.form_fields.create!(
116+
name: "Do you need either of the following?",
117+
answer_type: :multi_select_checkbox,
118+
status: :active,
119+
position: (form.form_fields.maximum(:position) || 0) + 1,
120+
required: false,
121+
field_identifier: described_class::ADDITIONAL_FORMS_IDENTIFIER,
122+
section: "additional_forms",
123+
visibility: :always_ask
124+
)
125+
[ described_class::ADDITIONAL_FORMS_INVOICE, described_class::ADDITIONAL_FORMS_W9 ].each_with_index do |opt, idx|
126+
ao = AnswerOption.find_or_create_by!(name: opt) { |a| a.position = idx }
127+
field.form_field_answer_options.create!(answer_option: ao)
128+
end
129+
field
130+
end
131+
132+
def register_with_additional_forms(selections)
133+
params = base_form_params(first_name: "Wendy", last_name: "Nein", email: "wendy@example.com")
134+
params = params.merge(additional_forms_field.id.to_s => selections) unless selections.nil?
135+
described_class.call(event: event, form: form, form_params: params)
136+
end
137+
138+
it "sets both flags when both options are checked" do
139+
registration = register_with_additional_forms([ "Invoice", "W-9" ]).event_registration
140+
expect(registration.invoice_requested).to be true
141+
expect(registration.w9_requested).to be true
142+
end
143+
144+
it "sets only w9_requested when only W-9 is checked" do
145+
registration = register_with_additional_forms([ "W-9" ]).event_registration
146+
expect(registration.w9_requested).to be true
147+
expect(registration.invoice_requested).to be false
148+
end
149+
150+
it "sets only invoice_requested when only Invoice is checked" do
151+
registration = register_with_additional_forms([ "Invoice" ]).event_registration
152+
expect(registration.invoice_requested).to be true
153+
expect(registration.w9_requested).to be false
154+
end
155+
156+
it "leaves both flags off when nothing is checked" do
157+
registration = register_with_additional_forms(nil).event_registration
158+
expect(registration.w9_requested).to be false
159+
expect(registration.invoice_requested).to be false
160+
end
161+
162+
it "turns the flags on for an existing registration that now checks the options" do
163+
person = create(:person, first_name: "Wendy", last_name: "Nein", email: "wendy@example.com")
164+
existing = create(:event_registration, event: event, registrant: person,
165+
w9_requested: false, invoice_requested: false)
166+
167+
result = register_with_additional_forms([ "Invoice", "W-9" ])
168+
169+
expect(result.event_registration).to eq(existing)
170+
expect(existing.reload.w9_requested).to be true
171+
expect(existing.reload.invoice_requested).to be true
172+
end
173+
end
174+
113175
describe "re-registration after cancellation" do
114176
let(:person) { create(:person, first_name: "Jane", last_name: "Doe", email: "jane@example.com") }
115177
let!(:cancelled_registration) do

0 commit comments

Comments
 (0)