June 2020
Location Quirks and DRY Code
Bounce is a platform where people pay to chat with other people. Everything happens in real-time, and is billed on a per-minute basis.
Seems fairly straightforward. We’ll create a Payment Intent (which is Stripe’s newer way of saying charge) after each conversation takes place.
Using Stripe Connect, we can even specify transfer_data
— this allows us to transfer money directly from student to teacher.
First Complication
Bounce’s network is global. A teacher in Mexico can help a student in Japan. Luckily, Stripe handles most of the heavy-lifting here.
However, Stripe Connect transactions have varying requirements depending on the location of the teacher. This is probably due to some combination of local regulations, fraud protection, and other things we don’t need to worry about.
So, before we create a Payment Intent, we need to know where the teacher is located — or more specifically, where her bank account is located.
Stripe has this information — and instead of making an API call to Stripe before creating every payment intent (to find the user’s location), we’ll save the location in our database.
Location Differences
From Stripe’s docs:
Since cross-border transfers are not supported in countries outside your region, some accounts will need to use the card payments capability and act as the settlement merchant to process payments.
So, when the teacher’s country differs from the platform country (this is our location, USA), the platform can no longer act as a middle man in the transaction — the teacher becomes the settlement merchant.
This distinction may seem confusing and boring, so we won’t linger here, other than to say that we need to specify something about card payments later on, when the teacher’s country differs from ours.
Payment Intents
Here’s what a generic Payment Intent might look like in Ruby:
pay_int = Stripe::PaymentIntent.create({
amount: amount,
customer: student.stripe_student_id,
currency: 'usd',
payment_method: customer_payment_method,
off_session: true,
confirm: true,
transfer_data:{
destination: teacher.stripe_teacher_id,
amount: transfer_amount
}
})
The first few parameters (amount
, customer
, currency
, payment_method
) are fairly straightforward.
off_session: true
means the student isn’t present in the checkout flow, which is half true for us — technically he might be present, but his saved card is automatically charged when a session ends.
confirm: true
means it’s go time, we’re ready to charge.
transfer_data
is the crucial parameter — this is how the platform operates, by transferring money from one person to another (minus the platform fee).
Cross border transfer
If a Mexican teacher helps a Japanese student, we need to use the card payments capability. Stripe Connect will automatically enable this for international accounts in the admin (for Express accounts, at least).
As mentioned in the docs, card payments allows teachers to receive funds directly from students, without the platform acting as an intermediary.
If you’re wondering why we don’t treat domestic transfers this way — the reason is that the account setup for a teacher is significantly more of a pain when they need to act as a settlement merchant. If the platform acts as the settlement merchant, things are easier for everyone involved.
Card Payments are already enabled in the admin (for international teachers), but we also have to specify this directly in the Payment Intent. To do this, we include an additional parameter on_behalf_of
— this specifies that the teacher will be the settlement merchant for this transaction.
However, we don’t want to have one Stripe::PaymentIntent.create
flow for US teachers and another Stripe::PaymentIntent.create
for International teachers, because we’d be repeating ourselves and risking a DRY violation.
Ruby’s tap
tap
is a ruby method that allows us to … tap into attributes of an object or the keys of a hash.
For example, when creating a new user, we could do something like this:
user = User.new.tap do |u|
u.username = "horat22"
u.name = "Horatio"
u.location = "Earth"
u.save
end
This mini-bind that we’re in with Stripe presents a great opportunity for tap
, since we can conditionally decide whether to add a particular attribute.
And we only want to include the on_behalf_of
sometimes, in the case of the cross-border payment mentioned above.
obj = {
amount: amount,
customer: student.stripe_student_id,
currency: 'usd',
payment_method: customer_payment_method,
off_session: true,
confirm: true,
transfer_data:{
destination: teacher.stripe_teacher_id,
amount: transfer_amount
}
}
obj.tap do |hash|
hash[:on_behalf_of] = teacher.stripe_teacher_id if not_us(teacher.country)
end
pi = Stripe::PaymentIntent.create(obj)
We have a helper method not_us
def not_us(country)
country != 'US' && !country.nil?
end
which returns true
if the teacher’s bank account is not located in the USA.
To recap - we first tap
into our obj
which contains the Payment Intent data. Then we decide whether we want to add :on_behalf_of
. Then, we pass obj
into Stripe::PaymentIntent.create
.
Ruby’s tap
has allowed us to maintain a single payment flow, and keep our code DRY.
Conclusion
That’s all for now.