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

com.adobe.cq.commerce.common.AbstractJcrCommerceSession Maven / Gradle / Ivy

/*************************************************************************
 *
 * ADOBE CONFIDENTIAL
 * __________________
 *
 *  Copyright 2012 Adobe Systems Incorporated
 *  All Rights Reserved.
 *
 * NOTICE:  All information contained herein is, and remains
 * the property of Adobe Systems Incorporated and its suppliers,
 * if any.  The intellectual and technical concepts contained
 * herein are proprietary to Adobe Systems Incorporated and its
 * suppliers and are protected by trade secret or copyright law.
 * Dissemination of this information or reproduction of this material
 * is strictly forbidden unless prior written permission is obtained
 * from Adobe Systems Incorporated.
 **************************************************************************/
package com.adobe.cq.commerce.common;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Currency;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.query.Query;
import javax.servlet.http.Cookie;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.Predicate;
import org.apache.commons.lang.StringUtils;
import org.apache.jackrabbit.api.JackrabbitSession;
import org.apache.jackrabbit.api.security.user.Authorizable;
import org.apache.jackrabbit.api.security.user.UserManager;
import org.apache.jackrabbit.util.ISO9075;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.api.wrappers.SlingHttpServletResponseWrapper;
import org.apache.sling.api.wrappers.ValueMapDecorator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import aQute.bnd.annotation.ConsumerType;
import com.adobe.cq.commerce.api.CommerceConstants;
import com.adobe.cq.commerce.api.CommerceException;
import com.adobe.cq.commerce.api.CommerceSession;
import com.adobe.cq.commerce.api.PaymentMethod;
import com.adobe.cq.commerce.api.PlacedOrder;
import com.adobe.cq.commerce.api.PlacedOrderResult;
import com.adobe.cq.commerce.api.PriceInfo;
import com.adobe.cq.commerce.api.Product;
import com.adobe.cq.commerce.api.ShippingMethod;
import com.adobe.cq.commerce.api.conf.CommerceBasePathsService;
import com.adobe.cq.commerce.api.promotion.Promotion;
import com.adobe.cq.commerce.api.promotion.PromotionHandler;
import com.adobe.cq.commerce.api.promotion.PromotionInfo;
import com.adobe.cq.commerce.api.promotion.PromotionInfo.PromotionStatus;
import com.adobe.cq.commerce.api.promotion.PromotionManager;
import com.adobe.cq.commerce.api.promotion.Voucher;
import com.adobe.cq.commerce.api.promotion.VoucherInfo;
import com.adobe.cq.commerce.api.smartlist.SmartListManager;
import com.adobe.cq.commerce.common.promotion.AbstractJcrVoucher;
import com.adobe.cq.commerce.impl.promotion.JcrPromotionImpl;
import com.adobe.cq.commerce.impl.promotion.JcrVoucherImpl;
import com.adobe.granite.security.user.UserProperties;
import com.adobe.granite.security.user.UserPropertiesManager;
import com.day.cq.commons.Language;
import com.day.cq.commons.jcr.JcrUtil;
import com.day.cq.i18n.I18n;
import com.day.cq.personalization.ContextSessionPersistence;
import com.day.cq.personalization.UserPropertiesUtil;


/**
 * This class is meant to be used as a base class for a CommerceSession implementation built
 * on top of a JCR repository.  GeoCommerceSessionImpl is an example.
 *
 * Parts of this class form a reference implementation.  That is, they are meant to show what
 * would be needed in an actual implementation, but aren't in themselves likely to be sufficient
 * for real-world cases.
 */
@ConsumerType
public class AbstractJcrCommerceSession implements CommerceSession {
    protected static final Logger log = LoggerFactory.getLogger(AbstractJcrCommerceSession.class);

    protected SlingHttpServletRequest request;
    protected SlingHttpServletResponse response;
    protected Resource resource;
    protected ResourceResolver resolver;
    protected AbstractJcrCommerceService commerceService;

    protected Locale locale = Locale.US;
    protected Locale userLocale = null;

    protected String PN_UNIT_PRICE = "price";
    protected String PN_ORDER_ID = "orderId";
    protected RoundingMode roundingMode = RoundingMode.HALF_UP;
    protected BigDecimal PRODUCT_TAX_RATE = new BigDecimal("0.06");
    protected BigDecimal SHIPPING_TAX_RATE = BigDecimal.ZERO;
    private String orderId;

    /**
     * The maximum size of the CommercePersistence cookie.
     * When this size is exceeded the cookie and subsequently the shopping cart is not updated.
     * The minimum cookie size that browsers are required to support is 4KB but there is a browser specific
     * overhead for a cookie which limits the length of useful content that can be stored in a cookie.
     * The value bellow has been chosen based on these considerations.
     */
    protected static final int COOKIE_SIZE_LIMIT = 4050;
    protected static final String PN_COMMERCE_PERSISTENCE_OVERFLOW = "cpo";
    protected static final int COOKIE_MAX_AGE_SECONDS = 3600 * 24 * 31;

    /**
     * @deprecated since 6.4, use {@link CommerceBasePathsService} instead
     */
    @Deprecated
    protected static final String ORDERS_BASE_PATH = "/var/commerce/orders/";

    protected static final String ORDERS_PATH_DATE_TEMPLATE = "yyyy/MM/dd";
    protected static final String ORDER_NAME = "order";

    protected static final String USER_ORDERS_PATH = "/commerce/orders/";
    protected static final String USER_ORDERS_DATE_TEMPLATE = "'order'-yyyy-MMM-dd";

    protected List cart = new ArrayList();

    protected List vouchers = new ArrayList();
    protected List promotions = new ArrayList();

    protected Map orderDetails = new HashMap();

    protected List prices;

    public AbstractJcrCommerceSession(AbstractJcrCommerceService commerceService,
                                      SlingHttpServletRequest request,
                                      SlingHttpServletResponse response,
                                      Resource resource) throws CommerceException {
        this.request = request;
        this.response = response;
        this.resource = resource;
        this.resolver = resource.getResourceResolver();
        this.commerceService = commerceService;

        Language lang = commerceService.serviceContext().languageManager.getCqLanguage(resource);
        if (lang != null && lang.getLocale().getCountry().length() > 0) {
            locale = lang.getLocale();
            loadCart();
        } else {
            loadCart();
            if (resource.getPath().startsWith("/content")) {
                log.debug("Unable to extract locale from page {}, falling back to default locale {}.", resource.getPath(), locale);
            } else {
                // Some resources, such as clientcontext store initializers and segmentation definitions,
                // are used for multiple languages/locales.  For empty carts we probably don't much care
                // what the locale is, and for non-empty carts we can infer the locale from the first item.
                if (cart.size() > 0) {
                    Resource firstProductPage = resolver.getResource(cart.get(0).getProduct().getPagePath());
                    if (firstProductPage != null) {
                        lang = commerceService.serviceContext().languageManager.getCqLanguage(firstProductPage);
                        if (lang != null && lang.getLocale().getCountry().length() > 0) {
                            locale = lang.getLocale();
                            calcOrder(); // re-calculate with the new locale
                        }
                    }
                }
                // If we didn't find a product page then we're going to go with the default locale, but
                // there's not much point in warning the author about something they can't fix, so we
                // suppress the log entry in this case.
                // log.warn("Unable to extract locale from page {}, falling back to default locale {}.", resource.getPath(), locale);
            }
        }
    }

    /**
     * Package internal constructor for testing.
     */
    AbstractJcrCommerceSession(ResourceResolver resolver) {
        this.resolver = resolver;
    }

    /**
     * Load a product into the cart.
     */
    private void loadProduct(String productPath, String quantityString, Map otherProperties) {
        try {
            Product product = commerceService.getProduct(productPath);
            if (product == null) {
                throw new CommerceException("product not found");  // handle all errors in catch block below
            }
            int quantity = 0;
            try {
                quantity = Integer.parseInt(quantityString);
            } catch (NumberFormatException e) {
                throw new CommerceException("quantity not a number");  // handle all errors in catch block below
            }
            if (quantity > 0) {
                doAddCartEntry(product, quantity, otherProperties);
            }
        } catch (CommerceException e) {
            // Probably due to a cart format change or a product change.  While it's not nice
            // to drop cart items, it's even worse to lock the shopper out, so we'll continue
            // bravely on.
            log.error("Unable to load product from cookie: " + productPath + "; qty: " + quantityString, e);
        }
    }

    /**
     * Load a voucher into the cart.
     */
    private void loadVoucher(String voucherPath) {
        try {
            Resource voucher = resolver.getResource(voucherPath);
            if (voucher == null) {
                throw new CommerceException("voucher not found");  // handle all errors in catch block below
            }
            vouchers.add(new JcrVoucherImpl(voucher));
        } catch (CommerceException e) {
            log.error("Unable to load voucher from cookie: " + voucherPath, e);
        }
    }

    /**
     * Load a promotion into the cart.
     */
    private void loadPromotion(String promotionPath) {
        try {
            Resource promotion = resolver.getResource(promotionPath);
            if (promotion == null) {
                throw new CommerceException("promotion not found");  // handle all errors in catch block below
            }
            promotions.add(new JcrPromotionImpl(promotion));
        } catch (CommerceException e) {
            log.error("Unable to load promotion from cookie: " + promotionPath);
            log.debug("Promotion not loaded", e);
        }
    }

    /**
     * Load the session state (including the cart entries, the order details, and any applied vouchers and promotions).
     *
     * 

This implementation uses cookies. In most cases concrete implementations will want to provide their own storage * architecture.

*/ protected void loadCart() throws CommerceException { // // Load cart from the cookie: // Map cartStore = ContextSessionPersistence.getStore(request, "CART", CommerceConstants.COMMERCE_COOKIE_NAME); String entryCountString = cartStore.get("entryCount"); if (entryCountString != null && entryCountString.length() > 0) { int entryCount = Integer.parseInt(entryCountString); for (int i = 0; i < entryCount; i++) { String product = cartStore.get("product" + i); String quantity = cartStore.get(PN_QUANTITY + i); Map properties = new HashMap(); String suffix = "_" + i; for (Map.Entry entry : cartStore.entrySet()) { String name = entry.getKey(); if (name.endsWith(suffix)) { name = name.substring(0, name.length() - suffix.length()); properties.put(name, entry.getValue()); } } loadProduct(product, quantity, properties); } } String voucherCountString = cartStore.get("voucherCount"); if (voucherCountString != null && voucherCountString.length() > 0) { int voucherCount = Integer.parseInt(voucherCountString); for (int i = 0; i < voucherCount; i++) { String voucher = cartStore.get("voucher" + i); loadVoucher(voucher); } } String promotionCountString = cartStore.get("promotionCount"); if (promotionCountString != null && promotionCountString.length() > 0) { int promotionCount = Integer.parseInt(promotionCountString); for (int i = 0; i < promotionCount; i++) { String promotion = cartStore.get("promotion" + i); loadPromotion(promotion); } } orderDetails = ContextSessionPersistence.getStore(request, "ORDER", CommerceConstants.COMMERCE_COOKIE_NAME); // assign an orderId if we don't already have one if (orderDetails.get(PN_ORDER_ID) == null) { orderDetails.put(PN_ORDER_ID, UUID.randomUUID().toString()); } calcOrder(); } /** * Save the session state (including the cart entries, the order details, and any applied vouchers and promotions). * *

This implementation uses cookies, although a commented-out JCR storage mechanism is also provided. In most * cases concrete implementations will want to provide their own storage architecture.

*/ protected void saveCart() throws CommerceException { // // Save cart to a cookie. // Map cartStore = new HashMap(); for (CartEntry entry : cart) { int entryIndex = entry.getEntryIndex(); cartStore.put("product" + entryIndex, entry.getProduct().getPath()); cartStore.put(PN_QUANTITY + entryIndex, "" + entry.getQuantity()); ValueMap properties = ((DefaultJcrCartEntry) entry).getProperties(); for (ValueMap.Entry property : properties.entrySet()) { String name = property.getKey(); String value = String.valueOf(property.getValue()); if (value != null) { cartStore.put(name + "_" + entryIndex, value); } } } cartStore.put("entryCount", "" + getCartEntryCount()); for (int i = 0; i < vouchers.size(); i++) { cartStore.put("voucher" + i, vouchers.get(i).getPath()); } cartStore.put("voucherCount", String.valueOf(vouchers.size())); for (int i = 0; i < promotions.size(); i++) { cartStore.put("promotion" + i, promotions.get(i).getPath()); } cartStore.put("promotionCount", String.valueOf(promotions.size())); Map> stores = new HashMap>(); stores.put("CART", cartStore); stores.put("ORDER", orderDetails); saveCommerceCookie(stores); } /** * A helper routine for writing cookies (and dealing with cookie length restrictions). */ private void saveCommerceCookie(Map> stores) { class MySlingHttpServletResponseWrapper extends SlingHttpServletResponseWrapper { private final List cookies = new ArrayList(); MySlingHttpServletResponseWrapper(SlingHttpServletResponse wrappedResponse) { super(wrappedResponse); } @Override public void addCookie(Cookie cookie) { cookie.setMaxAge(COOKIE_MAX_AGE_SECONDS); super.addCookie(cookie); if (cookie != null) cookies.add(cookie); } } Cookie commercePersistence = request.getCookie(CommerceConstants.COMMERCE_COOKIE_NAME); String oldValue = commercePersistence == null ? null : commercePersistence.getValue(); MySlingHttpServletResponseWrapper responseWrapper = new MySlingHttpServletResponseWrapper(response); ContextSessionPersistence.putStores(request, responseWrapper, stores, CommerceConstants.COMMERCE_COOKIE_NAME); boolean isCpoSet = false; for (Cookie cookie: responseWrapper.cookies) { if (CommerceConstants.COMMERCE_COOKIE_NAME.equals(cookie.getName())) { String value = cookie.getValue(); if (value != null && value.length() > COOKIE_SIZE_LIMIT) { if (oldValue != null) { cookie.setValue(oldValue); } else { cookie.setValue(""); } ContextSessionPersistence.put(request, response, PN_COMMERCE_PERSISTENCE_OVERFLOW, PN_COMMERCE_PERSISTENCE_OVERFLOW); isCpoSet = true; break; } } } if (!isCpoSet) { String cpl_param = ContextSessionPersistence.get(request, PN_COMMERCE_PERSISTENCE_OVERFLOW); if (cpl_param != null && cpl_param.trim().length() > 0) { ContextSessionPersistence.put(request, response, PN_COMMERCE_PERSISTENCE_OVERFLOW, ""); } } } /** * Returns true if an overflow is detected on the commerce persistence cookie. */ public static boolean hasCookieOverflow(SlingHttpServletRequest request, SlingHttpServletResponse response) { String cpl_param = ContextSessionPersistence.get(request, PN_COMMERCE_PERSISTENCE_OVERFLOW); boolean error = cpl_param != null && PN_COMMERCE_PERSISTENCE_OVERFLOW.equals(cpl_param.trim()); if (error) { ContextSessionPersistence.put(request, response, PN_COMMERCE_PERSISTENCE_OVERFLOW, ""); } return error; } /** * Logout is a NO-OP in this implementation. * *

To be overridden by concrete implementations requiring logout semantics (such as those employing * session state).

*/ @Override public void logout() throws CommerceException { } /** * Get the current locale. Will be the userLocale if set, otherwise the locale. */ protected Locale getLocale() { return userLocale != null ? userLocale : locale; } /** * Set the userLocale (which, if non-null, overrides the default locale). */ @Override public void setUserLocale(Locale locale) { userLocale = locale; try { calcOrder(); } catch (CommerceException e) { log.error("Could not recalculate order: ", e); } } @Override public Locale getUserLocale() { return userLocale; } /** * The base implementation does not support session- or order-specific destinations, so this * is a straight pass-through to the commerce service. */ @Override public List getAvailableCountries() throws CommerceException { return commerceService.getCountries(); } /** * The base implementation does not support session- or order-specific shipping methods, so this * is a straight pass-through to the commerce service. */ @Override public List getAvailableShippingMethods() throws CommerceException { return commerceService.getAvailableShippingMethods(); } /** * The base implementation does not support session- or order-specific payment methods, so this * is a straight pass-through to the commerce service. */ @Override public List getAvailablePaymentMethods() throws CommerceException { return commerceService.getAvailablePaymentMethods(); } @Override public List getProductPriceInfo(Product product) throws CommerceException { return getProductPriceInfo(product, null); } /** * A simple product pricing architecture supporting single-currency pricing and a fixed tax rate. * *

Note: most concrete implementations will want to override this to supply their own pricing * architecture.

*/ @Override public List getProductPriceInfo(Product product, Predicate filter) throws CommerceException { List prices = new ArrayList(); BigDecimal preTax = product.getProperty(PN_UNIT_PRICE, BigDecimal.class); if (preTax == null) { preTax = BigDecimal.ZERO; } final BigDecimal tax = preTax.multiply(PRODUCT_TAX_RATE); String currencyCode = Currency.getInstance(getLocale()).getCurrencyCode(); // // Note: order is important; non-fully-specified price requests will get the first match. // PriceInfo price = new PriceInfo(preTax, getLocale()); price.put(PriceFilter.PN_TYPES, new HashSet(Arrays.asList("UNIT", "PRE_TAX", currencyCode))); prices.add(price); price = new PriceInfo(tax, getLocale()); price.put(PriceFilter.PN_TYPES, new HashSet(Arrays.asList("UNIT", "TAX", currencyCode))); prices.add(price); price = new PriceInfo(preTax.add(tax), getLocale()); price.put(PriceFilter.PN_TYPES, new HashSet(Arrays.asList("UNIT", "POST_TAX", currencyCode))); prices.add(price); CollectionUtils.filter(prices, filter); return prices; } @Override public String getProductPrice(Product product) throws CommerceException { return getProductPrice(product, null); } @Override public String getProductPrice(Product product, Predicate filter) throws CommerceException { List prices = getProductPriceInfo(product, filter); return prices.size() > 0 ? prices.get(0).getFormattedString() : null; } @Override public int getCartEntryCount() { return cart.size(); } @Override public List getCartEntries() { return cart; } @Override /** * For a discussion of the classification and usage of prices, see {@link PriceInfo}. */ public List getCartPriceInfo(Predicate filter) { if (filter != null) { final ArrayList filteredPrices = new ArrayList(); CollectionUtils.select(prices, filter, filteredPrices); return filteredPrices; } return prices; } @Override public String getCartPrice(Predicate filter) throws CommerceException { final List prices = getCartPriceInfo(filter); return prices.isEmpty() ? "" : prices.get(0).getFormattedString(); } @Override public void addCartEntry(Product product, int quantity) throws CommerceException { addCartEntry(product, quantity, null); } @Override public void addCartEntry(Product product, int quantity, Map properties) throws CommerceException { doAddCartEntry(product, quantity, properties); calcCart(); saveCart(); } /** * Increment the quantity if the product already exists in the cart; otherwise add it. Any properties * associated with the product are updated in either case. * *

NB: recalculates the entry, but not the cart.

* * @param product * @param quantity * @param properties * @throws CommerceException */ protected void doAddCartEntry(Product product, int quantity, Map properties) throws CommerceException { for (CartEntry existingEntry : cart) { DefaultJcrCartEntry existingEntryImpl = (DefaultJcrCartEntry) existingEntry; if (existingEntryImpl.getProduct().getPath().equals(product.getPath())) { existingEntryImpl.setQuantity(existingEntryImpl.getQuantity() + quantity); existingEntryImpl.updateProperties(properties); calcEntry(existingEntryImpl.getEntryIndex()); return; } } DefaultJcrCartEntry newEntry = commerceService.newCartEntryImpl(cart.size(), product, quantity); newEntry.updateProperties(properties); cart.add(newEntry); doCalcEntry(newEntry, null, getLocale()); } @Override public void modifyCartEntry(int entryNumber, int quantity) throws CommerceException { doModifyCartEntry(entryNumber, quantity, null); } @Override public void modifyCartEntry(int entryNumber, Map delta) throws CommerceException { doModifyCartEntry(entryNumber, null, delta); calcCart(); saveCart(); } /** * Updates the quantity and properties of a cart entry. * *

NB: the cart entry is recalculated, but not the cart.

* * @param entryNumber * @param quantity * @param delta * @throws CommerceException */ protected void doModifyCartEntry(int entryNumber, Integer quantity, Map delta) throws CommerceException { if (entryNumber < cart.size()) { DefaultJcrCartEntry entry = (DefaultJcrCartEntry)cart.get(entryNumber); if (quantity != null) { entry.setQuantity(quantity); } entry.updateProperties(delta); calcEntry(entryNumber); } } @Override public void deleteCartEntry(int entryNumber) throws CommerceException { if (entryNumber < cart.size()) { cart.remove(entryNumber); } for (int i = 0; i < cart.size(); i++) { DefaultJcrCartEntry entry = (DefaultJcrCartEntry)cart.get(i); entry.setEntryIndex(i); } calcCart(); saveCart(); } /** * Recalculates the pricing info of an entry in the cart. * @param index The index of the entry. * @throws CommerceException */ public void calcEntry(int index) throws CommerceException { doCalcEntry((DefaultJcrCartEntry) cart.get(index), null, getLocale()); } /** * A simple entry pricing architecture in which a single currency unitPrice is stored in the * Product itself, and there is a single, fixed tax rate. * *

Note: most concrete implementations will want to override this to supply their own pricing * architecture.

* @param entry The line item in the cart to calculate. * @param discount A discount to apply to the total amount. * @param locale The locale (currency) to recalculate. */ protected void doCalcEntry(DefaultJcrCartEntry entry, BigDecimal discount, Locale locale) throws CommerceException { BigDecimal unitPrice; BigDecimal preTaxPrice; BigDecimal tax; BigDecimal totalPrice; unitPrice = entry.getProduct().getProperty(PN_UNIT_PRICE, BigDecimal.class); if (unitPrice == null) { unitPrice = BigDecimal.ZERO; preTaxPrice = BigDecimal.ZERO; tax = BigDecimal.ZERO; totalPrice = BigDecimal.ZERO; } else { preTaxPrice = unitPrice.multiply(new BigDecimal(entry.getQuantity())); if (discount != null) { preTaxPrice = preTaxPrice.subtract(discount); } tax = preTaxPrice.multiply(PRODUCT_TAX_RATE).setScale(2, roundingMode); totalPrice = preTaxPrice.add(tax); } // // Note: order is important; non-fully-specified price requests will get the first match. // entry.setPrice(new PriceInfo(preTaxPrice, locale), "LINE", "PRE_TAX"); entry.setPrice(new PriceInfo(tax, locale), "LINE", "TAX"); entry.setPrice(new PriceInfo(totalPrice, locale), "LINE", "POST_TAX"); entry.setPrice(new PriceInfo(unitPrice, locale), "UNIT", "PRE_TAX"); } /** * A helper routine which updates the order prices with a new {@link PriceInfo}. If all the * priceInfo's types match an existing entry, the entry will be updated. * *

Note: automatically add the priceInfo's currencyCode as an additional tag to ease * filtering on currency.

* * @param priceInfo A {@link PriceInfo} containing the amount and currency. * @param types Classifiers indicating the usage of the PriceInfo (ie: "UNIT", "PRE_TAX"). */ protected void setPrice(PriceInfo priceInfo, String... types) { if (prices == null) { prices = new ArrayList(); } List typeList = new ArrayList(Arrays.asList(types)); // Append the currencyCode to the typeList so it's easier to filter on currency... typeList.add(priceInfo.getCurrency().getCurrencyCode()); int index = prices.size(); for (int i=0; i < prices.size(); i++) { final PriceInfo price = prices.get(i); final Set priceTypes = (Set) price.get(PriceFilter.PN_TYPES); if (CollectionUtils.isEqualCollection(priceTypes, typeList)) { index = i; break; } } priceInfo.put(PriceFilter.PN_TYPES, new HashSet(typeList)); if (index == prices.size()) { prices.add(priceInfo); } else { prices.set(index, priceInfo); } } /** * A simple cart pricing architecture with no volume discount structure, shipping calculations, * etc., and a single currency. Just add up the cart entries. * *

Note: most concrete implementations will want to override this to supply their own pricing * architecture.

*/ protected void calcCart() { String currencyCode = Currency.getInstance(getLocale()).getCurrencyCode(); BigDecimal cartPreTaxPrice = BigDecimal.ZERO; BigDecimal cartTax = BigDecimal.ZERO; BigDecimal cartTotalPrice = BigDecimal.ZERO; BigDecimal cartDiscount = BigDecimal.ZERO; // find promotions List promotions = getActivePromotions(); try { for (CartEntry cartEntry : cart) { doCalcEntry((DefaultJcrCartEntry) cartEntry, null, getLocale()); // apply cart line item promotions BigDecimal entryDiscount = BigDecimal.ZERO; for (Promotion p : promotions) { try { PromotionHandler ph = p.adaptTo(PromotionHandler.class); PriceInfo discount = ph.applyCartEntryPromotion(this, p, cartEntry); if (discount != null && discount.getAmount().compareTo(BigDecimal.ZERO) > 0) { entryDiscount = entryDiscount.add(discount.getAmount()); } } catch (Exception e) { // NOSONAR (this is an extension point and we want to catch anything thrown) log.error("Applying cart line item promotion failed: ", e); } } doCalcEntry((DefaultJcrCartEntry) cartEntry, entryDiscount, getLocale()); cartDiscount = cartDiscount.add(entryDiscount); cartPreTaxPrice = cartPreTaxPrice.add(cartEntry.getPriceInfo(new PriceFilter("PRE_TAX", currencyCode)).get(0).getAmount()); cartTax = cartTax.add(cartEntry.getPriceInfo(new PriceFilter("TAX", currencyCode)).get(0).getAmount()); cartTotalPrice = cartTotalPrice.add(cartEntry.getPriceInfo(new PriceFilter("POST_TAX", currencyCode)).get(0).getAmount()); } setPrice(new PriceInfo(cartPreTaxPrice, getLocale()), "CART", "PRE_TAX"); setPrice(new PriceInfo(cartTax, getLocale()), "CART", "TAX"); setPrice(new PriceInfo(cartTotalPrice, getLocale()), "CART", "POST_TAX"); setPrice(new PriceInfo(cartDiscount, getLocale()), "DISCOUNT", "PRODUCTS"); } catch(CommerceException e) { log.error("Calculating cart failed: ", e); } } /** * Returns the list of currently active promotions (including those fired by vouchers). * @return List of active promotions */ public List getActivePromotions() { List activePromotions = new ArrayList(promotions.size()); for (Promotion promotion : promotions) { if (promotion.isValid()) { activePromotions.add(promotion); } } for (Voucher voucher : vouchers) { if (voucher.isValid(request)) { String path = voucher.getConfig().get("promotion", String.class); Resource resource = path == null ? null : resolver.getResource(path); Promotion promotion = resource == null ? null : resource.adaptTo(Promotion.class); if (promotion == null || !promotion.isValid()) { log.error("Cart contains voucher with invalid promotion: " + voucher.getPath()); } else { activePromotions.add(promotion); } } } Collections.sort(activePromotions, new Comparator() { public int compare(Promotion p1, Promotion p2) { return Long.valueOf(p2.getPriority()).compareTo(p1.getPriority()); } }); return activePromotions; } @Override public boolean supportsClientsidePromotionResolution() { return true; } /** * Adds a client-side-resolved promotion to the current session. * @param path A string uniquely identifying the promotion (the path for internal {@link Promotion}s; * an id for external ones).) * @throws CommerceException */ @Override public void addPromotion(String path) throws CommerceException { Promotion p = commerceService.getPromotion(path); if (p == null) { throw new CommerceException("Invalid promotion: " + path); } promotions.add(p); calcCart(); saveCart(); } @Override /** * Removes a client-side-resolved promotion form the current session. */ public void removePromotion(String path) throws CommerceException { for (int i = 0; i < promotions.size(); i++) { if (promotions.get(i).getPath().equals(path)) { promotions.remove(i--); } } calcCart(); saveCart(); } /** * A CQ-centric implementation which assumes all promotion resolution is done on the client. The list * of promotions is therefore exactly what was set by calls to add/removePromotion(). * *

To be overridden by concrete implementations wishing to provide external-commerce-engine-resolved * promotions (either exclusively or in addition to CQ-resolved promotions).

*/ @Override public List getPromotions() throws CommerceException { List promotionInfos = new ArrayList(0); for (Promotion p : promotions) { String description = null; Map messages = null; PromotionHandler handler = p.adaptTo(PromotionHandler.class); if (handler != null) { description = handler.getDescription(request, this, p); messages = handler.getMessages(request, this, p); } if (description == null || description.length() == 0) { description = p.getDescription(); } if (messages != null) { // Add a promotionInfo for each cartEntry that has a message. for (Map.Entry message : messages.entrySet()) { Integer key = message.getKey(); if (key != -1) { promotionInfos.add(new PromotionInfo(p.getPath(), p.getTitle(), PromotionStatus.FIRED, null, message.getValue(), key)); } } } promotionInfos.add(new PromotionInfo(p.getPath(), p.getTitle(), PromotionStatus.FIRED, description, messages != null ? messages.get(-1) : null, null)); } return promotionInfos; } /** * Calculate the shipping amount. * *

Note: to be overridden by concrete implementation.

*/ protected BigDecimal getShipping(String method) { throw new UnsupportedOperationException(); } /** * A simple, single-currency order pricing architecture. * *

Note: most concrete implementations will want to override this to supply their own pricing * architecture.

*/ protected void calcOrder() throws CommerceException { calcCart(); final String currencyCode = Currency.getInstance(getLocale()).getCurrencyCode(); final PriceInfo cartTax = getCartPriceInfo(new PriceFilter("TAX", currencyCode)).get(0); final PriceInfo cartPreTaxPrice = getCartPriceInfo(null).get(0); BigDecimal orderSubTotal = cartPreTaxPrice.getAmount(); BigDecimal orderShipping; try { String shippingMethod = orderDetails.get(CommerceConstants.SHIPPING_OPTION); orderShipping = getShipping(shippingMethod); } catch (Exception e) { log.error("Shipping calculation failed", e); orderShipping = BigDecimal.ZERO; } BigDecimal orderShippingTax = orderShipping.multiply(SHIPPING_TAX_RATE).setScale(2, roundingMode); BigDecimal orderTotalTax = cartTax.getAmount().add(orderShippingTax); BigDecimal orderTotalPrice = orderSubTotal.add(orderTotalTax.add(orderShipping)); // // Note: order is important; non-fully-specified price requests will get the first match. // setPrice(new PriceInfo(orderShipping, getLocale()), "SHIPPING", "PRE_TAX"); setPrice(new PriceInfo(orderShippingTax, getLocale()), "SHIPPING", "TAX"); setPrice(new PriceInfo(orderShipping.add(orderShippingTax), getLocale()), "SHIPPING", "POST_TAX"); setPrice(new PriceInfo(orderShipping, getLocale()), "SHIPPING", "PRE_PROMO"); setPrice(new PriceInfo(orderTotalPrice, getLocale()), "ORDER", "TOTAL"); setPrice(new PriceInfo(orderSubTotal, getLocale()), "ORDER", "SUB_TOTAL"); setPrice(new PriceInfo(orderTotalTax, getLocale()), "ORDER", "TAX"); // apply order-level promotions BigDecimal orderDiscount = BigDecimal.ZERO; List promotions = getActivePromotions(); for (Promotion p : promotions) { try { PromotionHandler ph = p.adaptTo(PromotionHandler.class); PriceInfo discount = ph.applyOrderPromotion(this, p); if (discount != null && discount.getAmount().compareTo(BigDecimal.ZERO) > 0) { orderSubTotal = orderSubTotal.subtract(discount.getAmount()); orderDiscount = orderDiscount.add(discount.getAmount()); break; // only a single order promotion is allowed } } catch (Exception e) { // NOSONAR (this is an extension point and we want to catch anything thrown) log.error("Applying order-level promotion failed: ", e); } } // ... and apply shipping discounts BigDecimal shippingDiscount = BigDecimal.ZERO; for (Promotion p : promotions) { try { PromotionHandler ph = p.adaptTo(PromotionHandler.class); PriceInfo discount = ph.applyShippingPromotion(this, p); if (discount != null && discount.getAmount().compareTo(BigDecimal.ZERO) > 0) { orderShipping = orderShipping.subtract(discount.getAmount()); shippingDiscount = shippingDiscount.add(discount.getAmount()); break; // only a single shipping promotion is allowed } } catch (Exception e) { // NOSONAR (this is an extension point and we want to catch anything thrown) log.error("Applying shipping promotion failed: ", e); } } final PriceInfo productDiscount = getCartPriceInfo(new PriceFilter("DISCOUNT", "PRODUCTS", currencyCode)).get(0); BigDecimal totalDiscount = orderDiscount.add(shippingDiscount).add(productDiscount.getAmount()); // Recalculate after promotions: orderShippingTax = orderShipping.multiply(SHIPPING_TAX_RATE).setScale(2, roundingMode); orderTotalTax = cartTax.getAmount().add(orderShippingTax); orderTotalPrice = orderSubTotal.add(orderTotalTax.add(orderShipping)); // // Note: order is important; non-fully-specified price requests will get the first match. // setPrice(new PriceInfo(orderShipping, getLocale()), "SHIPPING", "PRE_TAX"); setPrice(new PriceInfo(orderShippingTax, getLocale()), "SHIPPING", "TAX"); setPrice(new PriceInfo(orderShipping.add(orderShippingTax), getLocale()), "SHIPPING", "POST_TAX"); setPrice(new PriceInfo(orderTotalPrice, getLocale()), "ORDER", "TOTAL"); setPrice(new PriceInfo(orderSubTotal, getLocale()), "ORDER", "SUB_TOTAL"); setPrice(new PriceInfo(orderTotalTax, getLocale()), "ORDER", "TAX"); setPrice(new PriceInfo(totalDiscount, getLocale()), "DISCOUNT", "TOTAL"); setPrice(new PriceInfo(orderDiscount, getLocale()), "DISCOUNT", "ORDER"); setPrice(new PriceInfo(shippingDiscount, getLocale()), "DISCOUNT", "SHIPPING"); // Add in the available shipping method prices: List shippingMethods = getAvailableShippingMethods(); for (ShippingMethod shippingMethod : shippingMethods) { String method = shippingMethod.getPath(); setPrice(new PriceInfo(getShipping(method), getLocale()), CommerceConstants.SHIPPING_OPTION, method); } } /** * Return a list of vouchers that were added to the cart via {@link #addVoucher(String)}, complete with * run-time details such as shopper messages and applied discount information. */ @Override public List getVoucherInfos() throws CommerceException { List list = new ArrayList(); for (Voucher voucher : vouchers) { list.add(new VoucherInfo(voucher.getCode(), voucher.getPath(), voucher.getTitle(), voucher.getDescription(), voucher.isValid(request), voucher.getMessage(request))); } return list; } /** * Add a {@link Voucher}, identified by its code, to the cart. Invalid vouchers will result in a * {@link CommerceException} with an internationalized message regarding the validity status. */ @Override public void addVoucher(String code) throws CommerceException { PromotionManager pm = resolver.adaptTo(PromotionManager.class); final Voucher voucher = pm.findVoucher(request, code); if (voucher == null) { final I18n i18n = new I18n(request); throw new CommerceException(i18n.get("Invalid voucher code.")); } if (!voucher.isValid(request)) { throw new CommerceException(voucher.getMessage(request)); } for (Voucher existingVoucher : vouchers) { if (existingVoucher.getCode().equals(voucher.getCode())) { final I18n i18n = new I18n(request); throw new CommerceException(i18n.get("Voucher already added.")); } } vouchers.add(voucher); calcCart(); saveCart(); } /** * Remove all {@link Voucher}s from the cart which match a particular voucher code. */ @Override public void removeVoucher(String code) throws CommerceException { for (int i = 0; i < vouchers.size(); i++) { if (vouchers.get(i).getCode().equals(code)) { vouchers.remove(i--); } } calcCart(); saveCart(); } /** * Return an ID uniquely identifying the current order. */ @Override public String getOrderId() throws CommerceException { if (orderId == null) return orderDetails.get(PN_ORDER_ID); else return orderId; } /** * Handle order updates. This implementation assumes any relevant predicates will come in as prefixes * on the individual map keys (eg: "billing.street"), which is most friendly to CQ forms processing. * *

Will be called multiple times for multi-page checkouts.

* *

Much of the payment info is sensitive. Some of it, such as the CCV, should not be stored at all, * and other items, such as the primary-account-number should only be stored if encrypted.

* *

See https://www.pcisecuritystandards.org/documents/PCI%20SSC%20Quick%20Reference%20Guide.pdf for * further info.

* *

This implementation tokenizes payment info. For this reason, all payment info must be updated in * a single call (although it does not need to the the only call). Tokenization is left to the * concrete implementation (see {@link #tokenizePaymentInfo(Map)} for more details).

*/ protected void doUpdateOrderDetails(Map delta) throws CommerceException { Map paymentDetails = new HashMap(); boolean shippingAddressSame = StringUtils.isNotEmpty(delta.get(CommerceConstants.SHIPPING_ADDR_SAME)); PaymentMethod paymentMethod = getPaymentMethod(delta); String paymentPrefix = paymentMethod != null ? paymentMethod.getPredicate() + "." : CommerceConstants.PAYMENT_PREFIX; for (Map.Entry entry : delta.entrySet()) { String key = entry.getKey(); if (key.equals(CommerceConstants.SHIPPING_ADDR_SAME)) { continue; } if (key.startsWith(paymentPrefix)) { paymentDetails.put(key, entry.getValue()); continue; } orderDetails.put(key, entry.getValue()); if (shippingAddressSame && key.startsWith(CommerceConstants.BILLING_ADDRESS_PREDICATE + ".")) { String shippingKey = key.replace(CommerceConstants.BILLING_ADDRESS_PREDICATE, CommerceConstants.BILLING_ADDRESS_PREDICATE); orderDetails.put(shippingKey, entry.getValue()); } } if (!paymentDetails.isEmpty()) { orderDetails.put(CommerceConstants.PAYMENT_TOKEN, tokenizePaymentInfo(paymentDetails)); } } /** * This implementation assumes the payment option will be keyed with {@link CommerceConstants#PAYMENT_OPTION}, * and that the value associated with that key will be the path to a resource adaptable to {@link PaymentMethod}. * * It should be overridden to support other options/implementations. */ protected PaymentMethod getPaymentMethod(Map delta) { String paymentOption = delta.get(CommerceConstants.PAYMENT_OPTION); if (paymentOption != null) { Resource resource = resolver.getResource(paymentOption); if (resource != null) { return resource.adaptTo(PaymentMethod.class); } } return null; } @Override public void updateOrder(Map delta) throws CommerceException { Map newDelta = new HashMap(); ValueMap vm = new ValueMapDecorator(delta); for (String key : vm.keySet()) { String value = vm.get(key, String.class); if (value != null) { newDelta.put(key, value); } } doUpdateOrderDetails(newDelta); saveCart(); } /** * Handle predicate-based order updates. This implementation assumes a flat order property space, * and simply prefixes any keys with the predicate name. */ protected void doUpdateOrderDetails(Map delta, String predicate) throws CommerceException { if (StringUtils.isNotEmpty(predicate)) { predicate = "." + predicate; } Map newDelta = new HashMap(); ValueMap vm = new ValueMapDecorator(delta); for (String key : vm.keySet()) { String value = vm.get(key, String.class); if (value != null) { newDelta.put(predicate + "." + key, value); } } doUpdateOrderDetails(newDelta); } @Override public void updateOrderDetails(Map details, String predicate) throws CommerceException { doUpdateOrderDetails(details, predicate); saveCart(); } @Override public Map getOrderDetails() throws CommerceException { if (orderDetails.isEmpty() || (orderDetails.size() == 1 && orderDetails.containsKey(PN_ORDER_ID))) { try { // Initialize billing and shipping addresses from user profile: // Session userSession = resolver.adaptTo(Session.class); final UserProperties userProperties = request.adaptTo(UserProperties.class); if (userProperties != null && !UserPropertiesUtil.isAnonymous(userProperties)) { UserManager um = ((JackrabbitSession) userSession).getUserManager(); Authorizable authorizable = um.getAuthorizable(userProperties.getAuthorizableID()); UserPropertiesManager upm = commerceService.serviceContext().userPropertiesService.createUserPropertiesManager(resolver); UserProperties profile = upm.getUserProperties(authorizable.getID(), "profile"); Map address = new HashMap(); address.put("firstname", profile.getProperty(UserProperties.GIVEN_NAME)); address.put("lastname", profile.getProperty(UserProperties.FAMILY_NAME)); address.put("street1", profile.getProperty("streetAddress")); address.put("city", profile.getProperty("city")); address.put("state", profile.getProperty("region")); address.put("zip", profile.getProperty("postalCode")); doUpdateOrderDetails(address, CommerceConstants.BILLING_ADDRESS_PREDICATE); doUpdateOrderDetails(address, CommerceConstants.SHIPPING_ADDRESS_PREDICATE); } } catch (RepositoryException e) { // best efforts; if something goes wrong just leave the address blank } } return orderDetails; } @Override public Map getOrder() throws CommerceException { return new HashMap(getOrderDetails()); } /** * This implementation uses a flat internal order property storage with (optionally) prefixed * property keys. * *

Predicate-based queries match on prefix, and return "naked" keys (ie: with the prefixes * stripped off).

*/ @Override public Map getOrderDetails(String predicate) throws CommerceException { Map fullDetails = getOrderDetails(); Map returnDetails = new HashMap(); for (Map.Entry detail : fullDetails.entrySet()) { String key = detail.getKey(); if (key.startsWith(predicate + ".")) { returnDetails.put(key.substring(predicate.length() + 1), detail.getValue()); } } return returnDetails; } /** * Returns a token which uniquely identifies an in-progress payment. * *

To be overridden by concrete implementation.

* @param paymentDetails The payment details, such as credit card info, PayPal account info, etc. * @return A token uniquely identifying the payment. */ protected String tokenizePaymentInfo(Map paymentDetails) throws CommerceException { return UUID.randomUUID().toString(); // return a random number for integration testing.... } /** * Update any order details and then save the cart and order details to the repo. * Clear the shopping cart, and initiate any required order processing. */ protected void doPlaceOrder(Map orderDetailsDelta) throws CommerceException { doUpdateOrderDetails(orderDetailsDelta); String orderPath = savePlacedOrderVendorRecord(); savePlacedOrderShopperRecord(); orderId = getOrderId(); cart.clear(); vouchers.clear(); orderDetails.clear(); saveCart(); initiateOrderProcessing(orderPath); } /** * Perform application-specific steps to process an order (such as payment processing and fulfillment). * *

Some applications might delegate order processing to a workflow, in which case this method might * remain a NO-OP.

*/ protected void initiateOrderProcessing(String orderPath) throws CommerceException { } /** * Return a string describing the current status of a placed order. * *

To be overridden by concrete implementation.

*/ protected String getOrderStatus(String orderId) throws CommerceException { return ""; } @Override public void placeOrder(Map delta) throws CommerceException { Map newDelta = new HashMap(); ValueMap vm = new ValueMapDecorator(delta); for (String key : vm.keySet()) { String value = vm.get(key, String.class); if (value != null) { newDelta.put(key, value); } } doPlaceOrder(newDelta); } /** * Create the orders storage location if it doesn't yet exist. Make sure it's locked down by the * service session. */ protected void initializeOrderStorage(Session session) throws RepositoryException { CommerceBasePathsService cbps = resolver.adaptTo(CommerceBasePathsService.class); String ordersBasePath = cbps.getOrdersBasePath(); if (!session.nodeExists(ordersBasePath)) { JcrUtil.createPath(ordersBasePath, "nt:unstructured", session); session.save(); } } /** * Saves a copy of a placed order for the vendor in /var/commerce/orders. * *

To be overridden by concrete implementations which have more specific requirements on the details * saved, or the location of saved details.

*/ protected String savePlacedOrderVendorRecord() throws CommerceException { String vendorRecordPath = null; Calendar now = Calendar.getInstance(locale); Session serviceSession = null; try { serviceSession = commerceService.serviceContext().slingRepository.loginService("orders", null); CommerceBasePathsService cbps = resolver.adaptTo(CommerceBasePathsService.class); Node ordersBaseNode = serviceSession.getNode(cbps.getOrdersBasePath()); SimpleDateFormat dateFormatter = new SimpleDateFormat(ORDERS_PATH_DATE_TEMPLATE); String relativeOrderPath = dateFormatter.format(now.getTime()) + "/" + ORDER_NAME; // JcrUtil.createPath() is not atomic so we synchronize here to make sure we create a unique node synchronized (AbstractJcrCommerceSession.class) { Node vendorRecord = JcrUtil.createPath(ordersBaseNode, relativeOrderPath, true, "sling:Folder", "nt:unstructured", serviceSession, true); writeOrder(vendorRecord, now, serviceSession); serviceSession.save(); vendorRecordPath = vendorRecord.getPath(); } } catch (RepositoryException e) { throw new CommerceException("Failed to save completed order: ", e); } finally { if (serviceSession != null) { serviceSession.logout(); } } return vendorRecordPath; } /** * Saves a copy of a placed order for the shopper in ~/commerce/orders. * *

To be overridden by concrete implementations which have more specific requirements on the details * saved, or the location of saved details.

*/ protected void savePlacedOrderShopperRecord() throws CommerceException { Calendar now = Calendar.getInstance(locale); try { Session userSession = resolver.adaptTo(Session.class); final UserProperties userProperties = request.adaptTo(UserProperties.class); if (userProperties != null && !UserPropertiesUtil.isAnonymous(userProperties)) { UserManager um = ((JackrabbitSession) userSession).getUserManager(); Authorizable user = um.getAuthorizable(userProperties.getAuthorizableID()); SimpleDateFormat dateFormatter = new SimpleDateFormat(USER_ORDERS_DATE_TEMPLATE); String userOrderPath = user.getPath() + USER_ORDERS_PATH + dateFormatter.format(now.getTime()); Node shopperRecord = JcrUtil.createPath(userOrderPath, true, "sling:Folder", "nt:unstructured", userSession, false); writeOrder(shopperRecord, now, userSession); userSession.save(); } } catch (RepositoryException e) { throw new CommerceException("Failed to save completed order to user's home: ", e); } } /** * Writes a copy of an order (including cart line items, order details, applied promotions and applied vouchers) * to a node in the repository. */ protected void writeOrder(Node orderNode, Calendar placedTime, Session session) throws CommerceException, RepositoryException { List entries = new ArrayList(); for (CartEntry entry : cart) { entries.add(serializeCartEntry(entry)); } orderNode.setProperty("cartItems", entries.toArray(new String[entries.size()])); final String currencyCode = Currency.getInstance(getLocale()).getCurrencyCode(); final BigDecimal cartPreTaxPrice = getCartPriceInfo(new PriceFilter("PRE_TAX", currencyCode)).get(0).getAmount(); final BigDecimal orderShipping = getCartPriceInfo(new PriceFilter("SHIPPING", currencyCode)).get(0).getAmount(); final BigDecimal orderTotalTax = getCartPriceInfo(new PriceFilter("ORDER", "TAX", currencyCode)).get(0).getAmount(); final BigDecimal orderTotalPrice = getCartPriceInfo(new PriceFilter("ORDER", "TOTAL", currencyCode)).get(0).getAmount(); orderNode.setProperty("jcr:language", getLocale().toString()); orderNode.setProperty("currencyCode", currencyCode); orderNode.setProperty("cartSubtotal", cartPreTaxPrice); orderNode.setProperty("orderShipping", orderShipping); orderNode.setProperty("orderTotalTax", orderTotalTax); orderNode.setProperty("orderTotalPrice", orderTotalPrice); orderNode.setProperty("orderPlaced", placedTime); orderNode.setProperty("orderId", orderDetails.get(PN_ORDER_ID)); Node orderDetailsNode = JcrUtil.createUniqueNode(orderNode, "order-details", "nt:unstructured", session); for (Map.Entry entry : orderDetails.entrySet()) { String detail = serializeOrderDetail(entry.getKey(), entry.getValue()); if (detail != null) { String parts[] = detail.split("="); if (parts.length == 2) { orderDetailsNode.setProperty(parts[0], parts[1]); } } } List infos = new ArrayList(); for (PromotionInfo promotionInfo : getPromotions()) { String info = serializePromotionInfo(promotionInfo); if (info != null) { infos.add(info); } } orderNode.setProperty("promotions", infos.toArray(new String[infos.size()])); infos = new ArrayList(); for (VoucherInfo voucherInfo : getVoucherInfos()) { String info = serializeVoucherInfo(voucherInfo); if (info != null) { infos.add(info); } } orderNode.setProperty("vouchers", infos.toArray(new String[infos.size()])); } /** * Serializes a {@link CartEntry} to a String of the form "product_path;quantity;property1_name=property1_value\fproperty2_name=property2_value\f...". */ protected String serializeCartEntry(CartEntry entry) throws CommerceException { String str = commerceService.serializeCartEntryData(entry.getProduct().getPath(), entry.getQuantity(), ((DefaultJcrCartEntry) entry).getProperties()); return str; } /** * Constructs a CartEntry object from a String created by {@link #serializeCartEntry(com.adobe.cq.commerce.api.CommerceSession.CartEntry)}. * * @param str the serialized cart entry * @param index the cart entry index * @return the CartEntry instance * @throws CommerceException */ protected CartEntry deserializeCartEntry(String str, int index) throws CommerceException { Object[] entryData = commerceService.deserializeCartEntryData(str); Product product = (Product) entryData[0]; int quantity = (Integer) entryData[1]; DefaultJcrCartEntry entry = commerceService.newCartEntryImpl(index, product, quantity); if (entryData[2] == null) { return entry; } @SuppressWarnings("unchecked") Map properties = (Map) entryData[2]; entry.updateProperties(properties); return entry; } /** * Serializes an order property to a String of the form "property_name=property_value". * *

NB: sensitive information is stripped during serialization.

*/ protected String serializeOrderDetail(String key, String value) throws CommerceException { if (key.equals(PN_ORDER_ID)) { // written out in parent node return null; } // The current implementation tokenizes the paymentInfo, so no sensitive properties will be left in the // order details. However, we retain code to strip out sensitive information for backwards compatibility // and for added safety. if (key.contains("primary-account-number")) { if (value.length() > 4) { // mask all but last 4 characters value = value.substring(0, value.length()-4).replaceAll("[0-9]", "x") + value.substring(value.length()-4); } } else if (key.contains("ccv")) { // drop the ccv entirely: the PCI DSS forbids its storage return null; } return key + "=" + value; } /** * Serializes a valid {@link VoucherInfo} to a String of the form "code;path;message". * Invalid vouchers return null. */ protected String serializeVoucherInfo(VoucherInfo voucherInfo) { if (voucherInfo.getIsValid()) { return voucherInfo.getCode() + ";" + voucherInfo.getPath() + ";" + voucherInfo.getMessage(); } return null; } /** * Serializes a fired {@link PromotionInfo} to a String of the form * "promo_path;cart_entry_index;message". Un-fired promotions return null. */ protected String serializePromotionInfo(PromotionInfo promotionInfo) { if (promotionInfo.getStatus() == PromotionStatus.FIRED) { Integer entryIndex = promotionInfo.getCartEntryIndex(); return promotionInfo.getPath() + ";" + entryIndex + ";" + promotionInfo.getMessage(); } return null; } /** * Returns a list of placed orders for the current shopper. * @param predicate An optional implementation-specific predicate name. */ @Override public PlacedOrderResult getPlacedOrders(String predicate, int pageNumber, int pageSize, String sortId) throws CommerceException { List orders = new ArrayList(); try { Session userSession = resolver.adaptTo(Session.class); final UserProperties userProperties = request.adaptTo(UserProperties.class); if (userProperties != null && !UserPropertiesUtil.isAnonymous(userProperties)) { UserManager um = ((JackrabbitSession) userSession).getUserManager(); Authorizable user = um.getAuthorizable(userProperties.getAuthorizableID()); // example query: /jcr:root/home/users/geometrixx/[email protected]/commerce/orders//element(*)[@orderId] StringBuilder buffer = new StringBuilder(); buffer.append("/jcr:root") .append(ISO9075.encodePath(user.getPath() + USER_ORDERS_PATH)) .append("/element(*)[@orderId] option(traversal ok)"); final Query query = userSession.getWorkspace().getQueryManager().createQuery(buffer.toString(), Query.XPATH); NodeIterator nodeIterator = query.execute().getNodes(); Predicate filter = getPredicate(predicate); while (nodeIterator.hasNext()) { DefaultJcrPlacedOrder order = newPlacedOrderImpl(nodeIterator.nextNode().getPath()); if (filter == null || filter.evaluate(order)) { orders.add(order); } } } } catch (Exception e) { // NOSONAR (fail-safe for when the query above contains errors) log.error("Error while fetching orders", e); } return new PlacedOrderResult(orders, null, null); } /** * Return a {@link Predicate} (identified by a String key) for {@link PlacedOrder} filtering. * *

To be overridden by concrete implementation. Implementations should support at least the key * {@link CommerceConstants#OPEN_ORDERS_PREDICATE}.

*/ protected Predicate getPredicate(String predicateName) { return null; } /** * Instantiates a new placed order. Override this method to supply custom extensions of {@link DefaultJcrPlacedOrder}. * @param orderId The unique identifier of the order. * @return */ public DefaultJcrPlacedOrder newPlacedOrderImpl(String orderId) { return new DefaultJcrPlacedOrder(this, orderId); } @Override public PlacedOrder getPlacedOrder(String orderId) throws CommerceException { return newPlacedOrderImpl(orderId); } /** * Default implementation assumes a repository-based smart lists and the corresponding smart list manager. Override * this method to supply custom implementation of {@link SmartListManager}. */ @Override public SmartListManager getSmartListManager() { return request != null ? request.adaptTo(SmartListManager.class) : null; } /* * ================================================================================================== * Deprecated methods. For backwards-compatibility only. * ================================================================================================== */ @Deprecated // since 5.6 protected NumberFormat formatter = NumberFormat.getCurrencyInstance(locale); @Deprecated // since 5.6 public String getPriceInfo(Product product) throws CommerceException { List prices = getProductPriceInfo(product); return prices.size() > 0 ? prices.get(0).getFormattedString() : null; } @Deprecated // since 5.6 public String getCartPreTaxPrice() throws CommerceException { return getCartPrice(null); } @Deprecated // since 5.6 public String getCartTax() throws CommerceException { return getCartPrice(new PriceFilter("TAX")); } @Deprecated // since 5.6 public String getCartTotalPrice() throws CommerceException { return getCartPrice(new PriceFilter("POST_TAX")); } @Deprecated // since 5.6 public String getOrderShipping() throws CommerceException { return getCartPrice(new PriceFilter("SHIPPING")); } @Deprecated // since 5.6 public String getOrderTotalTax() throws CommerceException { return getCartPrice(new PriceFilter("ORDER", "TAX")); } @Deprecated // since 5.6 public String getOrderTotalPrice() throws CommerceException { return getCartPrice(new PriceFilter("ORDER")); } @Deprecated // since 5.6 public String getShippingPrice(String method) { return new PriceInfo(getShipping(method), userLocale != null ? userLocale : locale).getFormattedString(); } /** * A simple, single-currency, single-locale price formatter. * @deprecated since 5.6, use {@link PriceInfo} instead */ @Deprecated // since 5.6 protected String formatPrice(BigDecimal price) { if (price == null) { return ""; } else { return formatter.format(price.doubleValue()); } } /** * @deprecated since 6.0; use {@link #getVoucherInfos()} instead */ @Deprecated public List getVouchers() throws CommerceException { List vouchers = new ArrayList(); for (Voucher voucher : this.vouchers) { vouchers.add(new AbstractJcrVoucher(resolver.getResource(voucher.getPath()))); } return vouchers; } @Deprecated // since 5.6; use updateOrder() public void updateOrderDetails(Map delta) throws CommerceException { doUpdateOrderDetails(delta); saveCart(); } @Deprecated // since 5.6; use placeOrder() public void submitOrder(Map orderDetailsDelta) throws CommerceException { doPlaceOrder(orderDetailsDelta); } /** * Instantiates a new cart entry. Override this method to supply custom extensions of {@link DefaultJcrCartEntry}. * * @param index The index of the entry with the current cart or PlacedOrder. * @param product The product the new entry represents. * @param quantity The quantity. * * @deprecated since 6.1; use {@link AbstractJcrCommerceService#newCartEntryImpl(int, com.adobe.cq.commerce.api.Product, int)} instead. */ @Deprecated public DefaultJcrCartEntry newCartEntryImpl(int index, Product product, int quantity) { return commerceService.newCartEntryImpl(index, product, quantity); } /** * Increment the quantity if the product already exists in the cart; otherwise add it. * *

NB: re-calculates the entry, but not the cart.

* * @deprecated since 6.1; use {@link #doAddCartEntry(com.adobe.cq.commerce.api.Product, int, Map)} instead. */ @Deprecated protected void doAddCartEntry(Product product, int quantity) throws CommerceException { doAddCartEntry(product, quantity, null); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy