Skip to main content

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 as success. However, when other payment methods, such as Stripe, are used, the invoice.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 to Success, and the payment pipeline concludes.

  • If the invoice amount is partially paid using credits, update invoice.payment_status to Processing.

    • This step will operate tables of wallet, payment_transaction, invoice, so wrap these operations in a database transaction.
  • 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 to Processing.
    • Call Stripe payment API and then update payment_transaction.status and invoice.payment_status according to the API call result.
    • If an error occurs during this step, mark invoice.payment_status as Failed, 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, use PaymentIntent.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.
    • Check if PaymentIntent is still Processing. If not, update payment_transaction.status and invoice.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.

E2E Test

  1. [Credits only]
  • Payment was successful.
  • Refund.
  1. [Credits only]
  • The payment failed because there were not enough credits.
  • After adding credits, the payment retry was successful.
  1. [Credits + Card]
  • Payment was successful with multi credits and a card.
  • Refund.
  1. [Credits + Card]
  • Payment failed with multi credits and a card.
  • After add new card, payment retry was successful.
  1. [Credits + ACH]
  • Payment was successful with multi credits and ACH.
  • Refund.
  1. [Credits + ACH]
  • Payment failed with multi credits and ACH.