Skip to content

No billing history: a paying user cannot see what they bought, when, or why their limits dropped #509

Description

@KruGoL

The wallet can buy a subscription but never shows what was bought. Three
questions a paying user will ask have no answer anywhere in the UI:

  • "I paid — where is my order?" If the tab was closed and the order id is
    gone, the only remaining trace is Paymento's email. order-status is keyed by
    orderId alone and there is no list endpoint.
  • "What did I buy, and when?" Nothing records it. Settings → Subscription
    shows the CURRENT plan, its expiry and two usage bars — no history.
  • "Why did my limits drop?" The gateway lazily demotes a lapsed paid
    subscription to free with no error — silently lower limits.
    PlanDowngradeWatcher catches it and opens the upgrade modal with the
    'expired' banner, but nothing says which plan expired or when.

The data already exists

paymento_orders carries order_id, token, plan_id, email,
price_cents, status, status_name, api_key / upgrade_api_key,
key_revealed, settlement_json, fulfilled, awaiting_confirmation,
created_at, updated_at.

GET /api/paymento/order-status?orderId=… already returns orderId, status
(created | pending | paid | failed), statusName (raw Paymento, incl.
UserCanceled), fulfilled, confirming, settlement (on-chain details from
/verify), upgrade, maskedKey and planName. GET /api/paymento/key-info
(by x-api-key) returns maskedKey, planName, subscriptionState
(active | expired | inactive) and activeUntil.

So date, plan, amount, status, settlement and expiry are all available today.
What is missing is a way to ask for them per wallet, and a surface to show them.

The purchase IS linkable to the wallet on the normal path

PlanScreen.startCheckout always asks for an in-place upgrade of the key the
wallet already holds ("same key, new plan, fresh 30 days"); a fresh key is
minted only against a pre-upgrade gateway or when the user explicitly picks
forceNewKey. That existing key is the identity-bound one provisioned at
onboarding through /auth/verify, which carries
api_keys.owner_id = "<network>:<pubkey>".

So for a normal purchase the chain already closes server-side:

paymento_orders.upgrade_api_key → api_keys.owner_id → network:pubkey

No schema change is needed to answer "the orders belonging to this wallet" for
the common case. Only a fresh-key purchase stays unlinked (api_keys rows
created outside getOrCreateOwnerKey leave owner_id NULL).

Options

A. Local ledger (wallet only). Stop deleting the PendingOrderRecord
(src/sdk/subscription/pendingOrder.ts, added by #504) at terminal status;
fold it into a compact completed entry — order id, plan name, price, created,
settled, status, key mask, never the key — and render Billing from it. No
gateway change, works for fresh-key purchases too. Cost: the history lives in
one browser and cannot be recovered after a wipe or on another device.

B. Server-side history endpoint (gateway). A read endpoint listing orders
whose upgrade_api_key belongs to the caller's owner key. Covers the normal
path with no schema change; survives reinstalls and works across devices.

Authentication must be the pubkey challenge (/auth/challenge +
/auth/verify), NOT x-api-key.
Keys are deliberately portable — the wallet
offers "Use a different key" and the timeout path offers "I have a key" — so a
key-authenticated endpoint would hand anyone holding a pasted key the buyer's
order history and their email. Signature auth exposes nothing new: only the
holder of the private key can read, and they already know both halves.

C. Bind fresh-key purchases too. The wallet already calls
order-key-ack?orderId=… after storing a purchased key durably. Signing that
call would let the gateway set owner_id on the purchased key. Note two costs:
it ends the key's portability, and api_keys has a UNIQUE partial index on
owner_id, so a wallet cannot own two keys as the schema stands.

Recommended: A + B — the ledger for the surface and for offline/fresh-key
coverage, the endpoint so the history survives a reinstall. C only if key
portability is being dropped anyway.

Two related facts the page should state honestly

  • There is no cancellation and no refund. The Paymento store has no cancel
    endpoint; payment_sessions / refund_amount belong to the legacy
    /api/payment/* flow, not to this one. "Cancel this payment and start over"
    sets a local abandonedAt only — the order lives on and can still settle
    (which is Cut the abandon/background-settlement machinery: a recovered key from a cancelled order has nowhere to land #508). Nothing to cancel in the subscription sense either: it is
    not recurring, it simply expires after 30 days.
  • A downgrade costs the remaining days. isPlanSelectable blocks only
    re-buying the CURRENT active plan, so a cheaper plan is purchasable; on
    fulfilment changePlan(key, planId, expiry) sets the new plan with a fresh
    30 days and no proration. Buying down silently discards whatever was left.
    Either warn in the UI or refuse the move while a paid plan is active.

Bonus: this gives #508's orphaned key a home

#508 describes a key delivered by a cancelled-but-settled order that has
nowhere to land: the toast points at Settings → Subscription, which cannot
reach it, so a paid key is announced and then lost. A Billing list is exactly
that missing home — the order appears as a row with "key delivered" and an
"install this key" action.

Out of scope, worth filing separately

The browser learns about payment only by polling: the gateway has an IPN
callback from Paymento and its own 5-minute re-verification for 24h, but no
SSE or WebSocket anywhere, so the page updates on the next poll tick rather
than when the payment confirms.

Related: #501, #504, #508.


Where the gateway-side half is tracked

Everything this page needs from the SGW — the per-wallet order listing, a push
so the screen reacts when a payment confirms, cancellation, proration and an
expiry signal — is collected in
unicitynetwork/aggregator-subscription#83. Options B and C above are items 1
and 6 there; option A (the local ledger) needs nothing from the gateway and can
ship on its own.

The downgrade warning this page would have to explain is filed separately as
#510.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

  • Status
    In progress

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions