Spaces:
Sleeping
Sleeping
Payment Workflow
Overview
- Provider: Stripe
- Commission: Platform 15% | Sheikh 85%
- Currency: USD
Payment Status Lifecycle
βββββββββββ
β PENDING β β PaymentIntent created
ββββββ¬βββββ
β
ββββββββββββ΄βββββββββββ
β Stripe succeeds β Stripe fails / expires
βΌ βΌ
ββββββββ ββββββββββ
β PAID β β FAILED β β Can retry
βββββ¬βββ ββββββββββ
β
β Session COMPLETED
β Sheikh payout triggered
βΌ
ββββββββββββββ
β PAYOUT_DONEβ
ββββββββββββββ
PAID/PENDING β REFUNDED (session MISSED or CANCELLED)
Phase 1 β Initiate Payment
Trigger: Student clicks "Pay Now" on a TOPAY session
Component: StripePaymentModal.tsx
POST /api/payments/initiate/card
{
"sessionId": 123,
"studentId": 45,
"sheikhId": 67,
"amount": 50.00
}
Backend logic (PaymentServiceStripe.java):
Receive request
β
βΌ
Check existing Payment record for this session
β
βββ PAID / PAYOUT_DONE β Reject (already paid)
βββ PENDING β Return existing PaymentIntent (idempotent)
βββ FAILED / REFUNDED β Delete old record, create new
βββ No record β Create new
β
βΌ
Create Stripe PaymentIntent
amount = 5000 cents (USD)
β
βΌ
Save Payment record:
amount: $50.00
platformCommission: $7.50 (15%)
sheikhPayout: $42.50 (85%)
paymentStatus: PENDING
transactionId: pi_xxxxx
β
βΌ
Return to frontend:
{ clientSecret, publishableKey, paymentIntentId }
Phase 2 β Frontend Stripe Checkout
Component: StripePaymentModal.tsx
Load Stripe.js with publishableKey
β
βΌ
Mount Stripe CardElement (secure iframe)
β
βΌ
User enters card details
β
βΌ
stripe.confirmCardPayment(clientSecret, {
payment_method: { card: cardElement }
})
β
βββ success β proceed to Phase 3
βββ error β show error to user (can retry)
Phase 3 β Confirm Payment (Backend)
POST /api/payments/confirm-payment/{paymentIntentId}
β
βΌ
Backend retrieves PaymentIntent from Stripe API
β
βββ status: "succeeded" / "requires_capture"
β β
β βΌ
β Payment record β PAID
β Session status β SCHEDULED
β Return success to frontend
β
βββ status: "processing"
β β
β βΌ
β Keep PENDING (Stripe webhook handles later)
β
βββ other status
β
βΌ
Payment record β FAILED
Throw error
Phase 4 β Payout to Sheikh
Trigger: Sheikh calls POST /api/sheikh/sessions/{id}/end
Automatic β no manual action needed
Session ends β status: COMPLETED
β
βΌ
paymentServiceStripe.payoutSheikh(sessionId)
β
βΌ
Validate payment is PAID
β
βΌ
Validate sheikh has Stripe connected account
(sheikhProfile.stripeAccountId)
β
βΌ
Create Stripe Transfer:
amount: $42.50 (85% of $50.00)
destination: sheikh's Stripe account ID
β
βΌ
Payment record β PAYOUT_DONE
Transfer ID saved
β
βΌ
Funds arrive in sheikh's bank within 1β2 business days
> If transfer fails: error is logged, session stays COMPLETED
> Sheikh contacts support for manual payout
Refund Scenarios
| Scenario | Trigger | Result |
|---|---|---|
| Session MISSED β no-show | Auto, 10 min after start | Full refund to student |
| Session auto-cancelled (unpaid) | Auto, 5 min before start | No charge (PaymentIntent voided) |
| Sheikh manually rejects | Sheikh clicks Reject | Full refund if already paid |
Refund Flow:
POST /api/payments/refund/{sessionId}
β
βΌ
Check payment status
β
βββ PENDING (not captured yet)
β β
β βββ Mark REFUNDED locally (no Stripe call needed)
β
βββ PAID
β
βΌ
Create Stripe Refund
amount: full original charge
β
βΌ
Payment record β REFUNDED
Refund timestamp saved
Commission Split Diagram
Student pays $50.00
β
βΌ
βββββββββββββββββββββββββββββββββββββ
β Platform Account β
β β
β $7.50 (15%) Platform fee β
β $42.50 (85%) β Sheikh payout β
ββββββββββββββββββββ¬βββββββββββββββββ
β Stripe Transfer (on session complete)
βΌ
Sheikh's Stripe Account
$42.50
Commission percentage is configurable via
platform.commission.percentageinapplication.yaml(default: 15%)
API Endpoints Reference
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/payments/initiate/card |
Create PaymentIntent, return clientSecret |
| POST | /api/payments/confirm-payment/{paymentIntentId} |
Confirm payment after Stripe.js success |
| POST | /api/payments/confirm-session/{sessionId} |
Sheikh accepts β verify payment is PAID |
| POST | /api/payments/payout/{sessionId} |
Payout sheikh (called automatically on session end) |
| POST | /api/payments/refund/{sessionId} |
Refund student |
Key Notes
- Idempotent: Calling initiate twice for the same session returns the existing PaymentIntent β no duplicate charges.
- No double-pay: Backend checks for existing PAID/PAYOUT_DONE records and rejects the request.
- Auto-retry safe: FAILED/REFUNDED records are deleted and a fresh PaymentIntent is created on retry.
- Payout is non-blocking: A failed Stripe Transfer does not roll back the COMPLETED session status.