Technical Blog

2 Posts tagged with the order tag

One of the pain points that can arise during a development project is the payment gateway's support of recycled order numbers. Live production gateways will track all historical order numbers and generate a duplicate order number error if an existing order number is reused. This is fine in production but troublesome when the payment gateway test servers also behave in this manner. Most payment gateway providers I have worked with in the past offer duplicate checks within an hour or two on their test servers, but beyond that, order numbers can be recycled which is a good thing for development purposes. Some providers, however, appear to extend this duplicate check to a day, week, or may even provide no support for order number reuse.


If you use the database population scripts that come with Elastic Path to facilitate schema/data changes, this limitation can frustrate your development team, who will most likely end up manually tracking used order numbers and making frequent updates to TORDERNUMBERGENERATOR.


If your payment gateway provider doesn't support order number reuse or at least not to the frequency you desire, what can you do?

 

Some Alternatives

The simplest approach would be to externalize the initial order number to the env.config and assign everyone a unique order number block. The problem with this is that each environment would have to keep track of the last order number used and update the env.config with the next number in the block with every new database build.


Another approach would be to externalize the initial order number (or perhaps even the order number incrementer) to a central database. This database would have a simple mapping of environment to next order number and each environment would have a sufficient block assigned to last the duration of the project. The problem with this approach is that many teams/environments are distributed and you may not be able to set up a central database which can be accessed by all environments.

 

A Better Solution

A better solution (though not 100% foolproof) is to construct the initial order number during the database build process using a unique element of the environment as well as a random element to get around frequent rebuilds on the same environment. To do this, we can create a custom Ant task that takes a fixed component (derived from either a fixed prefix or the environment's IP address) and appends a randomly generated number. If the random number is sufficiently large, the likelihood of duplicate hits will be minimal.

 

Creating the Ant Task

The following is an example of a custom Ant task that accepts min and max parameters for the random number component and a prefix. Regardless of whether a fixed prefix or an environment's IP address was provided, the task will use the hash code of the fixed value for the resulting order number in order to work with the out of the box numerical order number incrementer.

public class OrderNumberGeneratorTask  extends Task {
    private String min = null;
    private String max = null;
    private String property = null;
    private String prefix = null;
    private Random random = new Random(System.currentTimeMillis());
 
    @Override
    public void execute() throws BuildException {
         String localPrefix = null;
        if (min == null || min.equals(""))
            throw new BuildException("Min property is missing.");
 
        if (max == null || max.equals(""))
            throw new BuildException("Max property is missing.");
 
        int minInt = Integer.parseInt(min);
        int maxInt = Integer.parseInt(max);
 
        if (minInt > maxInt)
            throw new BuildException("Min is bigger than max.");
 
        localPrefix = prefix;
        if (localPrefix == null) {
             try {
                  localPrefix = InetAddress.getLocalHost().getHostAddress();
               } catch (UnknownHostException e) {
                    throw new BuildException("Unable to get local host address.", e);
               }
        }
        int randomInt = calculateRandom(minInt, maxInt);
 
        getProject().setNewProperty(property, String.valueOf(localPrefix.hashCode()) + String.valueOf(randomInt));
    }
 
    protected int calculateRandom(final int minInt, final int maxInt) {
        return minInt + random.nextInt(maxInt - minInt + 1);
    }

 

Updating the Build Process

To use this task, we need to package the compiled class in a jar and add it to the Maven repository along with an entry in the ant/setup/pom.xml so that it gets included in the ant setup script. Once the class is available on the Ant class path, we add the following task definition and task invocation to the ant/default.xml setup file. This will create the order number and store it in the ep.initial.order.number variable.

<?xml version="1.0" encoding="UTF-8"?>

<project name="ant_default" xmlns:artifact="antlib:org.apache.maven.artifact.ant" xmlns:contrib="antlib:net.sf.antcontrib">

  <taskdef name="ordernumbergenerator" classname="com.elasticpath.antextension.OrderNumberGeneratorTask"></taskdef>
... 
  <ordernumbergenerator min="1" max="1000000" property="ep.initial.order.number"></ordernumbergenerator>
...

 

Now we can use this variable in the database/src/base-insert.xml.vm file as the initial order number of the TORDERNUMBERGENERATOR table.

     <Tordernumbergenerator Uidpk="1" NextOrderNumber="${ep_initial_order_number}" />
1 Comments Permalink

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