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) }