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.
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.