/**
* Copyright 2012 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.netflix.hystrix.examples.demo;
import java.math.BigDecimal;
import java.net.HttpCookie;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandProperties;
/**
* This class was originally taken from a functional example using the Authorize.net API
* but was modified for this example to use mock classes so that the real API does not need
* to be depended upon and so that a backend account with Authorize.net is not needed.
*/
// import net.authorize.Environment;
// import net.authorize.TransactionType;
// import net.authorize.aim.Result;
// import net.authorize.aim.Transaction;
/**
* HystrixCommand for submitting credit card payments.
* <p>
* No fallback implemented as a credit card failure must result in an error as no logical fallback exists.
* <p>
* This implementation originated from a functional HystrixCommand wrapper around an Authorize.net API.
* <p>
* The original used the Authorize.net 'duplicate window' setting to ensure an Order could be submitted multiple times
* and it would behave idempotently so that it would not result in duplicate transactions and each would return a successful
* response as if it was the first-and-only execution.
* <p>
* This idempotence (within the duplicate window time frame set to multiple hours) allows for clients that
* experience timeouts and failures to confidently retry the credit card transaction without fear of duplicate
* credit card charges.
* <p>
* This in turn allows the HystrixCommand to be configured for reasonable timeouts and isolation rather than
* letting it go 10+ seconds hoping for success when latency occurs.
* <p>
* In this example, the timeout is set to 3,000ms as normal behavior typically saw a credit card transaction taking around 1300ms
* and in this case it's better to wait longer and try to succeed as the result is a user error.
* <p>
* We do not want to wait the 10,000-20,000ms that Authorize.net can default to as that would allow severe resource
* saturation under high volume traffic when latency spikes.
*/
public class CreditCardCommand extends HystrixCommand<CreditCardAuthorizationResult> {
private final static AuthorizeNetGateway DEFAULT_GATEWAY = new AuthorizeNetGateway();
private final AuthorizeNetGateway gateway;
private final Order order;
private final PaymentInformation payment;
private final BigDecimal amount;
/**
* A HystrixCommand implementation accepts arguments into the constructor which are then accessible
* to the <code>run()</code> method when it executes.
*
* @param order
* @param payment
* @param amount
*/
public CreditCardCommand(Order order, PaymentInformation payment, BigDecimal amount) {
this(DEFAULT_GATEWAY, order, payment, amount);
}
private CreditCardCommand(AuthorizeNetGateway gateway, Order order, PaymentInformation payment, BigDecimal amount) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("CreditCard"))
// defaulting to a fairly long timeout value because failing a credit card transaction is a bad user experience and 'costly' to re-attempt
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter().withExecutionTimeoutInMilliseconds(3000)));
this.gateway = gateway;
this.order = order;
this.payment = payment;
this.amount = amount;
}
/**
* Actual work of submitting the credit card authorization occurs within this <code>HystrixCommand.run()</code> method.
*/
@Override
protected CreditCardAuthorizationResult run() {
// Simulate transitive dependency from CreditCardCommand to GetUserAccountCommand.
// UserAccount could be injected into this command as an argument (and that would be more accurate)
// but often in large codebase that ends up not happening and each library fetches common data
// such as user information directly such as this example.
UserAccount user = new GetUserAccountCommand(new HttpCookie("mockKey", "mockValueFromHttpRequest")).execute();
if (user.getAccountType() == 1) {
// do something
} else {
// do something else
}
// perform credit card transaction
Result<Transaction> result = gateway.submit(payment.getCreditCardNumber(),
String.valueOf(payment.getExpirationMonth()),
String.valueOf(payment.getExpirationYear()),
TransactionType.AUTH_CAPTURE, amount, order);
if (result.isApproved()) {
return CreditCardAuthorizationResult.createSuccessResponse(result.getTarget().getTransactionId(), result.getTarget().getAuthorizationCode());
} else if (result.isDeclined()) {
return CreditCardAuthorizationResult.createFailedResponse(result.getReasonResponseCode() + " : " + result.getResponseText());
} else {
// check for duplicate transaction
if (result.getReasonResponseCode().getResponseReasonCode() == 11) {
if (result.getTarget().getAuthorizationCode() != null) {
// We will treat this as a success as this is telling us we have a successful authorization code
// just that we attempted to re-post it again during the 'duplicateWindow' time period.
// This is part of the idempotent behavior we require so that we can safely timeout and/or fail and allow
// client applications to re-attempt submitting a credit card transaction for the same order again.
// In those cases if the client saw a failure but the transaction actually succeeded, this will capture the
// duplicate response and behave to the client as a success.
return CreditCardAuthorizationResult.createDuplicateSuccessResponse(result.getTarget().getTransactionId(), result.getTarget().getAuthorizationCode());
}
}
// handle all other errors
return CreditCardAuthorizationResult.createFailedResponse(result.getReasonResponseCode() + " : " + result.getResponseText());
/**
* NOTE that in this use case we do not throw an exception for an "error" as this type of error from the service is not a system error,
* but a legitimate usage problem successfully delivered back from the service.
*
* Unexpected errors will be allowed to throw RuntimeExceptions.
*
* The HystrixBadRequestException could potentially be used here, but with such a complex set of errors and reason codes
* it was chosen to stick with the response object approach rather than using an exception.
*/
}
}
/*
* The following inner classes are all mocks based on the Authorize.net API that this class originally used.
*
* They are statically mocked in this example to demonstrate how Hystrix might behave when wrapping this type of call.
*/
public static class AuthorizeNetGateway {
public AuthorizeNetGateway() {
}
public Result<Transaction> submit(String creditCardNumber, String expirationMonth, String expirationYear, TransactionType authCapture, BigDecimal amount, Order order) {
/* simulate varying length of time 800-1500ms which is typical for a credit card transaction */
try {
Thread.sleep((int) (Math.random() * 700) + 800);
} catch (InterruptedException e) {
// do nothing
}
/* and every once in a while we'll cause it to go longer than 3000ms which will cause the command to timeout */
if (Math.random() > 0.99) {
try {
Thread.sleep(8000);
} catch (InterruptedException e) {
// do nothing
}
}
if (Math.random() < 0.8) {
return new Result<Transaction>(true);
} else {
return new Result<Transaction>(false);
}
}
}
public static class Result<T> {
private final boolean approved;
public Result(boolean approved) {
this.approved = approved;
}
public boolean isApproved() {
return approved;
}
public ResponseCode getResponseText() {
return null;
}
public Target getTarget() {
return new Target();
}
public ResponseCode getReasonResponseCode() {
return new ResponseCode();
}
public boolean isDeclined() {
return !approved;
}
}
public static class ResponseCode {
public int getResponseReasonCode() {
return 0;
}
}
public static class Target {
public String getTransactionId() {
return "transactionId";
}
public String getAuthorizationCode() {
return "authorizedCode";
}
}
public static class Transaction {
}
public static enum TransactionType {
AUTH_CAPTURE
}
}