-
-
Save git-josip/d3ed6e751d0125e53065 to your computer and use it in GitHub Desktop.
Paypal IPN script for Play 2.1
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 | |
import play.api._ | |
import libs.ws.WS | |
import play.api.mvc._ | |
import com.micronautics.paypal.{TransactionProcessor, PaypalTransactions} | |
import java.net.URLEncoder | |
import concurrent.{Await, ExecutionContext} | |
import concurrent.duration.Duration | |
import java.util.concurrent.TimeUnit | |
import concurrent.Future | |
import play.api.libs.ws.{ Response => WSResponse } | |
import com.typesafe.config.ConfigFactory | |
object PayPalController extends Controller { | |
var live = true | |
lazy val defaultConfigStr = """ | |
| paypal { | |
| receiverEmail = "[email protected]" | |
| }""".stripMargin | |
lazy val defaultConfig = try { | |
ConfigFactory.parseString(defaultConfigStr) | |
} catch { | |
case e: Throwable => | |
println(e.getMessage) | |
sys.exit(-3) | |
} | |
lazy val config = ConfigFactory.load() | |
lazy val mergedConfig = config.withFallback(defaultConfig) | |
lazy val receiverEmail = findDef("paypal.receiverEmail") | |
/** Look up variable in environment then in merged config */ | |
def findDef(name: String): String = { | |
val envValue: Option[String] = sys.env.get(name) | |
if (envValue == None) mergedConfig.getString(name) else envValue.get | |
} | |
def url = | |
if (live) | |
"https://www.paypal.com/cgi-bin/webscr?cmd=_notify-validate&" | |
else | |
"https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_notify-validate&" | |
def synchronousPost(url: String, dataMap: Map[String, Seq[String]], timeout: Duration=Duration.create(30, TimeUnit.SECONDS)): String = { | |
import ExecutionContext.Implicits.global | |
val params = dataMap.map { case (k, v) => "%s=%s".format(k, URLEncoder.encode(v.head, "UTF-8")) }.mkString("&") | |
val future: Future[WSResponse] = WS.url(url). | |
withHeaders(("Content-Type", "application/x-www-form-urlencoded")). | |
post(params) | |
try { | |
Await.result(future, timeout) | |
future.value.get.get.body | |
} catch { | |
case ex: Exception => | |
Logger.error(ex.toString) | |
ex.toString | |
} | |
} | |
/** @see [[https://www.paypal.com/cgi-bin/webscr?cmd=p/acc/ipn-info-outside]] */ | |
def ipn = Action { implicit request => | |
request.body.asFormUrlEncoded match { | |
case Some(dataMap) => | |
Logger.info("\nPaypal request: " + dataMap) | |
synchronousPost(url, dataMap + ("cmd" -> List("_notify-validate"))) match { | |
case "VERIFIED" => | |
val paymentStatus = dataMap.getOrElse("payment_status", List("")).head | |
if (paymentStatus == "Completed") { | |
val txn = PaypalTransactions.applySeq(dataMap) | |
PaypalTransactions.findByTxnId(txn.txnId) match { | |
case Some(txn) => | |
Logger.info("Ignoring duplicate transaction: " + txn.toStringAll) | |
case None => | |
// Validate that the "receiver_email" is an email address registered in our PayPal account | |
if (txn.receiverEmail!=receiverEmail) | |
Logger.warn("Potential fraud attempt: receiver_email did not match in " + txn.toStringAll) | |
else | |
new TransactionProcessor(txn)(request).processTransaction | |
} | |
} else { // paymentStatus might be "Pending" or "Failed" | |
Logger.warn("Verified but payment_status is " + paymentStatus) | |
} | |
case response => // most likely response is "INVALID" | |
Logger.warn("Could not verify Paypal transaction via POST to %s; verification response: '%s'".format(url, response)) | |
} | |
case None => | |
} | |
Ok | |
} | |
} |
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.micronautics.paypal | |
import play.Logger | |
import java.util.{ List, Map } | |
import collection.JavaConversions._ | |
import collection.immutable.HashMap | |
import slick.driver.MySQLDriver.simple._ | |
/** The customerAddress table does not actually exist; this is my attempt to somehow put together nested tuples */ | |
object CustomerAddresses | |
extends Table[(Int, String, String, String, String, String, String, String, String)]("customerAddress") { | |
def id = column[Int]("id", O NotNull) | |
def addressCity = column[String]("address_city") | |
def addressCountry = column[String]("address_country") | |
def addressCountryCode = column[String]("address_country_code") | |
def addressName = column[String]("address_name") | |
def addressState = column[String]("address_state") | |
def addressStatus = column[String]("address_status") | |
def addressStreet = column[String]("address_street") | |
def addressZip = column[String]("address_zip") | |
def * = id ~ addressCity ~ addressCountry ~ addressCountryCode ~ addressName ~ addressState ~ addressStatus ~ addressStreet ~ addressZip | |
} | |
case class CustomerAddress( | |
/** City of customer’s address - 40 chars */ | |
addressCity: String, | |
/** Country of customer’s address - 64 chars */ | |
addressCountry: String, | |
/** ISO 3166 country code associated with customer’s address - 2 chars */ | |
addressCountryCode: String, | |
/** Name used with address (included when the customer provides a Gift Address) - 128 chars */ | |
addressName: String, | |
/** State of customer’s address - 40 chars */ | |
addressState: String, | |
/** Whether the customer provided a confirmed address ("confirmed" or "unconfirmed") - 20 chars */ | |
addressStatus: String, | |
/** Customer’s street address - 200 chars */ | |
addressStreet: String, | |
/** Zip code of customer’s address - 20 chars */ | |
addressZip: String) | |
/** The paymentDetail table does not actually exist; this is my attempt to somehow put together nested tuples */ | |
object PaymentDetails extends Table[(Int, String, String, String, String, String, String)]("paymentDetail") { | |
def id = column[Int]("id", O NotNull) // this field does not exist separately | |
def paymentDate = column[String]("payment_date") | |
def paymentStatus = column[String]("payment_status") | |
def paymentType = column[String]("payment_type") | |
def pendingReason = column[String]("pending_reason") | |
def reasonCode = column[String]("reason_code") | |
def tax = column[String]("tax") | |
def * = id ~ paymentDate ~ paymentStatus ~ paymentType ~ pendingReason ~ reasonCode ~ tax | |
} | |
case class PaymentDetail( | |
/** Time/date stamp generated by PayPal, in the following format: HH:MM:SS DD Mmm YY, YYYY PST - 28 chars */ | |
paymentDate: String, | |
/** The status of the payment: | |
Canceled_Reversal: A reversal has been canceled. For example, you won a dispute with the customer, and the funds for the transaction that was | |
reversed have been returned to you. | |
Completed: The payment has been completed, and the funds have been added successfully to your account balance. | |
Created: A German ELV payment is made using Express Checkout. | |
Denied: You denied the payment. This happens only if the payment was previously pending because of possible reasons described for the | |
pending_reason variable or the Fraud_Management_Filters_x variable. | |
Expired: This authorization has expired and cannot be captured. | |
Failed: The payment has failed. This happens only if the payment was made from your customer’s bank account. | |
Pending: The payment is pending. See pending_reason for more information. | |
Refunded: You refunded the payment. | |
Reversed: A payment was reversed due to a chargeback or other type of reversal. The funds have been removed from your account balance and | |
returned to the buyer. The reason for the reversal is specified in the ReasonCode element. | |
Processed: A payment has been accepted. | |
Voided: This authorization has been voided. */ | |
paymentStatus: String, | |
/** echeck: This payment was funded with an eCheck. | |
instant: This payment was funded with PayPal balance, credit card, or Instant Transfer. */ | |
paymentType: String, | |
/** This variable is set only if payment_status = Pending. | |
address: The payment is pending because your customer did not include a confirmed shipping address and your Payment Receiving Preferences is | |
set yo allow you to manually accept or deny each of these payments. To change your preference, go to the Preferences section of your Profile. | |
authorization: You set the payment action to Authorization and have not yet captured funds. | |
echeck: The payment is pending because it was made by an eCheck that has not yet cleared. | |
intl: The payment is pending because you hold a non-U.S. account and do not have a withdrawal mechanism. You must manually accept or deny | |
this payment from your Account Overview. | |
multi-currency: You do not have a balance in the currency sent, and you do not have your Payment Receiving Preferences set to | |
automatically convert and accept this payment. You must manually accept or deny this payment. | |
order: You set the payment action to Order and have not yet captured funds. | |
paymentreview: The payment is pending while it is being reviewed by PayPal for risk. | |
unilateral: The payment is pending because it was made to an email address that is not yet registered or confirmed. | |
upgrade: The payment is pending because it was made via credit card and you must upgrade your account to Business or Premier status in order | |
to receive the funds. upgrade can also mean that you have reached the monthly limit for transactions on your account. | |
verify: The payment is pending because you are not yet verified. You must verify your account before you can accept this payment. | |
other: The payment is pending for a reason other than those listed above. | |
For more information, contact PayPal Customer Service. */ | |
pendingReason: String, | |
/** This variable is set if payment_status =Reversed, Refunded, or Cancelled_Reversal. | |
adjustment_reversal: Reversal of an adjustment buyer-complaint: A reversal has occurred on this transaction due to a | |
complaint about the transaction from your customer. | |
chargeback: A reversal has occurred on this transaction due to a chargeback by your customer. | |
chargeback_reimbursement: Reimbursement for a chargeback chargeback_settlement: Settlement of a chargeback | |
guarantee: A reversal has occurred on this transaction due to your customer triggering a money-back guarantee. | |
other: Non-specified reason. | |
refund: A reversal has occurred on this transaction because you have given the customer a refund. | |
NOTE: Additional codes may be returned. */ | |
reasonCode: String, | |
/** Amount of tax charged on payment. PayPal appends the number of the item (e.g., item_name1, item_name2). The tax variable is included | |
only if there was a specific tax amount applied to a particular shopping cart item. Because total tax may apply to other items in the cart, the sum | |
of tax might not total to tax. */ | |
tax: String) | |
/** This is the only object that actually exists in persisted form */ | |
object PaypalTransactions extends Table[(Int, String, String, String, String, String, String, String, String, String, | |
String, String, String, String, String, String | |
/*, CustomerAddress, PaymentDetail*/)]("paypalTransaction") { | |
def id = column[Int]("id", O.PrimaryKey, O.AutoInc) | |
def charset = column[String]("charset") | |
def contactPhone = column[String]("contact_phone") | |
def custom = column[String]("custom") | |
def firstName = column[String]("first_name") | |
def lastName = column[String]("last_name") | |
def mcCurrency = column[String]("mc_currency") | |
def mcFee = column[String]("mc_fee") | |
def mcGross = column[String]("mc_gross") | |
def memo = column[String]("memo") | |
def payerBusinessName = column[String]("payer_business_name") | |
def payerEmail = column[String]("payer_email") | |
def payerId = column[String]("payer_id") | |
def payerStatus = column[String]("payer_status") | |
def txnId = column[String]("txn_id") | |
def verifySign = column[String]("verifySign") | |
// Should there be a def for CustomerAddress & PaymentDetail? | |
// When <> is not commented out, gives error: Overloaded method value [<>] cannot be applied to | |
// (com.micronautics.paypal.CustomerAddress.type, com.micronautics.paypal.CustomerAddress => | |
// Option[(String, String, String, String, String, String, String, String)]) | |
def * = id ~ charset ~ contactPhone ~ custom ~ firstName ~ lastName ~ mcCurrency ~ mcFee ~ mcGross ~ memo ~ | |
payerBusinessName ~ payerEmail ~ payerId ~ payerStatus ~ txnId ~ verifySign | |
//<> (CustomerAddress, CustomerAddress.unapply _) <> (PaymentDetail, PaymentDetail.unapply _) | |
// what is this for? | |
//val q = for { f <- PaypalTransactions } yield f | |
//p.firstOption.map{case(name, age, address) => PaypalTransaction(name, age, address)} | |
} | |
object PaypalTransaction { | |
def applyList(dataMap: Map[String, List[String]]) = { | |
val newMap = new HashMap[String, String]() | |
dataMap.keySet foreach { key => newMap.put(key, dataMap.get(key).get(0)) } | |
apply(newMap) | |
} | |
def apply(dataMap: Map[String, String]) = { | |
def getItem(fieldName: String): String = getItem2(fieldName, true) | |
def getItem2(fieldName: String, required: Boolean): String = { | |
dataMap.get(fieldName) match { | |
case value if value!=null => | |
value | |
case null => | |
if (required) | |
throw new Exception("Required parameter " + fieldName + " not present or has no value") | |
"" | |
} | |
} | |
Logger.debug("Form data: " + dataMap) | |
val address = CustomerAddress( | |
addressCity = getItem("address_city"), | |
addressCountry = getItem("address_country"), | |
addressCountryCode = getItem("address_country_code"), | |
addressName = getItem("address_name"), | |
addressState = getItem("address_state"), | |
addressStatus = getItem("address_status"), | |
addressStreet = getItem("address_street"), | |
addressZip = getItem("address_zip")) | |
val payment = PaymentDetail( | |
paymentDate = getItem("payment_date"), | |
paymentStatus = getItem("payment_status"), | |
paymentType = getItem("payment_type"), | |
pendingReason = getItem("pending_reason"), | |
reasonCode = getItem("reason_code"), | |
tax = getItem("tax") | |
) | |
// fields have been factored into other case classes so there are fewer than 23 remaining fields | |
new PaypalTransaction( | |
customerAddress = address, | |
charset = getItem("charset"), | |
contactPhone = getItem("contact_phone"), | |
custom = getItem2("custom", false), | |
firstName = getItem("first_name"), | |
lastName = getItem("last_name"), | |
mcCurrency = getItem("mc_currency"), | |
mcFee = getItem2("mc_fee", false), | |
mcGross = getItem("mc_gross"), | |
memo = getItem2("memo", false), | |
payerBusinessName = getItem2("payer_business_name", false), | |
payerEmail = getItem("payer_email"), | |
payerId = getItem("payer_id"), | |
payerStatus = getItem2("payer_status", false), | |
paymentDetail = payment, | |
txnId = getItem("txn_id"), | |
verifySign = getItem("verify_sign")) | |
} | |
} | |
case class PaypalTransaction( | |
customerAddress: CustomerAddress, | |
/** 20 chars */ | |
charset: String, | |
/** Customer’s telephone number - 20 chars */ | |
contactPhone: String, | |
/** Defined by store configuration - 128 chars is probably safe */ | |
custom: String, | |
/** Customer’s first name - 64 chars */ | |
firstName: String, | |
/** Customer's last name - 64 chars */ | |
lastName: String, | |
/** */ | |
mcCurrency: String, | |
/** Transaction fee associated with the payment. mc_gross minus mc_fee equals the amount deposited into the | |
* receiver_email account. Equivalent to payment_fee for USD payments. If this amount is negative, it signifies a | |
* refund or reversal, and either of those payment statuses can be for the full or partial amount of the original | |
* transaction fee. */ | |
mcFee: String, | |
/** Full amount of the customer's payment, before transaction fee is subtracted. Equivalent to payment_gross for USD | |
* payments. If this amount is negative, it signifies a refund or reversal, and either of those payment statuses can | |
* be for the full or partial amount of the original transaction. */ | |
mcGross: String, | |
/** Memo as entered by the customer in the PayPal Website Payments note field - 255 chars */ | |
memo: String, | |
/** Customer’s company name, if customer is a business - 127 chars */ | |
payerBusinessName: String, | |
/** Customer’s primary email address. Use this email to provide any credits - 127 chars */ | |
payerEmail: String, // TODO add this to schema | |
/** Unique customer ID - 13 chars */ | |
payerId: String, | |
/** Look for 'verified' - 20 chars */ | |
payerStatus: String, // TODO add this to schema | |
paymentDetail: PaymentDetail, | |
/** Transaction ID */ | |
txnId: String, | |
/** Not sure */ | |
verifySign: String) { | |
def processTransaction() { | |
// error is: Overloaded method value [insert] cannot be applied to (com.micronautics.paypal.PaypalTransaction) | |
//PaypalTransactions.insert(this) // how is this supposed to work? Use the * operator somehow? | |
// do more things... | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment