Refunds and receipts
Refunds, the receipts collection, and payments for a deleted account.
Refunds
Gemmein takes access back only on the events that plainly mean it: a subscription cancelled or lapsed, or a one-off purchase refunded in full. A full refund revokes only the grants that payment created, so a person who also subscribes keeps their subscription's access.
Every other refund is yours to act on:
- A partial refund leaves access in place, because a small goodwill refund on a large purchase shouldn't take away what someone bought.
- A refund on a subscription invoice touches no access at all. A webhook cannot tell a goodwill refund from a "take it all back" refund.
- When someone cancels at period end and asks for their money back, they keep access until the period ends unless you act. You have two moves, both one click and both audited: cancel the subscription immediately in Stripe, which ends the access the moment the event lands, or revoke it by hand on the person's page.
- Every refund the webhook did not act on is written to your activity view.
Receipts
Gemmein records every payment itself. Buyer, amount,
currency, Stripe reference, what it granted, and every refund against it. That record is
kept outside your collections, so renaming or deleting one can never change what your
revenue was. Each person reads their own copy with await g.purchases.mine();
refunds appear as refundedMinor and a status of part_refunded or
refunded.
A receipts collection is optional. Add one only if you want purchases inside your data model, so your app can list and edit them like any other record. Gemmein's own record above stays the authoritative payment record, and your collections cannot alter it.
Receipts carry { product, item?, status, amountTotal, currency, paidAt,
deliveryUrl?, deliveryFile?, paymentRef } in .data, where
amountTotal is minor units exactly as Stripe reported. You fulfil orders by
editing the receipt from the dashboard (status: "shipped"); your app reads it.
Refunds happen in your Stripe dashboard, and charge.refunded flips the
receipt's status to "refunded".
A receipt is app-owned: its top-level ownerUserId is
null (the webhook wrote it), and the audienceUserId is what scopes
it to the buyer.
That scoping keeps a receipt private. Receipts live in an
addressed collection, so the server narrows every read to the signed-in
buyer: another person calling list() on the same collection gets their own
receipts and nothing else. No request returns yours, whether a crafted filter or a guessed
id. A signed-in buyer's identity comes from the checkout reference Gemmein encoded, and a
guest's from the email Stripe Checkout collected, read from Stripe's signed webhook; never
from anything the browser sent back. So what someone paid for stays theirs
even when the frontend performs no check.
A deleted account
A person who deletes their account and later buys again is a new customer. A payment made
before the deletion grants nothing, whether it arrives from Stripe or from a relay; refund it
in your payment provider. Relays tell before from after by the payment time you map as
paid_at (see Relays). The rows Logs and Errors
keep for these refusals never contain the person's address.
When a member who pays for a plan deletes their account, Gemmein sets every Stripe
subscription they bought to cancel at the end of the period it is paid for: they are never
charged again, and nothing is refunded. One still owing or never paid (past due, unpaid,
incomplete) is cancelled at once, with no refund. Logs records each one. If Stripe refuses,
you get an alert to cancel it in Stripe. If Gemmein can't read the app's payments at that
moment, the deletion answers erase_unavailable and nothing is deleted.
Thanks for telling us.
Thanks for telling us.
Last updated