Record product→merchant-profile write path; refresh Current state

Document the now-functional product→profile write path in the payments
guide (set_product_merchant_profile, post-write pattern, picker gating,
double-Option clear). Mark the multi-profile GAP closed, drop the done
work-queue item, and note the discovered set_product_entitlements_catalog
rows_affected gap.
This commit is contained in:
Keysat
2026-06-15 21:40:25 -05:00
parent ef8b0aae21
commit ce5edaed29
2 changed files with 38 additions and 54 deletions
+29 -53
View File
@@ -101,63 +101,39 @@ Operator-specific memories at `~/.claude/projects/-Users-macpro-Projects-keysat/
- Build the admin SPA "API keys" management panel (create w/ role picker, list,
revoke) — backend is wired; UI deferred to a design-focused session.
## Current state (2026-06-13)
## Current state (2026-06-16)
- **Live**: registry `registry.keysat.xyz` now publishes **`0.2.0:55`** — universal
multi-arch (x86_64 + aarch64, verified via the registry index + s9pk header),
GitHub release `v0.2.0-55`, canonical s9pk at `files.keysat.xyz/keysat.s9pk`.
Migrations 00200022; four SDKs published; `keysat.xyz` + `docs.keysat.xyz`
deployed (docs redeployed this session). **The live server `immense-voyage.local`
still runs `:54` until updated from the registry.** `:55` ships the two
prior-session P1s (scoped keys, settle-amount tripwire) + this session's packaging
work.
- **Shipped this session (`:55` published; all committed + pushed)** — doc-auditor +
start9-spec-checker + reviewer passes, all approved/no blockers; `tsc`/`bash -n`
clean. Landed: **all 4 StartOS submission blockers** (new `instructions.md`;
manifest `packageRepo`/`docsUrls` dead-link fixes; **universal multi-arch publish**
`publish.sh``make universal`, registry entry carries both arches with no arch
restriction); enforce-mode drift cleaned from `activateLicense.ts`/`showCredentials.ts`
(enforce retired, `self_license.rs:15`); 5 doc-drift fixes (api 47→54; FK confirmed
`db/mod.rs:29`; `integrate.html` phantom `GET /v1/licenses/{id}/status``POST
/v1/validate`; `v0.2.0.ts` stale header; go README v0.2); **refunds scrubbed from
public docs** (Keysat has no refund feature — out-of-band only, v0.3 revoke-on-refund
hook is a no-op; admin-UI "handle out of band" disclaimers kept). Remotes: keysat-root
→ gitea-only; keysat-docs, daemon, go SDK → GitHub `origin` + gitea backup.
- **`:52`/`:53` = multi-provider/merchant-profile model**: data model + backend
resolution shipped and audited sound; resolution/CRUD query surface has tests.
Both `:54` P0s (provider-injection test seam; Zaprite webhook-forgery re-confirm)
remain fixed; live purchase + settle paths sound.
- **Shipped in `:55` (was the prior session's unshipped P1s)**:
1. **Settle-amount tripwire.** `get_invoice_status` now returns
`ProviderInvoiceSnapshot { status, amount }`; `audit_settle_amount` (shared by
webhook + reconcile issue paths) WARNs + writes an `invoice.amount_mismatch`
audit row on drift, then **issues anyway** (advisory, not a gate — a hard gate
would fight BTCPay payment tolerance). SAT-only: skips non-SAT (fiat sub
renewals) and `None`. Reviewed (caught + fixed a fiat-renewal false-positive).
- **Live**: registry `registry.keysat.xyz` publishes **`0.2.0:55`** — universal
multi-arch, GitHub release `v0.2.0-55`, canonical s9pk at
`files.keysat.xyz/keysat.s9pk`. Migrations 00200022; four SDKs published;
`keysat.xyz` + `docs.keysat.xyz` deployed. Live server `immense-voyage.local`
still runs `:54` until updated from the registry. **This session's work is
committed + pushed to `main` but UNRELEASED — no `:56` cut, not built.** (`:55`
shipped scoped keys, the settle-amount tripwire, and all 4 StartOS submission
blockers — see git log.)
- **Shipped this session (daemon `b088bfc`, pushed origin+gitea; unreleased)** —
**product→merchant-profile write path, closing the multi-profile GAP.**
`Product.merchant_profile_id` + all 4 product SELECTs + `row_to_product`; new
`repo::set_product_merchant_profile` (validates profile exists → 404, not FK 500);
threaded through `CreateProductReq` (post-write) + `UpdateProductReq`
(double-Option, `Some(None)` clears to default); admin SPA profile `<select>`
shown only when >1 profile. Mirrors the entitlements-catalog post-write pattern.
Reviewer pass: approved, no blockers. **Multi-profile is now functional
end-to-end** (resolver was already complete; only the write path was missing).
See `docs/guides/payments.md`.
2. **Scoped API keys wired.** 58 admin endpoints migrated `require_admin`
`require_scope`; 12 sensitive ones stay master-only (issuer key, provider
connect/disconnect, set-password, api-key CRUD, db-info, operator-name,
per-license tier change). `require_scope` re-exported from `api::admin`. Role
boundary tests added. Boundary documented in `api/api_keys.rs` module doc.
- **GAP — multi-profile still non-functional end-to-end**: nothing writes
`products.merchant_profile_id` (INSERT in `create_product_with_currency` omits
it; `update_product_with_currency` has no field; `Product` in `models.rs` lacks
it). Resolver fully supports it; only the product→profile **write path** is
missing. **Gating piece for multi-profile.**
- **Work queue (next, in order)**:
1. **product→merchant-profile picker** (the GAP — add `merchant_profile_id` to
`Product` + `repo.rs` SELECT; `set_product_merchant_profile` writer mirroring
`set_product_entitlements_catalog`; field on `CreateProductReq`/
`UpdateProductReq` applied post-write; profile `<select>` from
`GET /v1/admin/merchant-profiles`, shown only when >1 profile; no migration).
2. 3 other deferred UIs (rail picker, per-profile SMTP, rail-pref editor);
`unlimited_merchant_profiles` on master Pro/Patron policies.
3. Deferred this session (now in Open TODOs): split `audit:read` out of the
blanket `:read` scope; build the admin "API keys" management SPA panel.
1. 3 remaining multi-profile UIs (rail picker, per-profile SMTP, rail-pref
editor); `unlimited_merchant_profiles` on master Pro/Patron policies.
2. Cut `:56` to ship this session's write path to the registry (bump manifest →
`make universal``publish.sh`) — bundle with the UIs above if landing soon.
3. Deferred (now in Open TODOs): split `audit:read` out of the blanket `:read`
scope; build the admin "API keys" management SPA panel.
- **Discovered this session (P2, unfixed)**: `set_product_entitlements_catalog`
has no `rows_affected` guard — a bad product-id silently 200s with stale data
(latent since migration 0014; the new profile writer guards this correctly).
One-line fix, deferred pending the user's call.
- **Known debt (P2 — schedule, not urgent)**: no rate-limit on `/v1/purchase` +
`/v1/redeem`; rate-limit bucket keys on spoofable `X-Forwarded-For` (bypass
+8
View File
@@ -39,6 +39,14 @@ merchant_profiles (1) ──< (N) payment_providers
- **merchant_profile_rail_preferences** — tie-breaker `(profile, rail) → provider`
when a profile has two providers serving the same rail.
Products attach to a profile via `repo::set_product_merchant_profile` (validates
the profile exists → 404, else UPDATE), called **post-write** from the create /
update handlers — same pattern as the entitlements catalog. The admin product form
renders the profile `<select>` only when >1 profile exists; a NULL
`merchant_profile_id` resolves to the default profile (`merchant_profiles::for_product`,
NOT the bare repo query, applies that fallback). On `UpdateProductReq` the field is a
double-Option: `Some(None)` clears back to default-resolution.
**Rails** are buyer-facing methods (`lightning`, `onchain`, `card`), declared per
provider kind via `rails_for_kind` (BTCPay → lightning+onchain; Zaprite →
card+lightning+onchain), NOT stored per row.