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