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.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. | |