Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Model/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,12 @@
<artifactId>Jackfish</artifactId>
</dependency>

<dependency>
<groupId>com.cybersource</groupId>
<artifactId>cybersource-rest-client-java</artifactId>
<version>0.0.91</version>
</dependency>

<!-- Contains context listener used to prevent classloader memory leaks (used by web.xmls) -->
<dependency>
<groupId>se.jiderhamn.classloader-leak-prevention</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package org.eupathdb.common.service;

import java.util.Arrays;
import java.util.List;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import org.gusdb.wdk.model.WdkModelException;
import org.gusdb.wdk.model.WdkRuntimeException;
import org.gusdb.wdk.service.service.AbstractWdkService;
import org.json.JSONObject;

import com.cybersource.authsdk.core.MerchantConfig;

import Api.UnifiedCheckoutCaptureContextApi;
import Invokers.ApiClient;
import Model.GenerateUnifiedCheckoutCaptureContextRequest;
import Model.Upv1capturecontextsCaptureMandate;
import Model.Upv1capturecontextsCompleteMandate;
import Model.Upv1capturecontextsOrderInformation;
import Model.Upv1capturecontextsOrderInformationAmountDetails;

/**
* The single GET endpoint takes a payment amount and currency and returns the
* capture-context JWT that the client-side Unified Checkout JavaScript
* library needs to render its embedded payment form, along with the
* generated reference number (to be echoed back on the follow-up call to
* {@link CyberSourcePaymentService}) and the URL of the Unified Checkout JS
* asset to load (test vs. production, driven by the deployed cybersource
* config).
*/
@Path("payment-form-context")
public class CyberSourceCaptureContextService extends AbstractWdkService {

// model.prop property containing this site's base URL, e.g. https://plasmodb.org
private static final String LOCALHOST_PROP_KEY = "LOCALHOST";

// client version of the Unified Checkout JS library this capture context targets;
// must match the version of the <script> asset loaded on the front end.
// Front end must call createCheckout({ autoProcessing: false }) so that
// checkout.mount() resolves with a transient token instead of completing
// the transaction client-side (autoProcessing default changed in 0.30).
private static final String CLIENT_VERSION = "0.30";

private static final List<String> ALLOWED_CARD_NETWORKS = Arrays.asList(
"VISA", "MASTERCARD", "AMEX", "DISCOVER", "DINERSCLUB", "JCB");

private static final List<String> ALLOWED_PAYMENT_TYPES = Arrays.asList("PANENTRY");

@GET
@Produces(MediaType.APPLICATION_JSON)
public Response getCaptureContext(
@QueryParam("amount") String amount, // required; must match the pattern in CyberSourceUtil
@QueryParam("currency") String currency, // optional; defaults to USD
@QueryParam("invoice_number") String invoiceNumber // optional; logged with reference number for traceability
) {
amount = CyberSourceUtil.validateAmountParam(amount);
currency = CyberSourceUtil.validateCurrencyParam(currency);
invoiceNumber = CyberSourceUtil.validateInvoiceNumber(invoiceNumber);

String referenceNumber = CyberSourceUtil.generateReferenceNumber();
CyberSourceUtil.logPaymentEvent("capture-context", getRequestingUser(), referenceNumber, amount, currency, invoiceNumber);

JSONObject config = CyberSourceUtil.readConfig();
String localhost = getLocalhostUrl();

GenerateUnifiedCheckoutCaptureContextRequest requestObj = new GenerateUnifiedCheckoutCaptureContextRequest();
requestObj.clientVersion(CLIENT_VERSION);
requestObj.targetOrigins(Arrays.asList(localhost));
requestObj.allowedCardNetworks(ALLOWED_CARD_NETWORKS);
requestObj.allowedPaymentTypes(ALLOWED_PAYMENT_TYPES);
requestObj.country("US");
requestObj.locale("en_US");

Upv1capturecontextsCaptureMandate captureMandate = new Upv1capturecontextsCaptureMandate();
captureMandate.billingType("FULL");
captureMandate.requestEmail(true);
captureMandate.requestPhone(false);
captureMandate.requestShipping(false);
captureMandate.showAcceptedNetworkIcons(true);
requestObj.captureMandate(captureMandate);

Upv1capturecontextsOrderInformation orderInformation = new Upv1capturecontextsOrderInformation();
Upv1capturecontextsOrderInformationAmountDetails amountDetails = new Upv1capturecontextsOrderInformationAmountDetails();
amountDetails.totalAmount(amount);
amountDetails.currency(currency);
orderInformation.amountDetails(amountDetails);
requestObj.orderInformation(orderInformation);

Upv1capturecontextsCompleteMandate completeMandate = new Upv1capturecontextsCompleteMandate();
completeMandate.setType("CAPTURE");
completeMandate.setDecisionManager(false);
requestObj.setCompleteMandate(completeMandate);

try {
MerchantConfig merchantConfig = CyberSourceUtil.buildMerchantConfig(config);
ApiClient apiClient = new ApiClient();
apiClient.merchantConfig = merchantConfig;

UnifiedCheckoutCaptureContextApi apiInstance = new UnifiedCheckoutCaptureContextApi(apiClient);
String captureContextJwt = apiInstance.generateUnifiedCheckoutCaptureContext(requestObj);

JSONObject responseJson = new JSONObject()
.put("captureContext", captureContextJwt)
.put("referenceNumber", referenceNumber)
.put("scriptUrl", getUnifiedCheckoutScriptUrl(config));

return Response.ok(responseJson.toString()).build();
}
catch (Exception e) {
throw new WdkRuntimeException("Unable to generate CyberSource capture context", e);
}
}

private String getLocalhostUrl() {
String localhost = getWdkModel().getProperties().get(LOCALHOST_PROP_KEY);
if (localhost == null) {
throw new WdkRuntimeException(new WdkModelException("model.prop must contain the property: " + LOCALHOST_PROP_KEY));
}
return localhost;
}

private static String getUnifiedCheckoutScriptUrl(JSONObject config) {
String host = CyberSourceUtil.isTestEnvironment(config) ? "apitest.cybersource.com" : "api.cybersource.com";
return "https://" + host + "/uc/v1/assets/" + CLIENT_VERSION + "/UnifiedCheckout.js";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
/**
* The single GET endpoint takes a payment amount and currency and returns a
* JSON object where the keys/values represent all the form input fields
* required by CyberSource to being their checkout sequence. Web client code
* required by CyberSource to begin their checkout sequence. Web client code
* is responsible for converting this object to a form and submitting it to
* the appropriate CyberSource endpoint.
*/
Expand Down Expand Up @@ -68,7 +68,7 @@ public class CyberSourceFormService extends AbstractWdkService {
public Response generateCyberSourceForm(
@QueryParam("amount") String amount, // required; must match the pattern above
@QueryParam("currency") String currency, // optional; defaults to USD
@QueryParam("invoice_number") String invoiceNumber // optional; logged with reference number for trackability
@QueryParam("invoice_number") String invoiceNumber // optional; logged with reference number for traceability
) {

// validate and massage amount and currency params
Expand Down Expand Up @@ -171,7 +171,7 @@ private static String getUTCDateTime() {
return sdf.format(new Date());
}

private static JSONObject readConfig() {
static JSONObject readConfig() {
try (Reader in = new FileReader(CONFIG_FILE_LOCATION)) {
return new JSONObject(IoUtil.readAllChars(in));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package org.eupathdb.common.service;

import javax.ws.rs.BadRequestException;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import org.apache.log4j.Logger;
import org.gusdb.wdk.model.WdkRuntimeException;
import org.gusdb.wdk.service.service.AbstractWdkService;
import org.json.JSONObject;

import com.cybersource.authsdk.core.MerchantConfig;

import Api.PaymentsApi;
import Invokers.ApiClient;
import Invokers.ApiException;
import Model.CreatePaymentRequest;
import Model.PtsV2PaymentsPost201Response;
import Model.Ptsv2paymentsClientReferenceInformation;
import Model.Ptsv2paymentsOrderInformation;
import Model.Ptsv2paymentsOrderInformationAmountDetails;
import Model.Ptsv2paymentsTokenInformation;

/**
* Takes the transient token returned by the Unified Checkout JS widget (once
* the donor has entered their payment info in CyberSource's embedded iframe)
* along with the amount/currency/reference-number originally used to build
* the capture context, and performs the actual server-to-server authorize +
* capture ("sale") against CyberSource's Payments API. Card data is never
* present in this request; the transient token is an opaque, short-lived
* (~15 min) reference to it.
*/
@Path("payment-process")
public class CyberSourcePaymentService extends AbstractWdkService {

private static final Logger LOG = Logger.getLogger(CyberSourcePaymentService.class);

@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response processPayment(String body) {

JSONObject input = parseInput(body);

String amount = CyberSourceUtil.validateAmountParam(input.optString("amount", null));
String currency = CyberSourceUtil.validateCurrencyParam(input.optString("currency", null));
String invoiceNumber = CyberSourceUtil.validateInvoiceNumber(input.optString("invoiceNumber", null));
String referenceNumber = CyberSourceUtil.validateReferenceNumber(input.optString("referenceNumber", null));
String transientToken = CyberSourceUtil.validateTransientToken(input.optString("transientToken", null));

CyberSourceUtil.logPaymentEvent("payment-process", getRequestingUser(), referenceNumber, amount, currency, invoiceNumber);

JSONObject config = CyberSourceUtil.readConfig();

CreatePaymentRequest requestObj = new CreatePaymentRequest();

Ptsv2paymentsClientReferenceInformation clientReferenceInformation = new Ptsv2paymentsClientReferenceInformation();
clientReferenceInformation.code(referenceNumber);
requestObj.clientReferenceInformation(clientReferenceInformation);

Ptsv2paymentsOrderInformation orderInformation = new Ptsv2paymentsOrderInformation();
Ptsv2paymentsOrderInformationAmountDetails amountDetails = new Ptsv2paymentsOrderInformationAmountDetails();
amountDetails.totalAmount(amount);
amountDetails.currency(currency);
orderInformation.amountDetails(amountDetails);
requestObj.orderInformation(orderInformation);

Ptsv2paymentsTokenInformation tokenInformation = new Ptsv2paymentsTokenInformation();
tokenInformation.transientTokenJwt(transientToken);
requestObj.tokenInformation(tokenInformation);

try {
MerchantConfig merchantConfig = CyberSourceUtil.buildMerchantConfig(config);
ApiClient apiClient = new ApiClient();
apiClient.merchantConfig = merchantConfig;

PaymentsApi apiInstance = new PaymentsApi(apiClient);
PtsV2PaymentsPost201Response result = apiInstance.createPayment(requestObj);

LOG.info("CyberSource payment result\t" + referenceNumber + "\t" + result.getStatus() + "\t" + result.getId());

JSONObject responseJson = new JSONObject()
.put("status", result.getStatus())
.put("transactionId", result.getId())
.put("referenceNumber", referenceNumber);

return Response.ok(responseJson.toString()).build();
}
catch (ApiException e) {
LOG.error("CyberSource payment API error for reference " + referenceNumber + ": HTTP " + e.getCode() + " " + e.getResponseBody(), e);
throw new WdkRuntimeException("Unable to process CyberSource payment", e);
}
catch (Exception e) {
throw new WdkRuntimeException("Unable to process CyberSource payment", e);
}
}

private static JSONObject parseInput(String body) {
try {
return new JSONObject(body);
}
catch (Exception e) {
throw new BadRequestException("Request body must be a valid JSON object.");
}
}
}
Loading