rubyforgood / rubyforgood/awbw
Sync payments into the Program Monthly Reconciliation Google Sheet
Nobody has claimed this yet.
- Dominant language
- Ruby
- Stars
- 15
- Forks
- 26
- Avg merge
- 12h 42m
- Merged PRs (30d)
- 242
Description
Context
Staff hand-maintain a monthly reconciliation Google Sheet (Program Monthly Reconciliation List) that tracks, per program, who owes what and what's been received. Today it's fully manual. This enhancement keeps it in sync with the app so staff stop transcribing payments by hand.
The sheet's structure (from the uploaded August 2026 sample):
- Rows are grouped into sections with a hand-typed header in column A and blank spacer rows:
AWBW Membership Fees,On-Demand Training,Misc Refunds,Continuing Education Hours,Misc Payments, and one per Facilitator Training (TAC262 July 2026 Facilitator Training,TAC262 November 2026 Facilitator Training). - Header columns: A spacer · B Name (contact person) · C Agency (organization) · D Requested (expected $) · E Received (paid $) · F Payment For (program code, e.g.
TAC262, or membership year) · G Payment Method (CC,Check #101066) · H Payment Received (date, or a manual "Deadline …" while unpaid) · I Notes (free text). - Rows exist before payment: a registrant appears with Requested filled and Received blank, then Received / Payment Method / date fill in when money arrives.
Decisions (confirmed with product owner)
- Trigger: both — automatic on create/update and a manual "Sync to sheet" button.
- Scope: all payments (cash, check, Stripe).
- Layout: write directly into this sectioned layout — insert/update the right row under the right section, preserving manual headers, Notes, and deadline text.
- Behavior: mirror the sheet as it works today — include expected/unpaid rows (Requested filled, Received blank) that get completed when the payment lands.
Reframe: the row unit is a registration/invoice, not a raw Payment
Each data row = one payer's obligation to one program (an EventRegistration, ContinuingEducationRegistration, or MembershipInvoice — the allocatable), where:
- Requested (D) = the obligation's cost.
- Received (E) = sum of
Allocationamounts posted to it. - Payment Method (G) / Payment Received (H) = derived from the
Payment(s) behind those allocations (CCfor Stripe,Check #<n>for check,Cashfor cash; date from the payment).
So a payment event updates the Received/Method/date of an existing (or new) row; a new registration creates the Requested row.
Approach
Section routing — program → sheet section
Map each allocatable to its section header text (Facilitator Training event → that training's header; Membership → AWBW Membership Fees; CE → Continuing Education Hours; refund → Misc Refunds; unmapped → Misc Payments). A GoogleSheets::SectionRouter derives the section from Event attributes (code + session label) with a small config fallback for the fixed sections.
Row identity / upsert in a hand-edited sheet (core risk)
The sheet has no ID column and staff edit it by hand, so reliable row matching is the hard part.
- Add a hidden key column holding the
allocatableGID; match on that column only — never on visible cells — so re-syncs are idempotent and never clobber manual edits. - Best-effort adopt pre-existing manual rows once via (section + Name + Agency); otherwise append within the section.
- Never overwrite app-not-owned cells: I (Notes) always manual; H only set to
Received M/Dwhen paid, leaving manual "Deadline …" text untouched while unpaid.
Components
- Row builder (decorator):
reconciliation_rowon the allocatable's decorator (or a sharedReconciliationRowPORO) → ordered cells B..H, money viadollars_from_cents, sentence-case labels. - Service:
app/services/google_sheets/reconciliation_sheet.rb(PORO) — service-account auth (Google::Auth::ServiceAccountCredentials+Google::Apis::SheetsV4::SheetsService),#upsert(allocatable)that reads the tab, resolves/ensures the section, and updates the keyed row orInsertDimensions a new row under the right header (value_input_option: "USER_ENTERED"). No-op guard when ENV/credentials absent. - Job:
app/jobs/sync_reconciliation_row_job.rb—perform(allocatable_gid)on Solid Queue (inline in dev); tolerates deletion. - Automatic triggers:
after_commitonAllocation(covers Received/Method/date — all payment paths funnel through allocations: manualPaymentsController#create,PayChargeExtensions#create_external_processor_payment, and theapp/webhooks/processors) and onEventRegistration/ContinuingEducationRegistration/MembershipInvoice(creates/updates the Requested row before any payment). - Manual button + backfill:
POST /payments/:id/sync_to_sheet(admin-only via policy, honoring thereturn_to/eyebrow convention) + a button on the payment/registration admin view; apayments:sync_reconciliation_sheetrake task for the one-time backfill. - Config (ENV, matching Stripe/AWS):
GOOGLE_SHEETS_CREDENTIALS_JSON,PAYMENTS_SHEET_ID(1aQ6…rO6Q),PAYMENTS_SHEET_TABin.env.sample; integration no-ops until set (safe to merge before credentials land). - Gems: add
google-apis-sheets_v4+googleauth; updateAGENTS.md.
Prerequisite (product/ops, out of code scope)
Create a Google Cloud project, enable the Google Sheets API, create a service account + JSON key, and share the spreadsheet as Editor with the service-account email. The integration stays inert until the ENV vars are provided.
Key risks / open items
- Row matching in a hand-edited sheet — hidden-key column + "only touch owned cells" is the mitigation; validate against a copy of the real sheet before enabling production writes.
- Section/program mapping (esp. Facilitator Training headers with session labels) — confirm the
Eventcode/label source during build. - H column dual meaning (Deadline vs Received) — app only writes
Received M/D, never the deadline.
Verification
- Without ENV set: creating a payment no-ops cleanly (log line, no error).
- With a test service account + a copy of this sheet: creating a registration adds a Requested row under the right section; posting a cash/check/Stripe payment fills Received/Method/date on the same row (no duplicate, Notes untouched); editing updates it.
- Manual button pushes/updates on demand; non-admin gets 403.
- Backfill rake task seeds header + hidden key + all current rows once.
ai/test_extragreen before PR.
Tests
Service (stubbed Sheets client), section router, row/decorator, model after_commit callbacks, job, request (sync_to_sheet authorization), policy.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with app/services/google_sheets/reconciliation_sheet.rb, the SectionRouter, and the listed allocatable decorators or ReconciliationRow PORO; then review the Allocation and registration after_commit entry points. Run the service, router, row, callback, job, request, and policy tests, plus ai/test_extra. Done means stubbed and copied-sheet verification passes without ENV errors, duplicate rows, or changes to Notes and unpaid deadlines.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- google-cloud, ruby
- Domain
- api, backend, cloud, payments
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 45/100