Key Workflows
This chapter shows how SudoSOS behaves in practice. It does not try to document every endpoint; it explains the flows that keep the system correct.
After reading this page, you should know which entities and services are involved when money moves.
Purchase (create a transaction)
Trigger + actor
- Member buys items (self-service) or a cashier enters the purchase (cashier mode).
API surface
POST /transactions(create)POST /transactions/validate(validate without creating; used by some clients)GET /transactionsandGET /transactions/:id(read)
Entities touched
Transaction,SubTransaction,SubTransactionRow- product/container/POS revisions referenced by rows and sub-transactions
Balancecache (updated from transaction rows and transfers)
Critical checks
- product revisions exist and match the request
- container/POS configuration matches (what is available where)
- debt rules (
canGoIntoDebtfor the buyer) - POS token restrictions for “lesser” POS tokens
Who receives the money
- Revenue is split by
SubTransaction: the container owner is the seller for that part of the purchase. The POS owner and product creator are not used to decide who is paid.
Side effects
- receipts/notifications may be sent by subscribers after persistence
Invoice
Trigger + actor
- Finance/admin groups eligible purchase rows into an invoice. Conceptually this is a top-up paid via bank transfer.
API surface
GET /invoices/eligible-transactions(discover what can be invoiced)POST /invoices(create)GET /invoices/:idandGET /invoices(read)GET /invoices/:id/pdf(PDF)PATCH /invoices/:id(state changes, updates)DELETE /invoices/:id(delete/cancel invoice)
Entities touched
Invoice(and invoice entries)SubTransactionRowlinks to invoices (to prevent double invoicing)- an invoice transfer (void → user) that increases the user balance
Critical checks
- which rows are eligible (and for which user)
- linking/unlinking rows is consistent (no row belongs to multiple invoices)
VAT note
- VAT does not live on the invoice transfer. VAT is recorded via the underlying transactions and is reflected in seller-side settlement (seller payouts).
Deposit (Stripe top-up)
Trigger + actor
- Member tops up their balance via Stripe.
API surface
POST /stripe/deposit(create a Stripe payment intent; checks min/max top-up rules)POST /stripe/webhook(Stripe notifies the backend about payment intent updates)GET /stripe/public(frontend fetches Stripe public key and return URL)
Entities touched
- Stripe payment intent records (backend-owned representation)
- a top-up transfer (void → user) that credits the user when the payment succeeds
- balance cache (derived from transfers)
Critical checks
- webhook event authenticity (signature verification)
- event routing: only accept events intended for this service
- idempotency: the same Stripe event may be delivered multiple times
Terminal Payment (Stripe physical payment)
Trigger +actor
- Customer wants to perform an (anonymous) transaction and pay for it directly.
API surface
POST /terminal-payments(create transaction to be paid, either anonymous (fromUser is POS) or by existing user)POST /terminal-payments/{id}/process(assign terminal payment to Stripe Terminal Reader and start physical transaction)GET /terminal-payments/terminals(get all Stripe Terminal Readers)
Entities touched
TerminalPayment- Everything from
Purchase (create a transaction) - Everything from
Deposit (Stripe top-up)
Critical checks
- Amount is fixed at creation. When transaction needs to be edited, a new TerminalPayment must be created.
- Terminal can only process one TerminalPayment at a time.
Simplified flow
- Temporary transaction is created (as if you are creating an actual transaction).
- Balance is topped up with the amount to be paid for that transaction.
- Temporary transaction is transformed into an actual transaction. Temporary transaction is removed.
Payment request (shareable payment link)
Trigger + actor
- Treasurer (or a user for themself) creates a fixed-amount top-up request with an expiry. The recipient opens a share link (no login), pays via Stripe, and the money is credited to the linked user's balance.
API surface
POST /payment-requests(create; admin can create for anyone, regular users only for themselves)GET /payment-requests(admin only: list all)GET /users/:id/payment-requests(admin for any user, regular users only for themselves)GET /payment-requests/:id(read a single request; admin sees all, user sees own)POST /payment-requests/:id/cancel(cancel a PENDING request)POST /payment-requests/:id/start(authenticated: start a Stripe session)POST /payment-requests/:id/mark-fulfilled(admin escape hatch for out-of-band payment)GET /payment-requests-public/:id(unauthenticated: fetch a trimmed view via share link)POST /payment-requests-public/:id/start(unauthenticated: start a Stripe session via share link)
Entities touched
PaymentRequest(UUID pk, immutable amount, derived status)PaymentRequestAttempt(join table) links aPaymentRequestto eachStripePaymentIntentit spawned — one request can have many intents (retries)- on success: the standard Stripe deposit
Transfer(void → user) already created by the deposit flow also settles the payment request
Lifecycle (derived status)
PENDING→PAIDwhen a linked Stripe intent succeeds.PENDING→CANCELLEDwhen an authorized user cancels.PENDING→EXPIREDwhenexpiresAtpasses without payment.- Status is derived from
paidAt/cancelledAt/expiresAt; precedence isPAID>CANCELLED>EXPIRED>PENDING.
Critical checks
- amount is fixed at creation — a corrected amount means cancelling and creating a fresh request
startPaymentrejects non-PENDINGrequests- the Stripe webhook flow idempotently calls
markPaid: already-paid requests are left unchanged, cancelled requests refuse to flip, and expired-but-settled requests still becomePAID(the money arrived, the clock is secondary)
Invoices vs. payment requests
- Invoices create their own credit transfer on behalf of the user (a "paper-trail top-up").
- Payment requests do not pre-create a credit transfer — the Stripe deposit is the single settlement event. A future "invoice via payment link" flow will compose by leaving the settlement to the payment request.
Payout request (withdraw balance)
Trigger + actor
- Member requests a payout of part of their balance; admin approves and processes it.
API surface
POST /payoutrequests(create request)POST /payoutrequests/:id/status(cancel, approve, process, etc.)GET /payoutrequestsandGET /payoutrequests/:id(read)GET /payoutrequests/:id/pdf(PDF)
Entities touched
PayoutRequestandPayoutRequestStatusrecords- payout transfers (user → void) that move money out of the user account
Critical checks
- user has sufficient balance at request time and at approval time
- only admins can move a request to non-cancel states
- only the requesting user can cancel their own request