Last active
August 29, 2015 14:08
-
-
Save yoeluk/de2df7731b7c0ff2f947 to your computer and use it in GitHub Desktop.
InstaBT Integration
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.instabt | |
/** | |
* Created by yoelusa on 30/10/14. | |
*/ | |
import org.apache.commons.codec.binary.Base64 | |
import play.api.data.Form | |
import play.api.data.Forms._ | |
import play.api.libs.ws._ | |
import javax.crypto.spec.SecretKeySpec | |
import javax.crypto.Mac | |
import java.util.TimeZone | |
import java.util.Calendar | |
import play.api.Play.current | |
import scala.concurrent.Future | |
import play.api.libs.concurrent.Execution.Implicits.defaultContext | |
/** | |
* A configuration case class. | |
* @param key Api key. | |
* @param secret Api secret. | |
* @param amount The amount of this bill. | |
* @param currency The currency of the transaction. InstaBT accepts CAD and BTC. It defaults to BTC if the field is not supplied. | |
* @param options The options. See the available options in the InstaBT documentation. It defaults to None if the field is not supplied. | |
*/ | |
case class Configuration(key: String, | |
secret: String, | |
amount: Double, | |
currency: String = "BTC", | |
options: Option[Map[String, String]] = None) | |
/** | |
* A paydata case class. Its fields match those in the json response of InstaBT | |
* @param Id An order id. | |
* @param Url A url for redirecting the browser. | |
* @param Total The amount of the bill. | |
* @param Currency The currency of the bill. | |
* @param BtcRequired The amount of BTC required to fulfillment. | |
* @param Data The data supplied with the request. | |
* @param CreatedOn The created date. | |
* @param ExpiresOn The expiry date. | |
* @param LastUpdate The updated data. | |
* @param Status The status of the transaction. | |
*/ | |
case class PayData(Id: String, | |
Url: String, | |
Total: String, | |
Currency: String, | |
BtcRequired: String, | |
Data: String, | |
CreatedOn: String, | |
ExpiresOn: String, | |
LastUpdate: String, | |
Status: String) | |
/** | |
* A payresponse case class to return to the controller. | |
* @param status An http status code. | |
* @param statusText An http status text. | |
* @param payData An optional PayData. It defaults to None if the field is not supplied | |
*/ | |
case class PayResponse(status: Int, | |
statusText: String, | |
payData: Option[PayData] = None) | |
object InstaBT { | |
/** | |
* A paydata form to bind from the InstaBT response.json | |
*/ | |
val payDataForm = Form( | |
mapping( | |
"Id" -> text, | |
"Url" -> text, | |
"Total" -> text, | |
"Currency" -> text, | |
"BtcRequired" -> text, | |
"Data" -> text, | |
"CreatedOn" -> text, | |
"ExpiresOn" -> text, | |
"LastUpdate" -> text, | |
"Status" -> text | |
)(PayData.apply)(PayData.unapply) | |
) | |
/** | |
* Compose the request with the configuration param and make the call. It provides default values for url and end_point | |
* @param config A configuration for this transaction. | |
* @param end_point The services end point. It defaults to "/create_order" if the parameter is not supplied. | |
* @param url The base url for the request. It default to "https://api.instabt.com" if the parameter is not supplied. | |
* @return | |
*/ | |
def payWithConfiguration(config: Configuration, | |
end_point: String = "/create_order", | |
url: String = "https://api.instabt.com"): Future[PayResponse] = { | |
val utcTime = Calendar.getInstance(TimeZone.getTimeZone("UTC")) | |
// Until I know of a reliable way to get the time in microseconds in the jvm this should suffice. | |
val nonce = s"${utcTime.getTimeInMillis}000" | |
val payload = | |
s"amount=${config.amount}¤cy=${config.currency}&nonce=$nonce" + | |
(config.options match { | |
case Some(options) => | |
options.foldLeft("") { case (acc, (key, value)) => s"$acc&$key=$value" } | |
case None => "" | |
}) | |
val sigData = end_point + '\u0000' + payload | |
val mac = Mac.getInstance("HmacSHA512") | |
val secretKey = new SecretKeySpec(config.secret.getBytes("UTF-8"), mac.getAlgorithm) | |
val sign = { | |
mac.init(secretKey) | |
val byteString = mac.doFinal(sigData.getBytes).foldLeft("") { case (acc, b) => s"$acc%02x".format(b) } | |
Base64.encodeBase64String(byteString.getBytes)} | |
WS.url(url+end_point) | |
.withHeaders("API-KEY" -> config.key, "API-SIGN" -> sign) | |
.post(payload).map { wsResponse => | |
if (wsResponse.status == 200) { | |
PayResponse( | |
status = wsResponse.status, | |
statusText = wsResponse.statusText, | |
payData = Some(payDataForm.bind(wsResponse.json).get) | |
) | |
} else | |
PayResponse( | |
status = wsResponse.status, | |
statusText = wsResponse.statusText | |
) | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package controllers | |
/** | |
* Created by yoelusa on 30/10/14. | |
*/ | |
import play.api.mvc._ | |
import com.instabt._ | |
import play.api.Play.current | |
import scala.concurrent.Future | |
import scala.concurrent.duration._ | |
import play.api.libs.concurrent.Promise.timeout | |
import play.api.libs.concurrent.Execution.Implicits.defaultContext | |
object PayInstaBT extends Controller { | |
def doPay = Action.async { | |
val options = Some(Map( | |
"url_call_back" -> "someUrlCallback", | |
"url_success" -> "someUrlRedirect", | |
"url_failure" -> "someUrlRedirect" | |
)) | |
val key = current.configuration.getString("instaBT.key").get | |
val secret = current.configuration.getString("instaBT.secret").get | |
val config = Configuration(key = key, secret = secret, amount = 3.0, options = options) | |
val instaResponse = InstaBT.payWithConfiguration(config) | |
val timeoutResponse = timeout("Oops", 15.seconds) | |
Future.firstCompletedOf( Seq(instaResponse, timeoutResponse) ).map { | |
case payResponse: PayResponse => payResponse.payData match { | |
case Some(payData) => | |
Redirect(payData.Url) | |
case None => | |
ServiceUnavailable(payResponse.statusText) | |
} | |
case error: String => InternalServerError(error) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment