stripe-scala, API for Stripe Using Scala
stripe-scala is a wrapper over the Stripe REST api. Unlike stripe-java, stripe-scala binds JSON response to the stripe object models (using Scala case classes) and lets you create requests from typed case classes (rather than just using Java Map<String,Object>
)
Libraries Used
- circe for JSON (circe provides compile time macros for reading/writing JSON from/to scala case classes). It also provides a very powerful API for validating/querying JSON
- akka-http for making HTTP requests
- akka-stream-json for streaming JSON
- ficus for providing config (via typesafe-config)
- enumeratum for providing typesafe enumerations on stripe enum models as well
stripe-scala was intentionally designed to use bare minimum external dependencies so its easier to integrate with scala codebases
Installation
Currently, stripe-scala is in pre-1.0 stage. It is powering the payment processing of at least one company in production but not all endpoints are completed.
It has been deployed to Maven Central, so to install it add the following to your build definition:
libraryDependencies ++= Seq(
"org.mdedetrich" %% "stripe-scala" % "0.2.1"
)
To get the latest version please check the Maven repository search.
TODO for 1.0 release
- Add all operations for all endpoints
- Add tests
- Shade jawn/enumeratum if possible. These dependencies don't need to be exposed to users
- Document Stripe API with ScalaDoc
- Figure out how to deal with list collections
- Figure out how to deal with error handling
- Implement a single instance of all operation types to figure out if there are any potential issues
- get
- create
- update
- list
- delete
- Clean up/refactor code (still a lot of duplication)
- Webhooks/Events
Examples
There are integration tests that show how the library is intended to be used.
- Create token for a credit card and charge it
- Create a customer, add token and charge it
- Create managed/connected account and payout money to it
- Payout money to a connected account
- Refund a charge
Usage
Stripe Api key and url endpoint are provided implicitly by using the org.mdedetrich.stripe.ApiKey
and org.mdedetrich.stripe.Endpoint
types. The org.mdedetrich.stripe.Config
object provides these keys through environment variables/system settings (see application.conf
for more details), although you can manually provide your own implicit ApiKey
and Endpoint
instances.
All base responses made are in the format of Future[Try[T]]
where T
is the model for the object being returned (i.e. creating a charge will return a Future[Try[Charges.Charge]]
). If there is an error in making the response that involves either invalid JSON or an error in mapping the JSON to the models case class
, this will throw an exception which you need to catch as a failed Future
(it is by design that the models defined in stripe-scala are correct and that stripe does actually return valid JSON).
If there however is a checked error (such as an invalid API key) this will not throw an exception, instead it will be contained within the Try
monad (i.e. you will get a scala.util.Failure
)
The second parameter for stripe POST requests (often named as create in stripe-scala) has an optional idempotencyKey
which defaults to None
. You can specify a IdempotencyKey
to make sure that you don't create duplicate POST requests with the same input.
stripe-scala provides handle
/handleIdempotent
functions which provides the typical way of dealing with stripe-errors. It will attempt to retry the original request (using the IdempotencyKey
to prevent duplicate side effects with handleIdempotent
) for errors which are deemed to be network related errors, else it will return a failed Future
. If it fails due to going over the retry limit, handle
/handleIdempotent
will also return a failed Future
with MaxNumberOfRetries
import org.mdedetrich.stripe.v1.{Customers, handleIdempotent}
import scala.concurrent.Future
val customerInput: Customers.CustomerInput = ??? // Some customer input
val response: Future[Customers.Customer] = handleIdempotent(Customers.create(customerInput))
For the most part you will want to use handleIdempotent
/handle
however if you want more fine grained control over potential errors then you can use the various .create
/.get
methods
Building case classes
The Stripe object models in stripe-scala have named parameters set to default values which simplifies creating the Stripe models
import org.mdedetrich.stripe.v1.Customers._
val expMonth = 01
val expYear = 2020
val cardNumber = "4242424242424242"
val cvc = "536"
// Inefficient way
val source = Source.Card(expMonth,
expYear,
cardNumber,
None,
None,
None,
None,
None,
None,
None,
Option(cvc),
None,
None,
None
)
// Efficient way
val source2 = Source.Card(
expMonth = expMonth,
expYear = expYear,
number = cardNumber,
cvc = Option(cvc)
)
metadata
Stripe provides a metadata field which is available as an input field to most of the stripe objects. The metadata in stripe-scala has a type of Option[Map[String,String]]
. As you can see, the metadata is wrapped in an Option
. This is to make working with metadata easier.
Timestamps
Stripe represents all of its timestamps as unix timestamp numbers (https://support.stripe.com/questions/what-timezone-does-the-dashboard-and-api-use) however stripe-scala models store these timestamps as an OffsetDateTime
. stripe-scala handles converting the unix timestamp to OffsetDateTime
and vice versa by using custom circe encoders/decoders for JSON (defaults.stripeDateTimeDecoder
/defaults.stripeDateTimeEncoder
) and stripeDateTimeParamWrites
for form parameters.
These functions are exposed publicly via the package object.
Dealing with Card Errors
Since error messages from stripe are properly checked, dealing with errors like invalid CVC when adding a card are very easy to do. Here is an example (we assume that you are using Play, but this can work with any web framework. Only OK
,BadRequest
and Json.obj
are Play related methods)
import org.mdedetrich.stripe.v1.Cards
import org.mdedetrich.stripe.v1.Errors._
import org.mdedetrich.stripe.v1.{handleIdempotent,transformParam}
import play.api.mvc // Play related import
val expMonth = 01
val expYear = 2020
val cardNumber = "4000000000000127"
val cvc = "536"
val stripeCustomerId: String = ??? // Some stripe customer Id
val cardData = Cards.CardData.Source.Object(
expMonth = expMonth,
expYear = expYear,
number = cardNumber,
cvc = Option(cvc)
)
val cardInput = Cards.CardInput(cardData)
val futureResponse = handleIdempotent(Cards.create(stripeCustomerId, cardInput)).recover {
case Errors.Error.RequestFailed(CardError, _, Some(message), Some(param)) =>
// We have a parameter, this usually means one of our fields is incorrect such as an invalid CVC
BadRequest(Json.obj("message" -> List((transformParam(param), List(message)))))
case Errors.Error.RequestFailed(CardError, _, Some(message), None) =>
// No parameter, usually means a more general error, such as a declined card
BadRequest(Json.obj("message" -> message))
}.map { cardData =>
Ok(Json.toJson(cardData))
}
We attempt to create a card, and if it fails due to a CardError
we use the .recover
method on a Future
with pattern matching to map it to a BadRequest
. If the request passes, we simply wrap the card data around an Ok
. If we don't catch something of type CardError
we let it propagate as a failed Future
.
One thing to note is the transformParam
function. Since scala-stripe uses camel case instead of stripe's snake case, returned params for error messages from stripe will use snake case (i.e. "exp_month"). transformParam
will convert that to a "expMonth".
If you try and run the above code (remembering to implement stripeCustomerId
) with that credit card number in a test environment it should return an incorrect CVC, see stripe testing for more info.
List collection
stripe can return items in the form a of a list which has the following format
{
"object": "list",
"url": "/v1/customers/35/sources",
"has_more": false,
"data": [
{...},
{...}
]
}
In stripe-scala, there is a base List collection at org.mdedetrich.stripe.v1.Collections.List
with represents the model for the list. Other stripe objects extend org.mdedetrich.stripe.v1.Collections.List
to provide an implementation of the object as a list collection, i.e. BankAccountList
for BankAccount
Formatting/Style Guide
The project uses scalafmt to enforce consistent Scala formatting. Please run scalafmt before commiting your code to github (i.e. do scalafmt
inside of sbt)
Testing
The project has unit and integration tests. These can be run with:
sbt test
sbt it:test