CheckoutFiBean.java 13.5 KB
/*
 * Copyright Codecrew Ry
 * 
 * All rights reserved.
 * 
 * This license applies to any software containing a notice placed by the 
 * copyright holder. Such software is herein referred to as the Software. 
 * This license covers modification, distribution and use of the Software. 
 * 
 * Any distribution and use in source and binary forms, with or without 
 * modification is not permitted without explicit written permission from the 
 * copyright owner. 
 * 
 * A non-exclusive royalty-free right is granted to the copyright owner of the 
 * Software to use, modify and distribute all modifications to the Software in 
 * future versions of the Software. 
 * 
 */
package fi.codecrew.moya.beans;

import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.security.RolesAllowed;
import javax.ejb.EJB;
import javax.ejb.EJBException;
import javax.ejb.LocalBean;
import javax.ejb.Stateless;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import fi.codecrew.moya.checkoutfi.CheckoutFiParam;
import fi.codecrew.moya.clientutils.BortalLocalContextHolder;
import fi.codecrew.moya.enums.apps.BillPermission;
import fi.codecrew.moya.facade.BillFacade;
import fi.codecrew.moya.model.Bill;
import fi.codecrew.moya.model.LanEventPrivateProperty;
import fi.codecrew.moya.model.LanEventPrivatePropertyKey;
import fi.codecrew.moya.util.CheckoutBank;
import fi.codecrew.moya.util.CheckoutReturnType;
import fi.codecrew.moya.utilities.PasswordFunctions;

/**
 * Session Bean implementation class CheckoutFiBean
 */
@Stateless
@LocalBean
public class CheckoutFiBean implements CheckoutFiBeanLocal {

	private static final String STAMP_SPLITCHAR = "a";
	private static final String REMOTE_URL = "https://payment.checkout.fi";

	/**
	 * Default constructor.
	 */
	public CheckoutFiBean() {
		// TODO Auto-generated constructor stub
	}

	@EJB
	private EventBean eventbean;
	@EJB
	private PermissionBeanLocal permbean;
	@EJB
	private LoggingBeanLocal logbean;
	@EJB
	private BillFacade billfacade;
	@EJB
	private VerkkomaksuRunner vmrunner;

	private static final Logger logger = LoggerFactory.getLogger(CheckoutFiBean.class);
	private static final BigDecimal TO_CENTS = BigDecimal.valueOf(100);

	@Override
	public boolean isPaymentEnabled()
	{
		final LanEventPrivateProperty expire = eventbean.getPrivateProperty(LanEventPrivatePropertyKey.CHECKOUT_FI_KEY_EXPIRE);
		final String merchantid = eventbean.getPrivatePropertyString(LanEventPrivatePropertyKey.CHECKOUT_FI_MERCHANT_ID);
		final String merchantPassword = eventbean.getPrivatePropertyString(LanEventPrivatePropertyKey.CHECKOUT_FI_MERCHANT_PASSWORD);
		return !((expire != null && new Date().after(expire.getDateValue()))
				|| merchantid == null || merchantid.isEmpty()
				|| merchantPassword == null || merchantPassword.isEmpty());
	}

	public static void main(String[] asd) throws ParserConfigurationException, SAXException, IOException
	{
		// DocumentBuilderFactory dbFactory =
		// DocumentBuilderFactory.newInstance();
		// DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
		// Document doc = dBuilder.parse(xmlstr);
		// // DOMParser parser = new DOMParser();
		// // Document doc = parser.getDocument();
		// // parser.parse(new InputSource(new StringReader(xmlReturn)));
		// Element root = doc.getDocumentElement();
		// root.normalize();
		// // NodeList rootChildren = root.getChildNodes();
		// // for (int i = 0; i < rootChildren.getLength(); ++i)
		// // {
		// // logger.info("Rootchild {}", rootChildren.item(i).getNodeName());
		// // }
		//
		// Node payments = root.getElementsByTagName("banks").item(0);

	}

	private static final String DATEFORMAT = "yyyyMMdd";
	private static final Set<String> DISCARD_BANKS = new HashSet<String>(Arrays.asList("ape", "tilisiirto", "neopay"));

	@Override
	@RolesAllowed(BillPermission.S_CREATE_VERKKOMAKSU)
	public List<CheckoutBank> getToken(Bill bill)
	{
		if (bill.isFoowavePaymentOver())
		{
			return null;
		}

		final LanEventPrivateProperty expire = eventbean.getPrivateProperty(LanEventPrivatePropertyKey.CHECKOUT_FI_KEY_EXPIRE);
		final String merchantid = eventbean.getPrivatePropertyString(LanEventPrivatePropertyKey.CHECKOUT_FI_MERCHANT_ID);
		final String merchantPassword = eventbean.getPrivatePropertyString(LanEventPrivatePropertyKey.CHECKOUT_FI_MERCHANT_PASSWORD);

		if ((expire != null && new Date().after(expire.getDateValue()))
				|| merchantid == null || merchantid.isEmpty()
				|| merchantPassword == null || merchantPassword.isEmpty())
		{
			return null;
		}
		final String priceInCents = Integer.valueOf(bill.totalPrice().multiply(TO_CENTS).intValue()).toString();
		final Map<CheckoutFiParam, String> postParams = new HashMap<CheckoutFiParam, String>();

		final String returnUrl = new StringBuilder(BortalLocalContextHolder.isSsl() ? "https://" : "http://")
				.append(BortalLocalContextHolder.getHostname())
				.append("/MoyaWeb/checkout/")
				.toString();

		final StringBuilder stamp = new StringBuilder();
		stamp.append(bill.getId());
		stamp.append(STAMP_SPLITCHAR);
		stamp.append(bill.getSentDate().getTimeInMillis() / 1000);

		postParams.put(CheckoutFiParam.STAMP, stamp.toString());
		postParams.put(CheckoutFiParam.AMOUNT, priceInCents);
		postParams.put(CheckoutFiParam.REFERENCE, bill.getReferenceNumber().toString());
		postParams.put(CheckoutFiParam.MERCHANT, merchantid);
		postParams.put(CheckoutFiParam.RETURN, returnUrl + "return.jsf");
		postParams.put(CheckoutFiParam.CANCEL, returnUrl + "cancel.jsf");
		postParams.put(CheckoutFiParam.REJECT, returnUrl + "reject.jsf");
		postParams.put(CheckoutFiParam.DELAYED, returnUrl + "delayed.jsf");
		postParams.put(CheckoutFiParam.DELIVERY_DATE, new SimpleDateFormat(DATEFORMAT).format(new Date()));

		List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>();
		StringBuilder mdString = new StringBuilder();

		for (CheckoutFiParam v : CheckoutFiParam.values())
		{
			String value = null;
			if (postParams.containsKey(v)) {
				value = postParams.get(v);
			} else {
				value = v.getDefaultValue();
			}
			if (value != null) {
				mdString.append(value);
				nameValuePairs.add(new BasicNameValuePair(v.name(), value));
			}
			mdString.append("+");

		}
		mdString.append(merchantPassword);

		try {

			final String calculatedHash = PasswordFunctions.calculateMd5(mdString.toString());
			logger.info("Calculated checksum {} from {}",
					mdString.toString(), calculatedHash);

			nameValuePairs.add(new BasicNameValuePair("MAC", calculatedHash));

			final HttpPost postRequest = new HttpPost(REMOTE_URL);
			postRequest.setEntity(new UrlEncodedFormEntity(nameValuePairs));

			final DefaultHttpClient client = new DefaultHttpClient();
			HttpResponse response = client.execute(postRequest);

			//			final StringWriter writer = new StringWriter();
			//
			//			IOUtils.copy(, writer, "UTF8");
			//
			//			final String xmlReturn = writer.toString();
			//			// logger.info("Got response from checkout.fi msg {}, {} ",
			//			// response.getStatusLine().getStatusCode(), xmlReturn);

			return parseTokenXml(response.getEntity().getContent());

		} catch (IOException e) {
			logger.warn("Error sending checkout.fi request", e);
		}

		return null;
	}

	// @Override
	// public List<CheckoutBank> testXml()
	// {
	// List<CheckoutBank> ret = null;
	// try {
	// BufferedReader reader = new BufferedReader(new
	// FileReader("/Users/tuomari/Documents/git/bortal/code/LanBortalBeans/ejbModule/fi.codecrew.moya/beans/checkoutTestfile.xml"));
	// StringBuilder sb = new StringBuilder();
	//
	// String line = null;
	// while ((line = reader.readLine()) != null) {
	// sb.append(line);
	// sb.append("\n");
	// }
	//
	// reader.close();
	// String str = sb.toString();
	// ret = parseTokenXml(str);
	//
	// } catch (Throwable t)
	// {
	// logger.warn("error testing", t);
	// }
	// // logger.warn("str: {}", str);
	// return ret;
	// }

	private static List<CheckoutBank> parseTokenXml(InputStream inputStream) {
		try {

			// DOMParser parser = new DOMParser();
			// parser.parse(new InputSource(new StringReader(xmlReturn)));
			DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
			DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();

			//			InputStream xmlstr = IOUtils.toInputStream(inputStream);

			Document doc = dBuilder.parse(inputStream);
			// Document doc = parser.getDocument();
			Element root = doc.getDocumentElement();
			root.normalize();
			// NodeList rootChildren = root.getChildNodes();
			// for (int i = 0; i < rootChildren.getLength(); ++i)
			// {
			// logger.info("Rootchild {}", rootChildren.item(i).getNodeName());
			// }

			Node payments = root.getElementsByTagName("banks").item(0);

			NodeList paymentElements = payments.getChildNodes();
			List<CheckoutBank> retbanks = new ArrayList<CheckoutBank>();
			for (int i = 0; i < paymentElements.getLength(); ++i)
			{
				Node bank = paymentElements.item(i);
				if (bank.getNodeType() == 1)
				{
					CheckoutBank newBank = new CheckoutBank(bank);
					if (!DISCARD_BANKS.contains(newBank.getKey()))
					{
						retbanks.add(newBank);
					}
				}

			}
			return retbanks;
		} catch (SAXException | IOException | ParserConfigurationException e) {
			logger.warn("Error parsing response from checkout response:" + inputStream, e);
		}
		return null;
	}

	@Override
	public boolean validateReturn(CheckoutReturnType returnType, String version, String stamp, String reference, String payment, String status, String algorithm, String mac) {
		if (returnType == null || mac == null || mac.isEmpty() || stamp == null || stamp.isEmpty())
		{
			return false;
		}
		String merchantPass = eventbean.getPrivatePropertyString(LanEventPrivatePropertyKey.CHECKOUT_FI_MERCHANT_PASSWORD);

		boolean ret = false;
		String[] splittedStamp = stamp.split(STAMP_SPLITCHAR);
		if (splittedStamp.length != 2)
		{
			logbean.logMessage(SecurityLogType.verkkomaksu, permbean.getCurrentUser(), "Unable to split stamp ", stamp, " with splitchar ", STAMP_SPLITCHAR);
			return false;
		}

		// MD5(turva-avain&VERSION&STAMP&REFERENCE&PAYMENT&STATUS&ALGORITHM)
		String calculatedMac = PasswordFunctions.calculateMd5("&", merchantPass, version, stamp, reference, payment, status, algorithm);
		if (calculatedMac.equals(mac.toUpperCase())) {
			Bill bill = billfacade.find(Integer.parseInt(splittedStamp[0]));
			if (bill != null)
			{
				switch (returnType)
				{
				case CANCEL:
					logbean.logMessage(SecurityLogType.verkkomaksu, permbean.getCurrentUser(), "received cancel for stamp ", stamp);
					// Return true when checksum was correct
					ret = true;
					break;
				case DELAYED:
					logbean.logMessage(SecurityLogType.verkkomaksu, permbean.getCurrentUser(), "received delayed for stamp ", stamp);
					// Return true when checksum was correct
					ret = true;
					break;
				case REJECT:
					logbean.logMessage(SecurityLogType.verkkomaksu, permbean.getCurrentUser(), "received reject for stamp ", stamp);
					// Return true when checksum was correct
					ret = true;
					break;
				case RETURN:
					int statusInt = Integer.parseInt(status);
					// Verkkokauppaohjelmisto voi hyväksyä maksun suoritetuksi
					// kun maksun tila on 2,5,6,7,8,9 tai 10.
					switch (statusInt) {
					case 2:
					case 5:
					case 6:
					case 7:
					case 8:
					case 9:
					case 10:
						if (bill.getAccountEvent() == null
								&& bill.getPaidDate() == null)
						{
							logger.info("Trying to mark bill {} paid", bill);
							vmrunner.markPaid(bill, Calendar.getInstance());
							logbean.logMessage(SecurityLogType.verkkomaksu, permbean.getCurrentUser(), "Marking bill paid. Received bill status ", statusInt, " for bill ", bill, " stamp ", stamp, " payment: ", payment, " reference ", reference);
							ret = true;
						} else {
							logbean.logMessage(SecurityLogType.verkkomaksu, permbean.getCurrentUser(), "Bill already marked paid: ", bill, " status ", status, " stamp ", stamp, " payment ", payment);
						}
						break;
					default:
						logbean.logMessage(SecurityLogType.verkkomaksu, permbean.getCurrentUser(), "Not marking bill paid: Return status ", status, " for bill ", bill, " stamp ", stamp, " payment ", payment);

						break;
					}
					break;
				default:
					logbean.logMessage(SecurityLogType.verkkomaksu, permbean.getCurrentUser(), "Valid mac, but Invalid return type: ", returnType, " for stamp ", stamp, " payment ", payment, " status ", status);
					throw new EJBException("Unknown return type!");
				}
			}
			else {
				logbean.logMessage(SecurityLogType.verkkomaksu, permbean.getCurrentUser(), "Validated mac, but bill not found for id: ", splittedStamp[0], " stamp ", stamp, " mac: ", mac);
			}
		} else {

			logbean.logMessage(SecurityLogType.verkkomaksu, permbean.getCurrentUser(), "Unable to validate order reference: ", reference, " calculated checksum: ", calculatedMac, " version ", version, " stamp ", stamp, " status ", status, " mac ", mac);

		}

		return ret;
	}
}