Payment
Payment and Refund.
Payment
Payments are processed through the billing integration, which currently supports only Stripe. All payments are executed automatically, requiring no action from the buyer.
Payment trigger
After an invoice is issued and becomes FINALIZED
status, the payment engine will automatically launch payment pipeline for the invoice.
Payment transactions
A payment for an invoice may involve multiple transactions, as several credits from the buyer's wallet can be applied alongside payments made through a provider like Stripe.
Payment status
Payment has the following 4 statuses.
- Pending: The payment has not started yet.
- Processing: The payment is in process.
- Success: The payment was successful.
- Failed: The payment has failed.
Invoice's payment status is stored in invoice.payment_status
, and each payment transaction has its own status.
invoice.payment_status
:
- The
invoice.payment_status
will always align with the status of the latest payment transaction. - For credit payments, the status of the payment transaction will always be
success
. - If the total invoice amount is covered using credits, the
invoice.payment_status
will be marked assuccess
. However, when other payment methods, such as Stripe, are used, theinvoice.payment_status
will depend on the success or failure of the payment transaction with the provider. - When using payment providers like Stripe, transaction failures may occur, resulting in multiple transaction records. The
invoice.payment_status
will be updated to reflect the status of the latest transaction, ensuring an accurate and current representation of the invoice’s payment status.
Payment pipeline
-
Check invoice status, payment status, due date and amount.
-
Check invoice payment situation by its payment transactions.
-
Get buyer's wallets.
-
If credits are available, deduct credits first.
-
If the entire invoice amount is paid using credits, set
invoice.payment_status
toSuccess
, and the payment pipeline concludes. -
If the invoice amount is partially paid using credits, update
invoice.payment_status
toProcessing
.- This step will operate tables of
wallet
,payment_transaction
,invoice
, so wrap these operations in a database transaction.
- This step will operate tables of
-
If there is a remaining amount that needs to be paid by the payment provider, call the Stripe payment API.
- Select a valid payment method from the buyer’s wallets.
- Create a new record in table
payment_transaction
with status set toProcessing
. - Call Stripe payment API and then update
payment_transaction.status
andinvoice.payment_status
according to the API call result. - If an error occurs during this step, mark
invoice.payment_status
asFailed
, so it can be retried.
Payment retry
If the invoice payment fails, the seller can manually retry the payment through the Suger console.
- A portion of the invoice amount may have already been paid using credits, which will be deducted first during the retry process.
- During the retry, credits will continue to be prioritized for payment, with the remaining balance paid through the payment provider.
Payment dispute
If a dispute occurs, Stripe will handle this process. Suger will receive relevant webhook notifications from Stripe and then display the dispute in the corresponding payment transaction record on the Suger console.
More about it: Stripe doc
Abnormal payment alert
In certain situations, the payment status could be incorrect:
- Database operations may fail or be disrupted due to bugs or service issues.
- Suger might miss some Stripe webhook events.
To deal with these abnormal payments, Suger will regularly check the payments with status processing
and compare them with the transaction status on Stripe to correct any potential issues.
Pipeline:
- Loop through each organization.
- Get invoices where the payment due date is within the last 3 days.
- Get invoices issued within the last 3 days with a
payment_status
still in Processing.- Find the latest payment transaction of the invoice.
- If there is no payment transaction, mark as failed because it is abnormal.
- If
payment_transaction.status
is not Processing, mark as failed because it is abnormal. - Retrieve the Stripe PaymentIntent of the transaction by Stripe API.
- If
payment_transaction.Info.StripePaymentIntent
is not nil, usePaymentIntent.ID
directly. - Or else search Stripe PaymentIntents by payment transaction id. If the search results are empty or if there are multiple records, mark as failed because it is abnormal.
- If
- Check if PaymentIntent is still Processing. If not, update
payment_transaction.status
andinvoice.payment_status
.
Refund
Suger supports refund for a successful payment, and you need to initiate the refund manually in the Suger console.
- All payments, including credit and other methods, can be refunded.
- A single payment can be refunded multiple times, with each refund being a partial amount.
- Refunds will be returned to the payment method used at the time of payment.
- The refund process is done at the transaction level, not the invoice level.
Credit refund
- The refund of credits will be returned to the credit wallet used at the time of payment.
- Refunds are only accepted when the credit wallet is in active status.
- Credit refunds will always succeed.
Stripe payment refund
- The payment method for accepting refunds must be valid. If it has already expired or has been removed from the buyer's wallet, the refund cannot be processed.
- A refund will create a new payment transaction record for easy reference of refund history and its status.
- Refunds will be made using the Stripe SDK, and if this step fails, the refund will be marked as a failed.
ACH Debit Refund
If you request a refund for a payment that hasn’t completed yet (within a few hours of creating the Payment Intent), Stripe doesn’t submit the charge to the bank, essentially cancelling the original payment rather than refunding it.
Stripe doesn’t explicitly label ACH Direct Debit refunds as refunds when we deposit the funds back to a customer’s bank account. Instead, we process refunds as a credit and include a reference to the statement descriptor for the original payment.
More about it: Stripe doc
Abnormal refund alert
Similar to payment, Suger regularly checks for refunds in processing
status to ensure they align with the status on Stripe.
Pipeline:
- Loop through each organization.
- Get refunds created in the last 3 days of the organization.
- If a Stripe refund transaction is still in Processing, check it.
- Retrieve stripe Refund object by Stripe API.
- If
transaction.Info.StripeRefund
exists, use the refund id and get refund object from Stripe. - If
transaction.Info.StripeRefund
is nil, it means we failed to update the database after calling the Stripe API. Search for Stripe refunds with PaymentIntent id using Stripe API.- Filter the target refund object by transaction id in meta, because one PaymentIntent can have multiple refund objects.
- Check if the Refund is still in Processing, if not, update
payment_transaction.status
.
- If
- Retrieve stripe Refund object by Stripe API.
E2E Test
- [Credits only]
- Payment was successful.
- Refund.
- [Credits only]
- The payment failed because there were not enough credits.
- After adding credits, the payment retry was successful.
- [Credits + Card]
- Payment was successful with multi credits and a card.
- Refund.
- [Credits + Card]
- Payment failed with multi credits and a card.
- After add new card, payment retry was successful.
- [Credits + ACH]
- Payment was successful with multi credits and ACH.
- Refund.
- [Credits + ACH]
- Payment failed with multi credits and ACH.