Every order in Elastic Path Commerce must have a unique number. Out of the box, we use an auto-incrementing value stored in the TORDERNUMBERGENERATOR table. Here is the OpenJPA annotation for the orderNumber property in OrderImpl.java:

      @Basic
      @Column(name = "ORDER_NUMBER", length = GUID_LENGTH, nullable = false, unique = true)
      @GeneratedValue(strategy = GenerationType.TABLE, generator = NEXT_ORDER_NUMBER)
      @TableGenerator(name = NEXT_ORDER_NUMBER, table = "TORDERNUMBERGENERATOR", pkColumnName = "UIDPK",
                 valueColumnName = NEXT_ORDER_NUMBER, pkColumnValue = "1", allocationSize = 1)
      public String getOrderNumber() {
            return this.orderNumber;
      }

The first annotation declares orderNumber as a basic property. The second annotation declares ORDER_NUMBER as the column to save/load the property. The third annotation declares this as an auto-generated value, so no need to explicitly assign a value when the order is created in our code. The fourth annotation says that we are using the table generator. The next available order number is retrieved from the NEXT_ORDER_NUMBER column in TORDERNUMBERGENERATOR.

 

You may need to customize the number generation. For example, one of our customers has multiple data centers and each data center is running its own separate production deployment of Elastic Path Commerce. Order data from the different data centers is sent to a centralized financial application. The financial application needs to have unique order numbers, but each Elastic Path deployment uses the same order number generator algorithm, so there will be duplicate order numbers across the different data centers.

 

Having a centralized order number generator was not an option. The solution was to add a prefix to every order number on all data centers. The prefix would identify the data center where the order was created and ensure that order numbers would be unique across all data centers. To implement this, we would need to do the following:


  • Create a CUSTOMIZEDORDERNUMBERGENERATOR database table with columns (UIDPK number, PREPEND varchar 3, NEXT_ORDER_NUMBER varchar 100). Each data center would store a different prefix in the PREPEND column.
  • Create a CustomizedOrderNumberGenerator, which would read the PREPEND column and prepend it to  the NEXT_ORDER_NUMBER.
  • Override the annotations for OrderImpl.getOrderNumber to use the new custom generator.


OpenJPA represents all generators internally with the org.apache.openjpa.kernel.Seq interface. This interface supplies all the context we need to create  our own custom generators, including the current persistence  environment, the JDBC DataSource,  and other essentials. The org.apache.openjpa.jdbc.kernel.AbstractJDBCSeq helps us create custom JDBC-based sequences. There are many  implementations in OpenJPA to generate different values as well, like AbstractJDBCSeq, ClassTableJDBCSeq, DelegatingSeq, NativeJDBCSeq, TableJDBCSeq, TimeSeededSeq, UUIDHexSeq, UUIDStringSeq, UUIDType4HexSeq, UUIDType4StringSeq, ValueTableJDBCSeq. Here is our CustomizedOrderNumberGenerator:

public class CustomizedOrderNumberGenerator extends ValueTableJDBCSeq {
 
    private static final String TABLE_NAME = "CUSTOMIZEDORDERNUMBERGENERATOR";
    private static final String PRIMARY_KEY_COLUMN = "UIDPK";
    private static final String PRIMARY_KEY_VALUE = "1";
    private static final String SEQUENCE_COLUMN = "NEXT_ORDER_NUMBER";
    private static final String PREFIX_COLUMN = "PREPEND";
 
    protected Object currentInternal(final JDBCStore store, final ClassMapping mapping) throws Exception {        
        return getPrefix(store, mapping) + super.currentInternal(store, mapping);    
    }
    
    protected Object nextInternal(final JDBCStore store, final ClassMapping mapping) throws Exception {
        return getPrefix(store, mapping) + super.nextInternal(store, mapping);    
    }
    
    protected String getPrefix(final JDBCStore store, final ClassMapping mapping) throws EpSystemException, SQLException {    
        if (prefix == null) {
            // Load from db
            Object primaryKey = getPrimaryKey(mapping);
    
            if (primaryKey == null) {    
                LOG.error(ERROR_CANNOT_GET_ORDER_PREFIX);
                throw new EpSystemException(ERROR_CANNOT_GET_ORDER_PREFIX);            
            }
            
            Connection conn = getConnection(store);            
            try {            
                prefix = getPrefix(conn);            
            } finally {            
                closeConnection(conn);            
            }         
        }            
        return prefix;            
    }
        
    
    private String getPrefix(final Connection conn) throws EpSystemException, SQLException {
        // Prepare the statement
        DBDictionary dict = getConfiguration().getDBDictionaryInstance();
        SQLBuffer sel = new SQLBuffer(dict).append(PREFIX_COLUMN);
        SQLBuffer where = new SQLBuffer(dict).append(PRIMARY_KEY_COLUMN).append(" = ").append(PRIMARY_KEY_VALUE);
        SQLBuffer tables = new SQLBuffer(dict).append(TABLE_NAME);
        
        SQLBuffer select = dict.toSelect(sel, null, tables, where, null, null,
                null, false, false, 0, Long.MAX_VALUE, false, true);
 
        PreparedStatement stmnt = select.prepareStatement(conn);
        
        ResultSet resultSet = null;
        
        try {
            //Do query
            resultSet = stmnt.executeQuery();
 
            if (!resultSet.next()) {
                LOG.error(ERROR_CANNOT_GET_ORDER_PREFIX);
                throw new EpSystemException(ERROR_CANNOT_GET_ORDER_PREFIX);
            }
            return dict.getString(resultSet, 1);
    
        } finally {        
            ...        
        }   
    }
}

 

The last step is to overwrite  the generator in order-orm.xml:

    <entity class="OrderImpl">
        <sequence-generator name="NEXT_ORDER_NUMBER" sequence-name="com.customize.CustomizedOrderNumberGenerator()" allocation-size="1" />
       ...
   </entity>

 

For more information on table generators, see the following links:

http://openjpa.apache.org/builds/1.2.0/apache-openjpa-1.2.0/docs/manual/jpa_overview_mapping_sequence.html#jpa_overview_mapping_sequence_tablegen

http://openjpa.apache.org/builds/1.0.2/apache-openjpa-1.0.2/docs/manual/jpa_overview_mapping_sequence.html

http://openjpa.apache.org/builds/1.0.2/apache-openjpa-1.0.2/docs/manual/ref_guide_sequence.html

1 Comments Permalink

A while back, I wrote about our decision to change to a two-page checkout process, with the main goal being to reduce checkout process abandonment. We piloted this checkout process on the Hockey Canada store and the results were extremely positive, but we weren't content to sit on our laurels. So, when we started re-designing the official Vancouver 2010 Olympic store, we challenged ourselves to take it to the next level -- and we cut the checkout process down to just one page.

 

Structurally, the new single-page checkout looks very much like the two-page checkout, with shipping information first, followed by billing and confirmation. The Elastic Path Commerce platform is flexible enough to handle multiple checkout process flows for the same store, so there was no significant Google Website Optimizer integration required to make this work.

 

Variant A (Control): Multi-page Checkout

 

Page 1 (sign in):

blog_old_1.png


 

Page 2 (shipping address):

blog_old_3.png


 

Page 3 (shipping method):

blog_old_4.png


Page 4 (billing & review):

blog_old_5.png


Page 5 (receipt)

blog_old_2.png


 

 

Variant B: Single Page Checkout

 

Page 1 (shipping, billing):

blog_new_1.png


Page 2 (receipt and optional user registration form):

blog_new_2.png

 

In A/B split testing, 50% of site traffic was redirected to the OOTB checkout, while the other 50% was served the new single-page checkout. By the time we reached 300 transactions, the winner was clear, and we stopped the experiment after 606 transactions. Google Website Optimizer concluded that the single-page checkout outperformed the out-of-the-box checkout by a whopping 21.8%. But what does that 21.8% really mean? GWO only counts goal conversions and does not link to any ecommerce data on Google Analytics, so we used Advanced Segments to get this data passed on to Google Analytics.

 

dfvnscm6_112d6j5vsdp_b.png

 

We defined two Advanced Segments by creating the following expressions:

 

  • Multi-step checkout: /(?:checkout|shipping-address|billing-address|delivery-options|billing-and-review)\.html.*
  • Single page checkout: /check-out\.html.*

 

This allowed us to track metrics like Average Order Value and Conversion Rate for each experiment variation.

 

Here's what we observed:

 

  • Successful completion rate for the entire checkout process increased by 257.26%.
  • Overall site conversion rate increased by 0.54%.
  • We also observed some unexpected improvements during this experiment, like an increase of 8.54% in the average order value!

 

While these numbers are impressive, they should not be used as the sole indicator of how single-page checkout performs. This is just what we observed when changing from the standard four-page checkout to a single-page checkout process on the Vancouver 2010 Olympic Store. Your mileage may vary, depending on your product, target market, etc. There's no silver bullet checkout process that works best for all business models. Doing your own A/B split testing will give you a better idea of what kinds of numbers you can expect.

1 Comments Permalink