support-frontend/app/services/PayPalNvpService.scala (120 lines of code) (raw):

package services import com.gu.monitoring.SafeLogging import com.gu.support.config.PayPalConfig import com.gu.support.touchpoint.TouchpointService import io.lemonlabs.uri.QueryString import io.lemonlabs.uri.parsing.UrlParser.parseQuery import play.api.libs.ws.{WSClient, WSResponse} import services.paypal.{PayPalBillingDetails, PayPalCheckoutDetails, PayPalUserDetails, Token} import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future import scala.util.Try class PayPalNvpService(apiConfig: PayPalConfig, wsClient: WSClient) extends TouchpointService with SafeLogging { val defaultNVPParams = Map( "USER" -> apiConfig.user, "PWD" -> apiConfig.password, "SIGNATURE" -> apiConfig.signature, "VERSION" -> apiConfig.NVPVersion, ) private def logNVPResponse(response: QueryString) = { val msg = s"NVPResponse: $response" retrieveNVPParam(response, "ACK") match { case Some("Success") => logger.info(s"Successful PayPal NVP request, $msg") case Some("SuccessWithWarning") => logger.warn(s"Response (with warning) from PayPal was: $msg") case Some("Failure") => logger.error(scrub"Failure response from PayPal was: $msg") case Some("FailureWithWarning") => logger.error(scrub"Response from PayPal was: $msg") case _ => logger.warn("No ACK parameter was present in the response") } } private def extractResponse(response: WSResponse): Try[QueryString] = { val responseBody = response.body val parsedResponse = parseQuery(responseBody) parsedResponse.foreach(logNVPResponse) parsedResponse } // Takes a series of parameters, send a request to PayPal, returns response. private def nvpRequest(params: Map[String, String]) = { val allParams = (params ++ defaultNVPParams).view.mapValues(b => Seq(b)).toMap wsClient .url(apiConfig.url) .post(allParams) .flatMap(response => Future.fromTry(extractResponse(response))) } // Takes an NVP response and retrieves a given parameter as a string. private def retrieveNVPParam(response: QueryString, paramName: String) = response.paramMap.getOrElse(paramName, Nil).headOption match { case None => logger.warn(s"Parameter $paramName was missing from the NVP response - $response") None case Some(value) => Some(value) } def retrieveEmail(baid: String): Future[Option[String]] = { val params = Map( "METHOD" -> "BillAgreementUpdate", "REFERENCEID" -> baid, ) for { resp <- nvpRequest(params) } yield retrieveNVPParam(resp, "EMAIL") } // Sets up a payment by contacting PayPal and returns the token. def retrieveToken(returnUrl: String, cancelUrl: String)( billingDetails: PayPalBillingDetails, ): Future[Option[String]] = { val noShipping = if (billingDetails.requireShippingAddress) "0" else "1" val paymentParams = Map( "METHOD" -> "SetExpressCheckout", "PAYMENTREQUEST_0_PAYMENTACTION" -> "SALE", "L_PAYMENTREQUEST_0_NAME0" -> s"Guardian ${billingDetails.billingPeriod.capitalize} Contributor", "L_PAYMENTREQUEST_0_DESC0" -> s"You have chosen to pay ${billingDetails.billingPeriod}", "L_PAYMENTREQUEST_0_AMT0" -> billingDetails.amount.toString, "PAYMENTREQUEST_0_AMT" -> billingDetails.amount.toString, "PAYMENTREQUEST_0_CURRENCYCODE" -> billingDetails.currency.toString, "RETURNURL" -> returnUrl, "CANCELURL" -> cancelUrl, "BILLINGTYPE" -> "MerchantInitiatedBilling", "NOSHIPPING" -> noShipping, ) nvpRequest(paymentParams).map { resp => retrieveNVPParam(resp, "TOKEN") } } // Sets up a payment by contacting PayPal and also fetches the users details. def createAgreementAndRetrieveUser(token: Token): Future[Option[PayPalCheckoutDetails]] = for { maybeBaid <- createBillingAgreement(token) maybeUserDetails <- retrieveUserInformation(token) } yield maybeBaid.map(baid => PayPalCheckoutDetails(baid, maybeUserDetails)) def retrieveUserInformation(token: Token): Future[Option[PayPalUserDetails]] = { val paymentParams = Map( "METHOD" -> "GetExpressCheckoutDetails", "TOKEN" -> token.token, ) nvpRequest(paymentParams).map { resp => for { firstName <- retrieveNVPParam(resp, "FIRSTNAME") lastName <- retrieveNVPParam(resp, "LASTNAME") email <- retrieveNVPParam(resp, "EMAIL") shipToStreet <- retrieveNVPParam(resp, "PAYMENTREQUEST_0_SHIPTOSTREET") shipToCity <- retrieveNVPParam(resp, "PAYMENTREQUEST_0_SHIPTOCITY") shipToState = retrieveNVPParam(resp, "PAYMENTREQUEST_0_SHIPTOSTATE") // State/County may not be present shipToZip <- retrieveNVPParam(resp, "PAYMENTREQUEST_0_SHIPTOZIP") shipToCountryCode <- retrieveNVPParam(resp, "PAYMENTREQUEST_0_SHIPTOCOUNTRYCODE") } yield PayPalUserDetails( firstName, lastName, email, shipToStreet, shipToCity, shipToState, shipToZip, shipToCountryCode, ) } } // Sends a request to PayPal to create billing agreement and returns BAID. def createBillingAgreement(token: Token): Future[Option[String]] = { val agreementParams = Map( "METHOD" -> "CreateBillingAgreement", "TOKEN" -> token.token, ) nvpRequest(agreementParams).map { resp => retrieveNVPParam(resp, "BILLINGAGREEMENTID") } } }