The Guide to Designing and Quality Controlling Payment Logic on Stripe
The implementation of payment logic is important for any product that one way or another deals with transactions. A well-designed payment architecture in couple with a proper testing can save you a lot of time in future.
However, it may take too long to master the proficiency level of working with popular online payment gateways. That's the reason we're writing this guidance on designing payment logic on Stripe.
We'd like to help you better understand this payment gateway. For this, we've prepared interesting use cases, examples from our projects, and a bit of theory with code samples.
This guide is especially useful for QA engineers as they'll understand how to test payment logic based on Stripe. However, Project Managers and developers will also find lots of interesting information here.
How does it work?
Simple payment scheme
The scheme above is relevant for the case when users purchase content through a website or mobile application. It allows paying for the content seamlessly as users don't have to register or link their credit cards with their profiles. All they need to do is enter their credit card credentials and the rest happens behind the scenes:
- Credentials are delivered to Stripe;
- Stripe tokenizes the received data and returns a token to the back-end;
- Back-end creates a charge;
- All this is again sent to Stripe which now shares the received data with payment systems;
- Payment systems respond to Stripe and tell it whether everything alright or report about problems;
- Server gets a response from Stripe reporting about a state of the transaction.
Therefore, users get content if the transaction was successful or receive an error message in case something went wrong. This is the simplest scheme of work.
A while ago, you needed to have a merchant bank account to make the aforementioned process possible. With Stripe it becomes much easier but there are two obligatory conditions:
- You must have a bank account;
- You must be a resident of one of the 25 supported countries.
Countries that are supported by Stripe
Technically, the only restriction Stripe has at the moment is it's geography. It covers entire North America and Australia as well as a part of Europe, South America, and some other countries.
This restriction concerns only to business owners and isn't applicable to consumers. They can still pay for content (and other stuff) that's sold by a merchant wherever they are in the world. While entrepreneurs are able to get payouts only in case their bank account has been opened in one of those 25 counties.
How to connect a card
The procedure of linking your product user with Stripe customer should be done on the server-side and it looks like this:
- 1.Credit card credentials are sent to Stripe from your app or website.
- 2. Stripe returns a token which goes to the back-end.
- 3. Back-end sends it back to Stripe.
- 4. Stripe checks whether such customer exists (if yes, the card is added; if no, the new customer is created and then the card is added).
The first added card is set as a default payment method with the help of which a transaction will be made.
Connect with account
In order for someone could get payments in your application (e.g. Uber drivers), they first have to add an account. Stripe operates with three types of accounts:
- Standard account. This is an already existing account containing all the required credentials. It's registered by the user and validated by Stripe and a bank.
- Express account. This type enables easy onboarding as you can create an account on your own and all that remains for the user is to fill in some necessary fields. Note: This account works within the US only.
- Custom account. This account provides the greatest level of flexibility as it allows you to modify multiple parameters. However, at the same time, you as a platform are responsible for every interaction with your users.
Core features of Stripe
Let's now consider the core features of this payment processor.
Charges
There are two types of charge: direct and destination.
Direct charge
Your platform charges a certain amount from the user and that money goes directly to the linked account of your partner. Again, in the case with Uber, this money will be transferred to drivers. In turn, the direct charge implies that all the fees are paid by the linked account (the driver) and the platform itself charges a fixed percentage from that amount.
Scheme of a direct charge
Destination charge
All the fees are paid by the platform and the net worth is kept on your platform's account. The money is transferred to the Stripe account of your platform afterward the automatic transfer is sent to the partner.
Auth and capture
Stripe has a support for two-step payments which enable you to authorize a charge first and capture it later. In other words, the authorized payments are guaranteed by card issuers and the requested amount is frozen on the customer card. In case the charge isn't captured for this period of time, the authorization is canceled and money is unfrozen.
Uber is a bright example of auth and capture. Users see an approximate cost of their trip when they book a ride. This cost is frozen on their credit card until they finish the ride. Afterward, Uber calculates the final price of the trip and charges it from the card.
This allows app owners to be on the safe side since the app knows for sure whether one or another user is solvent and if there are some problems which may block the transaction.
Transfers
Transfers can be made from the platform account to suppliers. For example, Uber drivers link Stripe accounts with their profiles to get payouts.
Subscriptions
Stripe also supports subscriptions. This feature is very flexible and enables you to set various intervals, different trial periods and do much more to adjust the subscription for your needs.
Refunds
The support for refunds is also present. If customers want to cancel their order, money can be easily refunded to their cards.
Dealing with objects in Stripe
It's time to consider the Stripe objects in more detail.
Source object
Key | Expected value |
---|---|
id | stripe_id of added card |
last4 | last 4 numbers of added card |
brand | credit card company (Visa, AE, etc.) |
exp_month, exp_year | expiration date |
customer | your customer stripe id |
undefined
This object stores a payment method with the help of which a charge will be completed. On the scheme we've considered before, we simply created a source object of the card which isn't linked to any concrete user and so the charge can be made from it. Besides, the source object can be linked with users what gives them an opportunity to store all the payment methods there.
During testing, it's really important to make sure a payment method corresponds with the returned value. For this, you can check last4 and exp_month/year. In case the source object is linked with the specific customer and you want to ascertain it belongs to the right user, you can check the customer id.
Here is a JSON of this object:
{
"id": "card_1CboP4CLud4t5fBlZMiVrzBq",
"object": "card",
"address_city": null,
"address_country": null,
"address_line1": null,
"address_line1_check": null,
"address_line2": null,
"address_state": null,
"address_zip": null,
"address_zip_check": null,
"brand": "Visa",
"country": "US",
"customer": "cus_D1s9PQgvr6U46j",
"cvc_check": "pass",
"dynamic_last4": null,
"exp_month": 4,
"exp_year": 2024,
"fingerprint": "soMjdt25OvcMcObY",
"funding": "credit",
"last4": "4242",
"metadata": {},
"name": null,
"tokenization_method": null
}
Customer object
Key | Expected value |
---|---|
id | customer stripe_id |
default_source | stripe_id of the default card |
sources | the list of Sources |
subscriptions | the list of Subscriptions (check if you use subscriptions) |
undefined
The name of this object speaks for itself. It stores all the payment methods including the default one. The customer object contains information about users and their subscriptions. Stripe recalls users' credit cards and the default payment method that's chosen by them so you can charge users based on this data. The same thing with subscriptions -- Stripe manages them and withdraw subscription fees automatically.
It's possible to create webhooks for all the event in Stripe so the server could find out when the charge was made or when some error took place.
{
"id": "cus_D1s9PQgvr6U46j",
"object": "customer",
"account_balance": 0,
"created": 1528717303,
"currency": null,
"default_source": "card_1CboP4CLud4t5fBlZMiVrzBq",
"delinquent": false,
"description": null,
"discount": null,
"email": null,
"invoice_prefix": "4A178DE",
"livemode": false,
"metadata": {},
"shipping": null,
"sources": {
"object": "list",
"data": [
{
"id": "card_1CboP4CLud4t5fBlZMiVrzBq",
"object": "card",
"address_city": null,
"address_country": null,
"address_line1": null,
"address_line1_check": null,
"address_line2": null,
"address_state": null,
"address_zip": null,
"address_zip_check": null,
"brand": "Visa",
"country": "US",
"customer": "cus_D1s9PQgvr6U46j",
"cvc_check": "pass",
"dynamic_last4": null,
"exp_month": 4,
"exp_year": 2024,
"fingerprint": "soMjdt25OvcMcObY",
"funding": "credit",
"last4": "4242",
"metadata": {},
"name": null,
"tokenization_method": null
},
{
"id": "card_1CcC3uCLud4t5fBlW2UMknUW",
"object": "card",
"address_city": null,
"address_country": null,
"address_line1": null,
"address_line1_check": null,
"address_line2": null,
"address_state": null,
"address_zip": null,
"address_zip_check": null,
"brand": "Visa",
"country": "US",
"customer": "cus_D1s9PQgvr6U46j",
"cvc_check": "pass",
"dynamic_last4": null,
"exp_month": 4,
"exp_year": 2024,
"fingerprint": "soMjdt25OvcMcObY",
"funding": "credit",
"last4": "4242",
"metadata": {},
"name": null,
"tokenization_method": null
}
],
"has_more": false,
"total_count": 2,
"url": "/v1/customers/cus_D1s9PQgvr6U46j/sources"
},
"subscriptions": {
"object": "list",
"data": [],
"has_more": false,
"total_count": 0,
"url": "/v1/customers/cus_D1s9PQgvr6U46j/subscriptions"
}
}
Charge object
Key | Expected value |
---|---|
id | charge stripe_id |
amount | payment amount in cents |
amount_refunded | refunded amount in cents |
customer | customer_id of a payer |
captured | true - payment is made, false - payment is authorized |
destination | stripe account of payee |
amount
During testing, it's important to check the amount that was charged from the user. The amount is represented in cents, euro cents, etc.
amount_refunded
This field will have a value different from zero only if the whole amount of transaction (or a part of it) was refunded to the customer.
customer
The id of your customer is stored here.
captured
This key indicates a status of the transaction. The money can be held on the user credit card or can be charged.
destination
If you make use of destination charges, then the destination key will store a Stripe account of the user you've sent that money to.
"fingerprint": "soMjdt25OvcMcObY",
"funding": "credit",
"last4": "4242",
"metadata": {},
"name": null,
"tokenization_method": null
},
"source_transfer": null,
"statement_descriptor": null,
"status": "succeeded",
"transfer_group": null
}
Refund object
Key | Expected value |
---|---|
id | refund stripe_id |
amount | payment amount in cents |
status | success / pending / failed |
undefined
Refund object can be embedded in the charge object if any part of the payment or the whole payment was refunded to a customer.
{
"id": "re_1CcY10CLud4t5fBlN23KtYq7",
"object": "refund",
"amount": 999,
"balance_transaction": "txn_1CcY10CLud4t5fBlhlmzzJuK",
"charge": "ch_1CcD7dCLud4t5fBlC1srZNIB",
"created": 1528892634,
"currency": "usd",
"metadata": {},
"reason": null,
"receipt_number": null,
"status": "succeeded"
}
Transfer object
Key | Expected value |
---|---|
id | transfer_id |
amount | payout amount in cents |
destination | linked account of a payee |
reversed | false - money transaction, true - reverse of funds |
reversals | list of reverse transfer objects |
undefined
This object stores the data related to transfers from the platform balance to other accounts. In other words, all the payouts to your partners (e.g. Uber drivers) will be recorded and stored in this object.
Remember that all transactions should be loginized in the database. This makes testing easier as you can see the transfer id, go to Stripe, and make a query to look through parameters for checking:
amount
The amount that will be paid to a payee.
destination
Destination unveils a Stripe account of the user to whom a payment will be sent.
reversed
Sometimes it's necessary to cancel a transaction. This key acts as an indicator showing a false value if the transaction succeeded and a true value if it's reversed.
reversals
This key stores a list of the objects in case any part of your transfer was reversed.
{
"id": "tr_1CcApyCLud4t5fBlZyx5mEPI",
"object": "transfer",
"amount": 250,
"amount_reversed": 0,
"balance_transaction": "txn_1CcApyCLud4t5fBlfA5cgXBz",
"created": 1528803538,
"currency": "usd",
"description": null,
"destination": "acct_18bAS3KcT341ksb9",
"destination_payment": "py_1CcApyKcT341ksb9VawxIJdS",
"livemode": false,
"metadata": {},
"reversals": {
"object": "list",
"data": [],
"has_more": false,
"total_count": 0,
"url": "/v1/transfers/tr_1CcApyCLud4t5fBlZyx5mEPI/reversals"
},
"reversed": false,
"source_transaction": null,
"source_type": "card",
"transfer_group": null
}
Balance Transaction object
Key | Expected value |
---|---|
id | refund stripe_id |
amount | payment amount in cents (pay attention to +/- signs) |
available_on | date when money will be available for a payee |
fee | amount of Stripe fee |
fee_details | list of fee objects |
net | amount of net income/expenditure |
status | current status of operation |
type | type of transaction (charge, refund, transfer) |
undefined
This object contains information about any changes to the application balance. Basically, you don't need to test this object, but you have to understand where fees come from.
amount
The payment amount in cents. It can be followed by a minus or plus which indicate the process of sending and receiving funds respectively.
available_on
The money sent to partners will be available for them through a certain period of time. This key contains timestamps telling when the fund will be available.
fee
An amount of the Stripe fee.
fee_details
The list of fee objects with a detailed description of why one or another fee was charged.
net
The amount of net income.
status
The status of operation success.
type
This key stores a type of the object (charge, refund, transfer).
Balance transaction for transfer:
{
"id": "txn_1CcApyCLud4t5fBlfA5cgXBz",
"object": "balance_transaction",
"amount": -250,
"available_on": 1528803538,
"created": 1528803538,
"currency": "usd",
"description": null,
"exchange_rate": null,
"fee": 0,
"fee_details": [],
"net": -250,
"source": "tr_1CcApyCLud4t5fBlZyx5mEPI",
"status": "available",
"type": "transfer"
}
Balance transaction for charge:
{
"id": "txn_1CbrRTCLud4t5fBlhRfMLdq1",
"object": "balance_transaction",
"amount": 10000,
"available_on": 1529280000,
"created": 1528728983,
"currency": "usd",
"description": "Charge user asdf11@example.com for instructor sodom@example.com lesson id: 77",
"exchange_rate": null,
"fee": 320,
"fee_details": [
{
"amount": 320,
"application": null,
"currency": "usd",
"description": "Stripe processing fees",
"type": "stripe_fee"
}
],
"net": 9680,
"source": "ch_1CbrP3CLud4t5fBlztHMxVzv",
"status": "pending",
"type": "charge"
}
Subscription object
Key | Expected value |
---|---|
id | subscription stripe_id |
application_fee_percent | % that is charged by the app for subscription |
billing | automatic charge or sending invoice |
billing_cycle_anchor | time of the next cycle of subscription |
current_period_start current_period_end | timeframes of current subscription period |
plan | set of rules for subscription: amount, interval, number of trial days |
undefined
This object contains information about the customer subscription. If you need to test Stripe subscriptions, then you should look through the subscription object and check the following:
application_fee_percent
This is a percent of the overall amount the app charges as soon as the subscription is made. The rest is paid to the content owner.
billing
This key is responsible for how the billing process is organized. Everything can be done automatically or manually through the invoice.
billing_cycle_anchor
It contains the due date of the next payment for renewal of the subscription.
current_period_start & current_period_end
The validity period of the customer subscription.
plan
It stores the object of a subscription plan. This key includes a set of rules like the amount one should pay for the subscription, interval, number of trial days, etc.
{
"id": "sub_D2JskPBqcW24hu",
"object": "subscription",
"application_fee_percent": null,
"billing": "charge_automatically",
"billing_cycle_anchor": 1528820423,
"cancel_at_period_end": false,
"canceled_at": null,
"created": 1528820423,
"current_period_end": 1531412423,
"current_period_start": 1528820423,
"customer": "cus_D2Jsi3JgT5zPh1",
"days_until_due": null,
"discount": null,
"ended_at": null,
"items": {
"object": "list",
"data": [
{
"id": "si_D2Js7N4mYxzAaY",
"object": "subscription_item",
"created": 1528820424,
"metadata": {
},
"plan": {
"id": "ivory-express-917",
"object": "plan",
"active": true,
"aggregate_usage": null,
"amount": 999,
"billing_scheme": "per_unit",
"created": 1528819224,
"currency": "usd",
"interval": "month",
"interval_count": 1,
"livemode": false,
"metadata": {
},
"name": "Ivory Express",
"nickname": null,
"product": "prod_D2JYysdjdQ2gwT",
"statement_descriptor": null,
"tiers": null,
"tiers_mode": null,
"transform_usage": null,
"trial_period_days": null,
"usage_type": "licensed"
},
"quantity": 1,
"subscription": "sub_D2JskPBqcW24hu"
}
],
"has_more": false,
"total_count": 1,
"url": "/v1/subscription_items?subscription=sub_D2JskPBqcW24hu"
},
"livemode": false,
"metadata": {
},
"plan": {
"id": "ivory-express-917",
"object": "plan",
"active": true,
"aggregate_usage": null,
"amount": 999,
"billing_scheme": "per_unit",
"created": 1528819224,
"currency": "usd",
"interval": "month",
"interval_count": 1,
"livemode": false,
"metadata": {
},
"name": "Ivory Express",
"nickname": null,
"product": "prod_D2JYysdjdQ2gwT",
"statement_descriptor": null,
"tiers": null,
"tiers_mode": null,
"transform_usage": null,
"trial_period_days": null,
"usage_type": "licensed"
},
"quantity": 1,
"start": 1528820423,
"status": "active",
"tax_percent": null,
"trial_end": null,
"trial_start": null
}
Use cases
Let's consider the use of Stripe capabilities for building business logic.
Subscriptions
undefined
Description: Users pay $10/month for using the content. The author of the content earns 80% of the overall subscription cost. Users have 7 trial days.
Implementation:
- 1. Create the subscription plan in Stripe where you specify cost, % of app fee, and interval.
- 2. Integrate webhooks in order for the server could understand when someone subscribes and when the charge is made.
- 3. Integrate emails to send users an invoice or receipt about successful payments.
- 4. When a user purchases the subscription, Stripe counts down 7 days from that moment and makes a charge.
- 5. Author gets money, the platform gets a fee.
Fee: 2.9% + 30c
One-time content purchase
undefined
Description: Users can purchase a content on your website or app.
Implementation:
- 1. The customer tokenizes card.
- 2.Back-end executes Charge.
- 3. If the Charge has been successful, the platform's business logic comes into play and allows the customer to get the purchased content.
In this case, you'll pay the following fees: 2.9% from the Charge cost + 30 cents.
Uber-like app
undefined
Description: Customer pays for the service, the platform charges 20%, the driver gets 80%.
Preconditions:
- Driver linked an account
- User added a card
This variant implies you create transfers on your own after the user has paid. For this, you should authorize the payment during booking of the ride and perform a capture as soon as the ride is finished. Afterward, you create a transfer for the driver that's equal to 80% of the total amount, pay the Stripe fee, and the rest is your net income.
Fee: 2.9% + 30 cents
Uber-like app #2
undefined
Apps like Uber are the perfect example to describe Stripe use cases, so let's consider one more variation.
Description: Customer pays for the service, the platform charges 20%, the driver gets 80%. In addition, the driver can pay $5 for the priority booking right.
Preconditions:
- Driver linked an account
- User added a card
Variant #1
You charge $5 from the driver because the priority option is chosen, authorize payment for the customer, perform a capture as the ride ends, create a transfer for the driver, and keep the rest. In this case, you pay a 2.9% fee and 30 cents for each charge (customer and driver = $1).
By the way, here is a checklist to help you estimate how much does it cost to develop an app like Uber.
Variant #2
You can escape from paying fees by creating the inner monetization on your platform. For example, you can introduce an in-app balance for the driver. This way when you receive money from the customer, you calculate the driver's share and then transfer those funds to the inner balance. Next, add the button for making payouts to the app or implement automatic payouts (e.g. once a week as Uber does).
Cashflow for the variant #2
Load and stability tests
You can also perform the load testing in order to check how many transactions Stripe can process and if there any rate limit. Such tool as GMeter will be in handy for you during testing these parameters.
So, how to test all this? Create the event launching the payment logic on the server-side. Next, you check the log of the transaction in your database and check it's status using GMeter (don't forget that all transactions have to be loginized in the database). Then, you can emulate all those actions from N-users and define an approximate number of successful operations per unit time (e.g. 10 operations a minute). As a result, you can conclude whether the logic optimization is necessary or not. The good thing is that this approach enables to test the combination of Stripe with the server-side logic of your platform.
Wrapping up
We hope this guide will help you in designing and testing Stripe-based payment logic! If you've liked this piece of writing, you can subscribe to our newsletter and get more useful content to your inbox once a week.
Also, feel free to contact our managers if you have some questions regarding cooperation with our company.
Comments