Commit Graph
3 Commits
Author SHA1 Message Date
cgalo5758 5219a45a4b Keep only the forum ids of a Discourse webhook event
The Discourse receiver stored each verified event body whole in
core.webhook_events.payload, so every forum signup's email address,
username, name, avatar, profile fields and external id stayed in the
database and its backups, members and non-members alike (#163). Nothing
reads a stored Discourse payload back: the receiver handles the event
from the body it was delivered.

The receiver now parses the body once, after it takes the dedupe key,
and stores only the ids processing reads: {"user":{"id":...}} for
user_created, {"group_user":{"group_id":...,"user_id":...}} for the two
group events. A missing id is stored as 0. A body that does not parse,
or lacks the object its event names, is stored as {} and recorded
failed. Email mode still reads the address from the delivered body.

Two answers change. A signed body that is not JSON, or is empty, failed
the JSONB insert: 500, no row, and redelivery of the same bytes, which
can only fail again. It now gets a failed row and 200, as every
recorded event does. A body Go parses but JSONB rejects (a \u0000
escape, invalid UTF-8) answered 500 on every delivery; its ids are now
stored and it is processed.

docs/models/payments-billing.md says what each receiver stores, and the
comments of scripts/db-reader-role.sql stop calling the column raw
provider payloads; no reader column changes. The OpenSpec change
discourse-events-keep-forum-ids is added with this commit. Ride-along:
the test helper newMapping drops a return value nothing read.

Deliberate breaks, scripts/mutation-check -side new, 29 records, all
CAUGHT:
  the insert stores the raw body (8 rows of both tests)
  keep copies the email into the stored user
  keep drops UserID from the stored group user (both group rows)
  keep swaps GroupID and UserID (both group rows)
  keep takes the user object for every event type (3 rows)
  keep stores a user with id 0 when the user object is nil
  a parse error stores keep of the partly filled view
  the parse-error path answers 500 (the three parse-error rows)
  resolvePerson is called with an empty email
  an event of another type is not dropped
  a group event without group_user reconciles from the user object
  the insert writes the body into provider_environment
  the processed body is the stored one (NUL escape, invalid UTF-8)
  the stored group user omits a zero group_id, and a zero user_id
2026-10-22 19:08:33 -04:00
cgalo5758 7382877ed9 Record a tier delete as an act, inside one transaction on the runner
A plan-wide change to the catalog left no record of itself, so nothing
could say which change moved an organization or offer to run it again
(#187). Migration 00027 adds that record: core.acts, one row per act
with its cause, actor, note and request id, and core.plan_act_details,
the act's plan-specific detail. Both tables are enclosed: only
core.record_plan_act writes them, under the exclusive rendezvous for a
catalog cause, and core_writer can only read them. core_reader reads
every column but the note, which scripts/db-reader-role.sql lists as
personal.

runPlanAct is the one shape the plan acts will share: begin, find the
pools, lock them in ascending id, read again under the locks, act and
commit, with every failure named by its step and a refusal returned as
it is. The tier delete is the first act on it. Inside its exclusive
transaction it counts the tier's live holders (active and suspended),
reads the ladder's tiers and the org types defaulting to it, applies the
last-tier guard, records the act and deletes and renumbers; before this,
the count and the guard's read ran outside the transaction, and a
failed read of the org types let the delete through. A delete of a
product that is no longer a tier of the ladder, a repeated confirm,
writes nothing. The handler only parses, calls and renders:

- A lock wait or a deadlock at any step answers "Another change
  collided with this one; try again." (#216).
- Every other failure answers "Failed to remove tier. Reference:
  {request id}." and logs the step through the request's logger.
- A tier held only by suspended positions is a live holder: the tier
  page routes it to Preview removal, and the plain delete refuses it.

The plan-act golden harness builds the act's queriers through the new
PlanActs seam, so a scenario can fail a statement inside the act's own
transaction, and every golden gains the act and detail sections. Most
gain them empty. Behaviour changes in delete-begin-fails (the log line
names the step and carries the request id; the answer carries the
reference), delete-bottom-tier, delete-top-tier and
tier-change-during-conferral/a-tier-delete (the act row),
delete-held-tier and delete-last-tier-of-a-default (the reads now run
inside the transaction; the answers are unchanged) and contention (the
delete answers the collision sentence; later placeholders renumber
because the delete's log line now carries a request id). New:
delete-org-types-unread and delete-suspended-holder.

The design's failure table for the tier delete now gives the reference
answer for a failed holder count and a failed tier read, as the
approved copy for the delete states, and a row for the repeated delete.
The full suite passes.
2026-10-19 13:35:57 -04:00
cgalo5758 56e65b669c Read invoices back from Stripe and archive the change
Stripe delivers webhook events in the endpoint's API version. Under a
version later than the 2025-02-24.acacia stripe-go v81 pins, the
payload-shaped invoice handlers projected every invoice with no
subscription link and no lines, and every payment with no payment
intent or method.

- Invoice events read only the invoice id and run ReconcileInvoice,
  which reads the invoice, its lines and its payment intent's charges
  back at the pinned version and converges the core rows under a
  per-invoice lock, in any event order. A payment row is one attempt,
  keyed by its Stripe charge (Stripe store migration 00005).
- Two clocks: created_at is when the console wrote a row; invoices gain
  issued_at and payments paid_at (core migration 00022, backfilled),
  and every invoice and payment list, the activity feed and the
  composite's latest invoice read those.
- An hourly and at-boot invoice sweep reads back invoices never read,
  open invoices once a day, and invoices of dead-lettered events; the
  provider page states when it last finished. The first run is the
  repair of invoices projected so far.
- The API version skew is logged once per version, and
  payment_method.attached links payments recorded before it.
- A restricted key now needs Invoices read and Charges read.
- scripts/db-reader-role.sql maintains console_reader, a login role
  that reads the console's tables without personal data, documented in
  docs/database-management.md and checked against a golden column list
  by TestConsoleReaderColumns. The demo snapshot keeps owners and
  privileges so the role's checks hold on it.
- The demo seed and test fixtures carry issue and paid times.
  status/design-feedback.md gains four upstream notes, one written when
  #60 was closed.

OpenSpec change stripe-invoice-reconcile is archived and its deltas
synced into openspec/specs/. Follow-ups are #150, #151 and #152.
2026-09-28 18:42:29 -05:00