purchasability-sync-completion changes
Deferred live verification for ladder-tier-append and
multi-price-support to maintainer; stack left running. Validated
ladder-tier-append with openspec --strict. Closed purchasability
checklist issue and marked milestone 10c Done. Archived specs into
archive directory and created db-error-presentation spec. Updated
plan-ladder-management and price-management specs with tier append,
reorder, removal, and price default behavior.
Preserve stored ladder active state on update, and refresh
milestone/status
docs for the M10/M11 renumbering and audit debt tracking.
Hide inert ladder active controls
Preserve the stored ladder active flag on update, and remove active
status
from ladder list/topology views until retirement has real behavior.
Update audit and milestone docs for the M10 renumbering and 10h
completion.
Completes the purchasability create-to-sell journey with live-update UX.
- Extract readiness card into operator_product_readiness.html partial.
- Live-poll (hx-trigger every 3s) while a Stripe sync is pending; reply
HTTP 286 (htmx stop-polling) once terminal (synced/failed). Route:
GET /partials/operator/products/{productID}/readiness.
- Failed-state derivation: query integration.outbox for dead_letter rows
(create_stripe_product/price); render sync failure with the real outbox
error and a Retry button (hx-post to SyncProductToStripe, resets
dead_letter→pending).
- Rename 'Stripe-mapped price' row to 'Payment processing' with de-jargoned
detail/empty-state copy.
- Truthful Stripe-configured gate: OperatorPartialsHandler carries
StripeConfigured (set from cfg.StripeAPIKey/StripeWebhookSecret) instead
of guessing from stripeQ != nil.
- Render tests cover failed branch (badge + error + Retry), Stripe-off
empty-state, polling attrs present while pending/absent when terminal.
Plus status/: adversarial UX audit (56 confirmed findings, 16 high) and
cross-cutting audit entry in issues.md.
Closes: openspec/changes/purchasability-sync-completion
Add first-class default-price support: a product has a single default price
that readiness, the member catalog, and checkout all track.
- Migration 00016: is_default column + partial unique index (one default per
product) + backfill (oldest active price per product).
- New queries: GetDefaultPriceByProduct, ClearDefaultPrice, MarkDefaultPrice,
DeactivatePrice (guarded by is_default = FALSE).
- CreatePrice: first price for a product auto-becomes default (NOT EXISTS probe);
success copy differentiates auto-default from subsequent non-default prices.
- Per-row price affordances: Sync (posts price_id, hx-vals), Make default
(clear-then-set transaction), Deactivate (default-price guard).
- computePriceReadiness tracks the default price; resolvePurchasable documented
as the shared purchasability gate.
- SyncProductToStripe: selectable price (explicit price_id or default);
product-idempotent — product-synced guard prevents duplicate Stripe products.
- All error paths route through web.FieldErrorsFromDB (422) or slog+generic text,
never err.Error().
- Render tests for default badge, per-row affordance visibility, form-level
FieldErrors alert.
Closes: openspec/changes/multi-price-support
Fix the rank-collision UX bug at the source.
- Append-at-end: CreatePlanLadderTier INSERT uses COALESCE(MAX(rank)+1, 0);
handler drops the rank input and template shows a drag-to-position hint.
- Renumber-on-delete: DeletePlanLadderTier renumbers remaining tiers inline
so ranks stay contiguous after deletion.
- Confirm dialog copy is now honest: deleting the top tier explicitly
promotes the next tier to the new-org default.
- Friendly errors: map duplicate-product and rank-collision constraints
to 422 field errors via web.FieldErrorsFromDB.
- All fixtures updated for append-at-end order.
Closes: openspec/changes/ladder-tier-append
Gate Stripe readiness on real credentials, surface dead-lettered syncs
as
failed with retry, and add header-safe toast JSON encoding.
Switch the test Keycloak realm references to `test` and document the
OpenSpec change.
Seed Keycloak into a dedicated wikicafe app realm and repoint the
test OIDC, Temporal, and FedWiki wiring to it.
Label test credentials as throwaway, remove the unused realm export from
tracking, and have init write the embedded starter config.
Fail fast with aggregated config errors after resolving secret files,
before services initialize. Add Valkey session config, remove the unused
session-secret, and fix the production CSP env key.
Retry initial Temporal dials with bounded backoff and gate the test
Temporal service on healthy DB and Keycloak dependencies.
Replace absolute rank edits with a SortableJS row reorder route that
posts the
full product order and renumbers ranks transactionally with temporary
negative
values. Share the sortable initializer with topology column ordering.
- Add SortableJS drag-to-reorder for ladder columns on the topology
overview page
- Remove the manual "Sort order" input from the ladder edit form
- Add SetPlanLadderSortOrder SQL query and remove sort_order from
UpdatePlanLadder
- Update templates, CSS, specs, and tests to support the new flow
Provide addressable composite pages for products, plan ladders, and
entitlement sets. Rewire list/topology navigation to boosted links and
update mutation re-renders to refresh the composite body.
Enqueue product and active price mappings from the readiness panel with
guards for unconfigured, pending, and already-synced states.
Show sync/not-configured guidance, stop auto-enqueueing price syncs, and
omit empty Stripe product descriptions to avoid dead-lettering.
Add an is_reserved flag for app-managed types and stamp the system type
on
insert/backfill. Use a configurable-only list in the management UI and
reject
direct default-plan or backfill mutations for reserved types.
Expose `/operator/integrations` as a registry-driven landing for
provisioning providers, with provider links nested under the new sidebar
entry.
Update the OpenSpec archive and mark milestone 9b done.
Update the plan transitions specification to ensure that a downgrade
lands
on the requested lower-ranked tier instead of falling back to the org
default. Use the `end` transition type to trigger default
re-application.
Archive the corresponding change proposal to the archive directory.
End pools already at their configured default tier to the no-plan
baseline instead of minting a replacement default grant. Keep
above-default ends reapplying the default, and add regression and
OpenSpec coverage.
Only cooldown force-reduced reserve activations or at-limit swaps.
Restoring an incumbent site is free, and successful rotations clear the
force-reduced marker.
In the NULL-default reapply path, recompute pool entitlements after
recording the end transition so revoked or expired grants drop to
no-plan limits. Add regression coverage and OpenSpec change.
Add a configurable cooldown window and route restores, reactivations,
and swaps through the same rotation check to close the archive/restore
bypass while preserving headroom and no-reserve activations.
Create the singleton System organization/workspace by natural key and
use it
as the FedWiki sync default workspace. Remove the configured workspace
UUID
and holding-workspace seed, and cover creation/adoption with tests.
Add the OpenSpec proposal, design, tasks, and requirements for an
app-ensured tenant resolved by natural key instead of configured UUIDs.
Align FedWiki, entitlement, and provider lifecycle specs around active
and non-active site states, and record related operator UX follow-ups.
Consume wiki-plugin-farmmanager v0.4.1's reversible site states to implement the
force_reduce read-only downgrade and a reversible-delete (archive) model.
Implements the OpenSpec change fedwiki-lifecycle-states.
- Contract: replace the discrete set_readonly/suspend verbs with a parameterized
set_status verb; providers declare their supported lifecycle states
(integration.provider_states). FedWiki declares active/readonly/archived.
- FedWiki state model: fedwiki.sites gains status, storage_bytes, last_modified_at,
force_reduced_at, archived_at; FarmManagerClient.SetSiteStatus (PATCH) +
storageBytes/lastModified; SetSiteStatusWorkflow saga keeps quota = active sites
(quota-gated reactivation).
- Sync now PROJECTS observed status instead of deleting non-active rows, with an
out-of-band usage-drift backstop.
- force_reduce: ReconcileFedWikiQuotaActivity parks excess sites read-only
(keeping the most-recently-modified active) and reactivates on re-upgrade, run
each sync tick. Member "Keep active" swap with a 30-day per-workspace cooldown.
- Archive: member delete routes to the archived state (recoverable) with restore,
delete-permanently, and a 30-day retention purge.
- Operator FedWiki Sites tab surfaces site status. Docs (contract guide,
FARMMANAGER, wishlist) updated; status/issues force_reduce marked resolved.
Register FedWiki and Stripe providers at boot, persist provider
operations, and stamp owned resource keys.
Rename the FedWiki entitlement key from `sites` to `fedwiki_sites`
and drive the operator integration nav from provisioning providers.
Move change docs into
openspec/changes/archive/2026-05-31-operator-purchasability-readiness.
Update openspec/specs/product-management/spec.md to add an operator
product edit page "purchasability readiness" panel. The panel mirrors
member-catalog purchasability logic, lists prereqs (published, visible,
structural kind, active price, Stripe mapping), and includes example
scenarios with guidance for incomplete states.
Add a read-only operator surface at GET /operator/plan-topology with a
server-rendered cross-ladder grid (ladders by sort_order × ranks).
Include
shared-product detection via a new sqlc query (ListSharedTierProducts),
sticky rank-column CSS, add-on strip, org-type provisioning summary, and
a
reverse index of shared memberships. Register route/nav, add handler,
template, tests, specs, and docs.
Create Temporal billing sweep schedule, workflow and activities and
register them in the worker/start initialization. Add an HTMX preview
route and banner for plan switches (switch button now GETs a preview;
Confirm posts the switch). Extend the Stripe test mock to support
invoice previews and add integration tests for PreviewSwitch behavior.
Introduce fulfillment.SwitchPlan, CancelSubscription and
SweepDueScheduledChanges. Add
ApplyScheduledChangesForSubscription DB helper and call it from
reconcile's terminal
branch so webhook-driven period-end deletions close out pending
scheduled changes
idempotently. Wire HTMX partials (#member-plans, switch/cancel buttons)
and update tests.
Move change files into openspec/changes/archive/2026-05-24. Update
member-entitlement-view and member-product-discovery specs to group
plans
by ladder, mark current rung per ladder, render products under each
ladder
they belong to, separate add-ons as stackable, specify move-control
enablement and disabled-with-reason behavior, and require the
entitlement
view to be read-only with no plan-change controls.
Add sort_order to billing.plan_ladders and a migration.
Reshape models, queries, handlers and templates to group plans by
plan ladder (service axis) in rank order. Track the current rung per
ladder and render per-tier move affordances; only the free→paid path
is enabled, other moves render disabled with an explanatory reason.
Include operator sort_order edit, tests, docs and image assets.
Revise stripe-subscription-creation spec to mandate an idempotent
reconcile operation. Treat checkout.session.completed as a thin eager
trigger that invokes reconcile (not an authoritative fulfillment path);
webhook events also invoke reconcile. Ensure pool provisioning and
ladder
changes run through plan-transitions.Transition and reconcile fetches
item state from the Stripe API, not the webhook payload.
Replace the order-dependent, payload-trusting webhook fulfillment with a single
idempotent ReconcileSubscription (internal/fulfillment) that refetches
authoritative subscription state from the Stripe API and converges core records
and pool entitlements via the plan-transitions primitive. checkout.session.completed
and customer.subscription.* become thin triggers; the post-checkout return path
reconciles eagerly. This fixes paid upgrades provisioning zero entitlement, caused
by reading the empty line_items the webhook payload never carries.
- internal/fulfillment: ReconcileSubscription + ReconcileLatestSubscriptionForCustomer,
advisory-locked, convergent/idempotent, with unit + DB-gated integration tests
- internal/stripetest: in-process stripe.Backend mock for offline reconcile tests
- thin webhook handlers; eager reconcile on /?checkout=success (best-effort)
- stripe.Key set process-wide before the Temporal worker starts (cmd/start.go)
- OpenSpec change stripe-fulfillment-reconcile; archive completed member-upgrade-flow
now that its paid-upgrade demonstration (§5.3) is satisfied
Add /billing page and HTMX partials (list + detail) for invoice
history. Introduce MemberInvoicesHandler, wire routes, add templates,
unit and DB scoping tests, and openspec docs. Update nav links.
Update openspec task verifications to checked with evidence, add
composite expansion decisions to status/operator-ux.md, and tweak
status/milestones.md to reflect follow-up landing and archive plan
Resolve lookup terms for person/org on the operator landing
surface. POST /operator/lookup calls identity.LookupPersons (email-exact
first, else name ILIKE) then falls back to org slug lookup.
Use 303 redirects for unique person/org matches; ambiguous or no-match
results are re-rendered inline on the landing page via renderLanding.