All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.aoindustries.website.clientarea.accounting.MakePaymentNewCardCompletedAction Maven / Gradle / Ivy

/*
 * aoweb-struts-core - Core API for legacy Struts-based site framework with AOServ Platform control panels.
 * Copyright (C) 2007-2009, 2015, 2016, 2017, 2018, 2019  AO Industries, Inc.
 *     [email protected]
 *     7262 Bull Pen Cir
 *     Mobile, AL 36695
 *
 * This file is part of aoweb-struts-core.
 *
 * aoweb-struts-core is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * aoweb-struts-core is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with aoweb-struts-core.  If not, see .
 */
package com.aoindustries.website.clientarea.accounting;

import com.aoindustries.aoserv.client.AOServConnector;
import com.aoindustries.aoserv.client.account.Account;
import com.aoindustries.aoserv.client.account.Profile;
import com.aoindustries.aoserv.client.billing.TransactionType;
import com.aoindustries.aoserv.client.payment.CreditCard;
import com.aoindustries.aoserv.client.payment.PaymentType;
import com.aoindustries.aoserv.creditcards.AOServConnectorPrincipal;
import com.aoindustries.aoserv.creditcards.BusinessGroup;
import com.aoindustries.aoserv.creditcards.CreditCardProcessorFactory;
import com.aoindustries.creditcards.AuthorizationResult;
import com.aoindustries.creditcards.CaptureResult;
import com.aoindustries.creditcards.CreditCardProcessor;
import com.aoindustries.creditcards.TokenizedCreditCard;
import com.aoindustries.creditcards.Transaction;
import com.aoindustries.creditcards.TransactionRequest;
import com.aoindustries.creditcards.TransactionResult;
import com.aoindustries.net.Email;
import com.aoindustries.sql.SQLUtility;
import com.aoindustries.website.SiteSettings;
import com.aoindustries.website.Skin;
import java.io.IOException;
import java.math.BigDecimal;
import java.sql.SQLException;
import java.util.Currency;
import java.util.Locale;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
import org.apache.struts.action.ActionMessages;
import org.apache.struts.util.MessageResources;

/**
 * Payment from stored credit card.
 *
 * @author  AO Industries, Inc.
 */
public class MakePaymentNewCardCompletedAction extends MakePaymentNewCardAction {

	/**
	 * Process as separate authorize/capture.  This is useful for testing payment API implementations.
	 * This should always be "false" for a production release.
	 * TODO: Make this a context parameter?
	 */
	static final boolean DEBUG_AUTHORIZE_THEN_CAPTURE = false;

	static final int DUPLICATE_WINDOW = 120;

	static final Currency USD = Currency.getInstance("USD");

	static String trimNullIfEmpty(String s) {
		if(s == null) return null;
		s = s.trim();
		return s.isEmpty() ? null : s;
	}

	static String getFirstBillingEmail(Profile profile) {
		if(profile == null) return null;
		Set emails = profile.getBillingEmail();
		if(emails.isEmpty()) return null;
		return emails.iterator().next().toString();
	}

	@Override
	final public ActionForward execute(
		ActionMapping mapping,
		ActionForm form,
		HttpServletRequest request,
		HttpServletResponse response,
		SiteSettings siteSettings,
		Locale locale,
		Skin skin,
		AOServConnector aoConn
	) throws Exception {
		MakePaymentNewCardForm makePaymentNewCardForm=(MakePaymentNewCardForm)form;

		// Init request values
		initRequestAttributes(request, getServlet().getServletContext());

		String accounting = makePaymentNewCardForm.getAccounting();
		Account account = accounting == null ? null : aoConn.getAccount().getAccount().get(Account.Name.valueOf(accounting));
		if(account == null) {
			// Redirect back to make-payment if business not found
			return mapping.findForward("make-payment");
		}
		request.setAttribute("business", account);

		// Validation
		ActionMessages errors = makePaymentNewCardForm.validate(mapping, request);
		if(errors!=null && !errors.isEmpty()) {
			saveErrors(request, errors);

			return mapping.findForward("input");
		}

		// Convert to pennies
		int pennies = SQLUtility.getPennies(makePaymentNewCardForm.getPaymentAmount());
		BigDecimal paymentAmount = new BigDecimal(makePaymentNewCardForm.getPaymentAmount());

		// Encapsulate the values into a new credit card
		String cardNumber = makePaymentNewCardForm.getCardNumber();
		String principalName = aoConn.getThisBusinessAdministrator().getUsername().getUsername().toString();
		String groupName = accounting;
		Profile profile = account.getBusinessProfile();
		com.aoindustries.creditcards.CreditCard newCreditCard = new com.aoindustries.creditcards.CreditCard(
			null, // persistenceUniqueId
			principalName,
			groupName,
			null, // providerId
			null, // providerUniqueId
			cardNumber,
			null, // maskedCardNumber
			Byte.parseByte(makePaymentNewCardForm.getExpirationMonth()),
			Short.parseShort(makePaymentNewCardForm.getExpirationYear()),
			makePaymentNewCardForm.getCardCode(),
			makePaymentNewCardForm.getFirstName(),
			makePaymentNewCardForm.getLastName(),
			makePaymentNewCardForm.getCompanyName(),
			getFirstBillingEmail(profile),
			profile == null ? null : trimNullIfEmpty(profile.getPhone()),
			profile == null ? null : trimNullIfEmpty(profile.getFax()),
			null, // customerId: TODO: Set from account.Account once there is a constant identifier
			null, // customerTaxId
			makePaymentNewCardForm.getStreetAddress1(),
			makePaymentNewCardForm.getStreetAddress2(),
			makePaymentNewCardForm.getCity(),
			makePaymentNewCardForm.getState(),
			makePaymentNewCardForm.getPostalCode(),
			makePaymentNewCardForm.getCountryCode(),
			makePaymentNewCardForm.getDescription()
		);

		// Perform the transaction
		AOServConnector rootConn = siteSettings.getRootAOServConnector();

		// 1) Pick a processor
		CreditCardProcessor rootProcessor = CreditCardProcessorFactory.getCreditCardProcessor(rootConn);
		if(rootProcessor==null) throw new SQLException("Unable to find enabled CreditCardProcessor for root connector");
		com.aoindustries.aoserv.client.payment.Processor rootAoProcessor = rootConn.getPayment().getProcessor().get(rootProcessor.getProviderId());
		if(rootAoProcessor == null) throw new SQLException("Unable to find CreditCardProcessor: " + rootProcessor.getProviderId());

		// 2) Add the transaction as pending on this processor
		Account rootAccount = rootConn.getAccount().getAccount().get(Account.Name.valueOf(accounting));
		if(rootAccount == null) throw new SQLException("Unable to find Account: " + accounting);
		TransactionType paymentTransactionType = rootConn.getBilling().getTransactionType().get(TransactionType.PAYMENT);
		if(paymentTransactionType == null) throw new SQLException("Unable to find TransactionType: " + TransactionType.PAYMENT);
		MessageResources applicationResources = (MessageResources)request.getAttribute("/clientarea/accounting/ApplicationResources");
		String cardInfo = com.aoindustries.creditcards.CreditCard.maskCreditCardNumber(cardNumber);
		PaymentType paymentType;
		{
			String paymentTypeName;
			// TODO: Move to a card-type microproject API and shared with ao-credit-cards/ao-payments implementation
			if(
				cardNumber.startsWith("34")
				|| cardNumber.startsWith("37")
				|| cardNumber.startsWith("3" + com.aoindustries.creditcards.CreditCard.UNKNOWN_DIGIT)) {
				paymentTypeName = PaymentType.AMEX;
			} else if(cardNumber.startsWith("60")) {
				paymentTypeName = PaymentType.DISCOVER;
			} else if(
				cardNumber.startsWith("51")
				|| cardNumber.startsWith("52")
				|| cardNumber.startsWith("53")
				|| cardNumber.startsWith("54")
				|| cardNumber.startsWith("55")
				|| cardNumber.startsWith("5" + com.aoindustries.creditcards.CreditCard.UNKNOWN_DIGIT)
			) {
				paymentTypeName = PaymentType.MASTERCARD;
			} else if(cardNumber.startsWith("4")) {
				paymentTypeName = PaymentType.VISA;
			} else {
				paymentTypeName = null;
			}
			if(paymentTypeName==null) paymentType = null;
			else {
				paymentType = rootConn.getPayment().getPaymentType().get(paymentTypeName);
				if(paymentType == null) throw new SQLException("Unable to find PaymentType: " + paymentTypeName);
			}
		}
		int transID = rootAccount.addTransaction(
			rootAccount,
			aoConn.getThisBusinessAdministrator(),
			paymentTransactionType,
			applicationResources.getMessage(locale, "makePaymentStoredCardCompleted.transaction.description"),
			1000,
			-pennies,
			paymentType,
			com.aoindustries.creditcards.CreditCard.getCardNumberDisplay(cardInfo),
			rootAoProcessor,
			com.aoindustries.aoserv.client.billing.Transaction.WAITING_CONFIRMATION
		);
		com.aoindustries.aoserv.client.billing.Transaction aoTransaction = rootConn.getBilling().getTransaction().get(transID);
		if(aoTransaction == null) throw new SQLException("Unable to find Transaction: " + transID);

		// TODO: Store card before sale, and use its stored card ID (once ao-credit-cards can throw ErrorCodeException on store card)
		// TODO: This currently implementation provides disconnected first payment and stored card in Stripe.

		// 3) Process
		AOServConnectorPrincipal principal = new AOServConnectorPrincipal(rootConn, principalName);
		BusinessGroup businessGroup = new BusinessGroup(rootAccount, groupName);
		Transaction transaction;
		if(DEBUG_AUTHORIZE_THEN_CAPTURE) {
			transaction = rootProcessor.authorize(
				principal,
				businessGroup,
				new TransactionRequest(
					false, // testMode
					request.getRemoteAddr(), // customerIp
					DUPLICATE_WINDOW,
					Integer.toString(transID), // orderNumber
					USD, // currency
					paymentAmount, // amount
					null, // taxAmount
					false, // taxExempt
					null, // shippingAmount
					null, // dutyAmount
					null, // shippingFirstName
					null, // shippingLastName
					null, // shippingCompanyName
					null, // shippingStreetAddress1
					null, // shippingStreetAddress2
					null, // shippingCity
					null, // shippingState
					null, // shippingPostalCode
					null, // shippingCountryCode
					false, // emailCustomer
					null, // merchantEmail
					null, // invoiceNumber
					null, // purchaseOrderNumber
					applicationResources.getMessage(Locale.US, "makePaymentStoredCardCompleted.transaction.description") // description
				),
				newCreditCard
			);
		} else {
			transaction = rootProcessor.sale(
				principal,
				businessGroup,
				new TransactionRequest(
					false, // testMode
					request.getRemoteAddr(), // customerIp
					DUPLICATE_WINDOW,
					Integer.toString(transID), // orderNumber
					USD, // currency
					paymentAmount, // amount
					null, // taxAmount
					false, // taxExempt
					null, // shippingAmount
					null, // dutyAmount
					null, // shippingFirstName
					null, // shippingLastName
					null, // shippingCompanyName
					null, // shippingStreetAddress1
					null, // shippingStreetAddress2
					null, // shippingCity
					null, // shippingState
					null, // shippingPostalCode
					null, // shippingCountryCode
					false, // emailCustomer
					null, // merchantEmail
					null, // invoiceNumber
					null, // purchaseOrderNumber
					applicationResources.getMessage(Locale.US, "makePaymentStoredCardCompleted.transaction.description") // description
				),
				newCreditCard
			);
		}
		// TODO: CreditCard might have been updated on root connector, invalidate and get fresh object always to avoid possible race condition

		// 4) Decline/approve based on results
		AuthorizationResult authorizationResult = transaction.getAuthorizationResult();
		TokenizedCreditCard tokenizedCreditCard = authorizationResult.getTokenizedCreditCard();
		switch(authorizationResult.getCommunicationResult()) {
			case LOCAL_ERROR :
			case IO_ERROR :
			case GATEWAY_ERROR :
			{
				// Update transaction as failed
				aoTransaction.declined(
					Integer.parseInt(transaction.getPersistenceUniqueId()),
					tokenizedCreditCard == null ? null : com.aoindustries.creditcards.CreditCard.getCardNumberDisplay(tokenizedCreditCard.getReplacementMaskedCardNumber())
				);

				TransactionResult.ErrorCode errorCode = authorizationResult.getErrorCode();
				ActionMessages mappedErrors = makePaymentNewCardForm.mapTransactionError(errorCode);
				if(mappedErrors==null || mappedErrors.isEmpty()) {
					// Not mapped, store to request attributes as generate error (to be displayed separate from specific fields)
					request.setAttribute("errorReason", errorCode.toString());
				} else {
					// Store for display with specific fields
					saveErrors(request, mappedErrors);
				}
				return mapping.findForward("error");
			}
			case SUCCESS :
				// Check approval result
				switch(authorizationResult.getApprovalResult()) {
					case HOLD :
					{
						// Update transaction as held
						aoTransaction.held(
							Integer.parseInt(transaction.getPersistenceUniqueId()),
							tokenizedCreditCard == null ? null : com.aoindustries.creditcards.CreditCard.getCardNumberDisplay(tokenizedCreditCard.getReplacementMaskedCardNumber())
						);

						// Store to request attributes
						request.setAttribute("transaction", transaction);
						request.setAttribute("aoTransaction", aoTransaction);
						request.setAttribute("reviewReason", authorizationResult.getReviewReason().toString());

						String storeCard = makePaymentNewCardForm.getStoreCard();
						if(
							"store".equals(storeCard)
							|| "automatic".equals(storeCard)
						) {
							// Store card
							boolean storeSuccess;
							try {
								storeCard(rootProcessor, principal, businessGroup, newCreditCard);
								request.setAttribute("cardStored", "true");
								storeSuccess = true;
							} catch(IOException | SQLException | RuntimeException err) {
								getServlet().log("Unable to store card", err);
								request.setAttribute("storeError", err);
								storeSuccess = false;
							}
							if(storeSuccess && "automatic".equals(storeCard)) {
								// Set automatic
								try {
									setAutomatic(rootConn, newCreditCard, account);
									request.setAttribute("cardSetAutomatic", "true");
								} catch(SQLException | RuntimeException err) {
									getServlet().log("Unable to set automatic", err);
									request.setAttribute("setAutomaticError", err);
								}
							}
						}
						return mapping.findForward("hold");
					}
					case DECLINED :
					{
						// Update transaction as declined
						aoTransaction.declined(
							Integer.parseInt(transaction.getPersistenceUniqueId()),
							tokenizedCreditCard == null ? null : com.aoindustries.creditcards.CreditCard.getCardNumberDisplay(tokenizedCreditCard.getReplacementMaskedCardNumber())
						);

						// Store to request attributes
						request.setAttribute("declineReason", authorizationResult.getDeclineReason().toString());
						return mapping.findForward("declined");
					}
					case APPROVED :
					{
						if(DEBUG_AUTHORIZE_THEN_CAPTURE) {
							// Perform capture in second step
							CaptureResult captureResult = rootProcessor.capture(principal, transaction);
							switch(captureResult.getCommunicationResult()) {
								case LOCAL_ERROR :
								case IO_ERROR :
								case GATEWAY_ERROR :
								{
									// Update transaction as failed
									aoTransaction.declined(
										Integer.parseInt(transaction.getPersistenceUniqueId()),
										tokenizedCreditCard == null ? null : com.aoindustries.creditcards.CreditCard.getCardNumberDisplay(tokenizedCreditCard.getReplacementMaskedCardNumber())
									);

									TransactionResult.ErrorCode errorCode = authorizationResult.getErrorCode();
									ActionMessages mappedErrors = makePaymentNewCardForm.mapTransactionError(errorCode);
									if(mappedErrors==null || mappedErrors.isEmpty()) {
										// Not mapped, store to request attributes as generate error (to be displayed separate from specific fields)
										request.setAttribute("errorReason", errorCode.toString());
									} else {
										// Store for display with specific fields
										saveErrors(request, mappedErrors);
									}
									return mapping.findForward("error");
								}
								case SUCCESS :
								{
									// Continue with processing of SUCCESS below, same as used for direct sale(...)
									break;
								}
								default:
									throw new RuntimeException("Unexpected value for capture communication result: "+captureResult.getCommunicationResult());
							}
						}
						// Update transaction as successful
						aoTransaction.approved(
							Integer.parseInt(transaction.getPersistenceUniqueId()),
							tokenizedCreditCard == null ? null : com.aoindustries.creditcards.CreditCard.getCardNumberDisplay(tokenizedCreditCard.getReplacementMaskedCardNumber())
						);

						// Store to request attributes
						request.setAttribute("transaction", transaction);
						request.setAttribute("aoTransaction", aoTransaction);

						String storeCard = makePaymentNewCardForm.getStoreCard();
						if(
							"store".equals(storeCard)
							|| "automatic".equals(storeCard)
						) {
							// Store card
							boolean storeSuccess;
							try {
								storeCard(rootProcessor, principal, businessGroup, newCreditCard);
								request.setAttribute("cardStored", "true");
								storeSuccess = true;
							} catch(IOException | SQLException | RuntimeException err) {
								getServlet().log("Unable to store card", err);
								request.setAttribute("storeError", err);
								storeSuccess = false;
							}
							if(storeSuccess && "automatic".equals(storeCard)) {
								// Set automatic
								try {
									setAutomatic(rootConn, newCreditCard, account);
									request.setAttribute("cardSetAutomatic", "true");
								} catch(SQLException | RuntimeException err) {
									getServlet().log("Unable to set automatic", err);
									request.setAttribute("setAutomaticError", err);
								}
							}
						}
						return mapping.findForward("success");
					}
					default:
						throw new RuntimeException("Unexpected value for authorization approval result: "+authorizationResult.getApprovalResult());
				}
			default:
				throw new RuntimeException("Unexpected value for authorization communication result: "+authorizationResult.getCommunicationResult());
		}
	}

	private void storeCard(CreditCardProcessor rootProcessor, AOServConnectorPrincipal principal, BusinessGroup businessGroup, com.aoindustries.creditcards.CreditCard newCreditCard) throws SQLException, IOException {
		rootProcessor.storeCreditCard(
			principal,
			businessGroup,
			newCreditCard
		);
	}

	/**
	 * @param  rootConn  Since rootConn is used to store the card, it must also be used to get the new instance.
	 *                   Otherwise there is a race condition between the non-root AOServConnector getting the invalidation signal
	 *                   and this method being called.
	 */
	private void setAutomatic(AOServConnector rootConn, com.aoindustries.creditcards.CreditCard newCreditCard, Account business) throws SQLException, IOException {
		String persistenceUniqueId = newCreditCard.getPersistenceUniqueId();
		CreditCard creditCard = rootConn.getPayment().getCreditCard().get(Integer.parseInt(persistenceUniqueId));
		if(creditCard == null) throw new SQLException("Unable to find CreditCard: " + persistenceUniqueId);
		if(!creditCard.getBusiness().equals(business)) throw new SQLException("Requested business and CreditCard business do not match: "+creditCard.getBusiness().getName()+"!="+business.getName());
		business.setUseMonthlyCreditCard(creditCard);
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy