org.mayocat.shop.billing.internal.SendEmailsWhenOrderIsPaid.java Source code

Java tutorial

Introduction

Here is the source code for org.mayocat.shop.billing.internal.SendEmailsWhenOrderIsPaid.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.billing.internal;

import com.google.common.base.Strings;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.PipedInputStream;
import java.math.RoundingMode;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;

import javax.activation.DataSource;
import javax.inject.Inject;
import javax.inject.Provider;

import javax.mail.util.ByteArrayDataSource;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
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.mail.MailAttachment;
import org.mayocat.mail.MailException;
import org.mayocat.mail.MailTemplate;
import org.mayocat.mail.MailTemplateService;
import org.mayocat.rest.api.object.DateWebObject;
import org.mayocat.shop.billing.event.OrderPaidEvent;
import org.mayocat.shop.billing.model.Order;
import org.mayocat.shop.billing.model.OrderItem;
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.invoicing.InvoicingException;
import org.mayocat.shop.invoicing.InvoicingService;
import org.mayocat.shop.invoicing.model.InvoiceNumber;
import org.mayocat.url.URLHelper;
import org.slf4j.Logger;
import org.xwiki.component.annotation.Component;
import org.xwiki.observation.EventListener;
import org.xwiki.observation.event.Event;

import com.google.common.base.Optional;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

/**
 * Event listener that sends email to tenant and customer when an order is paid.
 *
 * The mails are sent asynchronously from a thread spawned just for that purpose, so the event returns immediately.
 *
 * @version $Id: 36b89874d290c26bdb99e4ba484bb0854453ce71 $
 */
@Component("paidItemsEmailSendingEventListener")
public class SendEmailsWhenOrderIsPaid implements EventListener {
    @Inject
    private MailTemplateService mailTemplateService;

    @Inject
    private InvoicingService invoicingService;

    @Inject
    private Logger logger;

    @Inject
    private WebContext webContext;

    @Inject
    private GeneralSettings generalSettings;

    @Inject
    private SiteSettings siteSettings;

    @Inject
    private URLHelper urlHelper;

    @Inject
    private MultitenancySettings multitenancySettings;

    @Inject
    private ConfigurationService configurationService;

    @Inject
    private Provider<CustomerStore> customerStore;

    @Inject
    private Provider<AddressStore> addressStore;

    /**
     * @see {@link org.xwiki.observation.EventListener#getName()}
     */
    public String getName() {
        return "paidItemsEmailSendingEventListener";
    }

    /**
     * @see {@link org.xwiki.observation.EventListener#getEvents()}
     */
    public List<Event> getEvents() {
        return Arrays.<Event>asList(new OrderPaidEvent());
    }

    /**
     * @see {@link org.xwiki.observation.EventListener#onEvent(org.xwiki.observation.event.Event, Object, Object)}
     */
    public void onEvent(Event event, Object source, Object data) {
        GeneralSettings settings = configurationService.getSettings(GeneralSettings.class);
        final Order order = (Order) source;
        final Locale customerLocale = webContext.getLocale();
        final Tenant tenant = webContext.getTenant();
        final Locale tenantLocale = settings.getLocales().getMainLocale().getValue();
        final boolean withInvoice = invoicingService.isEnabledInContext();

        Executors.newSingleThreadExecutor().submit(new Callable<Void>() {
            @Override
            public Void call() throws Exception {
                sendNotificationMails(order, tenant, customerLocale, tenantLocale, withInvoice);
                return null;
            }
        });
    }

    /**
     * Sends the actual notification mails.
     *
     * Warning: Since this is done in a spawned thread, so we can't use the web context thread local data from there
     *
     * @param order the order concerned by the notifications mails
     * @param tenant the tenant concerned by the notifications mails
     * @param customerLocale the locale the customer browsed the site in when checking out
     * @param tenantLocale the main locale of the tenant
     * @param withInvoice whether to generate and include a PDF invoice with the notification mail
     */
    private void sendNotificationMails(Order order, Tenant tenant, Locale customerLocale, Locale tenantLocale,
            boolean withInvoice) {
        try {
            Customer customer = order.getCustomer();

            Optional<Address> billingAddress;
            if (order.getBillingAddress() != null) {
                billingAddress = Optional.of(order.getBillingAddress());
            } else {
                billingAddress = Optional.absent();
            }
            Optional<Address> deliveryAddress;
            if (order.getDeliveryAddress() != null) {
                deliveryAddress = Optional.of(order.getDeliveryAddress());
            } else {
                deliveryAddress = Optional.absent();
            }

            Map<String, Object> emailContext = prepareMailContext(order, customer, billingAddress, deliveryAddress,
                    tenant, tenantLocale);

            String customerEmail = customer.getEmail();

            MailTemplate customerNotificationEmail = getCustomerNotificationEmail(tenant, customerEmail,
                    customerLocale);
            MailTemplate tenantNotificationEmail = getTenantNotificationEmail(tenant, tenantLocale);

            // Generate invoice number and add add invoice to email if invoicing is enabled
            if (withInvoice) {
                try {
                    // TODO: there should be a way to not load the whole PDF in memory...
                    ByteArrayOutputStream pdfStream = new ByteArrayOutputStream();
                    InvoiceNumber invoiceNumber = invoicingService.getOrCreateInvoiceNumber(order);
                    invoicingService.generatePdfInvoice(order, pdfStream);

                    MailAttachment attachment = new MailAttachment(
                            new ByteArrayDataSource(pdfStream.toByteArray(), "application/pdf"),
                            "invoice-" + invoiceNumber.getNumber() + ".pdf");

                    customerNotificationEmail.addAttachment(attachment);
                    tenantNotificationEmail.addAttachment(attachment);
                } catch (InvoicingException e) {
                    logger.error("Failed to to generate invoice for email", e);
                }
            }

            sendNotificationMail(customerNotificationEmail, emailContext, tenant);
            sendNotificationMail(tenantNotificationEmail, emailContext, tenant);
        } catch (Exception e) {
            logger.error("Failed to send order notification email when sending email", e);
        }
    }

    /**
     * @param tenant the tenant of the shop the customer checked out from
     * @param customerEmail the email of the customer
     * @param locale the locale of the customer
     * @return an optional notification email object, present if all information is there and valid and the template
     * exists, absent otherwise
     */
    private MailTemplate getCustomerNotificationEmail(Tenant tenant, String customerEmail, Locale locale) {
        return new MailTemplate().from(getTenantContactEmail(tenant)).to(customerEmail)
                .template("order-paid-customer").locale(locale);
    }

    /**
     * @param tenant the tenant of the shop the customer checked out from
     * @param locale the main locale of the tenant
     * @return an optional notification email object, present if all information is there and valid and the template
     * exists, absent otherwise
     */
    private MailTemplate getTenantNotificationEmail(Tenant tenant, Locale locale) {
        return new MailTemplate().from(generalSettings.getNotificationsEmail()).to(getTenantContactEmail(tenant))
                .template("order-paid-tenant").locale(locale);
    }

    /**
     * Retrieves the contact email for a tenant. If the tenant is null ("global tenant"), then use the global site
     * settings
     *
     * @param tenant the tenant for which to get the contact email, or null for a global tenant
     * @return the found contact email or null if no contact email has been found
     */
    private String getTenantContactEmail(Tenant tenant) {
        if (tenant == null) {
            return siteSettings.getContactEmail();
        } else {
            return StringUtils.isNotBlank(tenant.getContactEmail()) ? tenant.getContactEmail()
                    : generalSettings.getNotificationsEmail();
        }
    }

    /**
     * Actually sends a notification email
     *
     * @param template the mail template
     * @param context the mail JSON context
     */
    private void sendNotificationMail(MailTemplate template, Map<String, Object> context, Tenant tenant) {
        try {
            mailTemplateService.sendTemplateMail(template, context, tenant);
        } catch (MailException e) {
            logger.error("Failed to send order paid email", ExceptionUtils.getRootCause(e));
        }
    }

    /**
     * Prepares the JSON context for an order mail notification
     *
     * @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> prepareMailContext(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>> itemsData = Lists.newArrayList();

        for (OrderItem item : items) {
            // Replace big decimal values by formatted values
            Map<String, Object> itemData = Maps.newHashMap();
            itemData.put("unitPrice", formatter.withLocale(locale)
                    .print(Money.of(currencyUnit, item.getUnitPrice(), RoundingMode.HALF_EVEN)));
            itemData.put("itemTotal", formatter.withLocale(locale)
                    .print(Money.of(currencyUnit, item.getItemTotal(), RoundingMode.HALF_EVEN)));
            itemData.put("title", item.getTitle());
            itemData.put("quantity", item.getQuantity());
            itemData.put("vatRate", item.getVatRate().longValue());
            itemsData.add(itemData);
        }

        context.put("items", itemsData);

        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"));
        }

        String siteName;
        if (tenant != null) {
            siteName = Strings.isNullOrEmpty(tenant.getName()) ? tenant.getSlug() : tenant.getName();
        } else {
            siteName = siteSettings.getName();
        }
        context.put("siteName", siteName);
        context.put("itemsTotal", itemsTotal);
        context.put("orderId", order.getSlug());
        context.put("grandTotal", grandTotal);
        context.put("additionalInformation", order.getAdditionalInformation());
        context.put("orderDate", new DateWebObject().withDate(order.getCreationDate(), locale));

        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.getTenantWebURL(tenant, "").toString());
        context.put("backOfficeUrl", urlHelper.getTenantBackOfficeURL(tenant, "").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;
    }
}