Skip to content

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->user Transfer was created.
  • CANCELLED — explicitly cancelled by an authorized user (e.g. a wrong amount was issued and a fresh request will be created instead).
  • EXPIRED — expiresAt has 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 ​

EnumerationDescription
PaymentRequestStatusDerived status of a PaymentRequest.

Classes ​

ClassDescription
PaymentRequestA shareable, fixed-amount request to top up a specific user's SudoSOS balance. See the module page for full lifecycle and integration notes.
PaymentRequestControllerThe BaseController class is responsible for: - Storing route definitions. - Generating router objects based on the policy.
PaymentRequestPublicControllerThe BaseController class is responsible for: - Storing route definitions. - Generating router objects based on the policy.

Interfaces ​

InterfaceDescription
BasePaymentRequestResponsePaymentRequest 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-
PaymentRequestStartResponseResponse returned from the "start payment" endpoint. Contains just enough to let the browser redirect into the Stripe Payment Element.
PublicPaymentRequestResponseMinimal 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.