diff --git a/build.gradle b/build.gradle index ef29e02..d2331a4 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,10 @@ import org.gradle.api.XmlProvider import org.gradle.api.artifacts.maven.MavenDeployment +plugins { + id 'biz.aQute.bnd.builder' +} + // NetBeans will automatically add "run" and "debug" tasks relying on the // "mainClass" property. You may however define the property prior executing // tasks by passing a "-PmainClass=" argument. @@ -13,16 +17,15 @@ if (!hasProperty('mainClass')) { ext.mainClass = 'com.beanstream.api.test.SampleTransactions' } - apply plugin: 'java' apply plugin: 'maven' apply plugin: 'signing' -apply plugin: 'osgi' +//apply plugin: 'osgi' sourceCompatibility = '1.7' group = "com.beanstream.api" -version = "1.0.0" +version = "1.0.0-modified" diff --git a/settings.gradle b/settings.gradle index 0492399..760957e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,7 @@ +pluginManagement { + plugins { + id 'biz.aQute.bnd.builder' version '5.3.0' + } +} + rootProject.name='beanstream' diff --git a/src/main/java/com/beanstream/api/PaymentsAPI.java b/src/main/java/com/beanstream/api/PaymentsAPI.java index eff4d06..1e8157d 100644 --- a/src/main/java/com/beanstream/api/PaymentsAPI.java +++ b/src/main/java/com/beanstream/api/PaymentsAPI.java @@ -22,6 +22,17 @@ */ package com.beanstream.api; +import static com.beanstream.connection.BeanstreamUrls.getPaymentUrl; +import static com.beanstream.connection.BeanstreamUrls.getPreAuthCompletionsUrl; +import static com.beanstream.connection.BeanstreamUrls.getReturnUrl; +import static com.beanstream.connection.BeanstreamUrls.getUnreferencedReturnUrl; +import static com.beanstream.connection.BeanstreamUrls.getVoidPaymentUrl; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; + +import org.apache.http.HttpStatus; + import com.beanstream.Configuration; import com.beanstream.Gateway; import com.beanstream.connection.BeanstreamUrls; @@ -31,23 +42,19 @@ import com.beanstream.requests.CardPaymentRequest; import com.beanstream.requests.CashPaymentRequest; import com.beanstream.requests.ChequePaymentRequest; -import com.beanstream.requests.TokenPaymentRequest; +import com.beanstream.requests.InteracPaymentRequest; +import com.beanstream.requests.PaymentRequest; +import com.beanstream.requests.ProfilePaymentRequest; import com.beanstream.requests.ReturnRequest; +import com.beanstream.requests.TokenPaymentRequest; import com.beanstream.requests.UnreferencedCardReturnRequest; import com.beanstream.requests.UnreferencedSwipeReturnRequest; import com.beanstream.responses.BeanstreamResponse; +import com.beanstream.responses.InteracPaymentResponse; import com.beanstream.responses.PaymentResponse; import com.google.gson.Gson; import com.google.gson.JsonObject; -import org.apache.http.HttpStatus; - - - -import static com.beanstream.connection.BeanstreamUrls.*; -import com.beanstream.requests.PaymentRequest; -import com.beanstream.requests.ProfilePaymentRequest; - /** * The entry point for processing payments. * @@ -57,6 +64,7 @@ public class PaymentsAPI { private static final String AMOUNT_PARAM = "amount"; private static final String MERCHANT_ID_PARAM = "merchant_id"; + private static ThreadLocal communicationLog = new ThreadLocal<>(); private Configuration config; private HttpsConnector connector; private final Gson gson = new Gson(); @@ -67,6 +75,40 @@ public PaymentsAPI(Configuration config) { config.getPaymentsApiPasscode()); connector.setCustomHttpClient(config.getCustomHttpClient()); } + + public static String getLog() { + StringBuffer sb = communicationLog.get(); + if (sb!=null) { + try { + return sb.toString(); + } finally { + clearLog(); + } + } + return null; + } + + private static StringBuffer getLogInternal() { + StringBuffer sb = communicationLog.get(); + if (sb==null) { + sb = new StringBuffer(); + communicationLog.set( sb ); + } + return sb; + } + + public static void addLog(String str) { + if (str!=null && str.length()>0) { + StringBuffer sb = getLogInternal(); + if (sb.length()>0) + sb.append( "\n\n" ); + sb.append( str ); + } + } + + public static void clearLog() { + communicationLog.remove(); + } public void setConfig(Configuration config) { this.config = config; @@ -88,16 +130,23 @@ public void setConfig(Configuration config) { */ public PaymentResponse makePayment(CardPaymentRequest paymentRequest) throws BeanstreamApiException { - paymentRequest.setMerchantId("" + config.getMerchantId()); + clearLog(); + + paymentRequest.setMerchantId("" + config.getMerchantId()); paymentRequest.getCard().setComplete(true); // false for pre-auth // build the URL String url = BeanstreamUrls.getPaymentUrl(config.getPlatform(), config.getVersion()); + + addLog(url); + addLog(gson.toJson( paymentRequest )); // process the transaction using the REST API String response = connector.ProcessTransaction(HttpMethod.post, url, paymentRequest); + addLog(response); + // parse the output and return a PaymentResponse return gson.fromJson(response, PaymentResponse.class); } @@ -113,15 +162,22 @@ public PaymentResponse makePayment(CardPaymentRequest paymentRequest) * or any other error */ public PaymentResponse makePayment(TokenPaymentRequest paymentRequest) throws BeanstreamApiException { - paymentRequest.setMerchantId("" + config.getMerchantId()); + clearLog(); + + paymentRequest.setMerchantId("" + config.getMerchantId()); paymentRequest.getToken().setComplete(true); // true to make the payment // build the URL String url = BeanstreamUrls.getPaymentUrl( config.getPlatform(), config.getVersion()); + addLog(url); + addLog(gson.toJson( paymentRequest )); + // process the transaction using the REST API String response = connector.ProcessTransaction(HttpMethod.post, url, paymentRequest); + addLog(response); + // parse the output and return a PaymentResponse return gson.fromJson(response, PaymentResponse.class); } @@ -136,19 +192,86 @@ public PaymentResponse makePayment(TokenPaymentRequest paymentRequest) throws Be * or any other error */ public PaymentResponse makePayment(ProfilePaymentRequest paymentRequest) throws BeanstreamApiException { - paymentRequest.setMerchantId("" + config.getMerchantId()); + clearLog(); + + paymentRequest.setMerchantId("" + config.getMerchantId()); paymentRequest.getProfile().setComplete(true); // true to make the payment // build the URL String url = BeanstreamUrls.getPaymentUrl( config.getPlatform(), config.getVersion()); + addLog(url); + addLog(gson.toJson( paymentRequest )); + // process the transaction using the REST API String response = connector.ProcessTransaction(HttpMethod.post, url, paymentRequest); + addLog(response); + // parse the output and return a PaymentResponse return gson.fromJson(response, PaymentResponse.class); } + /** + * Make an interac payment request. + * + * @author ilya + * @param paymentRequest the interac payment request + * @return InteracPaymentResponse the result of the interac payment request + * @throws BeanstreamApiException as a result of a business logic validation + * or any other error @see + */ + public InteracPaymentResponse interacPayment(InteracPaymentRequest paymentRequest) throws BeanstreamApiException { + clearLog(); + + // build the URL + String url = BeanstreamUrls.getPaymentUrl( config.getPlatform(), config.getVersion()); + + addLog(url); + addLog(gson.toJson( paymentRequest )); + + // process the transaction using the REST API + String response = connector.ProcessTransaction(HttpMethod.post, url, paymentRequest); + + addLog(response); + + // parse the output and return a InteracPaymentResponse + InteracPaymentResponse res = gson.fromJson(response, InteracPaymentResponse.class); + try { + // contents is URL-encoded for some reason + res.contents = URLDecoder.decode( res.contents, "UTF-8" ); + } catch (UnsupportedEncodingException e) {} + return res; + } + + /** + * Complete the interac payment. + * + * @author ilya + * @param merchantData - this value should be stored in session while client is redirected to financial institution website + * @param paymentRequest the interac payment request should have InteracResponse object populated (important!) + * @return PaymentResponse the result of the interac payment transaction + * @throws BeanstreamApiException as a result of a business logic validation + * or any other error @see + */ + public PaymentResponse interacPaymentCompletion(String merchantData, InteracPaymentRequest paymentRequest) throws BeanstreamApiException { + clearLog(); + + // build the URL + String url = BeanstreamUrls.getPaymentContinuationsUrl( config.getPlatform(), config.getVersion(), merchantData); + + addLog(url); + addLog(gson.toJson( paymentRequest )); + + // process the transaction using the REST API + String response = connector.ProcessTransaction(HttpMethod.post, url, paymentRequest); + + addLog(response); + + // parse the output and return a PaymentResponse + return gson.fromJson(response, PaymentResponse.class); + } + /** * Make a cash payment. * @@ -159,13 +282,19 @@ public PaymentResponse makePayment(ProfilePaymentRequest paymentRequest) throws * or any other error @see */ public PaymentResponse makePayment(CashPaymentRequest paymentRequest) throws BeanstreamApiException { + clearLog(); // build the URL String url = BeanstreamUrls.getPaymentUrl( config.getPlatform(), config.getVersion()); + addLog(url); + addLog(gson.toJson( paymentRequest )); + // process the transaction using the REST API String response = connector.ProcessTransaction(HttpMethod.post, url, paymentRequest); + addLog(response); + // parse the output and return a PaymentResponse return gson.fromJson(response, PaymentResponse.class); } @@ -180,13 +309,19 @@ public PaymentResponse makePayment(CashPaymentRequest paymentRequest) throws Bea * or any other error @see */ public PaymentResponse makePayment(ChequePaymentRequest paymentRequest) throws BeanstreamApiException { + clearLog(); // build the URL String url = BeanstreamUrls.getPaymentUrl( config.getPlatform(), config.getVersion()); + addLog(url); + addLog(gson.toJson( paymentRequest )); + // process the transaction using the REST API String response = connector.ProcessTransaction(HttpMethod.post, url, paymentRequest); + addLog(response); + // parse the output and return a PaymentResponse return gson.fromJson(response, PaymentResponse.class); } @@ -211,6 +346,7 @@ public PaymentResponse makePayment(ChequePaymentRequest paymentRequest) throws B */ public PaymentResponse voidPayment(String paymentId, double amount) throws BeanstreamApiException { + clearLog(); Gateway.assertNotEmpty(paymentId, "invalid paymentId"); String url = getVoidPaymentUrl(config.getPlatform(), @@ -221,9 +357,14 @@ public PaymentResponse voidPayment(String paymentId, double amount) String.valueOf(config.getMerchantId())); voidRequest.addProperty(AMOUNT_PARAM, String.valueOf(amount)); + addLog(url); + addLog(gson.toJson( voidRequest )); + String response = connector.ProcessTransaction(HttpMethod.post, url, voidRequest); + addLog(response); + // parse the output and return a PaymentResponse return gson.fromJson(response, PaymentResponse.class); @@ -246,6 +387,7 @@ public PaymentResponse voidPayment(String paymentId, double amount) */ public PaymentResponse preAuth(CardPaymentRequest paymentRequest) throws BeanstreamApiException { + clearLog(); if (paymentRequest == null || paymentRequest.getCard() == null) { // TODO - do we need to supply category and code ids here? @@ -258,8 +400,14 @@ public PaymentResponse preAuth(CardPaymentRequest paymentRequest) String preAuthUrl = getPaymentUrl(config.getPlatform(), config.getVersion()); + addLog(preAuthUrl); + addLog(gson.toJson( paymentRequest )); + String response = connector.ProcessTransaction(HttpMethod.post, preAuthUrl, paymentRequest); + + addLog(response); + return gson.fromJson(response, PaymentResponse.class); } @@ -280,6 +428,7 @@ public PaymentResponse preAuth(CardPaymentRequest paymentRequest) */ public PaymentResponse preAuth(ProfilePaymentRequest paymentRequest) throws BeanstreamApiException { + clearLog(); if (paymentRequest == null || paymentRequest.getProfile() == null) { // TODO - do we need to supply category and code ids here? @@ -292,8 +441,14 @@ public PaymentResponse preAuth(ProfilePaymentRequest paymentRequest) String preAuthUrl = getPaymentUrl(config.getPlatform(), config.getVersion()); + addLog(preAuthUrl); + addLog(gson.toJson( paymentRequest )); + String response = connector.ProcessTransaction(HttpMethod.post, preAuthUrl, paymentRequest); + + addLog(response); + return gson.fromJson(response, PaymentResponse.class); } @@ -314,6 +469,7 @@ public PaymentResponse preAuth(ProfilePaymentRequest paymentRequest) */ public PaymentResponse preAuth(TokenPaymentRequest paymentRequest) throws BeanstreamApiException { + clearLog(); if (paymentRequest == null || paymentRequest.getToken() == null) { BeanstreamResponse response = BeanstreamResponse.fromMessage("invalid payment request"); @@ -324,7 +480,13 @@ public PaymentResponse preAuth(TokenPaymentRequest paymentRequest) String preAuthUrl = getPaymentUrl(config.getPlatform(), config.getVersion()); + addLog(preAuthUrl); + addLog(gson.toJson( paymentRequest )); + String response = connector.ProcessTransaction(HttpMethod.post, preAuthUrl, paymentRequest); + + addLog(response); + return gson.fromJson(response, PaymentResponse.class); } @@ -338,6 +500,7 @@ public PaymentResponse preAuth(TokenPaymentRequest paymentRequest) * @throws BeanstreamApiException */ public PaymentResponse preAuthCompletion(String paymentId, double amount) throws BeanstreamApiException { + clearLog(); Gateway.assertNotEmpty(paymentId, "Invalid Payment Id"); @@ -349,9 +512,14 @@ public PaymentResponse preAuthCompletion(String paymentId, double amount) throws String.valueOf(config.getMerchantId())); authorizeRequest.addProperty(AMOUNT_PARAM, String.valueOf(amount)); + addLog(authorizePaymentUrl); + addLog(gson.toJson( authorizeRequest )); + String response = connector.ProcessTransaction(HttpMethod.post, authorizePaymentUrl, authorizeRequest); + addLog(response); + return gson.fromJson(response, PaymentResponse.class); } @@ -365,12 +533,19 @@ public PaymentResponse preAuthCompletion(String paymentId, double amount) throws * @throws BeanstreamApiException if the transaction was declined */ public PaymentResponse preAuthCompletion(String paymentId, PaymentRequest request) throws BeanstreamApiException { + clearLog(); Gateway.assertNotEmpty(paymentId, "Invalid Payment Id"); String authorizePaymentUrl = getPreAuthCompletionsUrl( config.getPlatform(), config.getVersion(), paymentId); + addLog(authorizePaymentUrl); + addLog(gson.toJson( request )); + String response = connector.ProcessTransaction(HttpMethod.post, authorizePaymentUrl, request); + + addLog(response); + return gson.fromJson(response, PaymentResponse.class); } @@ -383,6 +558,7 @@ public PaymentResponse preAuthCompletion(String paymentId, PaymentRequest reques * @throws BeanstreamApiException */ public PaymentResponse returnPayment(String paymentId, double amount) throws BeanstreamApiException { + clearLog(); Gateway.assertNotEmpty(paymentId, "Invalid Payment Id"); @@ -392,8 +568,13 @@ public PaymentResponse returnPayment(String paymentId, double amount) throws Bea returnRequest.setMerchantId( String.valueOf(config.getMerchantId()) ); returnRequest.setAmount( amount ); + addLog(returnPaymentUrl); + addLog(gson.toJson( returnRequest )); + String response = connector.ProcessTransaction(HttpMethod.post, returnPaymentUrl, returnRequest); + addLog(response); + return gson.fromJson(response, PaymentResponse.class); } @@ -408,14 +589,20 @@ public PaymentResponse returnPayment(String paymentId, double amount) throws Bea * @throws BeanstreamApiException */ public PaymentResponse unreferencedReturn(UnreferencedCardReturnRequest returnRequest) throws BeanstreamApiException { + clearLog(); String unreferencedReturnUrl = getUnreferencedReturnUrl( config.getPlatform(), config.getVersion()); returnRequest.setMerchantId( String.valueOf(config.getMerchantId()) ); + addLog(unreferencedReturnUrl); + addLog(gson.toJson( returnRequest )); + String response = connector.ProcessTransaction(HttpMethod.post, unreferencedReturnUrl, returnRequest); + addLog(response); + return gson.fromJson(response, PaymentResponse.class); } @@ -430,14 +617,20 @@ public PaymentResponse unreferencedReturn(UnreferencedCardReturnRequest returnRe * @throws BeanstreamApiException */ public PaymentResponse unreferencedReturn(UnreferencedSwipeReturnRequest returnRequest) throws BeanstreamApiException { + clearLog(); String unreferencedReturnUrl = getUnreferencedReturnUrl( config.getPlatform(), config.getVersion()); returnRequest.setMerchantId( String.valueOf(config.getMerchantId()) ); + addLog(unreferencedReturnUrl); + addLog(gson.toJson( returnRequest )); + String response = connector.ProcessTransaction(HttpMethod.post, unreferencedReturnUrl, returnRequest); + addLog(response); + return gson.fromJson(response, PaymentResponse.class); } diff --git a/src/main/java/com/beanstream/api/ProfilesAPI.java b/src/main/java/com/beanstream/api/ProfilesAPI.java index 2b34aad..b4fbd9f 100644 --- a/src/main/java/com/beanstream/api/ProfilesAPI.java +++ b/src/main/java/com/beanstream/api/ProfilesAPI.java @@ -276,7 +276,7 @@ public ProfileResponse updateProfile(PaymentProfile profile) * * @param profileId * of the profile containing the cards - * @return List with all the cards for that profile + * @return List<Card> with all the cards for that profile * @throws BeanstreamApiException * if any validation fails or any error occur */ @@ -365,7 +365,7 @@ public ProfileResponse updateCard(String profileId, Card card) * cards. Make sure your Merchant account can support more cards. The * default amount is 1. You can change this limit in the online Members area * for Merchants located at: https://www.beanstream.com/admin/sDefault.asp - * and heading to Configuration -> Payment Profile Configuration + * and heading to Configuration -> Payment Profile Configuration * * @param profileId * @param card diff --git a/src/main/java/com/beanstream/connection/BeanstreamUrls.java b/src/main/java/com/beanstream/connection/BeanstreamUrls.java index 69e8bf7..bcaa08e 100644 --- a/src/main/java/com/beanstream/connection/BeanstreamUrls.java +++ b/src/main/java/com/beanstream/connection/BeanstreamUrls.java @@ -101,4 +101,11 @@ public static String getPaymentUrl(String platform, String version, return MessageFormat.format(BeanstreamUrls.GetPaymentUrl, platform, version, paymentId); } + + public static String getPaymentContinuationsUrl(String platform, + String version, String merchantData) { + return MessageFormat.format(ContinuationsUrl, platform, version, + merchantData); + } + } diff --git a/src/main/java/com/beanstream/connection/HttpsConnector.java b/src/main/java/com/beanstream/connection/HttpsConnector.java index 5d0046c..9830d84 100644 --- a/src/main/java/com/beanstream/connection/HttpsConnector.java +++ b/src/main/java/com/beanstream/connection/HttpsConnector.java @@ -23,29 +23,32 @@ package com.beanstream.connection; -import com.beanstream.exceptions.BeanstreamApiException; -import com.beanstream.responses.BeanstreamResponse; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.sun.org.apache.xerces.internal.impl.dv.util.Base64; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.net.ssl.HttpsURLConnection; + +import org.apache.commons.codec.binary.Base64; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.HttpClient; import org.apache.http.client.ResponseHandler; -import org.apache.http.client.methods.*; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; -import javax.net.ssl.HttpsURLConnection; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.util.logging.Level; -import java.util.logging.Logger; -import org.apache.http.client.HttpClient; -import org.apache.http.client.config.RequestConfig; -import org.apache.http.config.ConnectionConfig; -import org.apache.http.impl.client.DefaultHttpClient; +import com.beanstream.api.PaymentsAPI; +import com.beanstream.exceptions.BeanstreamApiException; +import com.beanstream.responses.BeanstreamResponse; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; /** * Performs the connection to the API. @@ -139,7 +142,7 @@ public BeanstreamResponse handleResponse(final HttpResponse http) BeanstreamResponse bsRes = process(http, responseHandler); int httpStatus = bsRes.getHttpStatusCode(); - if (httpStatus >= 200 && httpStatus < 300) { + if ((httpStatus >= 200 && httpStatus < 300) || httpStatus == 302 /* interac payment */) { return bsRes.getResponseBody(); } else { throw mappedException(httpStatus, bsRes); @@ -164,7 +167,7 @@ private BeanstreamResponse process(HttpUriRequest http, httpclient = HttpClients.createDefault(); // Remove newlines from base64 since they can bork the header. // Some base64 encoders will append a newline to the end - String auth = Base64.encode((merchantId + ":" + apiPasscode).trim().getBytes()); + String auth = Base64.encodeBase64String((merchantId + ":" + apiPasscode).trim().getBytes()); http.addHeader("Content-Type", "application/json"); http.addHeader("Authorization", "Passcode " + auth); @@ -208,6 +211,7 @@ private BeanstreamApiException handleException(Exception ex, HttpsURLConnection } else { message = "Connection error"; } + PaymentsAPI.addLog( message ); return new BeanstreamApiException(ex, message); } @@ -218,6 +222,8 @@ private BeanstreamApiException handleException(Exception ex, HttpsURLConnection private BeanstreamApiException mappedException(int status, BeanstreamResponse bsRes) { if (bsRes != null) { + if (bsRes.getResponseBody()!=null) + PaymentsAPI.addLog( bsRes.getResponseBody() ); return BeanstreamApiException.getMappedException(status, bsRes); } diff --git a/src/main/java/com/beanstream/exceptions/BeanstreamApiException.java b/src/main/java/com/beanstream/exceptions/BeanstreamApiException.java index 7c7aaa1..33d9530 100644 --- a/src/main/java/com/beanstream/exceptions/BeanstreamApiException.java +++ b/src/main/java/com/beanstream/exceptions/BeanstreamApiException.java @@ -46,9 +46,12 @@ /// in the 500+ range. Card holders should wait a minute and try the transaction again. /// /// -import com.beanstream.responses.BeanstreamResponse; import org.apache.http.HttpStatus; +import com.beanstream.Gateway; +import com.beanstream.responses.BeanstreamResponse; +import com.beanstream.responses.Detail; + /** * * @author bowens @@ -88,8 +91,13 @@ public static BeanstreamApiException getMappedException(int httpStatusCode, Bean int code = response.getCode(); int category = response.getCategory(); String message = response.getMessage(); - String details = response.getDetails(); - message = message + ", details: " + details; + Detail[] details = response.getDetails(); + if (details!=null && details.length>0) { + message += ", details: "; + for (Detail detail: details) { + message += "\n" + detail.getField() + ": " + detail.getMessage(); + } + } switch (httpStatusCode) { case HttpStatus.SC_MOVED_TEMPORARILY: { // 302 diff --git a/src/main/java/com/beanstream/requests/InteracPaymentRequest.java b/src/main/java/com/beanstream/requests/InteracPaymentRequest.java new file mode 100644 index 0000000..37a933a --- /dev/null +++ b/src/main/java/com/beanstream/requests/InteracPaymentRequest.java @@ -0,0 +1,49 @@ +/* The MIT License (MIT) + * + * Copyright (c) 2014 Beanstream Internet Commerce Corp, Digital River, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.beanstream.requests; + +import com.google.gson.annotations.SerializedName; + +/** + * Process a Interac payment. + * NOTE: You will need to have these payment options ACTIVATED by calling Beanstream + * support at 1-888-472-0811 + * @author ilya + */ +public class InteracPaymentRequest extends PaymentRequest { + @SerializedName("payment_method") + public final String paymentMethod = "interac"; + + @SerializedName("interac_response") + private InteracResponse interacResponse; + + public InteracResponse getInteracResponse() { + if (interacResponse == null) + interacResponse = new InteracResponse(); + return interacResponse; + } + + public void setInteracResponse(InteracResponse interacResponse) { + this.interacResponse = interacResponse; + } +} diff --git a/src/main/java/com/beanstream/requests/InteracResponse.java b/src/main/java/com/beanstream/requests/InteracResponse.java new file mode 100644 index 0000000..e874f89 --- /dev/null +++ b/src/main/java/com/beanstream/requests/InteracResponse.java @@ -0,0 +1,134 @@ +/* The MIT License (MIT) + * + * Copyright (c) 2014 Beanstream Internet Commerce Corp, Digital River, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.beanstream.requests; + +import com.google.gson.annotations.SerializedName; + +/** + * Parameters to complete Interac payment. + * @author ilya + */ +public class InteracResponse { + private String funded; + + @SerializedName("idebit_track2") + private String idebitTrack2; + + @SerializedName("idebit_isslang") + private String idebitIsslang; + + @SerializedName("idebit_version") + private String idebitVersion; + + @SerializedName("idebit_issconf") + private String idebitIssconf; + + @SerializedName("idebit_issname") + private String idebitIssname; + + @SerializedName("idebit_amount") + private String idebitAmount; + + @SerializedName("idebit_invoice") + private String idebitInvoice; + + public String getFunded() { + return funded; + } + + public void setFunded( String funded ) { + if (funded != null && funded.length() > 20) + throw new IllegalArgumentException("Funded cannot be longer than 20 characters!"); + this.funded = funded; + } + + public String getIdebitTrack2() { + return idebitTrack2; + } + + public void setIdebitTrack2( String idebitTrack2 ) { + if (idebitTrack2 != null && idebitTrack2.length() > 256) + throw new IllegalArgumentException("IdebitTrack2 cannot be longer than 256 characters!"); + this.idebitTrack2 = idebitTrack2; + } + + public String getIdebitIsslang() { + return idebitIsslang; + } + + public void setIdebitIsslang( String idebitIsslang ) { + if (idebitIsslang != null && idebitIsslang.length() > 2) + throw new IllegalArgumentException("IdebitIsslang cannot be longer than 2 characters!"); + this.idebitIsslang = idebitIsslang; + } + + public String getIdebitVersion() { + return idebitVersion; + } + + public void setIdebitVersion( String idebitVersion ) { + if (idebitVersion != null && idebitVersion.length() > 1) + throw new IllegalArgumentException("IdebitVersion cannot be longer than 1 character!"); + this.idebitVersion = idebitVersion; + } + + public String getIdebitIssconf() { + return idebitIssconf; + } + + public void setIdebitIssconf( String idebitIssconf ) { + if (idebitIssconf != null && idebitIssconf.length() > 32) + throw new IllegalArgumentException("IdebitIssconf cannot be longer than 32 characters!"); + this.idebitIssconf = idebitIssconf; + } + + public String getIdebitIssname() { + return idebitIssname; + } + + public void setIdebitIssname( String idebitIssname ) { + if (idebitIssname != null && idebitIssname.length() > 32) + throw new IllegalArgumentException("IdebitIssname cannot be longer than 32 characters!"); + this.idebitIssname = idebitIssname; + } + + public String getIdebitAmount() { + return idebitAmount; + } + + public void setIdebitAmount( String idebitAmount ) { + if (idebitAmount != null && idebitAmount.length() > 9) + throw new IllegalArgumentException("IdebitAmount cannot be longer than 9 characters!"); + this.idebitAmount = idebitAmount; + } + + public String getIdebitInvoice() { + return idebitInvoice; + } + + public void setIdebitInvoice( String idebitInvoice ) { + if (idebitInvoice != null && idebitInvoice.length() > 20) + throw new IllegalArgumentException("IdebitInvoice cannot be longer than 20 characters!"); + this.idebitInvoice = idebitInvoice; + } +} diff --git a/src/main/java/com/beanstream/requests/PaymentRequest.java b/src/main/java/com/beanstream/requests/PaymentRequest.java index 10f0976..a508332 100644 --- a/src/main/java/com/beanstream/requests/PaymentRequest.java +++ b/src/main/java/com/beanstream/requests/PaymentRequest.java @@ -119,6 +119,8 @@ public PaymentRequest setComments(String comments) { } public Address getBilling() { + if (billing==null) + billing = new Address(); return billing; } @@ -127,6 +129,8 @@ public void setBilling(Address billing) { } public Address getShipping() { + if (shipping==null) + shipping = new Address(); return shipping; } @@ -135,6 +139,8 @@ public void setShipping(Address shipping) { } public CustomFields getCustom() { + if (custom==null) + custom = new CustomFields(); return custom; } diff --git a/src/main/java/com/beanstream/responses/BeanstreamResponse.java b/src/main/java/com/beanstream/responses/BeanstreamResponse.java index 48dab07..67586cd 100644 --- a/src/main/java/com/beanstream/responses/BeanstreamResponse.java +++ b/src/main/java/com/beanstream/responses/BeanstreamResponse.java @@ -1,13 +1,18 @@ package com.beanstream.responses; -import com.beanstream.exceptions.BeanstreamApiException; -import com.google.common.net.MediaType; -import com.google.gson.*; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.util.EntityUtils; -import java.io.IOException; +import com.beanstream.exceptions.BeanstreamApiException; +import com.google.common.net.MediaType; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; public class BeanstreamResponse { @@ -15,14 +20,12 @@ public class BeanstreamResponse { private final int category; private final String message; private final String reference; - private final String details; + private final Detail[] details; private final int httpStatusCode; private final String responseBody; private final MediaType mediaType; - private static final Gson gson = new Gson(); - - BeanstreamResponse(int code, int category, String message, String reference, String details, int httpStatusCode, + BeanstreamResponse(int code, int category, String message, String reference, Detail[] details, int httpStatusCode, String responseBody, MediaType mediaType) { this.code = code; this.category = category; @@ -73,7 +76,7 @@ public static BeanstreamResponse fromHttpResponse(HttpResponse http) { MediaType responseType = contentType != null ? MediaType.parse(contentType) : null; // If the payload isn't json, or we got a 2XX response, just populate the responseBody field with the payload - if (responseType == null || responseType != MediaType.JSON_UTF_8 || (httpStatusCode >= 200 && httpStatusCode < 300)) { + if (responseType == null || responseType != MediaType.JSON_UTF_8 || (httpStatusCode >= 200 && httpStatusCode < 300) || httpStatusCode == 302 /* for interac */) { return new BeanstreamResponseBuilder() .withHttpStatusCode(httpStatusCode) .withResponseBody(jsonPayload) @@ -89,7 +92,8 @@ private static BeanstreamResponse fromJson(int httpStatusCode, String jsonPayloa JsonParser parser = new JsonParser(); JsonObject json = parser.parse(jsonPayload).getAsJsonObject(); - BeanstreamResponseBuilder builder = new BeanstreamResponseBuilder(); + BeanstreamResponseBuilder builder = new BeanstreamResponseBuilder() + .withResponseBody( jsonPayload ); builder.setMediaType(responseType); @@ -116,8 +120,27 @@ private static BeanstreamResponse fromJson(int httpStatusCode, String jsonPayloa } element = json.get("details"); - if (element != null && !element.isJsonNull()) { - builder.withDetails(element.toString()); + if (element != null && element.isJsonArray()) { + List details = new ArrayList<>(); + for (JsonElement elem: element.getAsJsonArray()) { + if (elem != null && elem.isJsonObject()) { + JsonObject obj = elem.getAsJsonObject(); + String field = null; + String message = null; + element = obj.get( "field" ); + if (element != null && !element.isJsonNull()) { + field = element.getAsString(); + } + element = obj.get( "message" ); + if (element != null && !element.isJsonNull()) { + message = element.getAsString(); + } + if (field!=null && message!=null) { + details.add( new Detail(field, message) ); + } + } + } + builder.withDetails(details.toArray(new Detail[details.size()])); } builder.withHttpStatusCode(httpStatusCode); @@ -140,7 +163,7 @@ public String getReference() { return reference; } - public String getDetails() { + public Detail[] getDetails() { return details; } diff --git a/src/main/java/com/beanstream/responses/BeanstreamResponseBuilder.java b/src/main/java/com/beanstream/responses/BeanstreamResponseBuilder.java index 54a34f9..d37aff3 100644 --- a/src/main/java/com/beanstream/responses/BeanstreamResponseBuilder.java +++ b/src/main/java/com/beanstream/responses/BeanstreamResponseBuilder.java @@ -8,7 +8,7 @@ public class BeanstreamResponseBuilder { private int category = -1; private String message = ""; private String reference = ""; - private String details = ""; + private Detail[] details = new Detail[0]; private int httpStatusCode = -1; private String responseBody = ""; private MediaType mediaType = null; @@ -43,7 +43,7 @@ public BeanstreamResponseBuilder withResponseBody(String responseBody) { return this; } - public BeanstreamResponseBuilder withDetails(String details) { + public BeanstreamResponseBuilder withDetails(Detail[] details) { this.details = details; return this; } diff --git a/src/main/java/com/beanstream/responses/Detail.java b/src/main/java/com/beanstream/responses/Detail.java new file mode 100644 index 0000000..17b24f6 --- /dev/null +++ b/src/main/java/com/beanstream/responses/Detail.java @@ -0,0 +1,16 @@ +package com.beanstream.responses; + +public class Detail { + private final String field; + private final String message; + public Detail(String field, String message) { + this.field = field; + this.message = message; + } + public String getField() { + return field; + } + public String getMessage() { + return message; + } +} diff --git a/src/main/java/com/beanstream/responses/InteracOnline.java b/src/main/java/com/beanstream/responses/InteracOnline.java new file mode 100644 index 0000000..0cd5189 --- /dev/null +++ b/src/main/java/com/beanstream/responses/InteracOnline.java @@ -0,0 +1,38 @@ +/* The MIT License (MIT) + * + * Copyright (c) 2014 Beanstream Internet Commerce Corp, Digital River, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.beanstream.responses; + +import com.google.gson.annotations.SerializedName; + +/** + * Interac Online payment result. + * + * @author ilya + */ +public class InteracOnline { + @SerializedName("idebit_issconf") + public String idebitIssconf; + + @SerializedName("idebit_issname") + public String idebitIssname; +} diff --git a/src/main/java/com/beanstream/responses/InteracPaymentResponse.java b/src/main/java/com/beanstream/responses/InteracPaymentResponse.java new file mode 100644 index 0000000..3a72ad6 --- /dev/null +++ b/src/main/java/com/beanstream/responses/InteracPaymentResponse.java @@ -0,0 +1,47 @@ +/* The MIT License (MIT) + * + * Copyright (c) 2014 Beanstream Internet Commerce Corp, Digital River, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.beanstream.responses; + +import com.google.gson.annotations.SerializedName; + +/** + * The response for interac payment request. + * Before returning the response to your users HTML client, you will need to save + * the merchant_data string in the users session. This value can be used as the {id} value + * when creating your 'continue' endpoint URL for the final request in step 3. + * The response will have HTML in the contents. This should be embedded in the user's + * browser client and this needs to be displayed to the customer to redirect them to the + * Interac login page. Here the customer will log onto their bank account and approve the payment. + * An approved or declined payment will forward the customer back to the + * Funded or Non-funded URLs (respectively) on your website. + * + * @author ilya + */ +public class InteracPaymentResponse { + @SerializedName("merchant_data") + public String merchantData; + + public String contents; + + public Link[] links; +} diff --git a/src/main/java/com/beanstream/responses/PaymentResponse.java b/src/main/java/com/beanstream/responses/PaymentResponse.java index 5a25955..457ed6e 100644 --- a/src/main/java/com/beanstream/responses/PaymentResponse.java +++ b/src/main/java/com/beanstream/responses/PaymentResponse.java @@ -55,6 +55,9 @@ public class PaymentResponse { public CardResponse card; + @SerializedName("interac_online") + public InteracOnline interacOnline; + public Link[] links; public boolean isApproved() { diff --git a/src/test/java/com/beanstream/api/test/PaymentAPITest.java b/src/test/java/com/beanstream/api/test/PaymentAPITest.java index fca606f..aa66505 100644 --- a/src/test/java/com/beanstream/api/test/PaymentAPITest.java +++ b/src/test/java/com/beanstream/api/test/PaymentAPITest.java @@ -42,8 +42,8 @@ public void preAuthCompletionGreaterAmount() throws BeanstreamApiException { } } } catch (BeanstreamApiException ex) { - Assert.assertEquals("Http status code did not match expected.", 400, ex.getHttpStatusCode()); - Assert.assertEquals("Error category did not match expected", 2, ex.getCategory()); + Assert.assertEquals("Http status code did not match expected.", 402, ex.getHttpStatusCode()); + Assert.assertEquals("Completion greater than remaining reserve amount., details: ", 2, ex.getCategory()); Assert.assertEquals("Error code did not match expected", 208, ex.getCode()); } } diff --git a/src/test/java/com/beanstream/api/test/ReportsAPITest.java b/src/test/java/com/beanstream/api/test/ReportsAPITest.java index 9111265..f8104bc 100644 --- a/src/test/java/com/beanstream/api/test/ReportsAPITest.java +++ b/src/test/java/com/beanstream/api/test/ReportsAPITest.java @@ -117,7 +117,7 @@ public void testGetSingleTransactionRecordById() { Transaction transaction = beanstream.reports().getTransaction(payment.id); Assert.assertNotNull(transaction); - Assert.assertEquals("Amounts did not match", "90.0", transaction.getAmount()); + Assert.assertEquals("Amounts did not match", "90.00", transaction.getAmount()); } catch (BeanstreamApiException ex) { Logger.getLogger(ReportsAPITest.class.getName()).log(Level.SEVERE, "Error accessing Beanstream API "+ex.getCode()+", "+ex.getCategory()+", "+ex.getMessage(), ex); diff --git a/src/test/java/com/beanstream/api/test/SampleTransactions.java b/src/test/java/com/beanstream/api/test/SampleTransactions.java index da09369..c33d806 100644 --- a/src/test/java/com/beanstream/api/test/SampleTransactions.java +++ b/src/test/java/com/beanstream/api/test/SampleTransactions.java @@ -1,5 +1,19 @@ package com.beanstream.api.test; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.apache.http.client.HttpClient; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.impl.client.HttpClients; +import org.junit.Assert; +import org.junit.Test; + import com.beanstream.Gateway; import com.beanstream.connection.HttpMethod; import com.beanstream.connection.HttpsConnector; @@ -14,6 +28,7 @@ import com.beanstream.requests.CashPaymentRequest; import com.beanstream.requests.ChequePaymentRequest; import com.beanstream.requests.Criteria; +import com.beanstream.requests.InteracPaymentRequest; import com.beanstream.requests.LegatoTokenRequest; import com.beanstream.requests.Operators; import com.beanstream.requests.ProfilePaymentRequest; @@ -21,6 +36,7 @@ import com.beanstream.requests.QueryFields; import com.beanstream.requests.TokenPaymentRequest; import com.beanstream.responses.BeanstreamResponse; +import com.beanstream.responses.InteracPaymentResponse; import com.beanstream.responses.LegatoTokenResponse; import com.beanstream.responses.PaymentResponse; import com.beanstream.responses.ProfileResponse; @@ -28,20 +44,6 @@ import com.google.gson.GsonBuilder; import com.google.gson.JsonSyntaxException; -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.Date; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.logging.Level; -import java.util.logging.Logger; -import org.apache.http.client.HttpClient; -import org.apache.http.client.config.RequestConfig; -import org.apache.http.impl.client.HttpClients; - -import org.junit.Assert; -import org.junit.Test; - /* The MIT License (MIT) * * Copyright (c) 2014 Beanstream Internet Commerce Corp, Digital River, Inc. @@ -73,8 +75,8 @@ public class SampleTransactions { public static void main(String[] args) { SampleTransactions t = new SampleTransactions(); - /*t.testPayment(); - t.testTokenPayment(); + t.testPayment(); + /*t.testTokenPayment(); t.testVoidPayment(); t.testPreAuthorization(); t.testGetTransaction(); @@ -132,6 +134,26 @@ public void testPayment() { Assert.fail(ex.getMessage()); } + /* Test Interac Payment Request Initialization */ + InteracPaymentRequest interacReq = new InteracPaymentRequest(); + interacReq.setAmount(123.45); + interacReq.setOrderNumber(getRandomOrderId("interac")); + + try { + + InteracPaymentResponse response = beanstream.payments().interacPayment(interacReq); + Assert.assertNotNull( "Merchant data is missing", response.merchantData ); + System.out.println("Merchant data:\n"+ response.merchantData); + Assert.assertNotNull( "Redirect link is missing", response.links ); + Assert.assertEquals( "Redirect link is missing", 1, response.links.length ); + System.out.println("Redirect link to complete payment: "+response.links[0].getHref()); + Assert.assertNotNull( "HTML form is missing", response.contents ); + System.out.println("Interac Form to submit:\n"+ response.contents); + } catch (BeanstreamApiException ex) { + Logger.getLogger(this.getClass().getName()).log(Level.SEVERE,"An error occurred", ex); + Assert.fail(ex.getMessage()); + } + /* Test Cash Payment */ CashPaymentRequest cashReq = new CashPaymentRequest(); cashReq.setAmount(123.45);