diff --git a/AGENTS.md b/AGENTS.md
index db37359ab7..5e959012f4 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -146,7 +146,7 @@ This codebase (Rails 8.1)
| `Communicable` | Gives a record the `notifications` association, nested attributes, a default `communications_scope`, and a `before_validation` addressing a hand-logged communication to the record's `communications_email`. Included by every model whose form renders `shared/_comments_and_communications` |
| `AuthorCreditable` | Author attribution. Credits are formatted by the credited **person's profile** (`Person#display_name_preference`), not by the record. The record's `author_credit_preference` is the consent snapshot taken at create time and is human-editable only on the author credit divergences page. Anonymity is a one-way latch: the profile or the record can set it, neither can strip it from the other. `credits_to_org` flips the unattributed label to "AWBW Staff"; `credits_creator` (idea models, workshop logs) credits the submitting account's person when no author is named |
| `Featureable` | `featured`, `publicly_featured` scopes |
-| `FilemakerCoded` | Reads `filemaker_code` as the trimmed, comma-separated **list** it is (Person, Organization, Affiliation): `#filemaker_codes` for the set a record answers to, `.join_filemaker_codes` to combine any mix of strings/lists into one normalized column value (both dedupers union the codes on merge). `filemaker_archived_in` names the `Fm*` archive table(s) a model's codes point into (`.filemaker_archives`), and `.with_filemaker_code(code)` finds records carrying a code. Nothing should compare the whole column against a single code — `FilemakerHelper#filemaker_code_links` renders one archive link per code, and `fm_archives#show` links back to the portal records carrying its code |
+| `FilemakerCoded` | Reads `filemaker_code` as the trimmed, comma-separated **list** it is (Person, Organization, Affiliation, Payment, Workshop, Resource): `#filemaker_codes` for the set a record answers to, `.join_filemaker_codes` to combine any mix of strings/lists into one normalized column value (both dedupers union the codes on merge). `filemaker_archived_in` names the `Fm*` archive table(s) a model's codes point into (`.filemaker_archives`), and `.with_filemaker_code(code)` finds records carrying a code. Nothing should compare the whole column against a single code — `FilemakerHelper#filemaker_code_links` renders one archive link per code, and `fm_archives#show` links back to the portal records carrying its code |
| `Mentioner` | ActionText @mention extraction and grouping |
| `NameFilterable` | Name-based filtering |
| `PayChargeExtensions` | Included into `Pay::Charge` (via `config/initializers/pay.rb`): turns a Stripe charge into an AWBW `Payment` + `Allocation`, resolving the membership invoice it pays |
diff --git a/app/controllers/fm_archives_controller.rb b/app/controllers/fm_archives_controller.rb
index 48891710c9..2ce09502b6 100644
--- a/app/controllers/fm_archives_controller.rb
+++ b/app/controllers/fm_archives_controller.rb
@@ -63,7 +63,7 @@ class FmArchivesController < ApplicationController
{ label: "Other", icon: "📎", tables: %w[notes] }
]
- PORTAL_MODELS = [ Person, Organization, Affiliation ].freeze
+ PORTAL_MODELS = [ Person, Organization, Affiliation, Payment, Workshop ].freeze
def index
authorize! :fm_archive
diff --git a/app/controllers/payments_controller.rb b/app/controllers/payments_controller.rb
index 69652f4910..c53ea16c58 100644
--- a/app/controllers/payments_controller.rb
+++ b/app/controllers/payments_controller.rb
@@ -252,7 +252,7 @@ def calculate_allocation_amount(payment, allocatable)
end
def edit_payment_params
- params.require(:payment).permit(:person_id, :organization_id, :form_submission_id, :payer_sgid, :additional_designation_sgid)
+ params.require(:payment).permit(:person_id, :organization_id, :form_submission_id, :payer_sgid, :additional_designation_sgid, :filemaker_code)
end
def redirect_path_for(allocatable)
diff --git a/app/controllers/workshops_controller.rb b/app/controllers/workshops_controller.rb
index 18b900a952..698cb92d75 100644
--- a/app/controllers/workshops_controller.rb
+++ b/app/controllers/workshops_controller.rb
@@ -207,7 +207,7 @@ def workshop_params
params.require(:workshop).permit(
:title, :featured, :published,
:full_name, :author_id, :windows_type_id, :workshop_idea_id, :author_credit_preference,
- :month, :year,
+ :month, :year, :filemaker_code,
:publicly_visible,
:publicly_featured,
diff --git a/app/helpers/filemaker_helper.rb b/app/helpers/filemaker_helper.rb
index c646c1f070..349b083da5 100644
--- a/app/helpers/filemaker_helper.rb
+++ b/app/helpers/filemaker_helper.rb
@@ -34,6 +34,8 @@ def filemaker_portal_link(record)
when Person then [ record.full_name, person_path(record) ]
when Organization then [ record.name, organization_path(record) ]
when Affiliation then [ "#{record.person&.full_name} at #{record.organization&.name}", edit_affiliation_path(record) ]
+ when Payment then [ [ dollars_from_cents(record.amount_cents), record.payer&.name ].compact.join(" from "), payment_path(record) ]
+ when Workshop then [ record.title, workshop_path(record) ]
end
link_to text, path, class: button_classes(:primary_outline, size: :sm)
diff --git a/app/models/payment.rb b/app/models/payment.rb
index a7e0e30661..4f6f4b67e5 100644
--- a/app/models/payment.rb
+++ b/app/models/payment.rb
@@ -1,4 +1,6 @@
class Payment < ApplicationRecord
+ include FilemakerCoded
+ filemaker_archived_in FmPayment
has_paper_trail
PAYER_TYPES = %w[Person Organization].freeze
diff --git a/app/models/resource.rb b/app/models/resource.rb
index b593611d19..0f4e99af9b 100644
--- a/app/models/resource.rb
+++ b/app/models/resource.rb
@@ -5,6 +5,7 @@ class Resource < ApplicationRecord
include Rails.application.routes.url_helpers
include ActionText::Attachable
include Mentionable
+ include FilemakerCoded
# Define rich text fields for mentions functionality
def self.mentionable_rich_text_fields
diff --git a/app/models/workshop.rb b/app/models/workshop.rb
index 11932ea209..5001ee05f3 100644
--- a/app/models/workshop.rb
+++ b/app/models/workshop.rb
@@ -3,6 +3,8 @@ class Workshop < ApplicationRecord
credits_to_org
include Featureable, Publishable, RemoteSearchable, TagFilterable, Trendable, WindowsTypeFilterable, RichTextSearchable
include Communicable
+ include FilemakerCoded
+ filemaker_archived_in FmWorkshop
include PunctuationStrippable
include Rails.application.routes.url_helpers
include ActionText::Attachable
diff --git a/app/views/payments/edit.html.erb b/app/views/payments/edit.html.erb
index ac644b9778..68af01c8e0 100644
--- a/app/views/payments/edit.html.erb
+++ b/app/views/payments/edit.html.erb
@@ -37,9 +37,20 @@
prompt: "Search people and organizations",
label: "Additional designation" %>
- <%= f.input :form_submission_id,
- input_html: { class: "bg-white" },
- label: "Form Submission ID" %>
+
+
+ <%= f.input :form_submission_id,
+ input_html: { class: "bg-white" },
+ label: "Form Submission ID" %>
+
+
+
+ <%= f.input :filemaker_code,
+ input_html: { class: "bg-white" },
+ label: "FileMaker code",
+ hint: filemaker_code_hint(f.object) %>
+
+
<%= link_to "Cancel", payment_path(@payment), class: button_classes(:secondary_outline) %>
diff --git a/app/views/resources/_form.html.erb b/app/views/resources/_form.html.erb
index 0a394342ee..1738c9cc92 100644
--- a/app/views/resources/_form.html.erb
+++ b/app/views/resources/_form.html.erb
@@ -37,6 +37,15 @@
input_html: { class: "w-24 rounded border-gray-300 px-2 py-1", min: 1, step: 1 },
label_html: { class: "font-medium mb-1 block" } %>
+
+ <% if allowed_to?(:manage?, @resource) %>
+
+ <%= f.input :filemaker_code,
+ label: "FileMaker code",
+ input_html: { class: "w-full rounded border-gray-300" },
+ label_html: { class: "font-medium mb-1 block" } %>
+
+ <% end %>
diff --git a/app/views/workshops/_form.html.erb b/app/views/workshops/_form.html.erb
index deab563095..83bec3b2e9 100644
--- a/app/views/workshops/_form.html.erb
+++ b/app/views/workshops/_form.html.erb
@@ -103,6 +103,10 @@
} %>
+ <%= f.input :filemaker_code,
+ label: "FileMaker code",
+ hint: filemaker_code_hint(f.object),
+ input_html: { class: "block w-full rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm" } %>
<%= render "shared/visibility_info", flags: %i[published featured publicly_visible publicly_featured] %>
diff --git a/config/features.yml b/config/features.yml
index f1cf79f513..981ec2c8c5 100644
--- a/config/features.yml
+++ b/config/features.yml
@@ -4261,3 +4261,15 @@
the archive record links back to the affiliation.
pro_tips:
- "An affiliation's code is usually its program's FileMaker project ID (like P2775), and occasionally a personnel ID (like PS9999)."
+
+- name: "FileMaker codes on payments, workshops, and resources"
+ area: content
+ display_status: admin_facing
+ released_on: 2026-10-12
+ pr_number: 2656
+ summary: >-
+ The payment, workshop, and resource editors now show each record's FileMaker
+ code so admins can see and fix it, the same way people and organizations do.
+ pro_tips:
+ - "Separate several codes with commas — a record can answer to more than one."
+ - "Each code links to its FileMaker archive record, and that record links back to the payment or workshop."
diff --git a/docs/adr/0005-filemaker-code-archive-mapping.md b/docs/adr/0005-filemaker-code-archive-mapping.md
index 76e70c197e..8f5ea1b3b5 100644
--- a/docs/adr/0005-filemaker-code-archive-mapping.md
+++ b/docs/adr/0005-filemaker-code-archive-mapping.md
@@ -26,9 +26,9 @@ drive the archive pages' links.
| `people.filemaker_code` | `fm_rolodexes` | `ID`, e.g. `00006` | `filemaker_code_links` on person edit + admin details |
| `organizations.filemaker_code` | `fm_projects`, then `fm_organizations` | `ProjectID`, e.g. `P2775`; `OrgID`, e.g. `ORG07206` | `filemaker_code_links` on org form; `filemaker_project_links` / `filemaker_organization_links` on org edit |
| `affiliations.filemaker_code` | `fm_projects`, then `fm_personnels` | `ProjectID`, e.g. `P2775`; rarely `PrsnlRecID`, e.g. `PS9999` | `filemaker_code_links` on affiliation edit |
-| `payments.filemaker_code` | `fm_payments` | `RecordID`, e.g. `D29186` | dev seed (no UI link yet) |
-| `workshops.filemaker_code` | `fm_workshops` | `WorkshopID` | dev seed (no UI link yet) |
-| `resources.filemaker_code` | `fm_quotations` | `RecordID`, e.g. `Q10215` | no UI link yet |
+| `payments.filemaker_code` | `fm_payments` | `RecordID`, e.g. `D29186` | `filemaker_code_hint` on payment edit |
+| `workshops.filemaker_code` | `fm_workshops` | `WorkshopID` | `filemaker_code_hint` on workshop form |
+| `resources.filemaker_code` | none | `RecordID` of the source quotation, e.g. `Q10215` | resource form (editable, no archive link) |
- **Organization → Project, or → Organization.** An app `Organization` is usually one
art program site, which is a FileMaker *Project*. FileMaker's own Organization row is
@@ -47,11 +47,12 @@ drive the archive pages' links.
never its org. One affiliation carries a real `PS` Personnel id, so Personnel stays
as the second table. The 13 that differ from the org's code are worth a look: the
affiliation may sit on the wrong org, or the org may be missing that code.
-- **Resource → Quotation.** In production, 116 of 128 resource codes are quotation ids
- (`Q…`), 115 of them on Leader spotlight resources and one on a Story; 115 of the 116
- exist in `fm_quotations` (quotes and testimonials, with a `WorkShopLeader` field).
- Form Letters are mail-merge templates, not resources. The dev seed still leaves
- `resources.filemaker_code` blank.
+- **Resource → none.** In production, 116 of 128 resource codes are quotation ids
+ (`Q…`), 115 of them on Leader spotlight resources and one on a Story: each spotlight's
+ body is the text of the FileMaker quotation it was built from. A resource is still its
+ own portal record, not an archive row, and quotations belong with the portal's quotes,
+ so resource codes stay editable but don't link into the archive or back. The dev seed
+ leaves `resources.filemaker_code` blank.
## Archive id patterns
@@ -173,6 +174,6 @@ Production, 2026-10-12: 84 records (41 organizations, 16 affiliations, 14 worksh
## Consequences
-A UI link for payments, workshops, or resources includes `FilemakerCoded` on the
+A UI link for a new FileMaker-coded column includes `FilemakerCoded` on the
model and declares the archive table above (e.g. `filemaker_archived_in FmPersonnel`);
`filemaker_code_links` reads it from there.
diff --git a/spec/requests/fm_archives_spec.rb b/spec/requests/fm_archives_spec.rb
index 128c65b9fd..e182b87d9b 100644
--- a/spec/requests/fm_archives_spec.rb
+++ b/spec/requests/fm_archives_spec.rb
@@ -84,6 +84,27 @@
expect(response.body).to include(ERB::Util.html_escape("#{affiliation.person.full_name} at #{affiliation.organization.name}"))
end
+ it "links an archived payment back to its portal payment" do
+ FmPayment.create!(fm_id: "D29186")
+ person = create(:person)
+ payment = create(:payment, person:, amount_cents: 15_000, filemaker_code: "D29186")
+
+ get fm_archive_path("D29186", table: "payments")
+
+ expect(response.body).to include(payment_path(payment))
+ expect(response.body).to include("$150 from #{ERB::Util.html_escape(person.name)}")
+ end
+
+ it "links an archived workshop back to its portal workshop" do
+ FmWorkshop.create!(fm_id: "W0123")
+ workshop = create(:workshop, filemaker_code: "W0123")
+
+ get fm_archive_path("W0123", table: "workshops")
+
+ expect(response.body).to include(workshop_path(workshop))
+ expect(response.body).to include(ERB::Util.html_escape(workshop.title))
+ end
+
it "omits the portal section when nothing carries the code" do
FmRolodex.create!(fm_id: "00006")
diff --git a/spec/requests/payments_spec.rb b/spec/requests/payments_spec.rb
index 958afeb0b7..bd0b5a286a 100644
--- a/spec/requests/payments_spec.rb
+++ b/spec/requests/payments_spec.rb
@@ -172,9 +172,40 @@
end
end
+ describe "GET /payments/:id/edit FileMaker code" do
+ it "renders the FileMaker code field" do
+ payment = create(:payment, person:)
+
+ get edit_payment_path(payment)
+
+ expect(response.body).to include("payment[filemaker_code]")
+ end
+
+ it "links each archived code to the FileMaker payments archive" do
+ FmPayment.create!(fm_id: "D29186", data: {})
+ payment = create(:payment, person:, filemaker_code: "D29186, D99999")
+
+ get edit_payment_path(payment)
+
+ expect(response.body).to include(fm_archive_path("D29186", table: "payments"))
+ expect(response.body).to include("No FileMaker archive record for D99999")
+ expect(response.body).not_to include(fm_archive_path("D99999", table: "payments"))
+ end
+ end
+
describe "PATCH /payments/:id" do
let(:organization) { create(:organization) }
+ it "updates the FileMaker code" do
+ payment = create(:payment, person:)
+
+ patch payment_path(payment), params: {
+ payment: { payer_sgid: person.to_sgid.to_s, filemaker_code: "D29186" }
+ }
+
+ expect(payment.reload.filemaker_code).to eq("D29186")
+ end
+
it "updates payer and designation via sgids" do
payment = create(:payment, person:, organization: nil)
diff --git a/spec/requests/resources_spec.rb b/spec/requests/resources_spec.rb
index 839a99d224..77b65bf814 100644
--- a/spec/requests/resources_spec.rb
+++ b/spec/requests/resources_spec.rb
@@ -139,6 +139,22 @@
expect(response.body).to include('name="resource[publicly_visible]"')
expect(response.body).to include(VisibilityFlagsHelper::FLAG_DEFINITIONS[:hidden_from_search][:description])
end
+
+ it "renders the FileMaker code field" do
+ resource = Resource.create! valid_attributes
+ get edit_resource_url(resource)
+
+ expect(response.body).to include('name="resource[filemaker_code]"')
+ end
+
+ it "shows the FileMaker code without an archive link" do
+ FmQuotation.create!(fm_id: "Q10215", data: {})
+ resource = Resource.create! valid_attributes.merge(filemaker_code: "Q10215")
+ get edit_resource_url(resource)
+
+ expect(response.body).to include('value="Q10215"')
+ expect(response.body).not_to include("table=quotations")
+ end
end
describe "POST /create" do
@@ -200,6 +216,13 @@
expect(resource.title).to eq("Updated Resource Title")
end
+ it "updates the FileMaker code" do
+ resource = Resource.create! valid_attributes
+ patch resource_url(resource), params: { resource: new_attributes.merge(filemaker_code: "R42") }
+
+ expect(resource.reload.filemaker_code).to eq("R42")
+ end
+
it "redirects to the updated resource" do
resource = Resource.create! valid_attributes
patch resource_url(resource), params: { resource: new_attributes }
diff --git a/spec/requests/workshops_spec.rb b/spec/requests/workshops_spec.rb
index 3dca2646b4..1ca40e066a 100644
--- a/spec/requests/workshops_spec.rb
+++ b/spec/requests/workshops_spec.rb
@@ -52,6 +52,36 @@
end
end
+ describe "FileMaker code" do
+ let(:admin) { create(:user, :admin) }
+ let(:workshop) { create(:workshop) }
+
+ before { sign_in admin }
+
+ it "renders the FileMaker code field on edit" do
+ get edit_workshop_url(workshop)
+
+ expect(response.body).to include("workshop[filemaker_code]")
+ end
+
+ it "links an archived code to the FileMaker workshops archive" do
+ FmWorkshop.create!(fm_id: "W123", data: {})
+ workshop.update!(filemaker_code: "W123")
+
+ get edit_workshop_url(workshop)
+
+ expect(response.body).to include(fm_archive_path("W123", table: "workshops"))
+ end
+
+ it "updates the FileMaker code" do
+ patch workshop_url(workshop), params: {
+ workshop: { title: workshop.title, category_ids: [ "" ], filemaker_code: "W123" }
+ }
+
+ expect(workshop.reload.filemaker_code).to eq("W123")
+ end
+ end
+
# --- STORIES THAT USE THIS WORKSHOP ----------------------------------------
describe "GET /show stories section" do
let(:user) { create(:user) }