SudoSOS Back-end API / stripe / payment-request
Stripe: Payment Request ​
This is the module page of the payment-request.
A PaymentRequest is an immutable, unauthenticated, shareable request to top up a specific user's balance with a specific amount before a specific deadline. It is the domain primitive behind what end-users perceive as a "payment link".
Lifecycle ​
A request is created in PENDING state with an expiresAt and a fixed amount. From there it transitions to exactly one of:
PAID— a linked StripePaymentIntent succeeded and the corresponding void->userTransferwas created.CANCELLED— explicitly cancelled by an authorized user (e.g. a wrong amount was issued and a fresh request will be created instead).EXPIRED—expiresAthas passed without payment.
Status is derived, not stored — see PaymentRequest.status. The only stored state is paidAt, cancelledAt, and expiresAt.
Stripe relationship (one-to-many on the inverse side) ​
A request may have many StripePaymentIntent rows over its lifetime — each call to startPayment mints a fresh intent (the user can abandon a session and try again). The request becomes "paid" when at least one of those intents reaches SUCCEEDED and its StripeDeposit.transfer exists.
Amount immutability ​
The amount is fixed at creation time and cannot be mutated. The user- facing email/PDF mentions a specific euro figure; if SudoSOS were to recompute the figure at pay-time (against a moving balance), the email and the actual charge would diverge. To correct a wrong amount, cancel the request and create a new one — never mutate.
Always credits balance on success ​
A successful payment always results in a standard void->user Transfer that credits the recipient's balance. PaymentRequest is a top-up primitive — callers that do not want a balance credit (e.g. a future "invoice via payment link" flow) must not pre-create their own credit Transfer; the Stripe deposit is the single settlement event.
Admin escape hatch ​
Reality intrudes: occasionally a user pays a payment-link's invoice via bank transfer instead of iDeal. The admin endpoint POST /payment-requests/:id/mark-fulfilled creates the credit Transfer manually (with a reason for audit) — this credits the recipient's balance exactly the same way a Stripe payment would — and flips the request to PAID without a Stripe deposit existing.
Out of scope (tracked elsewhere) ​
- Linking back to Invoice (
Invoice.paymentRequest?lives on the invoice side). - Settlement (positive-balance payouts) for disabled users — a separate primitive, not this one.
- Stripe refund handling.
Enumerations ​
| Enumeration | Description |
|---|---|
| PaymentRequestStatus | Derived status of a PaymentRequest. |
Classes ​
| Class | Description |
|---|---|
| PaymentRequest | A shareable, fixed-amount request to top up a specific user's SudoSOS balance. See the module page for full lifecycle and integration notes. |
| PaymentRequestController | The BaseController class is responsible for: - Storing route definitions. - Generating router objects based on the policy. |
| PaymentRequestPublicController | The BaseController class is responsible for: - Storing route definitions. - Generating router objects based on the policy. |
Interfaces ​
| Interface | Description |
|---|---|
| BasePaymentRequestResponse | PaymentRequest is UUID-keyed. To avoid overloading the id slot (which on other responses is the numeric DB id), the UUID is exposed under a semantically named field, mirroring how QRAuthenticator exposes its UUID as sessionId. The response therefore extends BaseResponseWithoutId (timestamps + version, no id) and declares a uuid of its own. |
| CreatePaymentRequestRequest | - |
| MarkFulfilledExternallyRequest | - |
| PaginatedBasePaymentRequestResponse | - |
| PaymentRequestStartResponse | Response returned from the "start payment" endpoint. Contains just enough to let the browser redirect into the Stripe Payment Element. |
| PublicPaymentRequestResponse | Minimal response returned to unauthenticated callers of the public share link endpoint. It deliberately omits createdBy, cancelledBy, and other internal audit fields — anyone with the link can see it, so we leak as little user info as possible. |