org.mayocat.shop.checkout.front.CheckoutResource.java Source code

Java tutorial

Introduction

Here is the source code for org.mayocat.shop.checkout.front.CheckoutResource.java

Source

/*
 * Copyright (c) 2012, Mayocat <hello@mayocat.org>
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */
package org.mayocat.shop.checkout.front;

import com.google.common.base.Optional;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import java.math.RoundingMode;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import org.apache.commons.lang3.StringUtils;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.joda.money.format.MoneyAmountStyle;
import org.joda.money.format.MoneyFormatter;
import org.joda.money.format.MoneyFormatterBuilder;
import org.mayocat.accounts.model.Tenant;
import org.mayocat.configuration.ConfigurationService;
import org.mayocat.configuration.MultitenancySettings;
import org.mayocat.configuration.SiteSettings;
import org.mayocat.configuration.general.GeneralSettings;
import org.mayocat.context.WebContext;
import org.mayocat.rest.Resource;
import org.mayocat.rest.annotation.ExistingTenant;
import org.mayocat.shop.billing.model.Order;
import org.mayocat.shop.billing.model.OrderItem;
import org.mayocat.shop.billing.store.OrderStore;
import org.mayocat.shop.catalog.model.Product;
import org.mayocat.shop.catalog.store.ProductStore;
import org.mayocat.shop.checkout.CheckoutException;
import org.mayocat.shop.checkout.CheckoutRegister;
import org.mayocat.shop.checkout.CheckoutRequest;
import org.mayocat.shop.checkout.CheckoutResponse;
import org.mayocat.shop.checkout.CheckoutSettings;
import org.mayocat.shop.checkout.RegularCheckoutException;
import org.mayocat.shop.checkout.internal.CheckoutRequestBuilder;
import org.mayocat.shop.customer.model.Address;
import org.mayocat.shop.customer.model.Customer;
import org.mayocat.shop.customer.store.AddressStore;
import org.mayocat.shop.customer.store.CustomerStore;
import org.mayocat.shop.front.views.WebView;
import org.mayocat.shop.payment.BasePaymentData;
import org.mayocat.shop.payment.CreditCardPaymentData;
import org.mayocat.shop.payment.PaymentData;
import org.mayocat.shop.payment.PaymentProcessor;
import org.mayocat.shop.payment.PaymentRequest;
import org.mayocat.shop.payment.PaymentStatus;
import org.mayocat.shop.payment.RequiredAction;
import org.mayocat.url.URLHelper;
import org.slf4j.Logger;
import org.xwiki.component.annotation.Component;

/**
 * @version $Id: 16071e763404f84c078ca610a989a9dc16c64782 $
 */
@Component(CheckoutResource.PATH)
@Path(CheckoutResource.PATH)
@Produces({ MediaType.TEXT_HTML, MediaType.APPLICATION_JSON })
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@ExistingTenant
public class CheckoutResource implements Resource {
    public static final String PATH = "checkout";

    public static final String PAYMENT_RETURN_PATH = "return";

    public static final String PAYMENT_CANCEL_PATH = "cancel";

    @Inject
    private Provider<CustomerStore> customerStore;

    @Inject
    private Provider<AddressStore> addressStore;

    @Inject
    private Provider<ProductStore> productStore;

    @Inject
    private CheckoutRegister checkoutRegister;

    @Inject
    private SiteSettings siteSettings;

    @Inject
    private URLHelper urlHelper;

    @Inject
    private MultitenancySettings multitenancySettings;

    @Inject
    private Provider<OrderStore> orderStore;

    @Inject
    private WebContext webContext;

    @Inject
    private ConfigurationService configurationService;

    @Inject
    private PaymentProcessor paymentProcessor;

    @Inject
    private Logger logger;

    @POST
    public Object checkout(final MultivaluedMap data) {

        CheckoutSettings checkoutSettings = configurationService.getSettings(CheckoutSettings.class);
        if (webContext.getUser() == null && !checkoutSettings.isGuestCheckoutEnabled().getValue()) {
            return Response.status(Response.Status.FORBIDDEN).entity("Configuration does not allow guest checkout")
                    .build();
        }

        CheckoutRequest checkoutRequest = null;
        if (webContext.getUser() != null) {
            Customer customer = customerStore.get().findByUserId(webContext.getUser().getId());
            if (customer != null) {
                Address deliveryAddress = addressStore.get().findByCustomerIdAndType(customer.getId(), "delivery");
                Address billingAddress = addressStore.get().findByCustomerIdAndType(customer.getId(), "billing");
                checkoutRequest = new CheckoutRequest();
                checkoutRequest.setCustomer(customer);
                checkoutRequest.setBillingAddress(billingAddress);
                checkoutRequest.setDeliveryAddress(deliveryAddress);
            }
        }
        if (checkoutRequest == null) {
            CheckoutRequestBuilder builder = new CheckoutRequestBuilder();
            checkoutRequest = builder.build(data);
        }

        if (checkoutRequest.getErrors().size() != 0) {
            Map<String, Object> bindings = new HashMap<>();

            bindings.put("request", data);
            bindings.put("errors", checkoutRequest.getErrors());
            return new WebView().template("checkout/form.html").with(WebView.Option.FALLBACK_ON_DEFAULT_THEME)
                    .data(bindings);
        }

        try {
            CheckoutResponse response;
            if (data.containsKey("product")) {
                // Direct checkout
                Product product = productStore.get().findBySlug((String) data.getFirst("product"));

                if (product == null) {
                    return Response.status(Response.Status.BAD_REQUEST).entity("Direct checkout product not found")
                            .build();
                }

                Long quantity;
                if (data.containsKey("quantity")) {
                    try {
                        quantity = Long.parseLong((String) data.getFirst("quantity"));
                    } catch (NumberFormatException e) {
                        return Response.status(Response.Status.BAD_REQUEST).entity("Malformed quantity.").build();
                    }
                } else {
                    quantity = 1l;
                }
                response = checkoutRegister.directCheckout(checkoutRequest, product, quantity);

            } else {
                // Cart checkout
                response = checkoutRegister.checkoutCart(checkoutRequest);
            }

            return generateCheckoutResponse(response);
        } catch (final Exception e) {
            this.logger.error("Failed to checkout", e);
            return renderError(e.getMessage());
        }
    }

    private Object generateCheckoutResponse(CheckoutResponse response) throws URISyntaxException {
        PaymentRequest paymentRequest = response.getPaymentRequest();
        Map<String, Object> bindings = new HashMap<>();

        switch (paymentRequest.getNextAction()) {
        case GET_EXTERNAL_URL:
            return Response.seeOther(new URI(paymentRequest.getRedirectionTarget().get())).build();
        case POST_EXTERNAL_URL:
        case INTERNAL_FORM:
            bindings.put("formURL",
                    paymentRequest.getRedirectionTarget().isPresent() ? paymentRequest.getRedirectionTarget().get()
                            : "/checkout/" + response.getOrder().getId() + "/payment");
            bindings.put("paymentData", paymentRequest.getData());

            // Also add the payment data keys in the response. This can be useful for JSON clients
            // Since keys are not ordered in JSON, objects keys are not ordered, and for some
            // payment gateways, respecting the order of keys is mandatory.
            bindings.put("paymentDataKeys", paymentRequest.getData().keySet());
            return new WebView().template("checkout/payment.html").with(WebView.Option.FALLBACK_ON_DEFAULT_THEME)
                    .data(bindings);

        case MANUAL_VALIDATION:
        case NONE:
        default:
            bindings.putAll(prepareContext(response.getOrder(), response.getOrder().getCustomer(),
                    Optional.fromNullable(response.getOrder().getBillingAddress()),
                    Optional.fromNullable(response.getOrder().getDeliveryAddress()), webContext.getTenant(),
                    configurationService.getSettings(GeneralSettings.class).getLocales().getMainLocale()
                            .getValue()));

            String template = "checkout/success.html";
            bindings.put("checkoutResult", "success");
            if (paymentRequest.getNextAction().equals(RequiredAction.MANUAL_VALIDATION)) {
                bindings.put("checkoutResult", "paymentPending");
                template = "checkout/pending.html";
            } else if (paymentRequest.getStatus().equals(PaymentStatus.FAILED)
                    || paymentRequest.getStatus().equals(PaymentStatus.REFUSED)) {
                bindings.put("checkoutResult", "failed");
                template = "checkout/failed.html";
            }

            return new WebView().template(template).with(WebView.Option.FALLBACK_ON_DEFAULT_THEME).data(bindings);
        }
    }

    @GET
    public Object checkout() {
        Map<String, Object> bindings = new HashMap<>();
        return new WebView().template("checkout/form.html").with(WebView.Option.FALLBACK_ON_DEFAULT_THEME)
                .data(bindings);
    }

    @POST
    @Path("{orderId}/payment")
    public Object internalPayment(@PathParam("orderId") UUID orderId, MultivaluedMap<String, String> data) {

        try {
            Order order = orderStore.get().findById(orderId);
            Map<PaymentData, Object> paymentData = Maps.newHashMap();
            paymentData.put(BasePaymentData.CURRENCY, order.getCurrency());
            paymentData.put(BasePaymentData.ORDER_ID, order.getId());
            paymentData.put(BasePaymentData.CUSTOMER, order.getCustomer());
            if (order.getBillingAddress() != null) {
                paymentData.put(BasePaymentData.BILLING_ADDRESS, order.getBillingAddress());
            }
            paymentData.put(BasePaymentData.DELIVERY_ADDRESS, order.getDeliveryAddress());
            paymentData.put(BasePaymentData.ORDER, order);

            if (data.containsKey("cardNumber")) {
                paymentData.put(CreditCardPaymentData.CARD_NUMBER, data.getFirst("cardNumber"));
                paymentData.put(CreditCardPaymentData.HOLDER_NAME, data.getFirst("holderName"));
                paymentData.put(CreditCardPaymentData.EXPIRATION_MONTH, data.getFirst("expiryMonth"));
                paymentData.put(CreditCardPaymentData.EXPIRATION_YEAR, data.getFirst("expiryYear"));
                paymentData.put(CreditCardPaymentData.VERIFICATION_CODE, data.getFirst("cvv"));
            }

            PaymentRequest paymentRequest = paymentProcessor.requestPayment(order, paymentData);
            CheckoutResponse response = new CheckoutResponse(order, paymentRequest);

            return generateCheckoutResponse(response);
        } catch (Exception e) {
            return renderError(e.getMessage());
        }
    }

    // Return from payment gateway -----------------------------------------------------------------

    @GET
    @Path(PAYMENT_RETURN_PATH + "/{order}")
    public WebView returnFromPaymentService(@Context UriInfo uriInfo, @PathParam("order") String orderId) {
        Map<String, Object> bindings = new HashMap<>();
        if (StringUtils.isNotBlank(orderId)) {
            Order order = orderStore.get().findById(UUID.fromString(orderId));
            if (order != null) {
                Optional<Address> da = order.getDeliveryAddress() != null
                        ? Optional.fromNullable(order.getDeliveryAddress())
                        : Optional.<Address>absent();
                Optional<Address> ba = order.getBillingAddress() != null
                        ? Optional.fromNullable(order.getBillingAddress())
                        : Optional.<Address>absent();
                bindings.putAll(prepareContext(order, order.getCustomer(), ba, da, webContext.getTenant(),
                        configurationService.getSettings(GeneralSettings.class).getLocales().getMainLocale()
                                .getValue()));
            }
        }
        return new WebView().template("checkout/success.html").with(WebView.Option.FALLBACK_ON_DEFAULT_THEME)
                .data(bindings);
    }

    @POST
    @Path(PAYMENT_RETURN_PATH + "/{order}")
    public WebView postReturnFromPaymentService(@Context UriInfo uriInfo, @PathParam("order") String orderId) {
        return this.returnFromPaymentService(uriInfo, orderId);
    }

    @GET
    @Path(PAYMENT_CANCEL_PATH + "/{orderId}")
    public WebView cancelFromPaymentService(@PathParam("orderId") UUID orderId) {
        try {
            checkoutRegister.dropOrder(orderId);
        } catch (final CheckoutException e) {
            if (!RegularCheckoutException.class.isAssignableFrom(e.getClass())) {
                // If this is not a regular checkout exception (like for example: order has already been deleted),
                // we need to take additional measures
                this.logger.error("Failed to cancel order", e);
            }

            Map<String, Object> bindings = new HashMap<>();
            bindings.put("exception", new HashMap<String, Object>() {
                {
                    put("message", e.getMessage());
                }
            });
            return new WebView().template("checkout/exception.html").with(WebView.Option.FALLBACK_ON_DEFAULT_THEME)
                    .data(bindings);
        }
        Map<String, Object> bindings = new HashMap<>();
        return new WebView().template("checkout/cancelled.html").with(WebView.Option.FALLBACK_ON_DEFAULT_THEME)
                .data(bindings);
    }

    @POST
    @Path(PAYMENT_CANCEL_PATH + "/{orderId}")
    public WebView postCancelFromPaymentService(@PathParam("orderId") UUID orderId) {
        return this.cancelFromPaymentService(orderId);
    }

    // Private helpers -----------------------------------------------------------------------------

    /**
     * Render a user facing error page
     *
     * @param message the message associated with the error
     * @return the error page
     */
    private Object renderError(final String message) {
        Map<String, Object> bindings = new HashMap<>();
        bindings.put("exception", new HashMap<String, Object>() {
            {
                put("message", message);
            }
        });
        return new WebView().template("checkout/exception.html").with(WebView.Option.FALLBACK_ON_DEFAULT_THEME)
                .data(bindings);
    }

    /**
     * Prepare checkout response context
     *
     * @param order the order concerned by the notification
     * @param customer the customer
     * @param ba an optional billing address
     * @param da an optional shipping address
     * @param tenant the tenant the order was checked out from
     * @param locale the main locale of the tenant
     * @return a JSON context as map
     */
    private Map<String, Object> prepareContext(Order order, Customer customer, Optional<Address> ba,
            Optional<Address> da, Tenant tenant, Locale locale) {
        List<OrderItem> items = order.getOrderItems();
        Map<String, Object> context = Maps.newHashMap();

        MoneyFormatter formatter = new MoneyFormatterBuilder().appendAmount(MoneyAmountStyle.of(locale))
                .appendLiteral(" ").appendCurrencySymbolLocalized().toFormatter();

        CurrencyUnit currencyUnit = CurrencyUnit.of(order.getCurrency());
        String grandTotal = formatter.withLocale(locale)
                .print(Money.of(currencyUnit, order.getGrandTotal(), RoundingMode.HALF_EVEN));
        String itemsTotal = formatter.withLocale(locale)
                .print(Money.of(currencyUnit, order.getItemsTotal(), RoundingMode.HALF_EVEN));

        List<Map<String, Object>> orderItems = Lists.newArrayList();
        for (OrderItem item : items) {
            Map<String, Object> orderItem = Maps.newHashMap();
            orderItem.put("title", item.getTitle());
            orderItem.put("quantity", item.getQuantity());

            // Replace big decimal values by formatted values
            Double unitPrice = item.getUnitPrice().doubleValue();
            Double itemTotal = item.getItemTotal().doubleValue();

            orderItem.put("unitPrice",
                    formatter.withLocale(locale).print(Money.of(currencyUnit, unitPrice, RoundingMode.HALF_EVEN)));
            orderItem.put("itemTotal",
                    formatter.withLocale(locale).print(Money.of(currencyUnit, itemTotal, RoundingMode.HALF_EVEN)));

            orderItems.add(orderItem);
        }

        context.put("items", orderItems);

        if (order.getShipping() != null) {
            String shippingTotal = formatter.withLocale(locale)
                    .print(Money.of(currencyUnit, order.getShipping(), RoundingMode.HALF_EVEN));
            context.put("shippingTotal", shippingTotal);
            context.put("shipping", order.getOrderData().get("shipping"));
        }

        context.put("siteName", tenant.getName());
        context.put("itemsTotal", itemsTotal);
        context.put("orderId", order.getSlug());
        context.put("grandTotal", grandTotal);

        Map<String, Object> customerMap = Maps.newHashMap();
        customerMap.put("firstName", customer.getFirstName());
        customerMap.put("lastName", customer.getLastName());
        customerMap.put("email", customer.getEmail());
        customerMap.put("phone", customer.getPhoneNumber());
        context.put("customer", customerMap);

        if (ba.isPresent()) {
            context.put("billingAddress", prepareAddressContext(ba.get()));
        }
        if (da.isPresent()) {
            context.put("deliveryAddress", prepareAddressContext(da.get()));
        }

        context.put("siteUrl", urlHelper.getContextWebURL("").toString());

        return context;
    }

    /**
     * Prepares the context for an address
     *
     * @param address the address to get the context of
     * @return the prepared context
     */
    private Map<String, Object> prepareAddressContext(Address address) {
        Map<String, Object> addressContext = Maps.newHashMap();
        addressContext.put("street", address.getStreet());
        addressContext.put("streetComplement", address.getStreetComplement());
        addressContext.put("zip", address.getZip());
        addressContext.put("city", address.getCity());
        addressContext.put("country", address.getCountry());
        addressContext.put("company", address.getCompany());
        return addressContext;
    }
}