diff --git a/Model/lib/conifer/roles/conifer/templates/EbrcWebsiteCommon/log4j2.json.j2 b/Model/lib/conifer/roles/conifer/templates/EbrcWebsiteCommon/log4j2.json.j2 index bda17d3e..78fd50a8 100644 --- a/Model/lib/conifer/roles/conifer/templates/EbrcWebsiteCommon/log4j2.json.j2 +++ b/Model/lib/conifer/roles/conifer/templates/EbrcWebsiteCommon/log4j2.json.j2 @@ -185,7 +185,7 @@ "level": "info", "AppenderRef": { "ref": "page-view-log" } },{ - "name": "org.eupathdb.common.service.CyberSourceFormService", + "name": "org.eupathdb.common.service.CyberSourceLogger", "additivity": "false", "level": "info", "AppenderRef": { "ref": "payment-form-request-log" } diff --git a/Model/pom.xml b/Model/pom.xml index 0c7e9f15..26c35590 100644 --- a/Model/pom.xml +++ b/Model/pom.xml @@ -150,6 +150,12 @@ Jackfish + + com.cybersource + cybersource-rest-client-java + 0.0.93 + + se.jiderhamn.classloader-leak-prevention diff --git a/Model/src/main/java/org/eupathdb/common/service/CyberSourceCaptureContextService.java b/Model/src/main/java/org/eupathdb/common/service/CyberSourceCaptureContextService.java new file mode 100644 index 00000000..48abaab6 --- /dev/null +++ b/Model/src/main/java/org/eupathdb/common/service/CyberSourceCaptureContextService.java @@ -0,0 +1,157 @@ +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.apache.log4j.Logger; +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.UnifiedCheckoutV1CaptureContextApi; +import Invokers.ApiClient; +import Invokers.ApiException; +import Model.GenerateUnifiedCheckoutV1CaptureContextRequest; +import Model.Ucv1sessionsCaptureMandate; +import Model.Ucv1sessionsCompleteMandate; +import Model.Ucv1sessionsData; +import Model.Ucv1sessionsDataOrderInformation; +import Model.Ucv1sessionsDataOrderInformationAmountDetails; + +/** + * 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 (plus SRI integrity hash) of + * the Unified Checkout JS asset to load, as extracted from the capture + * context JWT itself (CyberSource docs require these NOT be hardcoded, since + * they're unique per transaction). + */ +@Path("payment-form-context") +public class CyberSourceCaptureContextService extends AbstractWdkService { + + private static final Logger LOG = Logger.getLogger(CyberSourceCaptureContextService.class); + + // model.prop property containing this site's base URL, e.g. https://plasmodb.org + private static final String LOCALHOST_PROP_KEY = "LOCALHOST"; + + // Pinned to MAJOR.MINOR (the only pinned format the /uc/v1/sessions endpoint + // accepts -- confirmed via its own SDK javadoc and by a live "Invalid Client + // Version '1'" validation error when a bare MAJOR value was tried). This + // still auto-receives patch fixes within v1.0, but won't silently jump to a + // future breaking v2 API. See "Pin to a Version" in CyberSource's + // Server-Side Set Up docs, and the "Version 1 Update Checklist" for what + // changed from v0 (e.g. the v0 Accept(session).unifiedPayments()/up.show() + // API was replaced in v1 by + // VAS.UnifiedCheckout(session)/client.createCheckout()/checkout.mount()). + // Front end must call createCheckout({ autoProcessing: false }) so that + // checkout.mount() resolves with a transient token instead of completing + // the transaction client-side. + private static final String CLIENT_VERSION = "1.0"; + + private static final List ALLOWED_CARD_NETWORKS = Arrays.asList( + "VISA", "MASTERCARD", "AMEX", "DISCOVER", "DINERSCLUB", "JCB"); + + private static final List 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(); + CyberSourceLogger.logPaymentEvent("capture-context", getRequestingUser(), referenceNumber, amount, currency, invoiceNumber); + + JSONObject config = CyberSourceUtil.readConfig(); + String localhost = getLocalhostUrl(); + + GenerateUnifiedCheckoutV1CaptureContextRequest requestObj = new GenerateUnifiedCheckoutV1CaptureContextRequest(); + 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"); + + Ucv1sessionsCaptureMandate captureMandate = new Ucv1sessionsCaptureMandate(); + captureMandate.billingType("FULL"); + captureMandate.requestEmail(true); + captureMandate.requestPhone(false); + captureMandate.requestShipping(false); + captureMandate.showAcceptedNetworkIcons(true); + captureMandate.showConfirmationStep(true); + requestObj.captureMandate(captureMandate); + + // In v1, orderInformation moved under the new top-level `data` object. + Ucv1sessionsDataOrderInformationAmountDetails amountDetails = new Ucv1sessionsDataOrderInformationAmountDetails(); + amountDetails.totalAmount(amount); + amountDetails.currency(currency); + Ucv1sessionsDataOrderInformation orderInformation = new Ucv1sessionsDataOrderInformation(); + orderInformation.amountDetails(amountDetails); + Ucv1sessionsData data = new Ucv1sessionsData(); + data.orderInformation(orderInformation); + requestObj.data(data); + + Ucv1sessionsCompleteMandate completeMandate = new Ucv1sessionsCompleteMandate(); + completeMandate.setType("CAPTURE"); + completeMandate.setDecisionManager(false); + requestObj.setCompleteMandate(completeMandate); + + try { + MerchantConfig merchantConfig = CyberSourceUtil.buildMerchantConfig(config); + ApiClient apiClient = new ApiClient(); + apiClient.merchantConfig = merchantConfig; + + UnifiedCheckoutV1CaptureContextApi apiInstance = new UnifiedCheckoutV1CaptureContextApi(apiClient); + String captureContextJwt = apiInstance.generateUnifiedCheckoutV1CaptureContext(requestObj); + + // Per CyberSource docs, the JS library URL and its SRI hash must be read + // out of the capture context JWT itself (ctx[0].data.clientLibrary / + // clientLibraryIntegrity) rather than hardcoded/guessed, since they are + // unique to each transaction and can change without notice. + JSONObject ctxData = CyberSourceUtil.decodeJwtPayload(captureContextJwt) + .getJSONArray("ctx").getJSONObject(0).getJSONObject("data"); + + JSONObject responseJson = new JSONObject() + .put("captureContext", captureContextJwt) + .put("referenceNumber", referenceNumber) + .put("scriptUrl", ctxData.getString("clientLibrary")) + .put("scriptIntegrity", ctxData.optString("clientLibraryIntegrity", null)); + + return Response.ok(responseJson.toString()).build(); + } + catch (ApiException e) { + LOG.error("CyberSource capture-context API error for reference " + referenceNumber + ": HTTP " + e.getCode() + " " + e.getResponseBody(), e); + throw new WdkRuntimeException("Unable to generate CyberSource capture context", e); + } + 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; + } + +} diff --git a/Model/src/main/java/org/eupathdb/common/service/CyberSourceFormService.java b/Model/src/main/java/org/eupathdb/common/service/CyberSourceFormService.java index bdb051ea..9e06b284 100644 --- a/Model/src/main/java/org/eupathdb/common/service/CyberSourceFormService.java +++ b/Model/src/main/java/org/eupathdb/common/service/CyberSourceFormService.java @@ -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. */ @@ -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 @@ -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)); } diff --git a/Model/src/main/java/org/eupathdb/common/service/CyberSourceLogger.java b/Model/src/main/java/org/eupathdb/common/service/CyberSourceLogger.java new file mode 100644 index 00000000..9c21ea74 --- /dev/null +++ b/Model/src/main/java/org/eupathdb/common/service/CyberSourceLogger.java @@ -0,0 +1,21 @@ +package org.eupathdb.common.service; + +import org.apache.log4j.Logger; +import org.gusdb.wdk.model.user.User; + +public class CyberSourceLogger { + + private static final Logger LOG = Logger.getLogger(CyberSourceLogger.class); + + static void logPaymentEvent(String stage, User requestingUser, String referenceNumber, String amount, String currency, String invoiceNumber) { + LOG.info("\t" + String.join("\t", + stage, + String.valueOf(requestingUser.getUserId()), + "guest=" + requestingUser.isGuest(), + referenceNumber, + amount, + currency, + invoiceNumber + )); + } +} diff --git a/Model/src/main/java/org/eupathdb/common/service/CyberSourcePaymentService.java b/Model/src/main/java/org/eupathdb/common/service/CyberSourcePaymentService.java new file mode 100644 index 00000000..8b0f3eea --- /dev/null +++ b/Model/src/main/java/org/eupathdb/common/service/CyberSourcePaymentService.java @@ -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)); + + CyberSourceLogger.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."); + } + } +} diff --git a/Model/src/main/java/org/eupathdb/common/service/CyberSourceUtil.java b/Model/src/main/java/org/eupathdb/common/service/CyberSourceUtil.java new file mode 100644 index 00000000..cc56cccb --- /dev/null +++ b/Model/src/main/java/org/eupathdb/common/service/CyberSourceUtil.java @@ -0,0 +1,193 @@ +package org.eupathdb.common.service; + +import java.io.FileReader; +import java.io.IOException; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Date; +import java.util.Properties; +import java.util.Random; +import java.util.regex.Pattern; + +import javax.ws.rs.BadRequestException; + +import org.gusdb.fgputil.IoUtil; +import org.gusdb.wdk.model.WdkRuntimeException; +import org.json.JSONObject; + +import com.cybersource.authsdk.core.ConfigException; +import com.cybersource.authsdk.core.MerchantConfig; + +/** + * Shared config/validation/logging support for the Unified Checkout capture + * context and payment-processing services. The config file at + * {@link #CONFIG_FILE_LOCATION} is expected to contain the following keys. + * merchant_id and run_environment come from CyberSource Business Center; the + * key_* fields describe the P12 key file (generated in Business Center under + * Payment Configuration -> Key Management -> API Keys -> JWT) used to sign + * the JWT for authentication. The response_mle_* fields are optional and, if + * present, describe a *separate* P12 key pair (Business Center -> Key + * Management -> API Keys -> REST - API Response MLE) used to decrypt + * CyberSource's encrypted API responses; enable_request_mle optionally turns + * on encryption of our outgoing requests, using a cert normally + * auto-extracted from the auth P12 above (override via + * request_mle_cert_path/request_mle_key_alias if that P12 wasn't generated + * with an embedded MLE cert). CyberSource is making the updated MLE version + * mandatory across the platform by September 2026, so both should be + * configured before then even though they're optional here: + * + *
+ * {
+ *   "merchant_id": "...",
+ *   "run_environment": "apitest.cybersource.com", // or "api.cybersource.com" in production
+ *   "keys_directory": "/usr/local/tomcat_instances/shared",
+ *   "key_file_name": "...", // .p12 filename, without the .p12 extension
+ *   "key_alias": "...",
+ *   "key_password": "...",
+ *   "enable_request_mle": true,
+ *   "request_mle_cert_path": "...",   // optional override
+ *   "request_mle_key_alias": "...",   // optional override
+ *   "response_mle_key_path": "...",   // path to the separate response-MLE .p12
+ *   "response_mle_key_password": "...",
+ *   "response_mle_kid": "..."         // optional; required only for non-P12 key formats
+ * }
+ * 
+ */ +class CyberSourceUtil { + + // location of file containing cybersource REST API credentials + //private static final String CONFIG_FILE_LOCATION = "/usr/local/tomcat_instances/shared/.cybersource.config.json"; + private static final String CONFIG_FILE_LOCATION = "/home/rdoherty/cybersource/.cybersource.config.json"; + + // regex to recognize proper amount values + private static final Pattern MONEY_PATTERN = Pattern.compile("^[0-9]+(\\.[0-9][0-9])?$"); + + // regex to recognize proper invoice/reference numbers + private static final Pattern INVOICE_NUMBER_PATTERN = Pattern.compile("^[0-9A-Za-z\\-]+$"); + + // will appear in log when invoice param not sent or empty + static final String INVOICE_NOT_SPECIFIED = "Not_Specified"; + + private static final String TEST_RUN_ENVIRONMENT = "apitest.cybersource.com"; + + static String validateAmountParam(String amount) { + if (amount == null || !MONEY_PATTERN.matcher(amount).matches()) { + throw new BadRequestException("'amount' parameter is required and represent a numeric payment amount in US Dollars."); + } + return amount.indexOf(".") == -1 ? amount + ".00" : amount; + } + + // TODO: in the future we will probably support multiple currencies + static String validateCurrencyParam(String currency) { + if (currency != null && !currency.toUpperCase().equals("USD")) { + throw new BadRequestException("'currency' parameter, if passed, must be 'USD'; other currencies are not yet supported"); + } + return "USD"; + } + + static String validateInvoiceNumber(String invoiceNumber) { + if (invoiceNumber == null || invoiceNumber.isBlank()) { + return INVOICE_NOT_SPECIFIED; + } + if (INVOICE_NUMBER_PATTERN.matcher(invoiceNumber).matches()) { + return invoiceNumber; + } + throw new BadRequestException("'invoice_number' parameter is malformed; only alphanumeric and hyphen characters are allowed"); + } + + static String validateReferenceNumber(String referenceNumber) { + if (referenceNumber == null || !INVOICE_NUMBER_PATTERN.matcher(referenceNumber).matches()) { + throw new BadRequestException("'reference_number' parameter is required and must match the value returned by the capture-context request."); + } + return referenceNumber; + } + + static String validateTransientToken(String transientToken) { + if (transientToken == null || transientToken.isBlank()) { + throw new BadRequestException("'transient_token' parameter is required."); + } + return transientToken; + } + + // reference number for tracking (add 5 random digits at each ms) + static String generateReferenceNumber() { + return String.valueOf(new Date().getTime()) + + String.format("%05d", new Random().nextInt(100000)); + } + + static JSONObject readConfig() { + try (Reader in = new FileReader(CONFIG_FILE_LOCATION)) { + return new JSONObject(IoUtil.readAllChars(in)); + } + catch (IOException e) { + throw new WdkRuntimeException("Unable to read/parse config file at: " + CONFIG_FILE_LOCATION, e); + } + } + + static boolean isTestEnvironment(JSONObject config) { + return TEST_RUN_ENVIRONMENT.equals(config.getString("run_environment")); + } + + /** + * Decodes the (unencrypted, base64url) payload segment of a JWT without + * verifying its signature. Used to pull the {@code clientLibrary} / + * {@code clientLibraryIntegrity} values out of the capture-context JWT; + * per CyberSource's docs these must NOT be hardcoded/guessed on our end, + * since they're unique per transaction: + * https://developer.cybersource.com/docs/cybs/en-us/unified-checkout/developer/all/rest/unified-checkout/uc-getting-started-cs-setup-intro/uc-getting-started-cs-js-library-intro.html + */ + static JSONObject decodeJwtPayload(String jwt) { + String[] parts = jwt.split("\\."); + if (parts.length < 2) { + throw new WdkRuntimeException("Malformed JWT returned by CyberSource (expected header.payload.signature)"); + } + byte[] decoded = Base64.getUrlDecoder().decode(parts[1]); + return new JSONObject(new String(decoded, StandardCharsets.UTF_8)); + } + + static MerchantConfig buildMerchantConfig(JSONObject config) { + Properties props = new Properties(); + // JWT auth signed with a P12 key pair (HTTP Signature is deprecated). + props.setProperty("authenticationType", "jwt"); + props.setProperty("merchantID", config.getString("merchant_id")); + props.setProperty("runEnvironment", config.getString("run_environment")); + props.setProperty("keysDirectory", config.getString("keys_directory")); + props.setProperty("keyFileName", config.getString("key_file_name")); + props.setProperty("keyAlias", config.getString("key_alias")); + props.setProperty("keyPass", config.getString("key_password")); + props.setProperty("enableLog", "false"); + + // Request MLE: encrypts the JSON we send to CyberSource. The cert is + // auto-extracted from the auth P12 above (via the default + // "CyberSource_SJC_US" alias) unless overridden below. + if (config.optBoolean("enable_request_mle", false)) { + props.setProperty("enableRequestMLEForOptionalApisGlobally", "true"); + if (config.has("request_mle_cert_path")) { + props.setProperty("mleForRequestPublicCertPath", config.getString("request_mle_cert_path")); + } + if (config.has("request_mle_key_alias")) { + props.setProperty("requestMleKeyAlias", config.getString("request_mle_key_alias")); + } + } + + // Response MLE: decrypts CyberSource's JSON responses using the + // dedicated "REST - API Response MLE" P12 key pair (separate from the + // auth key pair above). + if (config.has("response_mle_key_path")) { + props.setProperty("enableResponseMleGlobally", "true"); + props.setProperty("responseMlePrivateKeyFilePath", config.getString("response_mle_key_path")); + props.setProperty("responseMlePrivateKeyFilePassword", config.getString("response_mle_key_password")); + if (config.has("response_mle_kid")) { + props.setProperty("responseMleKID", config.getString("response_mle_kid")); + } + } + + try { + return new MerchantConfig(props); + } + catch (ConfigException e) { + throw new WdkRuntimeException("Invalid CyberSource merchant configuration", e); + } + } +} diff --git a/Model/src/main/java/org/eupathdb/common/service/EuPathServiceApplication.java b/Model/src/main/java/org/eupathdb/common/service/EuPathServiceApplication.java index 29a0f0b1..43466b59 100644 --- a/Model/src/main/java/org/eupathdb/common/service/EuPathServiceApplication.java +++ b/Model/src/main/java/org/eupathdb/common/service/EuPathServiceApplication.java @@ -27,6 +27,8 @@ public Set> getClasses() { .add(BlastFormInternalValuesService.class) .add(UserProfileVocabulariesService.class) .add(CyberSourceFormService.class) + .add(CyberSourceCaptureContextService.class) + .add(CyberSourcePaymentService.class) .add(RawFileDownloadService.class) .add(ConfigurableRedirectsService.class) .add(PubMedProxyService.class)