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.