package net.liftweb.paypal
/*
* Copyright 2007-2008 WorldWide Conferencing, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import _root_.net.liftweb.util.Helpers
import Helpers._
import _root_.net.liftweb.util._
import _root_.net.liftweb.http._
import _root_.org.apache.commons.httpclient._
import _root_.org.apache.commons.httpclient.methods._
import _root_.java.io._
import _root_.scala.collection.mutable.ListBuffer
import _root_.scala.actors.Actor
import Actor._
import _root_.scala.xml.{NodeSeq}
/**
* sealed abstract type PaypalMode so we can cast to the super
* class in our method declerations. Cannot be subclasses outside
* of this source file.
*/
sealed trait PaypalMode {
def domain: String
override def toString = "PaypalMode: "+domain
}
object PaypalSandbox extends PaypalMode {
def domain = "www.sandbox.paypal.com"
}
object PaypalLive extends PaypalMode {
def domain = "www.paypal.com"
}
/**
* Represents the type of connection that can be made
* to paypal, irrespecitve of the mode of connection
*/
sealed trait PaypalConnection {
def protocol: String
def port: Int = 80
override def toString = "PaypalConnection: "+protocol+":"+port
}
object PaypalHTTP extends PaypalConnection {
def protocol = "http"
}
object PaypalSSL extends PaypalConnection {
def protocol = "https"
override def port: Int = 443
}
/**
* Contatins all the papyal status abstractions as enumberable vals
*/
object PaypalTransactionStatus extends Enumeration {
val CancelledReversalPayment = Value(1, "Cancelled_Reversal")
val ClearedPayment = Value(2, "Cleared")
val CompletedPayment = Value(3, "Completed")
val DeniedPayment = Value(4, "Denied")
val ExpiredPayment = Value(5, "Expired")
val FailedPayment = Value(6, "Failed")
val PendingPayment = Value(7, "Pending")
val RefundedPayment = Value(8, "Refunded")
val ReturnedPayment = Value(9, "Returned")
val ReversedPayment = Value(10, "Reversed")
val UnclaimedPayment = Value(11, "Unclaimed")
val UnclearedPayment = Value(12, "Uncleared")
val VoidedPayment = Value(13, "Voided")
val InProgressPayment = Value(14, "In-Progress")
val PartiallyRefundedPayment = Value(15, "Partially-Refunded")
val ProcessedPayment = Value(16, "Processed")
def find(name: String): Box[Value] = {
val n = name.trim.toLowerCase
this.elements.filter(v => v.toString.toLowerCase == n).toList.firstOption
}
}
/**
* As the HTTP Commons HttpClient class is by definition very mutable, we
* provide this factory method to produce an instance we can assign to a val
*
* @param url The domain url your sending to
* @param port The TCP port the message will be sent over
* @param connection The protocal to use: http, or https
*/
private object HttpClientFactory {
def apply(url: String, port: Int, connection: String): HttpClient = {
val c: HttpClient = new HttpClient()
c.getHostConfiguration().setHost(url, port, connection)
c
}
}
/**
* Creates a new PostMethod and applys the passed paramaters
*
* @param url The string representation of the endpoing (e.g. www.paypal.com)
* @paypal paramaters A Seq[(String,String)] of paramaters that will become the paypload of the request
*/
private object PostMethodFactory {
def apply(url: String, paramaters: Seq[(String, String)]): PostMethod = {
val p: PostMethod = new PostMethod(url)
p.setRequestBody(paramaters)
p
}
implicit def tonvp(in: Seq[(String, String)]): Array[NameValuePair] =
in.map(p => new NameValuePair(p._1, p._2)).toArray
}
/**
* Common functionality for paypal PDT and IPN
*/
trait PaypalBase {
/**
* Create a new HTTP client
*
* @param mode The PaypalMode type that your targeting. Options are PaypalLive or PaypalSandbox
* @param connection The protocol the invocation is made over. Options are PaypalHTTP or PaypalSSL
*/
protected def client(mode: PaypalMode, connection: PaypalConnection): HttpClient = HttpClientFactory(mode.domain, connection.port, connection.protocol)
}
/**
* A simple abstraction for all HTTP operations. By definition they will return a HTTP error
* code. We are invaribly only concerned with if it was a good one or not.
*/
trait PaypalUtilities {
def wasSuccessful(code: Int): Boolean = code match {
case 200 => true
case _ => false
}
}
/**
* All HTTP requests to the paypal servers must subclass PaypalRequest.
*
* @param client Must be a HTTP client; the simplest way to create this is by using HttpClientFactory
* @param post Specify the payload of the HTTP request. Must be an instance of PostMethod from HTTP commons
*/
private object PaypalRequest extends PaypalUtilities {
def apply(client: HttpClient, post: PostMethod): List[String] = wasSuccessful(tryo(client.executeMethod(post)).openOr(500)) match {
case true => StreamResponseProcessor(post)
case _ => List("Failure")
}
}
/**
* As InputStream is a mutable I/O, we need to use a singleton to access
* it / process it and return us a immutable result we can work with. If
* we didnt do this then we get into a whole world of hurt and null pointers.
*/
private object StreamResponseProcessor {
/**
* @param p PostMethod Takes the raw HTTP commons PostMethod and processes its stream response
*/
def apply(p: PostMethod): List[String] = {
val stream: InputStream = p.getResponseBodyAsStream()
val reader: BufferedReader = new BufferedReader(new InputStreamReader(stream))
val ret: ListBuffer[String] = new ListBuffer
try {
def doRead {
reader.readLine() match {
case null => ()
case line =>
ret += line
doRead
}
}
doRead
ret.toList
} catch {
case _ => Nil
}
}
}
/**
* All paypal service classes need to subclass PaypalResponse explicitally.
*/
trait PaypalResponse extends PaypalUtilities with HasParams {
def response: List[String]
def isVerified: Boolean
private lazy val info: Map[String, String] =
Map((for (v <- response; s <- split(v)) yield s) :_*)
def param(name: String): Box[String] = Box(info.get(name))
lazy val paypalInfo: Box[PayPalInfo] =
if (this.isVerified) Full(new PayPalInfo(this))
else Empty
def rawHead: Box[String] = Box(response.firstOption)
private def split(in: String): Box[(String, String)] = {
val pos = in.indexOf("=")
if (pos < 0) Empty
else Full((urlDecode(in.substring(0, pos)),
urlDecode(in.substring(pos + 1))))
}
}
object PaypalDataTransfer extends PaypalBase {
/**
* payloadArray is the array of the post body we'll be sending.
* As the payload body is different in PDT vs IPN
*
* @retrn List[(String,String)]
*/
private def payloadArray(authToken: String, transactionToken: String) =
List("cmd" -> "_notify-synch",
"tx" -> transactionToken,
"at" -> authToken)
/**
* Execute the PDT call
*
* @param authToken The token you obtain from the paypal merchant console
* @param transactionToken The token that is passed back to your application as the "tx" part of the query string
* @param mode The PaypalMode type that your targeting. Options are PaypalLive or PaypalSandbox
* @param connection The protocol the invocation is made over. Options are PaypalHTTP or PaypalSSL
* @return PaypalDataTransferResponse
*/
def apply(authToken: String, transactionToken: String, mode: PaypalMode, connection: PaypalConnection): PaypalResponse =
PaypalDataTransferResponse(
PaypalRequest(client(mode, connection),
PostMethodFactory("/cgi-bin/webscr",payloadArray(authToken, transactionToken))))
}
/**
* Wrapper instance for handling the response from a paypal data transfer.
*
* @param response The processed response List[String]. The response
* input should be created with StreamResponseProcessor
*/
case class PaypalDataTransferResponse(response: List[String]) extends PaypalResponse {
def isVerified = paymentSuccessful
/**
* Quick utility method for letting you know if the payment data is returning a sucsessfull message
*
* @return Boolean
*/
def paymentSuccessful: Boolean = rawHead match {
case Full("SUCCESS") => true
case _ => false
}
}
//
// PAYPAL IPN
//
/**
* Users would generally invoke this case class in a DispatchPF call in
* Boot.scala as it handles the incomming request and dispatches the IPN
* callback, and handles the subsequent response.
*/
private object PaypalIPN extends PaypalUtilities {
/**
* @todo Really need to make sure that multiple custom paramaters can be mapped through.
* The current solution is not good!
*/
private def paramsAsPayloadList(request: Req): Seq[(String, String)] =
(for(p <- request.params; mp <- p._2.map(v => (p._1, v))) yield (mp._1, mp._2)).toList
def apply(request: Req, mode: PaypalMode, connection: PaypalConnection) = {
//create request, get response and pass response object to the specified event handlers
val ipnResponse: PaypalIPNPostbackReponse = PaypalIPNPostback(mode, connection, paramsAsPayloadList(request))
ipnResponse
}
}
/**
* In response to the IPN postback from paypal, its nessicary to then call paypal and pass back
* the exact set of paramaters that you were given by paypal - this stops spoofing. Use the
* PaypalInstantPaymentTransferPostback exactly as you would PaypalDataTransferResponse.
*/
private[paypal] object PaypalIPNPostback extends PaypalBase {
def payloadArray(paramaters: Seq[(String, String)]) = List("cmd" -> "_notify-validate") ++ paramaters
/**
* @return PaypalIPNPostbackReponse
*/
def apply(mode: PaypalMode, connection: PaypalConnection, paramaters: Seq[(String, String)]): PaypalIPNPostbackReponse =
new PaypalIPNPostbackReponse(
PaypalRequest(client(mode, connection),PostMethodFactory("/cgi-bin/webscr",payloadArray(paramaters)))
)
}
/**
* An abstration for the response from Paypal during the to and frow of IPN validation
*
* @param response The processed List[String] from the paypal IPN request response cycle
*/
private[paypal] class PaypalIPNPostbackReponse(val response: List[String]) extends PaypalResponse {
def isVerified: Boolean = rawHead match {
case Full("VERIFIED") => true
case _ => false
}
}
object SimplePaypal extends PaypalIPN with PaypalPDT {
val paypalAuthToken = "123"
def actions = {
case (status, info, resp) =>
Log.info("Got a verified PayPal IPN: "+status)
}
def pdtResponse = {
case (info, resp) =>
Log.info("Got a verified PayPal PDT: "+resp)
DoRedirectResponse.apply("/")
}
}
trait BasePaypalTrait extends LiftRules.DispatchPF {
lazy val RootPath = rootPath
def functionName = "Paypal"
def rootPath = "paypal"
def dispatch: List[LiftRules.DispatchPF] = Nil
lazy val mode: PaypalMode = Props.mode match {
case Props.RunModes.Production => PaypalLive
case _ => PaypalSandbox
}
def connection: PaypalConnection = PaypalSSL
def isDefinedAt(r: Req) = NamedPF.isDefinedAt(r, dispatch)
def apply(r: Req) = NamedPF(r, dispatch)
}
trait PaypalPDT extends BasePaypalTrait {
def paypalAuthToken: String
lazy val PDTPath = pdtPath
def pdtPath = "pdt"
override def dispatch: List[LiftRules.DispatchPF] = {
val nf: LiftRules.DispatchPF = NamedPF("Default PDT") {
case r @ Req(RootPath :: PDTPath :: Nil, "", _) =>
r.params // force the lazy value to be evaluated
processPDT(r) _
}
super.dispatch ::: List(nf)
}
def pdtResponse: PartialFunction[(PayPalInfo, Req), LiftResponse]
def processPDT(r: Req)(): Box[LiftResponse] = {
for (tx <- r.param("tx");
val resp = PaypalDataTransfer(paypalAuthToken, tx, mode, connection);
info <- resp.paypalInfo;
redir <- tryo(pdtResponse(info, r))) yield {
redir
}
}
}
/**
* To handle IPN transactions you need to do the following:
*
* <code>
* // in Whatever.scala
* object MyPayPalHandler extends PayPal {
* import PaypalTransactionStatus._
* def actions = {
* case (ClearedPayment, info, _) => // write the payment to the database
* case (RefundedPayment, info, _) => // process refund
* }
* }
*
* // in Boot.scala
*
* LiftRules.statelessDispatchTable = MyPayPalHandler orElse
* LiftRules.statelessDispatchTable
* </code>
*
* In this way you then get all the DispatchPF processing stuff for free.
*
*/
trait PaypalIPN extends BasePaypalTrait {
lazy val IPNPath = ipnPath
def ipnPath = "ipn"
def defaultResponse(): Box[LiftResponse] = Full(PlainTextResponse("ok"))
override def dispatch: List[LiftRules.DispatchPF] = {
val nf: LiftRules.DispatchPF = NamedPF("Default PaypalIPN") {
case r @ Req(RootPath :: IPNPath :: Nil, "", PostRequest) =>
r.params // force the lazy value to be evaluated
requestQueue ! IPNRequest(r, 0, millis)
defaultResponse _
}
super.dispatch ::: List(nf)
}
def actions: PartialFunction[(PaypalTransactionStatus.Value, PayPalInfo, Req), Unit]
protected case class IPNRequest(r: Req, cnt: Int, when: Long)
protected case object PingMe
protected def buildInfo(resp: PaypalResponse, req: Req): Box[PayPalInfo] = {
if (resp.isVerified) Full(new PayPalInfo(req))
else Empty
}
/**
* How many times do we try to verify the request
*/
val MaxRetry = 6
protected object requestQueue extends Actor {
def act = {
loop {
react {
case PingMe => ActorPing.schedule(this, PingMe, 10 seconds)
case IPNRequest(r, cnt, _) if cnt > MaxRetry => // discard the transaction
case IPNRequest(r, cnt, when) if when <= millis =>
tryo {
val resp = PaypalIPN(r, mode, connection)
for (info <- buildInfo(resp, r);
stat <- info.paymentStatus) yield {
actions((stat, info, r))
true
}
} match {
case Full(Full(true)) => // it succeeded
case _ => // retry
this ! IPNRequest(r, cnt + 1, millis + (1000 * 8 << (cnt + 2)))
}
case _ =>
}
}
}
}
requestQueue.start
requestQueue ! PingMe
}
/**
* A paramater set that takes request paramaters (from Req) and assigns them
* to properties of this class
*
* @param params The paramaters from the incooming request
*/
class PayPalInfo(val params: HasParams) {
private val r = params
val itemName = r.param("item_name")
val business = r.param("business")
val itemNumber = r.param("item_number")
val paymentStatus: Box[PaypalTransactionStatus.Value] = r.param("payment_status").flatMap(PaypalTransactionStatus.find)
val mcGross = r.param("mc_gross")
val paymentCurrency = r.param("mc_currency")
val txnId = r.param("txn_id")
val receiverEmail = r.param("receiver_email")
val receiverId = r.param("receiver_id")
val quantity = r.param("quantity")
val numCartItems = r.param("num_cart_items")
val paymentDate = r.param("payment_date")
val firstName = r.param("first_name")
val lastName = r.param("last_name")
val paymentType = r.param("payment_type")
val paymentGross = r.param("payment_gross")
val paymentFee = r.param("payment_fee")
val settleAmount = r.param("settle_amount")
val memo = r.param("memo")
val payerEmail = r.param("payer_email")
val txnType = r.param("txn_type")
val payerStatus = r.param("payer_status")
val addressStreet = r.param("address_street")
val addressCity = r.param("address_city")
val addressState = r.param("address_state")
val addressZip = r.param("address_zip")
val addressCountry = r.param("address_country")
val addressStatus = r.param("address_status")
val tax = r.param("tax")
val optionName1 = r.param("option_name1")
val optionSelection1 = r.param("option_selection1")
val optionName2 = r.param("option_name2")
val optionSelection2 = r.param("option_selection2")
val forAuction = r.param("for_auction")
val invoice = r.param("invoice")
val custom = r.param("custom")
val notifyVersion = r.param("notify_version")
val verifySign = r.param("verify_sign")
val payerBusinessName = r.param("payer_business_name")
val payerId =r.param("payer_id")
val mcCurrency = r.param("mc_currency")
val mcFee = r.param("mc_fee")
val exchangeRate = r.param("exchange_rate")
val settleCurrency = r.param("settle_currency")
val parentTxnId = r.param("parent_txn_id")
val pendingReason = r.param("pending_reason")
val reasonCode = r.param("reason_code")
val subscrId = r.param("subscr_id")
val subscrDate = r.param("subscr_date")
val subscrEffective = r.param("subscr_effective")
val period1 = r.param("period1")
val period2 = r.param("period2")
val period3 = r.param("period3")
val amount = r.param("amt")
val amount1 = r.param("amount1")
val amount2 = r.param("amount2")
val amount3 = r.param("amount3")
val mcAmount1 = r.param("mc_amount1")
val mcAmount2 = r.param("mc_amount2")
val mcAmount3 = r.param("mcamount3")
val recurring = r.param("recurring")
val reattempt = r.param("reattempt")
val retryAt = r.param("retry_at")
val recurTimes = r.param("recur_times")
val username = r.param("username")
val password = r.param("password")
val auctionClosingDate = r.param("auction_closing_date")
val auctionMultiItem = r.param("auction_multi_item")
val auctionBuyerId = r.param("auction_buyer_id")
}