-
- <% if @person.profile_show_bio? && @person.bio.present? %>
-
- <% allocations = @payment.allocations.to_a %>
+ <% allocations = @payment.allocations.order(created_at: :desc).to_a %>
<% if allocations.any? %>
Allocations
@@ -53,7 +53,7 @@
Allocated To
Amount
- Date
+ Date/Time
Actions
@@ -68,7 +68,7 @@
<% end %>
$<%= "%.2f" % (allocation.amount.to_d / 100) %>
-
<%= allocation.created_at.strftime("%B %d, %Y") %>
+
<%= allocation.created_at.strftime("%B %d, %Y at %I:%M %p") %>
<% unless allocation.reverted? || allocation.amount.to_i < 0 %>
<%= button_to "Revert Allocation", revert_allocation_path(allocation), method: :post, data: { confirm: "Are you sure you want to revert this allocation? The funds will be returned to the payment." }, class: "text-danger hover:text-danger-dark" %>
@@ -81,7 +81,7 @@
<% end %>
- <% refunds = @payment.refunds.to_a %>
+ <% refunds = @payment.refunds.order(created_at: :desc).to_a %>
<% if refunds.any? %>
Refunds
@@ -92,7 +92,7 @@
Recipient
Method
Amount
-
Date
+
Date/Time
@@ -107,7 +107,7 @@
<%= refund.method.titleize %>
$<%= "%.2f" % refund.amount_dollars %>
- <%= refund.created_at.strftime("%B %d, %Y") %>
+ <%= refund.created_at.strftime("%B %d, %Y at %I:%M %p") %>
<% end %>
From 0ebcfd20360a031cbec6e98d34684d6423b7dd41 Mon Sep 17 00:00:00 2001
From: Justin Miller <16829344+jmilljr24@users.noreply.github.com>
Date: Wed, 8 Apr 2026 12:41:59 -0400
Subject: [PATCH 39/74] fix amount cents remaning for new payments
---
app/models/payment.rb | 8 ++++++++
app/views/payments/index.html.erb | 2 +-
...ove_default_from_amount_cents_remaining_on_payments.rb | 5 +++++
db/schema.rb | 4 ++--
4 files changed, 16 insertions(+), 3 deletions(-)
create mode 100644 db/migrate/20260408163911_remove_default_from_amount_cents_remaining_on_payments.rb
diff --git a/app/models/payment.rb b/app/models/payment.rb
index 97eff0cb4..36e183317 100644
--- a/app/models/payment.rb
+++ b/app/models/payment.rb
@@ -8,6 +8,8 @@ class Payment < ApplicationRecord
validates :amount_cents, numericality: true
validates :currency, presence: true
+ before_validation :set_amount_cents_remaining, if: :new_record?
+
def amount_dollars
amount_cents.to_d / 100 if amount_cents
end
@@ -31,4 +33,10 @@ def remaining_dollars
def remaining_dollars=(value)
self.amount_cents_remaining = (value.to_d * 100).to_i if value.present?
end
+
+ private
+
+ def set_amount_cents_remaining
+ self.amount_cents_remaining = amount_cents if amount_cents_remaining.nil? && amount_cents.present?
+ end
end
diff --git a/app/views/payments/index.html.erb b/app/views/payments/index.html.erb
index 0b1bab967..f37171d25 100644
--- a/app/views/payments/index.html.erb
+++ b/app/views/payments/index.html.erb
@@ -22,7 +22,7 @@
<%= payment.payer.name %>
$<%= "%.2f" % payment.amount_dollars %>
$<%= "%.2f" % payment.remaining_dollars %>
-
<%= payment.created_at.strftime("%B %d, %Y") %>
+
<%= payment.created_at.strftime("%B %d, %Y at %I:%M %p") %>
<%= link_to "View", payment_path(payment), class: "text-primary hover:text-primary-dark" %>
<% end %>
diff --git a/db/migrate/20260408163911_remove_default_from_amount_cents_remaining_on_payments.rb b/db/migrate/20260408163911_remove_default_from_amount_cents_remaining_on_payments.rb
new file mode 100644
index 000000000..1c004da56
--- /dev/null
+++ b/db/migrate/20260408163911_remove_default_from_amount_cents_remaining_on_payments.rb
@@ -0,0 +1,5 @@
+class RemoveDefaultFromAmountCentsRemainingOnPayments < ActiveRecord::Migration[8.1]
+ def change
+ change_column_default :payments, :amount_cents_remaining, from: 0, to: nil
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index d0170690e..3a8cf7228 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -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_04_07_231104) do
+ActiveRecord::Schema[8.1].define(version: 2026_04_08_163911) 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
@@ -789,7 +789,7 @@
create_table "payments", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
t.integer "amount_cents", null: false
- t.integer "amount_cents_remaining", default: 0, null: false
+ t.integer "amount_cents_remaining", null: false
t.string "check_number"
t.datetime "created_at", null: false
t.string "currency", default: "usd", null: false
From fb6ed64cd8d2bc0fd3cfe4af7e29c6f0e0e2574f Mon Sep 17 00:00:00 2001
From: Justin Miller <16829344+jmilljr24@users.noreply.github.com>
Date: Thu, 9 Apr 2026 07:31:26 -0400
Subject: [PATCH 40/74] refactor search type selector to be generic
---
.../controllers/search_type_select_controller.js | 12 +++++-------
app/views/allocations/new.html.erb | 14 +++++++++-----
app/views/refunds/new.html.erb | 4 ++--
3 files changed, 16 insertions(+), 14 deletions(-)
diff --git a/app/frontend/javascript/controllers/search_type_select_controller.js b/app/frontend/javascript/controllers/search_type_select_controller.js
index 17d60a4af..31ec0ecd5 100644
--- a/app/frontend/javascript/controllers/search_type_select_controller.js
+++ b/app/frontend/javascript/controllers/search_type_select_controller.js
@@ -1,19 +1,17 @@
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
- static targets = ["person", "organization", "pickerContainer"];
+ static targets = ["template", "pickerContainer"];
toggle(event) {
const selectedType = event.target.value;
this.pickerContainerTarget.innerHTML = "";
- if (selectedType === "Person") {
- const template = this.personTarget.content.cloneNode(true);
- this.pickerContainerTarget.appendChild(template);
- } else if (selectedType === "Organization") {
- const template = this.organizationTarget.content.cloneNode(true);
- this.pickerContainerTarget.appendChild(template);
+ const template = this.templateTargets.find(t => t.dataset.type === selectedType);
+ if (template) {
+ const cloned = template.content.cloneNode(true);
+ this.pickerContainerTarget.appendChild(cloned);
}
}
}
diff --git a/app/views/allocations/new.html.erb b/app/views/allocations/new.html.erb
index 727e39d8c..94f7ce741 100644
--- a/app/views/allocations/new.html.erb
+++ b/app/views/allocations/new.html.erb
@@ -1,4 +1,4 @@
-
+
New Allocation
<%= simple_form_for @allocation, url: allocations_path do |f| %>
<%= f.hidden_field :source_type %>
@@ -15,14 +15,18 @@
Error: No payment source found
<% end %>
-
+ <%= f.input :allocatable_type, as: :select, collection: ["EventRegistration"],
+ include_blank: true,
+ input_html: { data: { action: "search-type-select#toggle" } } %>
+
<%= label_tag "allocation[allocatable_id]", "Allocate To", class: "block text-sm font-medium text-gray-700 mb-1" %>
<%= select_tag "allocation[allocatable_id]",
options_from_collection_for_select(@event_registrations, :id, :id),
include_blank: "Select Event Registration",
- class: "w-full" %>
- <%= hidden_field_tag "allocation[allocatable_type]", "EventRegistration" %>
-
+ class: "w-full",
+ data: { controller: "remote-select", remote_select_model_value: "event_registration" } %>
+
+
<%= f.input :amount_dollars, label: "Amount ($)", input_html: { min: 0, step: 0.01, placeholder: "0.00", data: { controller: "currency-input", action: "currency-input#format" } } %>
<%= f.button :submit, "Create Allocation", class: "btn btn-primary" %>
diff --git a/app/views/refunds/new.html.erb b/app/views/refunds/new.html.erb
index 2623327e4..039f51540 100644
--- a/app/views/refunds/new.html.erb
+++ b/app/views/refunds/new.html.erb
@@ -15,7 +15,7 @@
<% end %>
<%= f.input :recipient_type, as: :select, collection: ["Person", "Organization"], include_blank: true, input_html: { data: { action: "search-type-select#toggle" } } %>
-
+
<%= f.input :recipient_id,
collection: [],
include_blank: true,
@@ -28,7 +28,7 @@
prompt: "Search for a person",
label: "Recipient" %>
-
+
<%= f.input :recipient_id,
collection: [],
include_blank: true,
From 587295e624b3fb63fee5d5e5493b6f8191f5ecfa Mon Sep 17 00:00:00 2001
From: Justin Miller <16829344+jmilljr24@users.noreply.github.com>
Date: Thu, 9 Apr 2026 07:54:23 -0400
Subject: [PATCH 41/74] add join scope to remote search concern
---
app/controllers/search_controller.rb | 3 +-
app/models/concerns/remote_searchable.rb | 36 +++++++++++++++---------
app/models/event_registration.rb | 30 ++++++++++++++++++++
app/views/allocations/new.html.erb | 25 ++++++++++++----
4 files changed, 74 insertions(+), 20 deletions(-)
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index f181c56f6..bbe54dc05 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -38,7 +38,8 @@ def allowed_model(model_param)
"person" => Person,
"user" => User,
"workshop" => Workshop,
- "organization" => Organization
+ "organization" => Organization,
+ "event_registration" => EventRegistration
}[model_param]
end
diff --git a/app/models/concerns/remote_searchable.rb b/app/models/concerns/remote_searchable.rb
index c7554e7de..898c30d83 100644
--- a/app/models/concerns/remote_searchable.rb
+++ b/app/models/concerns/remote_searchable.rb
@@ -2,31 +2,41 @@ module RemoteSearchable
extend ActiveSupport::Concern
class_methods do
- def remote_searchable_by(*columns)
+ def remote_searchable_by(*columns, **options)
@remote_search_columns = columns.map(&:to_s)
+ @remote_search_scope = options[:scope]
end
def remote_search_columns
@remote_search_columns || []
end
+ def remote_search_scope
+ @remote_search_scope
+ end
+
def remote_search(query)
return none if query.blank?
- raise "remote_searchable_by not defined for #{name}" if remote_search_columns.empty?
- words = query.split.flat_map { |w| w.split(/[\s\-]+/) }.reject(&:blank?)
- return none if words.blank?
+ if remote_search_scope
+ instance_exec(query, &remote_search_scope)
+ else
+ raise "remote_searchable_by not defined for #{name}" if remote_search_columns.empty?
- conditions = words.each_with_index.map do |word, i|
- bind_var = "pattern_#{i}".to_sym
- column_conditions = remote_search_columns.map { |column| "#{table_name}.#{column} LIKE :#{bind_var}" }
- "(#{column_conditions.join(' OR ')})"
- end
- bindings = words.each_with_index.each_with_object({}) do |(word, i), hash|
- hash["pattern_#{i}".to_sym] = "%#{word}%"
+ words = query.split.flat_map { |w| w.split(/[\s\-]+/) }.reject(&:blank?)
+ return none if words.blank?
+
+ conditions = words.each_with_index.map do |word, i|
+ bind_var = "pattern_#{i}".to_sym
+ column_conditions = remote_search_columns.map { |column| "#{table_name}.#{column} LIKE :#{bind_var}" }
+ "(#{column_conditions.join(' OR ')})"
+ end
+ bindings = words.each_with_index.each_with_object({}) do |(word, i), hash|
+ hash["pattern_#{i}".to_sym] = "%#{word}%"
+ end
+ where(conditions.join(" AND "), bindings)
+ .order(remote_search_columns.index_with { :asc })
end
- where(conditions.join(" AND "), bindings)
- .order(remote_search_columns.index_with { :asc })
end
end
diff --git a/app/models/event_registration.rb b/app/models/event_registration.rb
index 9f97d1b89..3ed03cea2 100644
--- a/app/models/event_registration.rb
+++ b/app/models/event_registration.rb
@@ -1,4 +1,6 @@
class EventRegistration < ApplicationRecord
+ include RemoteSearchable
+
belongs_to :registrant, class_name: "Person"
belongs_to :event
has_many :comments, -> { newest_first }, as: :commentable, dependent: :destroy
@@ -122,6 +124,34 @@ def attendance_status_label
end
end
+ remote_searchable_by :registrant,
+ scope: ->(query) {
+ return none if query.blank?
+
+ words = query.split.flat_map { |w| w.split(/[\s\-]+/) }.reject(&:blank?)
+ return none if words.blank?
+
+ pattern = "%#{words.join('%')}%"
+ active
+ .joins(:registrant, :event)
+ .where(
+ "people.first_name LIKE :p
+ OR people.last_name LIKE :p
+ OR people.email LIKE :p
+ OR people.legal_first_name LIKE :p
+ OR people.email_2 LIKE :p
+ OR events.title LIKE :p",
+ p: pattern
+ )
+ }
+
+ def remote_search_label
+ {
+ id: id,
+ label: "#{registrant.full_name} - #{event.title} (##{id})"
+ }
+ end
+
private
def snapshot_registrant_organizations
diff --git a/app/views/allocations/new.html.erb b/app/views/allocations/new.html.erb
index 94f7ce741..e077ab80d 100644
--- a/app/views/allocations/new.html.erb
+++ b/app/views/allocations/new.html.erb
@@ -1,9 +1,12 @@
New Allocation
+
<%= simple_form_for @allocation, url: allocations_path do |f| %>
<%= f.hidden_field :source_type %>
<%= f.hidden_field :source_id %>
+
<% source = @source || (@allocation.source if @allocation.source.present?) %>
+
<% if source.present? && source.is_a?(Payment) %>
From Payment
@@ -15,21 +18,31 @@
Error: No payment source found
<% end %>
+
<%= f.input :allocatable_type, as: :select, collection: ["EventRegistration"],
include_blank: true,
input_html: { data: { action: "search-type-select#toggle" } } %>
+
- <%= label_tag "allocation[allocatable_id]", "Allocate To", class: "block text-sm font-medium text-gray-700 mb-1" %>
- <%= select_tag "allocation[allocatable_id]",
- options_from_collection_for_select(@event_registrations, :id, :id),
- include_blank: "Select Event Registration",
- class: "w-full",
- data: { controller: "remote-select", remote_select_model_value: "event_registration" } %>
+ <%= f.input :allocatable_id,
+ collection: [],
+ include_blank: true,
+ input_html: {
+ data: {
+ controller: "remote-select",
+ remote_select_model_value: "event_registration"
+ }
+ },
+ prompt: "Search for a Person or Event",
+ label: "Allocatable" %>
+
<%= f.input :amount_dollars, label: "Amount ($)", input_html: { min: 0, step: 0.01, placeholder: "0.00", data: { controller: "currency-input", action: "currency-input#format" } } %>
+
<%= f.button :submit, "Create Allocation", class: "btn btn-primary" %>
+
<% if source.present? %>
<%= link_to "Cancel", payment_path(source), class: "btn btn-secondary ml-2" %>
<% else %>
From 57ee01f221b7448dcb3a76cced96e5de7560a0e8 Mon Sep 17 00:00:00 2001
From: Justin Miller <16829344+jmilljr24@users.noreply.github.com>
Date: Thu, 9 Apr 2026 08:55:39 -0400
Subject: [PATCH 42/74] create payment seeds
---
db/seeds.rb | 6 ++
db/seeds/payments.rb | 167 +++++++++++++++++++++++++++++++++++++++++++
2 files changed, 173 insertions(+)
create mode 100644 db/seeds/payments.rb
diff --git a/db/seeds.rb b/db/seeds.rb
index 56758799a..df1eeaf91 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -1,6 +1,10 @@
# Disable email delivery during seeding
ActionMailer::Base.perform_deliveries = false
+def seed(file)
+ require_relative "seeds/#{file}"
+end
+
puts "Creating Users…"
# Helper: case-insensitive find-or-create by name
@@ -481,3 +485,5 @@ def find_or_create_by_name!(klass, name, **attrs, &block)
unless Form.standalone.exists?(name: ScholarshipApplicationFormBuilder::FORM_NAME)
ScholarshipApplicationFormBuilder.build_standalone!
end
+
+seed "payments"
diff --git a/db/seeds/payments.rb b/db/seeds/payments.rb
new file mode 100644
index 000000000..b77b7f612
--- /dev/null
+++ b/db/seeds/payments.rb
@@ -0,0 +1,167 @@
+ # Payment seeds - can be run independently
+ # Usage: "rails runner db/seeds/payments.rb"
+
+ puts "Seeding Payments, Allocations, and Refunds..."
+
+ payment_ids_start = Payment.maximum(:id) || 0
+ allocation_ids_start = Allocation.maximum(:id) || 0
+ refund_ids_start = Refund.maximum(:id) || 0
+ event_reg_ids_start = EventRegistration.maximum(:id) || 0
+
+ Refund.where("id > ?", refund_ids_start).delete_all
+ Allocation.where("id > ?", allocation_ids_start).delete_all
+ Payment.where("id > ?", payment_ids_start).delete_all
+ EventRegistration.where("id > ?", event_reg_ids_start).delete_all
+
+ event_cost_cents = 150000
+
+ bob = Person.find_or_create_by!(email: "bob.payment@seed.example.com") do |p|
+ p.first_name = "Bob"
+ p.last_name = "Barker"
+ end
+
+ alice = Person.find_or_create_by!(email: "alice.payment@seed.example.com") do |p|
+ p.first_name = "Alice"
+ p.last_name = "Test"
+ end
+
+ charlie = Person.find_or_create_by!(email: "charlie.payment@seed.example.com") do |p|
+ p.first_name = "Charlie"
+ p.last_name = "Test"
+ end
+
+ diana = Person.find_or_create_by!(email: "diana.payment@seed.example.com") do |p|
+ p.first_name = "Diana"
+ p.last_name = "Test"
+ end
+
+ eve = Person.find_or_create_by!(email: "eve.payment@seed.example.com") do |p|
+ p.first_name = "Eve"
+ p.last_name = "Test"
+ end
+
+ frank = Person.find_or_create_by!(email: "frank.payment@seed.example.com") do |p|
+ p.first_name = "Frank"
+ p.last_name = "Test"
+ end
+
+ gary = Person.find_or_create_by!(email: "gary.payment@seed.example.com") do |p|
+ p.first_name = "Gary"
+ p.last_name = "Test"
+ end
+
+ holly = Person.find_or_create_by!(email: "holly.payment@seed.example.com") do |p|
+ p.first_name = "Holly"
+ p.last_name = "Test"
+ end
+
+ iris = Person.find_or_create_by!(email: "iris.payment@seed.example.com") do |p|
+ p.first_name = "Iris"
+ p.last_name = "Test"
+ end
+
+ event = Event.find_or_create_by!(
+ title: "Payment Test Workshop",
+ start_date: (1).months.from_now,
+ end_date: (1).months.from_now + 2.days,
+ published: true,
+ cost_cents: event_cost_cents)
+
+ reg_bob = EventRegistration.find_or_create_by!(registrant: bob, event: event)
+ reg_alice = EventRegistration.find_or_create_by!(registrant: alice, event: event)
+ reg_charlie = EventRegistration.find_or_create_by!(registrant: charlie, event: event)
+ reg_diana = EventRegistration.find_or_create_by!(registrant: diana, event: event)
+ reg_eve = EventRegistration.find_or_create_by!(registrant: eve, event: event)
+ reg_frank = EventRegistration.find_or_create_by!(registrant: frank, event: event)
+ reg_gary = EventRegistration.find_or_create_by!(registrant: gary, event: event)
+ reg_iris = EventRegistration.find_or_create_by!(registrant: iris, event: event)
+
+ puts " Self-pay (full allocation)"
+ payment1 = CashPayment.find_or_create_by!(
+ payer: bob,
+ amount_cents: event_cost_cents,
+ amount_cents_remaining: 0
+ ) do |p|
+ p.created_at = 5.days.ago
+ end
+ Allocation.find_or_create_by!(
+ source: payment1,
+ allocatable: reg_bob,
+ amount: event_cost_cents
+ ) do |a|
+ a.created_at = 5.days.ago
+ end
+
+ puts " Overpayment with full allocation (Alice pays $6000, covers 4 people)"
+ payment2 = CashPayment.find_or_create_by!(
+ payer: alice,
+ amount_cents: 600000,
+ amount_cents_remaining: 0
+ ) do |p|
+ p.created_at = 4.days.ago
+ end
+ Allocation.find_or_create_by!(source: payment2, allocatable: reg_alice, amount: event_cost_cents) { |a| a.created_at = 4.days.ago }
+ Allocation.find_or_create_by!(source: payment2, allocatable: reg_charlie, amount: event_cost_cents) { |a| a.created_at = 4.days.ago }
+ Allocation.find_or_create_by!(source: payment2, allocatable: reg_diana, amount: event_cost_cents) { |a| a.created_at = 4.days.ago }
+ Allocation.find_or_create_by!(source: payment2, allocatable: reg_eve, amount: event_cost_cents) { |a| a.created_at = 4.days.ago }
+
+ puts " Payment with remaining available ($2000 payment, $1500 allocated, $500 remaining)"
+ payment3 = CashPayment.find_or_create_by!(
+ payer: frank,
+ amount_cents: 200000,
+ amount_cents_remaining: 50000
+ ) do |p|
+ p.created_at = 3.days.ago
+ end
+ Allocation.find_or_create_by!(
+ source: payment3,
+ allocatable: reg_frank,
+ amount: 150000
+ ) do |a|
+ a.created_at = 3.days.ago
+ end
+
+ puts " Full refund ($1500 payment, fully allocated, fully refunded)"
+ payment4 = CashPayment.find_or_create_by!(
+ payer: gary,
+ amount_cents: event_cost_cents,
+ amount_cents_remaining: 0
+ ) do |p|
+ p.created_at = 2.days.ago
+ end
+ Allocation.find_or_create_by!(source: payment4, allocatable: reg_gary, amount: event_cost_cents) { |a| a.created_at = 2.days.ago }
+ Refund.find_or_create_by!(
+ refundable: payment4,
+ recipient: gary,
+ amount_cents: event_cost_cents,
+ method: "check"
+ ) do |r|
+ r.created_at = 1.day.ago
+ end
+
+ puts " Creating Scenario 8: Payment with no allocations ($10000, full amount remaining)"
+ CashPayment.find_or_create_by!(
+ payer: holly,
+ amount_cents: 1000000,
+ amount_cents_remaining: 1000000
+ ) do |p|
+ p.created_at = 7.days.ago
+ end
+
+ puts " Partial payment"
+ payment9 = CashPayment.find_or_create_by!(
+ payer: iris,
+ amount_cents: 100000,
+ amount_cents_remaining: 0
+ ) do |p|
+ p.created_at = 3.days.ago
+ end
+ Allocation.find_or_create_by!(
+ source: payment9,
+ allocatable: reg_iris,
+ amount: 100000
+ ) do |a|
+ a.created_at = 3.days.ago
+ end
+
+ puts " Payment seeds complete!"
From aa2b24fef0c6644a8d4e1969a87e7c81f48f0f70 Mon Sep 17 00:00:00 2001
From: Justin Miller <16829344+jmilljr24@users.noreply.github.com>
Date: Thu, 9 Apr 2026 09:05:53 -0400
Subject: [PATCH 43/74] add seed for unallocation
---
db/seeds/payments.rb | 20 +++++++++++++-------
1 file changed, 13 insertions(+), 7 deletions(-)
diff --git a/db/seeds/payments.rb b/db/seeds/payments.rb
index b77b7f612..f0031bfdb 100644
--- a/db/seeds/payments.rb
+++ b/db/seeds/payments.rb
@@ -76,21 +76,27 @@
reg_gary = EventRegistration.find_or_create_by!(registrant: gary, event: event)
reg_iris = EventRegistration.find_or_create_by!(registrant: iris, event: event)
- puts " Self-pay (full allocation)"
+ puts " Payment made but allocation reverted)"
payment1 = CashPayment.find_or_create_by!(
payer: bob,
amount_cents: event_cost_cents,
- amount_cents_remaining: 0
+ amount_cents_remaining: event_cost_cents
) do |p|
p.created_at = 5.days.ago
end
- Allocation.find_or_create_by!(
+ original_allocation1 = Allocation.create!(
source: payment1,
allocatable: reg_bob,
- amount: event_cost_cents
- ) do |a|
- a.created_at = 5.days.ago
- end
+ amount: event_cost_cents,
+ created_at: 5.days.ago
+ )
+ reversal_allocation1 = Allocation.create!(
+ source: payment1,
+ allocatable: reg_bob,
+ amount: -event_cost_cents,
+ created_at: 4.days.ago
+ )
+ original_allocation1.update!(reverted_id: reversal_allocation1.id)
puts " Overpayment with full allocation (Alice pays $6000, covers 4 people)"
payment2 = CashPayment.find_or_create_by!(
From c08322b9c3a077c7f3ebb532455398e688a997fc Mon Sep 17 00:00:00 2001
From: Justin Miller <16829344+jmilljr24@users.noreply.github.com>
Date: Thu, 9 Apr 2026 09:22:29 -0400
Subject: [PATCH 44/74] fix event create seed
---
db/seeds/payments.rb | 13 +++++++------
1 file changed, 7 insertions(+), 6 deletions(-)
diff --git a/db/seeds/payments.rb b/db/seeds/payments.rb
index f0031bfdb..ab4ceba7c 100644
--- a/db/seeds/payments.rb
+++ b/db/seeds/payments.rb
@@ -60,12 +60,13 @@
p.last_name = "Test"
end
- event = Event.find_or_create_by!(
- title: "Payment Test Workshop",
- start_date: (1).months.from_now,
- end_date: (1).months.from_now + 2.days,
- published: true,
- cost_cents: event_cost_cents)
+ event = Event.find_or_create_by!(title: "Payment Test Workshop") do |e|
+ e.start_date = 1.month.from_now.to_date
+ e.end_date = 1.month.from_now.to_date + 2.days
+ e.published = true
+ e.cost_cents = event_cost_cents
+ end
+ event.update!(start_date: 1.month.from_now.to_date, end_date: 1.month.from_now.to_date + 2.days, published: true, cost_cents: event_cost_cents)
reg_bob = EventRegistration.find_or_create_by!(registrant: bob, event: event)
reg_alice = EventRegistration.find_or_create_by!(registrant: alice, event: event)
From 454f868697fc1ea07508bc215b6f58a78cbbf68e Mon Sep 17 00:00:00 2001
From: Justin Miller <16829344+jmilljr24@users.noreply.github.com>
Date: Thu, 9 Apr 2026 09:27:11 -0400
Subject: [PATCH 45/74] add cursor to revert button
---
app/views/payments/show.html.erb | 26 ++++++++++++++++++++++++--
1 file changed, 24 insertions(+), 2 deletions(-)
diff --git a/app/views/payments/show.html.erb b/app/views/payments/show.html.erb
index 9f40caa5a..5b0a9b367 100644
--- a/app/views/payments/show.html.erb
+++ b/app/views/payments/show.html.erb
@@ -1,52 +1,64 @@
Payment
+
<% if @payment.amount_cents_remaining > 0 %>
<%= link_to "Allocate", new_allocation_path(source_sgid: @payment.to_sgid.to_s), class: "btn btn-primary" %>
<%= link_to "Refund", new_refund_path(payment_sgid: @payment.to_sgid.to_s), class: "btn btn-warning" %>
<% end %>
+
<%= link_to "Back", payments_path, class: "btn btn-secondary" %>
+
Type
<%= @payment.type.underscore.titleize %>
+
Payer
<%= @payment.payer.name %>
+
Amount
$<%= "%.2f" % @payment.amount_dollars %>
+
Amount remaining to allocate
$<%= "%.2f" % @payment.remaining_dollars %>
+
<% if @payment.check_number.present? %>
Check Number
<%= @payment.check_number %>
<% end %>
+
<% if @payment.pay_charge_id.present? %>
Pay Charge ID
<%= @payment.pay_charge_id %>
<% end %>
+
Created
<%= @payment.created_at.strftime("%B %d, %Y at %I:%M %p") %>
+
<% allocations = @payment.allocations.order(created_at: :desc).to_a %>
+
<% if allocations.any? %>
Allocations
+
@@ -57,21 +69,26 @@
Actions
+
<% allocations.each do |allocation| %>
<% if allocation.allocatable_type == "EventRegistration" %>
- <%= allocation.allocatable.registrant.full_name %> - <%= allocation.allocatable.event.title %>
+ <%= allocation.allocatable.registrant.full_name %>
+ -
+ <%= allocation.allocatable.event.title %>
<% else %>
<%= allocation.allocatable_type.underscore.titleize %>
<% end %>
+
$<%= "%.2f" % (allocation.amount.to_d / 100) %>
<%= allocation.created_at.strftime("%B %d, %Y at %I:%M %p") %>
+
<% unless allocation.reverted? || allocation.amount.to_i < 0 %>
- <%= button_to "Revert Allocation", revert_allocation_path(allocation), method: :post, data: { confirm: "Are you sure you want to revert this allocation? The funds will be returned to the payment." }, class: "text-danger hover:text-danger-dark" %>
+ <%= button_to "Revert", revert_allocation_path(allocation), method: :post, class: "cursor-pointer text-danger hover:text-danger-dark", data: { turbo_confirm: "Are you sure you want to revert this allocation? The funds will be returned to the payment." } %>
<% end %>
@@ -81,10 +98,13 @@
<% end %>
+
<% refunds = @payment.refunds.order(created_at: :desc).to_a %>
+
<% if refunds.any? %>
Refunds
+
@@ -95,6 +115,7 @@
Date/Time
+
<% refunds.each do |refund| %>
@@ -105,6 +126,7 @@
<%= refund.recipient_type.underscore.titleize %>
<% end %>
+
<%= refund.method.titleize %>
$<%= "%.2f" % refund.amount_dollars %>
<%= refund.created_at.strftime("%B %d, %Y at %I:%M %p") %>
From 19290580958e20801630997dad525e90e65f5c39 Mon Sep 17 00:00:00 2001
From: Justin Miller <16829344+jmilljr24@users.noreply.github.com>
Date: Thu, 9 Apr 2026 09:32:24 -0400
Subject: [PATCH 46/74] more refund query to controller
---
app/controllers/payments_controller.rb | 2 ++
app/views/payments/show.html.erb | 37 +++++++-------------------
2 files changed, 12 insertions(+), 27 deletions(-)
diff --git a/app/controllers/payments_controller.rb b/app/controllers/payments_controller.rb
index 2d0c1601b..552286e9b 100644
--- a/app/controllers/payments_controller.rb
+++ b/app/controllers/payments_controller.rb
@@ -52,6 +52,8 @@ def create
def show
@payment = Payment.find(params[:id])
+ @allocations = @payment.allocations.order(created_at: :desc)
+ @refunds = @payment.refunds.order(created_at: :desc)
authorize! @payment, with: PaymentPolicy
end
diff --git a/app/views/payments/show.html.erb b/app/views/payments/show.html.erb
index 5b0a9b367..4351fcd07 100644
--- a/app/views/payments/show.html.erb
+++ b/app/views/payments/show.html.erb
@@ -1,64 +1,51 @@
Payment
-
<% if @payment.amount_cents_remaining > 0 %>
<%= link_to "Allocate", new_allocation_path(source_sgid: @payment.to_sgid.to_s), class: "btn btn-primary" %>
<%= link_to "Refund", new_refund_path(payment_sgid: @payment.to_sgid.to_s), class: "btn btn-warning" %>
<% end %>
-
<%= link_to "Back", payments_path, class: "btn btn-secondary" %>
-
Type
<%= @payment.type.underscore.titleize %>
-
Payer
<%= @payment.payer.name %>
-
Amount
$<%= "%.2f" % @payment.amount_dollars %>
-
Amount remaining to allocate
$<%= "%.2f" % @payment.remaining_dollars %>
-
<% if @payment.check_number.present? %>
Check Number
<%= @payment.check_number %>
<% end %>
-
<% if @payment.pay_charge_id.present? %>
Pay Charge ID
<%= @payment.pay_charge_id %>
<% end %>
-
Created
<%= @payment.created_at.strftime("%B %d, %Y at %I:%M %p") %>
-
- <% allocations = @payment.allocations.order(created_at: :desc).to_a %>
-
- <% if allocations.any? %>
+ <% if @allocations.any? %>
Allocations
-
@@ -69,10 +56,14 @@
Actions
-
- <% allocations.each do |allocation| %>
-
+ <% @allocations.each do |allocation| %>
+ <% row_class = if allocation.amount.to_i < 0
+ 'bg-red-50'
+ elsif allocation.reverted_id.present?
+ 'bg-gray-100 border border-gray-300 opacity-60'
+ end %>
+
<% if allocation.allocatable_type == "EventRegistration" %>
<%= allocation.allocatable.registrant.full_name %>
@@ -82,10 +73,8 @@
<%= allocation.allocatable_type.underscore.titleize %>
<% end %>
-
$<%= "%.2f" % (allocation.amount.to_d / 100) %>
<%= allocation.created_at.strftime("%B %d, %Y at %I:%M %p") %>
-
<% unless allocation.reverted? || allocation.amount.to_i < 0 %>
<%= button_to "Revert", revert_allocation_path(allocation), method: :post, class: "cursor-pointer text-danger hover:text-danger-dark", data: { turbo_confirm: "Are you sure you want to revert this allocation? The funds will be returned to the payment." } %>
@@ -98,13 +87,9 @@
<% end %>
-
- <% refunds = @payment.refunds.order(created_at: :desc).to_a %>
-
- <% if refunds.any? %>
+ <% if @refunds.any? %>
Refunds
-
@@ -115,9 +100,8 @@
Date/Time
-
- <% refunds.each do |refund| %>
+ <% @refunds.each do |refund| %>
<% if refund.recipient.present? %>
@@ -126,7 +110,6 @@
<%= refund.recipient_type.underscore.titleize %>
<% end %>
-
<%= refund.method.titleize %>
$<%= "%.2f" % refund.amount_dollars %>
<%= refund.created_at.strftime("%B %d, %Y at %I:%M %p") %>
From 8fa8800f80e31602a3fb5696055d2e7a9c0ba48a Mon Sep 17 00:00:00 2001
From: Justin Miller <16829344+jmilljr24@users.noreply.github.com>
Date: Thu, 9 Apr 2026 15:37:43 -0400
Subject: [PATCH 47/74] add search boxes for payments
---
app/controllers/payments_controller.rb | 2 +-
app/models/payment.rb | 20 ++++++
app/views/payments/_search_boxes.html.erb | 81 +++++++++++++++++++++++
app/views/payments/index.html.erb | 5 ++
4 files changed, 107 insertions(+), 1 deletion(-)
create mode 100644 app/views/payments/_search_boxes.html.erb
diff --git a/app/controllers/payments_controller.rb b/app/controllers/payments_controller.rb
index 552286e9b..4e42e56e5 100644
--- a/app/controllers/payments_controller.rb
+++ b/app/controllers/payments_controller.rb
@@ -2,7 +2,7 @@ class PaymentsController < ApplicationController
def index
authorize!
per_page = params[:number_of_items_per_page].presence || 10
- @payments = Payment.order(created_at: :desc).paginate(page: params[:page], per_page: per_page)
+ @payments = Payment.search_by_params(params).order(created_at: :desc).paginate(page: params[:page], per_page: per_page)
end
def new
diff --git a/app/models/payment.rb b/app/models/payment.rb
index 36e183317..b562b5903 100644
--- a/app/models/payment.rb
+++ b/app/models/payment.rb
@@ -10,6 +10,26 @@ class Payment < ApplicationRecord
before_validation :set_amount_cents_remaining, if: :new_record?
+ scope :by_type, ->(types) {
+ types = types.split(",") if types.is_a?(String)
+ where(type: types) if types.present?
+ }
+ scope :by_payer, ->(payer_type, payer_id) { where(payer_type: payer_type, payer_id: payer_id) if payer_type.present? && payer_id.present? }
+ scope :has_remaining, ->(value) {
+ case value
+ when "yes" then where.arel_table[:amount_cents_remaining].gt(0)
+ when "no" then where.arel_table[:amount_cents_remaining].eq(0).or(where.arel_table[:amount_cents_remaining].eq(nil))
+ end
+ }
+
+ def self.search_by_params(params)
+ results = all
+ results = results.by_type(params[:type]) if params[:type].present?
+ results = results.by_payer(params[:payer_type], params[:payer_id]) if params[:payer_type].present? && params[:payer_id].present?
+ results = results.has_remaining(params[:has_remaining]) if params[:has_remaining].present? && params[:has_remaining] != "all"
+ results
+ end
+
def amount_dollars
amount_cents.to_d / 100 if amount_cents
end
diff --git a/app/views/payments/_search_boxes.html.erb b/app/views/payments/_search_boxes.html.erb
new file mode 100644
index 000000000..c12840cb0
--- /dev/null
+++ b/app/views/payments/_search_boxes.html.erb
@@ -0,0 +1,81 @@
+
+
+ <%= form_tag payments_path, method: :get, class: "space-y-4", data: { controller: "search-type-select" }, autocomplete: "off" do %>
+
+
+
Type
+
+
+
+ Type
+
+
+
+
+
<%= check_box_tag "type[]", "CashPayment", Array(params[:type]).include?("CashPayment"), id: "type_CashPayment", class: "category-checkbox" %><%= label_tag "type_CashPayment", "Cash Payment", class: "text-sm text-gray-700" %>
+
<%= check_box_tag "type[]", "CheckPayment", Array(params[:type]).include?("CheckPayment"), id: "type_CheckPayment", class: "category-checkbox" %><%= label_tag "type_CheckPayment", "Check Payment", class: "text-sm text-gray-700" %>
+
+
+
+
+
+ Amount Remaining
+
+ <%= select_tag "has_remaining",
+ options_for_select({ "All" => "all", "Yes" => "yes", "No" => "no" }, params[:has_remaining] || "all"),
+ class: "w-full bg-white border border-gray-300 rounded-lg px-3 py-2 focus:ring-blue-500 focus:border-blue-500" %>
+
+
+
+ Payer Type
+
+ <%= select_tag "payer_type",
+ options_for_select(["Person", "Organization"], params[:payer_type]),
+ include_blank: true,
+ class: "w-full bg-white border border-gray-300 rounded-lg px-3 py-2 focus:ring-blue-500 focus:border-blue-500",
+ data: { action: "search-type-select#toggle" } %>
+
+
+
+
+ Payer
+
+ <%= select_tag "payer_id",
+ options_for_select(Person.where(id: params[:payer_id]).map { |p| [p.full_name, p.id] }),
+ include_blank: true,
+ class: "w-full bg-white border border-gray-300 rounded-lg px-3 py-2 focus:ring-blue-500 focus:border-blue-500",
+ data: {
+ controller: "remote-select",
+ remote_select_model_value: "person"
+ },
+ prompt: "Search for a person" %>
+
+
+
+
+
+ Payer
+
+ <%= select_tag "payer_id",
+ options_for_select(Organization.where(id: params[:payer_id]).map { |o| [o.name, o.id] }),
+ include_blank: true,
+ class: "w-full bg-white border border-gray-300 rounded-lg px-3 py-2 focus:ring-blue-500 focus:border-blue-500",
+ data: {
+ controller: "remote-select",
+ remote_select_model_value: "organization"
+ },
+ prompt: "Search for an organization" %>
+
+
+
+
+
+
<%= submit_tag "Search", class: "btn btn-primary" %><%= link_to "Clear", payments_path, class: "btn btn-secondary" %>
+
+ <% end %>
+
diff --git a/app/views/payments/index.html.erb b/app/views/payments/index.html.erb
index f37171d25..167dd362a 100644
--- a/app/views/payments/index.html.erb
+++ b/app/views/payments/index.html.erb
@@ -3,6 +3,9 @@
Payments
<%= link_to "Add Cash Payment", new_payment_path(type: "CashPayment"), class: "btn btn-secondary" %><%= link_to "Add Check Payment", new_payment_path(type: "CheckPayment"), class: "btn btn-secondary" %>
+
+ <%= render 'search_boxes' %>
+
@@ -15,6 +18,7 @@
Actions
+
<% @payments.each do |payment| %>
@@ -29,5 +33,6 @@
+
From 8928dad360f0ccbd01b1103a3569d4e349463ec4 Mon Sep 17 00:00:00 2001
From: Justin Miller <16829344+jmilljr24@users.noreply.github.com>
Date: Thu, 9 Apr 2026 15:42:39 -0400
Subject: [PATCH 48/74] fix search on amount remaining
---
app/models/payment.rb | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/models/payment.rb b/app/models/payment.rb
index b562b5903..91672457a 100644
--- a/app/models/payment.rb
+++ b/app/models/payment.rb
@@ -17,8 +17,8 @@ class Payment < ApplicationRecord
scope :by_payer, ->(payer_type, payer_id) { where(payer_type: payer_type, payer_id: payer_id) if payer_type.present? && payer_id.present? }
scope :has_remaining, ->(value) {
case value
- when "yes" then where.arel_table[:amount_cents_remaining].gt(0)
- when "no" then where.arel_table[:amount_cents_remaining].eq(0).or(where.arel_table[:amount_cents_remaining].eq(nil))
+ when "yes" then where("amount_cents_remaining > 0")
+ when "no" then where("amount_cents_remaining = 0")
end
}
From eaab324ec6e22a2d3f48b2e006da44c1f81929e7 Mon Sep 17 00:00:00 2001
From: Justin Miller <16829344+jmilljr24@users.noreply.github.com>
Date: Thu, 9 Apr 2026 15:49:42 -0400
Subject: [PATCH 49/74] fix dropdown toggle
---
app/views/payments/_search_boxes.html.erb | 13 +------------
1 file changed, 1 insertion(+), 12 deletions(-)
diff --git a/app/views/payments/_search_boxes.html.erb b/app/views/payments/_search_boxes.html.erb
index c12840cb0..da043abb6 100644
--- a/app/views/payments/_search_boxes.html.erb
+++ b/app/views/payments/_search_boxes.html.erb
@@ -4,10 +4,10 @@
Type
-
Type
-
<%= check_box_tag "type[]", "CashPayment", Array(params[:type]).include?("CashPayment"), id: "type_CashPayment", class: "category-checkbox" %><%= label_tag "type_CashPayment", "Cash Payment", class: "text-sm text-gray-700" %>
<%= check_box_tag "type[]", "CheckPayment", Array(params[:type]).include?("CheckPayment"), id: "type_CheckPayment", class: "category-checkbox" %><%= label_tag "type_CheckPayment", "Check Payment", class: "text-sm text-gray-700" %>
-
Amount Remaining
-
<%= select_tag "has_remaining",
options_for_select({ "All" => "all", "Yes" => "yes", "No" => "no" }, params[:has_remaining] || "all"),
class: "w-full bg-white border border-gray-300 rounded-lg px-3 py-2 focus:ring-blue-500 focus:border-blue-500" %>
-
Payer Type
-
<%= select_tag "payer_type",
options_for_select(["Person", "Organization"], params[:payer_type]),
include_blank: true,
class: "w-full bg-white border border-gray-300 rounded-lg px-3 py-2 focus:ring-blue-500 focus:border-blue-500",
data: { action: "search-type-select#toggle" } %>
-
Payer
-
<%= select_tag "payer_id",
options_for_select(Person.where(id: params[:payer_id]).map { |p| [p.full_name, p.id] }),
include_blank: true,
@@ -56,11 +49,9 @@
prompt: "Search for a person" %>
-
Payer
-
<%= select_tag "payer_id",
options_for_select(Organization.where(id: params[:payer_id]).map { |o| [o.name, o.id] }),
include_blank: true,
@@ -72,9 +63,7 @@
prompt: "Search for an organization" %>
-
-
<%= submit_tag "Search", class: "btn btn-primary" %><%= link_to "Clear", payments_path, class: "btn btn-secondary" %>
<% end %>
From 6db7f34242781aba4f8355021abe210b9c2ed71f Mon Sep 17 00:00:00 2001
From: Justin Miller <16829344+jmilljr24@users.noreply.github.com>
Date: Thu, 9 Apr 2026 15:57:19 -0400
Subject: [PATCH 50/74] add turbo frame results
---
app/controllers/payments_controller.rb | 8 ++-
app/views/payments/_search_boxes.html.erb | 15 ++++-
app/views/payments/index.html.erb | 63 +++++++++++----------
app/views/payments/payment_results.html.erb | 33 +++++++++++
4 files changed, 85 insertions(+), 34 deletions(-)
create mode 100644 app/views/payments/payment_results.html.erb
diff --git a/app/controllers/payments_controller.rb b/app/controllers/payments_controller.rb
index 4e42e56e5..d685596ba 100644
--- a/app/controllers/payments_controller.rb
+++ b/app/controllers/payments_controller.rb
@@ -2,7 +2,13 @@ class PaymentsController < ApplicationController
def index
authorize!
per_page = params[:number_of_items_per_page].presence || 10
- @payments = Payment.search_by_params(params).order(created_at: :desc).paginate(page: params[:page], per_page: per_page)
+
+ if turbo_frame_request?
+ @payments = Payment.search_by_params(params).order(created_at: :desc).paginate(page: params[:page], per_page: per_page)
+ render :payment_results
+ else
+ @payments = Payment.search_by_params(params).order(created_at: :desc).paginate(page: params[:page], per_page: per_page)
+ end
end
def new
diff --git a/app/views/payments/_search_boxes.html.erb b/app/views/payments/_search_boxes.html.erb
index da043abb6..69fa6b06d 100644
--- a/app/views/payments/_search_boxes.html.erb
+++ b/app/views/payments/_search_boxes.html.erb
@@ -1,6 +1,14 @@
+<% default_btn = "text-gray-600 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-800 shadow-sm" %>
+<% selected_btn = "text-white bg-blue-600 border-blue-600 hover:bg-blue-700 hover:text-white shadow-md ring-2 ring-blue-300" %>
- <%= form_tag payments_path, method: :get, class: "space-y-4", data: { controller: "search-type-select" }, autocomplete: "off" do %>
+ <%= form_tag payments_path, method: :get, class: "space-y-4",
+ data: {
+ controller: "collection search-type-select",
+ turbo_frame: "payment_results",
+ collection_unselected_class: default_btn,
+ collection_selected_class: selected_btn
+ }, autocomplete: "off" do %>
Type
@@ -64,7 +72,10 @@
-
<%= submit_tag "Search", class: "btn btn-primary" %><%= link_to "Clear", payments_path, class: "btn btn-secondary" %>
+
+ <%= submit_tag "Search", class: "btn btn-primary" %>
+ <%= link_to "Clear filters", payments_path, class: "btn btn-secondary", data: { action: "collection#clearAndSubmit" } %>
+
<% end %>
diff --git a/app/views/payments/index.html.erb b/app/views/payments/index.html.erb
index 167dd362a..c809c9820 100644
--- a/app/views/payments/index.html.erb
+++ b/app/views/payments/index.html.erb
@@ -3,36 +3,37 @@
Payments
<%= link_to "Add Cash Payment", new_payment_path(type: "CashPayment"), class: "btn btn-secondary" %><%= link_to "Add Check Payment", new_payment_path(type: "CheckPayment"), class: "btn btn-secondary" %>
-
<%= render 'search_boxes' %>
-
-
-
-
-
- Type
- Payer
- Amount
- Remaining
- Created
- Actions
-
-
-
-
- <% @payments.each do |payment| %>
-
- <%= payment.type.underscore.titleize %>
- <%= payment.payer.name %>
- $<%= "%.2f" % payment.amount_dollars %>
- $<%= "%.2f" % payment.remaining_dollars %>
- <%= payment.created_at.strftime("%B %d, %Y at %I:%M %p") %>
- <%= link_to "View", payment_path(payment), class: "text-primary hover:text-primary-dark" %>
-
- <% end %>
-
-
-
-
-
+ <% result_src = payments_path + "?" + request.query_string %>
+ <%= turbo_frame_tag :payment_results, src: result_src, autoscroll: "start", data: { turbo: "temporary" } do %>
+
+
+
+
+ <% 10.times do %>
+
+ <% end %>
+
+
+
+
+ <% end %>
diff --git a/app/views/payments/payment_results.html.erb b/app/views/payments/payment_results.html.erb
new file mode 100644
index 000000000..c976617d6
--- /dev/null
+++ b/app/views/payments/payment_results.html.erb
@@ -0,0 +1,33 @@
+<%= turbo_frame_tag :payment_results do %>
+ <% if @payments.any? %>
+
+
+
+
+ Type
+ Payer
+ Amount
+ Remaining
+ Created
+ Actions
+
+
+
+ <% @payments.each do |payment| %>
+
+ <%= payment.type.underscore.titleize %>
+ <%= payment.payer.name %>
+ $<%= "%.2f" % payment.amount_dollars %>
+ $<%= "%.2f" % payment.remaining_dollars %>
+ <%= payment.created_at.strftime("%B %d, %Y at %I:%M %p") %>
+ <%= link_to "View", payment_path(payment), class: "text-primary hover:text-primary-dark" %>
+
+ <% end %>
+
+
+
+ <% else %>
+ There are no payments that match your search. Please try again.
+ <% end %>
+
+<% end %>
From 19a43be88f0f4b30a7523a412b280ad0fd1399f9 Mon Sep 17 00:00:00 2001
From: Justin Miller <16829344+jmilljr24@users.noreply.github.com>
Date: Thu, 9 Apr 2026 16:10:32 -0400
Subject: [PATCH 51/74] exclude remove select input typing from auto submit
---
.../javascript/controllers/collection_controller.js | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/app/frontend/javascript/controllers/collection_controller.js b/app/frontend/javascript/controllers/collection_controller.js
index b9f56ce46..7954e7aa8 100644
--- a/app/frontend/javascript/controllers/collection_controller.js
+++ b/app/frontend/javascript/controllers/collection_controller.js
@@ -21,7 +21,12 @@ export default class extends Controller {
}
});
this.element.addEventListener("input", (event) => {
- if (event.target.type === "text") {
+ const target = event.target;
+ if (target.type !== "text") return;
+
+ const isTomSelect = target.closest(".ts-control");
+
+ if (!isTomSelect) {
this.debouncedSubmit();
}
});
From ea33b8f761107deb5e01fb7b1f80fc93b9e280e1 Mon Sep 17 00:00:00 2001
From: Justin Miller <16829344+jmilljr24@users.noreply.github.com>
Date: Thu, 9 Apr 2026 16:12:39 -0400
Subject: [PATCH 52/74] fix payment type select
---
app/models/payment.rb | 2 ++
app/views/payments/_search_boxes.html.erb | 23 +++++------------------
2 files changed, 7 insertions(+), 18 deletions(-)
diff --git a/app/models/payment.rb b/app/models/payment.rb
index 91672457a..92b0ac1bf 100644
--- a/app/models/payment.rb
+++ b/app/models/payment.rb
@@ -11,7 +11,9 @@ class Payment < ApplicationRecord
before_validation :set_amount_cents_remaining, if: :new_record?
scope :by_type, ->(types) {
+ return if types.blank?
types = types.split(",") if types.is_a?(String)
+ types = types.reject(&:blank?)
where(type: types) if types.present?
}
scope :by_payer, ->(payer_type, payer_id) { where(payer_type: payer_type, payer_id: payer_id) if payer_type.present? && payer_id.present? }
diff --git a/app/views/payments/_search_boxes.html.erb b/app/views/payments/_search_boxes.html.erb
index 69fa6b06d..bec4b3223 100644
--- a/app/views/payments/_search_boxes.html.erb
+++ b/app/views/payments/_search_boxes.html.erb
@@ -10,24 +10,11 @@
collection_selected_class: selected_btn
}, autocomplete: "off" do %>
-
-
Type
-
-
- Type
-
-
-
-
<%= check_box_tag "type[]", "CashPayment", Array(params[:type]).include?("CashPayment"), id: "type_CashPayment", class: "category-checkbox" %><%= label_tag "type_CashPayment", "Cash Payment", class: "text-sm text-gray-700" %>
-
<%= check_box_tag "type[]", "CheckPayment", Array(params[:type]).include?("CheckPayment"), id: "type_CheckPayment", class: "category-checkbox" %><%= label_tag "type_CheckPayment", "Check Payment", class: "text-sm text-gray-700" %>
-
-
+
+ Type
+ <%= select_tag "type[]",
+ options_for_select({ "All" => "", "Cash Payment" => "CashPayment", "Check Payment" => "CheckPayment" }, Array(params[:type]).first),
+ class: "w-full bg-white border border-gray-300 rounded-lg px-3 py-2 focus:ring-blue-500 focus:border-blue-500" %>
Amount Remaining
From c8f86e4ac7e4f2e0ad428961d993abdddd6d2f5e Mon Sep 17 00:00:00 2001
From: Justin Miller <16829344+jmilljr24@users.noreply.github.com>
Date: Thu, 9 Apr 2026 16:17:49 -0400
Subject: [PATCH 53/74] fix new payment form type select
---
app/views/payments/_form.html.erb | 16 +++++++++++++---
1 file changed, 13 insertions(+), 3 deletions(-)
diff --git a/app/views/payments/_form.html.erb b/app/views/payments/_form.html.erb
index 0bb91d4fb..2267d9fd9 100644
--- a/app/views/payments/_form.html.erb
+++ b/app/views/payments/_form.html.erb
@@ -1,9 +1,12 @@
From eb1b58058e6fa0d2b2b55d86f641bab83e73be85 Mon Sep 17 00:00:00 2001
From: Justin Miller <16829344+jmilljr24@users.noreply.github.com>
Date: Thu, 9 Apr 2026 16:19:59 -0400
Subject: [PATCH 54/74] use turbo frame top for payment view link
---
app/views/payments/index.html.erb | 4 ++++
app/views/payments/payment_results.html.erb | 4 +++-
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/app/views/payments/index.html.erb b/app/views/payments/index.html.erb
index c809c9820..0808a7e0c 100644
--- a/app/views/payments/index.html.erb
+++ b/app/views/payments/index.html.erb
@@ -3,8 +3,10 @@
Payments
<%= link_to "Add Cash Payment", new_payment_path(type: "CashPayment"), class: "btn btn-secondary" %><%= link_to "Add Check Payment", new_payment_path(type: "CheckPayment"), class: "btn btn-secondary" %>
+
<%= render 'search_boxes' %>
<% result_src = payments_path + "?" + request.query_string %>
+
<%= turbo_frame_tag :payment_results, src: result_src, autoscroll: "start", data: { turbo: "temporary" } do %>
+
<% 10.times do %>
@@ -29,6 +32,7 @@
<% end %>
+
diff --git a/app/views/payments/payment_results.html.erb b/app/views/payments/payment_results.html.erb
index c976617d6..f363b00c6 100644
--- a/app/views/payments/payment_results.html.erb
+++ b/app/views/payments/payment_results.html.erb
@@ -12,6 +12,7 @@
Actions
+
<% @payments.each do |payment| %>
@@ -20,7 +21,7 @@
$<%= "%.2f" % payment.amount_dollars %>
$<%= "%.2f" % payment.remaining_dollars %>
<%= payment.created_at.strftime("%B %d, %Y at %I:%M %p") %>
- <%= link_to "View", payment_path(payment), class: "text-primary hover:text-primary-dark" %>
+ <%= link_to "View", payment_path(payment), class: "text-primary hover:text-primary-dark", data: { turbo_frame: "_top"} %>
<% end %>
@@ -29,5 +30,6 @@
<% else %>
There are no payments that match your search. Please try again.
<% end %>
+
<% end %>
From e1e332c9f68ef1113531126af7a25a581e61fa56 Mon Sep 17 00:00:00 2001
From: Justin Miller <16829344+jmilljr24@users.noreply.github.com>
Date: Thu, 9 Apr 2026 16:26:36 -0400
Subject: [PATCH 55/74] remove redudant payment name
---
app/views/allocations/index.html.erb | 2 +-
app/views/payments/_form.html.erb | 10 +---------
app/views/payments/_search_boxes.html.erb | 18 +++++++++++++-----
app/views/payments/index.html.erb | 6 +-----
app/views/payments/payment_results.html.erb | 4 +---
app/views/payments/show.html.erb | 2 +-
6 files changed, 18 insertions(+), 24 deletions(-)
diff --git a/app/views/allocations/index.html.erb b/app/views/allocations/index.html.erb
index 1c4ee2453..840cf4eff 100644
--- a/app/views/allocations/index.html.erb
+++ b/app/views/allocations/index.html.erb
@@ -53,7 +53,7 @@
end %>
<%= allocation.id %>
- <%= allocation.source.class.name.underscore.titleize %>
+ <%= allocation.source.class.name.underscore.titleize.gsub(" Payment", "") %>
<%= allocation.source&.payer.name || "Unknown" %>
<% unless @allocatable %>
diff --git a/app/views/payments/_form.html.erb b/app/views/payments/_form.html.erb
index 2267d9fd9..fc268cc61 100644
--- a/app/views/payments/_form.html.erb
+++ b/app/views/payments/_form.html.erb
@@ -1,12 +1,9 @@
diff --git a/app/views/payments/_search_boxes.html.erb b/app/views/payments/_search_boxes.html.erb
index bec4b3223..e29ddecde 100644
--- a/app/views/payments/_search_boxes.html.erb
+++ b/app/views/payments/_search_boxes.html.erb
@@ -1,6 +1,7 @@
<% default_btn = "text-gray-600 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-800 shadow-sm" %>
<% selected_btn = "text-white bg-blue-600 border-blue-600 hover:bg-blue-700 hover:text-white shadow-md ring-2 ring-blue-300" %>
+
<%= form_tag payments_path, method: :get, class: "space-y-4",
data: {
@@ -12,27 +13,34 @@
Type
+
<%= select_tag "type[]",
- options_for_select({ "All" => "", "Cash Payment" => "CashPayment", "Check Payment" => "CheckPayment" }, Array(params[:type]).first),
+ options_for_select({ "All" => "", "Cash" => "CashPayment", "Check" => "CheckPayment", "Stripe" => "ExternalProcessorPayment" }, Array(params[:type]).first),
class: "w-full bg-white border border-gray-300 rounded-lg px-3 py-2 focus:ring-blue-500 focus:border-blue-500" %>
+
Amount Remaining
+
<%= select_tag "has_remaining",
options_for_select({ "All" => "all", "Yes" => "yes", "No" => "no" }, params[:has_remaining] || "all"),
class: "w-full bg-white border border-gray-300 rounded-lg px-3 py-2 focus:ring-blue-500 focus:border-blue-500" %>
+
Payer Type
+
<%= select_tag "payer_type",
options_for_select(["Person", "Organization"], params[:payer_type]),
include_blank: true,
class: "w-full bg-white border border-gray-300 rounded-lg px-3 py-2 focus:ring-blue-500 focus:border-blue-500",
data: { action: "search-type-select#toggle" } %>
+
Payer
+
<%= select_tag "payer_id",
options_for_select(Person.where(id: params[:payer_id]).map { |p| [p.full_name, p.id] }),
include_blank: true,
@@ -44,9 +52,11 @@
prompt: "Search for a person" %>
+
Payer
+
<%= select_tag "payer_id",
options_for_select(Organization.where(id: params[:payer_id]).map { |o| [o.name, o.id] }),
include_blank: true,
@@ -58,11 +68,9 @@
prompt: "Search for an organization" %>
+
-
- <%= submit_tag "Search", class: "btn btn-primary" %>
- <%= link_to "Clear filters", payments_path, class: "btn btn-secondary", data: { action: "collection#clearAndSubmit" } %>
-
+
<%= submit_tag "Search", class: "btn btn-primary" %><%= link_to "Clear filters", payments_path, class: "btn btn-secondary", data: { action: "collection#clearAndSubmit" } %>
<% end %>
diff --git a/app/views/payments/index.html.erb b/app/views/payments/index.html.erb
index 0808a7e0c..a0b415411 100644
--- a/app/views/payments/index.html.erb
+++ b/app/views/payments/index.html.erb
@@ -1,12 +1,10 @@
Payments
-
<%= link_to "Add Cash Payment", new_payment_path(type: "CashPayment"), class: "btn btn-secondary" %><%= link_to "Add Check Payment", new_payment_path(type: "CheckPayment"), class: "btn btn-secondary" %>
+
<%= link_to "Add Cash", new_payment_path(type: "CashPayment"), class: "btn btn-secondary" %><%= link_to "Add Check", new_payment_path(type: "CheckPayment"), class: "btn btn-secondary" %>
-
<%= render 'search_boxes' %>
<% result_src = payments_path + "?" + request.query_string %>
-
<%= turbo_frame_tag :payment_results, src: result_src, autoscroll: "start", data: { turbo: "temporary" } do %>
-
<% 10.times do %>
@@ -32,7 +29,6 @@
<% end %>
-
diff --git a/app/views/payments/payment_results.html.erb b/app/views/payments/payment_results.html.erb
index f363b00c6..59a53df1a 100644
--- a/app/views/payments/payment_results.html.erb
+++ b/app/views/payments/payment_results.html.erb
@@ -12,11 +12,10 @@
Actions
-
<% @payments.each do |payment| %>
- <%= payment.type.underscore.titleize %>
+ <%= payment.type.underscore.titleize.gsub(" Payment", "") %>
<%= payment.payer.name %>
$<%= "%.2f" % payment.amount_dollars %>
$<%= "%.2f" % payment.remaining_dollars %>
@@ -30,6 +29,5 @@
<% else %>
There are no payments that match your search. Please try again.
<% end %>
-
<% end %>
diff --git a/app/views/payments/show.html.erb b/app/views/payments/show.html.erb
index 4351fcd07..092f8b4d5 100644
--- a/app/views/payments/show.html.erb
+++ b/app/views/payments/show.html.erb
@@ -12,7 +12,7 @@
Type
- <%= @payment.type.underscore.titleize %>
+ <%= @payment.type.underscore.titleize.gsub(" Payment", "") %>
Payer
From dddf9186ff078468967ca2a0ba8cd5ecc6ec4c70 Mon Sep 17 00:00:00 2001
From: Justin Miller <16829344+jmilljr24@users.noreply.github.com>
Date: Thu, 9 Apr 2026 16:31:24 -0400
Subject: [PATCH 56/74] hard code stripe name
---
app/views/allocations/index.html.erb | 2 +-
app/views/payments/_form.html.erb | 2 +-
app/views/payments/index.html.erb | 6 +++++-
app/views/payments/payment_results.html.erb | 6 ++++--
app/views/payments/show.html.erb | 2 +-
5 files changed, 12 insertions(+), 6 deletions(-)
diff --git a/app/views/allocations/index.html.erb b/app/views/allocations/index.html.erb
index 840cf4eff..e1dbd1dff 100644
--- a/app/views/allocations/index.html.erb
+++ b/app/views/allocations/index.html.erb
@@ -53,7 +53,7 @@
end %>
<%= allocation.id %>
- <%= allocation.source.class.name.underscore.titleize.gsub(" Payment", "") %>
+ <%= allocation.source.class.name.underscore.titleize.gsub(" Payment", "").gsub("External Processor", "Stripe") %>
<%= allocation.source&.payer.name || "Unknown" %>
<% unless @allocatable %>
diff --git a/app/views/payments/_form.html.erb b/app/views/payments/_form.html.erb
index fc268cc61..c0c6a03b0 100644
--- a/app/views/payments/_form.html.erb
+++ b/app/views/payments/_form.html.erb
@@ -1,5 +1,5 @@
+
<% @payments.each do |payment| %>
- <%= payment.type.underscore.titleize.gsub(" Payment", "") %>
+ <%= payment.type.underscore.titleize.gsub(" Payment", "").gsub("External Processor", "Stripe") %>
<%= payment.payer.name %>
$<%= "%.2f" % payment.amount_dollars %>
$<%= "%.2f" % payment.remaining_dollars %>
@@ -29,5 +30,6 @@
<% else %>
There are no payments that match your search. Please try again.
<% end %>
+
<% end %>
diff --git a/app/views/payments/show.html.erb b/app/views/payments/show.html.erb
index 092f8b4d5..76d3bb541 100644
--- a/app/views/payments/show.html.erb
+++ b/app/views/payments/show.html.erb
@@ -12,7 +12,7 @@
Type
- <%= @payment.type.underscore.titleize.gsub(" Payment", "") %>
+ <%= @payment.type.underscore.titleize.gsub(" Payment", "").gsub("External Processor", "Stripe") %>
Payer
From 968750759f2090347d2d61597d0c20cc8bf6606d Mon Sep 17 00:00:00 2001
From: Justin Miller <16829344+jmilljr24@users.noreply.github.com>
Date: Thu, 9 Apr 2026 16:48:30 -0400
Subject: [PATCH 57/74] comments
---
.../controllers/collection_controller.js | 25 +++++++++++--------
1 file changed, 15 insertions(+), 10 deletions(-)
diff --git a/app/frontend/javascript/controllers/collection_controller.js b/app/frontend/javascript/controllers/collection_controller.js
index 7954e7aa8..783aba1ff 100644
--- a/app/frontend/javascript/controllers/collection_controller.js
+++ b/app/frontend/javascript/controllers/collection_controller.js
@@ -21,6 +21,7 @@ export default class extends Controller {
}
});
this.element.addEventListener("input", (event) => {
+ // skip submit on tom-select keyboard input
const target = event.target;
if (target.type !== "text") return;
@@ -62,18 +63,22 @@ export default class extends Controller {
clearAndSubmit(event) {
event.preventDefault();
- this.element.querySelectorAll('input[type="text"], input[type="search"]').forEach(input => {
- input.value = '';
- });
- this.element.querySelectorAll('select').forEach(select => {
+ this.element
+ .querySelectorAll('input[type="text"], input[type="search"]')
+ .forEach((input) => {
+ input.value = "";
+ });
+ this.element.querySelectorAll("select").forEach((select) => {
select.selectedIndex = 0;
});
- this.element.querySelectorAll('input[type="checkbox"], input[type="radio"]').forEach(input => {
- if (input.checked) {
- this.toggleClass(input);
- }
- input.checked = false;
- });
+ this.element
+ .querySelectorAll('input[type="checkbox"], input[type="radio"]')
+ .forEach((input) => {
+ if (input.checked) {
+ this.toggleClass(input);
+ }
+ input.checked = false;
+ });
this.element.reset();
this.submitForm();
}
From 172f753e0e2788dd8397acabd1624d3b6a4499c0 Mon Sep 17 00:00:00 2001
From: Justin Miller <16829344+jmilljr24@users.noreply.github.com>
Date: Thu, 9 Apr 2026 18:55:18 -0400
Subject: [PATCH 58/74] fix check for nil on event allocation
---
app/controllers/allocations_controller.rb | 48 +++++++++++------------
1 file changed, 22 insertions(+), 26 deletions(-)
diff --git a/app/controllers/allocations_controller.rb b/app/controllers/allocations_controller.rb
index 01932d45d..629df28c1 100644
--- a/app/controllers/allocations_controller.rb
+++ b/app/controllers/allocations_controller.rb
@@ -2,13 +2,13 @@ class AllocationsController < ApplicationController
before_action :authenticate_user!
def index
+ authorize!
if params[:allocatable_sgid].present?
@allocatable = GlobalID::Locator.locate_signed(params[:allocatable_sgid])
@allocations = @allocatable.allocations.includes(:source).order(created_at: :desc).paginate(page: params[:page], per_page: 25)
else
@allocations = Allocation.all.includes(:source).order(created_at: :desc).paginate(page: params[:page], per_page: 25)
end
- authorize! @allocations
end
def new
@@ -35,29 +35,12 @@ def create
amount: amount_val
)
- # Locate the source
if @allocation.source_type && @allocation.source_id
@source = @allocation.source_type.constantize.find_by(id: @allocation.source_id)
end
@event_registrations = EventRegistration.all.order(created_at: :desc).limit(100)
- # Ensure we have a valid source before proceeding
- unless @source.present?
- @allocation.errors.add(:base, "Source is required")
- render :new, status: :unprocessable_content
- return
- end
-
- if @source.is_a?(Payment)
- remaining = @source.amount_cents_remaining
- if @allocation.amount > remaining
- @allocation.errors.add(:amount, "cannot exceed remaining amount (#{remaining})")
- render :new, status: :unprocessable_content
- return
- end
- end
-
if @allocation.allocatable_type == "EventRegistration"
unless validate_event_registration_cost(@allocation, amount_val)
flash[:error] = @allocation.errors.full_messages.join(", ")
@@ -66,17 +49,30 @@ def create
end
end
- if @allocation.save
+ unless @source.present?
+ @allocation.errors.add(:base, "Source is required")
+ render :new, status: :unprocessable_content
+ return
+ end
+
+ @source.with_lock do
if @source.is_a?(Payment)
- @source.with_lock do
+ remaining = @source.amount_cents_remaining
+ if @allocation.amount > remaining
+ @allocation.errors.add(:amount, "cannot exceed remaining amount (#{remaining})")
+ render :new, status: :unprocessable_content
+ return
+ end
+
+ if @allocation.save
@source.update!(amount_cents_remaining: @source.amount_cents_remaining - amount_val)
+ flash[:notice] = "Allocation created. $#{'%.2f' % @source.remaining_dollars} remaining on payment."
+ redirect_to payment_path(@source)
+ else
+ Rails.logger.error "Allocation save failed: #{@allocation.errors.full_messages}"
+ render :new, status: :unprocessable_content
end
end
- flash[:notice] = "Allocation created. $#{'%.2f' % @source.remaining_dollars} remaining on payment."
- redirect_to payment_path(@source)
- else
- Rails.logger.error "Allocation save failed: #{@allocation.errors.full_messages}"
- render :new, status: :unprocessable_content
end
end
@@ -119,7 +115,7 @@ def validate_event_registration_cost(allocation, amount_val)
event_reg = allocation.allocatable
event = event_reg.event
- return true unless event.cost_cents.present?
+ return false if event.cost_cents.blank?
current_allocated = event_reg.allocations_sum || 0
new_total = current_allocated + amount_val
From ba6614ec86fa311eb4346551263b8c637fa05e21 Mon Sep 17 00:00:00 2001
From: Justin Miller <16829344+jmilljr24@users.noreply.github.com>
Date: Thu, 9 Apr 2026 19:18:51 -0400
Subject: [PATCH 59/74] rubocop
---
.../20260328221935_create_pay_tables.pay.rb | 26 +++++++++----------
1 file changed, 13 insertions(+), 13 deletions(-)
diff --git a/db/migrate/20260328221935_create_pay_tables.pay.rb b/db/migrate/20260328221935_create_pay_tables.pay.rb
index 2ec88a565..1ec5d50f9 100644
--- a/db/migrate/20260328221935_create_pay_tables.pay.rb
+++ b/db/migrate/20260328221935_create_pay_tables.pay.rb
@@ -13,8 +13,8 @@ def change
t.datetime :deleted_at
t.timestamps
end
- add_index :pay_customers, [:owner_type, :owner_id, :deleted_at], name: :pay_customer_owner_index, unique: true
- add_index :pay_customers, [:processor, :processor_id], unique: true
+ add_index :pay_customers, [ :owner_type, :owner_id, :deleted_at ], name: :pay_customer_owner_index, unique: true
+ add_index :pay_customers, [ :processor, :processor_id ], unique: true
create_table :pay_merchants, id: primary_key_type do |t|
t.belongs_to :owner, polymorphic: true, index: false, type: foreign_key_type
@@ -24,10 +24,10 @@ def change
t.public_send Pay::Adapter.json_column_type, :data
t.timestamps
end
- add_index :pay_merchants, [:owner_type, :owner_id, :processor]
+ add_index :pay_merchants, [ :owner_type, :owner_id, :processor ]
create_table :pay_payment_methods, id: primary_key_type do |t|
- t.belongs_to :customer, foreign_key: {to_table: :pay_customers}, null: false, index: false, type: foreign_key_type
+ t.belongs_to :customer, foreign_key: { to_table: :pay_customers }, null: false, index: false, type: foreign_key_type
t.string :processor_id, null: false
t.boolean :default
t.string :type
@@ -35,10 +35,10 @@ def change
t.string :stripe_account
t.timestamps
end
- add_index :pay_payment_methods, [:customer_id, :processor_id], unique: true
+ add_index :pay_payment_methods, [ :customer_id, :processor_id ], unique: true
create_table :pay_subscriptions, id: primary_key_type do |t|
- t.belongs_to :customer, foreign_key: {to_table: :pay_customers}, null: false, index: false, type: foreign_key_type
+ t.belongs_to :customer, foreign_key: { to_table: :pay_customers }, null: false, index: false, type: foreign_key_type
t.string :name, null: false
t.string :processor_id, null: false
t.string :processor_plan, null: false
@@ -59,13 +59,13 @@ def change
t.string :payment_method_id
t.timestamps
end
- add_index :pay_subscriptions, [:customer_id, :processor_id], unique: true
- add_index :pay_subscriptions, [:metered]
- add_index :pay_subscriptions, [:pause_starts_at]
+ add_index :pay_subscriptions, [ :customer_id, :processor_id ], unique: true
+ add_index :pay_subscriptions, [ :metered ]
+ add_index :pay_subscriptions, [ :pause_starts_at ]
create_table :pay_charges, id: primary_key_type do |t|
- t.belongs_to :customer, foreign_key: {to_table: :pay_customers}, null: false, index: false, type: foreign_key_type
- t.belongs_to :subscription, foreign_key: {to_table: :pay_subscriptions}, null: true, type: foreign_key_type
+ t.belongs_to :customer, foreign_key: { to_table: :pay_customers }, null: false, index: false, type: foreign_key_type
+ t.belongs_to :subscription, foreign_key: { to_table: :pay_subscriptions }, null: true, type: foreign_key_type
t.string :processor_id, null: false
t.integer :amount, null: false
t.string :currency
@@ -76,7 +76,7 @@ def change
t.string :stripe_account
t.timestamps
end
- add_index :pay_charges, [:customer_id, :processor_id], unique: true
+ add_index :pay_charges, [ :customer_id, :processor_id ], unique: true
create_table :pay_webhooks, id: primary_key_type do |t|
t.string :processor
@@ -93,6 +93,6 @@ def primary_and_foreign_key_types
setting = config.options[config.orm][:primary_key_type]
primary_key_type = setting || :primary_key
foreign_key_type = setting || :bigint
- [primary_key_type, foreign_key_type]
+ [ primary_key_type, foreign_key_type ]
end
end
From 2ee51786e1e6622d7ad6444a18c7c700508b1675 Mon Sep 17 00:00:00 2001
From: Justin Miller <16829344+jmilljr24@users.noreply.github.com>
Date: Fri, 10 Apr 2026 10:24:35 -0400
Subject: [PATCH 60/74] add turbo flash to allocations
---
app/controllers/allocations_controller.rb | 47 ++++++++++++-----------
1 file changed, 25 insertions(+), 22 deletions(-)
diff --git a/app/controllers/allocations_controller.rb b/app/controllers/allocations_controller.rb
index 629df28c1..086f7b338 100644
--- a/app/controllers/allocations_controller.rb
+++ b/app/controllers/allocations_controller.rb
@@ -19,7 +19,6 @@ def new
end
@allocation = Allocation.new(source: @source)
- @event_registrations = EventRegistration.all.order(created_at: :desc).limit(100)
end
def create
@@ -39,12 +38,13 @@ def create
@source = @allocation.source_type.constantize.find_by(id: @allocation.source_id)
end
- @event_registrations = EventRegistration.all.order(created_at: :desc).limit(100)
-
if @allocation.allocatable_type == "EventRegistration"
- unless validate_event_registration_cost(@allocation, amount_val)
- flash[:error] = @allocation.errors.full_messages.join(", ")
- render :new, status: :unprocessable_content
+ unless validate_event_registration_cost(amount_val)
+ flash.now[:error] = @allocation.errors.full_messages.join(", ")
+ respond_to do |format|
+ format.turbo_stream { render turbo_stream: turbo_stream.replace("flash_now", partial: "shared/flash_messages"), status: :unprocessable_content }
+ format.html { render :new, status: :unprocessable_content }
+ end
return
end
end
@@ -57,10 +57,14 @@ def create
@source.with_lock do
if @source.is_a?(Payment)
- remaining = @source.amount_cents_remaining
- if @allocation.amount > remaining
- @allocation.errors.add(:amount, "cannot exceed remaining amount (#{remaining})")
- render :new, status: :unprocessable_content
+ if @allocation.amount > @source.amount_cents_remaining
+ @allocation.errors.add(:base, "Cannot exceed remaining amount ($#{@source.remaining_dollars})")
+
+ flash.now[:error] = @allocation.errors.full_messages.join(", ")
+ respond_to do |format|
+ format.turbo_stream { render turbo_stream: turbo_stream.replace("flash_now", partial: "shared/flash_messages"), status: :unprocessable_content }
+ format.html { render :new, status: :unprocessable_content }
+ end
return
end
@@ -69,7 +73,6 @@ def create
flash[:notice] = "Allocation created. $#{'%.2f' % @source.remaining_dollars} remaining on payment."
redirect_to payment_path(@source)
else
- Rails.logger.error "Allocation save failed: #{@allocation.errors.full_messages}"
render :new, status: :unprocessable_content
end
end
@@ -79,15 +82,15 @@ def create
def revert
authorize!
- @allocation = Allocation.find(params[:id])
+ @allocation = allocation.find(params[:id])
if @allocation.reverted?
- flash[:error] = "This allocation has already been reverted"
+ flash[:error] = "this allocation has already been reverted"
redirect_to payment_path(@allocation.source)
return
end
- @revert = Allocation.new(
+ @revert = allocation.new(
source: @allocation.source,
allocatable: @allocation.allocatable,
amount: -@allocation.amount
@@ -101,7 +104,7 @@ def revert
payment.update!(amount_cents_remaining: payment.amount_cents_remaining + @allocation.amount)
end
- redirect_to payment_path(payment), notice: "Allocation reverted"
+ redirect_to payment_path(payment), notice: "allocation reverted"
else
flash[:error] = @revert.errors.full_messages.join(", ")
redirect_to payment_path(@allocation.source)
@@ -110,19 +113,19 @@ def revert
private
- def validate_event_registration_cost(allocation, amount_val)
- return true unless allocation.allocatable.present?
-
- event_reg = allocation.allocatable
+ def validate_event_registration_cost(amount_val)
+ event_reg = @allocation.allocatable
event = event_reg.event
- return false if event.cost_cents.blank?
-
+ if event.cost_cents.blank?
+ @allocation.errors.add(:base, "cannot allocate to a free event.")
+ return false
+ end
current_allocated = event_reg.allocations_sum || 0
new_total = current_allocated + amount_val
if new_total > event.cost_cents
remaining = [ event.cost_cents - current_allocated, 0 ].max
- allocation.errors.add(:base, "Cannot allocate more than remaining event cost. Remaining: $#{'%.2f' % (remaining / 100.0)}")
+ @allocation.errors.add(:base, "cannot allocate more than remaining event cost. remaining: $#{'%.2f' % (remaining / 100.0)}")
return false
end
From 0812b915a975e971b5717c01f2251d4d7d5348b3 Mon Sep 17 00:00:00 2001
From: Justin Miller <16829344+jmilljr24@users.noreply.github.com>
Date: Fri, 10 Apr 2026 10:42:32 -0400
Subject: [PATCH 61/74] add allocation validation
---
app/controllers/allocations_controller.rb | 37 ++++++++++++-----------
app/models/allocation.rb | 2 ++
app/views/allocations/new.html.erb | 12 ++------
3 files changed, 25 insertions(+), 26 deletions(-)
diff --git a/app/controllers/allocations_controller.rb b/app/controllers/allocations_controller.rb
index 086f7b338..e50a77734 100644
--- a/app/controllers/allocations_controller.rb
+++ b/app/controllers/allocations_controller.rb
@@ -38,7 +38,7 @@ def create
@source = @allocation.source_type.constantize.find_by(id: @allocation.source_id)
end
- if @allocation.allocatable_type == "EventRegistration"
+ if @allocation.allocatable_type == "EventRegistration" && @allocation.allocatable.present?
unless validate_event_registration_cost(amount_val)
flash.now[:error] = @allocation.errors.full_messages.join(", ")
respond_to do |format|
@@ -82,32 +82,35 @@ def create
def revert
authorize!
- @allocation = allocation.find(params[:id])
+ @allocation = Allocation.find(params[:id])
if @allocation.reverted?
- flash[:error] = "this allocation has already been reverted"
+ flash[:error] = "This allocation has already been reverted"
redirect_to payment_path(@allocation.source)
return
end
- @revert = allocation.new(
+ @revert = Allocation.new(
source: @allocation.source,
allocatable: @allocation.allocatable,
amount: -@allocation.amount
)
- if @revert.save
- @allocation.update!(reverted_id: @revert.id)
+ payment = @allocation.source
- payment = @allocation.source
- payment.with_lock do
- payment.update!(amount_cents_remaining: payment.amount_cents_remaining + @allocation.amount)
- end
+ payment.with_lock do
+ ActiveRecord::Base.transaction do
+ if @revert.save
+ @allocation.update!(reverted_id: @revert.id)
- redirect_to payment_path(payment), notice: "allocation reverted"
- else
- flash[:error] = @revert.errors.full_messages.join(", ")
- redirect_to payment_path(@allocation.source)
+ payment.update!(amount_cents_remaining: payment.amount_cents_remaining + @allocation.amount)
+
+ redirect_to payment_path(payment), notice: "Allocation reverted"
+ else
+ flash[:error] = @revert.errors.full_messages.join(", ")
+ redirect_to payment_path(@allocation.source)
+ end
+ end
end
end
@@ -117,7 +120,7 @@ def validate_event_registration_cost(amount_val)
event_reg = @allocation.allocatable
event = event_reg.event
if event.cost_cents.blank?
- @allocation.errors.add(:base, "cannot allocate to a free event.")
+ @allocation.errors.add(:base, "Cannot allocate to a free event.")
return false
end
current_allocated = event_reg.allocations_sum || 0
@@ -125,7 +128,7 @@ def validate_event_registration_cost(amount_val)
if new_total > event.cost_cents
remaining = [ event.cost_cents - current_allocated, 0 ].max
- @allocation.errors.add(:base, "cannot allocate more than remaining event cost. remaining: $#{'%.2f' % (remaining / 100.0)}")
+ @allocation.errors.add(:base, "Cannot allocate more than remaining event cost. remaining: $#{'%.2f' % (remaining / 100.0)}")
return false
end
@@ -133,6 +136,6 @@ def validate_event_registration_cost(amount_val)
end
def allocation_params
- params.require(:allocation).permit(:source_type, :source_id, :allocatable_type, :allocatable_id, :amount_dollars)
+ params.expect(allocation: [ :source_type, :source_id, :allocatable_type, :allocatable_id, :amount_dollars ])
end
end
diff --git a/app/models/allocation.rb b/app/models/allocation.rb
index 4d623d860..6ab74075a 100644
--- a/app/models/allocation.rb
+++ b/app/models/allocation.rb
@@ -8,6 +8,8 @@ class Allocation < ApplicationRecord
has_one :revert_record, class_name: "Allocation", foreign_key: "reverted_id", inverse_of: :reverted
validates :amount, numericality: true
+ validates :allocatable_type, presence: true
+ validates :allocatable_id, presence: true
validate :reverted_requires_positive_amount, :negative_cannot_be_reverted
diff --git a/app/views/allocations/new.html.erb b/app/views/allocations/new.html.erb
index e077ab80d..b8d58ece3 100644
--- a/app/views/allocations/new.html.erb
+++ b/app/views/allocations/new.html.erb
@@ -1,12 +1,9 @@
New Allocation
-
<%= simple_form_for @allocation, url: allocations_path do |f| %>
<%= f.hidden_field :source_type %>
<%= f.hidden_field :source_id %>
-
<% source = @source || (@allocation.source if @allocation.source.present?) %>
-
<% if source.present? && source.is_a?(Payment) %>
From Payment
@@ -18,11 +15,10 @@
Error: No payment source found
<% end %>
-
<%= f.input :allocatable_type, as: :select, collection: ["EventRegistration"],
include_blank: true,
+ required: true,
input_html: { data: { action: "search-type-select#toggle" } } %>
-
<%= f.input :allocatable_id,
collection: [],
@@ -34,15 +30,13 @@
}
},
prompt: "Search for a Person or Event",
- label: "Allocatable" %>
+ label: "Allocatable",
+ required: true %>
-
<%= f.input :amount_dollars, label: "Amount ($)", input_html: { min: 0, step: 0.01, placeholder: "0.00", data: { controller: "currency-input", action: "currency-input#format" } } %>
-
<%= f.button :submit, "Create Allocation", class: "btn btn-primary" %>
-
<% if source.present? %>
<%= link_to "Cancel", payment_path(source), class: "btn btn-secondary ml-2" %>
<% else %>
From 7622884016a84a7481d5f8a84d04ac79a34c1369 Mon Sep 17 00:00:00 2001
From: Justin Miller <16829344+jmilljr24@users.noreply.github.com>
Date: Fri, 10 Apr 2026 10:56:29 -0400
Subject: [PATCH 62/74] add search to allocations
---
app/controllers/allocations_controller.rb | 2 +-
app/models/allocation.rb | 33 ++++++++
app/views/allocations/_search_boxes.html.erb | 87 ++++++++++++++++++++
app/views/allocations/index.html.erb | 3 +
4 files changed, 124 insertions(+), 1 deletion(-)
create mode 100644 app/views/allocations/_search_boxes.html.erb
diff --git a/app/controllers/allocations_controller.rb b/app/controllers/allocations_controller.rb
index e50a77734..e1db3bee5 100644
--- a/app/controllers/allocations_controller.rb
+++ b/app/controllers/allocations_controller.rb
@@ -7,7 +7,7 @@ def index
@allocatable = GlobalID::Locator.locate_signed(params[:allocatable_sgid])
@allocations = @allocatable.allocations.includes(:source).order(created_at: :desc).paginate(page: params[:page], per_page: 25)
else
- @allocations = Allocation.all.includes(:source).order(created_at: :desc).paginate(page: params[:page], per_page: 25)
+ @allocations = Allocation.search_by_params(params).includes(:source).order(created_at: :desc).paginate(page: params[:page], per_page: 25)
end
end
diff --git a/app/models/allocation.rb b/app/models/allocation.rb
index 6ab74075a..787bd5d3e 100644
--- a/app/models/allocation.rb
+++ b/app/models/allocation.rb
@@ -25,6 +25,39 @@ def amount_dollars=(value)
self.amount = (value.to_d * 100).to_i if value.present?
end
+ scope :by_source_type, ->(types) {
+ return if types.blank?
+ types = types.split(",") if types.is_a?(String)
+ types = types.reject(&:blank?)
+ return unless types.present?
+ where("source_type = 'Payment' AND EXISTS (SELECT 1 FROM payments WHERE payments.id = allocations.source_id AND payments.type IN (?))", types)
+ }
+ scope :by_allocatable_type, ->(type) { where(allocatable_type: type) if type.present? }
+ scope :has_reverted, ->(value) {
+ case value
+ when "yes" then where("reverted_id IS NOT NULL OR amount < 0")
+ when "no" then where("reverted_id IS NULL AND amount >= 0")
+ end
+ }
+ scope :by_payer, ->(payer_type, payer_id) {
+ if payer_type.present? && payer_id.present?
+ where("source_type LIKE ? AND source_id IN (?)", "%Payment", Payment.where(payer_type: payer_type, payer_id: payer_id).select(:id))
+ end
+ }
+ scope :by_allocatable_id, ->(allocatable_type, allocatable_id) {
+ where(allocatable_type: allocatable_type, allocatable_id: allocatable_id) if allocatable_type.present? && allocatable_id.present?
+ }
+
+ def self.search_by_params(params)
+ results = all
+ results = results.by_source_type(params[:source_type]) if params[:source_type].present?
+ results = results.by_allocatable_type(params[:allocatable_type]) if params[:allocatable_type].present?
+ results = results.has_reverted(params[:has_reverted]) if params[:has_reverted].present? && params[:has_reverted] != "all"
+ results = results.by_payer(params[:payer_type], params[:payer_id]) if params[:payer_type].present? && params[:payer_id].present?
+ results = results.by_allocatable_id(params[:allocatable_type], params[:allocatable_id]) if params[:allocatable_type].present? && params[:allocatable_id].present?
+ results
+ end
+
private
def reverted_requires_positive_amount
diff --git a/app/views/allocations/_search_boxes.html.erb b/app/views/allocations/_search_boxes.html.erb
new file mode 100644
index 000000000..f82b05fa1
--- /dev/null
+++ b/app/views/allocations/_search_boxes.html.erb
@@ -0,0 +1,87 @@
+
+<% default_btn = "text-gray-600 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-800 shadow-sm" %>
+<% selected_btn = "text-white bg-blue-600 border-blue-600 hover:bg-blue-700 hover:text-white shadow-md ring-2 ring-blue-300" %>
+
+ <%= form_tag allocations_path, method: :get, class: "space-y-4",
+ data: {
+ controller: "collection search-type-select",
+ collection_unselected_class: default_btn,
+ collection_selected_class: selected_btn
+ }, autocomplete: "off" do %>
+
+
+ Source Type
+ <%= select_tag "source_type[]",
+ options_for_select({ "All" => "", "Cash" => "CashPayment", "Check" => "CheckPayment", "Stripe" => "ExternalProcessorPayment" }, Array(params[:source_type]).first),
+ class: "w-full bg-white border border-gray-300 rounded-lg px-3 py-2 focus:ring-blue-500 focus:border-blue-500" %>
+
+
+ Reverted
+ <%= select_tag "has_reverted",
+ options_for_select({ "All" => "all", "Yes" => "yes", "No" => "no" }, params[:has_reverted] || "all"),
+ class: "w-full bg-white border border-gray-300 rounded-lg px-3 py-2 focus:ring-blue-500 focus:border-blue-500" %>
+
+
+ Payer Type
+ <%= select_tag "payer_type",
+ options_for_select(["Person", "Organization"], params[:payer_type]),
+ include_blank: true,
+ class: "w-full bg-white border border-gray-300 rounded-lg px-3 py-2 focus:ring-blue-500 focus:border-blue-500",
+ data: { action: "search-type-select#toggle" } %>
+
+
+
+ Payer
+ <%= select_tag "payer_id",
+ options_for_select(Person.where(id: params[:payer_id]).map { |p| [p.full_name, p.id] }),
+ include_blank: true,
+ class: "w-full bg-white border border-gray-300 rounded-lg px-3 py-2 focus:ring-blue-500 focus:border-blue-500",
+ data: {
+ controller: "remote-select",
+ remote_select_model_value: "person"
+ },
+ prompt: "Search for a person" %>
+
+
+
+
+ Payer
+ <%= select_tag "payer_id",
+ options_for_select(Organization.where(id: params[:payer_id]).map { |o| [o.name, o.id] }),
+ include_blank: true,
+ class: "w-full bg-white border border-gray-300 rounded-lg px-3 py-2 focus:ring-blue-500 focus:border-blue-500",
+ data: {
+ controller: "remote-select",
+ remote_select_model_value: "organization"
+ },
+ prompt: "Search for an organization" %>
+
+
+
+
+ Allocated To Type
+ <%= select_tag "allocatable_type",
+ options_for_select(["EventRegistration"], params[:allocatable_type]),
+ include_blank: true,
+ class: "w-full bg-white border border-gray-300 rounded-lg px-3 py-2 focus:ring-blue-500 focus:border-blue-500",
+ data: { action: "search-type-select#toggle" } %>
+
+
+
+ Allocated To
+ <%= select_tag "allocatable_id",
+ options_for_select(EventRegistration.where(id: params[:allocatable_id]).map { |r| ["#{r.registrant.full_name} - #{r.event.title}", r.id] }),
+ include_blank: true,
+ class: "w-full bg-white border border-gray-300 rounded-lg px-3 py-2 focus:ring-blue-500 focus:border-blue-500",
+ data: {
+ controller: "remote-select",
+ remote_select_model_value: "event_registration"
+ },
+ prompt: "Search for a registration" %>
+
+
+
+
<%= submit_tag "Search", class: "btn btn-primary" %><%= link_to "Clear filters", allocations_path, class: "btn btn-secondary", data: { action: "collection#clearAndSubmit" } %>
+
+ <% end %>
+
diff --git a/app/views/allocations/index.html.erb b/app/views/allocations/index.html.erb
index e1dbd1dff..575dca433 100644
--- a/app/views/allocations/index.html.erb
+++ b/app/views/allocations/index.html.erb
@@ -17,6 +17,9 @@
All Allocations
<% end %>
+ <% unless params[:allocatable_sgid].present? %>
+ <%= render 'search_boxes' %>
+ <% end %>
<% if @allocatable.is_a?(EventRegistration) %>
+
<% unless params[:allocatable_sgid].present? %>
<%= render 'search_boxes' %>
- <% end %>
- <% if @allocatable.is_a?(EventRegistration) %>
-
- <%= button_to "Add Cash Payment",
- allocation_form_payments_path(type: "CashPayment", allocatable_sgid: params[:allocatable_sgid]),
- class: "btn btn-secondary" %>
- <%= button_to "Add Check Payment",
- allocation_form_payments_path(type: "CheckPayment", allocatable_sgid: params[:allocatable_sgid]),
- class: "btn btn-secondary" %>
-
- <% end %>
-
-
-
-
- ID
- Type
- Source
- <% unless @allocatable %>
- Allocated To
- <% end %>
- Amount
- Ref
- Date
-
-
-
- <% @allocations.each do |allocation| %>
- <% row_class = if allocation.amount.to_i < 0
- 'bg-red-50 hover:bg-red-100'
- elsif allocation.reverted_id.present?
- 'bg-gray-100 border border-gray-300 opacity-60 hover:bg-gray-200 hover:opacity-100'
- else
- 'hover:bg-gray-50'
- end %>
-
- <%= allocation.id %>
- <%= allocation.source.class.name.underscore.titleize.gsub(" Payment", "").gsub("External Processor", "Stripe") %>
- <%= allocation.source&.payer.name || "Unknown" %>
+ <% result_src = allocations_path + "?" + request.query_string %>
+ <%= turbo_frame_tag :allocation_results, src: result_src, data: { turbo: "temporary" } do %>
+
+
+
+
+
+ <% 10.times do %>
+
+ <% end %>
+
+
+
+
+
+ <% end %>
+ <% else %>
+ <% if @allocatable.is_a?(EventRegistration) %>
+
+ <%= button_to "Add Cash Payment",
+ allocation_form_payments_path(type: "CashPayment", allocatable_sgid: params[:allocatable_sgid]),
+ class: "btn btn-secondary" %>
+
+ <%= button_to "Add Check Payment",
+ allocation_form_payments_path(type: "CheckPayment", allocatable_sgid: params[:allocatable_sgid]),
+ class: "btn btn-secondary" %>
+
+ <% end %>
+
+
+
+
+ ID
+ Type
+ Source
+
<% unless @allocatable %>
+ Allocated To
+ <% end %>
+
+ Amount
+ Ref
+ Date
+
+
+
+
+ <% @allocations.each do |allocation| %>
+ <% row_class = if allocation.amount.to_i < 0
+ 'bg-red-50 hover:bg-red-100'
+ elsif allocation.reverted_id.present?
+ 'bg-gray-100 border border-gray-300 opacity-60 hover:bg-gray-200 hover:opacity-100'
+ else
+ 'hover:bg-gray-50'
+ end %>
+
+
+ <%= allocation.id %>
+ <%= allocation.source.class.name.underscore.titleize.gsub(" Payment", "").gsub("External Processor", "Stripe") %>
+ <%= allocation.source&.payer.name || "Unknown" %>
+
+ <% unless @allocatable %>
+
+ <% if allocation.allocatable_type == "EventRegistration" && allocation.allocatable.present? %>
+ <%= truncate("#{allocation.allocatable.registrant.full_name} - #{allocation.allocatable.event.title}", length: 40) %>
+ <% else %>
+ <%= allocation.allocatable_type.underscore.titleize %>
+ <% end %>
+
+ <% end %>
+
+ $<%= "%.2f" % allocation.amount_dollars %>
+
- <% if allocation.allocatable_type == "EventRegistration" && allocation.allocatable.present? %>
- <%= truncate("#{allocation.allocatable.registrant.full_name} - #{allocation.allocatable.event.title}", length: 40) %>
+ <% if allocation.amount.to_i < 0 %>
+ <%= Allocation.find_by(reverted_id: allocation.id)&.id || "-" %>
<% else %>
- <%= allocation.allocatable_type.underscore.titleize %>
+ <%= allocation.reverted_id ? allocation.reverted_id : "-" %>
<% end %>
- <% end %>
- $<%= "%.2f" % allocation.amount_dollars %>
-
- <% if allocation.amount.to_i < 0 %>
- <%= Allocation.find_by(reverted_id: allocation.id)&.id || "-" %>
- <% else %>
- <%= allocation.reverted_id ? allocation.reverted_id : "-" %>
- <% end %>
-
- <%= allocation.created_at.strftime("%B %d, %Y") %>
-
- <% end %>
-
-
-
- <% if @allocations.respond_to?(:total_pages) && @allocations.total_pages > 1 %>
-
+
+ <%= allocation.created_at.strftime("%B %d, %Y") %>
+
+ <% end %>
+
+
+
+ <% if @allocations.respond_to?(:total_pages) && @allocations.total_pages > 1 %>
+
+ <% end %>
<% end %>
From b303d527557c1af2fb44a7ba086002d877440b12 Mon Sep 17 00:00:00 2001
From: Justin Miller <16829344+jmilljr24@users.noreply.github.com>
Date: Fri, 10 Apr 2026 16:06:55 -0400
Subject: [PATCH 64/74] move search box columns
---
app/views/allocations/_search_boxes.html.erb | 97 ++++++++++++--------
1 file changed, 60 insertions(+), 37 deletions(-)
diff --git a/app/views/allocations/_search_boxes.html.erb b/app/views/allocations/_search_boxes.html.erb
index 507591818..84e0b5b78 100644
--- a/app/views/allocations/_search_boxes.html.erb
+++ b/app/views/allocations/_search_boxes.html.erb
@@ -1,39 +1,50 @@
<% default_btn = "text-gray-600 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-800 shadow-sm" %>
<% selected_btn = "text-white bg-blue-600 border-blue-600 hover:bg-blue-700 hover:text-white shadow-md ring-2 ring-blue-300" %>
+
<%= form_tag allocations_path, method: :get, class: "space-y-4",
data: {
- controller: "collection search-type-select",
+ controller: "collection",
turbo_frame: "allocation_results",
collection_unselected_class: default_btn,
collection_selected_class: selected_btn
}, autocomplete: "off" do %>
-
+
-
Source Type
- <%= select_tag "source_type[]",
+
+ Source Type
+
+ <%= select_tag "source_type[]",
options_for_select({ "All" => "", "Cash" => "CashPayment", "Check" => "CheckPayment", "Stripe" => "ExternalProcessorPayment" }, Array(params[:source_type]).first),
class: "w-full bg-white border border-gray-300 rounded-lg px-3 py-2 focus:ring-blue-500 focus:border-blue-500" %>
-
-
- Reverted
- <%= select_tag "has_reverted",
+
+
+
+ Reverted
+
+ <%= select_tag "has_reverted",
options_for_select({ "All" => "all", "Yes" => "yes", "No" => "no" }, params[:has_reverted] || "all"),
class: "w-full bg-white border border-gray-300 rounded-lg px-3 py-2 focus:ring-blue-500 focus:border-blue-500" %>
+
-
-
Payer Type
- <%= select_tag "payer_type",
+
+ <%= tag.div data: { controller: "search-type-select"} do %>
+
+ Payer Type
+
+ <%= select_tag "payer_type",
options_for_select(["Person", "Organization"], params[:payer_type]),
include_blank: true,
class: "w-full bg-white border border-gray-300 rounded-lg px-3 py-2 focus:ring-blue-500 focus:border-blue-500",
data: { action: "search-type-select#toggle" } %>
-
-
-
- Payer
- <%= select_tag "payer_id",
+
+
+
+
+ Payer
+
+ <%= select_tag "payer_id",
options_for_select(Person.where(id: params[:payer_id]).map { |p| [p.full_name, p.id] }),
include_blank: true,
class: "w-full bg-white border border-gray-300 rounded-lg px-3 py-2 focus:ring-blue-500 focus:border-blue-500",
@@ -42,12 +53,14 @@
remote_select_model_value: "person"
},
prompt: "Search for a person" %>
-
-
-
-
- Payer
- <%= select_tag "payer_id",
+
+
+
+
+
+ Payer
+
+ <%= select_tag "payer_id",
options_for_select(Organization.where(id: params[:payer_id]).map { |o| [o.name, o.id] }),
include_blank: true,
class: "w-full bg-white border border-gray-300 rounded-lg px-3 py-2 focus:ring-blue-500 focus:border-blue-500",
@@ -56,21 +69,28 @@
remote_select_model_value: "organization"
},
prompt: "Search for an organization" %>
-
-
-
-
- Allocated To Type
- <%= select_tag "allocatable_type",
+
+
+
+
+ <% end %>
+
+ <%= tag.div data: { controller: "search-type-select"} do %>
+
+ Allocated To Type
+
+ <%= select_tag "allocatable_type",
options_for_select(["EventRegistration"], params[:allocatable_type]),
include_blank: true,
class: "w-full bg-white border border-gray-300 rounded-lg px-3 py-2 focus:ring-blue-500 focus:border-blue-500",
data: { action: "search-type-select#toggle" } %>
-
-
-
- Allocated To
- <%= select_tag "allocatable_id",
+
+
+
+
+ Allocated To
+
+ <%= select_tag "allocatable_id",
options_for_select(EventRegistration.where(id: params[:allocatable_id]).map { |r| ["#{r.registrant.full_name} - #{r.event.title}", r.id] }),
include_blank: true,
class: "w-full bg-white border border-gray-300 rounded-lg px-3 py-2 focus:ring-blue-500 focus:border-blue-500",
@@ -79,10 +99,13 @@
remote_select_model_value: "event_registration"
},
prompt: "Search for a registration" %>
-
-
-
- <%= submit_tag "Search", class: "btn btn-primary" %><%= link_to "Clear filters", allocations_path, class: "btn btn-secondary", data: { action: "collection#clearAndSubmit" } %>
+
+
+
+
+ <% end %>
+
+
<%= submit_tag "Search", class: "btn btn-primary" %><%= link_to "Clear filters", allocations_path, class: "btn btn-secondary", data: { action: "collection#clearAndSubmit" } %>
<% end %>
From a77703cc228dc130c2a2ca5c4a17507e83950017 Mon Sep 17 00:00:00 2001
From: Justin Miller <16829344+jmilljr24@users.noreply.github.com>
Date: Sat, 11 Apr 2026 08:24:14 -0400
Subject: [PATCH 65/74] add placeholder for type select
---
.../search_type_select_controller.js | 9 ++++++
app/views/allocations/_search_boxes.html.erb | 31 +++++++------------
app/views/allocations/new.html.erb | 7 ++++-
app/views/payments/_form.html.erb | 7 ++++-
app/views/payments/_search_boxes.html.erb | 18 ++++-------
app/views/refunds/new.html.erb | 7 ++++-
6 files changed, 45 insertions(+), 34 deletions(-)
diff --git a/app/frontend/javascript/controllers/search_type_select_controller.js b/app/frontend/javascript/controllers/search_type_select_controller.js
index 31ec0ecd5..046c3d2a9 100644
--- a/app/frontend/javascript/controllers/search_type_select_controller.js
+++ b/app/frontend/javascript/controllers/search_type_select_controller.js
@@ -3,11 +3,20 @@ import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
static targets = ["template", "pickerContainer"];
+ connect() {
+ this._placeholder = this.pickerContainerTarget.innerHTML;
+ }
+
toggle(event) {
const selectedType = event.target.value;
this.pickerContainerTarget.innerHTML = "";
+ if (!selectedType) {
+ this.pickerContainerTarget.innerHTML = this._placeholder;
+ return;
+ }
+
const template = this.templateTargets.find(t => t.dataset.type === selectedType);
if (template) {
const cloned = template.content.cloneNode(true);
diff --git a/app/views/allocations/_search_boxes.html.erb b/app/views/allocations/_search_boxes.html.erb
index 84e0b5b78..d242a4c9e 100644
--- a/app/views/allocations/_search_boxes.html.erb
+++ b/app/views/allocations/_search_boxes.html.erb
@@ -1,7 +1,6 @@
<% default_btn = "text-gray-600 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-800 shadow-sm" %>
<% selected_btn = "text-white bg-blue-600 border-blue-600 hover:bg-blue-700 hover:text-white shadow-md ring-2 ring-blue-300" %>
-
<%= form_tag allocations_path, method: :get, class: "space-y-4",
data: {
@@ -14,36 +13,29 @@
Source Type
-
<%= select_tag "source_type[]",
options_for_select({ "All" => "", "Cash" => "CashPayment", "Check" => "CheckPayment", "Stripe" => "ExternalProcessorPayment" }, Array(params[:source_type]).first),
class: "w-full bg-white border border-gray-300 rounded-lg px-3 py-2 focus:ring-blue-500 focus:border-blue-500" %>
-
Reverted
-
<%= select_tag "has_reverted",
options_for_select({ "All" => "all", "Yes" => "yes", "No" => "no" }, params[:has_reverted] || "all"),
class: "w-full bg-white border border-gray-300 rounded-lg px-3 py-2 focus:ring-blue-500 focus:border-blue-500" %>
-
<%= tag.div data: { controller: "search-type-select"} do %>
Payer Type
-
<%= select_tag "payer_type",
options_for_select(["Person", "Organization"], params[:payer_type]),
include_blank: true,
class: "w-full bg-white border border-gray-300 rounded-lg px-3 py-2 focus:ring-blue-500 focus:border-blue-500",
data: { action: "search-type-select#toggle" } %>
-
Payer
-
<%= select_tag "payer_id",
options_for_select(Person.where(id: params[:payer_id]).map { |p| [p.full_name, p.id] }),
include_blank: true,
@@ -55,11 +47,9 @@
prompt: "Search for a person" %>
-
Payer
-
<%= select_tag "payer_id",
options_for_select(Organization.where(id: params[:payer_id]).map { |o| [o.name, o.id] }),
include_blank: true,
@@ -71,25 +61,25 @@
prompt: "Search for an organization" %>
-
-
+
<% end %>
-
<%= tag.div data: { controller: "search-type-select"} do %>
Allocated To Type
-
<%= select_tag "allocatable_type",
options_for_select(["EventRegistration"], params[:allocatable_type]),
include_blank: true,
class: "w-full bg-white border border-gray-300 rounded-lg px-3 py-2 focus:ring-blue-500 focus:border-blue-500",
data: { action: "search-type-select#toggle" } %>
-
Allocated To
-
<%= select_tag "allocatable_id",
options_for_select(EventRegistration.where(id: params[:allocatable_id]).map { |r| ["#{r.registrant.full_name} - #{r.event.title}", r.id] }),
include_blank: true,
@@ -101,11 +91,14 @@
prompt: "Search for a registration" %>
-
-
+
+
+
Select Allocated To Type
+
+
+
<% end %>
-
<%= submit_tag "Search", class: "btn btn-primary" %><%= link_to "Clear filters", allocations_path, class: "btn btn-secondary", data: { action: "collection#clearAndSubmit" } %>
<% end %>
diff --git a/app/views/allocations/new.html.erb b/app/views/allocations/new.html.erb
index b8d58ece3..5873d70db 100644
--- a/app/views/allocations/new.html.erb
+++ b/app/views/allocations/new.html.erb
@@ -33,7 +33,12 @@
label: "Allocatable",
required: true %>
-
+
+
+
Select Allocatable Type
+
+
+
<%= f.input :amount_dollars, label: "Amount ($)", input_html: { min: 0, step: 0.01, placeholder: "0.00", data: { controller: "currency-input", action: "currency-input#format" } } %>
<%= f.button :submit, "Create Allocation", class: "btn btn-primary" %>
diff --git a/app/views/payments/_form.html.erb b/app/views/payments/_form.html.erb
index c0c6a03b0..d6996a1f6 100644
--- a/app/views/payments/_form.html.erb
+++ b/app/views/payments/_form.html.erb
@@ -37,7 +37,12 @@
prompt: "Search for an organization",
label: false %>
-
+
<%= f.input :amount_dollars, label: "Amount ($)", input_html: { min: 0, step: 0.01, placeholder: "0.00", data: { controller: "currency-input", action: "currency-input#format" } } %>
<%= f.input :currency, as: :hidden, input_html: { value: "usd" } %>
<% if @payment.type == "CheckPayment" %>
diff --git a/app/views/payments/_search_boxes.html.erb b/app/views/payments/_search_boxes.html.erb
index e29ddecde..67a568f6b 100644
--- a/app/views/payments/_search_boxes.html.erb
+++ b/app/views/payments/_search_boxes.html.erb
@@ -1,7 +1,6 @@
<% default_btn = "text-gray-600 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-800 shadow-sm" %>
<% selected_btn = "text-white bg-blue-600 border-blue-600 hover:bg-blue-700 hover:text-white shadow-md ring-2 ring-blue-300" %>
-
<%= form_tag payments_path, method: :get, class: "space-y-4",
data: {
@@ -13,34 +12,27 @@
Type
-
<%= select_tag "type[]",
options_for_select({ "All" => "", "Cash" => "CashPayment", "Check" => "CheckPayment", "Stripe" => "ExternalProcessorPayment" }, Array(params[:type]).first),
class: "w-full bg-white border border-gray-300 rounded-lg px-3 py-2 focus:ring-blue-500 focus:border-blue-500" %>
-
Amount Remaining
-
<%= select_tag "has_remaining",
options_for_select({ "All" => "all", "Yes" => "yes", "No" => "no" }, params[:has_remaining] || "all"),
class: "w-full bg-white border border-gray-300 rounded-lg px-3 py-2 focus:ring-blue-500 focus:border-blue-500" %>
-
Payer Type
-
<%= select_tag "payer_type",
options_for_select(["Person", "Organization"], params[:payer_type]),
include_blank: true,
class: "w-full bg-white border border-gray-300 rounded-lg px-3 py-2 focus:ring-blue-500 focus:border-blue-500",
data: { action: "search-type-select#toggle" } %>
-
Payer
-
<%= select_tag "payer_id",
options_for_select(Person.where(id: params[:payer_id]).map { |p| [p.full_name, p.id] }),
include_blank: true,
@@ -52,11 +44,9 @@
prompt: "Search for a person" %>
-
Payer
-
<%= select_tag "payer_id",
options_for_select(Organization.where(id: params[:payer_id]).map { |o| [o.name, o.id] }),
include_blank: true,
@@ -68,8 +58,12 @@
prompt: "Search for an organization" %>
-
-
+
<%= submit_tag "Search", class: "btn btn-primary" %><%= link_to "Clear filters", payments_path, class: "btn btn-secondary", data: { action: "collection#clearAndSubmit" } %>
<% end %>
diff --git a/app/views/refunds/new.html.erb b/app/views/refunds/new.html.erb
index 039f51540..27aa33b03 100644
--- a/app/views/refunds/new.html.erb
+++ b/app/views/refunds/new.html.erb
@@ -41,7 +41,12 @@
prompt: "Search for an organization",
label: "Recipient" %>
-
+
+
+
Select Recipient Type
+
+
+
Method
From 5b25736e6e9063bb5d7e710785d01a7faecf27ad Mon Sep 17 00:00:00 2001
From: Justin Miller <16829344+jmilljr24@users.noreply.github.com>
Date: Sat, 11 Apr 2026 09:04:51 -0400
Subject: [PATCH 66/74] clear search
---
.../controllers/collection_controller.js | 7 +++++++
app/views/allocations/_search_boxes.html.erb | 5 +++--
.../allocations/allocation_results.html.erb | 9 +--------
app/views/payments/_search_boxes.html.erb | 16 +++++++++++++++-
app/views/payments/index.html.erb | 2 +-
app/views/payments/payment_results.html.erb | 4 +---
6 files changed, 28 insertions(+), 15 deletions(-)
diff --git a/app/frontend/javascript/controllers/collection_controller.js b/app/frontend/javascript/controllers/collection_controller.js
index 783aba1ff..e5b984935 100644
--- a/app/frontend/javascript/controllers/collection_controller.js
+++ b/app/frontend/javascript/controllers/collection_controller.js
@@ -3,6 +3,8 @@ import { Controller } from "@hotwired/stimulus";
// Connects to data-controller="collection"
export default class extends Controller {
static classes = ["unselected", "selected"];
+ static outlets = ["search-type-select"];
+
connect() {
this.element.addEventListener("change", (event) => {
const { type } = event.target;
@@ -80,6 +82,11 @@ export default class extends Controller {
input.checked = false;
});
this.element.reset();
+
+ this.searchTypeSelectOutlets.forEach((controller) => {
+ controller.toggle({ target: { value: "" } });
+ });
+
this.submitForm();
}
diff --git a/app/views/allocations/_search_boxes.html.erb b/app/views/allocations/_search_boxes.html.erb
index d242a4c9e..d1ee9aa7d 100644
--- a/app/views/allocations/_search_boxes.html.erb
+++ b/app/views/allocations/_search_boxes.html.erb
@@ -5,6 +5,7 @@
<%= form_tag allocations_path, method: :get, class: "space-y-4",
data: {
controller: "collection",
+ collection_search_type_select_outlet: "[data-type-picker]",
turbo_frame: "allocation_results",
collection_unselected_class: default_btn,
collection_selected_class: selected_btn
@@ -24,7 +25,7 @@
class: "w-full bg-white border border-gray-300 rounded-lg px-3 py-2 focus:ring-blue-500 focus:border-blue-500" %>
- <%= tag.div data: { controller: "search-type-select"} do %>
+ <%= tag.div data: { controller: "search-type-select", type_picker: "" } do %>
Payer Type
<%= select_tag "payer_type",
@@ -68,7 +69,7 @@
<% end %>
- <%= tag.div data: { controller: "search-type-select"} do %>
+ <%= tag.div data: { controller: "search-type-select", type_picker: "" } do %>
Allocated To Type
<%= select_tag "allocatable_type",
diff --git a/app/views/allocations/allocation_results.html.erb b/app/views/allocations/allocation_results.html.erb
index a6b3a2f48..3fbe65bb1 100644
--- a/app/views/allocations/allocation_results.html.erb
+++ b/app/views/allocations/allocation_results.html.erb
@@ -1,6 +1,6 @@
<%= turbo_frame_tag :allocation_results do %>
<% if @allocations.any? %>
-
+
@@ -13,7 +13,6 @@
Date
-
<% @allocations.each do |allocation| %>
<% row_class = if allocation.amount.to_i < 0
@@ -23,12 +22,10 @@
else
'hover:bg-gray-50'
end %>
-
<%= allocation.id %>
<%= allocation.source.class.name.underscore.titleize.gsub(" Payment", "").gsub("External Processor", "Stripe") %>
<%= allocation.source&.payer.name || "Unknown" %>
-
<% if allocation.allocatable_type == "EventRegistration" && allocation.allocatable.present? %>
<%= truncate("#{allocation.allocatable.registrant.full_name} - #{allocation.allocatable.event.title}", length: 40) %>
@@ -36,9 +33,7 @@
<%= allocation.allocatable_type.underscore.titleize %>
<% end %>
-
$<%= "%.2f" % allocation.amount_dollars %>
-
<% if allocation.amount.to_i < 0 %>
<%= Allocation.find_by(reverted_id: allocation.id)&.id || "-" %>
@@ -46,7 +41,6 @@
<%= allocation.reverted_id ? allocation.reverted_id : "-" %>
<% end %>
-
<%= allocation.created_at.strftime("%B %d, %Y") %>
<% end %>
@@ -56,7 +50,6 @@
<% else %>
There are no allocations that match your search. Please try again.
<% end %>
-
<% if @allocations.respond_to?(:total_pages) && @allocations.total_pages > 1 %>
<% end %>
diff --git a/app/views/payments/_search_boxes.html.erb b/app/views/payments/_search_boxes.html.erb
index 67a568f6b..d62513358 100644
--- a/app/views/payments/_search_boxes.html.erb
+++ b/app/views/payments/_search_boxes.html.erb
@@ -1,38 +1,48 @@
<% default_btn = "text-gray-600 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-800 shadow-sm" %>
<% selected_btn = "text-white bg-blue-600 border-blue-600 hover:bg-blue-700 hover:text-white shadow-md ring-2 ring-blue-300" %>
+
<%= form_tag payments_path, method: :get, class: "space-y-4",
data: {
controller: "collection search-type-select",
+ collection_search_type_select_outlet: "[data-type-picker]",
turbo_frame: "payment_results",
collection_unselected_class: default_btn,
- collection_selected_class: selected_btn
+ collection_selected_class: selected_btn,
+ type_picker: ""
}, autocomplete: "off" do %>
Type
+
<%= select_tag "type[]",
options_for_select({ "All" => "", "Cash" => "CashPayment", "Check" => "CheckPayment", "Stripe" => "ExternalProcessorPayment" }, Array(params[:type]).first),
class: "w-full bg-white border border-gray-300 rounded-lg px-3 py-2 focus:ring-blue-500 focus:border-blue-500" %>
+
Amount Remaining
+
<%= select_tag "has_remaining",
options_for_select({ "All" => "all", "Yes" => "yes", "No" => "no" }, params[:has_remaining] || "all"),
class: "w-full bg-white border border-gray-300 rounded-lg px-3 py-2 focus:ring-blue-500 focus:border-blue-500" %>
+
Payer Type
+
<%= select_tag "payer_type",
options_for_select(["Person", "Organization"], params[:payer_type]),
include_blank: true,
class: "w-full bg-white border border-gray-300 rounded-lg px-3 py-2 focus:ring-blue-500 focus:border-blue-500",
data: { action: "search-type-select#toggle" } %>
+
Payer
+
<%= select_tag "payer_id",
options_for_select(Person.where(id: params[:payer_id]).map { |p| [p.full_name, p.id] }),
include_blank: true,
@@ -44,9 +54,11 @@
prompt: "Search for a person" %>
+
Payer
+
<%= select_tag "payer_id",
options_for_select(Organization.where(id: params[:payer_id]).map { |o| [o.name, o.id] }),
include_blank: true,
@@ -58,12 +70,14 @@
prompt: "Search for an organization" %>
+
+
<%= submit_tag "Search", class: "btn btn-primary" %><%= link_to "Clear filters", payments_path, class: "btn btn-secondary", data: { action: "collection#clearAndSubmit" } %>
<% end %>
diff --git a/app/views/payments/index.html.erb b/app/views/payments/index.html.erb
index 0808a7e0c..8cb7411f0 100644
--- a/app/views/payments/index.html.erb
+++ b/app/views/payments/index.html.erb
@@ -7,7 +7,7 @@
<%= render 'search_boxes' %>
<% result_src = payments_path + "?" + request.query_string %>
- <%= turbo_frame_tag :payment_results, src: result_src, autoscroll: "start", data: { turbo: "temporary" } do %>
+ <%= turbo_frame_tag :payment_results, src: result_src, data: { turbo: "temporary" } do %>
diff --git a/app/views/payments/payment_results.html.erb b/app/views/payments/payment_results.html.erb
index 7fdbc7219..fbca0bc0f 100644
--- a/app/views/payments/payment_results.html.erb
+++ b/app/views/payments/payment_results.html.erb
@@ -1,6 +1,6 @@
<%= turbo_frame_tag :payment_results do %>
<% if @payments.any? %>
-
+
@@ -12,7 +12,6 @@
Actions
-
<% @payments.each do |payment| %>
@@ -30,6 +29,5 @@
<% else %>
There are no payments that match your search. Please try again.
<% end %>
-
<% end %>
From e880e14ce33a73aa45184fe4dcd7731762665b9c Mon Sep 17 00:00:00 2001
From: Justin Miller <16829344+jmilljr24@users.noreply.github.com>
Date: Sun, 12 Apr 2026 15:26:46 -0400
Subject: [PATCH 67/74] check for outlet
---
.../javascript/controllers/collection_controller.js | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/app/frontend/javascript/controllers/collection_controller.js b/app/frontend/javascript/controllers/collection_controller.js
index e5b984935..2d5818a75 100644
--- a/app/frontend/javascript/controllers/collection_controller.js
+++ b/app/frontend/javascript/controllers/collection_controller.js
@@ -83,9 +83,11 @@ export default class extends Controller {
});
this.element.reset();
- this.searchTypeSelectOutlets.forEach((controller) => {
- controller.toggle({ target: { value: "" } });
- });
+ if (this.hasSearchTypeSelectOutlet) {
+ this.searchTypeSelectOutlets.forEach((controller) => {
+ controller.toggle({ target: { value: "" } });
+ });
+ }
this.submitForm();
}
From 969ce18946094e5e1e9404ea17d52ee4fc897fa3 Mon Sep 17 00:00:00 2001
From: Justin Miller <16829344+jmilljr24@users.noreply.github.com>
Date: Sun, 12 Apr 2026 15:27:40 -0400
Subject: [PATCH 68/74] justify end for clear filters buttons
---
app/views/payments/_search_boxes.html.erb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/views/payments/_search_boxes.html.erb b/app/views/payments/_search_boxes.html.erb
index d62513358..573c7cbb2 100644
--- a/app/views/payments/_search_boxes.html.erb
+++ b/app/views/payments/_search_boxes.html.erb
@@ -78,7 +78,7 @@
- <%= submit_tag "Search", class: "btn btn-primary" %><%= link_to "Clear filters", payments_path, class: "btn btn-secondary", data: { action: "collection#clearAndSubmit" } %>
+ <%= submit_tag "Search", class: "btn btn-primary" %><%= link_to "Clear filters", payments_path, class: "btn btn-secondary", data: { action: "collection#clearAndSubmit" } %>
<% end %>
From 3ea05bd709b30c33dd095f4cd77e179328ceee71 Mon Sep 17 00:00:00 2001
From: Justin Miller <16829344+jmilljr24@users.noreply.github.com>
Date: Mon, 13 Apr 2026 18:47:17 -0400
Subject: [PATCH 69/74] use model card helper
---
app/helpers/admin_cards_helper.rb | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/helpers/admin_cards_helper.rb b/app/helpers/admin_cards_helper.rb
index 7a094c29d..3ba78ff02 100644
--- a/app/helpers/admin_cards_helper.rb
+++ b/app/helpers/admin_cards_helper.rb
@@ -35,8 +35,8 @@ def user_content_cards
model_card(:workshop_ideas, icon: "💡", intensity: 100),
model_card(:workshop_variation_ideas, icon: "🔀", intensity: 100),
model_card(:workshop_logs, icon: "📝", intensity: 100),
- custom_card("Payments", payments_path, icon: "💳"),
- custom_card("Allocations", allocations_path, icon: "📤")
+ model_card(:payments, icon: "💳"),
+ model_card(:allocations, icon: "📤")
]
end
From 7519974240f019a184ffe62b1091fad4548a1fab Mon Sep 17 00:00:00 2001
From: Justin Miller <16829344+jmilljr24@users.noreply.github.com>
Date: Mon, 13 Apr 2026 19:24:57 -0400
Subject: [PATCH 70/74] clean up
---
app/models/allocation.rb | 2 --
app/models/payment.rb | 2 --
app/models/refund.rb | 2 --
3 files changed, 6 deletions(-)
diff --git a/app/models/allocation.rb b/app/models/allocation.rb
index 787bd5d3e..179010f90 100644
--- a/app/models/allocation.rb
+++ b/app/models/allocation.rb
@@ -1,6 +1,4 @@
class Allocation < ApplicationRecord
- attr_accessor :amount_dollars
-
belongs_to :source, polymorphic: true
belongs_to :allocatable, polymorphic: true
belongs_to :reverted, class_name: "Allocation", optional: true
diff --git a/app/models/payment.rb b/app/models/payment.rb
index 92b0ac1bf..bee7c61bf 100644
--- a/app/models/payment.rb
+++ b/app/models/payment.rb
@@ -1,6 +1,4 @@
class Payment < ApplicationRecord
- attr_accessor :amount_dollars, :remaining_dollars
-
has_many :allocations, as: :source
has_many :refunds, as: :refundable
belongs_to :payer, polymorphic: true
diff --git a/app/models/refund.rb b/app/models/refund.rb
index 5a559df43..fce590d62 100644
--- a/app/models/refund.rb
+++ b/app/models/refund.rb
@@ -1,8 +1,6 @@
class Refund < ApplicationRecord
METHODS = %w[check cash stripe].freeze
- attr_accessor :amount_dollars
-
belongs_to :refundable, polymorphic: true
belongs_to :recipient, polymorphic: true
has_many :allocations, as: :source
From 641f8aaf3cbb05f9adf7cb2f2565f1fdc1537c97 Mon Sep 17 00:00:00 2001
From: maebeale
Date: Thu, 16 Apr 2026 22:11:05 -0400
Subject: [PATCH 71/74] Fix system notifications showing Oopsie error (#1488)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Fix system notifications showing "Oopsie!" by adopting lazy-load pattern
The notifications index rendered data inline inside a turbo frame while
also setting a src attribute to re-fetch the same data. If the redundant
request failed, the turbo:frame-missing handler replaced the content
with an error message. Aligns with the lazy-load pattern used by other
controllers (stories, people, etc.) — skeleton on initial load, data
fetched via turbo frame request.
Co-Authored-By: Claude Opus 4.6
* Add turbo_frame: _top to links inside notifications turbo frame
Links inside the notifications_results turbo frame were being
intercepted by Turbo, which tried to load the target page inside
the frame. The show page and polymorphic record pages don't contain
a matching turbo frame, triggering the turbo:frame-missing handler
and showing the "Oopsie!" error.
Co-Authored-By: Claude Opus 4.6
---------
Co-authored-by: Claude Opus 4.6
---
app/controllers/notifications_controller.rb | 17 ++++++++-----
app/views/notifications/_index.html.erb | 2 ++
.../_notifications_results.html.erb | 21 ----------------
.../notifications/_results_skeleton.html.erb | 24 +++++++++++++++++++
app/views/notifications/index.html.erb | 9 +------
spec/requests/notifications_spec.rb | 19 ++++++++-------
6 files changed, 49 insertions(+), 43 deletions(-)
delete mode 100644 app/views/notifications/_notifications_results.html.erb
create mode 100644 app/views/notifications/_results_skeleton.html.erb
diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb
index 0b76fd2a0..6c948f914 100644
--- a/app/controllers/notifications_controller.rb
+++ b/app/controllers/notifications_controller.rb
@@ -3,13 +3,18 @@ class NotificationsController < ApplicationController
def index
authorize!
- per_page = params[:number_of_items_per_page].presence || 25
- base_scope = authorized_scope(Notification.includes(:noticeable))
- filtered = base_scope.search_by_params(params.to_unsafe_h)
- @notifications = filtered.order(created_at: :desc)
- .paginate(page: params[:page], per_page: per_page)
- render turbo_frame_request? ? :notifications_results : :index
+ if turbo_frame_request?
+ per_page = params[:number_of_items_per_page].presence || 25
+ base_scope = authorized_scope(Notification.includes(:noticeable))
+ filtered = base_scope.search_by_params(params.to_unsafe_h)
+ @notifications = filtered.order(created_at: :desc)
+ .paginate(page: params[:page], per_page: per_page)
+
+ render :notifications_results
+ else
+ render :index
+ end
end
def show
diff --git a/app/views/notifications/_index.html.erb b/app/views/notifications/_index.html.erb
index ad0e3e9bd..22898cca7 100644
--- a/app/views/notifications/_index.html.erb
+++ b/app/views/notifications/_index.html.erb
@@ -44,6 +44,7 @@
<% if notification.noticeable.present? %>
<%= link_to notification.noticeable.class.name,
polymorphic_path(notification.noticeable),
+ data: { turbo_frame: "_top" },
class: "btn btn-secondary-outline" %>
<% else %>
—
@@ -54,6 +55,7 @@
<%= link_to "View",
notification_path(notification),
+ data: { turbo_frame: "_top" },
class: "btn btn-secondary-outline" %>
diff --git a/app/views/notifications/_notifications_results.html.erb b/app/views/notifications/_notifications_results.html.erb
deleted file mode 100644
index 5638b31c2..000000000
--- a/app/views/notifications/_notifications_results.html.erb
+++ /dev/null
@@ -1,21 +0,0 @@
-<%= turbo_stream.replace("notifications_count", partial: "notifications_count") %>
-<% if @notifications.any? %>
-
- <%= render "index" %>
-
-
-
-
-
-<% else %>
-
-
No notifications yet
- <% if allowed_to?(:new?, Notification) %>
-
- <%= link_to "Create a notification",
- new_notification_path,
- class: "btn btn-primary" %>
-
- <% end %>
-
-<% end %>
diff --git a/app/views/notifications/_results_skeleton.html.erb b/app/views/notifications/_results_skeleton.html.erb
new file mode 100644
index 000000000..3f95119aa
--- /dev/null
+++ b/app/views/notifications/_results_skeleton.html.erb
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ <% 5.times do %>
+
+
+
+
+
+
+
+ <% end %>
+
+
+
diff --git a/app/views/notifications/index.html.erb b/app/views/notifications/index.html.erb
index 9bde34ce3..25317c709 100644
--- a/app/views/notifications/index.html.erb
+++ b/app/views/notifications/index.html.erb
@@ -11,14 +11,7 @@
<% result_src = notifications_path + "?" + request.query_string %>
<%= turbo_frame_tag "notifications_results", src: result_src do %>
-
- <%= render "index" %>
-
-
-
-
+ <%= render "results_skeleton" %>
<% end %>
diff --git a/spec/requests/notifications_spec.rb b/spec/requests/notifications_spec.rb
index 7cfdc557f..89629af49 100644
--- a/spec/requests/notifications_spec.rb
+++ b/spec/requests/notifications_spec.rb
@@ -8,22 +8,25 @@
describe "GET /notifications" do
before { sign_in admin }
+ let(:turbo_headers) { { "Turbo-Frame" => "notifications_results" } }
let!(:story_notification) { create(:notification, noticeable: create(:story_idea), email_subject: "New story idea") }
let!(:user_notification) { create(:notification, noticeable: create(:user), email_subject: "Welcome") }
- it "preserves the email_topic filter selection" do
- get notifications_path, params: { email_topic: "User: confirm new email" }
- expect(response.body).to include('selected="selected"')
- expect(response.body).to include("User: confirm new email")
+ it "filters by email_topic" do
+ matching = create(:notification, email_subject: "Confirm your new email address")
+ get notifications_path, params: { email_topic: "User: confirm new email" }, headers: turbo_headers
+ expect(response.body).to include(matching.recipient_email)
+ expect(response.body).not_to include(story_notification.recipient_email)
end
- it "preserves the record_type filter selection" do
- get notifications_path, params: { record_type: "StoryIdea" }
- expect(response.body).to include('selected="selected"')
+ it "filters by record_type" do
+ get notifications_path, params: { record_type: "StoryIdea" }, headers: turbo_headers
+ expect(response.body).to include(story_notification.recipient_email)
+ expect(response.body).not_to include(user_notification.recipient_email)
end
it "wraps results in a turbo frame" do
- get notifications_path
+ get notifications_path, headers: turbo_headers
expect(response.body).to include('id="notifications_results"')
end
end
From c48d1dad38685f4080038ba3a009a7d64dbd0663 Mon Sep 17 00:00:00 2001
From: maebeale
Date: Fri, 15 May 2026 10:13:42 -0400
Subject: [PATCH 72/74] Bump vulnerable gems flagged by bundler-audit (#1495)
Updates addressable, net-imap, nokogiri, rack, and rack-session to
patched versions to clear CVE advisories from the scan_ruby CI job.
Co-authored-by: Claude Opus 4.7
---
Gemfile.lock | 48 ++++++++++++++++++++++++------------------------
1 file changed, 24 insertions(+), 24 deletions(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index 683bbdd6c..5bd0ab93d 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -95,7 +95,7 @@ GEM
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
- addressable (2.8.8)
+ addressable (2.9.0)
public_suffix (>= 2.0.2, < 8.0)
ahoy_matey (5.4.1)
activesupport (>= 7.1)
@@ -315,7 +315,7 @@ GEM
multi_xml (0.8.1)
bigdecimal (>= 3.1, < 5)
mutex_m (0.3.0)
- net-imap (0.6.3)
+ net-imap (0.6.4)
date
net-protocol
net-pop (0.1.2)
@@ -325,21 +325,21 @@ GEM
net-smtp (0.5.1)
net-protocol
nio4r (2.7.5)
- nokogiri (1.19.2-aarch64-linux-gnu)
+ nokogiri (1.19.3-aarch64-linux-gnu)
racc (~> 1.4)
- nokogiri (1.19.2-aarch64-linux-musl)
+ nokogiri (1.19.3-aarch64-linux-musl)
racc (~> 1.4)
- nokogiri (1.19.2-arm-linux-gnu)
+ nokogiri (1.19.3-arm-linux-gnu)
racc (~> 1.4)
- nokogiri (1.19.2-arm-linux-musl)
+ nokogiri (1.19.3-arm-linux-musl)
racc (~> 1.4)
- nokogiri (1.19.2-arm64-darwin)
+ nokogiri (1.19.3-arm64-darwin)
racc (~> 1.4)
- nokogiri (1.19.2-x86_64-darwin)
+ nokogiri (1.19.3-x86_64-darwin)
racc (~> 1.4)
- nokogiri (1.19.2-x86_64-linux-gnu)
+ nokogiri (1.19.3-x86_64-linux-gnu)
racc (~> 1.4)
- nokogiri (1.19.2-x86_64-linux-musl)
+ nokogiri (1.19.3-x86_64-linux-musl)
racc (~> 1.4)
opentelemetry-api (1.7.0)
opentelemetry-common (0.23.0)
@@ -561,12 +561,12 @@ GEM
nio4r (~> 2.0)
raabro (1.4.0)
racc (1.8.1)
- rack (3.2.5)
+ rack (3.2.6)
rack-mini-profiler (4.0.1)
rack (>= 1.2.0)
rack-proxy (0.7.7)
rack
- rack-session (2.1.1)
+ rack-session (2.1.2)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rack-test (2.2.0)
@@ -857,7 +857,7 @@ CHECKSUMS
activerecord (8.1.3) sha256=8003be7b2466ba0a2a670e603eeb0a61dd66058fccecfc49901e775260ac70ab
activestorage (8.1.3) sha256=0564ce9309143951a67615e1bb4e090ee54b8befed417133cae614479b46384d
activesupport (8.1.3) sha256=21a5e0dfbd4c3ddd9e1317ec6a4d782fa226e7867dc70b0743acda81a1dca20e
- addressable (2.8.8) sha256=7c13b8f9536cf6364c03b9d417c19986019e28f7c00ac8132da4eb0fe393b057
+ addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af
ahoy_matey (5.4.1) sha256=b9ccac7f497889de6982f1503b41a7e275c5a6e4e12a6947b418c69c2c2119b1
ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
aws-eventstream (1.4.0) sha256=116bf85c436200d1060811e6f5d2d40c88f65448f2125bc77ffce5121e6e183b
@@ -959,19 +959,19 @@ CHECKSUMS
msgpack (1.8.0) sha256=e64ce0212000d016809f5048b48eb3a65ffb169db22238fb4b72472fecb2d732
multi_xml (0.8.1) sha256=addba0290bac34e9088bfe73dc4878530297a82a7bbd66cb44dcd0a4b86edf5a
mutex_m (0.3.0) sha256=cfcb04ac16b69c4813777022fdceda24e9f798e48092a2b817eb4c0a782b0751
- net-imap (0.6.3) sha256=9bab75f876596d09ee7bf911a291da478e0cd6badc54dfb82874855ccc82f2ad
+ net-imap (0.6.4) sha256=9a5598c67a3022c284d98430ef1d4948e7dbdb62596f61081ea8ca933270a02b
net-pop (0.1.2) sha256=848b4e982013c15b2f0382792268763b748cce91c9e91e36b0f27ed26420dff3
net-protocol (0.2.2) sha256=aa73e0cba6a125369de9837b8d8ef82a61849360eba0521900e2c3713aa162a8
net-smtp (0.5.1) sha256=ed96a0af63c524fceb4b29b0d352195c30d82dd916a42f03c62a3a70e5b70736
nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1
- nokogiri (1.19.2-aarch64-linux-gnu) sha256=c34d5c8208025587554608e98fd88ab125b29c80f9352b821964e9a5d5cfbd19
- nokogiri (1.19.2-aarch64-linux-musl) sha256=7f6b4b0202d507326841a4f790294bf75098aef50c7173443812e3ac5cb06515
- nokogiri (1.19.2-arm-linux-gnu) sha256=b7fa1139016f3dc850bda1260988f0d749934a939d04ef2da13bec060d7d5081
- nokogiri (1.19.2-arm-linux-musl) sha256=61114d44f6742ff72194a1b3020967201e2eb982814778d130f6471c11f9828c
- nokogiri (1.19.2-arm64-darwin) sha256=58d8ea2e31a967b843b70487a44c14c8ba1866daa1b9da9be9dbdf1b43dee205
- nokogiri (1.19.2-x86_64-darwin) sha256=7d9af11fda72dfaa2961d8c4d5380ca0b51bc389dc5f8d4b859b9644f195e7a4
- nokogiri (1.19.2-x86_64-linux-gnu) sha256=fa8feca882b73e871a9845f3817a72e9734c8e974bdc4fbad6e4bc6e8076b94f
- nokogiri (1.19.2-x86_64-linux-musl) sha256=93128448e61a9383a30baef041bf1f5817e22f297a1d400521e90294445069a8
+ nokogiri (1.19.3-aarch64-linux-gnu) sha256=46b89e5d7b9e844c2ee360794240c6ea2a4e6fa0c5892a4ed487db621224b639
+ nokogiri (1.19.3-aarch64-linux-musl) sha256=8392dfdcd21be7a94dbbe9ccc138dea01b97b24cb2dc02a114ca98bfb1d9a0b7
+ nokogiri (1.19.3-arm-linux-gnu) sha256=3919d5ffc334ad778a4a9eb88fda7dcb8b1fb58c8a52ac640c6dcd2f038e774f
+ nokogiri (1.19.3-arm-linux-musl) sha256=9ce1cb6346bb9c67b1550eb537aa183ead91e4b6eadb2f36ade02d8dd2a79fb6
+ nokogiri (1.19.3-arm64-darwin) sha256=71b9bd424b1b7abc18b05052a1a3cfd3627abdca62be280854cc411791357e42
+ nokogiri (1.19.3-x86_64-darwin) sha256=77f3fba57d46c53ab31e62fc6c28f705109d1bf6264356c76f132b2be5728d4d
+ nokogiri (1.19.3-x86_64-linux-gnu) sha256=2f5078620fe12e83669b5b17311b32532a8153d02eee7ad06948b926d6080976
+ nokogiri (1.19.3-x86_64-linux-musl) sha256=248c906d2166eca5efb56d52fdee5f9a1f51d69a72e2b64fdac647b4ce39ea3f
opentelemetry-api (1.7.0) sha256=ccfd264ea6f2db5bf4185e3c07a1297977b44a944e2ce65457c4fe63a697214f
opentelemetry-common (0.23.0) sha256=da721190479d57bae0ad2207468f47f3e2c3b9a91024b5bc32c9d280183eb32c
opentelemetry-exporter-otlp (0.31.1) sha256=5358be17d7849cbcc4f49e1fc24105edc780a6f96c8e57b64192ab9a8e47474a
@@ -1050,10 +1050,10 @@ CHECKSUMS
puma (6.6.1) sha256=b9b56e4a4ea75d1bfa6d9e1972ee2c9f43d0883f011826d914e8e37b3694ea1e
raabro (1.4.0) sha256=d4fa9ff5172391edb92b242eed8be802d1934b1464061ae5e70d80962c5da882
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
- rack (3.2.5) sha256=4cbd0974c0b79f7a139b4812004a62e4c60b145cba76422e288ee670601ed6d3
+ rack (3.2.6) sha256=5ed78e1f73b2e25679bec7d45ee2d4483cc4146eb1be0264fc4d94cb5ef212c2
rack-mini-profiler (4.0.1) sha256=485810c23211f908196c896ea10cad72ed68780ee2998bec1f1dfd7558263d78
rack-proxy (0.7.7) sha256=446a4b57001022145d5c3ba73b775f66a2260eaf7420c6907483141900395c8a
- rack-session (2.1.1) sha256=0b6dc07dea7e4b583f58a48e8b806d4c9f1c6c9214ebc202ec94562cbea2e4e9
+ rack-session (2.1.2) sha256=595434f8c0c3473ae7d7ac56ecda6cc6dfd9d37c0b2b5255330aa1576967ffe8
rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463
rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868
rails (8.1.3) sha256=6d017ba5348c98fc909753a8169b21d44de14d2a0b92d140d1a966834c3c9cd3
From 22deee6a5d742e0f1ab3dffd292254699a2d98ba Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 15 May 2026 10:36:51 -0400
Subject: [PATCH 73/74] Bump actions/upload-artifact from 4 to 7 (#1485)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v7)
---
updated-dependencies:
- dependency-name: actions/upload-artifact
dependency-version: '7'
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
.github/workflows/ci.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e3dd3ed11..e57a1eeb1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -87,7 +87,7 @@ jobs:
CI: "true"
- name: Publish test results
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
if: always()
with:
name: rspec-results
From 4a65874eaa41a9380286c4d598a4572a44d216e9 Mon Sep 17 00:00:00 2001
From: maebeale
Date: Fri, 15 May 2026 10:39:33 -0400
Subject: [PATCH 74/74] Hide locked users' profiles from the non-admin people
index (#1489)
Add a where_user_not_locked Person scope and chain it onto the
non-admin relation_scope so that profiles whose user account is
locked are excluded. People with no user record are still included.
Co-authored-by: Claude Opus 4.7
---
app/models/person.rb | 5 +++++
app/policies/person_policy.rb | 2 +-
spec/policies/person_policy_spec.rb | 10 ++++++----
3 files changed, 12 insertions(+), 5 deletions(-)
diff --git a/app/models/person.rb b/app/models/person.rb
index 346dbe9bf..fd952a9fa 100644
--- a/app/models/person.rb
+++ b/app/models/person.rb
@@ -100,6 +100,11 @@ class Person < ApplicationRecord
.merge(Affiliation.active)
.distinct
}
+ scope :where_user_not_locked, -> {
+ left_joins(:user).where(users: { locked_at: nil }).or(
+ left_joins(:user).where(users: { id: nil })
+ )
+ }
scope :organization_name, ->(organization_name) {
return all if organization_name.blank?
left_joins(affiliations: :organization)
diff --git a/app/policies/person_policy.rb b/app/policies/person_policy.rb
index 5c1dafb0c..1f12418ba 100644
--- a/app/policies/person_policy.rb
+++ b/app/policies/person_policy.rb
@@ -38,7 +38,7 @@ def search?
relation_scope do |relation|
next relation if admin?
- relation.searchable.with_active_affiliations
+ relation.searchable.with_active_affiliations.where_user_not_locked
end
private
diff --git a/spec/policies/person_policy_spec.rb b/spec/policies/person_policy_spec.rb
index f95fcf550..77aedbf97 100644
--- a/spec/policies/person_policy_spec.rb
+++ b/spec/policies/person_policy_spec.rb
@@ -124,11 +124,13 @@ def policy_for(record: nil, user:)
context "with regular user" do
let(:policy) { policy_for(record: Person, user: regular_user) }
- it "filters to searchable people with active affiliations" do
+ it "filters to searchable people with active affiliations and unlocked users" do
scope = policy.apply_scope(Person.all, type: :active_record_relation)
- expect(scope.to_sql).to include('`people`.`profile_is_searchable` = TRUE')
- expect(scope.to_sql).to include('INNER JOIN `affiliations`')
- expect(scope.to_sql).to include('`affiliations`.`inactive` = FALSE')
+ sql = scope.to_sql
+ expect(sql).to include('`people`.`profile_is_searchable` = TRUE')
+ expect(sql).to include('INNER JOIN `affiliations`')
+ expect(sql).to include('`affiliations`.`inactive` = FALSE')
+ expect(sql).to include('`users`.`locked_at` IS NULL')
end
end
end