From fed1e22bd51dc8ae2ff2001405fb0eb47c14ce0d Mon Sep 17 00:00:00 2001 From: Manu Gurudath Date: Thu, 16 Jul 2026 15:34:12 -0400 Subject: [PATCH 01/20] add admin ui --- .../admin/legal_entities_controller.rb | 31 +++++++++ app/controllers/admin/payments_controller.rb | 22 +++++++ .../admin/payroll_positions_controller.rb | 46 +++++++++++++ app/controllers/admin/tax_forms_controller.rb | 22 +++++++ app/models/admin/nav.rb | 24 +++++++ app/models/payroll/position.rb | 3 + app/views/admin/legal_entities/index.html.erb | 64 +++++++++++++++++++ app/views/admin/payments/index.html.erb | 48 ++++++++++++++ .../admin/payroll_positions/index.html.erb | 52 +++++++++++++++ app/views/admin/tax_forms/index.html.erb | 56 ++++++++++++++++ app/views/payroll/positions/show.html.erb | 16 +++++ config/routes.rb | 7 ++ 12 files changed, 391 insertions(+) create mode 100644 app/controllers/admin/legal_entities_controller.rb create mode 100644 app/controllers/admin/payments_controller.rb create mode 100644 app/controllers/admin/payroll_positions_controller.rb create mode 100644 app/controllers/admin/tax_forms_controller.rb create mode 100644 app/views/admin/legal_entities/index.html.erb create mode 100644 app/views/admin/payments/index.html.erb create mode 100644 app/views/admin/payroll_positions/index.html.erb create mode 100644 app/views/admin/tax_forms/index.html.erb diff --git a/app/controllers/admin/legal_entities_controller.rb b/app/controllers/admin/legal_entities_controller.rb new file mode 100644 index 0000000000..6ff7e22879 --- /dev/null +++ b/app/controllers/admin/legal_entities_controller.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Admin + class LegalEntitiesController < Admin::BaseController + def index + @page = params[:page] || 1 + @per = params[:per] || 20 + + relation = LegalEntity.includes(:users, :managing_event, :latest_tax_form) + + @q = params[:q].presence + if @q + relation = relation.left_joins(:users).where("legal_entities.name ILIKE :q OR users.full_name ILIKE :q OR users.email ILIKE :q", q: "%#{LegalEntity.sanitize_sql_like(@q)}%").distinct + end + + @entity_type = params[:entity_type].presence + relation = relation.where(entity_type: @entity_type) if @entity_type + + case params[:filter] + when "managed" + relation = relation.managed + when "archived" + relation = relation.where.not(archived_at: nil) + end + + @count = relation.count + @legal_entities = relation.order(created_at: :desc).page(@page).per(@per) + end + + end +end diff --git a/app/controllers/admin/payments_controller.rb b/app/controllers/admin/payments_controller.rb new file mode 100644 index 0000000000..10cd767749 --- /dev/null +++ b/app/controllers/admin/payments_controller.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Admin + class PaymentsController < Admin::BaseController + def index + @page = params[:page] || 1 + @per = params[:per] || 20 + + relation = Payment.includes(:payee, :event) + + @q = params[:q].presence + relation = relation.search_recipient(@q) if @q + + @state = params[:state].presence + relation = relation.where(aasm_state: @state) if @state + + @count = relation.count + @payments = relation.order(created_at: :desc).page(@page).per(@per) + end + + end +end diff --git a/app/controllers/admin/payroll_positions_controller.rb b/app/controllers/admin/payroll_positions_controller.rb new file mode 100644 index 0000000000..1076f65686 --- /dev/null +++ b/app/controllers/admin/payroll_positions_controller.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Admin + class PayrollPositionsController < Admin::BaseController + def index + @page = params[:page] || 1 + @per = params[:per] || 20 + + relation = Payroll::Position.includes(:payee, :event) + + @q = params[:q].presence + relation = relation.search_recipient(@q) if @q + + @state = params[:state].presence + relation = relation.where(aasm_state: @state) if @state + + @count = relation.count + @positions = relation.order(Arel.sql("CASE WHEN aasm_state = 'under_review' THEN 0 ELSE 1 END, created_at DESC")).page(@page).per(@per) + end + + # Approval normally happens by signing the contract as HCB (see + # Payroll::Position#on_contract_party_signed) — this is a fallback for + # positions without a contract. + def approve + position = Payroll::Position.find(params[:id]) + + if position.contracts.not_voided.any? { |contract| contract.party(:hcb)&.pending? } + return redirect_back fallback_location: admin_payroll_positions_path, flash: { error: "This contractor has a contract awaiting HCB's signature — approve it by signing." } + end + + position.mark_onboarding! + redirect_back fallback_location: admin_payroll_positions_path, flash: { success: "Contractor approved — onboarding started." } + rescue AASM::InvalidTransition + redirect_back fallback_location: admin_payroll_positions_path, flash: { error: "This contractor is not awaiting review." } + end + + def reject + position = Payroll::Position.find(params[:id]) + position.mark_rejected! + redirect_back fallback_location: admin_payroll_positions_path, flash: { success: "Contractor rejected." } + rescue AASM::InvalidTransition + redirect_back fallback_location: admin_payroll_positions_path, flash: { error: "This contractor can no longer be rejected." } + end + + end +end diff --git a/app/controllers/admin/tax_forms_controller.rb b/app/controllers/admin/tax_forms_controller.rb new file mode 100644 index 0000000000..c4fea6f177 --- /dev/null +++ b/app/controllers/admin/tax_forms_controller.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Admin + class TaxFormsController < Admin::BaseController + def index + @page = params[:page] || 1 + @per = params[:per] || 20 + + relation = Tax::Form.includes(:legal_entity) + + @state = params[:state].presence + relation = relation.where(aasm_state: @state) if @state + + @form_type = params[:form_type].presence + relation = relation.where(form_type: @form_type) if @form_type + + @count = relation.count + @tax_forms = relation.order(created_at: :desc).page(@page).per(@per) + end + + end +end diff --git a/app/models/admin/nav.rb b/app/models/admin/nav.rb index 4889cc8b45..6b75fc0739 100644 --- a/app/models/admin/nav.rb +++ b/app/models/admin/nav.rb @@ -301,6 +301,30 @@ def payroll Section.new( name: "Payroll", items: [ + make_item( + name: "Contractors", + path: admin_payroll_positions_path, + count: ->{ Payroll::Position.under_review.count }, + count_type: :tasks + ), + make_item( + name: "Contractor Payments", + path: admin_payments_path, + count: ->{ Payment.under_review.count }, + count_type: :tasks + ), + make_item( + name: "Legal Entities", + path: admin_legal_entities_path, + count: ->{ LegalEntity.count }, + count_type: :records + ), + make_item( + name: "Tax Forms", + path: admin_tax_forms_path, + count: ->{ Tax::Form.count }, + count_type: :records + ), make_item( name: "Employees", path: employees_admin_index_path, diff --git a/app/models/payroll/position.rb b/app/models/payroll/position.rb index 8540b2077e..d6b08c5127 100644 --- a/app/models/payroll/position.rb +++ b/app/models/payroll/position.rb @@ -168,6 +168,9 @@ def on_contract_signed(contract) end def on_contract_party_signed(party) + # HCB ops review the contract by signing it: HCB's signature is what + # moves the position out of review and into onboarding. + mark_onboarding! if party.hcb? && may_mark_onboarding? refresh_onboarding_state! end diff --git a/app/views/admin/legal_entities/index.html.erb b/app/views/admin/legal_entities/index.html.erb new file mode 100644 index 0000000000..59b5dabab5 --- /dev/null +++ b/app/views/admin/legal_entities/index.html.erb @@ -0,0 +1,64 @@ +<% title "Legal Entities" %> + +<%= form_with local: true, class: "card overflow-visible flex items-center", url: admin_legal_entities_path, method: :get do |form| %> + <%= form.text_field :q, value: @q, placeholder: "Search by name or user email", class: "flex-grow" %> + <%= form.select :entity_type, + options_for_select([%w[Person person], %w[Business business]], @entity_type), + { include_blank: "All types" } %> + <%= form.select :filter, + options_for_select([%w[Managed managed], %w[Archived archived]], params[:filter]), + { include_blank: "All entities" } %> + <%= form.submit "Filter" %> +<% end %> + +
+
+ <%= page_entries_info @legal_entities, entry_name: "legal entities" %> +
+ <%= paginate @legal_entities %> +
+ + + + + + + + + <%# presence only — masked TINs are payee-visible, not admin-visible %> + + + + + + <% @legal_entities.each do |legal_entity| %> + + + + + + + + + + <% end %> + +
DateNameTypeUsers / Managing orgTINPayable?Actions
<%= legal_entity.created_at.strftime("%Y-%m-%d") %> + <%= legal_entity.name || legal_entity.display_name %> + <% if legal_entity.archived? %>Archived<% end %> + <% if legal_entity.tin_banned? %>Banned<% end %> + <%= legal_entity.entity_type&.humanize %> + <% if legal_entity.managed? %> + Managed by <%= link_to legal_entity.managing_event.name, legal_entity.managing_event %> + <% else %> + <%= legal_entity.users.map(&:email).to_sentence.presence || "—" %> + <% end %> + <%= legal_entity.tin_hash.present? ? "On file" : "—" %> + <% if legal_entity.payable? %> + Yes + <% else %> + No + <% end %> + <%= link_to "View", legal_entity_path(legal_entity) %>
+ +<%= paginate @legal_entities %> diff --git a/app/views/admin/payments/index.html.erb b/app/views/admin/payments/index.html.erb new file mode 100644 index 0000000000..16dd13778d --- /dev/null +++ b/app/views/admin/payments/index.html.erb @@ -0,0 +1,48 @@ +<% title "Contractor Payments" %> + +<%= form_with local: true, class: "card overflow-visible flex items-center", url: admin_payments_path, method: :get do |form| %> + <%= form.text_field :q, value: @q, placeholder: "Search by recipient name or email", class: "flex-grow" %> + <%= form.select :state, + options_for_select(Payment.aasm.states.map { |s| [s.human_name, s.name.to_s] }, @state), + { include_blank: "All states" } %> + <%= form.submit "Filter" %> +<% end %> + +
+
+ <%= page_entries_info @payments, entry_name: "payments" %> +
+ <%= paginate @payments %> +
+ + + + + + + + + + + + + + + <% @payments.each do |payment| %> + "> + + + + + + + + + <% end %> + +
DateOrganizationRecipientPurposeAmountStatusActions
<%= payment.created_at.strftime("%Y-%m-%d") %><%= link_to payment.event.name, payment.event %> + <%= payment.payee.display_name %> +
<%= payment.payee.email %>
+
<%= payment.purpose %><%= payment.amount.format %><%= payment.state_text %><%= link_to "View", payment_path(payment) %>
+ +<%= paginate @payments %> diff --git a/app/views/admin/payroll_positions/index.html.erb b/app/views/admin/payroll_positions/index.html.erb new file mode 100644 index 0000000000..bd66c78930 --- /dev/null +++ b/app/views/admin/payroll_positions/index.html.erb @@ -0,0 +1,52 @@ +<% title "Contractors" %> + +<%= form_with local: true, class: "card overflow-visible flex items-center", url: admin_payroll_positions_path, method: :get do |form| %> + <%= form.text_field :q, value: @q, placeholder: "Search by contractor name or email", class: "flex-grow" %> + <%= form.select :state, + options_for_select(Payroll::Position.aasm.states.map { |s| [s.human_name, s.name.to_s] }, @state), + { include_blank: "All states" } %> + <%= form.submit "Filter" %> +<% end %> + +
+
+ <%= page_entries_info @positions, entry_name: "contractors" %> +
+ <%= paginate @positions %> +
+ + + + + + + + + + + + + + + + <% @positions.each do |position| %> + "> + + + + + + + + + + <% end %> + +
DateOrganizationContractorTitleRatePeriodStatusActions
<%= position.created_at.strftime("%Y-%m-%d") %><%= link_to position.event.name, position.event %> + <%= position.payee.display_name %> +
<%= position.payee.email %>
+
<%= position.title %><%= position.rate.format %><%= position.period_label %>"><%= position.under_review? ? "Under review" : position.status_text %> + <%= link_to position.under_review? ? "Review" : "View", event_payroll_position_path(event_id: position.event.slug, id: position.hashid), data: { turbo: false } %> +
+ +<%= paginate @positions %> diff --git a/app/views/admin/tax_forms/index.html.erb b/app/views/admin/tax_forms/index.html.erb new file mode 100644 index 0000000000..0ffb05bf8f --- /dev/null +++ b/app/views/admin/tax_forms/index.html.erb @@ -0,0 +1,56 @@ +<% title "Tax Forms" %> + +<%= form_with local: true, class: "card overflow-visible flex items-center", url: admin_tax_forms_path, method: :get do |form| %> + <%= form.select :state, + options_for_select(Tax::Form.aasm.states.map { |s| [s.human_name, s.name.to_s] }, @state), + { include_blank: "All states" } %> + <%= form.select :form_type, + options_for_select(Tax::Form.form_types.keys, @form_type), + { include_blank: "All form types" } %> + <%= form.submit "Filter" %> +<% end %> + +
+
+ <%= page_entries_info @tax_forms, entry_name: "tax forms" %> +
+ <%= paginate @tax_forms %> +
+ + + + + + + + + + + + + + + + + <% @tax_forms.each do |form| %> + + + + + + + + + + + + <% end %> + +
DateLegal entityFormServiceStatusTaxBandits statusTIN matchCompletedActions
<%= form.created_at.strftime("%Y-%m-%d") %><%= link_to form.legal_entity.name || form.legal_entity.display_name, legal_entity_path(form.legal_entity) %><%= form.form_type || "—" %><%= form.external_service.humanize %><%= form.aasm_state.humanize %><%= form.taxbandits_status&.humanize || "—" %><%= form.taxbandits_tin_matching_status&.humanize || "—" %><%= form.completed_at&.strftime("%Y-%m-%d") || "—" %> + <%= link_to "View", tax_form_path(form) %> + <% if form.sent_with_taxbandits? && !form.discarded? %> + <%= link_to "Sync", sync_tax_form_path(form), data: { turbo_method: :post } %> + <% end %> +
+ +<%= paginate @tax_forms %> diff --git a/app/views/payroll/positions/show.html.erb b/app/views/payroll/positions/show.html.erb index a79a0aeeae..4331ca0869 100644 --- a/app/views/payroll/positions/show.html.erb +++ b/app/views/payroll/positions/show.html.erb @@ -50,6 +50,22 @@ + <% if @position.under_review? %> + <% hcb_party = @position.contracts.not_voided.order(created_at: :desc).first&.party(:hcb) %> + <% admin_tool("mt-4 flex items-center justify-between") do %> + This contract is awaiting HCB review. It's approved by signing it as HCB. + + <% if hcb_party&.pending? %> + <%= link_to "Review & sign contract", contract_party_path(hcb_party), class: "btn bg-success", data: { turbo_frame: "_top" } %> + <% else %> + <%# No contract has been issued (or HCB already signed) — allow a manual approval. %> + <%= button_to "Approve without contract", approve_admin_payroll_position_path(@position), class: "btn bg-success", data: { turbo_frame: "_top", turbo_confirm: "No contract to sign — approve this contractor anyway?" } %> + <% end %> + <%= button_to "Reject", reject_admin_payroll_position_path(@position), class: "btn bg-muted", data: { turbo_frame: "_top", turbo_confirm: "Reject this contractor?" } %> + + <% end %> + <% end %> + <% if @position.status == :onboarding %> <%= render "application/callout", type: "info", title: "Onboarding in progress", class: "mt-4" do %> <%= @position.payee.display_name %> needs to sign their contract and complete W-9/W-8BEN tax forms before payments can be processed. diff --git a/config/routes.rb b/config/routes.rb index 71e9b7485f..d0cf4ce17a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -354,6 +354,13 @@ post "submit", on: :member post "reject", on: :member end + resources :payments, only: [:index] + resources :payroll_positions, only: [:index] do + post "approve", on: :member + post "reject", on: :member + end + resources :legal_entities, only: [:index] + resources :tax_forms, only: [:index] resources :column_statements, only: :index do get "bank_account_summary_report" end From 70fcbf258f156718d8e1ed31da0a4f2cf4e4c251 Mon Sep 17 00:00:00 2001 From: Manu Gurudath Date: Thu, 16 Jul 2026 16:36:46 -0400 Subject: [PATCH 02/20] fix bugs --- .../admin/payroll_positions_controller.rb | 19 ++--------- .../contract/parties_controller.rb | 10 ++++++ app/models/contract/payroll_position.rb | 2 +- app/models/payroll/position.rb | 32 ++++++++++++++----- app/views/payroll/positions/show.html.erb | 19 ++++++----- config/routes.rb | 1 - db/schema.rb | 12 +++++++ 7 files changed, 59 insertions(+), 36 deletions(-) diff --git a/app/controllers/admin/payroll_positions_controller.rb b/app/controllers/admin/payroll_positions_controller.rb index 1076f65686..50fb942a55 100644 --- a/app/controllers/admin/payroll_positions_controller.rb +++ b/app/controllers/admin/payroll_positions_controller.rb @@ -18,22 +18,9 @@ def index @positions = relation.order(Arel.sql("CASE WHEN aasm_state = 'under_review' THEN 0 ELSE 1 END, created_at DESC")).page(@page).per(@per) end - # Approval normally happens by signing the contract as HCB (see - # Payroll::Position#on_contract_party_signed) — this is a fallback for - # positions without a contract. - def approve - position = Payroll::Position.find(params[:id]) - - if position.contracts.not_voided.any? { |contract| contract.party(:hcb)&.pending? } - return redirect_back fallback_location: admin_payroll_positions_path, flash: { error: "This contractor has a contract awaiting HCB's signature — approve it by signing." } - end - - position.mark_onboarding! - redirect_back fallback_location: admin_payroll_positions_path, flash: { success: "Contractor approved — onboarding started." } - rescue AASM::InvalidTransition - redirect_back fallback_location: admin_payroll_positions_path, flash: { error: "This contractor is not awaiting review." } - end - + # There is no "approve" action here: HCB approves a contractor by signing + # their contract as HCB (see Payroll::Position#on_contract_party_signed), + # from the contract's own signing page. def reject position = Payroll::Position.find(params[:id]) position.mark_rejected! diff --git a/app/controllers/contract/parties_controller.rb b/app/controllers/contract/parties_controller.rb index 55a065370f..0749935677 100644 --- a/app/controllers/contract/parties_controller.rb +++ b/app/controllers/contract/parties_controller.rb @@ -17,6 +17,11 @@ def show end end + # The DocuSeal webhook may not have landed yet (or, in dev, may never + # land at all — it can't reach localhost) by the time this page loads, + # so reconcile directly with DocuSeal rather than relying on it alone. + @party.sync_with_docuseal if @party.pending? && @contract.sent_with_docuseal? + if @party.signed? && !(@contract.contractable.is_a?(Event::Application) && @party.hcb?) redirect_to completed_contract_party_path(@party) return @@ -42,6 +47,11 @@ def resend def completed authorize @party + # This is where DocuSeal's embedded form redirects to right after the + # signature completes, so this is the most important place to catch up + # — the webhook is often still in flight when the browser lands here. + @party.sync_with_docuseal if @party.pending? && @contract.sent_with_docuseal? + if (@party.signee? && @contract.signed?) || @party.contractor? case @contract.contractable when Event::Application diff --git a/app/models/contract/payroll_position.rb b/app/models/contract/payroll_position.rb index dbe4e4c07f..51e9df7c85 100644 --- a/app/models/contract/payroll_position.rb +++ b/app/models/contract/payroll_position.rb @@ -36,7 +36,7 @@ class Contract class PayrollPosition < Contract - DOCUSEAL_TEMPLATE_ID = 4983902 + DOCUSEAL_TEMPLATE_ID = 5023480 after_update_commit :create_document!, if: -> { event.present? && sent_with_docuseal? && aasm_state_previously_changed?(to: "signed") } diff --git a/app/models/payroll/position.rb b/app/models/payroll/position.rb index 59dd746104..5a0ded770e 100644 --- a/app/models/payroll/position.rb +++ b/app/models/payroll/position.rb @@ -174,21 +174,27 @@ def on_contract_signed(contract) end def on_contract_party_signed(party) + # The generic "notify HCB once every other party has signed" logic + # (Contract#on_party_signed) never fires for us: the contractor is only + # invited to sign *after* HCB does, so `parties.not_hcb.all?(&:signed?)` + # can never be true before HCB's turn. Notify HCB ourselves as soon as + # the organizer — the only party required to sign before HCB — has. + if party.organizer? + hcb_party = party.contract.party(:hcb) + notify_hcb_of_review(hcb_party) if hcb_party.present? && !hcb_party.signed? + end + # HCB ops review the contract by signing it: HCB's signature is what # moves the position out of review and into onboarding. - mark_onboarding! if party.hcb? && may_mark_onboarding? - refresh_onboarding_state! - - # The contractor is only invited to sign once HCB has signed if party.hcb? - # HCB signing is the "reviewed by HCB operations" step, so begin onboarding. mark_onboarding! if may_mark_onboarding? + # The contractor is only invited to sign once HCB has signed contractor = party.contract.party(:contractor) - return if contractor.nil? || contractor.signed? - - notify_contractor_of_onboarding(contractor) + notify_contractor_of_onboarding(contractor) if contractor.present? && !contractor.signed? end + + refresh_onboarding_state! end def send_contract(organizer_user: nil, cosigner_email: nil, reissue_messages: {}, reissue_of: nil, **options) @@ -268,6 +274,16 @@ def notify_contractor_of_onboarding(contractor) Rails.error.report(e, context: { payroll_position_id: id }) end + # Same best-effort reasoning as notify_contractor_of_onboarding: this runs + # from inside the party's own signing transaction, so a mailer/job failure + # here must not roll back the organizer's signature. + def notify_hcb_of_review(hcb_party) + hcb_party.notify + hcb_party.schedule_reminders + rescue => e + Rails.error.report(e, context: { payroll_position_id: id }) + end + def contract_signed_by?(role) contracts.not_voided.any? { |contract| contract.party(role)&.signed? } end diff --git a/app/views/payroll/positions/show.html.erb b/app/views/payroll/positions/show.html.erb index 277a5f2966..dfd42b7187 100644 --- a/app/views/payroll/positions/show.html.erb +++ b/app/views/payroll/positions/show.html.erb @@ -51,18 +51,17 @@ <% if @position.under_review? %> - <% hcb_party = @position.contracts.not_voided.order(created_at: :desc).first&.party(:hcb) %> <% admin_tool("mt-4 flex items-center justify-between") do %> - This contract is awaiting HCB review. It's approved by signing it as HCB. - - <% if hcb_party&.pending? %> - <%= link_to "Review & sign contract", contract_party_path(hcb_party), class: "btn bg-success", data: { turbo_frame: "_top" } %> - <% else %> - <%# No contract has been issued (or HCB already signed) — allow a manual approval. %> - <%= button_to "Approve without contract", approve_admin_payroll_position_path(@position), class: "btn bg-success", data: { turbo_frame: "_top", turbo_confirm: "No contract to sign — approve this contractor anyway?" } %> - <% end %> + <% if @position.contract.present? %> + This contract is awaiting HCB review. Reviewing it means signing it as HCB. + + <%= link_to "Review & sign contract", contract_party_path(@position.contract.party(:hcb)), class: "btn bg-success", data: { turbo_frame: "_top" } %> + <%= button_to "Reject", reject_admin_payroll_position_path(@position), class: "btn bg-muted", data: { turbo_frame: "_top", turbo_confirm: "Reject this contractor?" } %> + + <% else %> + No contract has been sent for this contractor yet — the organizer needs to retry sending it from the edit page before HCB can review it. <%= button_to "Reject", reject_admin_payroll_position_path(@position), class: "btn bg-muted", data: { turbo_frame: "_top", turbo_confirm: "Reject this contractor?" } %> - + <% end %> <% end %> <% end %> diff --git a/config/routes.rb b/config/routes.rb index 5e5e9b5fff..fd46af3f33 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -359,7 +359,6 @@ end resources :payments, only: [:index] resources :payroll_positions, only: [:index] do - post "approve", on: :member post "reject", on: :member end resources :legal_entities, only: [:index] diff --git a/db/schema.rb b/db/schema.rb index b9f9cc6e72..dadddc8f99 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -2025,6 +2025,17 @@ t.index ["user_id"], name: "index_paypal_transfers_on_user_id" end + create_table "payroll_contracts", force: :cascade do |t| + t.datetime "created_at", null: false + t.date "ends_on", null: false + t.integer "hourly_rate_cents", null: false + t.bigint "payee_id", null: false + t.string "purpose", null: false + t.date "starts_on", null: false + t.datetime "updated_at", null: false + t.index ["payee_id"], name: "index_payroll_contracts_on_payee_id" + end + create_table "payroll_invoices", force: :cascade do |t| t.string "aasm_state", null: false t.integer "amount_cents", null: false @@ -3153,6 +3164,7 @@ add_foreign_key "payment_recipients", "events" add_foreign_key "paypal_transfers", "events" add_foreign_key "paypal_transfers", "users" + add_foreign_key "payroll_contracts", "payees" add_foreign_key "payroll_invoices", "payments" add_foreign_key "payroll_invoices", "payroll_positions" add_foreign_key "payroll_invoices", "users", column: "reviewed_by_id" From c8dafed864fde47aeb6b92522f2bee58649d40cd Mon Sep 17 00:00:00 2001 From: Manu Gurudath Date: Thu, 16 Jul 2026 16:46:15 -0400 Subject: [PATCH 03/20] fix --- app/models/admin/nav.rb | 44 ++++++++++++++---------- app/views/admin/tax_forms/index.html.erb | 9 ++--- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/app/models/admin/nav.rb b/app/models/admin/nav.rb index 6b75fc0739..d93bfdee34 100644 --- a/app/models/admin/nav.rb +++ b/app/models/admin/nav.rb @@ -325,28 +325,36 @@ def payroll count: ->{ Tax::Form.count }, count_type: :records ), - make_item( - name: "Employees", - path: employees_admin_index_path, - count: ->{ Employee.onboarding.count }, - count_type: :tasks - ), - make_item( - name: "Payments", - path: employee_payments_admin_index_path, - count: ->{ Employee::Payment.paid.count }, - count_type: :records - ), - make_item( - name: "W9s", - path: admin_w9s_path, - count: ->{ W9.count }, - count_type: :records - ) + *legacy_payroll_items ] ) end + def legacy_payroll_items + return [] unless Flipper.enabled?(:payments_contractors_refresh_2026_06_26) + + [ + make_item( + name: "Employees", + path: employees_admin_index_path, + count: ->{ Employee.onboarding.count }, + count_type: :tasks + ), + make_item( + name: "Payments", + path: employee_payments_admin_index_path, + count: ->{ Employee::Payment.paid.count }, + count_type: :records + ), + make_item( + name: "W9s", + path: admin_w9s_path, + count: ->{ W9.count }, + count_type: :records + ) + ] + end + def misc Section.new( name: "Misc", diff --git a/app/views/admin/tax_forms/index.html.erb b/app/views/admin/tax_forms/index.html.erb index 0ffb05bf8f..e152252b17 100644 --- a/app/views/admin/tax_forms/index.html.erb +++ b/app/views/admin/tax_forms/index.html.erb @@ -21,6 +21,7 @@ Date + ID Legal entity Form Service @@ -28,13 +29,13 @@ TaxBandits status TIN match Completed - Actions <% @tax_forms.each do |form| %> <%= form.created_at.strftime("%Y-%m-%d") %> + <%= form.hashid %> <%= link_to form.legal_entity.name || form.legal_entity.display_name, legal_entity_path(form.legal_entity) %> <%= form.form_type || "—" %> <%= form.external_service.humanize %> @@ -42,12 +43,6 @@ <%= form.taxbandits_status&.humanize || "—" %> <%= form.taxbandits_tin_matching_status&.humanize || "—" %> <%= form.completed_at&.strftime("%Y-%m-%d") || "—" %> - - <%= link_to "View", tax_form_path(form) %> - <% if form.sent_with_taxbandits? && !form.discarded? %> - <%= link_to "Sync", sync_tax_form_path(form), data: { turbo_method: :post } %> - <% end %> - <% end %> From f81a3c45ea22a9162c40e11fbc93232e2fadfd37 Mon Sep 17 00:00:00 2001 From: Manu Gurudath Date: Thu, 16 Jul 2026 16:50:11 -0400 Subject: [PATCH 04/20] undo --- db/schema.rb | 34 +--------------------------------- 1 file changed, 1 insertion(+), 33 deletions(-) diff --git a/db/schema.rb b/db/schema.rb index dadddc8f99..edc67682ee 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -12,7 +12,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2026_07_16_000000) do +ActiveRecord::Schema[8.0].define(version: 2026_07_14_120100) do create_schema "google_sheets" # These are extensions that must be enabled in order to support this database @@ -395,7 +395,6 @@ t.bigint "raw_pending_bank_fee_transaction_id" t.bigint "raw_pending_column_transaction_id" t.bigint "raw_pending_donation_transaction_id" - t.bigint "raw_pending_fee_revenue_transaction_id" t.bigint "raw_pending_incoming_disbursement_transaction_id" t.bigint "raw_pending_invoice_transaction_id" t.bigint "raw_pending_outgoing_ach_transaction_id" @@ -416,7 +415,6 @@ t.index ["raw_pending_column_transaction_id"], name: "idx_on_raw_pending_column_transaction_id_ceea9a99e1", unique: true t.index ["raw_pending_column_transaction_id"], name: "index_canonical_pending_txs_on_rpct_id" t.index ["raw_pending_donation_transaction_id"], name: "index_canonical_pending_txs_on_raw_pending_donation_tx_id" - t.index ["raw_pending_fee_revenue_transaction_id"], name: "index_canonical_pending_txs_on_raw_pending_fee_revenue_tx_id" t.index ["raw_pending_incoming_disbursement_transaction_id"], name: "index_cpts_on_raw_pending_incoming_disbursement_transaction_id" t.index ["raw_pending_invoice_transaction_id"], name: "index_canonical_pending_txs_on_raw_pending_invoice_tx_id" t.index ["raw_pending_outgoing_ach_transaction_id"], name: "index_canonical_pending_txs_on_raw_pending_outgoing_ach_tx_id" @@ -1420,22 +1418,17 @@ end create_table "hcb_codes", force: :cascade do |t| - t.datetime "card_charge_settled_at" t.datetime "created_at", null: false t.bigint "event_id" t.text "hcb_code", null: false t.bigint "ledger_item_id" t.datetime "marked_no_or_lost_receipt_at", precision: nil - t.datetime "receipt_due_at" - t.datetime "receipt_resolved_at" t.text "short_code" t.bigint "subledger_id" t.datetime "updated_at", null: false - t.index ["card_charge_settled_at"], name: "index_hcb_codes_on_card_charge_settled_at" t.index ["event_id"], name: "index_hcb_codes_on_event_id" t.index ["hcb_code"], name: "index_hcb_codes_on_hcb_code", unique: true t.index ["ledger_item_id"], name: "index_hcb_codes_on_ledger_item_id" - t.index ["receipt_due_at"], name: "index_hcb_codes_on_open_receipt_due_at", where: "((receipt_due_at IS NOT NULL) AND (receipt_resolved_at IS NULL))" t.index ["short_code"], name: "index_hcb_codes_on_short_code", unique: true t.check_constraint "short_code = upper(short_code)", name: "constraint_hcb_codes_on_short_code_to_uppercase" end @@ -1622,7 +1615,6 @@ t.index ["id"], name: "index_ledger_items_on_receipt_missing", where: "(receipt_required AND (marked_no_or_lost_receipt_at IS NULL) AND (receipt_count = 0))" t.index ["linked_object_type", "linked_object_id"], name: "index_ledger_items_on_linked_object" t.index ["short_code"], name: "index_ledger_items_on_short_code", unique: true - t.index ["status"], name: "index_ledger_items_on_status" end create_table "ledger_mappings", force: :cascade do |t| @@ -2025,17 +2017,6 @@ t.index ["user_id"], name: "index_paypal_transfers_on_user_id" end - create_table "payroll_contracts", force: :cascade do |t| - t.datetime "created_at", null: false - t.date "ends_on", null: false - t.integer "hourly_rate_cents", null: false - t.bigint "payee_id", null: false - t.string "purpose", null: false - t.date "starts_on", null: false - t.datetime "updated_at", null: false - t.index ["payee_id"], name: "index_payroll_contracts_on_payee_id" - end - create_table "payroll_invoices", force: :cascade do |t| t.string "aasm_state", null: false t.integer "amount_cents", null: false @@ -2172,15 +2153,6 @@ t.datetime "updated_at", null: false end - create_table "raw_pending_fee_revenue_transactions", force: :cascade do |t| - t.integer "amount_cents" - t.datetime "created_at", null: false - t.date "date_posted" - t.bigint "fee_revenue_id", null: false - t.datetime "updated_at", null: false - t.index ["fee_revenue_id"], name: "index_raw_pending_fee_revenue_transactions_on_fee_revenue_id" - end - create_table "raw_pending_incoming_disbursement_transactions", force: :cascade do |t| t.integer "amount_cents" t.datetime "created_at", null: false @@ -2264,7 +2236,6 @@ t.string "unique_bank_identifier", null: false t.datetime "updated_at", null: false t.index "(((stripe_transaction -> 'card'::text) ->> 'id'::text))", name: "index_raw_stripe_transactions_on_card_id_text", using: :hash - t.index "((stripe_transaction ->> 'card'::text))", name: "index_raw_stripe_transactions_on_card" t.index ["stripe_authorization_id"], name: "index_raw_stripe_transactions_on_stripe_authorization_id" end @@ -2875,7 +2846,6 @@ create_table "users", force: :cascade do |t| t.integer "access_level", default: 0, null: false t.text "birthday_ciphertext" - t.datetime "card_locking_suppressed_until" t.boolean "cards_locked", default: false, null: false t.integer "charge_notifications", default: 0, null: false t.integer "comment_notifications", default: 0, null: false @@ -3164,14 +3134,12 @@ add_foreign_key "payment_recipients", "events" add_foreign_key "paypal_transfers", "events" add_foreign_key "paypal_transfers", "users" - add_foreign_key "payroll_contracts", "payees" add_foreign_key "payroll_invoices", "payments" add_foreign_key "payroll_invoices", "payroll_positions" add_foreign_key "payroll_invoices", "users", column: "reviewed_by_id" add_foreign_key "payroll_positions", "payees" add_foreign_key "raffles", "raffles", column: "referring_raffle_id", validate: false add_foreign_key "raffles", "users" - add_foreign_key "raw_pending_fee_revenue_transactions", "fee_revenues" add_foreign_key "raw_pending_incoming_disbursement_transactions", "disbursements" add_foreign_key "raw_pending_outgoing_disbursement_transactions", "disbursements" add_foreign_key "receipts", "users" From 051a3e1da6dcc7d2d1ce9e2fecce4b04f27ce5ff Mon Sep 17 00:00:00 2001 From: Manu Gurudath Date: Thu, 16 Jul 2026 16:50:43 -0400 Subject: [PATCH 05/20] undo --- db/schema.rb | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/db/schema.rb b/db/schema.rb index edc67682ee..b9f9cc6e72 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -12,7 +12,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2026_07_14_120100) do +ActiveRecord::Schema[8.0].define(version: 2026_07_16_000000) do create_schema "google_sheets" # These are extensions that must be enabled in order to support this database @@ -395,6 +395,7 @@ t.bigint "raw_pending_bank_fee_transaction_id" t.bigint "raw_pending_column_transaction_id" t.bigint "raw_pending_donation_transaction_id" + t.bigint "raw_pending_fee_revenue_transaction_id" t.bigint "raw_pending_incoming_disbursement_transaction_id" t.bigint "raw_pending_invoice_transaction_id" t.bigint "raw_pending_outgoing_ach_transaction_id" @@ -415,6 +416,7 @@ t.index ["raw_pending_column_transaction_id"], name: "idx_on_raw_pending_column_transaction_id_ceea9a99e1", unique: true t.index ["raw_pending_column_transaction_id"], name: "index_canonical_pending_txs_on_rpct_id" t.index ["raw_pending_donation_transaction_id"], name: "index_canonical_pending_txs_on_raw_pending_donation_tx_id" + t.index ["raw_pending_fee_revenue_transaction_id"], name: "index_canonical_pending_txs_on_raw_pending_fee_revenue_tx_id" t.index ["raw_pending_incoming_disbursement_transaction_id"], name: "index_cpts_on_raw_pending_incoming_disbursement_transaction_id" t.index ["raw_pending_invoice_transaction_id"], name: "index_canonical_pending_txs_on_raw_pending_invoice_tx_id" t.index ["raw_pending_outgoing_ach_transaction_id"], name: "index_canonical_pending_txs_on_raw_pending_outgoing_ach_tx_id" @@ -1418,17 +1420,22 @@ end create_table "hcb_codes", force: :cascade do |t| + t.datetime "card_charge_settled_at" t.datetime "created_at", null: false t.bigint "event_id" t.text "hcb_code", null: false t.bigint "ledger_item_id" t.datetime "marked_no_or_lost_receipt_at", precision: nil + t.datetime "receipt_due_at" + t.datetime "receipt_resolved_at" t.text "short_code" t.bigint "subledger_id" t.datetime "updated_at", null: false + t.index ["card_charge_settled_at"], name: "index_hcb_codes_on_card_charge_settled_at" t.index ["event_id"], name: "index_hcb_codes_on_event_id" t.index ["hcb_code"], name: "index_hcb_codes_on_hcb_code", unique: true t.index ["ledger_item_id"], name: "index_hcb_codes_on_ledger_item_id" + t.index ["receipt_due_at"], name: "index_hcb_codes_on_open_receipt_due_at", where: "((receipt_due_at IS NOT NULL) AND (receipt_resolved_at IS NULL))" t.index ["short_code"], name: "index_hcb_codes_on_short_code", unique: true t.check_constraint "short_code = upper(short_code)", name: "constraint_hcb_codes_on_short_code_to_uppercase" end @@ -1615,6 +1622,7 @@ t.index ["id"], name: "index_ledger_items_on_receipt_missing", where: "(receipt_required AND (marked_no_or_lost_receipt_at IS NULL) AND (receipt_count = 0))" t.index ["linked_object_type", "linked_object_id"], name: "index_ledger_items_on_linked_object" t.index ["short_code"], name: "index_ledger_items_on_short_code", unique: true + t.index ["status"], name: "index_ledger_items_on_status" end create_table "ledger_mappings", force: :cascade do |t| @@ -2153,6 +2161,15 @@ t.datetime "updated_at", null: false end + create_table "raw_pending_fee_revenue_transactions", force: :cascade do |t| + t.integer "amount_cents" + t.datetime "created_at", null: false + t.date "date_posted" + t.bigint "fee_revenue_id", null: false + t.datetime "updated_at", null: false + t.index ["fee_revenue_id"], name: "index_raw_pending_fee_revenue_transactions_on_fee_revenue_id" + end + create_table "raw_pending_incoming_disbursement_transactions", force: :cascade do |t| t.integer "amount_cents" t.datetime "created_at", null: false @@ -2236,6 +2253,7 @@ t.string "unique_bank_identifier", null: false t.datetime "updated_at", null: false t.index "(((stripe_transaction -> 'card'::text) ->> 'id'::text))", name: "index_raw_stripe_transactions_on_card_id_text", using: :hash + t.index "((stripe_transaction ->> 'card'::text))", name: "index_raw_stripe_transactions_on_card" t.index ["stripe_authorization_id"], name: "index_raw_stripe_transactions_on_stripe_authorization_id" end @@ -2846,6 +2864,7 @@ create_table "users", force: :cascade do |t| t.integer "access_level", default: 0, null: false t.text "birthday_ciphertext" + t.datetime "card_locking_suppressed_until" t.boolean "cards_locked", default: false, null: false t.integer "charge_notifications", default: 0, null: false t.integer "comment_notifications", default: 0, null: false @@ -3140,6 +3159,7 @@ add_foreign_key "payroll_positions", "payees" add_foreign_key "raffles", "raffles", column: "referring_raffle_id", validate: false add_foreign_key "raffles", "users" + add_foreign_key "raw_pending_fee_revenue_transactions", "fee_revenues" add_foreign_key "raw_pending_incoming_disbursement_transactions", "disbursements" add_foreign_key "raw_pending_outgoing_disbursement_transactions", "disbursements" add_foreign_key "receipts", "users" From 949a9e468db78f006d4406170b844503c0a8e7c6 Mon Sep 17 00:00:00 2001 From: Manu Gurudath Date: Thu, 16 Jul 2026 16:51:54 -0400 Subject: [PATCH 06/20] remove comments --- app/controllers/contract/parties_controller.rb | 6 ------ app/models/payroll/position.rb | 11 ----------- 2 files changed, 17 deletions(-) diff --git a/app/controllers/contract/parties_controller.rb b/app/controllers/contract/parties_controller.rb index 0749935677..7e4c38934f 100644 --- a/app/controllers/contract/parties_controller.rb +++ b/app/controllers/contract/parties_controller.rb @@ -17,9 +17,6 @@ def show end end - # The DocuSeal webhook may not have landed yet (or, in dev, may never - # land at all — it can't reach localhost) by the time this page loads, - # so reconcile directly with DocuSeal rather than relying on it alone. @party.sync_with_docuseal if @party.pending? && @contract.sent_with_docuseal? if @party.signed? && !(@contract.contractable.is_a?(Event::Application) && @party.hcb?) @@ -47,9 +44,6 @@ def resend def completed authorize @party - # This is where DocuSeal's embedded form redirects to right after the - # signature completes, so this is the most important place to catch up - # — the webhook is often still in flight when the browser lands here. @party.sync_with_docuseal if @party.pending? && @contract.sent_with_docuseal? if (@party.signee? && @contract.signed?) || @party.contractor? diff --git a/app/models/payroll/position.rb b/app/models/payroll/position.rb index 5a0ded770e..a28f715985 100644 --- a/app/models/payroll/position.rb +++ b/app/models/payroll/position.rb @@ -174,22 +174,14 @@ def on_contract_signed(contract) end def on_contract_party_signed(party) - # The generic "notify HCB once every other party has signed" logic - # (Contract#on_party_signed) never fires for us: the contractor is only - # invited to sign *after* HCB does, so `parties.not_hcb.all?(&:signed?)` - # can never be true before HCB's turn. Notify HCB ourselves as soon as - # the organizer — the only party required to sign before HCB — has. if party.organizer? hcb_party = party.contract.party(:hcb) notify_hcb_of_review(hcb_party) if hcb_party.present? && !hcb_party.signed? end - # HCB ops review the contract by signing it: HCB's signature is what - # moves the position out of review and into onboarding. if party.hcb? mark_onboarding! if may_mark_onboarding? - # The contractor is only invited to sign once HCB has signed contractor = party.contract.party(:contractor) notify_contractor_of_onboarding(contractor) if contractor.present? && !contractor.signed? end @@ -274,9 +266,6 @@ def notify_contractor_of_onboarding(contractor) Rails.error.report(e, context: { payroll_position_id: id }) end - # Same best-effort reasoning as notify_contractor_of_onboarding: this runs - # from inside the party's own signing transaction, so a mailer/job failure - # here must not roll back the organizer's signature. def notify_hcb_of_review(hcb_party) hcb_party.notify hcb_party.schedule_reminders From c5ae0a92ce6c88e76f3ca4d02e6b7a3480010245 Mon Sep 17 00:00:00 2001 From: Manu Gurudath Date: Thu, 16 Jul 2026 16:52:33 -0400 Subject: [PATCH 07/20] Update payroll_positions_controller.rb --- app/controllers/admin/payroll_positions_controller.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/controllers/admin/payroll_positions_controller.rb b/app/controllers/admin/payroll_positions_controller.rb index 50fb942a55..74e69526fe 100644 --- a/app/controllers/admin/payroll_positions_controller.rb +++ b/app/controllers/admin/payroll_positions_controller.rb @@ -18,9 +18,6 @@ def index @positions = relation.order(Arel.sql("CASE WHEN aasm_state = 'under_review' THEN 0 ELSE 1 END, created_at DESC")).page(@page).per(@per) end - # There is no "approve" action here: HCB approves a contractor by signing - # their contract as HCB (see Payroll::Position#on_contract_party_signed), - # from the contract's own signing page. def reject position = Payroll::Position.find(params[:id]) position.mark_rejected! From e206f2ed0ca1fedf85310a9ec509c6df7c94703f Mon Sep 17 00:00:00 2001 From: Manu Gurudath Date: Fri, 17 Jul 2026 09:52:25 -0400 Subject: [PATCH 08/20] remove unrelated changes --- .../contract/parties_controller.rb | 4 ---- app/models/contract/payroll_position.rb | 2 +- app/models/payroll/position.rb | 20 ++++++------------- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/app/controllers/contract/parties_controller.rb b/app/controllers/contract/parties_controller.rb index 7e4c38934f..55a065370f 100644 --- a/app/controllers/contract/parties_controller.rb +++ b/app/controllers/contract/parties_controller.rb @@ -17,8 +17,6 @@ def show end end - @party.sync_with_docuseal if @party.pending? && @contract.sent_with_docuseal? - if @party.signed? && !(@contract.contractable.is_a?(Event::Application) && @party.hcb?) redirect_to completed_contract_party_path(@party) return @@ -44,8 +42,6 @@ def resend def completed authorize @party - @party.sync_with_docuseal if @party.pending? && @contract.sent_with_docuseal? - if (@party.signee? && @contract.signed?) || @party.contractor? case @contract.contractable when Event::Application diff --git a/app/models/contract/payroll_position.rb b/app/models/contract/payroll_position.rb index 51e9df7c85..dbe4e4c07f 100644 --- a/app/models/contract/payroll_position.rb +++ b/app/models/contract/payroll_position.rb @@ -36,7 +36,7 @@ class Contract class PayrollPosition < Contract - DOCUSEAL_TEMPLATE_ID = 5023480 + DOCUSEAL_TEMPLATE_ID = 4983902 after_update_commit :create_document!, if: -> { event.present? && sent_with_docuseal? && aasm_state_previously_changed?(to: "signed") } diff --git a/app/models/payroll/position.rb b/app/models/payroll/position.rb index a28f715985..bd3af62fb5 100644 --- a/app/models/payroll/position.rb +++ b/app/models/payroll/position.rb @@ -174,19 +174,18 @@ def on_contract_signed(contract) end def on_contract_party_signed(party) - if party.organizer? - hcb_party = party.contract.party(:hcb) - notify_hcb_of_review(hcb_party) if hcb_party.present? && !hcb_party.signed? - end + refresh_onboarding_state! + # The contractor is only invited to sign once HCB has signed if party.hcb? + # HCB signing is the "reviewed by HCB operations" step, so begin onboarding. mark_onboarding! if may_mark_onboarding? contractor = party.contract.party(:contractor) - notify_contractor_of_onboarding(contractor) if contractor.present? && !contractor.signed? - end + return if contractor.nil? || contractor.signed? - refresh_onboarding_state! + notify_contractor_of_onboarding(contractor) + end end def send_contract(organizer_user: nil, cosigner_email: nil, reissue_messages: {}, reissue_of: nil, **options) @@ -266,13 +265,6 @@ def notify_contractor_of_onboarding(contractor) Rails.error.report(e, context: { payroll_position_id: id }) end - def notify_hcb_of_review(hcb_party) - hcb_party.notify - hcb_party.schedule_reminders - rescue => e - Rails.error.report(e, context: { payroll_position_id: id }) - end - def contract_signed_by?(role) contracts.not_voided.any? { |contract| contract.party(role)&.signed? } end From 314dd10ffd8a2e9a87a57bd64d093bcfc64918ce Mon Sep 17 00:00:00 2001 From: Manu Gurudath Date: Fri, 17 Jul 2026 10:00:17 -0400 Subject: [PATCH 09/20] fix search --- app/controllers/admin/tax_forms_controller.rb | 5 +++++ app/views/admin/legal_entities/index.html.erb | 18 +++++++++--------- app/views/admin/payments/index.html.erb | 14 ++++++++------ .../admin/payroll_positions/index.html.erb | 14 ++++++++------ app/views/admin/tax_forms/index.html.erb | 17 +++++++++-------- 5 files changed, 39 insertions(+), 29 deletions(-) diff --git a/app/controllers/admin/tax_forms_controller.rb b/app/controllers/admin/tax_forms_controller.rb index c4fea6f177..251fc20eab 100644 --- a/app/controllers/admin/tax_forms_controller.rb +++ b/app/controllers/admin/tax_forms_controller.rb @@ -8,6 +8,11 @@ def index relation = Tax::Form.includes(:legal_entity) + @q = params[:q].presence + if @q + relation = relation.left_joins(legal_entity: :users).where("legal_entities.name ILIKE :q OR users.full_name ILIKE :q OR users.email ILIKE :q", q: "%#{Tax::Form.sanitize_sql_like(@q)}%").distinct + end + @state = params[:state].presence relation = relation.where(aasm_state: @state) if @state diff --git a/app/views/admin/legal_entities/index.html.erb b/app/views/admin/legal_entities/index.html.erb index 59b5dabab5..9af5ab8dc6 100644 --- a/app/views/admin/legal_entities/index.html.erb +++ b/app/views/admin/legal_entities/index.html.erb @@ -1,14 +1,14 @@ <% title "Legal Entities" %> -<%= form_with local: true, class: "card overflow-visible flex items-center", url: admin_legal_entities_path, method: :get do |form| %> - <%= form.text_field :q, value: @q, placeholder: "Search by name or user email", class: "flex-grow" %> - <%= form.select :entity_type, - options_for_select([%w[Person person], %w[Business business]], @entity_type), - { include_blank: "All types" } %> - <%= form.select :filter, - options_for_select([%w[Managed managed], %w[Archived archived]], params[:filter]), - { include_blank: "All entities" } %> - <%= form.submit "Filter" %> +<%= form_with local: true, class: "card", url: admin_legal_entities_path, method: :get do |form| %> +
+ <%= render "events/filters/search", form: %> + <%= form.select :entity_type, [%w[Person person], %w[Business business]], { include_blank: "All types", selected: @entity_type } %> + <%= form.select :filter, [%w[Managed managed], %w[Archived archived]], { include_blank: "All entities", selected: params[:filter] } %> +
+
+ <%= form.submit "Search", class: "ml-auto" %> +
<% end %>
diff --git a/app/views/admin/payments/index.html.erb b/app/views/admin/payments/index.html.erb index 16dd13778d..ac013506a3 100644 --- a/app/views/admin/payments/index.html.erb +++ b/app/views/admin/payments/index.html.erb @@ -1,11 +1,13 @@ <% title "Contractor Payments" %> -<%= form_with local: true, class: "card overflow-visible flex items-center", url: admin_payments_path, method: :get do |form| %> - <%= form.text_field :q, value: @q, placeholder: "Search by recipient name or email", class: "flex-grow" %> - <%= form.select :state, - options_for_select(Payment.aasm.states.map { |s| [s.human_name, s.name.to_s] }, @state), - { include_blank: "All states" } %> - <%= form.submit "Filter" %> +<%= form_with local: true, class: "card", url: admin_payments_path, method: :get do |form| %> +
+ <%= render "events/filters/search", form: %> + <%= form.select :state, Payment.aasm.states_for_select, { include_blank: "All states", selected: @state } %> +
+
+ <%= form.submit "Search", class: "ml-auto" %> +
<% end %>
diff --git a/app/views/admin/payroll_positions/index.html.erb b/app/views/admin/payroll_positions/index.html.erb index bd66c78930..3c3d1dc5c1 100644 --- a/app/views/admin/payroll_positions/index.html.erb +++ b/app/views/admin/payroll_positions/index.html.erb @@ -1,11 +1,13 @@ <% title "Contractors" %> -<%= form_with local: true, class: "card overflow-visible flex items-center", url: admin_payroll_positions_path, method: :get do |form| %> - <%= form.text_field :q, value: @q, placeholder: "Search by contractor name or email", class: "flex-grow" %> - <%= form.select :state, - options_for_select(Payroll::Position.aasm.states.map { |s| [s.human_name, s.name.to_s] }, @state), - { include_blank: "All states" } %> - <%= form.submit "Filter" %> +<%= form_with local: true, class: "card", url: admin_payroll_positions_path, method: :get do |form| %> +
+ <%= render "events/filters/search", form: %> + <%= form.select :state, Payroll::Position.aasm.states_for_select, { include_blank: "All states", selected: @state } %> +
+
+ <%= form.submit "Search", class: "ml-auto" %> +
<% end %>
diff --git a/app/views/admin/tax_forms/index.html.erb b/app/views/admin/tax_forms/index.html.erb index e152252b17..c36600914b 100644 --- a/app/views/admin/tax_forms/index.html.erb +++ b/app/views/admin/tax_forms/index.html.erb @@ -1,13 +1,14 @@ <% title "Tax Forms" %> -<%= form_with local: true, class: "card overflow-visible flex items-center", url: admin_tax_forms_path, method: :get do |form| %> - <%= form.select :state, - options_for_select(Tax::Form.aasm.states.map { |s| [s.human_name, s.name.to_s] }, @state), - { include_blank: "All states" } %> - <%= form.select :form_type, - options_for_select(Tax::Form.form_types.keys, @form_type), - { include_blank: "All form types" } %> - <%= form.submit "Filter" %> +<%= form_with local: true, class: "card", url: admin_tax_forms_path, method: :get do |form| %> +
+ <%= render "events/filters/search", form: %> + <%= form.select :state, Tax::Form.aasm.states_for_select, { include_blank: "All states", selected: @state } %> + <%= form.select :form_type, Tax::Form.form_types.keys, { include_blank: "All form types", selected: @form_type } %> +
+
+ <%= form.submit "Search", class: "ml-auto" %> +
<% end %>
From fdabd033cc6d3487dc2b3f891a5035d97d537312 Mon Sep 17 00:00:00 2001 From: Manu Gurudath Date: Fri, 17 Jul 2026 10:03:04 -0400 Subject: [PATCH 10/20] Update index.html.erb --- app/views/admin/legal_entities/index.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/admin/legal_entities/index.html.erb b/app/views/admin/legal_entities/index.html.erb index 9af5ab8dc6..a021adad14 100644 --- a/app/views/admin/legal_entities/index.html.erb +++ b/app/views/admin/legal_entities/index.html.erb @@ -25,7 +25,7 @@ Name Type Users / Managing org - TIN<%# presence only — masked TINs are payee-visible, not admin-visible %> + TIN Payable? Actions From e0d8c20ef007fff1807e2c8a6ae7a698664c26ae Mon Sep 17 00:00:00 2001 From: Manu Gurudath Date: Fri, 17 Jul 2026 07:03:37 -0700 Subject: [PATCH 11/20] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- app/controllers/admin/payments_controller.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/controllers/admin/payments_controller.rb b/app/controllers/admin/payments_controller.rb index 10cd767749..3b88da7a86 100644 --- a/app/controllers/admin/payments_controller.rb +++ b/app/controllers/admin/payments_controller.rb @@ -14,7 +14,6 @@ def index @state = params[:state].presence relation = relation.where(aasm_state: @state) if @state - @count = relation.count @payments = relation.order(created_at: :desc).page(@page).per(@per) end From 82ab3caf2c15fafe1bd7db299dd4ac2e6fec2c3d Mon Sep 17 00:00:00 2001 From: Manu Gurudath Date: Fri, 17 Jul 2026 07:03:44 -0700 Subject: [PATCH 12/20] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- app/controllers/admin/payroll_positions_controller.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/controllers/admin/payroll_positions_controller.rb b/app/controllers/admin/payroll_positions_controller.rb index 74e69526fe..c31a94017d 100644 --- a/app/controllers/admin/payroll_positions_controller.rb +++ b/app/controllers/admin/payroll_positions_controller.rb @@ -14,7 +14,6 @@ def index @state = params[:state].presence relation = relation.where(aasm_state: @state) if @state - @count = relation.count @positions = relation.order(Arel.sql("CASE WHEN aasm_state = 'under_review' THEN 0 ELSE 1 END, created_at DESC")).page(@page).per(@per) end From 9d44b1e590f8c6f796ed831fd6fae556db0d3e60 Mon Sep 17 00:00:00 2001 From: Manu Gurudath Date: Fri, 17 Jul 2026 07:03:51 -0700 Subject: [PATCH 13/20] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- app/controllers/admin/legal_entities_controller.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/controllers/admin/legal_entities_controller.rb b/app/controllers/admin/legal_entities_controller.rb index 6ff7e22879..02630be28a 100644 --- a/app/controllers/admin/legal_entities_controller.rb +++ b/app/controllers/admin/legal_entities_controller.rb @@ -23,7 +23,6 @@ def index relation = relation.where.not(archived_at: nil) end - @count = relation.count @legal_entities = relation.order(created_at: :desc).page(@page).per(@per) end From 3f36a45e7884d31f85452476b17a9c2f4c9774f2 Mon Sep 17 00:00:00 2001 From: Manu Gurudath Date: Fri, 17 Jul 2026 07:04:00 -0700 Subject: [PATCH 14/20] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- app/controllers/admin/tax_forms_controller.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/controllers/admin/tax_forms_controller.rb b/app/controllers/admin/tax_forms_controller.rb index 251fc20eab..2b210d2a15 100644 --- a/app/controllers/admin/tax_forms_controller.rb +++ b/app/controllers/admin/tax_forms_controller.rb @@ -19,7 +19,6 @@ def index @form_type = params[:form_type].presence relation = relation.where(form_type: @form_type) if @form_type - @count = relation.count @tax_forms = relation.order(created_at: :desc).page(@page).per(@per) end From c4533a1d23d03dd5096ecf5a67d5080002d99120 Mon Sep 17 00:00:00 2001 From: Manu Gurudath Date: Fri, 17 Jul 2026 07:04:09 -0700 Subject: [PATCH 15/20] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- app/views/admin/payroll_positions/index.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/admin/payroll_positions/index.html.erb b/app/views/admin/payroll_positions/index.html.erb index 3c3d1dc5c1..4a08af9c57 100644 --- a/app/views/admin/payroll_positions/index.html.erb +++ b/app/views/admin/payroll_positions/index.html.erb @@ -44,7 +44,7 @@ <%= position.period_label %> "><%= position.under_review? ? "Under review" : position.status_text %> - <%= link_to position.under_review? ? "Review" : "View", event_payroll_position_path(event_id: position.event.slug, id: position.hashid), data: { turbo: false } %> + <%= link_to position.under_review? ? "Review" : "View", event_payroll_position_path(event_id: position.event.slug, id: position), data: { turbo: false } %> <% end %> From c9ed0d36467e7255c05229ce1ee853df5383e1b9 Mon Sep 17 00:00:00 2001 From: Manu Gurudath Date: Fri, 17 Jul 2026 10:09:19 -0400 Subject: [PATCH 16/20] Update nav.rb --- app/models/admin/nav.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/admin/nav.rb b/app/models/admin/nav.rb index d93bfdee34..341f3de6cb 100644 --- a/app/models/admin/nav.rb +++ b/app/models/admin/nav.rb @@ -331,7 +331,7 @@ def payroll end def legacy_payroll_items - return [] unless Flipper.enabled?(:payments_contractors_refresh_2026_06_26) + return [] if Flipper.enabled?(:payments_contractors_refresh_2026_06_26) [ make_item( From 74e9c149695c8837dab210c756e0c821e5944746 Mon Sep 17 00:00:00 2001 From: Manu Gurudath Date: Fri, 17 Jul 2026 12:46:17 -0400 Subject: [PATCH 17/20] improve code --- .../admin/legal_entities_controller.rb | 11 +++-- app/models/admin/nav.rb | 40 ++++--------------- app/views/admin/legal_entities/index.html.erb | 7 +++- app/views/payroll/positions/show.html.erb | 2 +- 4 files changed, 19 insertions(+), 41 deletions(-) diff --git a/app/controllers/admin/legal_entities_controller.rb b/app/controllers/admin/legal_entities_controller.rb index 02630be28a..b113ce278e 100644 --- a/app/controllers/admin/legal_entities_controller.rb +++ b/app/controllers/admin/legal_entities_controller.rb @@ -16,12 +16,11 @@ def index @entity_type = params[:entity_type].presence relation = relation.where(entity_type: @entity_type) if @entity_type - case params[:filter] - when "managed" - relation = relation.managed - when "archived" - relation = relation.where.not(archived_at: nil) - end + @managed = params[:managed].present? + relation = relation.managed if @managed + + @archived = params[:archived].present? + relation = relation.where.not(archived_at: nil) if @archived @legal_entities = relation.order(created_at: :desc).page(@page).per(@per) end diff --git a/app/models/admin/nav.rb b/app/models/admin/nav.rb index 341f3de6cb..b6091f7587 100644 --- a/app/models/admin/nav.rb +++ b/app/models/admin/nav.rb @@ -160,6 +160,12 @@ def spending path: reimbursements_admin_index_path, count: ->{ Reimbursement::Report.reimbursement_requested.count }, count_type: :tasks + ), + make_item( + name: "Contractor Payments", + path: admin_payments_path, + count: ->{ Payment.under_review.count }, + count_type: :tasks ) ] ) @@ -307,12 +313,6 @@ def payroll count: ->{ Payroll::Position.under_review.count }, count_type: :tasks ), - make_item( - name: "Contractor Payments", - path: admin_payments_path, - count: ->{ Payment.under_review.count }, - count_type: :tasks - ), make_item( name: "Legal Entities", path: admin_legal_entities_path, @@ -324,37 +324,11 @@ def payroll path: admin_tax_forms_path, count: ->{ Tax::Form.count }, count_type: :records - ), - *legacy_payroll_items + ) ] ) end - def legacy_payroll_items - return [] if Flipper.enabled?(:payments_contractors_refresh_2026_06_26) - - [ - make_item( - name: "Employees", - path: employees_admin_index_path, - count: ->{ Employee.onboarding.count }, - count_type: :tasks - ), - make_item( - name: "Payments", - path: employee_payments_admin_index_path, - count: ->{ Employee::Payment.paid.count }, - count_type: :records - ), - make_item( - name: "W9s", - path: admin_w9s_path, - count: ->{ W9.count }, - count_type: :records - ) - ] - end - def misc Section.new( name: "Misc", diff --git a/app/views/admin/legal_entities/index.html.erb b/app/views/admin/legal_entities/index.html.erb index a021adad14..74a4c44ff1 100644 --- a/app/views/admin/legal_entities/index.html.erb +++ b/app/views/admin/legal_entities/index.html.erb @@ -4,7 +4,12 @@
<%= render "events/filters/search", form: %> <%= form.select :entity_type, [%w[Person person], %w[Business business]], { include_blank: "All types", selected: @entity_type } %> - <%= form.select :filter, [%w[Managed managed], %w[Archived archived]], { include_blank: "All entities", selected: params[:filter] } %> + +
<%= form.submit "Search", class: "ml-auto" %> diff --git a/app/views/payroll/positions/show.html.erb b/app/views/payroll/positions/show.html.erb index dfd42b7187..a3590283cc 100644 --- a/app/views/payroll/positions/show.html.erb +++ b/app/views/payroll/positions/show.html.erb @@ -55,7 +55,7 @@ <% if @position.contract.present? %> This contract is awaiting HCB review. Reviewing it means signing it as HCB. - <%= link_to "Review & sign contract", contract_party_path(@position.contract.party(:hcb)), class: "btn bg-success", data: { turbo_frame: "_top" } %> + <%= link_to "Review & sign contract", contract_party_path(@position.contract.party(:hcb)), class: "btn bg-success nowrap", data: { turbo_frame: "_top" } %> <%= button_to "Reject", reject_admin_payroll_position_path(@position), class: "btn bg-muted", data: { turbo_frame: "_top", turbo_confirm: "Reject this contractor?" } %> <% else %> From 0b5b6046a00f8e062ec20f9641408e73980c7646 Mon Sep 17 00:00:00 2001 From: Manu Gurudath Date: Fri, 17 Jul 2026 12:46:37 -0400 Subject: [PATCH 18/20] Update show.html.erb --- app/views/payroll/positions/show.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/payroll/positions/show.html.erb b/app/views/payroll/positions/show.html.erb index a3590283cc..cadfb1b965 100644 --- a/app/views/payroll/positions/show.html.erb +++ b/app/views/payroll/positions/show.html.erb @@ -55,7 +55,7 @@ <% if @position.contract.present? %> This contract is awaiting HCB review. Reviewing it means signing it as HCB. - <%= link_to "Review & sign contract", contract_party_path(@position.contract.party(:hcb)), class: "btn bg-success nowrap", data: { turbo_frame: "_top" } %> + <%= link_to "Review", contract_party_path(@position.contract.party(:hcb)), class: "btn bg-success nowrap", data: { turbo_frame: "_top" } %> <%= button_to "Reject", reject_admin_payroll_position_path(@position), class: "btn bg-muted", data: { turbo_frame: "_top", turbo_confirm: "Reject this contractor?" } %> <% else %> From ed75523078adbe43afc57165941263fa651be0dc Mon Sep 17 00:00:00 2001 From: Manu Gurudath Date: Tue, 21 Jul 2026 12:55:12 -0700 Subject: [PATCH 19/20] Update legal_entities_controller.rb --- app/controllers/admin/legal_entities_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/admin/legal_entities_controller.rb b/app/controllers/admin/legal_entities_controller.rb index b113ce278e..85f0596262 100644 --- a/app/controllers/admin/legal_entities_controller.rb +++ b/app/controllers/admin/legal_entities_controller.rb @@ -16,10 +16,10 @@ def index @entity_type = params[:entity_type].presence relation = relation.where(entity_type: @entity_type) if @entity_type - @managed = params[:managed].present? + @managed = params[:managed] == "1" relation = relation.managed if @managed - @archived = params[:archived].present? + @archived = params[:archived] == "1" relation = relation.where.not(archived_at: nil) if @archived @legal_entities = relation.order(created_at: :desc).page(@page).per(@per) From 14e4ff475d8a2a2e70019461154c6c5a735b4d02 Mon Sep 17 00:00:00 2001 From: Samuel Fernandez Date: Wed, 22 Jul 2026 15:15:13 -0400 Subject: [PATCH 20/20] Rename contractor payments -> payments --- app/models/admin/nav.rb | 2 +- app/views/admin/payments/index.html.erb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/admin/nav.rb b/app/models/admin/nav.rb index b6091f7587..e8a6e6f46e 100644 --- a/app/models/admin/nav.rb +++ b/app/models/admin/nav.rb @@ -162,7 +162,7 @@ def spending count_type: :tasks ), make_item( - name: "Contractor Payments", + name: "Payments", path: admin_payments_path, count: ->{ Payment.under_review.count }, count_type: :tasks diff --git a/app/views/admin/payments/index.html.erb b/app/views/admin/payments/index.html.erb index ac013506a3..dc448dafcf 100644 --- a/app/views/admin/payments/index.html.erb +++ b/app/views/admin/payments/index.html.erb @@ -1,4 +1,4 @@ -<% title "Contractor Payments" %> +<% title "Payments" %> <%= form_with local: true, class: "card", url: admin_payments_path, method: :get do |form| %>