/*
* Copyright 2015 Evgeny Dolganov (evgenij.dolganov@gmail.com).
*
* 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 och.comp.paypal;
import static och.api.model.BaseBean.*;
import static och.api.model.PropKey.*;
import static och.api.model.billing.PaymentProvider.*;
import static och.util.DateUtil.*;
import static och.util.Util.*;
import static urn.ebay.apis.eBLBaseComponents.AckCodeType.*;
import static urn.ebay.apis.eBLBaseComponents.PaymentStatusCodeType.*;
import java.io.File;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import och.api.exception.paypal.PaypalSoapException;
import och.api.model.billing.PayData;
import och.api.model.billing.PaymentBase;
import och.api.model.billing.PaymentStatus;
import och.service.props.Props;
import och.service.props.impl.FileProps;
import och.util.Util;
import och.util.exception.ContinueLoopException;
import org.apache.commons.logging.Log;
import urn.ebay.api.PayPalAPI.DoExpressCheckoutPaymentReq;
import urn.ebay.api.PayPalAPI.DoExpressCheckoutPaymentRequestType;
import urn.ebay.api.PayPalAPI.DoExpressCheckoutPaymentResponseType;
import urn.ebay.api.PayPalAPI.PayPalAPIInterfaceServiceService;
import urn.ebay.api.PayPalAPI.SetExpressCheckoutReq;
import urn.ebay.api.PayPalAPI.SetExpressCheckoutRequestType;
import urn.ebay.api.PayPalAPI.SetExpressCheckoutResponseType;
import urn.ebay.api.PayPalAPI.TransactionSearchReq;
import urn.ebay.api.PayPalAPI.TransactionSearchRequestType;
import urn.ebay.api.PayPalAPI.TransactionSearchResponseType;
import urn.ebay.apis.CoreComponentTypes.BasicAmountType;
import urn.ebay.apis.eBLBaseComponents.AbstractResponseType;
import urn.ebay.apis.eBLBaseComponents.AckCodeType;
import urn.ebay.apis.eBLBaseComponents.CurrencyCodeType;
import urn.ebay.apis.eBLBaseComponents.DoExpressCheckoutPaymentRequestDetailsType;
import urn.ebay.apis.eBLBaseComponents.DoExpressCheckoutPaymentResponseDetailsType;
import urn.ebay.apis.eBLBaseComponents.ErrorType;
import urn.ebay.apis.eBLBaseComponents.ItemCategoryType;
import urn.ebay.apis.eBLBaseComponents.PaymentActionCodeType;
import urn.ebay.apis.eBLBaseComponents.PaymentDetailsItemType;
import urn.ebay.apis.eBLBaseComponents.PaymentDetailsType;
import urn.ebay.apis.eBLBaseComponents.PaymentInfoType;
import urn.ebay.apis.eBLBaseComponents.PaymentTransactionSearchResultType;
import urn.ebay.apis.eBLBaseComponents.SetExpressCheckoutRequestDetailsType;
import urn.ebay.apis.eBLBaseComponents.SeverityCodeType;
public class PaypalSoapClient implements PaypalClient {
public static final String TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
private Log log = getLog(getClass());
private Props sysProps;
private Props conProps;
private CopyOnWriteArrayList<PaypalClientListener> listeners = new CopyOnWriteArrayList<>();
public PaypalSoapClient(Props systemProps, Props connectProps) {
this.sysProps = systemProps;
this.conProps = connectProps;
}
@Override
public String getKey() {
return sysProps.getStrVal(paypal_key);
}
@Override
public void addListener(PaypalClientListener l){
listeners.add(l);
}
@Override
public PayData payWithAccReq(BigDecimal val, long userId) throws Exception {
validateState(val.doubleValue() > 0, "val");
String redirectUrl = conProps.findVal("service.ExpressUrl");
SetExpressCheckoutRequestDetailsType details = new SetExpressCheckoutRequestDetailsType();
details.setReturnURL(sysProps.findVal(httpsServerUrl) + sysProps.getStrVal(paypal_preConfirmUri));
details.setCancelURL(sysProps.findVal(httpsServerUrl) + sysProps.getStrVal(paypal_failUri));
details.setPaymentDetails(list(createPaymentDetailsType(val, userId)));
details.setNoShipping("1");
details.setBrandName("SomeBrand");
SetExpressCheckoutReq req = new SetExpressCheckoutReq();
req.setSetExpressCheckoutRequest(new SetExpressCheckoutRequestType(details));
//soap call
SetExpressCheckoutResponseType resp = getApi().setExpressCheckout(req);
checkForErrors(resp, "payWithAccReq");
String token = resp.getToken();
return new PayData(token, redirectUrl + token);
}
@Override
public PaymentBase finishPayment(String token, String payerId, BigDecimal val, long userId) throws Exception {
validateForEmpty(token, "token");
validateForEmpty(payerId, "payerId");
validateState(val.doubleValue() > 0, "val");
DoExpressCheckoutPaymentRequestDetailsType details = new DoExpressCheckoutPaymentRequestDetailsType();
details.setToken(token);
details.setPayerID(payerId);
details.setPaymentAction(PaymentActionCodeType.SALE);
details.setPaymentDetails(list(createPaymentDetailsType(val, userId)));
DoExpressCheckoutPaymentReq req = new DoExpressCheckoutPaymentReq();
req.setDoExpressCheckoutPaymentRequest(new DoExpressCheckoutPaymentRequestType(details));
DoExpressCheckoutPaymentResponseType resp = getApi().doExpressCheckoutPayment(req);
checkForErrors(resp, "finishPayment");
AckCodeType ack = resp.getAck();
if(ack == SUCCESS){
return createInputPayment(resp);
}
else if(ack == SUCCESSWITHWARNING){
String data = toJson(resp, true);
for(PaypalClientListener l : listeners) l.onPaymentWarning(data);
return createInputPayment(resp);
}
else {
throw new PaypalSoapException("Invalid payment: wrong status '"+ack+"': "+toJson(resp));
}
}
private PaymentBase createInputPayment(DoExpressCheckoutPaymentResponseType resp) {
DoExpressCheckoutPaymentResponseDetailsType details = resp.getDoExpressCheckoutPaymentResponseDetails();
List<PaymentInfoType> paymentsList = details.getPaymentInfo();
if(isEmpty(paymentsList)) throw new PaypalSoapException("Invalid payment: no payment info: "+toJson(resp));
//expected single info
PaymentInfoType info = paymentsList.get(0);
PaymentBase out = new PaymentBase();
out.provider = PAYPAL;
out.externalId = info.getTransactionID();
out.created = Util.tryParseDate(info.getPaymentDate(), TIME_FORMAT, new Date());
out.paymentStatus = getStatus(info.getPaymentStatus().getValue());
out.amount = getAmountVal(info.getGrossAmount());
return out;
}
@Override
public List<PaymentBase> getPaymentHistory(int daysBefore) throws Exception{
String dayBefore = formatReqDate(addDays(new Date(), -daysBefore));
TransactionSearchReq req = new TransactionSearchReq();
req.setTransactionSearchRequest(new TransactionSearchRequestType(dayBefore));
//soap call
TransactionSearchResponseType resp = getApi().transactionSearch(req);
checkForErrors(resp, "getPaymentHistory");
List<PaymentTransactionSearchResultType> payments = resp.getPaymentTransactions();
List<PaymentBase> out = convert(payments, (p)-> createPayment(p));
return out;
}
@Override
public PaymentBase getPayment(Date startDate, String transactionID) throws Exception {
TransactionSearchRequestType reqType = new TransactionSearchRequestType(formatReqDate(startDate));
reqType.setTransactionID(transactionID);
TransactionSearchReq req = new TransactionSearchReq();
req.setTransactionSearchRequest(reqType);
TransactionSearchResponseType resp = getApi().transactionSearch(req);
checkForErrors(resp, "getPayment");
List<PaymentTransactionSearchResultType> payments = resp.getPaymentTransactions();
if(isEmpty(payments)) return null;
PaymentBase out = createPayment(payments.get(0));
return out;
}
public static String formatReqDate(Date date){
Date dayStart = dateStart(date);
return new SimpleDateFormat(TIME_FORMAT).format(dayStart);
}
public static PaymentBase createPayment(PaymentTransactionSearchResultType p){
PaymentBase out = new PaymentBase();
out.provider = PAYPAL;
out.externalId = p.getTransactionID();
out.created = Util.tryParseDate(p.getTimestamp(), TIME_FORMAT, null);
out.paymentStatus = getStatus(p.getStatus());
out.amount = getAmountVal(p.getGrossAmount());
return out;
}
public static PaypalSoapClient create(Props systemProps){
File file = new File(systemProps.findVal(paypal_configPath));
if( ! file.exists()) throw new IllegalStateException("Can' find file : "+file);
FileProps connectProps = FileProps.createPropsWithoutUpdate(file);
return new PaypalSoapClient(systemProps, connectProps);
}
private PaymentDetailsType createPaymentDetailsType(BigDecimal val, long userId){
BasicAmountType amt = new BasicAmountType(CurrencyCodeType.USD, String.valueOf(val));
String name = "Online Chat Payment for Account#" + getAccId(userId);
PaymentDetailsItemType item = new PaymentDetailsItemType();
item.setQuantity(1);
item.setName(name);
item.setItemCategory(ItemCategoryType.DIGITAL);
item.setAmount(amt);
PaymentDetailsType paydtl = new PaymentDetailsType();
paydtl.setPaymentAction(PaymentActionCodeType.SALE);
paydtl.setOrderDescription(name);
paydtl.setOrderTotal(amt);
paydtl.setItemTotal(amt);
paydtl.setPaymentDetailsItem(list(item));
return paydtl;
}
private void checkForErrors(AbstractResponseType resp, String callerMsg) throws PaypalSoapException {
if(resp == null) throw new IllegalStateException("soap resp data is null");
List<ErrorType> errors = resp.getErrors();
if(isEmpty(errors)) return;
List<String> errMsgs = convert(errors, (error)->{
String json = toJson(error);
SeverityCodeType type = error.getSeverityCode();
if(type == SeverityCodeType.WARNING){
log.warn("Warn from paypal in "+callerMsg+": "+json);
throw new ContinueLoopException();
}
return json;
});
if(isEmpty(errMsgs)) return;
String finalMsg = errMsgs.size() == 1? errMsgs.get(0) : errMsgs.toString();
throw new PaypalSoapException(finalMsg);
}
private PayPalAPIInterfaceServiceService getApi() {
Map<String, String> map = conProps.toMap();
//lib has url by itself
map.remove("service.EndPoint");
return new PayPalAPIInterfaceServiceService(map);
}
public static PaymentStatus getStatus(String status) {
if(CREATED.getValue().equalsIgnoreCase(status)) return PaymentStatus.CREATED;
if(COMPLETED.getValue().equalsIgnoreCase(status)) return PaymentStatus.COMPLETED;
if(PENDING.getValue().equalsIgnoreCase(status)) return PaymentStatus.WAIT;
if(INPROGRESS.getValue().equalsIgnoreCase(status)) return PaymentStatus.WAIT;
if(REFUNDED.getValue().equalsIgnoreCase(status)) return PaymentStatus.RETURNED;
return PaymentStatus.ERROR;
}
public static BigDecimal getAmountVal(BasicAmountType amount){
return new BigDecimal(amount.getValue());
}
public static long getAccId(long userId) {
return userId+1000;
}
}