Quran_Tech_Server / docs /payment-workflow.md
aboalaa147's picture
Initial deployment
eb6a2f9
|
Raw
History Blame Contribute Delete
6.73 kB
# 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.percentage` in `application.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.