killbill-memoizeit

Merge branch 'payment-plus-account' of github.com:alenad/killbill

1/5/2012 3:33:58 PM

Changes

account/pom.xml 2(+1 -1)

api/pom.xml 2(+1 -1)

beatrix/pom.xml 2(+1 -1)

catalog/pom.xml 2(+1 -1)

invoice/pom.xml 2(+1 -1)

payment/pom.xml 2(+1 -1)

pom.xml 2(+1 -1)

util/pom.xml 2(+1 -1)

Details

account/pom.xml 2(+1 -1)

diff --git a/account/pom.xml b/account/pom.xml
index 16ecca5..2107a34 100644
--- a/account/pom.xml
+++ b/account/pom.xml
@@ -13,7 +13,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill</artifactId>
-        <version>0.0.20-SNAPSHOT</version>
+        <version>0.0.21-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-account</artifactId>
diff --git a/account/src/main/java/com/ning/billing/account/api/user/DefaultAccountChangeNotification.java b/account/src/main/java/com/ning/billing/account/api/user/DefaultAccountChangeNotification.java
index 6bcc634..178100f 100644
--- a/account/src/main/java/com/ning/billing/account/api/user/DefaultAccountChangeNotification.java
+++ b/account/src/main/java/com/ning/billing/account/api/user/DefaultAccountChangeNotification.java
@@ -50,40 +50,44 @@ public class DefaultAccountChangeNotification implements AccountChangeNotificati
     }
 
     private List<ChangedField> calculateChangedFields(Account oldData, Account newData) {
-        List<ChangedField> changedFields = new ArrayList<ChangedField>();
 
-        if (!newData.getExternalKey().equals(oldData.getExternalKey())) {
-            changedFields.add(new DefaultChangedField("externalKey", oldData.getExternalKey(), newData.getExternalKey()));
-        }
-        if (!newData.getEmail().equals(oldData.getEmail())) {
-            changedFields.add(new DefaultChangedField("email", oldData.getEmail(), newData.getEmail()));
-        }
-        if (!newData.getName().equals(oldData.getName())) {
-            changedFields.add(new DefaultChangedField("firstName", oldData.getName(), newData.getName()));
-        }
-        if (!newData.getPhone().equals(oldData.getPhone())) {
-            changedFields.add(new DefaultChangedField("phone", oldData.getPhone(), newData.getPhone()));
-        }
-        if (!newData.getCurrency().equals(oldData.getCurrency())) {
-            changedFields.add(new DefaultChangedField("currency", oldData.getCurrency().toString(), newData.getCurrency().toString()));
-        }
-        if (newData.getBillCycleDay() != oldData.getBillCycleDay()) {
-            changedFields.add(new DefaultChangedField("billCycleDay", Integer.toString(oldData.getBillCycleDay()),
-                                                               Integer.toString(newData.getBillCycleDay())));
-        }
+        List<ChangedField> tmpChangedFields = new ArrayList<ChangedField>();
 
-        String oldProviderName = oldData.getPaymentProviderName();
-        String newProviderName = newData.getPaymentProviderName();
+        addIfValueChanged(tmpChangedFields, "externalKey",
+                oldData.getExternalKey(), newData.getExternalKey());
 
-        if ((newProviderName == null) && (oldProviderName == null)) {
-        } else if ((newProviderName == null) && (oldProviderName != null)) {
-            changedFields.add((new DefaultChangedField("paymentProviderName", oldProviderName, newProviderName)));
-        } else if ((newProviderName != null) && (oldProviderName == null)) {
-            changedFields.add((new DefaultChangedField("paymentProviderName", oldProviderName, newProviderName)));
-        } else if (!newProviderName.equals(oldProviderName)) {
-            changedFields.add((new DefaultChangedField("paymentProviderName", oldProviderName, newProviderName)));
-        }
+        addIfValueChanged(tmpChangedFields, "email",
+                oldData.getEmail(), newData.getEmail());
 
-        return changedFields;
+        addIfValueChanged(tmpChangedFields, "firstName",
+                oldData.getName(), newData.getName());
+
+        addIfValueChanged(tmpChangedFields, "phone",
+                oldData.getPhone(), newData.getPhone());
+
+        addIfValueChanged(tmpChangedFields, "currency",
+                (oldData.getCurrency() != null) ? oldData.getCurrency().toString() : null,
+                 (newData.getCurrency() != null) ? newData.getCurrency().toString() : null);
+
+        addIfValueChanged(tmpChangedFields,
+                "billCycleDay",
+                Integer.toString(oldData.getBillCycleDay()), Integer.toString(newData.getBillCycleDay()));
+
+        addIfValueChanged(tmpChangedFields,"paymentProviderName",
+                oldData.getPaymentProviderName(), newData.getPaymentProviderName());
+        return tmpChangedFields;
+    }
+
+    private void addIfValueChanged(List<ChangedField> inputList, String key, String oldData, String newData) {
+        // If both null => no changes
+        if (newData == null && oldData == null) {
+            return;
+        // If only one is null
+        } else if (newData == null || oldData == null) {
+            inputList.add(new DefaultChangedField(key, oldData, newData));
+        // If non are null we can safely compare values
+        } else if (!newData.equals(oldData)) {
+            inputList.add(new DefaultChangedField(key, oldData, newData));
+        }
     }
 }
diff --git a/account/src/main/java/com/ning/billing/account/glue/AccountModule.java b/account/src/main/java/com/ning/billing/account/glue/AccountModule.java
index 275d4c9..2905db3 100644
--- a/account/src/main/java/com/ning/billing/account/glue/AccountModule.java
+++ b/account/src/main/java/com/ning/billing/account/glue/AccountModule.java
@@ -16,13 +16,15 @@
 
 package com.ning.billing.account.glue;
 
+import org.skife.config.ConfigurationObjectFactory;
+
 import com.google.inject.AbstractModule;
+import com.ning.billing.account.api.AccountService;
 import com.ning.billing.account.api.AccountUserApi;
+import com.ning.billing.account.api.DefaultAccountService;
 import com.ning.billing.account.api.user.DefaultAccountUserApi;
 import com.ning.billing.account.dao.AccountDao;
-import com.ning.billing.account.dao.AccountSqlDao;
 import com.ning.billing.account.dao.DefaultAccountDao;
-import org.skife.config.ConfigurationObjectFactory;
 
 public class AccountModule extends AbstractModule {
 
@@ -34,19 +36,23 @@ public class AccountModule extends AbstractModule {
     private void installAccountCore() {
     }
 
-    private void installAccountDao() {
+    protected void installAccountDao() {
         bind(AccountDao.class).to(DefaultAccountDao.class).asEagerSingleton();
     }
 
-    private void installAccountUserApi() {
+    protected void installAccountUserApi() {
         bind(AccountUserApi.class).to(DefaultAccountUserApi.class).asEagerSingleton();
     }
 
+    private void installAccountService() {
+        bind(AccountService.class).to(DefaultAccountService.class).asEagerSingleton();
+    }
+
     @Override
     protected void configure() {
         installConfig();
-        installAccountCore();
         installAccountDao();
+        installAccountService();
         installAccountUserApi();
     }
 }
diff --git a/account/src/test/java/com/ning/billing/account/dao/MockAccountDao.java b/account/src/test/java/com/ning/billing/account/dao/MockAccountDao.java
new file mode 100644
index 0000000..3a49474
--- /dev/null
+++ b/account/src/test/java/com/ning/billing/account/dao/MockAccountDao.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.account.dao;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+import com.ning.billing.account.api.Account;
+
+public class MockAccountDao implements AccountDao {
+    private final Map<String, Account> accounts = new ConcurrentHashMap<String, Account>();
+
+    @Override
+    public void save(Account entity) {
+        accounts.put(entity.getId().toString(), entity);
+    }
+
+    @Override
+    public Account getById(String id) {
+        return accounts.get(id);
+    }
+
+    @Override
+    public List<Account> get() {
+        return new ArrayList<Account>(accounts.values());
+    }
+
+    @Override
+    public void test() {
+    }
+
+    @Override
+    public Account getAccountByKey(String key) {
+        for (Account account : accounts.values()) {
+            if (key.equals(account.getExternalKey())) {
+                return account;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public UUID getIdFromKey(String externalKey) {
+        Account account = getAccountByKey(externalKey);
+        return account == null ? null : account.getId();
+    }
+
+}
diff --git a/analytics/pom.xml b/analytics/pom.xml
index 1ed7927..03e7d72 100644
--- a/analytics/pom.xml
+++ b/analytics/pom.xml
@@ -13,7 +13,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill</artifactId>
-        <version>0.0.20-SNAPSHOT</version>
+        <version>0.0.21-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-analytics</artifactId>
diff --git a/analytics/src/main/java/com/ning/billing/analytics/AnalyticsListener.java b/analytics/src/main/java/com/ning/billing/analytics/AnalyticsListener.java
index cce74e7..262e9d9 100644
--- a/analytics/src/main/java/com/ning/billing/analytics/AnalyticsListener.java
+++ b/analytics/src/main/java/com/ning/billing/analytics/AnalyticsListener.java
@@ -38,9 +38,12 @@ public class AnalyticsListener
     public void handleSubscriptionTransitionChange(final SubscriptionTransition event)
     {
         switch (event.getTransitionType()) {
+            case MIGRATE_ENTITLEMENT:
+                // TODO do nothing for now
+            break;
             case CREATE:
                 bstRecorder.subscriptionCreated(event);
-                break;
+            break;
             case CANCEL:
                 bstRecorder.subscriptionCancelled(event);
                 break;
diff --git a/analytics/src/test/java/com/ning/billing/analytics/MockIEntitlementUserApi.java b/analytics/src/test/java/com/ning/billing/analytics/MockIEntitlementUserApi.java
index 3017413..12b6f77 100644
--- a/analytics/src/test/java/com/ning/billing/analytics/MockIEntitlementUserApi.java
+++ b/analytics/src/test/java/com/ning/billing/analytics/MockIEntitlementUserApi.java
@@ -24,7 +24,9 @@ import org.joda.time.DateTime;
 import com.ning.billing.account.api.AccountData;
 import com.ning.billing.catalog.api.BillingPeriod;
 import com.ning.billing.catalog.api.PhaseType;
+import com.ning.billing.catalog.api.PlanPhaseSpecifier;
 import com.ning.billing.entitlement.api.user.EntitlementUserApi;
+
 import com.ning.billing.entitlement.api.user.EntitlementUserApiException;
 import com.ning.billing.entitlement.api.user.Subscription;
 import com.ning.billing.entitlement.api.user.SubscriptionBundle;
@@ -104,9 +106,13 @@ public class MockIEntitlementUserApi implements EntitlementUserApi
     }
 
 	@Override
-	public Subscription createSubscription(UUID bundleId, String productName,
-			BillingPeriod term, String priceList, PhaseType initialPhase,
+	public Subscription createSubscription(UUID bundleId, PlanPhaseSpecifier spec,
 			DateTime requestedDate) throws EntitlementUserApiException {
-		throw new UnsupportedOperationException();
+        throw new UnsupportedOperationException();
 	}
+
+    @Override
+    public SubscriptionBundle getBundleForKey(String bundleKey) {
+        throw new UnsupportedOperationException();
+    }
 }
diff --git a/analytics/src/test/java/com/ning/billing/analytics/MockSubscription.java b/analytics/src/test/java/com/ning/billing/analytics/MockSubscription.java
index cff4ad7..5982bcd 100644
--- a/analytics/src/test/java/com/ning/billing/analytics/MockSubscription.java
+++ b/analytics/src/test/java/com/ning/billing/analytics/MockSubscription.java
@@ -127,4 +127,9 @@ public class MockSubscription implements Subscription
     public List<SubscriptionTransition> getActiveTransitions() {
         throw new UnsupportedOperationException();
     }
+
+    @Override
+    public SubscriptionTransition getPendingTransition() {
+        throw new UnsupportedOperationException();
+    }
 }

api/pom.xml 2(+1 -1)

diff --git a/api/pom.xml b/api/pom.xml
index a511d8e..b787d9f 100644
--- a/api/pom.xml
+++ b/api/pom.xml
@@ -13,7 +13,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill</artifactId>
-        <version>0.0.20-SNAPSHOT</version>
+        <version>0.0.21-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-api</artifactId>
diff --git a/api/src/main/java/com/ning/billing/entitlement/api/EntitlementService.java b/api/src/main/java/com/ning/billing/entitlement/api/EntitlementService.java
index b325d6b..53854d1 100644
--- a/api/src/main/java/com/ning/billing/entitlement/api/EntitlementService.java
+++ b/api/src/main/java/com/ning/billing/entitlement/api/EntitlementService.java
@@ -17,13 +17,13 @@
 package com.ning.billing.entitlement.api;
 
 import com.ning.billing.entitlement.api.billing.EntitlementBillingApi;
+import com.ning.billing.entitlement.api.migration.EntitlementMigrationApi;
 import com.ning.billing.entitlement.api.test.EntitlementTestApi;
 import com.ning.billing.entitlement.api.user.EntitlementUserApi;
 import com.ning.billing.lifecycle.KillbillService;
 
 public interface EntitlementService extends KillbillService {
 
-
     @Override
     public String getName();
 
@@ -33,5 +33,5 @@ public interface EntitlementService extends KillbillService {
 
     public EntitlementTestApi getTestApi();
 
-
+    public EntitlementMigrationApi getMigrationApi();
 }
diff --git a/api/src/main/java/com/ning/billing/entitlement/api/migration/EntitlementMigrationApi.java b/api/src/main/java/com/ning/billing/entitlement/api/migration/EntitlementMigrationApi.java
new file mode 100644
index 0000000..9498c6f
--- /dev/null
+++ b/api/src/main/java/com/ning/billing/entitlement/api/migration/EntitlementMigrationApi.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.entitlement.api.migration;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import com.ning.billing.catalog.api.PlanPhaseSpecifier;
+import com.ning.billing.catalog.api.ProductCategory;
+
+public interface EntitlementMigrationApi {
+
+
+    public interface EntitlementAccountMigration {
+        public UUID getAccountKey();
+        public EntitlementBundleMigration [] getBundles();
+    }
+
+    public interface EntitlementBundleMigration {
+        public String getBundleKey();
+        public EntitlementSubscriptionMigration [] getSubscriptions();
+    }
+
+    public interface EntitlementSubscriptionMigration {
+        public ProductCategory getCategory();
+        public EntitlementSubscriptionMigrationCase [] getSubscriptionCases();
+    }
+
+    /**
+     *
+     * Each case is either a PHASE or a different PlanSpecifer
+     */
+    public interface EntitlementSubscriptionMigrationCase {
+        public PlanPhaseSpecifier getPlanPhaseSpecifer();
+        public DateTime getEffectiveDate();
+        public DateTime getCancelledDate();
+    }
+
+
+    /**
+     * Migrate all the existing entitlements associated with that account.
+     * The semantics is 'all or nothing' (atomic operation)
+     *
+     * @param toBeMigrated all the bundles and associated subscription that should be migrated for the account
+     *
+     */
+    public void migrate(EntitlementAccountMigration toBeMigrated)
+        throws EntitlementMigrationApiException;
+
+    /**
+     * Remove all the data pertaining to that acount
+     *
+     * @param accountKey
+     */
+    public void undoMigration(UUID accountKey)
+        throws EntitlementMigrationApiException;
+
+}
diff --git a/api/src/main/java/com/ning/billing/entitlement/api/migration/EntitlementMigrationApiException.java b/api/src/main/java/com/ning/billing/entitlement/api/migration/EntitlementMigrationApiException.java
new file mode 100644
index 0000000..55835fd
--- /dev/null
+++ b/api/src/main/java/com/ning/billing/entitlement/api/migration/EntitlementMigrationApiException.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.entitlement.api.migration;
+
+public class EntitlementMigrationApiException extends Exception {
+
+    private static final long serialVersionUID = 7623133L;
+
+    public EntitlementMigrationApiException() {
+        super();
+    }
+
+    public EntitlementMigrationApiException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public EntitlementMigrationApiException(String message) {
+        super(message);
+    }
+
+    public EntitlementMigrationApiException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/api/src/main/java/com/ning/billing/entitlement/api/user/EntitlementUserApi.java b/api/src/main/java/com/ning/billing/entitlement/api/user/EntitlementUserApi.java
index 1f431ed..22b9830 100644
--- a/api/src/main/java/com/ning/billing/entitlement/api/user/EntitlementUserApi.java
+++ b/api/src/main/java/com/ning/billing/entitlement/api/user/EntitlementUserApi.java
@@ -19,9 +19,7 @@ package com.ning.billing.entitlement.api.user;
 import java.util.List;
 import java.util.UUID;
 import org.joda.time.DateTime;
-import com.ning.billing.account.api.AccountData;
-import com.ning.billing.catalog.api.BillingPeriod;
-import com.ning.billing.catalog.api.PhaseType;
+import com.ning.billing.catalog.api.PlanPhaseSpecifier;
 
 
 public interface EntitlementUserApi {
@@ -30,6 +28,8 @@ public interface EntitlementUserApi {
 
     public Subscription getSubscriptionFromId(UUID id);
 
+    public SubscriptionBundle getBundleForKey(String bundleKey);
+
     public List<SubscriptionBundle> getBundlesForAccount(UUID accountId);
 
     public List<Subscription> getSubscriptionsForBundle(UUID bundleId);
@@ -39,6 +39,7 @@ public interface EntitlementUserApi {
     public SubscriptionBundle createBundleForAccount(UUID accountId, String bundleKey)
         throws EntitlementUserApiException;
 
-    public Subscription createSubscription(UUID bundleId, String productName, BillingPeriod term, String priceList, PhaseType initialPhase, DateTime requestedDate)
+    public Subscription createSubscription(UUID bundleId, PlanPhaseSpecifier spec, DateTime requestedDate)
         throws EntitlementUserApiException;
+
 }
diff --git a/api/src/main/java/com/ning/billing/entitlement/api/user/Subscription.java b/api/src/main/java/com/ning/billing/entitlement/api/user/Subscription.java
index 8f13e06..4b56301 100644
--- a/api/src/main/java/com/ning/billing/entitlement/api/user/Subscription.java
+++ b/api/src/main/java/com/ning/billing/entitlement/api/user/Subscription.java
@@ -67,4 +67,6 @@ public interface Subscription {
 
     public List<SubscriptionTransition> getActiveTransitions();
 
+    public SubscriptionTransition getPendingTransition();
+
 }
diff --git a/api/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionTransition.java b/api/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionTransition.java
index 517a7b8..26ce81f 100644
--- a/api/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionTransition.java
+++ b/api/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionTransition.java
@@ -27,6 +27,7 @@ import java.util.UUID;
 public interface SubscriptionTransition extends EventBusNotification {
 
     public enum SubscriptionTransitionType {
+        MIGRATE_ENTITLEMENT,
         CREATE,
         CHANGE,
         PAUSE,
diff --git a/api/src/main/java/com/ning/billing/invoice/api/InvoicePaymentApi.java b/api/src/main/java/com/ning/billing/invoice/api/InvoicePaymentApi.java
index bde12a9..f196379 100644
--- a/api/src/main/java/com/ning/billing/invoice/api/InvoicePaymentApi.java
+++ b/api/src/main/java/com/ning/billing/invoice/api/InvoicePaymentApi.java
@@ -16,19 +16,23 @@
 
 package com.ning.billing.invoice.api;
 
-import com.ning.billing.catalog.api.Currency;
-
 import java.math.BigDecimal;
 import java.util.List;
 import java.util.UUID;
+
 import org.joda.time.DateTime;
 
+import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.payment.api.InvoicePayment;
+
 public interface InvoicePaymentApi {
-    public void paymentSuccessful(UUID invoiceId, BigDecimal amount, Currency currency, UUID paymentId, DateTime paymentAttemptDate);
+    public void paymentSuccessful(UUID invoiceId, BigDecimal amount, Currency currency, UUID paymentAttemptId, DateTime paymentAttemptDate);
 
-    public void paymentFailed(UUID invoiceId, UUID paymentId, DateTime paymentAttemptDate);
+    public void paymentFailed(UUID invoiceId, UUID paymentAttemptId, DateTime paymentAttemptDate);
 
     public List<Invoice> getInvoicesByAccount(UUID accountId);
 
     public Invoice getInvoice(UUID invoiceId);
+
+    public InvoicePayment getInvoicePayment(UUID invoiceId, UUID paymentAttemptId);
 }
diff --git a/api/src/main/java/com/ning/billing/payment/api/InvoicePayment.java b/api/src/main/java/com/ning/billing/payment/api/InvoicePayment.java
new file mode 100644
index 0000000..aaed6ff
--- /dev/null
+++ b/api/src/main/java/com/ning/billing/payment/api/InvoicePayment.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.payment.api;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import com.ning.billing.catalog.api.Currency;
+
+public class InvoicePayment {
+        private final UUID invoiceId;
+        private final UUID paymentAttemptId;
+        private final DateTime paymentAttemptDate;
+        private final BigDecimal amount;
+        private final Currency currency;
+
+        public InvoicePayment(UUID invoiceId, BigDecimal amount, Currency currency, UUID paymentAttemptId, DateTime paymentAttemptDate) {
+            this.invoiceId = invoiceId;
+            this.paymentAttemptId = paymentAttemptId;
+            this.paymentAttemptDate = paymentAttemptDate;
+            this.amount = amount;
+            this.currency = currency;
+        }
+
+        public UUID getInvoiceId() {
+            return invoiceId;
+        }
+
+        public UUID getPaymentAttemptId() {
+            return paymentAttemptId;
+        }
+
+        public DateTime getPaymentAttemptDate() {
+            return paymentAttemptDate;
+        }
+
+        public BigDecimal getAmount() {
+            return amount;
+        }
+
+        public Currency getCurrency() {
+            return currency;
+        }
+
+        @Override
+        public String toString() {
+            return "InvoicePayment [invoiceId=" + invoiceId + ", paymentAttemptId=" + paymentAttemptId + ", paymentAttemptDate=" + paymentAttemptDate + ", amount=" + amount + ", currency=" + currency + "]";
+        }
+
+}
diff --git a/api/src/main/java/com/ning/billing/payment/api/PaymentAttempt.java b/api/src/main/java/com/ning/billing/payment/api/PaymentAttempt.java
new file mode 100644
index 0000000..c4b6bfd
--- /dev/null
+++ b/api/src/main/java/com/ning/billing/payment/api/PaymentAttempt.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.payment.api;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+
+import com.ning.billing.invoice.api.Invoice;
+
+public class PaymentAttempt {
+    private final UUID paymentAttemptId;
+    private final DateTime paymentAttemptDate;
+    private final Invoice invoice;
+
+    public PaymentAttempt(UUID paymentAttemptId, Invoice invoice) {
+        this.paymentAttemptId = paymentAttemptId;
+        this.paymentAttemptDate = new DateTime(DateTimeZone.UTC);
+        this.invoice = invoice;
+    }
+
+    public UUID getPaymentAttemptId() {
+        return paymentAttemptId;
+    }
+
+    public DateTime getPaymentAttemptDate() {
+        return paymentAttemptDate;
+    }
+
+    public Invoice getInvoice() {
+        return invoice;
+    }
+
+    @Override
+    public String toString() {
+        return "PaymentAttempt [paymentAttemptId=" + paymentAttemptId + ", paymentAttemptDate=" + paymentAttemptDate + ", invoice=" + invoice + "]";
+    }
+
+}
diff --git a/api/src/main/java/com/ning/billing/payment/api/PaymentInfo.java b/api/src/main/java/com/ning/billing/payment/api/PaymentInfo.java
index d19afe7..897ba89 100644
--- a/api/src/main/java/com/ning/billing/payment/api/PaymentInfo.java
+++ b/api/src/main/java/com/ning/billing/payment/api/PaymentInfo.java
@@ -26,6 +26,113 @@ import com.google.common.base.Objects;
 import com.ning.billing.util.eventbus.EventBusNotification;
 
 public class PaymentInfo implements EventBusNotification {
+    private final String id;
+    private final BigDecimal amount;
+    private final BigDecimal refundAmount;
+    private final BigDecimal appliedCreditBalanceAmount;
+    private final String paymentNumber;
+    private final String bankIdentificationNumber;
+    private final String status;
+    private final String type;
+    private final String referenceId;
+    private final DateTime effectiveDate;
+    private final DateTime createdDate;
+    private final DateTime updatedDate;
+
+    public PaymentInfo(PaymentInfo src) {
+        this.id = src.id;
+        this.amount = src.amount;
+        this.refundAmount = src.refundAmount;
+        this.appliedCreditBalanceAmount = src.appliedCreditBalanceAmount;
+        this.paymentNumber = src.paymentNumber;
+        this.bankIdentificationNumber = src.bankIdentificationNumber;
+        this.status = src.status;
+        this.type = src.type;
+        this.referenceId = src.referenceId;
+        this.effectiveDate = src.effectiveDate;
+        this.createdDate = src.createdDate;
+        this.updatedDate = src.updatedDate;
+    }
+
+    @JsonCreator
+    public PaymentInfo(@JsonProperty("id") String id,
+                       @JsonProperty("amount") BigDecimal amount,
+                       @JsonProperty("appliedCreditBalanceAmount") BigDecimal appliedCreditBalanceAmount,
+                       @JsonProperty("bankIdentificationNumber") String bankIdentificationNumber,
+                       @JsonProperty("paymentNumber") String paymentNumber,
+                       @JsonProperty("refundAmount") BigDecimal refundAmount,
+                       @JsonProperty("status") String status,
+                       @JsonProperty("type") String type,
+                       @JsonProperty("referenceId") String referenceId,
+                       @JsonProperty("effectiveDate") DateTime effectiveDate,
+                       @JsonProperty("createdDate") DateTime createdDate,
+                       @JsonProperty("updatedDate") DateTime updatedDate) {
+        this.id = id;
+        this.amount = amount;
+        this.appliedCreditBalanceAmount = appliedCreditBalanceAmount;
+        this.bankIdentificationNumber = bankIdentificationNumber;
+        this.createdDate = createdDate;
+        this.effectiveDate = effectiveDate;
+        this.paymentNumber = paymentNumber;
+        this.referenceId = referenceId;
+        this.refundAmount = refundAmount;
+        this.status = status;
+        this.type = type;
+        this.updatedDate = updatedDate;
+    }
+
+    public Builder cloner() {
+        return new Builder(this);
+    }
+
+    public String getId() {
+        return id;
+    }
+
+    public BigDecimal getAmount() {
+        return amount;
+    }
+
+    public BigDecimal getAppliedCreditBalanceAmount() {
+        return appliedCreditBalanceAmount;
+    }
+
+    public String getBankIdentificationNumber() {
+        return bankIdentificationNumber;
+    }
+
+    public DateTime getCreatedDate() {
+        return createdDate;
+    }
+
+    public DateTime getEffectiveDate() {
+        return effectiveDate;
+    }
+
+    public String getPaymentNumber() {
+        return paymentNumber;
+    }
+
+    public String getReferenceId() {
+        return referenceId;
+    }
+
+    public BigDecimal getRefundAmount() {
+        return refundAmount;
+    }
+
+    public String getStatus() {
+        return status;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public DateTime getUpdatedDate() {
+        return updatedDate;
+    }
+
     public static class Builder {
         private String id;
         private BigDecimal amount;
@@ -134,113 +241,6 @@ public class PaymentInfo implements EventBusNotification {
         }
     }
 
-    private final String id;
-    private final BigDecimal amount;
-    private final BigDecimal refundAmount;
-    private final BigDecimal appliedCreditBalanceAmount;
-    private final String paymentNumber;
-    private final String bankIdentificationNumber;
-    private final String status;
-    private final String type;
-    private final String referenceId;
-    private final DateTime effectiveDate;
-    private final DateTime createdDate;
-    private final DateTime updatedDate;
-
-    public PaymentInfo(PaymentInfo src) {
-        this.id = src.id;
-        this.amount = src.amount;
-        this.refundAmount = src.refundAmount;
-        this.appliedCreditBalanceAmount = src.appliedCreditBalanceAmount;
-        this.paymentNumber = src.paymentNumber;
-        this.bankIdentificationNumber = src.bankIdentificationNumber;
-        this.status = src.status;
-        this.type = src.type;
-        this.referenceId = src.referenceId;
-        this.effectiveDate = src.effectiveDate;
-        this.createdDate = src.createdDate;
-        this.updatedDate = src.updatedDate;
-    }
-
-    @JsonCreator
-    public PaymentInfo(@JsonProperty("id") String id,
-                       @JsonProperty("amount") BigDecimal amount,
-                       @JsonProperty("appliedCreditBalanceAmount") BigDecimal appliedCreditBalanceAmount,
-                       @JsonProperty("bankIdentificationNumber") String bankIdentificationNumber,
-                       @JsonProperty("paymentNumber") String paymentNumber,
-                       @JsonProperty("refundAmount") BigDecimal refundAmount,
-                       @JsonProperty("status") String status,
-                       @JsonProperty("type") String type,
-                       @JsonProperty("referenceId") String referenceId,
-                       @JsonProperty("effectiveDate") DateTime effectiveDate,
-                       @JsonProperty("createdDate") DateTime createdDate,
-                       @JsonProperty("updatedDate") DateTime updatedDate) {
-        this.id = id;
-        this.amount = amount;
-        this.appliedCreditBalanceAmount = appliedCreditBalanceAmount;
-        this.bankIdentificationNumber = bankIdentificationNumber;
-        this.createdDate = createdDate;
-        this.effectiveDate = effectiveDate;
-        this.paymentNumber = paymentNumber;
-        this.referenceId = referenceId;
-        this.refundAmount = refundAmount;
-        this.status = status;
-        this.type = type;
-        this.updatedDate = updatedDate;
-    }
-
-    public Builder cloner() {
-        return new Builder(this);
-    }
-
-    public String getId() {
-        return id;
-    }
-
-    public BigDecimal getAmount() {
-        return amount;
-    }
-
-    public BigDecimal getAppliedCreditBalanceAmount() {
-        return appliedCreditBalanceAmount;
-    }
-
-    public String getBankIdentificationNumber() {
-        return bankIdentificationNumber;
-    }
-
-    public DateTime getCreatedDate() {
-        return createdDate;
-    }
-
-    public DateTime getEffectiveDate() {
-        return effectiveDate;
-    }
-
-    public String getPaymentNumber() {
-        return paymentNumber;
-    }
-
-    public String getReferenceId() {
-        return referenceId;
-    }
-
-    public BigDecimal getRefundAmount() {
-        return refundAmount;
-    }
-
-    public String getStatus() {
-        return status;
-    }
-
-    public String getType() {
-        return type;
-    }
-
-    public DateTime getUpdatedDate() {
-        return updatedDate;
-    }
-
     @Override
     public int hashCode() {
         return Objects.hashCode(amount,

beatrix/pom.xml 2(+1 -1)

diff --git a/beatrix/pom.xml b/beatrix/pom.xml
index 1d16996..ec79685 100644
--- a/beatrix/pom.xml
+++ b/beatrix/pom.xml
@@ -13,7 +13,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill</artifactId>
-        <version>0.0.20-SNAPSHOT</version>
+        <version>0.0.21-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-beatrix</artifactId>

catalog/pom.xml 2(+1 -1)

diff --git a/catalog/pom.xml b/catalog/pom.xml
index b8dfecc..8d5c9c1 100644
--- a/catalog/pom.xml
+++ b/catalog/pom.xml
@@ -13,7 +13,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill</artifactId>
-        <version>0.0.20-SNAPSHOT</version>
+        <version>0.0.21-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-catalog</artifactId>
diff --git a/catalog/src/main/java/com/ning/billing/catalog/rules/Case.java b/catalog/src/main/java/com/ning/billing/catalog/rules/Case.java
index 737373e..00bc275 100644
--- a/catalog/src/main/java/com/ning/billing/catalog/rules/Case.java
+++ b/catalog/src/main/java/com/ning/billing/catalog/rules/Case.java
@@ -16,6 +16,7 @@
 
 package com.ning.billing.catalog.rules;
 
+
 import com.ning.billing.catalog.DefaultPriceList;
 import com.ning.billing.catalog.DefaultProduct;
 import com.ning.billing.catalog.StandaloneCatalog;
@@ -28,12 +29,15 @@ import com.ning.billing.util.config.ValidationErrors;
 
 public abstract class Case<T> extends ValidatingConfig<StandaloneCatalog> {
 
-	protected DefaultProduct product;
-	protected ProductCategory productCategory;
-	protected BillingPeriod billingPeriod;
-	protected DefaultPriceList priceList;
-
 	protected abstract T getResult();
+	
+	public abstract DefaultProduct getProduct();
+
+	public abstract ProductCategory getProductCategory();
+
+	public abstract BillingPeriod getBillingPeriod();
+	    
+	public abstract DefaultPriceList getPriceList();
 
 	public T getResult(PlanSpecifier planPhase, StandaloneCatalog c) throws CatalogApiException {
 		if (satisfiesCase(planPhase, c)	) {
@@ -43,10 +47,10 @@ public abstract class Case<T> extends ValidatingConfig<StandaloneCatalog> {
 	}
 	
 	protected boolean satisfiesCase(PlanSpecifier planPhase, StandaloneCatalog c) throws CatalogApiException {
-		return (product         == null || product.equals(c.findProduct(planPhase.getProductName()))) &&
-		(productCategory == null || productCategory.equals(planPhase.getProductCategory())) &&
-		(billingPeriod   == null || billingPeriod.equals(planPhase.getBillingPeriod())) &&
-		(priceList       == null || priceList.equals(c.getPriceListFromName(planPhase.getPriceListName())));
+		return (getProduct()         == null || getProduct().equals(c.findProduct(planPhase.getProductName()))) &&
+		(getProductCategory() == null || getProductCategory().equals(planPhase.getProductCategory())) &&
+		(getBillingPeriod()   == null || getBillingPeriod().equals(planPhase.getBillingPeriod())) &&
+		(getPriceList()       == null || getPriceList().equals(c.getPriceListFromName(planPhase.getPriceListName())));
 	}
 
 	public static <K> K getResult(Case<K>[] cases, PlanSpecifier planSpec, StandaloneCatalog catalog) throws CatalogApiException {
@@ -67,26 +71,11 @@ public abstract class Case<T> extends ValidatingConfig<StandaloneCatalog> {
 		return errors;
 	}
 
-	protected Case<T> setProduct(DefaultProduct product) {
-		this.product = product;
-		return this;
-	}
-
-	protected Case<T> setProductCategory(ProductCategory productCategory) {
-		this.productCategory = productCategory;
-		return this;
-	}
+	protected abstract Case<T> setProduct(DefaultProduct product);
 
-	protected Case<T> setBillingPeriod(BillingPeriod billingPeriod) {
-		this.billingPeriod = billingPeriod;
-		return this;
-	}
+	protected abstract  Case<T> setProductCategory(ProductCategory productCategory);
 
-	protected Case<T> setPriceList(DefaultPriceList priceList) {
-		this.priceList = priceList;
-		return this;
-	}
+	protected abstract  Case<T> setBillingPeriod(BillingPeriod billingPeriod);
 
-	
-	
+	protected abstract  Case<T> setPriceList(DefaultPriceList priceList);
 }
diff --git a/catalog/src/main/java/com/ning/billing/catalog/rules/CasePhase.java b/catalog/src/main/java/com/ning/billing/catalog/rules/CasePhase.java
index 1a3b0b5..b38c1f8 100644
--- a/catalog/src/main/java/com/ning/billing/catalog/rules/CasePhase.java
+++ b/catalog/src/main/java/com/ning/billing/catalog/rules/CasePhase.java
@@ -31,8 +31,8 @@ public abstract class CasePhase<T> extends CaseStandardNaming<T> {
 	private PhaseType phaseType;	
 	
 	public T getResult(PlanPhaseSpecifier specifier, StandaloneCatalog c) throws CatalogApiException {
-		if (	
-				(phaseType       == null || specifier.getPhaseType() == null || specifier.getPhaseType() == phaseType) &&
+		if ((phaseType       == null || 
+				specifier.getPhaseType() == null || specifier.getPhaseType() == phaseType) &&
 				satisfiesCase(new PlanSpecifier(specifier), c)
 				) {
 			return getResult(); 
diff --git a/catalog/src/main/java/com/ning/billing/catalog/rules/CasePriceList.java b/catalog/src/main/java/com/ning/billing/catalog/rules/CasePriceList.java
index fbb9cab..5063131 100644
--- a/catalog/src/main/java/com/ning/billing/catalog/rules/CasePriceList.java
+++ b/catalog/src/main/java/com/ning/billing/catalog/rules/CasePriceList.java
@@ -25,37 +25,66 @@ import javax.xml.bind.annotation.XmlElement;
 import javax.xml.bind.annotation.XmlIDREF;
 
 public class CasePriceList extends Case<DefaultPriceList> {
+    @XmlElement(required=false, name="fromProduct")
+    @XmlIDREF
+    private DefaultProduct fromProduct;
+    
+    @XmlElement(required=false, name="fromProductCategory")
+    private ProductCategory fromProductCategory;
+    
+    @XmlElement(required=false, name="fromBillingPeriod")
+    private BillingPeriod fromBillingPeriod;
+    
+    @XmlElement(required=false, name="fromPriceList")
+    @XmlIDREF
+    private DefaultPriceList fromPriceList;
 
-	private DefaultPriceList toPriceList;
+    @XmlElement(required=true, name="toPriceList")
+    @XmlIDREF
+    private DefaultPriceList toPriceList;
 
-	@XmlElement(required=false, name="fromProduct")
-	@XmlIDREF
-	public DefaultProduct getProduct(){
-		return product;
-	}
+    public DefaultProduct getProduct(){
+        return fromProduct;
+    }
 
-	@XmlElement(required=false, name="fromProductCategory")
-	public ProductCategory getProductCategory() {
-		return productCategory;
-	}
+    public ProductCategory getProductCategory() {
+        return fromProductCategory;
+    }
 
-	@XmlElement(required=false, name="fromBillingPeriod")
-	public BillingPeriod getBillingPeriod() {
-		return billingPeriod;
-	}
-	
-	@XmlElement(required=false, name="fromPriceList")
-	@XmlIDREF
-	public DefaultPriceList getPriceList() {
-		return priceList;
-	}
+    public BillingPeriod getBillingPeriod() {
+        return fromBillingPeriod;
+    }
+    
+    public DefaultPriceList getPriceList() {
+        return fromPriceList;
+    }
+
+    protected DefaultPriceList getResult() {
+        return toPriceList;
+    }
+
+    protected CasePriceList setProduct(DefaultProduct product) {
+        this.fromProduct = product;
+        return this;
+    }
+
+    protected CasePriceList setProductCategory(ProductCategory productCategory) {
+        this.fromProductCategory = productCategory;
+        return this;
+    }
+
+    protected CasePriceList setBillingPeriod(BillingPeriod billingPeriod) {
+        this.fromBillingPeriod = billingPeriod;
+        return this;
+    }
+
+    protected CasePriceList setPriceList(DefaultPriceList priceList) {
+        this.fromPriceList = priceList;
+        return this;
+    }
+    
+    
 
-	@Override
-	@XmlElement(required=true, name="toPriceList")
-	@XmlIDREF
-	protected DefaultPriceList getResult() {
-		return toPriceList;
-	}
 
 	protected CasePriceList setToPriceList(DefaultPriceList toPriceList) {
 		this.toPriceList = toPriceList;
diff --git a/catalog/src/main/java/com/ning/billing/catalog/rules/CaseStandardNaming.java b/catalog/src/main/java/com/ning/billing/catalog/rules/CaseStandardNaming.java
index cf61fcd..8e161ce 100644
--- a/catalog/src/main/java/com/ning/billing/catalog/rules/CaseStandardNaming.java
+++ b/catalog/src/main/java/com/ning/billing/catalog/rules/CaseStandardNaming.java
@@ -25,27 +25,53 @@ import javax.xml.bind.annotation.XmlElement;
 import javax.xml.bind.annotation.XmlIDREF;
 
 public abstract class CaseStandardNaming<T> extends Case<T> {
+    @XmlElement(required=false, name="product")
+    @XmlIDREF
+    private DefaultProduct product;
+    @XmlElement(required=false, name="productCategory")
+    private ProductCategory productCategory;
+    
+    @XmlElement(required=false, name="billingPeriod")
+    private BillingPeriod billingPeriod;
+    
+    @XmlElement(required=false, name="priceList")
+    @XmlIDREF
+    private DefaultPriceList priceList;
 
-	@XmlElement(required=false, name="product")
-	@XmlIDREF
 	public DefaultProduct getProduct(){
 		return product;
 	}
 
-	@XmlElement(required=false, name="productCategory")
 	public ProductCategory getProductCategory() {
 		return productCategory;
 	}
 
-	@XmlElement(required=false, name="billingPeriod")
 	public BillingPeriod getBillingPeriod() {
 		return billingPeriod;
 	}
 	
-	@XmlElement(required=false, name="priceList")
-	@XmlIDREF
 	public DefaultPriceList getPriceList() {
 		return priceList;
 	}
 
+    protected CaseStandardNaming<T> setProduct(DefaultProduct product) {
+        this.product = product;
+        return this;
+    }
+
+    protected CaseStandardNaming<T> setProductCategory(ProductCategory productCategory) {
+        this.productCategory = productCategory;
+        return this;
+    }
+
+    protected CaseStandardNaming<T> setBillingPeriod(BillingPeriod billingPeriod) {
+        this.billingPeriod = billingPeriod;
+        return this;
+    }
+
+    protected CaseStandardNaming<T> setPriceList(DefaultPriceList priceList) {
+        this.priceList = priceList;
+        return this;
+    }
+
 }
diff --git a/catalog/src/test/java/com/ning/billing/catalog/rules/TestCase.java b/catalog/src/test/java/com/ning/billing/catalog/rules/TestCase.java
index c00f1c4..0c2f3b0 100644
--- a/catalog/src/test/java/com/ning/billing/catalog/rules/TestCase.java
+++ b/catalog/src/test/java/com/ning/billing/catalog/rules/TestCase.java
@@ -24,201 +24,251 @@ import com.ning.billing.catalog.api.*;
 import org.testng.annotations.Test;
 
 import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlIDREF;
 
 import static org.testng.AssertJUnit.assertEquals;
 import static org.testng.AssertJUnit.assertNull;
 
 public class TestCase {
 
-	protected class CaseResult extends Case<Result>  {
-
-		@XmlElement(required=true)
-		private Result policy;
-
-		public CaseResult(DefaultProduct product, ProductCategory productCategory, BillingPeriod billingPeriod, DefaultPriceList priceList,
-				 Result policy) {
-			setProduct(product);
-			setProductCategory(productCategory);
-			setBillingPeriod(billingPeriod);
-			setPriceList(priceList);
-			this.policy = policy;
-		}
-
-		@Override
-		protected Result getResult() {
-			return policy;
-		}
-	}
-
-	@Test(enabled=true)
-	public void testBasic() throws CatalogApiException{
-		MockCatalog cat = new MockCatalog();
-
-		DefaultProduct product = cat.getProducts()[0];
-		DefaultPriceList priceList = cat.getPriceListFromName(PriceListSet.DEFAULT_PRICELIST_NAME);
-
-
-		CaseResult cr = new CaseResult(
-				product, 
-				ProductCategory.BASE,
-				BillingPeriod.MONTHLY, 
-				priceList,
-				Result.FOO);
-
-		assertion(Result.FOO, cr, product.getName(), ProductCategory.BASE, BillingPeriod.MONTHLY, priceList.getName(), cat);
-		assertionNull(cr, cat.getProducts()[1].getName(), ProductCategory.BASE, BillingPeriod.MONTHLY, priceList.getName(), cat);
-		assertionNull(cr, product.getName(), ProductCategory.ADD_ON,BillingPeriod.MONTHLY, priceList.getName(), cat);
-		assertionNull(cr, product.getName(), ProductCategory.BASE,BillingPeriod.ANNUAL, priceList.getName(), cat);
-		assertionNull(cr, product.getName(), ProductCategory.BASE,BillingPeriod.MONTHLY, "dipsy", cat);
-	}
-
-	@Test(enabled=true)
-	public void testWildCardProduct() throws CatalogApiException{
-		MockCatalog cat = new MockCatalog();
-
-		DefaultProduct product = cat.getProducts()[0];
-		DefaultPriceList priceList = cat.getPriceListFromName(PriceListSet.DEFAULT_PRICELIST_NAME);
-
-
-		CaseResult cr = new CaseResult(
-				null, 
-				ProductCategory.BASE,
-				BillingPeriod.MONTHLY, 
-				priceList,
-
-				Result.FOO);
-
-		assertion(Result.FOO, cr, product.getName(), ProductCategory.BASE,BillingPeriod.MONTHLY, priceList.getName(), cat);
-		assertion(Result.FOO, cr, cat.getProducts()[1].getName(), ProductCategory.BASE,BillingPeriod.MONTHLY, priceList.getName(), cat);
-		assertionNull(cr, product.getName(), ProductCategory.ADD_ON,BillingPeriod.MONTHLY, priceList.getName(), cat);
-		assertionNull(cr, product.getName(), ProductCategory.BASE,BillingPeriod.ANNUAL, priceList.getName(), cat);
-		assertionNull(cr, product.getName(), ProductCategory.BASE,BillingPeriod.MONTHLY, "dipsy", cat);
-	}
-	
-	@Test(enabled=true)
-	public void testWildCardProductCategory() throws CatalogApiException{
-		MockCatalog cat = new MockCatalog();
-
-		DefaultProduct product = cat.getProducts()[0];
-		DefaultPriceList priceList = cat.getPriceListFromName(PriceListSet.DEFAULT_PRICELIST_NAME);
-
-
-		CaseResult cr = new CaseResult(
-				product, 
-				null,
-				BillingPeriod.MONTHLY, 
-				priceList,
-
-				Result.FOO);
-
-		assertion(Result.FOO, cr, product.getName(), ProductCategory.BASE, BillingPeriod.MONTHLY, priceList.getName(), cat);
-		assertionNull(cr,  cat.getProducts()[1].getName(), ProductCategory.BASE, BillingPeriod.MONTHLY, priceList.getName(), cat);
-		assertion(Result.FOO, cr, product.getName(), ProductCategory.ADD_ON,BillingPeriod.MONTHLY, priceList.getName(), cat);
-		assertionNull(cr, product.getName(), ProductCategory.BASE,BillingPeriod.ANNUAL, priceList.getName(), cat);
-		assertionNull(cr, product.getName(), ProductCategory.BASE,BillingPeriod.MONTHLY, "dipsy", cat);
-	}
-	
-	@Test(enabled=true)
-	public void testWildCardBillingPeriod() throws CatalogApiException{
-		MockCatalog cat = new MockCatalog();
-
-		DefaultProduct product = cat.getProducts()[0];
-		DefaultPriceList priceList = cat.getPriceListFromName(PriceListSet.DEFAULT_PRICELIST_NAME);
-
-
-		CaseResult cr = new CaseResult(
-				product, 
-				ProductCategory.BASE,
-				null, 
-				priceList,
-
-				Result.FOO);
-
-		assertion(Result.FOO, cr, product.getName(), ProductCategory.BASE, BillingPeriod.MONTHLY, priceList.getName(), cat);
-		assertionNull(cr,  cat.getProducts()[1].getName(), ProductCategory.BASE,BillingPeriod.MONTHLY, priceList.getName(), cat);
-		assertionNull(cr, product.getName(), ProductCategory.ADD_ON,BillingPeriod.MONTHLY, priceList.getName(), cat);
-		assertion(Result.FOO,cr, product.getName(), ProductCategory.BASE,BillingPeriod.ANNUAL, priceList.getName(), cat);
-		assertionNull(cr, product.getName(), ProductCategory.BASE,BillingPeriod.MONTHLY, "dipsy", cat);
-	}
-
-	@Test(enabled=true)
-	public void testWildCardPriceList() throws CatalogApiException{
-		MockCatalog cat = new MockCatalog();
-
-		DefaultProduct product = cat.getProducts()[0];
-		DefaultPriceList priceList = cat.getPriceListFromName(PriceListSet.DEFAULT_PRICELIST_NAME);
-
-
-		CaseResult cr = new CaseResult(
-				product, 
-				ProductCategory.BASE,
-				BillingPeriod.MONTHLY, 
-				null,
-
-				Result.FOO);
-
-		assertion(Result.FOO, cr, product.getName(), ProductCategory.BASE,BillingPeriod.MONTHLY, priceList.getName(), cat);
-		assertionNull(cr,  cat.getProducts()[1].getName(), ProductCategory.BASE,BillingPeriod.MONTHLY, priceList.getName(), cat);
-		assertionNull(cr, product.getName(), ProductCategory.ADD_ON,BillingPeriod.MONTHLY, priceList.getName(), cat);
-		assertionNull(cr, product.getName(), ProductCategory.BASE,BillingPeriod.ANNUAL, priceList.getName(), cat);
-		assertion(Result.FOO, cr, product.getName(), ProductCategory.BASE,BillingPeriod.MONTHLY, "dipsy", cat);
-	}
-	
-	@Test
-	public void testCaseOrder() throws CatalogApiException {
-		MockCatalog cat = new MockCatalog();
-
-		DefaultProduct product = cat.getProducts()[0];
-		DefaultPriceList priceList = cat.getPriceListFromName(PriceListSet.DEFAULT_PRICELIST_NAME);
-
-
-		CaseResult cr0 = new CaseResult(
-				product, 
-				ProductCategory.BASE,
-				BillingPeriod.MONTHLY, 
-				priceList,
-				Result.FOO);
-
-		CaseResult cr1 = new CaseResult(
-				product, 
-				ProductCategory.BASE,
-				BillingPeriod.MONTHLY, 
-				priceList,
-				Result.BAR);
-
-		CaseResult cr2 = new CaseResult(
-				product, 
-				ProductCategory.BASE,
-				BillingPeriod.ANNUAL, 
-				priceList,
-				Result.DIPSY);
-
-		CaseResult cr3 = new CaseResult(
-				product, 
-				ProductCategory.BASE,
-				BillingPeriod.ANNUAL, 
-				priceList,
-				Result.LALA);
-
-		Result r1 = Case.getResult(new CaseResult[]{cr0, cr1, cr2,cr3}, 
-				new PlanSpecifier(product.getName(), product.getCategory(), BillingPeriod.MONTHLY, priceList.getName()), cat);
-		assertEquals(Result.FOO, r1);
-		
-		Result r2 = Case.getResult(new CaseResult[]{cr0, cr1, cr2}, 
-				new PlanSpecifier(product.getName(), product.getCategory(), BillingPeriod.ANNUAL, priceList.getName()), cat);
-		assertEquals(Result.DIPSY, r2);
-	}
-
-	
-
-
-	protected void assertionNull(CaseResult cr, String productName, ProductCategory productCategory, BillingPeriod bp, String priceListName, StandaloneCatalog cat) throws CatalogApiException{
-		assertNull(cr.getResult(new PlanSpecifier(productName, productCategory, bp, priceListName), cat));
-	}
-
-	protected void assertion(Result result, CaseResult cr, String productName, ProductCategory productCategory, BillingPeriod bp, String priceListName,StandaloneCatalog cat) throws CatalogApiException{
-		assertEquals(result, cr.getResult(new PlanSpecifier(productName, productCategory, bp, priceListName), cat));
-	}
+    protected class CaseResult extends Case<Result>  {
+
+        @XmlElement(required=true)
+        private Result policy;
+
+        public CaseResult(DefaultProduct product, ProductCategory productCategory, BillingPeriod billingPeriod, DefaultPriceList priceList,
+                Result policy) {
+            setProduct(product);
+            setProductCategory(productCategory);
+            setBillingPeriod(billingPeriod);
+            setPriceList(priceList);
+            this.policy = policy;
+        }
+
+        @Override
+        protected Result getResult() {
+            return policy;
+        }
+
+        @XmlElement(required=false, name="product")
+        @XmlIDREF
+        protected DefaultProduct product;
+        @XmlElement(required=false, name="productCategory")
+        protected ProductCategory productCategory;
+
+        @XmlElement(required=false, name="billingPeriod")
+        protected BillingPeriod billingPeriod;
+
+        @XmlElement(required=false, name="priceList")
+        @XmlIDREF
+        protected DefaultPriceList priceList;
+
+        public DefaultProduct getProduct(){
+            return product;
+        }
+
+        public ProductCategory getProductCategory() {
+            return productCategory;
+        }
+
+        public BillingPeriod getBillingPeriod() {
+            return billingPeriod;
+        }
+
+        public DefaultPriceList getPriceList() {
+            return priceList;
+        }
+
+        protected CaseResult setProduct(DefaultProduct product) {
+            this.product = product;
+            return this;
+        }
+
+        protected CaseResult setProductCategory(ProductCategory productCategory) {
+            this.productCategory = productCategory;
+            return this;
+        }
+
+        protected CaseResult setBillingPeriod(BillingPeriod billingPeriod) {
+            this.billingPeriod = billingPeriod;
+            return this;
+        }
+
+        protected CaseResult setPriceList(DefaultPriceList priceList) {
+            this.priceList = priceList;
+            return this;
+        }
+    }
+
+    @Test(enabled=true)
+    public void testBasic() throws CatalogApiException{
+        MockCatalog cat = new MockCatalog();
+
+        DefaultProduct product = cat.getProducts()[0];
+        DefaultPriceList priceList = cat.getPriceListFromName(PriceListSet.DEFAULT_PRICELIST_NAME);
+
+
+        CaseResult cr = new CaseResult(
+                product, 
+                ProductCategory.BASE,
+                BillingPeriod.MONTHLY, 
+                priceList,
+                Result.FOO);
+
+        assertion(Result.FOO, cr, product.getName(), ProductCategory.BASE, BillingPeriod.MONTHLY, priceList.getName(), cat);
+        assertionNull(cr, cat.getProducts()[1].getName(), ProductCategory.BASE, BillingPeriod.MONTHLY, priceList.getName(), cat);
+        assertionNull(cr, product.getName(), ProductCategory.ADD_ON,BillingPeriod.MONTHLY, priceList.getName(), cat);
+        assertionNull(cr, product.getName(), ProductCategory.BASE,BillingPeriod.ANNUAL, priceList.getName(), cat);
+        assertionNull(cr, product.getName(), ProductCategory.BASE,BillingPeriod.MONTHLY, "dipsy", cat);
+    }
+
+    @Test(enabled=true)
+    public void testWildCardProduct() throws CatalogApiException{
+        MockCatalog cat = new MockCatalog();
+
+        DefaultProduct product = cat.getProducts()[0];
+        DefaultPriceList priceList = cat.getPriceListFromName(PriceListSet.DEFAULT_PRICELIST_NAME);
+
+
+        CaseResult cr = new CaseResult(
+                null, 
+                ProductCategory.BASE,
+                BillingPeriod.MONTHLY, 
+                priceList,
+
+                Result.FOO);
+
+        assertion(Result.FOO, cr, product.getName(), ProductCategory.BASE,BillingPeriod.MONTHLY, priceList.getName(), cat);
+        assertion(Result.FOO, cr, cat.getProducts()[1].getName(), ProductCategory.BASE,BillingPeriod.MONTHLY, priceList.getName(), cat);
+        assertionNull(cr, product.getName(), ProductCategory.ADD_ON,BillingPeriod.MONTHLY, priceList.getName(), cat);
+        assertionNull(cr, product.getName(), ProductCategory.BASE,BillingPeriod.ANNUAL, priceList.getName(), cat);
+        assertionNull(cr, product.getName(), ProductCategory.BASE,BillingPeriod.MONTHLY, "dipsy", cat);
+    }
+
+    @Test(enabled=true)
+    public void testWildCardProductCategory() throws CatalogApiException{
+        MockCatalog cat = new MockCatalog();
+
+        DefaultProduct product = cat.getProducts()[0];
+        DefaultPriceList priceList = cat.getPriceListFromName(PriceListSet.DEFAULT_PRICELIST_NAME);
+
+
+        CaseResult cr = new CaseResult(
+                product, 
+                null,
+                BillingPeriod.MONTHLY, 
+                priceList,
+
+                Result.FOO);
+
+        assertion(Result.FOO, cr, product.getName(), ProductCategory.BASE, BillingPeriod.MONTHLY, priceList.getName(), cat);
+        assertionNull(cr,  cat.getProducts()[1].getName(), ProductCategory.BASE, BillingPeriod.MONTHLY, priceList.getName(), cat);
+        assertion(Result.FOO, cr, product.getName(), ProductCategory.ADD_ON,BillingPeriod.MONTHLY, priceList.getName(), cat);
+        assertionNull(cr, product.getName(), ProductCategory.BASE,BillingPeriod.ANNUAL, priceList.getName(), cat);
+        assertionNull(cr, product.getName(), ProductCategory.BASE,BillingPeriod.MONTHLY, "dipsy", cat);
+    }
+
+    @Test(enabled=true)
+    public void testWildCardBillingPeriod() throws CatalogApiException{
+        MockCatalog cat = new MockCatalog();
+
+        DefaultProduct product = cat.getProducts()[0];
+        DefaultPriceList priceList = cat.getPriceListFromName(PriceListSet.DEFAULT_PRICELIST_NAME);
+
+
+        CaseResult cr = new CaseResult(
+                product, 
+                ProductCategory.BASE,
+                null, 
+                priceList,
+
+                Result.FOO);
+
+        assertion(Result.FOO, cr, product.getName(), ProductCategory.BASE, BillingPeriod.MONTHLY, priceList.getName(), cat);
+        assertionNull(cr,  cat.getProducts()[1].getName(), ProductCategory.BASE,BillingPeriod.MONTHLY, priceList.getName(), cat);
+        assertionNull(cr, product.getName(), ProductCategory.ADD_ON,BillingPeriod.MONTHLY, priceList.getName(), cat);
+        assertion(Result.FOO,cr, product.getName(), ProductCategory.BASE,BillingPeriod.ANNUAL, priceList.getName(), cat);
+        assertionNull(cr, product.getName(), ProductCategory.BASE,BillingPeriod.MONTHLY, "dipsy", cat);
+    }
+
+    @Test(enabled=true)
+    public void testWildCardPriceList() throws CatalogApiException{
+        MockCatalog cat = new MockCatalog();
+
+        DefaultProduct product = cat.getProducts()[0];
+        DefaultPriceList priceList = cat.getPriceListFromName(PriceListSet.DEFAULT_PRICELIST_NAME);
+
+
+        CaseResult cr = new CaseResult(
+                product, 
+                ProductCategory.BASE,
+                BillingPeriod.MONTHLY, 
+                null,
+
+                Result.FOO);
+
+        assertion(Result.FOO, cr, product.getName(), ProductCategory.BASE,BillingPeriod.MONTHLY, priceList.getName(), cat);
+        assertionNull(cr,  cat.getProducts()[1].getName(), ProductCategory.BASE,BillingPeriod.MONTHLY, priceList.getName(), cat);
+        assertionNull(cr, product.getName(), ProductCategory.ADD_ON,BillingPeriod.MONTHLY, priceList.getName(), cat);
+        assertionNull(cr, product.getName(), ProductCategory.BASE,BillingPeriod.ANNUAL, priceList.getName(), cat);
+        assertion(Result.FOO, cr, product.getName(), ProductCategory.BASE,BillingPeriod.MONTHLY, "dipsy", cat);
+    }
+
+    @Test
+    public void testCaseOrder() throws CatalogApiException {
+        MockCatalog cat = new MockCatalog();
+
+        DefaultProduct product = cat.getProducts()[0];
+        DefaultPriceList priceList = cat.getPriceListFromName(PriceListSet.DEFAULT_PRICELIST_NAME);
+
+
+        CaseResult cr0 = new CaseResult(
+                product, 
+                ProductCategory.BASE,
+                BillingPeriod.MONTHLY, 
+                priceList,
+                Result.FOO);
+
+        CaseResult cr1 = new CaseResult(
+                product, 
+                ProductCategory.BASE,
+                BillingPeriod.MONTHLY, 
+                priceList,
+                Result.BAR);
+
+        CaseResult cr2 = new CaseResult(
+                product, 
+                ProductCategory.BASE,
+                BillingPeriod.ANNUAL, 
+                priceList,
+                Result.DIPSY);
+
+        CaseResult cr3 = new CaseResult(
+                product, 
+                ProductCategory.BASE,
+                BillingPeriod.ANNUAL, 
+                priceList,
+                Result.LALA);
+
+        Result r1 = Case.getResult(new CaseResult[]{cr0, cr1, cr2,cr3}, 
+                new PlanSpecifier(product.getName(), product.getCategory(), BillingPeriod.MONTHLY, priceList.getName()), cat);
+        assertEquals(Result.FOO, r1);
+
+        Result r2 = Case.getResult(new CaseResult[]{cr0, cr1, cr2}, 
+                new PlanSpecifier(product.getName(), product.getCategory(), BillingPeriod.ANNUAL, priceList.getName()), cat);
+        assertEquals(Result.DIPSY, r2);
+    }
+
+
+
+
+    protected void assertionNull(CaseResult cr, String productName, ProductCategory productCategory, BillingPeriod bp, String priceListName, StandaloneCatalog cat) throws CatalogApiException{
+        assertNull(cr.getResult(new PlanSpecifier(productName, productCategory, bp, priceListName), cat));
+    }
+
+    protected void assertion(Result result, CaseResult cr, String productName, ProductCategory productCategory, BillingPeriod bp, String priceListName,StandaloneCatalog cat) throws CatalogApiException{
+        assertEquals(result, cr.getResult(new PlanSpecifier(productName, productCategory, bp, priceListName), cat));
+    }
 
 
 }
diff --git a/catalog/src/test/java/com/ning/billing/catalog/rules/TestLoadRules.java b/catalog/src/test/java/com/ning/billing/catalog/rules/TestLoadRules.java
new file mode 100644
index 0000000..6f1d9e2
--- /dev/null
+++ b/catalog/src/test/java/com/ning/billing/catalog/rules/TestLoadRules.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.catalog.rules;
+
+import java.io.File;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.catalog.StandaloneCatalog;
+import com.ning.billing.catalog.api.BillingPeriod;
+import com.ning.billing.catalog.api.Plan;
+import com.ning.billing.catalog.api.PlanAlignmentCreate;
+import com.ning.billing.catalog.api.PlanSpecifier;
+import com.ning.billing.catalog.api.ProductCategory;
+import com.ning.billing.util.config.XMLLoader;
+
+public class TestLoadRules {
+
+    @Test
+    public void test() throws Exception {
+        StandaloneCatalog catalog = XMLLoader.getObjectFromUri(new File("src/test/resources/WeaponsHireSmall.xml").toURI(), StandaloneCatalog.class);
+        Assert.assertNotNull(catalog);
+        PlanRules rules = catalog.getPlanRules();
+        
+        PlanSpecifier specifier = new PlanSpecifier("Laser-Scope", ProductCategory.ADD_ON , BillingPeriod.MONTHLY,
+                "DEFAULT");
+        
+        PlanAlignmentCreate alignment=  rules.getPlanCreateAlignment(specifier, catalog);
+        Assert.assertEquals(alignment, PlanAlignmentCreate.START_OF_SUBSCRIPTION);
+        
+        PlanSpecifier specifier2 = new PlanSpecifier("Extra-Ammo", ProductCategory.ADD_ON , BillingPeriod.MONTHLY,
+                "DEFAULT");
+        
+        PlanAlignmentCreate alignment2 = rules.getPlanCreateAlignment(specifier2, catalog);
+        Assert.assertEquals(alignment2, PlanAlignmentCreate.START_OF_BUNDLE);
+    }
+}
diff --git a/catalog/src/test/resources/versionedCatalog/WeaponsHireSmall-1.xml b/catalog/src/test/resources/versionedCatalog/WeaponsHireSmall-1.xml
index a29e929..fe66528 100644
--- a/catalog/src/test/resources/versionedCatalog/WeaponsHireSmall-1.xml
+++ b/catalog/src/test/resources/versionedCatalog/WeaponsHireSmall-1.xml
@@ -57,6 +57,15 @@
 				<alignment>START_OF_SUBSCRIPTION</alignment>
 			</changeAlignmentCase>
 		</changeAlignment>
+		 <createAlignment>
+            <createAlignmentCase>
+                <product>Laser-Scope</product>
+                <alignment>START_OF_SUBSCRIPTION</alignment>
+            </createAlignmentCase>
+            <createAlignmentCase>
+                <alignment>START_OF_BUNDLE</alignment>
+            </createAlignmentCase>
+        </createAlignment>
 	</rules>
 
 
diff --git a/catalog/src/test/resources/versionedCatalog/WeaponsHireSmall-2.xml b/catalog/src/test/resources/versionedCatalog/WeaponsHireSmall-2.xml
index 135a89e..66f2cca 100644
--- a/catalog/src/test/resources/versionedCatalog/WeaponsHireSmall-2.xml
+++ b/catalog/src/test/resources/versionedCatalog/WeaponsHireSmall-2.xml
@@ -57,6 +57,15 @@
 				<alignment>START_OF_SUBSCRIPTION</alignment>
 			</changeAlignmentCase>
 		</changeAlignment>
+		 <createAlignment>
+            <createAlignmentCase>
+                <product>Laser-Scope</product>
+                <alignment>START_OF_SUBSCRIPTION</alignment>
+            </createAlignmentCase>
+            <createAlignmentCase>
+                <alignment>START_OF_BUNDLE</alignment>
+            </createAlignmentCase>
+        </createAlignment>
 	</rules>
 
 	<plans>
diff --git a/catalog/src/test/resources/versionedCatalog/WeaponsHireSmall-3.xml b/catalog/src/test/resources/versionedCatalog/WeaponsHireSmall-3.xml
index 747f54b..88caefd 100644
--- a/catalog/src/test/resources/versionedCatalog/WeaponsHireSmall-3.xml
+++ b/catalog/src/test/resources/versionedCatalog/WeaponsHireSmall-3.xml
@@ -57,6 +57,15 @@
 				<alignment>START_OF_SUBSCRIPTION</alignment>
 			</changeAlignmentCase>
 		</changeAlignment>
+		 <createAlignment>
+            <createAlignmentCase>
+                <product>Laser-Scope</product>
+                <alignment>START_OF_SUBSCRIPTION</alignment>
+            </createAlignmentCase>
+            <createAlignmentCase>
+                <alignment>START_OF_BUNDLE</alignment>
+            </createAlignmentCase>
+        </createAlignment>
 	</rules>
 
 
diff --git a/catalog/src/test/resources/WeaponsHireSmall.xml b/catalog/src/test/resources/WeaponsHireSmall.xml
index 0a3ed52..21a9f08 100644
--- a/catalog/src/test/resources/WeaponsHireSmall.xml
+++ b/catalog/src/test/resources/WeaponsHireSmall.xml
@@ -37,6 +37,9 @@
 		<product name="Laser-Scope">
 			<category>ADD_ON</category>
 		</product>
+        <product name="Extra-Ammo">
+            <category>ADD_ON</category>
+        </product>
 	</products>
 	 
 	<rules>
@@ -52,11 +55,20 @@
 				<policy>IMMEDIATE</policy>
 			</changePolicyCase>	
 		</changePolicy>
-		<changeAlignment>
-			<changeAlignmentCase>
-				<alignment>START_OF_SUBSCRIPTION</alignment>
-			</changeAlignmentCase>
-		</changeAlignment>
+        <changeAlignment>
+            <changeAlignmentCase>
+                <alignment>START_OF_SUBSCRIPTION</alignment>
+            </changeAlignmentCase>
+        </changeAlignment>
+        <createAlignment>
+            <createAlignmentCase>
+                <product>Laser-Scope</product>
+                <alignment>START_OF_SUBSCRIPTION</alignment>
+            </createAlignmentCase>
+            <createAlignmentCase>
+                <alignment>START_OF_BUNDLE</alignment>
+            </createAlignmentCase>
+        </createAlignment>
 	</rules>
 
 	<plans>
@@ -136,6 +148,34 @@
 				</recurringPrice>
 			</finalPhase>
 		</plan>
+		<plan name="laser-scope-monthly">
+        <product>Laser-Scope</product>
+            <finalPhase type="EVERGREEN">
+                <duration>
+                    <unit>UNLIMITED</unit>
+                </duration>
+                <billingPeriod>MONTHLY</billingPeriod>
+                <recurringPrice>
+                    <price><currency>USD</currency><value>1999.95</value></price>                               
+                    <price><currency>EUR</currency><value>1499.95</value></price>
+                    <price><currency>GBP</currency><value>1999.95</value></price>
+                </recurringPrice>
+            </finalPhase>
+        </plan>
+        <plan name="extra-ammo-monthly">
+        <product>Extra-Ammo</product>
+            <finalPhase type="EVERGREEN">
+                <duration>
+                    <unit>UNLIMITED</unit>
+                </duration>
+                <billingPeriod>MONTHLY</billingPeriod>
+                <recurringPrice>
+                    <price><currency>USD</currency><value>1999.95</value></price>                               
+                    <price><currency>EUR</currency><value>1499.95</value></price>
+                    <price><currency>GBP</currency><value>1999.95</value></price>
+                </recurringPrice>
+            </finalPhase>
+        </plan>
 	</plans>
 	<priceLists>
 		<defaultPriceList name="DEFAULT">
diff --git a/entitlement/pom.xml b/entitlement/pom.xml
index e534884..5c5f921 100644
--- a/entitlement/pom.xml
+++ b/entitlement/pom.xml
@@ -13,7 +13,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill</artifactId>
-        <version>0.0.20-SNAPSHOT</version>
+        <version>0.0.21-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-entitlement</artifactId>
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/alignment/MigrationPlanAligner.java b/entitlement/src/main/java/com/ning/billing/entitlement/alignment/MigrationPlanAligner.java
new file mode 100644
index 0000000..4290f73
--- /dev/null
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/alignment/MigrationPlanAligner.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.entitlement.alignment;
+
+
+import org.joda.time.DateTime;
+
+import com.google.inject.Inject;
+import com.ning.billing.catalog.api.CatalogApiException;
+import com.ning.billing.catalog.api.CatalogService;
+import com.ning.billing.catalog.api.Duration;
+import com.ning.billing.catalog.api.PhaseType;
+import com.ning.billing.catalog.api.Plan;
+import com.ning.billing.catalog.api.PlanPhase;
+import com.ning.billing.catalog.api.PlanPhaseSpecifier;
+import com.ning.billing.entitlement.api.migration.EntitlementMigrationApiException;
+import com.ning.billing.entitlement.api.migration.EntitlementMigrationApi.EntitlementSubscriptionMigrationCase;
+import com.ning.billing.entitlement.events.EntitlementEvent.EventType;
+import com.ning.billing.entitlement.events.user.ApiEventType;
+import com.ning.billing.util.clock.DefaultClock;
+
+public class MigrationPlanAligner {
+
+    private final CatalogService catalogService;
+
+    @Inject
+    public MigrationPlanAligner(CatalogService catalogService) {
+        this.catalogService = catalogService;
+    }
+
+
+    public TimedMigration [] getEventsMigration(EntitlementSubscriptionMigrationCase [] input, DateTime now)
+        throws EntitlementMigrationApiException {
+
+        try {
+            TimedMigration [] events = null;
+            Plan plan0 = catalogService.getCatalog().findPlan(input[0].getPlanPhaseSpecifer().getProductName(),
+                    input[0].getPlanPhaseSpecifer().getBillingPeriod(), input[0].getPlanPhaseSpecifer().getPriceListName());
+
+            Plan plan1 = (input.length > 1) ? catalogService.getCatalog().findPlan(input[1].getPlanPhaseSpecifer().getProductName(),
+                    input[1].getPlanPhaseSpecifer().getBillingPeriod(), input[1].getPlanPhaseSpecifer().getPriceListName()) :
+                        null;
+
+            DateTime migrationStartDate = now;
+
+            if (isRegularMigratedSubscription(input)) {
+
+                events = getEventsOnRegularMigration(plan0,
+                        getPlanPhase(plan0, input[0].getPlanPhaseSpecifer().getPhaseType()),
+                        input[0].getPlanPhaseSpecifer().getPriceListName(),
+                        now);
+
+            } else if (isRegularFutureCancelledMigratedSubscription(input)) {
+
+                events = getEventsOnFuturePlanCancelMigration(plan0,
+                        getPlanPhase(plan0, input[0].getPlanPhaseSpecifer().getPhaseType()),
+                        input[0].getPlanPhaseSpecifer().getPriceListName(),
+                        now,
+                        input[0].getCancelledDate());
+
+            } else if (isPhaseChangeMigratedSubscription(input)) {
+
+                PhaseType curPhaseType = input[0].getPlanPhaseSpecifer().getPhaseType();
+                Duration curPhaseDuration = null;
+                for (PlanPhase cur : plan0.getAllPhases()) {
+                    if (cur.getPhaseType() == curPhaseType) {
+                        curPhaseDuration = cur.getDuration();
+                        break;
+                    }
+                }
+                if (curPhaseDuration == null) {
+                    throw new EntitlementMigrationApiException(String.format("Failed to compute current phase duration for plan %s and phase %s",
+                            plan0.getName(), curPhaseType));
+                }
+
+                migrationStartDate = DefaultClock.removeDuration(input[1].getEffectiveDate(), curPhaseDuration);
+                events = getEventsOnFuturePhaseChangeMigration(plan0,
+                        getPlanPhase(plan0, input[0].getPlanPhaseSpecifer().getPhaseType()),
+                        input[0].getPlanPhaseSpecifer().getPriceListName(),
+                        migrationStartDate,
+                        input[1].getEffectiveDate());
+
+            } else if (isPlanChangeMigratedSubscription(input)) {
+
+                events = getEventsOnFuturePlanChangeMigration(plan0,
+                        getPlanPhase(plan0, input[0].getPlanPhaseSpecifer().getPhaseType()),
+                        plan1,
+                        getPlanPhase(plan1, input[1].getPlanPhaseSpecifer().getPhaseType()),
+                        input[0].getPlanPhaseSpecifer().getPriceListName(),
+                        now,
+                        input[1].getEffectiveDate());
+
+            } else {
+                throw new EntitlementMigrationApiException("Unknown migration type");
+            }
+
+            return events;
+        } catch (CatalogApiException e) {
+            throw new EntitlementMigrationApiException(e);
+        }
+    }
+
+    private TimedMigration [] getEventsOnRegularMigration(Plan plan, PlanPhase initialPhase, String priceList, DateTime effectiveDate) {
+        TimedMigration [] result = new TimedMigration[1];
+        result[0] = new TimedMigration(effectiveDate, EventType.API_USER, ApiEventType.MIGRATE_ENTITLEMENT, plan, initialPhase, priceList);
+        return result;
+    }
+
+    private TimedMigration [] getEventsOnFuturePhaseChangeMigration(Plan plan, PlanPhase initialPhase, String priceList, DateTime effectiveDate, DateTime effectiveDateForNextPhase)
+        throws EntitlementMigrationApiException {
+
+        TimedMigration [] result = new TimedMigration[2];
+        result[0] = new TimedMigration(effectiveDate, EventType.API_USER, ApiEventType.MIGRATE_ENTITLEMENT, plan, initialPhase, priceList);
+        boolean foundCurrent = false;
+        PlanPhase nextPhase = null;
+        for (PlanPhase cur : plan.getAllPhases()) {
+            if (cur == initialPhase) {
+                foundCurrent = true;
+                continue;
+            }
+            if (foundCurrent) {
+                nextPhase = cur;
+            }
+        }
+        if (nextPhase == null) {
+            throw new EntitlementMigrationApiException(String.format("Cannot find next phase for Plan %s and current Phase %s",
+                        plan.getName(), initialPhase.getName()));
+        }
+        result[1] = new TimedMigration(effectiveDateForNextPhase, EventType.PHASE, null, plan, nextPhase, priceList);
+        return result;
+    }
+
+    private TimedMigration [] getEventsOnFuturePlanChangeMigration(Plan currentPlan, PlanPhase currentPhase, Plan newPlan, PlanPhase newPhase, String priceList, DateTime effectiveDate, DateTime effectiveDateForChangePlan) {
+        TimedMigration [] result = new TimedMigration[2];
+        result[0] = new TimedMigration(effectiveDate, EventType.API_USER, ApiEventType.MIGRATE_ENTITLEMENT, currentPlan, currentPhase, priceList);
+        result[1] = new TimedMigration(effectiveDateForChangePlan, EventType.API_USER, ApiEventType.CHANGE, newPlan, newPhase, priceList);
+        return result;
+    }
+
+    private TimedMigration [] getEventsOnFuturePlanCancelMigration(Plan plan, PlanPhase initialPhase, String priceList, DateTime effectiveDate, DateTime effectiveDateForCancellation) {
+        TimedMigration [] result = new TimedMigration[2];
+        result[0] = new TimedMigration(effectiveDate, EventType.API_USER, ApiEventType.MIGRATE_ENTITLEMENT, plan, initialPhase, priceList);
+        result[1] = new TimedMigration(effectiveDateForCancellation, EventType.API_USER, ApiEventType.CANCEL, null, null, null);
+        return result;
+    }
+
+
+    // STEPH should be in catalog
+    private PlanPhase getPlanPhase(Plan plan, PhaseType phaseType) throws EntitlementMigrationApiException {
+        for (PlanPhase cur: plan.getAllPhases()) {
+            if (cur.getPhaseType() == phaseType) {
+                return cur;
+            }
+        }
+        throw new EntitlementMigrationApiException(String.format("Cannot find PlanPhase from Plan %s and type %s", plan.getName(), phaseType));
+    }
+
+    private boolean isRegularMigratedSubscription(EntitlementSubscriptionMigrationCase [] input) {
+        return (input.length == 1 && input[0].getCancelledDate() == null);
+    }
+
+    private boolean isRegularFutureCancelledMigratedSubscription(EntitlementSubscriptionMigrationCase [] input) {
+        return (input.length == 1 && input[0].getCancelledDate() != null);
+    }
+
+    private boolean isPhaseChangeMigratedSubscription(EntitlementSubscriptionMigrationCase [] input) {
+        if (input.length != 2) {
+            return false;
+        }
+        return (isSamePlan(input[0].getPlanPhaseSpecifer(), input[1].getPlanPhaseSpecifer()) &&
+                !isSamePhase(input[0].getPlanPhaseSpecifer(), input[1].getPlanPhaseSpecifer()));
+    }
+
+    private boolean isPlanChangeMigratedSubscription(EntitlementSubscriptionMigrationCase [] input) {
+        if (input.length != 2) {
+            return false;
+        }
+        return ! isSamePlan(input[0].getPlanPhaseSpecifer(), input[1].getPlanPhaseSpecifer());
+    }
+
+    private boolean isSamePlan(PlanPhaseSpecifier plan0, PlanPhaseSpecifier plan1) {
+        if (plan0.getPriceListName().equals(plan1.getPriceListName()) &&
+                plan0.getProductName().equals(plan1.getProductName())) {
+            return true;
+        }
+        return false;
+    }
+
+    private boolean isSamePhase(PlanPhaseSpecifier plan0, PlanPhaseSpecifier plan1) {
+        if (plan0.getPhaseType() == plan1.getPhaseType()) {
+            return true;
+        }
+        return false;
+    }
+}
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/alignment/TimedMigration.java b/entitlement/src/main/java/com/ning/billing/entitlement/alignment/TimedMigration.java
new file mode 100644
index 0000000..e1ad564
--- /dev/null
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/alignment/TimedMigration.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.entitlement.alignment;
+
+import org.joda.time.DateTime;
+
+import com.ning.billing.catalog.api.Plan;
+import com.ning.billing.catalog.api.PlanPhase;
+import com.ning.billing.entitlement.events.EntitlementEvent.EventType;
+import com.ning.billing.entitlement.events.user.ApiEventType;
+
+public class TimedMigration {
+
+    private final DateTime eventTime;
+    private final EventType eventType;
+    private final ApiEventType apiEventType;
+
+    private final Plan plan;
+    private final PlanPhase phase;
+    private final String priceList;
+
+
+    public TimedMigration(DateTime eventTime, EventType eventType,
+            ApiEventType apiEventType, Plan plan, PlanPhase phase, String priceList) {
+        super();
+        this.eventTime = eventTime;
+        this.eventType = eventType;
+        this.apiEventType = apiEventType;
+        this.plan = plan;
+        this.phase = phase;
+        this.priceList = priceList;
+    }
+
+    public DateTime getEventTime() {
+        return eventTime;
+    }
+
+    public EventType getEventType() {
+        return eventType;
+    }
+
+
+    public ApiEventType getApiEventType() {
+        return apiEventType;
+    }
+
+    public Plan getPlan() {
+        return plan;
+    }
+
+    public PlanPhase getPhase() {
+        return phase;
+    }
+
+    public String getPriceList() {
+        return priceList;
+    }
+}
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/api/migration/AccountMigrationData.java b/entitlement/src/main/java/com/ning/billing/entitlement/api/migration/AccountMigrationData.java
new file mode 100644
index 0000000..7396136
--- /dev/null
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/api/migration/AccountMigrationData.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.entitlement.api.migration;
+
+import java.util.List;
+
+import com.ning.billing.entitlement.api.user.SubscriptionBundleData;
+import com.ning.billing.entitlement.api.user.SubscriptionData;
+import com.ning.billing.entitlement.events.EntitlementEvent;
+
+public class AccountMigrationData {
+
+    private final List<BundleMigrationData> data;
+
+    public AccountMigrationData(List<BundleMigrationData> data) {
+        super();
+        this.data = data;
+    }
+
+    public List<BundleMigrationData> getData() {
+        return data;
+    }
+
+    public static class BundleMigrationData {
+
+        private final SubscriptionBundleData data;
+        private final List<SubscriptionMigrationData> subscriptions;
+
+        public BundleMigrationData(SubscriptionBundleData data,
+                List<SubscriptionMigrationData> subscriptions) {
+            super();
+            this.data = data;
+            this.subscriptions = subscriptions;
+        }
+
+        public SubscriptionBundleData getData() {
+            return data;
+        }
+
+        public List<SubscriptionMigrationData> getSubscriptions() {
+            return subscriptions;
+        }
+    }
+
+    public static class SubscriptionMigrationData {
+
+        private final SubscriptionData data;
+        private final List<EntitlementEvent> initialEvents;
+
+        public SubscriptionMigrationData(SubscriptionData data,
+
+                List<EntitlementEvent> initialEvents) {
+            super();
+            this.data = data;
+            this.initialEvents = initialEvents;
+        }
+
+        public SubscriptionData getData() {
+            return data;
+        }
+
+        public List<EntitlementEvent> getInitialEvents() {
+            return initialEvents;
+        }
+    }
+}
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/api/migration/DefaultEntitlementMigrationApi.java b/entitlement/src/main/java/com/ning/billing/entitlement/api/migration/DefaultEntitlementMigrationApi.java
new file mode 100644
index 0000000..d8a0100
--- /dev/null
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/api/migration/DefaultEntitlementMigrationApi.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.entitlement.api.migration;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import com.google.inject.Inject;
+import com.ning.billing.catalog.api.CatalogApiException;
+import com.ning.billing.catalog.api.CatalogService;
+import com.ning.billing.catalog.api.Duration;
+import com.ning.billing.catalog.api.PhaseType;
+import com.ning.billing.catalog.api.Plan;
+import com.ning.billing.catalog.api.PlanPhase;
+import com.ning.billing.catalog.api.PlanPhaseSpecifier;
+import com.ning.billing.catalog.api.ProductCategory;
+import com.ning.billing.entitlement.alignment.MigrationPlanAligner;
+import com.ning.billing.entitlement.alignment.TimedMigration;
+import com.ning.billing.entitlement.api.migration.AccountMigrationData.BundleMigrationData;
+import com.ning.billing.entitlement.api.migration.AccountMigrationData.SubscriptionMigrationData;
+import com.ning.billing.entitlement.api.user.SubscriptionBundleData;
+import com.ning.billing.entitlement.api.user.SubscriptionData;
+import com.ning.billing.entitlement.api.user.SubscriptionFactory;
+import com.ning.billing.entitlement.api.user.SubscriptionFactory.SubscriptionBuilder;
+import com.ning.billing.entitlement.engine.dao.EntitlementDao;
+import com.ning.billing.entitlement.events.EntitlementEvent;
+import com.ning.billing.entitlement.events.EntitlementEvent.EventType;
+import com.ning.billing.entitlement.events.phase.PhaseEvent;
+import com.ning.billing.entitlement.events.phase.PhaseEventData;
+import com.ning.billing.entitlement.events.user.ApiEventBuilder;
+import com.ning.billing.entitlement.events.user.ApiEventCancel;
+import com.ning.billing.entitlement.events.user.ApiEventChange;
+import com.ning.billing.entitlement.events.user.ApiEventMigrate;
+import com.ning.billing.entitlement.exceptions.EntitlementError;
+import com.ning.billing.util.clock.Clock;
+import com.ning.billing.util.clock.DefaultClock;
+
+public class DefaultEntitlementMigrationApi implements EntitlementMigrationApi {
+
+
+    private final EntitlementDao dao;
+    private final MigrationPlanAligner migrationAligner;
+    private final SubscriptionFactory factory;
+    private final CatalogService catalogService;
+    private final Clock clock;
+
+    @Inject
+    public DefaultEntitlementMigrationApi(MigrationPlanAligner migrationAligner,
+            SubscriptionFactory factory,
+            CatalogService catalogService,
+            EntitlementDao dao,
+            Clock clock) {
+        this.dao = dao;
+        this.migrationAligner = migrationAligner;
+        this.factory = factory;
+        this.catalogService = catalogService;
+        this.clock = clock;
+    }
+
+    @Override
+    public void migrate(EntitlementAccountMigration toBeMigrated)
+    throws EntitlementMigrationApiException {
+        AccountMigrationData accountMigrationData = createAccountMigrationData(toBeMigrated);
+        dao.migrate(toBeMigrated.getAccountKey(), accountMigrationData);
+    }
+
+    @Override
+    public void undoMigration(UUID accountId) {
+        dao.undoMigration(accountId);
+    }
+
+    private AccountMigrationData createAccountMigrationData(EntitlementAccountMigration toBeMigrated)
+    throws EntitlementMigrationApiException  {
+
+        final UUID accountId = toBeMigrated.getAccountKey();
+        final DateTime now = clock.getUTCNow();
+
+        List<BundleMigrationData> accountBundleData = new LinkedList<BundleMigrationData>();
+
+        for (final EntitlementBundleMigration curBundle : toBeMigrated.getBundles()) {
+
+            SubscriptionBundleData bundleData = new SubscriptionBundleData(curBundle.getBundleKey(), accountId);
+            List<SubscriptionMigrationData> bundleSubscriptionData = new LinkedList<AccountMigrationData.SubscriptionMigrationData>();
+
+            for (EntitlementSubscriptionMigration curSub : curBundle.getSubscriptions()) {
+                SubscriptionMigrationData data = null;
+                switch (curSub.getCategory()) {
+                case BASE:
+                    data = createBaseSubscriptionMigrationData(bundleData.getId(), curSub.getCategory(), curSub.getSubscriptionCases(), now);
+                    break;
+                case ADD_ON:
+                    // Not implemented yet
+                    break;
+                case STANDALONE:
+                    // Not implemented yet
+                    break;
+                default:
+                    throw new EntitlementMigrationApiException(String.format("Unkown product type ", curSub.getCategory()));
+                }
+                if (data != null) {
+                    bundleSubscriptionData.add(data);
+                }
+            }
+            BundleMigrationData bundleMigrationData = new BundleMigrationData(bundleData, bundleSubscriptionData);
+            accountBundleData.add(bundleMigrationData);
+        }
+        AccountMigrationData accountMigrationData = new AccountMigrationData(accountBundleData);
+        return accountMigrationData;
+    }
+
+    private SubscriptionMigrationData createBaseSubscriptionMigrationData(UUID bundleId, ProductCategory productCategory,
+            EntitlementSubscriptionMigrationCase [] input, DateTime now)
+        throws EntitlementMigrationApiException {
+
+        TimedMigration [] events = migrationAligner.getEventsMigration(input, now);
+        DateTime migrationStartDate= events[0].getEventTime();
+        List<EntitlementEvent> emptyEvents =  Collections.emptyList();
+        SubscriptionData subscriptionData = factory.createSubscription(new SubscriptionBuilder()
+            .setId(UUID.randomUUID())
+            .setBundleId(bundleId)
+            .setCategory(productCategory)
+            .setBundleStartDate(migrationStartDate)
+            .setStartDate(migrationStartDate),
+            emptyEvents);
+        return new SubscriptionMigrationData(subscriptionData, toEvents(subscriptionData, now, events));
+    }
+
+    private List<EntitlementEvent> toEvents(SubscriptionData subscriptionData, DateTime now, TimedMigration [] migrationEvents) {
+
+        List<EntitlementEvent> events = new ArrayList<EntitlementEvent>(migrationEvents.length);
+        for (TimedMigration cur : migrationEvents) {
+
+            if (cur.getEventType() == EventType.PHASE) {
+                PhaseEvent nextPhaseEvent = PhaseEventData.getNextPhaseEvent(cur.getPhase().getName(), subscriptionData, now, cur.getEventTime());
+                events.add(nextPhaseEvent);
+
+            } else if (cur.getEventType() == EventType.API_USER) {
+
+                ApiEventBuilder builder = new ApiEventBuilder()
+                .setSubscriptionId(subscriptionData.getId())
+                .setEventPlan((cur.getPlan() != null) ? cur.getPlan().getName() : null)
+                .setEventPlanPhase((cur.getPhase() != null) ? cur.getPhase().getName() : null)
+                .setEventPriceList(cur.getPriceList())
+                .setActiveVersion(subscriptionData.getActiveVersion())
+                .setEffectiveDate(cur.getEventTime())
+                .setProcessedDate(now)
+                .setRequestedDate(now);
+
+                switch(cur.getApiEventType()) {
+                case MIGRATE_ENTITLEMENT:
+                    events.add(new ApiEventMigrate(builder));
+                    break;
+
+                case CHANGE:
+                    events.add(new ApiEventChange(builder));
+                    break;
+                case CANCEL:
+                    events.add(new ApiEventCancel(builder));
+                    break;
+                default:
+                    throw new EntitlementError(String.format("Unexpected type of api migration event %s", cur.getApiEventType()));
+                }
+            } else {
+                throw new EntitlementError(String.format("Unexpected type of migration event %s", cur.getEventType()));
+            }
+        }
+        return events;
+    }
+}
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/api/user/DefaultEntitlementUserApi.java b/entitlement/src/main/java/com/ning/billing/entitlement/api/user/DefaultEntitlementUserApi.java
index 2cf23bc..8b6d59a 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/api/user/DefaultEntitlementUserApi.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/api/user/DefaultEntitlementUserApi.java
@@ -21,13 +21,11 @@ import java.util.UUID;
 import org.joda.time.DateTime;
 import com.google.inject.Inject;
 import com.ning.billing.ErrorCode;
-import com.ning.billing.account.api.AccountData;
-import com.ning.billing.catalog.api.BillingPeriod;
 import com.ning.billing.catalog.api.CatalogApiException;
 import com.ning.billing.catalog.api.CatalogService;
-import com.ning.billing.catalog.api.PhaseType;
 import com.ning.billing.catalog.api.Plan;
 import com.ning.billing.catalog.api.PlanPhase;
+import com.ning.billing.catalog.api.PlanPhaseSpecifier;
 import com.ning.billing.catalog.api.PriceListSet;
 import com.ning.billing.entitlement.api.user.SubscriptionFactory.SubscriptionBuilder;
 import com.ning.billing.entitlement.engine.dao.EntitlementDao;
@@ -62,6 +60,11 @@ public class DefaultEntitlementUserApi implements EntitlementUserApi {
     }
 
     @Override
+    public SubscriptionBundle getBundleForKey(String bundleKey) {
+        return dao.getSubscriptionBundleFromKey(bundleKey);
+    }
+
+    @Override
     public List<SubscriptionBundle> getBundlesForAccount(UUID accountId) {
         return dao.getSubscriptionBundleForAccount(accountId);
     }
@@ -85,11 +88,10 @@ public class DefaultEntitlementUserApi implements EntitlementUserApi {
     }
 
     @Override
-    public Subscription createSubscription(UUID bundleId, String productName,
-            BillingPeriod term, String priceList, PhaseType initialPhase, DateTime requestedDate) throws EntitlementUserApiException {
+    public Subscription createSubscription(UUID bundleId, PlanPhaseSpecifier spec, DateTime requestedDate) throws EntitlementUserApiException {
 
         try {
-            String realPriceList = (priceList == null) ? PriceListSet.DEFAULT_PRICELIST_NAME : priceList;
+            String realPriceList = (spec.getPriceListName() == null) ? PriceListSet.DEFAULT_PRICELIST_NAME : spec.getPriceListName();
             DateTime now = clock.getUTCNow();
             requestedDate = (requestedDate != null) ? DefaultClock.truncateMs(requestedDate) : now;
             if (requestedDate != null && requestedDate.isAfter(now)) {
@@ -98,13 +100,13 @@ public class DefaultEntitlementUserApi implements EntitlementUserApi {
             requestedDate = (requestedDate == null) ? now : requestedDate;
             DateTime effectiveDate = requestedDate;
 
-            Plan plan = catalogService.getCatalog().findPlan(productName, term, realPriceList);
+            Plan plan = catalogService.getCatalog().findPlan(spec.getProductName(), spec.getBillingPeriod(), realPriceList);
 
 
             PlanPhase phase = (plan.getInitialPhases() != null) ? plan.getInitialPhases()[0] : plan.getFinalPhase();
             if (phase == null) {
                 throw new EntitlementError(String.format("No initial PlanPhase for Product %s, term %s and set %s does not exist in the catalog",
-                        productName, term.toString(), realPriceList));
+                        spec.getProductName(), spec.getBillingPeriod().toString(), realPriceList));
             }
 
             SubscriptionBundle bundle = dao.getSubscriptionBundleFromId(bundleId);
@@ -139,7 +141,7 @@ public class DefaultEntitlementUserApi implements EntitlementUserApi {
             .setCategory(plan.getProduct().getCategory())
             .setBundleStartDate(bundleStartDate)
             .setStartDate(effectiveDate),
-            plan, initialPhase, realPriceList, requestedDate, effectiveDate, now);
+            plan, spec.getPhaseType(), realPriceList, requestedDate, effectiveDate, now);
 
             return subscription;
         } catch (CatalogApiException e) {
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionApiService.java b/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionApiService.java
index 3b3ff73..3aebac0 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionApiService.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionApiService.java
@@ -71,7 +71,10 @@ public class SubscriptionApiService {
             .setEffectiveDate(effectiveDate)
             .setRequestedDate(requestedDate));
 
-            PhaseEvent nextPhaseEvent = PhaseEventData.getNextPhaseEvent(curAndNextPhases[1], subscription, processedDate);
+            TimedPhase nextTimedPhase = curAndNextPhases[1];
+            PhaseEvent nextPhaseEvent = (nextTimedPhase != null) ?
+                    PhaseEventData.getNextPhaseEvent(nextTimedPhase.getPhase().getName(), subscription, processedDate, nextTimedPhase.getStartPhase()) :
+                        null;
             List<EntitlementEvent> events = new ArrayList<EntitlementEvent>();
             events.add(creationEvent);
             if (nextPhaseEvent != null) {
@@ -95,7 +98,8 @@ public class SubscriptionApiService {
             }
 
             DateTime now = clock.getUTCNow();
-            requestedDate = (requestedDate != null) ? DefaultClock.truncateMs(requestedDate) : null;
+            requestedDate = (requestedDate != null) ? DefaultClock.truncateMs(requestedDate) : now;
+            // STEPH needs to check if requestedDate is before last 'erasable event'?
             if (requestedDate != null && requestedDate.isAfter(now)) {
                 throw new EntitlementUserApiException(ErrorCode.ENT_INVALID_REQUESTED_DATE, requestedDate.toString());
             }
@@ -109,7 +113,7 @@ public class SubscriptionApiService {
 
             ActionPolicy policy = null;
             policy = catalogService.getCatalog().planCancelPolicy(planPhase);
-            DateTime effectiveDate = subscription.getPlanChangeEffectiveDate(policy, now);
+            DateTime effectiveDate = subscription.getPlanChangeEffectiveDate(policy, requestedDate);
 
             EntitlementEvent cancelEvent = new ApiEventCancel(new ApiEventBuilder()
             .setSubscriptionId(subscription.getId())
@@ -146,7 +150,9 @@ public class SubscriptionApiService {
 
         DateTime planStartDate = subscription.getCurrentPlanStart();
         TimedPhase nextTimedPhase = planAligner.getNextTimedPhase(subscription.getCurrentPlan(), subscription.getInitialPhaseOnCurrentPlan().getPhaseType(), now, planStartDate);
-        PhaseEvent nextPhaseEvent = PhaseEventData.getNextPhaseEvent(nextTimedPhase, subscription, now);
+        PhaseEvent nextPhaseEvent = (nextTimedPhase != null) ?
+                PhaseEventData.getNextPhaseEvent(nextTimedPhase.getPhase().getName(), subscription, now, nextTimedPhase.getStartPhase()) :
+                    null;
         if (nextPhaseEvent != null) {
             uncancelEvents.add(nextPhaseEvent);
         }
@@ -159,66 +165,74 @@ public class SubscriptionApiService {
         throws EntitlementUserApiException {
 
         try {
-        requestedDate = (requestedDate != null) ? DefaultClock.truncateMs(requestedDate) : null;
-        String currentPriceList = subscription.getCurrentPriceList();
 
-        SubscriptionState currentState = subscription.getState();
-        if (currentState != SubscriptionState.ACTIVE) {
-            throw new EntitlementUserApiException(ErrorCode.ENT_CHANGE_NON_ACTIVE, subscription.getId(), currentState);
-        }
 
-        if (subscription.isSubscriptionFutureCancelled()) {
-            throw new EntitlementUserApiException(ErrorCode.ENT_CHANGE_FUTURE_CANCELLED, subscription.getId());
-        }
+            DateTime now = clock.getUTCNow();
+            requestedDate = (requestedDate != null) ? DefaultClock.truncateMs(requestedDate) : now;
+            // STEPH needs to check if requestedDate is before last 'erasable event'?
+            if (requestedDate != null && requestedDate.isAfter(now)) {
+                throw new EntitlementUserApiException(ErrorCode.ENT_INVALID_REQUESTED_DATE, requestedDate.toString());
+            }
 
-        DateTime now = clock.getUTCNow();
-        PlanChangeResult planChangeResult = null;
-        try {
+            String currentPriceList = subscription.getCurrentPriceList();
 
-            Product destProduct = catalogService.getCatalog().findProduct(productName);
-            Plan currentPlan = subscription.getCurrentPlan();
-            PlanPhaseSpecifier fromPlanPhase = new PlanPhaseSpecifier(currentPlan.getProduct().getName(),
-                    currentPlan.getProduct().getCategory(),
-                    currentPlan.getBillingPeriod(),
-                    currentPriceList, subscription.getCurrentPhase().getPhaseType());
-            PlanSpecifier toPlanPhase = new PlanSpecifier(productName,
-                    destProduct.getCategory(),
-                    term,
-                    priceList);
-
-            planChangeResult = catalogService.getCatalog().planChange(fromPlanPhase, toPlanPhase);
-        } catch (CatalogApiException e) {
-            throw new EntitlementUserApiException(e);
-        }
+            SubscriptionState currentState = subscription.getState();
+            if (currentState != SubscriptionState.ACTIVE) {
+                throw new EntitlementUserApiException(ErrorCode.ENT_CHANGE_NON_ACTIVE, subscription.getId(), currentState);
+            }
 
-        ActionPolicy policy = planChangeResult.getPolicy();
-        PriceList newPriceList = planChangeResult.getNewPriceList();
+            if (subscription.isSubscriptionFutureCancelled()) {
+                throw new EntitlementUserApiException(ErrorCode.ENT_CHANGE_FUTURE_CANCELLED, subscription.getId());
+            }
+            PlanChangeResult planChangeResult = null;
+            try {
+
+                Product destProduct = catalogService.getCatalog().findProduct(productName);
+                Plan currentPlan = subscription.getCurrentPlan();
+                PlanPhaseSpecifier fromPlanPhase = new PlanPhaseSpecifier(currentPlan.getProduct().getName(),
+                        currentPlan.getProduct().getCategory(),
+                        currentPlan.getBillingPeriod(),
+                        currentPriceList, subscription.getCurrentPhase().getPhaseType());
+                PlanSpecifier toPlanPhase = new PlanSpecifier(productName,
+                        destProduct.getCategory(),
+                        term,
+                        priceList);
+
+                planChangeResult = catalogService.getCatalog().planChange(fromPlanPhase, toPlanPhase);
+            } catch (CatalogApiException e) {
+                throw new EntitlementUserApiException(e);
+            }
 
-        Plan newPlan = catalogService.getCatalog().findPlan(productName, term, newPriceList.getName());
-        DateTime effectiveDate = subscription.getPlanChangeEffectiveDate(policy, now);
+            ActionPolicy policy = planChangeResult.getPolicy();
+            PriceList newPriceList = planChangeResult.getNewPriceList();
 
-        TimedPhase currentTimedPhase = planAligner.getCurrentTimedPhaseOnChange(subscription, newPlan, newPriceList.getName(), effectiveDate);
+            Plan newPlan = catalogService.getCatalog().findPlan(productName, term, newPriceList.getName());
+            DateTime effectiveDate = subscription.getPlanChangeEffectiveDate(policy, now);
 
-        EntitlementEvent changeEvent = new ApiEventChange(new ApiEventBuilder()
-        .setSubscriptionId(subscription.getId())
-        .setEventPlan(newPlan.getName())
-        .setEventPlanPhase(currentTimedPhase.getPhase().getName())
-        .setEventPriceList(newPriceList.getName())
-        .setActiveVersion(subscription.getActiveVersion())
-        .setProcessedDate(now)
-        .setEffectiveDate(effectiveDate)
-        .setRequestedDate(now));
-
-        TimedPhase nextTimedPhase = planAligner.getNextTimedPhaseOnChange(subscription, newPlan, newPriceList.getName(), effectiveDate);
-        PhaseEvent nextPhaseEvent = PhaseEventData.getNextPhaseEvent(nextTimedPhase, subscription, now);
-        List<EntitlementEvent> changeEvents = new ArrayList<EntitlementEvent>();
-        // Only add the PHASE if it does not coincide with the CHANGE, if not this is 'just' a CHANGE.
-        if (nextPhaseEvent != null && ! nextPhaseEvent.getEffectiveDate().equals(changeEvent.getEffectiveDate())) {
-            changeEvents.add(nextPhaseEvent);
-        }
-        changeEvents.add(changeEvent);
-        dao.changePlan(subscription.getId(), changeEvents);
-        subscription.rebuildTransitions(dao.getEventsForSubscription(subscription.getId()), catalogService.getCatalog());
+            TimedPhase currentTimedPhase = planAligner.getCurrentTimedPhaseOnChange(subscription, newPlan, newPriceList.getName(), effectiveDate);
+
+            EntitlementEvent changeEvent = new ApiEventChange(new ApiEventBuilder()
+            .setSubscriptionId(subscription.getId())
+            .setEventPlan(newPlan.getName())
+            .setEventPlanPhase(currentTimedPhase.getPhase().getName())
+            .setEventPriceList(newPriceList.getName())
+            .setActiveVersion(subscription.getActiveVersion())
+            .setProcessedDate(now)
+            .setEffectiveDate(effectiveDate)
+            .setRequestedDate(now));
+
+            TimedPhase nextTimedPhase = planAligner.getNextTimedPhaseOnChange(subscription, newPlan, newPriceList.getName(), effectiveDate);
+            PhaseEvent nextPhaseEvent = (nextTimedPhase != null) ?
+                    PhaseEventData.getNextPhaseEvent(nextTimedPhase.getPhase().getName(), subscription, now, nextTimedPhase.getStartPhase()) :
+                        null;
+                    List<EntitlementEvent> changeEvents = new ArrayList<EntitlementEvent>();
+                    // Only add the PHASE if it does not coincide with the CHANGE, if not this is 'just' a CHANGE.
+                    if (nextPhaseEvent != null && ! nextPhaseEvent.getEffectiveDate().equals(changeEvent.getEffectiveDate())) {
+                        changeEvents.add(nextPhaseEvent);
+                    }
+                    changeEvents.add(changeEvent);
+                    dao.changePlan(subscription.getId(), changeEvents);
+                    subscription.rebuildTransitions(dao.getEventsForSubscription(subscription.getId()), catalogService.getCatalog());
         } catch (CatalogApiException e) {
             throw new EntitlementUserApiException(e);
         }
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionData.java b/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionData.java
index 669c77d..3e8ede5 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionData.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionData.java
@@ -170,6 +170,18 @@ public class SubscriptionData implements Subscription {
         return activeTransitions;
     }
 
+    @Override
+    public SubscriptionTransition getPendingTransition() {
+        if (transitions == null) {
+            return null;
+        }
+        for (SubscriptionTransition cur : transitions) {
+            if (cur.getEffectiveTransitionTime().isAfter(clock.getUTCNow())) {
+                return cur;
+            }
+        }
+        return null;
+    }
 
     public SubscriptionTransition getLatestTranstion() {
 
@@ -265,10 +277,10 @@ public class SubscriptionData implements Subscription {
     }
 
 
-    public DateTime getPlanChangeEffectiveDate(ActionPolicy policy, DateTime now) {
+    public DateTime getPlanChangeEffectiveDate(ActionPolicy policy, DateTime requestedDate) {
 
         if (policy == ActionPolicy.IMMEDIATE) {
-            return now;
+            return requestedDate;
         }
         if (policy != ActionPolicy.END_OF_TERM) {
             throw new EntitlementError(String.format("Unexpected policy type %s", policy.toString()));
@@ -342,6 +354,7 @@ public class SubscriptionData implements Subscription {
                 ApiEvent userEV = (ApiEvent) cur;
                 apiEventType = userEV.getEventType();
                 switch(apiEventType) {
+                case MIGRATE_ENTITLEMENT:
                 case CREATE:
                     nextState = SubscriptionState.ACTIVE;
                     nextPlanName = userEV.getEventPlan();
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionFactory.java b/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionFactory.java
index 59d7c6e..207b4b2 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionFactory.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionFactory.java
@@ -43,12 +43,13 @@ public class SubscriptionFactory {
 
     public SubscriptionData createSubscription(SubscriptionBuilder builder, List<EntitlementEvent> events) {
         SubscriptionData subscription = new SubscriptionData(builder, apiService, clock);
-        subscription.rebuildTransitions(events, catalogService.getCatalog());
+        if (events.size() > 0) {
+            subscription.rebuildTransitions(events, catalogService.getCatalog());
+        }
         return subscription;
     }
 
 
-
     public static class SubscriptionBuilder {
 
         private  UUID id;
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/engine/core/Engine.java b/entitlement/src/main/java/com/ning/billing/entitlement/engine/core/Engine.java
index aed7937..3eb5dd3 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/engine/core/Engine.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/engine/core/Engine.java
@@ -16,6 +16,10 @@
 
 package com.ning.billing.entitlement.engine.core;
 
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import com.google.inject.Inject;
 import com.ning.billing.config.EntitlementConfig;
 import com.ning.billing.entitlement.alignment.PlanAligner;
@@ -23,6 +27,8 @@ import com.ning.billing.entitlement.alignment.TimedPhase;
 import com.ning.billing.entitlement.api.EntitlementService;
 import com.ning.billing.entitlement.api.billing.DefaultEntitlementBillingApi;
 import com.ning.billing.entitlement.api.billing.EntitlementBillingApi;
+import com.ning.billing.entitlement.api.migration.DefaultEntitlementMigrationApi;
+import com.ning.billing.entitlement.api.migration.EntitlementMigrationApi;
 import com.ning.billing.entitlement.api.test.DefaultEntitlementTestApi;
 import com.ning.billing.entitlement.api.test.EntitlementTestApi;
 import com.ning.billing.entitlement.api.user.DefaultEntitlementUserApi;
@@ -39,9 +45,6 @@ import com.ning.billing.lifecycle.LifecycleHandlerType.LifecycleLevel;
 import com.ning.billing.util.clock.Clock;
 import com.ning.billing.util.eventbus.EventBus;
 import com.ning.billing.util.eventbus.EventBus.EventBusException;
-import org.joda.time.DateTime;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class Engine implements EventListener, EntitlementService {
 
@@ -60,6 +63,7 @@ public class Engine implements EventListener, EntitlementService {
     private final EntitlementUserApi userApi;
     private final EntitlementBillingApi billingApi;
     private final EntitlementTestApi testApi;
+    private final EntitlementMigrationApi migrationApi;
     private final EventBus eventBus;
 
     private boolean startedNotificationThread;
@@ -67,7 +71,8 @@ public class Engine implements EventListener, EntitlementService {
     @Inject
     public Engine(Clock clock, EntitlementDao dao, EventNotifier apiEventProcessor,
             PlanAligner planAligner, EntitlementConfig config, DefaultEntitlementUserApi userApi,
-            DefaultEntitlementBillingApi billingApi, DefaultEntitlementTestApi testApi, EventBus eventBus) {
+            DefaultEntitlementBillingApi billingApi, DefaultEntitlementTestApi testApi,
+            DefaultEntitlementMigrationApi migrationApi, EventBus eventBus) {
         super();
         this.clock = clock;
         this.dao = dao;
@@ -76,6 +81,7 @@ public class Engine implements EventListener, EntitlementService {
         this.userApi = userApi;
         this.testApi = testApi;
         this.billingApi = billingApi;
+        this.migrationApi = migrationApi;
         this.eventBus = eventBus;
 
         this.startedNotificationThread = false;
@@ -120,6 +126,12 @@ public class Engine implements EventListener, EntitlementService {
     }
 
     @Override
+    public EntitlementMigrationApi getMigrationApi() {
+        return migrationApi;
+    }
+
+
+    @Override
     public void processEventReady(EntitlementEvent event) {
         SubscriptionData subscription = (SubscriptionData) dao.getSubscriptionFromId(event.getSubscriptionId());
         if (subscription == null) {
@@ -176,7 +188,9 @@ public class Engine implements EventListener, EntitlementService {
         try {
             DateTime now = clock.getUTCNow();
             TimedPhase nextTimedPhase = planAligner.getNextTimedPhase(subscription.getCurrentPlan(), subscription.getInitialPhaseOnCurrentPlan().getPhaseType(), now, subscription.getCurrentPlanStart());
-            PhaseEvent nextPhaseEvent = PhaseEventData.getNextPhaseEvent(nextTimedPhase, subscription, now);
+            PhaseEvent nextPhaseEvent = (nextTimedPhase != null) ?
+                    PhaseEventData.getNextPhaseEvent(nextTimedPhase.getPhase().getName(), subscription, now, nextTimedPhase.getStartPhase()) :
+                        null;
             if (nextPhaseEvent != null) {
                 dao.createNextPhaseEvent(subscription.getId(), nextPhaseEvent);
             }
@@ -184,4 +198,5 @@ public class Engine implements EventListener, EntitlementService {
             log.error(String.format("Failed to insert next phase for subscription %s", subscription.getId()), e);
         }
     }
+
 }
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/BundleSqlDao.java b/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/BundleSqlDao.java
index 8ce78bb..d2b9f11 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/BundleSqlDao.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/BundleSqlDao.java
@@ -46,6 +46,9 @@ public interface BundleSqlDao extends Transactional<BundleSqlDao>, CloseMe, Tran
     @SqlUpdate
     public void insertBundle(@Bind(binder = SubscriptionBundleBinder.class) SubscriptionBundleData bundle);
 
+    @SqlUpdate
+    public void removeBundle(@Bind("id") String id);
+
     @SqlQuery
     @Mapper(ISubscriptionBundleSqlMapper.class)
     public SubscriptionBundle getBundleFromId(@Bind("id") String id);
@@ -58,7 +61,6 @@ public interface BundleSqlDao extends Transactional<BundleSqlDao>, CloseMe, Tran
     @Mapper(ISubscriptionBundleSqlMapper.class)
     public List<SubscriptionBundle> getBundleFromAccount(@Bind("account_id") String accountId);
 
-
     public static class SubscriptionBundleBinder implements Binder<Bind, SubscriptionBundleData> {
 
         private Date getDate(DateTime dateTime) {
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/EntitlementDao.java b/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/EntitlementDao.java
index 6c87a40..b118110 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/EntitlementDao.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/EntitlementDao.java
@@ -16,12 +16,12 @@
 
 package com.ning.billing.entitlement.engine.dao;
 
+import com.ning.billing.entitlement.api.migration.AccountMigrationData;
 import com.ning.billing.entitlement.api.user.Subscription;
 import com.ning.billing.entitlement.api.user.SubscriptionBundle;
 import com.ning.billing.entitlement.api.user.SubscriptionBundleData;
 import com.ning.billing.entitlement.api.user.SubscriptionData;
 import com.ning.billing.entitlement.events.EntitlementEvent;
-
 import java.util.Collection;
 import java.util.List;
 import java.util.UUID;
@@ -32,6 +32,8 @@ public interface EntitlementDao {
     // Bundle apis
     public List<SubscriptionBundle> getSubscriptionBundleForAccount(UUID accountId);
 
+    public SubscriptionBundle getSubscriptionBundleFromKey(String bundleKey);
+
     public SubscriptionBundle getSubscriptionBundleFromId(UUID bundleId);
 
     public SubscriptionBundle createSubscriptionBundle(SubscriptionBundleData bundle);
@@ -68,4 +70,8 @@ public interface EntitlementDao {
     public void uncancelSubscription(UUID subscriptionId, List<EntitlementEvent> uncancelEvents);
 
     public void changePlan(UUID subscriptionId, List<EntitlementEvent> changeEvents);
+
+    public void migrate(UUID acountId, AccountMigrationData data);
+
+    public void undoMigration(UUID accountId);
 }
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/EntitlementSqlDao.java b/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/EntitlementSqlDao.java
index e676af4..016f005 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/EntitlementSqlDao.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/EntitlementSqlDao.java
@@ -19,7 +19,14 @@ package com.ning.billing.entitlement.engine.dao;
 import com.google.inject.Inject;
 import com.ning.billing.catalog.api.ProductCategory;
 import com.ning.billing.config.EntitlementConfig;
-import com.ning.billing.entitlement.api.user.*;
+import com.ning.billing.entitlement.api.migration.AccountMigrationData;
+import com.ning.billing.entitlement.api.migration.AccountMigrationData.BundleMigrationData;
+import com.ning.billing.entitlement.api.migration.AccountMigrationData.SubscriptionMigrationData;
+import com.ning.billing.entitlement.api.user.Subscription;
+import com.ning.billing.entitlement.api.user.SubscriptionBundle;
+import com.ning.billing.entitlement.api.user.SubscriptionBundleData;
+import com.ning.billing.entitlement.api.user.SubscriptionData;
+import com.ning.billing.entitlement.api.user.SubscriptionFactory;
 import com.ning.billing.entitlement.api.user.SubscriptionFactory.SubscriptionBuilder;
 import com.ning.billing.entitlement.events.EntitlementEvent;
 import com.ning.billing.entitlement.events.EntitlementEvent.EventType;
@@ -60,6 +67,11 @@ public class EntitlementSqlDao implements EntitlementDao {
     }
 
     @Override
+    public SubscriptionBundle getSubscriptionBundleFromKey(String bundleKey) {
+        return bundlesDao.getBundleFromKey(bundleKey);
+    }
+
+    @Override
     public List<SubscriptionBundle> getSubscriptionBundleForAccount(
             UUID accountId) {
         return bundlesDao.getBundleFromAccount(accountId.toString());
@@ -334,4 +346,64 @@ public class EntitlementSqlDao implements EntitlementDao {
         }
         return result;
     }
+
+    @Override
+    public void migrate(final UUID accountId, final AccountMigrationData accountData) {
+
+        eventsDao.inTransaction(new Transaction<Void, EventSqlDao>() {
+
+            @Override
+            public Void inTransaction(EventSqlDao transEventDao,
+                    TransactionStatus status) throws Exception {
+
+                SubscriptionSqlDao transSubDao = transEventDao.become(SubscriptionSqlDao.class);
+                BundleSqlDao transBundleDao = transEventDao.become(BundleSqlDao.class);
+
+                // First get rid of any data from account
+                undoMigrationFromTransaction(accountId, transEventDao, transBundleDao, transSubDao);
+
+                for (BundleMigrationData curBundle : accountData.getData()) {
+                    SubscriptionBundleData bundleData = curBundle.getData();
+                    for (SubscriptionMigrationData curSubscription : curBundle.getSubscriptions()) {
+                        SubscriptionData subData = curSubscription.getData();
+                        for (EntitlementEvent curEvent : curSubscription.getInitialEvents()) {
+                            transEventDao.insertEvent(curEvent);
+                        }
+                        transSubDao.insertSubscription(subData);
+                    }
+                    transBundleDao.insertBundle(bundleData);
+                }
+                return null;
+            }
+        });
+    }
+
+    @Override
+    public void undoMigration(final UUID accountId) {
+
+        eventsDao.inTransaction(new Transaction<Void, EventSqlDao>() {
+
+            @Override
+            public Void inTransaction(EventSqlDao transEventDao,
+                    TransactionStatus status) throws Exception {
+
+                SubscriptionSqlDao transSubDao = transEventDao.become(SubscriptionSqlDao.class);
+                BundleSqlDao transBundleDao = transEventDao.become(BundleSqlDao.class);
+                undoMigrationFromTransaction(accountId, transEventDao, transBundleDao, transSubDao);
+                return null;
+            }
+        });
+    }
+
+    private void undoMigrationFromTransaction(final UUID accountId, EventSqlDao transEventDao, BundleSqlDao transBundleDao, SubscriptionSqlDao transSubDao) {
+        final List<SubscriptionBundle> bundles = transBundleDao.getBundleFromAccount(accountId.toString());
+        for (SubscriptionBundle curBundle : bundles) {
+            List<Subscription> subscriptions = transSubDao.getSubscriptionsFromBundleId(curBundle.getId().toString());
+            for (Subscription cur : subscriptions) {
+                eventsDao.removeEvents(cur.getId().toString());
+                transSubDao.removeSubscription(cur.getId().toString());
+            }
+            transBundleDao.removeBundle(curBundle.getId().toString());
+        }
+    }
 }
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/EventSqlDao.java b/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/EventSqlDao.java
index 704d722..de61217 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/EventSqlDao.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/EventSqlDao.java
@@ -39,7 +39,6 @@ import org.skife.jdbi.v2.sqlobject.mixins.Transactional;
 import org.skife.jdbi.v2.sqlobject.mixins.Transmogrifier;
 import org.skife.jdbi.v2.sqlobject.stringtemplate.ExternalizedSqlViaStringTemplate3;
 import org.skife.jdbi.v2.tweak.ResultSetMapper;
-
 import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.sql.Timestamp;
@@ -61,6 +60,9 @@ public interface EventSqlDao extends Transactional<EventSqlDao>, CloseMe, Transm
     public int claimEvent(@Bind("owner") String owner, @Bind("next_available") Date nextAvailable, @Bind("event_id") String eventId, @Bind("now") Date now);
 
     @SqlUpdate
+    public void removeEvents(@Bind("subscription_id") String subscriptionId);
+
+    @SqlUpdate
     public void clearEvent(@Bind("event_id") String eventId, @Bind("owner") String owner);
 
     @SqlUpdate
@@ -154,12 +156,9 @@ public interface EventSqlDao extends Transactional<EventSqlDao>, CloseMe, Transm
 
             EntitlementEvent result = null;
             if (eventType == EventType.PHASE) {
-                EventBaseBuilder<PhaseEventBuilder> realBase = (EventBaseBuilder<PhaseEventBuilder>) base;
-                result = new PhaseEventData(new PhaseEventBuilder(realBase).setPhaseName(phaseName));
+                result = new PhaseEventData(new PhaseEventBuilder(base).setPhaseName(phaseName));
             } else if (eventType == EventType.API_USER) {
-
-                EventBaseBuilder<ApiEventBuilder> realBase = (EventBaseBuilder<ApiEventBuilder>) base;
-                ApiEventBuilder builder = new ApiEventBuilder(realBase)
+                ApiEventBuilder builder = new ApiEventBuilder(base)
                     .setEventPlan(planName)
                     .setEventPlanPhase(phaseName)
                     .setEventPriceList(priceListName)
@@ -167,6 +166,8 @@ public interface EventSqlDao extends Transactional<EventSqlDao>, CloseMe, Transm
 
                 if (userType == ApiEventType.CREATE) {
                     result = new ApiEventCreate(builder);
+                } else if (userType == ApiEventType.MIGRATE_ENTITLEMENT) {
+                    result = new ApiEventMigrate(builder);
                 } else if (userType == ApiEventType.CHANGE) {
                     result = new ApiEventChange(builder);
                 } else if (userType == ApiEventType.CANCEL) {
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/SubscriptionSqlDao.java b/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/SubscriptionSqlDao.java
index 02f651d..d896ebe 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/SubscriptionSqlDao.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/SubscriptionSqlDao.java
@@ -49,6 +49,9 @@ public interface SubscriptionSqlDao extends Transactional<SubscriptionSqlDao>, C
     @SqlUpdate
     public void insertSubscription(@Bind(binder = ISubscriptionDaoBinder.class) SubscriptionData sub);
 
+    @SqlUpdate
+    public void removeSubscription(@Bind("id") String id);
+
     @SqlQuery
     @Mapper(ISubscriptionDaoSqlMapper.class)
     public Subscription getSubscriptionFromId(@Bind("id") String id);
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/events/EventBase.java b/entitlement/src/main/java/com/ning/billing/entitlement/events/EventBase.java
index d0db679..93dc9e1 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/events/EventBase.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/events/EventBase.java
@@ -37,7 +37,7 @@ public abstract class EventBase implements EntitlementEvent {
     private DateTime nextAvailableProcessingTime;
     private EventLifecycleState processingState;
 
-    public EventBase(EventBaseBuilder builder) {
+    public EventBase(EventBaseBuilder<?> builder) {
         this.uuid = builder.getUuid();
         this.subscriptionId = builder.getSubscriptionId();
         this.requestedDate = builder.getRequestedDate();
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/events/phase/PhaseEventBuilder.java b/entitlement/src/main/java/com/ning/billing/entitlement/events/phase/PhaseEventBuilder.java
index 00ac8e2..5847970 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/events/phase/PhaseEventBuilder.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/events/phase/PhaseEventBuilder.java
@@ -26,7 +26,7 @@ public class PhaseEventBuilder extends EventBaseBuilder<PhaseEventBuilder> {
         super();
     }
 
-    public PhaseEventBuilder(EventBaseBuilder<PhaseEventBuilder> base) {
+    public PhaseEventBuilder(EventBaseBuilder<?> base) {
         super(base);
     }
 
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/events/phase/PhaseEventData.java b/entitlement/src/main/java/com/ning/billing/entitlement/events/phase/PhaseEventData.java
index be0ab6c..9744363 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/events/phase/PhaseEventData.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/events/phase/PhaseEventData.java
@@ -56,15 +56,15 @@ public class PhaseEventData extends EventBase implements PhaseEvent {
                 + ", isActive()=" + isActive() + "]\n";
     }
 
-    public static final PhaseEvent getNextPhaseEvent(TimedPhase nextTimedPhase, SubscriptionData subscription, DateTime now) {
-        return (nextTimedPhase == null) ?
+    public static final PhaseEvent getNextPhaseEvent(String phaseName, SubscriptionData subscription, DateTime now, DateTime effectiveDate) {
+        return (phaseName == null) ?
                 null :
                     new PhaseEventData(new PhaseEventBuilder()
                         .setSubscriptionId(subscription.getId())
                         .setRequestedDate(now)
-                        .setEffectiveDate(nextTimedPhase.getStartPhase())
+                        .setEffectiveDate(effectiveDate)
                         .setProcessedDate(now)
                         .setActiveVersion(subscription.getActiveVersion())
-                        .setPhaseName(nextTimedPhase.getPhase().getName()));
+                        .setPhaseName(phaseName));
     }
 }
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/events/user/ApiEventBuilder.java b/entitlement/src/main/java/com/ning/billing/entitlement/events/user/ApiEventBuilder.java
index cd3857e..2ff026b 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/events/user/ApiEventBuilder.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/events/user/ApiEventBuilder.java
@@ -29,7 +29,7 @@ public class ApiEventBuilder extends EventBaseBuilder<ApiEventBuilder> {
         super();
     }
 
-    public ApiEventBuilder(EventBaseBuilder<ApiEventBuilder> base) {
+    public ApiEventBuilder(EventBaseBuilder<?> base) {
         super(base);
     }
 
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/events/user/ApiEventMigrate.java b/entitlement/src/main/java/com/ning/billing/entitlement/events/user/ApiEventMigrate.java
new file mode 100644
index 0000000..636a940
--- /dev/null
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/events/user/ApiEventMigrate.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.entitlement.events.user;
+
+public class ApiEventMigrate extends ApiEventBase {
+
+    public ApiEventMigrate(ApiEventBuilder builder) {
+        super(builder.setEventType(ApiEventType.MIGRATE_ENTITLEMENT));
+    }
+}
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/events/user/ApiEventType.java b/entitlement/src/main/java/com/ning/billing/entitlement/events/user/ApiEventType.java
index c28941e..b994899 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/events/user/ApiEventType.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/events/user/ApiEventType.java
@@ -20,6 +20,10 @@ import com.ning.billing.entitlement.api.user.SubscriptionTransition.Subscription
 
 
 public enum ApiEventType {
+    MIGRATE_ENTITLEMENT {
+        @Override
+        public SubscriptionTransitionType getSubscriptionTransitionType() { return SubscriptionTransitionType.MIGRATE_ENTITLEMENT; }
+    },
     CREATE {
         @Override
         public SubscriptionTransitionType getSubscriptionTransitionType() { return SubscriptionTransitionType.CREATE; }
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/glue/EntitlementModule.java b/entitlement/src/main/java/com/ning/billing/entitlement/glue/EntitlementModule.java
index 1a9fffc..ab04700 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/glue/EntitlementModule.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/glue/EntitlementModule.java
@@ -18,10 +18,13 @@ package com.ning.billing.entitlement.glue;
 
 import com.google.inject.AbstractModule;
 import com.ning.billing.config.EntitlementConfig;
+import com.ning.billing.entitlement.alignment.MigrationPlanAligner;
 import com.ning.billing.entitlement.alignment.PlanAligner;
 import com.ning.billing.entitlement.api.EntitlementService;
 import com.ning.billing.entitlement.api.billing.DefaultEntitlementBillingApi;
 import com.ning.billing.entitlement.api.billing.EntitlementBillingApi;
+import com.ning.billing.entitlement.api.migration.DefaultEntitlementMigrationApi;
+import com.ning.billing.entitlement.api.migration.EntitlementMigrationApi;
 import com.ning.billing.entitlement.api.test.DefaultEntitlementTestApi;
 import com.ning.billing.entitlement.api.test.EntitlementTestApi;
 import com.ning.billing.entitlement.api.user.DefaultEntitlementUserApi;
@@ -63,9 +66,11 @@ public class EntitlementModule extends AbstractModule {
         bind(EntitlementService.class).to(Engine.class).asEagerSingleton();
         bind(Engine.class).asEagerSingleton();
         bind(PlanAligner.class).asEagerSingleton();
+        bind(MigrationPlanAligner.class).asEagerSingleton();
         bind(EntitlementTestApi.class).to(DefaultEntitlementTestApi.class).asEagerSingleton();
         bind(EntitlementUserApi.class).to(DefaultEntitlementUserApi.class).asEagerSingleton();
         bind(EntitlementBillingApi.class).to(DefaultEntitlementBillingApi.class).asEagerSingleton();
+        bind(EntitlementMigrationApi.class).to(DefaultEntitlementMigrationApi.class).asEagerSingleton();
     }
 
     @Override
diff --git a/entitlement/src/main/resources/com/ning/billing/entitlement/ddl.sql b/entitlement/src/main/resources/com/ning/billing/entitlement/ddl.sql
index f79666e..4540ad8 100644
--- a/entitlement/src/main/resources/com/ning/billing/entitlement/ddl.sql
+++ b/entitlement/src/main/resources/com/ning/billing/entitlement/ddl.sql
@@ -3,7 +3,7 @@ CREATE TABLE events (
     id int(11) unsigned NOT NULL AUTO_INCREMENT,
     event_id char(36) NOT NULL,
     event_type varchar(9) NOT NULL,
-    user_type varchar(10) DEFAULT NULL,
+    user_type varchar(25) DEFAULT NULL,
     created_dt datetime NOT NULL,
     updated_dt datetime NOT NULL,
     requested_dt datetime NOT NULL,
diff --git a/entitlement/src/main/resources/com/ning/billing/entitlement/engine/dao/BundleSqlDao.sql.stg b/entitlement/src/main/resources/com/ning/billing/entitlement/engine/dao/BundleSqlDao.sql.stg
index cb15e9b..d9f26c2 100644
--- a/entitlement/src/main/resources/com/ning/billing/entitlement/engine/dao/BundleSqlDao.sql.stg
+++ b/entitlement/src/main/resources/com/ning/billing/entitlement/engine/dao/BundleSqlDao.sql.stg
@@ -14,6 +14,12 @@ insertBundle() ::= <<
     );
 >>
 
+removeBundle(id) ::= <<
+    delete from bundles
+    where
+      id = :id
+    ;
+>>
 
 getBundleFromId(id) ::= <<
     select
diff --git a/entitlement/src/main/resources/com/ning/billing/entitlement/engine/dao/EventSqlDao.sql.stg b/entitlement/src/main/resources/com/ning/billing/entitlement/engine/dao/EventSqlDao.sql.stg
index a3b677a..31e9eaf 100644
--- a/entitlement/src/main/resources/com/ning/billing/entitlement/engine/dao/EventSqlDao.sql.stg
+++ b/entitlement/src/main/resources/com/ning/billing/entitlement/engine/dao/EventSqlDao.sql.stg
@@ -96,6 +96,13 @@ insertEvent() ::= <<
     );   
 >>
 
+removeEvents(subscription_id) ::= <<
+    delete from events
+      where
+    subscription_id = :subscription_id
+    ;
+>>
+
 insertClaimedHistory(sequence_id, owner_id, hostname, claimed_dt, event_id) ::= <<
     insert into claimed_events (
         sequence_id
diff --git a/entitlement/src/main/resources/com/ning/billing/entitlement/engine/dao/SubscriptionSqlDao.sql.stg b/entitlement/src/main/resources/com/ning/billing/entitlement/engine/dao/SubscriptionSqlDao.sql.stg
index 094bdf1..bb7400b 100644
--- a/entitlement/src/main/resources/com/ning/billing/entitlement/engine/dao/SubscriptionSqlDao.sql.stg
+++ b/entitlement/src/main/resources/com/ning/billing/entitlement/engine/dao/SubscriptionSqlDao.sql.stg
@@ -22,6 +22,12 @@ insertSubscription() ::= <<
     );
 >>
 
+removeSubscription(id) ::= <<
+    delete from subscriptions
+     where id = :id
+    ;    
+>>
+
 getSubscriptionFromId(id) ::= <<
     select
         id
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/ApiTestListener.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/ApiTestListener.java
index f76f775..6bee471 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/ApiTestListener.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/ApiTestListener.java
@@ -36,6 +36,7 @@ public class ApiTestListener {
     private volatile boolean completed;
 
     public enum NextEvent {
+        MIGRATE_ENTITLEMENT,
         CREATE,
         CHANGE,
         CANCEL,
@@ -52,6 +53,9 @@ public class ApiTestListener {
     @Subscribe
     public void handleEntitlementEvent(SubscriptionTransition event) {
         switch (event.getTransitionType()) {
+        case MIGRATE_ENTITLEMENT:
+            subscriptionMigrated(event);
+            break;
         case CREATE:
             subscriptionCreated(event);
             break;
@@ -135,6 +139,12 @@ public class ApiTestListener {
     }
 
 
+    public void subscriptionMigrated(SubscriptionTransition migrated) {
+        log.debug("-> Got event MIGRATED");
+        assertEqualsNicely(NextEvent.MIGRATE_ENTITLEMENT);
+        notifyIfStackEmpty();
+    }
+
     public void subscriptionCreated(SubscriptionTransition created) {
         log.debug("-> Got event CREATED");
         assertEqualsNicely(NextEvent.CREATE);
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/migration/TestMigration.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/migration/TestMigration.java
new file mode 100644
index 0000000..271de3c
--- /dev/null
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/migration/TestMigration.java
@@ -0,0 +1,371 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.entitlement.api.migration;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Stage;
+import com.ning.billing.catalog.api.BillingPeriod;
+import com.ning.billing.catalog.api.Duration;
+import com.ning.billing.catalog.api.PhaseType;
+import com.ning.billing.catalog.api.PlanPhaseSpecifier;
+import com.ning.billing.catalog.api.PriceListSet;
+import com.ning.billing.catalog.api.ProductCategory;
+import com.ning.billing.entitlement.api.TestApiBase;
+import com.ning.billing.entitlement.api.ApiTestListener.NextEvent;
+import com.ning.billing.entitlement.api.migration.EntitlementMigrationApiException;
+import com.ning.billing.entitlement.api.migration.EntitlementMigrationApi.EntitlementAccountMigration;
+import com.ning.billing.entitlement.api.migration.EntitlementMigrationApi.EntitlementBundleMigration;
+import com.ning.billing.entitlement.api.migration.EntitlementMigrationApi.EntitlementSubscriptionMigration;
+import com.ning.billing.entitlement.api.migration.EntitlementMigrationApi.EntitlementSubscriptionMigrationCase;
+import com.ning.billing.entitlement.api.user.Subscription;
+import com.ning.billing.entitlement.api.user.SubscriptionBundle;
+import com.ning.billing.entitlement.api.user.Subscription.SubscriptionState;
+import com.ning.billing.entitlement.glue.MockEngineModuleSql;
+
+public abstract class TestMigration extends TestApiBase {
+
+
+    public void testSingleBasePlan() {
+
+        try {
+            DateTime beforeMigration = clock.getUTCNow();
+            EntitlementAccountMigration toBeMigrated = createAccountWithRegularBasePlan();
+            DateTime afterMigration = clock.getUTCNow();
+
+            testListener.pushExpectedEvent(NextEvent.MIGRATE_ENTITLEMENT);
+            migrationApi.migrate(toBeMigrated);
+            assertTrue(testListener.isCompleted(5000));
+
+            List<SubscriptionBundle> bundles = entitlementApi.getBundlesForAccount(toBeMigrated.getAccountKey());
+            assertEquals(bundles.size(), 1);
+            SubscriptionBundle bundle = bundles.get(0);
+
+            List<Subscription> subscriptions = entitlementApi.getSubscriptionsForBundle(bundle.getId());
+            assertEquals(subscriptions.size(), 1);
+            Subscription subscription = subscriptions.get(0);
+            assertDateWithin(subscription.getStartDate(), beforeMigration, afterMigration);
+            assertEquals(subscription.getEndDate(), null);
+            assertEquals(subscription.getCurrentPriceList(), PriceListSet.DEFAULT_PRICELIST_NAME);
+            assertEquals(subscription.getCurrentPhase().getPhaseType(), PhaseType.EVERGREEN);
+            assertEquals(subscription.getState(), SubscriptionState.ACTIVE);
+            assertEquals(subscription.getCurrentPlan().getName(), "assault-rifle-annual");
+        } catch (EntitlementMigrationApiException e) {
+            Assert.fail("", e);
+        }
+    }
+
+
+    public void testSingleBasePlanFutureCancelled() {
+
+        try {
+
+            DateTime beforeMigration = clock.getUTCNow();
+            EntitlementAccountMigration toBeMigrated = createAccountWithRegularBasePlanFutreCancelled();
+            DateTime afterMigration = clock.getUTCNow();
+
+            testListener.pushExpectedEvent(NextEvent.MIGRATE_ENTITLEMENT);
+            migrationApi.migrate(toBeMigrated);
+            assertTrue(testListener.isCompleted(5000));
+
+            List<SubscriptionBundle> bundles = entitlementApi.getBundlesForAccount(toBeMigrated.getAccountKey());
+            assertEquals(bundles.size(), 1);
+            SubscriptionBundle bundle = bundles.get(0);
+            //assertEquals(bundle.getStartDate(), effectiveDate);
+
+            List<Subscription> subscriptions = entitlementApi.getSubscriptionsForBundle(bundle.getId());
+            assertEquals(subscriptions.size(), 1);
+            Subscription subscription = subscriptions.get(0);
+            assertDateWithin(subscription.getStartDate(), beforeMigration, afterMigration);
+            assertEquals(subscription.getCurrentPriceList(), PriceListSet.DEFAULT_PRICELIST_NAME);
+            assertEquals(subscription.getCurrentPhase().getPhaseType(), PhaseType.EVERGREEN);
+            assertEquals(subscription.getState(), SubscriptionState.ACTIVE);
+            assertEquals(subscription.getCurrentPlan().getName(), "assault-rifle-annual");
+
+            testListener.pushExpectedEvent(NextEvent.CANCEL);
+            Duration oneYear = getDurationYear(1);
+            clock.setDeltaFromReality(oneYear, 0);
+            assertTrue(testListener.isCompleted(5000));
+
+            assertDateWithin(subscription.getStartDate(), beforeMigration, afterMigration);
+            assertNotNull(subscription.getEndDate());
+            assertTrue(subscription.getEndDate().isAfterNow());
+            assertEquals(subscription.getCurrentPriceList(), PriceListSet.DEFAULT_PRICELIST_NAME);
+            assertEquals(subscription.getCurrentPhase(), null);
+            assertEquals(subscription.getState(), SubscriptionState.CANCELLED);
+            assertNull(subscription.getCurrentPlan());
+
+        } catch (EntitlementMigrationApiException e) {
+            Assert.fail("", e);
+        }
+    }
+
+    public void testSingleBasePlanWithPendingPhase() {
+
+        try {
+            DateTime beforeMigration = clock.getUTCNow();
+            final DateTime trialDate = clock.getUTCNow().minusDays(10);
+            EntitlementAccountMigration toBeMigrated = createAccountFuturePendingPhase(trialDate);
+            DateTime afterMigration = clock.getUTCNow();
+
+            testListener.pushExpectedEvent(NextEvent.MIGRATE_ENTITLEMENT);
+            migrationApi.migrate(toBeMigrated);
+            assertTrue(testListener.isCompleted(5000));
+
+            List<SubscriptionBundle> bundles = entitlementApi.getBundlesForAccount(toBeMigrated.getAccountKey());
+            assertEquals(bundles.size(), 1);
+            SubscriptionBundle bundle = bundles.get(0);
+
+            List<Subscription> subscriptions = entitlementApi.getSubscriptionsForBundle(bundle.getId());
+            assertEquals(subscriptions.size(), 1);
+            Subscription subscription = subscriptions.get(0);
+
+            assertEquals(subscription.getStartDate(), trialDate);
+            assertEquals(subscription.getEndDate(), null);
+            assertEquals(subscription.getCurrentPriceList(), PriceListSet.DEFAULT_PRICELIST_NAME);
+            assertEquals(subscription.getCurrentPhase().getPhaseType(), PhaseType.TRIAL);
+            assertEquals(subscription.getState(), SubscriptionState.ACTIVE);
+            assertEquals(subscription.getCurrentPlan().getName(), "assault-rifle-monthly");
+
+            testListener.pushExpectedEvent(NextEvent.PHASE);
+            Duration thirtyDays = getDurationDay(30);
+            clock.setDeltaFromReality(thirtyDays, 0);
+            assertTrue(testListener.isCompleted(5000));
+
+            assertEquals(subscription.getStartDate(), trialDate);
+            assertEquals(subscription.getEndDate(), null);
+            assertEquals(subscription.getCurrentPriceList(), PriceListSet.DEFAULT_PRICELIST_NAME);
+            assertEquals(subscription.getCurrentPhase().getPhaseType(), PhaseType.EVERGREEN);
+            assertEquals(subscription.getState(), SubscriptionState.ACTIVE);
+            assertEquals(subscription.getCurrentPlan().getName(), "assault-rifle-monthly");
+            assertEquals(subscription.getCurrentPhase().getName(), "assault-rifle-monthly-evergreen");
+
+        } catch (EntitlementMigrationApiException e) {
+            Assert.fail("", e);
+        }
+    }
+
+
+    public void testSingleBasePlanWithPendingChange() {
+
+        try {
+            DateTime beforeMigration = clock.getUTCNow();
+            EntitlementAccountMigration toBeMigrated = createAccountFuturePendingChange();
+            DateTime afterMigration = clock.getUTCNow();
+
+            testListener.pushExpectedEvent(NextEvent.MIGRATE_ENTITLEMENT);
+            migrationApi.migrate(toBeMigrated);
+            assertTrue(testListener.isCompleted(5000));
+
+            List<SubscriptionBundle> bundles = entitlementApi.getBundlesForAccount(toBeMigrated.getAccountKey());
+            assertEquals(bundles.size(), 1);
+            SubscriptionBundle bundle = bundles.get(0);
+
+            List<Subscription> subscriptions = entitlementApi.getSubscriptionsForBundle(bundle.getId());
+            assertEquals(subscriptions.size(), 1);
+            Subscription subscription = subscriptions.get(0);
+            assertDateWithin(subscription.getStartDate(), beforeMigration, afterMigration);
+            assertEquals(subscription.getEndDate(), null);
+            assertEquals(subscription.getCurrentPriceList(), PriceListSet.DEFAULT_PRICELIST_NAME);
+            assertEquals(subscription.getCurrentPhase().getPhaseType(), PhaseType.EVERGREEN);
+            assertEquals(subscription.getState(), SubscriptionState.ACTIVE);
+            assertEquals(subscription.getCurrentPlan().getName(), "assault-rifle-monthly");
+
+            testListener.pushExpectedEvent(NextEvent.CHANGE);
+            Duration oneMonth = getDurationMonth(1);
+            clock.setDeltaFromReality(oneMonth, 0);
+            assertTrue(testListener.isCompleted(5000));
+
+            assertDateWithin(subscription.getStartDate(), beforeMigration, afterMigration);
+            assertEquals(subscription.getEndDate(), null);
+            assertEquals(subscription.getCurrentPriceList(), PriceListSet.DEFAULT_PRICELIST_NAME);
+
+            assertEquals(subscription.getCurrentPhase().getPhaseType(), PhaseType.EVERGREEN);
+            assertEquals(subscription.getState(), SubscriptionState.ACTIVE);
+            assertEquals(subscription.getCurrentPlan().getName(), "shotgun-annual");
+
+        } catch (EntitlementMigrationApiException e) {
+            Assert.fail("", e);
+        }
+    }
+
+
+    private EntitlementAccountMigration createAccountWithSingleBasePlan(final List<EntitlementSubscriptionMigrationCase> cases) {
+
+        return new EntitlementAccountMigration() {
+
+            private final UUID accountId = UUID.randomUUID();
+
+            @Override
+            public EntitlementBundleMigration[] getBundles() {
+                List<EntitlementBundleMigration> bundles = new ArrayList<EntitlementBundleMigration>();
+                EntitlementBundleMigration bundle0 = new EntitlementBundleMigration() {
+
+                    @Override
+                    public EntitlementSubscriptionMigration[] getSubscriptions() {
+                        EntitlementSubscriptionMigration subscription = new EntitlementSubscriptionMigration() {
+                            @Override
+                            public EntitlementSubscriptionMigrationCase[] getSubscriptionCases() {
+                                return cases.toArray(new EntitlementSubscriptionMigrationCase[cases.size()]);
+                            }
+                            @Override
+                            public ProductCategory getCategory() {
+                                return ProductCategory.BASE;
+                            }
+                        };
+                        EntitlementSubscriptionMigration[] result = new EntitlementSubscriptionMigration[1];
+                        result[0] = subscription;
+                        return result;
+                    }
+                    @Override
+                    public String getBundleKey() {
+                        return "12345";
+                    }
+                };
+                bundles.add(bundle0);
+                return bundles.toArray(new EntitlementBundleMigration[bundles.size()]);
+            }
+
+            @Override
+            public UUID getAccountKey() {
+                return accountId;
+            }
+        };
+    }
+
+    private EntitlementAccountMigration createAccountWithRegularBasePlan() {
+        List<EntitlementSubscriptionMigrationCase> cases = new LinkedList<EntitlementSubscriptionMigrationCase>();
+        cases.add(new EntitlementSubscriptionMigrationCase() {
+            @Override
+            public PlanPhaseSpecifier getPlanPhaseSpecifer() {
+                return new PlanPhaseSpecifier("Assault-Rifle", ProductCategory.BASE, BillingPeriod.ANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.EVERGREEN);
+            }
+            @Override
+            public DateTime getEffectiveDate() {
+                return clock.getUTCNow().minusMonths(3);
+            }
+            @Override
+            public DateTime getCancelledDate() {
+                return null;
+            }
+        });
+        return createAccountWithSingleBasePlan(cases);
+    }
+
+    private EntitlementAccountMigration createAccountWithRegularBasePlanFutreCancelled() {
+        List<EntitlementSubscriptionMigrationCase> cases = new LinkedList<EntitlementSubscriptionMigrationCase>();
+        final DateTime effectiveDate = clock.getUTCNow().minusMonths(3);
+        cases.add(new EntitlementSubscriptionMigrationCase() {
+            @Override
+            public PlanPhaseSpecifier getPlanPhaseSpecifer() {
+                return new PlanPhaseSpecifier("Assault-Rifle", ProductCategory.BASE, BillingPeriod.ANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.EVERGREEN);
+            }
+            @Override
+            public DateTime getEffectiveDate() {
+                return effectiveDate;
+            }
+            @Override
+            public DateTime getCancelledDate() {
+                return effectiveDate.plusYears(1);
+            }
+        });
+        return createAccountWithSingleBasePlan(cases);
+    }
+
+
+    private EntitlementAccountMigration createAccountFuturePendingPhase(final DateTime trialDate) {
+        List<EntitlementSubscriptionMigrationCase> cases = new LinkedList<EntitlementSubscriptionMigrationCase>();
+        cases.add(new EntitlementSubscriptionMigrationCase() {
+            @Override
+            public PlanPhaseSpecifier getPlanPhaseSpecifer() {
+                return new PlanPhaseSpecifier("Assault-Rifle", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.TRIAL);
+            }
+            @Override
+            public DateTime getEffectiveDate() {
+                return trialDate;
+            }
+            @Override
+            public DateTime getCancelledDate() {
+                return trialDate.plusDays(30);
+            }
+        });
+        cases.add(new EntitlementSubscriptionMigrationCase() {
+            @Override
+            public PlanPhaseSpecifier getPlanPhaseSpecifer() {
+                return new PlanPhaseSpecifier("Assault-Rifle", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.EVERGREEN);
+            }
+            @Override
+            public DateTime getEffectiveDate() {
+                return trialDate.plusDays(30);
+            }
+            @Override
+            public DateTime getCancelledDate() {
+                return null;
+            }
+        });
+        return createAccountWithSingleBasePlan(cases);
+    }
+
+    private EntitlementAccountMigration createAccountFuturePendingChange() {
+        List<EntitlementSubscriptionMigrationCase> cases = new LinkedList<EntitlementSubscriptionMigrationCase>();
+        final DateTime effectiveDate = clock.getUTCNow().minusDays(10);
+        cases.add(new EntitlementSubscriptionMigrationCase() {
+            @Override
+            public PlanPhaseSpecifier getPlanPhaseSpecifer() {
+                return new PlanPhaseSpecifier("Assault-Rifle", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.EVERGREEN);
+            }
+            @Override
+            public DateTime getEffectiveDate() {
+                return effectiveDate;
+            }
+            @Override
+            public DateTime getCancelledDate() {
+                return effectiveDate.plusMonths(1);
+            }
+        });
+        cases.add(new EntitlementSubscriptionMigrationCase() {
+            @Override
+            public PlanPhaseSpecifier getPlanPhaseSpecifer() {
+                return new PlanPhaseSpecifier("Shotgun", ProductCategory.BASE, BillingPeriod.ANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.EVERGREEN);
+            }
+            @Override
+            public DateTime getEffectiveDate() {
+                return effectiveDate.plusMonths(1).plusDays(1);
+            }
+            @Override
+            public DateTime getCancelledDate() {
+                return null;
+            }
+        });
+        return createAccountWithSingleBasePlan(cases);
+    }
+
+}
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/migration/TestMigrationMemory.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/migration/TestMigrationMemory.java
new file mode 100644
index 0000000..05d6fb5
--- /dev/null
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/migration/TestMigrationMemory.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.entitlement.api.migration;
+
+import org.testng.annotations.Test;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Stage;
+import com.ning.billing.entitlement.glue.MockEngineModuleMemory;
+
+public class TestMigrationMemory extends TestMigration {
+    @Override
+    protected Injector getInjector() {
+        return Guice.createInjector(Stage.DEVELOPMENT, new MockEngineModuleMemory());
+    }
+
+    @Override
+    @Test(enabled=true, groups="fast")
+    public void testSingleBasePlan() {
+        super.testSingleBasePlan();
+    }
+
+    @Override
+    @Test(enabled=true, groups="fast")
+    public void testSingleBasePlanFutureCancelled() {
+        super.testSingleBasePlanFutureCancelled();
+    }
+
+    @Override
+    @Test(enabled=true, groups="fast")
+    public void testSingleBasePlanWithPendingPhase() {
+        super.testSingleBasePlanWithPendingPhase();
+    }
+
+    @Override
+    @Test(enabled=true, groups="fast")
+    public void testSingleBasePlanWithPendingChange() {
+        super.testSingleBasePlanWithPendingChange();
+    }
+}
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/migration/TestMigrationSql.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/migration/TestMigrationSql.java
new file mode 100644
index 0000000..b3eb168
--- /dev/null
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/migration/TestMigrationSql.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.entitlement.api.migration;
+
+import org.testng.annotations.Test;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Stage;
+import com.ning.billing.entitlement.glue.MockEngineModuleSql;
+
+public class TestMigrationSql extends TestMigration {
+
+    @Override
+    protected Injector getInjector() {
+        return Guice.createInjector(Stage.DEVELOPMENT, new MockEngineModuleSql());
+    }
+
+    @Override
+    @Test(enabled=true, groups="sql")
+    public void testSingleBasePlan() {
+        super.testSingleBasePlan();
+    }
+
+    @Override
+    @Test(enabled=true, groups="sql")
+    public void testSingleBasePlanFutureCancelled() {
+        super.testSingleBasePlanFutureCancelled();
+    }
+
+    @Override
+    @Test(enabled=true, groups="sql")
+    public void testSingleBasePlanWithPendingPhase() {
+        super.testSingleBasePlanWithPendingPhase();
+    }
+
+    @Override
+    @Test(enabled=true, groups="sql")
+    public void testSingleBasePlanWithPendingChange() {
+        super.testSingleBasePlanWithPendingChange();
+    }
+}
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiCancel.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiCancel.java
index 72e5556..3326bc8 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiCancel.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiCancel.java
@@ -16,19 +16,29 @@
 
 package com.ning.billing.entitlement.api.user;
 
-import com.ning.billing.catalog.api.*;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.assertFalse;
+
 import com.ning.billing.entitlement.api.ApiTestListener.NextEvent;
 import com.ning.billing.util.clock.DefaultClock;
 import org.joda.time.DateTime;
 import org.testng.Assert;
 
-import java.util.List;
 
-import static org.testng.Assert.*;
+import com.ning.billing.catalog.api.BillingPeriod;
+import com.ning.billing.catalog.api.Duration;
+import com.ning.billing.catalog.api.Plan;
+import com.ning.billing.catalog.api.PlanPhase;
+import com.ning.billing.catalog.api.PriceListSet;
+import com.ning.billing.catalog.api.PhaseType;
+import com.ning.billing.entitlement.api.TestApiBase;
+import java.util.List;
 
-public abstract class TestUserApiCancel extends TestUserApiBase {
+public abstract class TestUserApiCancel extends TestApiBase {
 
-    protected void testCancelSubscriptionIMMReal() {
+    protected void testCancelSubscriptionIMM() {
 
         log.info("Starting testCancelSubscriptionIMM");
 
@@ -70,7 +80,7 @@ public abstract class TestUserApiCancel extends TestUserApiBase {
     }
 
 
-    protected void testCancelSubscriptionEOTWithChargeThroughDateReal() {
+    protected void testCancelSubscriptionEOTWithChargeThroughDate() {
         log.info("Starting testCancelSubscriptionEOTWithChargeThroughDate");
 
         try {
@@ -122,7 +132,7 @@ public abstract class TestUserApiCancel extends TestUserApiBase {
     }
 
 
-    protected void testCancelSubscriptionEOTWithNoChargeThroughDateReal() {
+    protected void testCancelSubscriptionEOTWithNoChargeThroughDate() {
 
         log.info("Starting testCancelSubscriptionEOTWithNoChargeThroughDate");
 
@@ -166,7 +176,7 @@ public abstract class TestUserApiCancel extends TestUserApiBase {
     // Similar test to testCancelSubscriptionEOTWithChargeThroughDate except we uncancel and check things
     // are as they used to be and we can move forward without hitting cancellation
     //
-    protected void testUncancelReal() {
+    protected void testUncancel() {
 
         log.info("Starting testUncancel");
 
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiCancelMemory.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiCancelMemory.java
index 962a6ef..630d925 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiCancelMemory.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiCancelMemory.java
@@ -30,23 +30,27 @@ public class TestUserApiCancelMemory extends TestUserApiCancel {
         return Guice.createInjector(Stage.PRODUCTION, new MockEngineModuleMemory());
     }
 
+    @Override
     @Test(enabled=true, groups={"fast"})
     public void testCancelSubscriptionIMM() {
-        invokeRealMethod(this);
+        super.testCancelSubscriptionIMM();
     }
 
+    @Override
     @Test(enabled=true, groups={"fast"})
     public void testCancelSubscriptionEOTWithChargeThroughDate() {
-        invokeRealMethod(this);
+        super.testCancelSubscriptionEOTWithChargeThroughDate();
     }
 
+    @Override
     @Test(enabled=true, groups={"fast"})
     public void testCancelSubscriptionEOTWithNoChargeThroughDate() {
-        invokeRealMethod(this);
+        super.testCancelSubscriptionEOTWithNoChargeThroughDate();
     }
 
+    @Override
     @Test(enabled=true, groups={"fast"})
     public void testUncancel() {
-        invokeRealMethod(this);
+        super.testUncancel();
     }
 }
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiCancelSql.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiCancelSql.java
index 682fa06..187c080 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiCancelSql.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiCancelSql.java
@@ -47,24 +47,28 @@ public class TestUserApiCancelSql extends TestUserApiCancel {
         }
     }
 
+    @Override
     @Test(enabled=true, groups={"sql"})
     public void testCancelSubscriptionIMM() {
-        invokeRealMethod(this);
+        super.testCancelSubscriptionIMM();
     }
 
+    @Override
     @Test(enabled=true, groups={"sql"})
     public void testCancelSubscriptionEOTWithChargeThroughDate() {
-        invokeRealMethod(this);
+        super.testCancelSubscriptionEOTWithChargeThroughDate();
     }
 
+    @Override
     @Test(enabled=true, groups={"sql"})
     public void testCancelSubscriptionEOTWithNoChargeThroughDate() {
-        invokeRealMethod(this);
+        super.testCancelSubscriptionEOTWithNoChargeThroughDate();
     }
 
+    @Override
     @Test(enabled=true, groups={"sql"})
     public void testUncancel() {
-        invokeRealMethod(this);
+        super.testUncancel();
     }
 
 }
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiChangePlan.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiChangePlan.java
index 76f6d44..dc6567f 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiChangePlan.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiChangePlan.java
@@ -16,20 +16,32 @@
 
 package com.ning.billing.entitlement.api.user;
 
-import com.ning.billing.catalog.api.*;
-import com.ning.billing.entitlement.api.ApiTestListener.NextEvent;
-import com.ning.billing.entitlement.events.EntitlementEvent;
-import com.ning.billing.entitlement.events.user.ApiEvent;
-import com.ning.billing.util.clock.DefaultClock;
-import org.joda.time.DateTime;
-import org.testng.Assert;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
 
 import java.util.ArrayList;
 import java.util.List;
 
-import static org.testng.Assert.*;
+import org.joda.time.DateTime;
+import org.testng.Assert;
+
+import com.ning.billing.catalog.api.BillingPeriod;
+import com.ning.billing.catalog.api.Duration;
+import com.ning.billing.catalog.api.Plan;
+import com.ning.billing.catalog.api.PlanPhase;
+import com.ning.billing.catalog.api.PriceListSet;
+import com.ning.billing.catalog.api.PhaseType;
+import com.ning.billing.catalog.api.ProductCategory;
+import com.ning.billing.entitlement.api.TestApiBase;
+
+import com.ning.billing.entitlement.api.ApiTestListener.NextEvent;
+import com.ning.billing.entitlement.events.EntitlementEvent;
+import com.ning.billing.entitlement.events.user.ApiEvent;
+import com.ning.billing.util.clock.DefaultClock;
 
-public abstract class TestUserApiChangePlan extends TestUserApiBase {
+public abstract class TestUserApiChangePlan extends TestApiBase {
 
 
 
@@ -49,7 +61,7 @@ public abstract class TestUserApiChangePlan extends TestUserApiBase {
 
 
 
-    protected void testChangePlanBundleAlignEOTWithNoChargeThroughDateReal() {
+    protected void testChangePlanBundleAlignEOTWithNoChargeThroughDate() {
         tChangePlanBundleAlignEOTWithNoChargeThroughDate("Shotgun", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, "Pistol", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME);
     }
 
@@ -88,7 +100,7 @@ public abstract class TestUserApiChangePlan extends TestUserApiBase {
     }
 
 
-    protected void testChangePlanBundleAlignEOTWithChargeThroughDateReal() {
+    protected void testChangePlanBundleAlignEOTWithChargeThroughDate() {
         testChangePlanBundleAlignEOTWithChargeThroughDate("Shotgun", BillingPeriod.ANNUAL, "gunclubDiscount", "Pistol", BillingPeriod.ANNUAL, "gunclubDiscount");
     }
 
@@ -153,7 +165,7 @@ public abstract class TestUserApiChangePlan extends TestUserApiBase {
     }
 
 
-    protected void testChangePlanBundleAlignIMMReal() {
+    protected void testChangePlanBundleAlignIMM() {
         tChangePlanBundleAlignIMM("Shotgun", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, "Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME);
     }
 
@@ -195,7 +207,7 @@ public abstract class TestUserApiChangePlan extends TestUserApiBase {
     }
 
 
-    protected void testChangePlanChangePlanAlignEOTWithChargeThroughDateReal() {
+    protected void testChangePlanChangePlanAlignEOTWithChargeThroughDate() {
         tChangePlanChangePlanAlignEOTWithChargeThroughDate("Shotgun", BillingPeriod.ANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME, "Assault-Rifle", BillingPeriod.ANNUAL, "rescue");
     }
 
@@ -276,7 +288,7 @@ public abstract class TestUserApiChangePlan extends TestUserApiBase {
         }
     }
 
-    protected void testMultipleChangeLastIMMReal() {
+    protected void testMultipleChangeLastIMM() {
 
         try {
             SubscriptionData subscription = createSubscription("Assault-Rifle", BillingPeriod.MONTHLY, "gunclubDiscount");
@@ -323,7 +335,7 @@ public abstract class TestUserApiChangePlan extends TestUserApiBase {
         }
     }
 
-    protected void testMultipleChangeLastEOTReal() {
+    protected void testMultipleChangeLastEOT() {
 
         try {
 
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiChangePlanMemory.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiChangePlanMemory.java
index 173239c..aecaaac 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiChangePlanMemory.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiChangePlanMemory.java
@@ -30,35 +30,40 @@ public class TestUserApiChangePlanMemory extends TestUserApiChangePlan {
     }
 
 
+    @Override
     @Test(enabled=true, groups={"fast"})
     public void testChangePlanBundleAlignEOTWithNoChargeThroughDate() {
-        invokeRealMethod(this);
+         super.testChangePlanBundleAlignEOTWithNoChargeThroughDate();
     }
 
+    @Override
     @Test(enabled=true, groups={"fast"})
     public void testChangePlanBundleAlignEOTWithChargeThroughDate() {
-        invokeRealMethod(this);
+        super.testChangePlanBundleAlignEOTWithChargeThroughDate();
     }
 
+    @Override
     @Test(enabled=true, groups={"fast"})
     public void testChangePlanBundleAlignIMM() {
-        invokeRealMethod(this);
+        super.testChangePlanBundleAlignIMM();
     }
 
+    @Override
     @Test(enabled=true, groups={"fast"})
     public void testMultipleChangeLastIMM() {
-        invokeRealMethod(this);
+        super.testMultipleChangeLastIMM();
     }
 
+    @Override
     @Test(enabled=true, groups={"fast"})
     public void testMultipleChangeLastEOT() {
-        invokeRealMethod(this);
+        super.testMultipleChangeLastEOT();
     }
 
     // Set to false until we implement rescue example.
+    @Override
     @Test(enabled=false, groups={"fast"})
     public void testChangePlanChangePlanAlignEOTWithChargeThroughDate() {
-        invokeRealMethod(this);
+        super.testChangePlanChangePlanAlignEOTWithChargeThroughDate();
     }
-
 }
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiChangePlanSql.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiChangePlanSql.java
index 255e894..f5ad803 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiChangePlanSql.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiChangePlanSql.java
@@ -52,35 +52,41 @@ public class TestUserApiChangePlanSql extends TestUserApiChangePlan {
         }
     }
 
+    @Override
     @Test(enabled=true, groups={"sql"})
     public void testChangePlanBundleAlignEOTWithNoChargeThroughDate() {
-        invokeRealMethod(this);
+        super.testChangePlanBundleAlignEOTWithNoChargeThroughDate();
     }
 
+    @Override
     @Test(enabled=true, groups={"sql"})
     public void testChangePlanBundleAlignEOTWithChargeThroughDate() {
-        invokeRealMethod(this);
+        super.testChangePlanBundleAlignEOTWithChargeThroughDate();
     }
 
+    @Override
     @Test(enabled=true, groups={"sql"})
     public void testChangePlanBundleAlignIMM() {
-        invokeRealMethod(this);
+        super.testChangePlanBundleAlignIMM();
     }
 
+    @Override
     @Test(enabled=true, groups={"sql"})
     public void testMultipleChangeLastIMM() {
-        invokeRealMethod(this);
+        super.testMultipleChangeLastIMM();
     }
 
+    @Override
     @Test(enabled=true, groups={"sql"})
     public void testMultipleChangeLastEOT() {
-        invokeRealMethod(this);
+        super.testMultipleChangeLastEOT();
     }
 
     // rescue not implemented yet
+    @Override
     @Test(enabled=false, groups={"sql"})
     public void testChangePlanChangePlanAlignEOTWithChargeThroughDate() {
-        invokeRealMethod(this);
+        super.testChangePlanChangePlanAlignEOTWithChargeThroughDate();
     }
 
 }
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiCreate.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiCreate.java
index 365374e..ff55ca0 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiCreate.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiCreate.java
@@ -16,23 +16,31 @@
 
 package com.ning.billing.entitlement.api.user;
 
-import com.ning.billing.catalog.api.*;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+
+import java.util.List;
+import org.joda.time.DateTime;
+import org.testng.Assert;
+
+import com.ning.billing.catalog.api.BillingPeriod;
+import com.ning.billing.catalog.api.Plan;
+import com.ning.billing.catalog.api.PlanPhase;
+import com.ning.billing.catalog.api.PriceListSet;
+import com.ning.billing.catalog.api.PhaseType;
+import com.ning.billing.catalog.api.ProductCategory;
+import com.ning.billing.entitlement.api.TestApiBase;
 import com.ning.billing.entitlement.api.ApiTestListener.NextEvent;
 import com.ning.billing.entitlement.events.EntitlementEvent;
 import com.ning.billing.entitlement.events.phase.PhaseEvent;
 import com.ning.billing.util.clock.DefaultClock;
-import org.joda.time.DateTime;
-import org.testng.Assert;
-
-import java.util.List;
 
-import static org.testng.Assert.*;
+public abstract class TestUserApiCreate extends TestApiBase {
 
-public abstract class TestUserApiCreate extends TestUserApiBase {
 
-
-
-    protected void testCreateWithRequestedDateReal() {
+    public void testCreateWithRequestedDate() {
         log.info("Starting testCreateWithRequestedDate");
         try {
 
@@ -48,7 +56,8 @@ public abstract class TestUserApiCreate extends TestUserApiBase {
             testListener.pushExpectedEvent(NextEvent.CREATE);
 
 
-            SubscriptionData subscription = (SubscriptionData) entitlementApi.createSubscription(bundle.getId(), productName, term, planSetName, null, requestedDate);
+            SubscriptionData subscription = (SubscriptionData) entitlementApi.createSubscription(bundle.getId(),
+                    getProductSpecifier(productName, planSetName, term, null), requestedDate);
             assertNotNull(subscription);
 
             assertEquals(subscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION);
@@ -63,7 +72,7 @@ public abstract class TestUserApiCreate extends TestUserApiBase {
         }
     }
 
-    protected void testCreateWithInitialPhaseReal() {
+    protected void testCreateWithInitialPhase() {
         log.info("Starting testCreateWithInitialPhase");
         try {
 
@@ -76,7 +85,8 @@ public abstract class TestUserApiCreate extends TestUserApiBase {
 
             testListener.pushExpectedEvent(NextEvent.CREATE);
 
-            SubscriptionData subscription = (SubscriptionData) entitlementApi.createSubscription(bundle.getId(), productName, term, planSetName, PhaseType.EVERGREEN, clock.getUTCNow());
+            SubscriptionData subscription = (SubscriptionData) entitlementApi.createSubscription(bundle.getId(),
+                    getProductSpecifier(productName, planSetName, term, PhaseType.EVERGREEN), clock.getUTCNow());
             assertNotNull(subscription);
 
             assertEquals(subscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION);
@@ -102,7 +112,7 @@ public abstract class TestUserApiCreate extends TestUserApiBase {
         }
     }
 
-    protected void testSimpleCreateSubscriptionReal() {
+    protected void testSimpleCreateSubscription() {
 
         log.info("Starting testSimpleCreateSubscription");
         try {
@@ -115,7 +125,9 @@ public abstract class TestUserApiCreate extends TestUserApiBase {
 
             testListener.pushExpectedEvent(NextEvent.CREATE);
 
-            SubscriptionData subscription = (SubscriptionData) entitlementApi.createSubscription(bundle.getId(), productName, term, planSetName, null, clock.getUTCNow());
+            SubscriptionData subscription = (SubscriptionData) entitlementApi.createSubscription(bundle.getId(),
+                    getProductSpecifier(productName, planSetName, term, null),
+                    clock.getUTCNow());
             assertNotNull(subscription);
 
             assertEquals(subscription.getActiveVersion(), SubscriptionEvents.INITIAL_VERSION);
@@ -166,7 +178,7 @@ public abstract class TestUserApiCreate extends TestUserApiBase {
     }
 
 
-    protected void testSimpleSubscriptionThroughPhasesReal() {
+    protected void testSimpleSubscriptionThroughPhases() {
 
         log.info("Starting testSimpleSubscriptionThroughPhases");
         try {
@@ -180,7 +192,8 @@ public abstract class TestUserApiCreate extends TestUserApiBase {
             testListener.pushExpectedEvent(NextEvent.CREATE);
 
             // CREATE SUBSCRIPTION
-            SubscriptionData subscription = (SubscriptionData) entitlementApi.createSubscription(bundle.getId(), productName, term, planSetName, null, clock.getUTCNow());
+            SubscriptionData subscription = (SubscriptionData) entitlementApi.createSubscription(bundle.getId(),
+                    getProductSpecifier(productName, planSetName, term, null), clock.getUTCNow());
             assertNotNull(subscription);
 
             PlanPhase currentPhase = subscription.getCurrentPhase();
@@ -216,7 +229,7 @@ public abstract class TestUserApiCreate extends TestUserApiBase {
         }
     }
 
-    protected void testSubscriptionWithAddOnReal() {
+    protected void testSubscriptionWithAddOn() {
 
         log.info("Starting testSubscriptionWithAddOn");
         try {
@@ -227,7 +240,8 @@ public abstract class TestUserApiCreate extends TestUserApiBase {
 
             testListener.pushExpectedEvent(NextEvent.CREATE);
 
-            SubscriptionData subscription = (SubscriptionData) entitlementApi.createSubscription(bundle.getId(), productName, term, planSetName, null, clock.getUTCNow());
+            SubscriptionData subscription = (SubscriptionData) entitlementApi.createSubscription(bundle.getId(),
+                    getProductSpecifier(productName, planSetName, term, null), clock.getUTCNow());
             assertNotNull(subscription);
 
         } catch (EntitlementUserApiException e) {
@@ -236,4 +250,5 @@ public abstract class TestUserApiCreate extends TestUserApiBase {
     }
 
 
+
 }
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiCreateMemory.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiCreateMemory.java
index bff20db..f4474f9 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiCreateMemory.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiCreateMemory.java
@@ -30,29 +30,30 @@ public class TestUserApiCreateMemory extends TestUserApiCreate {
         return Guice.createInjector(Stage.PRODUCTION, new MockEngineModuleMemory());
     }
 
+    @Override
     @Test(enabled=true, groups={"fast"})
     public void testCreateWithRequestedDate() {
-        invokeRealMethod(this);
+        super.testCreateWithRequestedDate();
     }
 
     @Test(enabled=true, groups={"fast"})
     public void testCreateWithInitialPhase() {
-        invokeRealMethod(this);
+        super.testSimpleSubscriptionThroughPhases();
     }
 
     @Test(enabled=true, groups={"fast"})
     public void testSimpleCreateSubscription() {
-        invokeRealMethod(this);
+        super.testSimpleCreateSubscription();
     }
 
     @Test(enabled=true, groups={"fast"})
     protected void testSimpleSubscriptionThroughPhases() {
-        invokeRealMethod(this);
+        super.testSimpleSubscriptionThroughPhases();
     }
 
     @Test(enabled=false, groups={"fast"})
     protected void testSubscriptionWithAddOn() {
-        invokeRealMethod(this);
+        super.testSubscriptionWithAddOn();
     }
 
 }
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiCreateSql.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiCreateSql.java
index 25996e2..8170b6d 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiCreateSql.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiCreateSql.java
@@ -29,29 +29,34 @@ public class TestUserApiCreateSql extends TestUserApiCreate {
         return Guice.createInjector(Stage.DEVELOPMENT, new MockEngineModuleSql());
     }
 
+    @Override
     @Test(enabled=true, groups={"sql"})
     public void testCreateWithRequestedDate() {
-        invokeRealMethod(this);
+        super.testCreateWithRequestedDate();
     }
 
+    @Override
     @Test(enabled=true, groups={"sql"})
     public void testCreateWithInitialPhase() {
-        invokeRealMethod(this);
+        super.testCreateWithInitialPhase();
     }
 
+    @Override
     @Test(enabled=true, groups={"sql"})
     public void testSimpleCreateSubscription() {
-        invokeRealMethod(this);
+        super.testSimpleCreateSubscription();
     }
 
+    @Override
     @Test(enabled=true, groups={"sql"})
     protected void testSimpleSubscriptionThroughPhases() {
-        invokeRealMethod(this);
+        super.testSimpleSubscriptionThroughPhases();
     }
 
+    @Override
     @Test(enabled=false, groups={"sql"})
     protected void testSubscriptionWithAddOn() {
-        invokeRealMethod(this);
+        super.testSubscriptionWithAddOn();
     }
 
 }
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiDemos.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiDemos.java
index 91ac96f..4a8b4b1 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiDemos.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiDemos.java
@@ -19,7 +19,15 @@ package com.ning.billing.entitlement.api.user;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
 import com.google.inject.Stage;
-import com.ning.billing.catalog.api.*;
+
+import com.ning.billing.catalog.api.BillingPeriod;
+import com.ning.billing.catalog.api.Duration;
+import com.ning.billing.catalog.api.Plan;
+import com.ning.billing.catalog.api.PlanPhase;
+import com.ning.billing.catalog.api.PhaseType;
+import com.ning.billing.catalog.api.ProductCategory;
+import com.ning.billing.entitlement.api.TestApiBase;
+
 import com.ning.billing.entitlement.api.ApiTestListener.NextEvent;
 import com.ning.billing.entitlement.glue.MockEngineModuleSql;
 import com.ning.billing.util.clock.DefaultClock;
@@ -33,7 +41,7 @@ import java.util.UUID;
 
 import static org.testng.Assert.*;
 
-public class TestUserApiDemos extends TestUserApiBase {
+public class TestUserApiDemos extends TestApiBase {
 
     @Override
     protected Injector getInjector() {
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiError.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiError.java
index d683244..c32b654 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiError.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiError.java
@@ -24,6 +24,7 @@ import com.ning.billing.catalog.api.BillingPeriod;
 import com.ning.billing.catalog.api.Duration;
 import com.ning.billing.catalog.api.PlanPhase;
 import com.ning.billing.catalog.api.PriceListSet;
+import com.ning.billing.entitlement.api.TestApiBase;
 import com.ning.billing.entitlement.api.ApiTestListener.NextEvent;
 import com.ning.billing.entitlement.glue.MockEngineModuleMemory;
 import com.ning.billing.util.clock.DefaultClock;
@@ -36,7 +37,7 @@ import java.util.UUID;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertFalse;
 
-public class TestUserApiError extends TestUserApiBase {
+public class TestUserApiError extends TestApiBase {
 
 
     @Override
@@ -86,7 +87,9 @@ public class TestUserApiError extends TestUserApiBase {
     private void tCreateSubscriptionInternal(UUID bundleId, String productName,
             BillingPeriod term, String planSet, ErrorCode expected)  {
         try {
-            entitlementApi.createSubscription(bundleId, productName, term, planSet, null, clock.getUTCNow());
+            entitlementApi.createSubscription(bundleId,
+                    getProductSpecifier(productName, planSet, term, null),
+                    clock.getUTCNow());
             assertFalse(true);
         } catch (EntitlementUserApiException e) {
             assertEquals(e.getCode(), expected.getCode());
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiPriceList.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiPriceList.java
index d6a2b12..a1a0dc7 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiPriceList.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiPriceList.java
@@ -17,8 +17,9 @@
 package com.ning.billing.entitlement.api.user;
 
 import com.google.inject.Injector;
+import com.ning.billing.entitlement.api.TestApiBase;
 
-public class TestUserApiPriceList extends TestUserApiBase  {
+public class TestUserApiPriceList extends TestApiBase  {
 
     @Override
     protected Injector getInjector() {
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiScenarios.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiScenarios.java
index 7b600dd..84d1031 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiScenarios.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiScenarios.java
@@ -23,7 +23,9 @@ import com.ning.billing.catalog.api.BillingPeriod;
 import com.ning.billing.catalog.api.Duration;
 import com.ning.billing.catalog.api.PhaseType;
 import com.ning.billing.catalog.api.PlanPhase;
+
 import com.ning.billing.entitlement.api.ApiTestListener.NextEvent;
+import com.ning.billing.entitlement.api.TestApiBase;
 import com.ning.billing.entitlement.glue.MockEngineModuleSql;
 import com.ning.billing.util.clock.DefaultClock;
 import org.joda.time.DateTime;
@@ -32,7 +34,7 @@ import org.testng.annotations.Test;
 
 import static org.testng.Assert.*;
 
-public class TestUserApiScenarios extends TestUserApiBase {
+public class TestUserApiScenarios extends TestApiBase {
 
     @Override
     protected Injector getInjector() {
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/engine/dao/MockEntitlementDaoMemory.java b/entitlement/src/test/java/com/ning/billing/entitlement/engine/dao/MockEntitlementDaoMemory.java
index e9ff1bd..7fc8342 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/engine/dao/MockEntitlementDaoMemory.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/engine/dao/MockEntitlementDaoMemory.java
@@ -20,7 +20,16 @@ import com.google.inject.Inject;
 import com.ning.billing.catalog.api.ProductCategory;
 import com.ning.billing.catalog.api.TimeUnit;
 import com.ning.billing.config.EntitlementConfig;
-import com.ning.billing.entitlement.api.user.*;
+
+import com.ning.billing.entitlement.api.migration.AccountMigrationData;
+import com.ning.billing.entitlement.api.migration.AccountMigrationData.BundleMigrationData;
+import com.ning.billing.entitlement.api.migration.AccountMigrationData.SubscriptionMigrationData;
+import com.ning.billing.entitlement.api.user.Subscription;
+import com.ning.billing.entitlement.api.user.SubscriptionBundle;
+import com.ning.billing.entitlement.api.user.SubscriptionData;
+import com.ning.billing.entitlement.api.user.SubscriptionBundleData;
+import com.ning.billing.entitlement.api.user.SubscriptionFactory;
+
 import com.ning.billing.entitlement.api.user.SubscriptionFactory.SubscriptionBuilder;
 import com.ning.billing.entitlement.events.EntitlementEvent;
 import com.ning.billing.entitlement.events.EntitlementEvent.EventType;
@@ -86,6 +95,17 @@ public class MockEntitlementDaoMemory implements EntitlementDao, MockEntitlement
     }
 
     @Override
+    public SubscriptionBundle getSubscriptionBundleFromKey(String bundleKey) {
+        for (SubscriptionBundle cur : bundles) {
+            if (cur.getKey().equals(bundleKey)) {
+                return cur;
+            }
+        }
+        return null;
+    }
+
+
+    @Override
     public SubscriptionBundle createSubscriptionBundle(SubscriptionBundleData bundle) {
         bundles.add(bundle);
         return getSubscriptionBundleFromId(bundle.getId());
@@ -333,4 +353,44 @@ public class MockEntitlementDaoMemory implements EntitlementDao, MockEntitlement
         }
     }
 
+
+    @Override
+    public void migrate(final UUID accountId, final AccountMigrationData accountData) {
+        synchronized(events) {
+
+            undoMigration(accountId);
+
+            for (BundleMigrationData curBundle : accountData.getData()) {
+                SubscriptionBundleData bundleData = curBundle.getData();
+                for (SubscriptionMigrationData curSubscription : curBundle.getSubscriptions()) {
+                    SubscriptionData subData = curSubscription.getData();
+                    for (EntitlementEvent curEvent : curSubscription.getInitialEvents()) {
+                        events.add(curEvent);
+                    }
+                    subscriptions.add(subData);
+                }
+                bundles.add(bundleData);
+            }
+        }
+    }
+
+    @Override
+    public void undoMigration(UUID accountId) {
+        synchronized(events) {
+
+            List<SubscriptionBundle> allBundles = getSubscriptionBundleForAccount(accountId);
+            for (SubscriptionBundle bundle : allBundles) {
+                List<Subscription> allSubscriptions = getSubscriptions(bundle.getId());
+                for (Subscription subscription : allSubscriptions) {
+                    List<EntitlementEvent> allEvents = getEventsForSubscription(subscription.getId());
+                    for (EntitlementEvent event : allEvents) {
+                        events.remove(event);
+                    }
+                    subscriptions.remove(subscription);
+                }
+                bundles.remove(bundle);
+            }
+        }
+
+    }
 }

invoice/pom.xml 2(+1 -1)

diff --git a/invoice/pom.xml b/invoice/pom.xml
index 9db99d3..3d7dc27 100644
--- a/invoice/pom.xml
+++ b/invoice/pom.xml
@@ -13,7 +13,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill</artifactId>
-        <version>0.0.20-SNAPSHOT</version>
+        <version>0.0.21-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-invoice</artifactId>
diff --git a/invoice/src/main/java/com/ning/billing/invoice/api/invoice/DefaultInvoicePaymentApi.java b/invoice/src/main/java/com/ning/billing/invoice/api/invoice/DefaultInvoicePaymentApi.java
index fac63ba..a36cfea 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/api/invoice/DefaultInvoicePaymentApi.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/api/invoice/DefaultInvoicePaymentApi.java
@@ -1,4 +1,5 @@
 /*
+
  * Copyright 2010-2011 Ning, Inc.
  *
  * Ning licenses this file to you under the Apache License, version 2.0
@@ -19,13 +20,15 @@ package com.ning.billing.invoice.api.invoice;
 import java.math.BigDecimal;
 import java.util.List;
 import java.util.UUID;
+
 import org.joda.time.DateTime;
-import org.skife.jdbi.v2.IDBI;
+
 import com.google.inject.Inject;
 import com.ning.billing.catalog.api.Currency;
 import com.ning.billing.invoice.api.Invoice;
 import com.ning.billing.invoice.api.InvoicePaymentApi;
 import com.ning.billing.invoice.dao.InvoiceDao;
+import com.ning.billing.payment.api.InvoicePayment;
 
 public class DefaultInvoicePaymentApi implements InvoicePaymentApi {
     private final InvoiceDao dao;
@@ -54,4 +57,10 @@ public class DefaultInvoicePaymentApi implements InvoicePaymentApi {
     public Invoice getInvoice(UUID invoiceId) {
         return dao.getById(invoiceId.toString());
     }
+
+    @Override
+    public InvoicePayment getInvoicePayment(UUID invoiceId, UUID paymentAttemptId) {
+        throw new UnsupportedOperationException();
+    }
+
 }
diff --git a/invoice/src/main/java/com/ning/billing/invoice/glue/InvoiceModule.java b/invoice/src/main/java/com/ning/billing/invoice/glue/InvoiceModule.java
index 54990c5..0e1c657 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/glue/InvoiceModule.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/glue/InvoiceModule.java
@@ -18,14 +18,14 @@ package com.ning.billing.invoice.glue;
 
 import com.google.inject.AbstractModule;
 import com.ning.billing.invoice.api.InvoicePaymentApi;
+import com.ning.billing.invoice.api.InvoiceUserApi;
 import com.ning.billing.invoice.api.invoice.DefaultInvoicePaymentApi;
 import com.ning.billing.invoice.api.user.DefaultInvoiceUserApi;
-import com.ning.billing.invoice.api.InvoiceUserApi;
 import com.ning.billing.invoice.dao.DefaultInvoiceDao;
 import com.ning.billing.invoice.dao.InvoiceDao;
 
 public class InvoiceModule extends AbstractModule {
-    private void installInvoiceDao() {
+    protected void installInvoiceDao() {
         bind(InvoiceDao.class).to(DefaultInvoiceDao.class).asEagerSingleton();
     }
 
diff --git a/invoice/src/main/java/com/ning/billing/invoice/model/DefaultInvoice.java b/invoice/src/main/java/com/ning/billing/invoice/model/DefaultInvoice.java
index 797f587..cc408fb 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/model/DefaultInvoice.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/model/DefaultInvoice.java
@@ -16,26 +16,27 @@
 
 package com.ning.billing.invoice.model;
 
-import com.ning.billing.catalog.api.Currency;
-import com.ning.billing.invoice.api.Invoice;
-import com.ning.billing.invoice.api.InvoiceItem;
-import com.ning.billing.util.clock.DefaultClock;
-import org.joda.time.DateTime;
-
 import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.UUID;
 
+import org.joda.time.DateTime;
+
+import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.invoice.api.Invoice;
+import com.ning.billing.invoice.api.InvoiceItem;
+import com.ning.billing.util.clock.DefaultClock;
+
 public class DefaultInvoice implements Invoice {
     private final InvoiceItemList items = new InvoiceItemList();
     private final UUID id;
-    private UUID accountId;
+    private final UUID accountId;
     private final DateTime invoiceDate;
     private final DateTime targetDate;
-    private Currency currency;
-    private BigDecimal amountPaid;
-    private DateTime lastPaymentAttempt;
+    private final Currency currency;
+    private final BigDecimal amountPaid;
+    private final DateTime lastPaymentAttempt;
 
     public DefaultInvoice(UUID accountId, DateTime targetDate, Currency currency) {
         this(UUID.randomUUID(), accountId, new DefaultClock().getUTCNow(), targetDate, currency, null, BigDecimal.ZERO, new ArrayList<InvoiceItem>());
@@ -132,5 +133,11 @@ public class DefaultInvoice implements Invoice {
 
         return lastPaymentAttempt.plusDays(numberOfDays).isBefore(targetDate);
     }
+
+    @Override
+    public String toString() {
+        return "DefaultInvoice [items=" + items + ", id=" + id + ", accountId=" + accountId + ", invoiceDate=" + invoiceDate + ", targetDate=" + targetDate + ", currency=" + currency + ", amountPaid=" + amountPaid + ", lastPaymentAttempt=" + lastPaymentAttempt + "]";
+    }
+
 }
 
diff --git a/invoice/src/test/java/com/ning/billing/invoice/api/MockInvoicePaymentApi.java b/invoice/src/test/java/com/ning/billing/invoice/api/MockInvoicePaymentApi.java
index 6f42ec8..49ab67c 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/api/MockInvoicePaymentApi.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/api/MockInvoicePaymentApi.java
@@ -25,18 +25,27 @@ import java.util.concurrent.CopyOnWriteArrayList;
 import org.joda.time.DateTime;
 
 import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.payment.api.InvoicePayment;
 
 public class MockInvoicePaymentApi implements InvoicePaymentApi
 {
     private final CopyOnWriteArrayList<Invoice> invoices = new CopyOnWriteArrayList<Invoice>();
+    private final CopyOnWriteArrayList<InvoicePayment> invoicePayments = new CopyOnWriteArrayList<InvoicePayment>();
 
     public void add(Invoice invoice) {
         invoices.add(invoice);
     }
 
     @Override
-    public void paymentSuccessful(UUID invoiceId, BigDecimal amount, Currency currency, UUID paymentId, DateTime paymentAttemptDate) {
-        throw new UnsupportedOperationException();
+    public void paymentSuccessful(UUID invoiceId, BigDecimal amount, Currency currency, UUID paymentAttemptId, DateTime paymentAttemptDate) {
+        InvoicePayment invoicePayment = new InvoicePayment(invoiceId, amount, currency, paymentAttemptId, paymentAttemptDate);
+
+        for (InvoicePayment existingInvoicePayment : invoicePayments) {
+            if (existingInvoicePayment.getInvoiceId().equals(invoiceId) && existingInvoicePayment.getPaymentAttemptId().equals(paymentAttemptId)) {
+                invoicePayments.remove(existingInvoicePayment);
+            }
+        }
+        invoicePayments.add(invoicePayment);
     }
 
     @Override
@@ -62,7 +71,24 @@ public class MockInvoicePaymentApi implements InvoicePaymentApi
     }
 
     @Override
-    public void paymentFailed(UUID invoiceId, UUID paymentId, DateTime paymentAttemptDate) {
-        throw new UnsupportedOperationException();
+    public void paymentFailed(UUID invoiceId, UUID paymentAttemptId, DateTime paymentAttemptDate) {
+        InvoicePayment invoicePayment = new InvoicePayment(invoiceId, null, null, paymentAttemptId, paymentAttemptDate);
+        for (InvoicePayment existingInvoicePayment : invoicePayments) {
+            if (existingInvoicePayment.getInvoiceId().equals(invoiceId) && existingInvoicePayment.getPaymentAttemptId().equals(paymentAttemptId)) {
+                invoicePayments.remove(existingInvoicePayment);
+            }
+        }
+        invoicePayments.add(invoicePayment);
+    }
+
+    @Override
+    public InvoicePayment getInvoicePayment(UUID invoiceId, UUID paymentAttemptId) {
+        for (InvoicePayment existingInvoicePayment : invoicePayments) {
+            if (existingInvoicePayment.getInvoiceId().equals(invoiceId) && existingInvoicePayment.getPaymentAttemptId().equals(paymentAttemptId)) {
+                return existingInvoicePayment;
+            }
+        }
+        return null;
     }
+
 }
diff --git a/invoice/src/test/java/com/ning/billing/invoice/dao/MockInvoiceDao.java b/invoice/src/test/java/com/ning/billing/invoice/dao/MockInvoiceDao.java
new file mode 100644
index 0000000..8570608
--- /dev/null
+++ b/invoice/src/test/java/com/ning/billing/invoice/dao/MockInvoiceDao.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.invoice.dao;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.invoice.api.Invoice;
+import com.ning.billing.invoice.api.InvoiceItem;
+import com.ning.billing.invoice.model.DefaultInvoice;
+
+public class MockInvoiceDao implements InvoiceDao {
+    private static final class InvoicePaymentInfo {
+        public final String invoiceId;
+        public final String paymentId;
+        public final DateTime paymentDate;
+        public final BigDecimal amount;
+        public final Currency currency;
+
+        public InvoicePaymentInfo(String invoiceId, String paymentId, DateTime paymentDate, BigDecimal amount, Currency currency) {
+            this.invoiceId = invoiceId;
+            this.paymentId = paymentId;
+            this.paymentDate = paymentDate;
+            this.amount = amount;
+            this.currency = currency;
+        }
+    }
+
+    private final Object monitor = new Object();
+    private final Map<String, Invoice> invoices = new LinkedHashMap<String, Invoice>();
+    private final List<InvoicePaymentInfo> paymentInfos = new ArrayList<MockInvoiceDao.InvoicePaymentInfo>();
+
+    @Override
+    public void save(Invoice invoice) {
+        synchronized (monitor) {
+            invoices.put(invoice.getId().toString(), invoice);
+        }
+    }
+
+    private Invoice munge(Invoice invoice) {
+        if (invoice == null) {
+            return null;
+        }
+
+        DateTime lastPaymentDate = null;
+        BigDecimal amountPaid = new BigDecimal("0");
+
+        for (InvoicePaymentInfo info : paymentInfos) {
+            if (info.invoiceId.equals(invoice.getId().toString())) {
+                if (lastPaymentDate == null || lastPaymentDate.isBefore(info.paymentDate)) {
+                    lastPaymentDate = info.paymentDate;
+                }
+                if (info.amount != null) {
+                    amountPaid.add(info.amount);
+                }
+            }
+        }
+        return new DefaultInvoice(invoice.getId(),
+                                  invoice.getAccountId(),
+                                  invoice.getInvoiceDate(),
+                                  invoice.getTargetDate(),
+                                  invoice.getCurrency(),
+                                  lastPaymentDate,
+                                  amountPaid,
+                                  invoice.getItems());
+    }
+
+    private List<Invoice> munge(Collection<Invoice> invoices) {
+        List<Invoice> result = new ArrayList<Invoice>();
+        for (Invoice invoice : invoices) {
+            result.add(munge(invoice));
+        }
+        return result;
+    }
+
+    @Override
+    public Invoice getById(String id) {
+        synchronized (monitor) {
+            return munge(invoices.get(id));
+        }
+    }
+
+    @Override
+    public List<Invoice> get() {
+        synchronized (monitor) {
+            return munge(invoices.values());
+        }
+    }
+
+    @Override
+    public List<Invoice> getInvoicesByAccount(String accountId) {
+        List<Invoice> result = new ArrayList<Invoice>();
+
+        synchronized (monitor) {
+            for (Invoice invoice : invoices.values()) {
+                if (accountId.equals(invoice.getAccountId().toString())) {
+                    result.add(invoice);
+                }
+            }
+        }
+        return munge(result);
+    }
+
+    @Override
+    public List<Invoice> getInvoicesBySubscription(String subscriptionId) {
+        List<Invoice> result = new ArrayList<Invoice>();
+
+        synchronized (monitor) {
+            for (Invoice invoice : invoices.values()) {
+                for (InvoiceItem item : invoice.getItems()) {
+                    if (subscriptionId.equals(item.getSubscriptionId().toString())) {
+                        result.add(invoice);
+                        break;
+                    }
+                }
+            }
+        }
+        return munge(result);
+    }
+
+    @Override
+    public List<UUID> getInvoicesForPayment(Date targetDate, int numberOfDays) {
+        Set<UUID> result = new LinkedHashSet<UUID>();
+
+        synchronized (monitor) {
+            for (InvoicePaymentInfo info : paymentInfos) {
+                Invoice invoice = invoices.get(info.invoiceId);
+                if ((invoice != null) &&
+                    (((info.paymentDate == null) || !info.paymentDate.plusDays(numberOfDays).isAfter(targetDate.getTime())) &&
+                    (invoice.getTotalAmount() != null) && invoice.getTotalAmount().doubleValue() >= 0) &&
+                    ((info.amount == null) || info.amount.doubleValue() >= invoice.getTotalAmount().doubleValue())) {
+
+                    result.add(invoice.getId());
+                }
+            }
+        }
+
+        return new ArrayList<UUID>(result);
+    }
+
+    @Override
+    public void notifySuccessfulPayment(String invoiceId,
+                                        BigDecimal paymentAmount,
+                                        String currency, String paymentId,
+                                        Date paymentDate) {
+        synchronized (monitor) {
+            paymentInfos.add(new InvoicePaymentInfo(invoiceId, paymentId, new DateTime(paymentDate), paymentAmount, Currency.valueOf(currency)));
+        }
+    }
+
+    @Override
+    public void notifyFailedPayment(String invoiceId, String paymentId,
+                                    Date paymentAttemptDate) {
+        synchronized (monitor) {
+            paymentInfos.add(new InvoicePaymentInfo(invoiceId, paymentId, new DateTime(paymentAttemptDate), null, null));
+        }
+    }
+
+    @Override
+    public void test() {
+    }
+}

payment/pom.xml 2(+1 -1)

diff --git a/payment/pom.xml b/payment/pom.xml
index a72c672..409851b 100644
--- a/payment/pom.xml
+++ b/payment/pom.xml
@@ -13,7 +13,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill</artifactId>
-        <version>0.0.20-SNAPSHOT</version>
+        <version>0.0.21-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-payment</artifactId>
diff --git a/payment/src/main/java/com/ning/billing/payment/dao/PaymentDao.java b/payment/src/main/java/com/ning/billing/payment/dao/PaymentDao.java
new file mode 100644
index 0000000..f16adb0
--- /dev/null
+++ b/payment/src/main/java/com/ning/billing/payment/dao/PaymentDao.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.payment.dao;
+
+public class PaymentDao {
+
+}
diff --git a/payment/src/main/java/com/ning/billing/payment/PaymentAttempt.java b/payment/src/main/java/com/ning/billing/payment/PaymentAttempt.java
new file mode 100644
index 0000000..353587d
--- /dev/null
+++ b/payment/src/main/java/com/ning/billing/payment/PaymentAttempt.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.payment;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+
+import com.ning.billing.invoice.api.Invoice;
+
+public class PaymentAttempt {
+    private final UUID paymentAttemptId;
+    private final UUID accountId;
+    private final UUID invoiceId;
+    private final BigDecimal paymentAttemptAmount;
+    private final DateTime paymentAttemptDate;
+
+    public PaymentAttempt(UUID paymentAttemptId, Invoice invoice) {
+        this.paymentAttemptId = paymentAttemptId;
+        this.accountId = invoice.getAccountId();
+        this.invoiceId = invoice.getId();
+        this.paymentAttemptAmount = invoice.getAmountOutstanding();
+        this.paymentAttemptDate = new DateTime(DateTimeZone.UTC);
+    }
+
+    public UUID getPaymentAttemptId() {
+        return paymentAttemptId;
+    }
+
+    public UUID getAccountId() {
+        return accountId;
+    }
+
+    public UUID getInvoiceId() {
+        return invoiceId;
+    }
+
+    public BigDecimal getPaymentAttemptAmount() {
+        return paymentAttemptAmount;
+    }
+
+        public DateTime getPaymentAttemptDate() {
+            return paymentAttemptDate;
+    }
+
+    @Override
+    public String toString() {
+        return "PaymentAttempt [paymentAttemptId=" + paymentAttemptId + ", accountId=" + accountId + ", invoiceId=" + invoiceId + ", paymentAttemptAmount=" + paymentAttemptAmount + "]";
+    }
+
+}
diff --git a/payment/src/main/java/com/ning/billing/payment/RequestProcessor.java b/payment/src/main/java/com/ning/billing/payment/RequestProcessor.java
index 2033d59..5b671bc 100644
--- a/payment/src/main/java/com/ning/billing/payment/RequestProcessor.java
+++ b/payment/src/main/java/com/ning/billing/payment/RequestProcessor.java
@@ -17,6 +17,7 @@
 package com.ning.billing.payment;
 
 import java.math.BigDecimal;
+import java.util.UUID;
 
 import com.google.common.eventbus.Subscribe;
 import com.google.inject.Inject;
@@ -26,6 +27,7 @@ import com.ning.billing.invoice.api.Invoice;
 import com.ning.billing.invoice.api.InvoiceCreationNotification;
 import com.ning.billing.invoice.api.InvoicePaymentApi;
 import com.ning.billing.payment.api.Either;
+import com.ning.billing.payment.api.PaymentAttempt;
 import com.ning.billing.payment.api.PaymentError;
 import com.ning.billing.payment.api.PaymentInfo;
 import com.ning.billing.payment.provider.PaymentProviderPlugin;
@@ -58,21 +60,40 @@ public class RequestProcessor {
             if (invoice == null) {
                 // TODO: log a warning
             }
-            else if (invoice.getAmountOutstanding().compareTo(BigDecimal.ZERO) == 0 ) {
-                // TODO: send a notification that invoice was ignored ?
-            }
             else {
-                final Account account = accountUserApi.getAccountById(event.getAccountId());
-                if (account == null) {
-                    // TODO: log a warning
+                PaymentAttempt paymentAttempt = new PaymentAttempt(UUID.randomUUID(), invoice);
+
+                if (invoice.getAmountOutstanding().compareTo(BigDecimal.ZERO) == 0 ) {
+                // TODO: send a notification that invoice was ignored ?
                 }
                 else {
-                    final String paymentProviderName = account.getPaymentProviderName();
-                    final PaymentProviderPlugin plugin = pluginRegistry.getPlugin(paymentProviderName);
+                    final Account account = accountUserApi.getAccountById(event.getAccountId());
 
-                    Either<PaymentError, PaymentInfo> result = plugin.processInvoice(account, invoice);
+                    if (account == null) {
+                        // TODO: log a warning
+                    }
+                    else {
+                        final BigDecimal paymentAmount = invoice.getAmountOutstanding();
+                        final String paymentProviderName = account.getPaymentProviderName();
+                        final PaymentProviderPlugin plugin = pluginRegistry.getPlugin(paymentProviderName);
 
-                    eventBus.post(result.isLeft() ? result.getLeft() : result.getRight());
+                        Either<PaymentError, PaymentInfo> result = plugin.processInvoice(account, invoice);
+
+                        if (result.isLeft()) {
+                            invoiceApi.paymentFailed(invoice.getId(),
+                                                     paymentAttempt.getPaymentAttemptId(),
+                                                     paymentAttempt.getPaymentAttemptDate());
+                        }
+                        else {
+                            updatePaymentAttemptWithPaymentInfoId(result.getRight().getId(), plugin);
+                            invoiceApi.paymentSuccessful(invoice.getId(),
+                                                         paymentAmount,
+                                                         invoice.getCurrency(),
+                                                         paymentAttempt.getPaymentAttemptId(),
+                                                         paymentAttempt.getPaymentAttemptDate());
+                        }
+                        eventBus.post(result.isLeft() ? result.getLeft() : result.getRight());
+                    }
                 }
             }
         }
@@ -81,6 +102,10 @@ public class RequestProcessor {
         }
     }
 
+    private void updatePaymentAttemptWithPaymentInfoId(String id, PaymentProviderPlugin plugin) {
+        // TODO update PaymentAttempt with paymentId
+    }
+
     @Subscribe
     public void receivePaymentInfoRequest(PaymentInfoRequest paymentInfoRequest) throws EventBusException {
         final Account account = accountUserApi.getAccountById(paymentInfoRequest.getAccountId());
diff --git a/payment/src/test/java/com/ning/billing/payment/api/TestMockPaymentApi.java b/payment/src/test/java/com/ning/billing/payment/api/TestMockPaymentApi.java
new file mode 100644
index 0000000..10f8d8f
--- /dev/null
+++ b/payment/src/test/java/com/ning/billing/payment/api/TestMockPaymentApi.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.payment.api;
+
+import org.testng.annotations.Guice;
+import org.testng.annotations.Test;
+
+import com.ning.billing.payment.setup.PaymentTestModule;
+
+@Guice(modules = PaymentTestModule.class)
+@Test(groups = "fast")
+public class TestMockPaymentApi extends TestPaymentApi {
+
+}
diff --git a/payment/src/test/java/com/ning/billing/payment/api/TestPaymentApi.java b/payment/src/test/java/com/ning/billing/payment/api/TestPaymentApi.java
new file mode 100644
index 0000000..7f1c53b
--- /dev/null
+++ b/payment/src/test/java/com/ning/billing/payment/api/TestPaymentApi.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.payment.api;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.List;
+import java.util.UUID;
+
+import org.apache.commons.lang.RandomStringUtils;
+import org.joda.time.DateTime;
+import org.testng.annotations.Test;
+
+import com.google.inject.Inject;
+import com.ning.billing.account.api.Account;
+import com.ning.billing.account.api.MockAccountUserApi;
+import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.invoice.api.Invoice;
+import com.ning.billing.invoice.api.MockInvoicePaymentApi;
+import com.ning.billing.invoice.model.DefaultInvoice;
+import com.ning.billing.invoice.model.DefaultInvoiceItem;
+
+public abstract class TestPaymentApi {
+    @Inject
+    protected MockInvoicePaymentApi invoicePaymentApi;
+    @Inject
+    protected MockAccountUserApi accountUserApi;
+    @Inject
+    protected PaymentApi paymentApi;
+
+    protected Account createAccount() {
+        String name = "First" + RandomStringUtils.random(5) + " " + "Last" + RandomStringUtils.random(5);
+        String externalKey = "12345";
+        return accountUserApi.createAccount(UUID.randomUUID(),
+                                            externalKey,
+                                            "user@example.com",
+                                            name,
+                                            name.length(),
+                                            "123-456-7890",
+                                            Currency.USD,
+                                            1,
+                                            null,
+                                            BigDecimal.ZERO);
+    }
+
+    protected Invoice createInvoice(Account account,
+                                    DateTime targetDate,
+                                    Currency currency) {
+        Invoice invoice = new DefaultInvoice(account.getId(), targetDate, currency);
+
+        invoicePaymentApi.add(invoice);
+        return invoice;
+    }
+
+    @Test
+    public void testCreatePayment() {
+        final DateTime now = new DateTime();
+        final Account account = createAccount();
+        final Invoice invoice = createInvoice(account, now, Currency.USD);
+        final BigDecimal amount = new BigDecimal("10.00");
+        final UUID subscriptionId = UUID.randomUUID();
+
+        invoice.add(new DefaultInvoiceItem(invoice.getId(),
+                                           subscriptionId,
+                                           now,
+                                           now.plusMonths(1),
+                                           "Test",
+                                           amount,
+                                           new BigDecimal("1.0"),
+                                           Currency.USD));
+
+        List<Either<PaymentError, PaymentInfo>> results = paymentApi.createPayment(account.getExternalKey(), Arrays.asList(invoice.getId().toString()));
+
+        assertEquals(results.size(), 1);
+        assertTrue(results.get(0).isRight());
+
+        PaymentInfo paymentInfo = results.get(0).getRight();
+
+        assertNotNull(paymentInfo.getId());
+        assertEquals(paymentInfo.getAmount().doubleValue(), amount.doubleValue());
+    }
+}
diff --git a/payment/src/test/java/com/ning/billing/payment/provider/MockPaymentProviderPlugin.java b/payment/src/test/java/com/ning/billing/payment/provider/MockPaymentProviderPlugin.java
index 578fd15..5f9900f 100644
--- a/payment/src/test/java/com/ning/billing/payment/provider/MockPaymentProviderPlugin.java
+++ b/payment/src/test/java/com/ning/billing/payment/provider/MockPaymentProviderPlugin.java
@@ -21,6 +21,8 @@ import java.util.Map;
 import java.util.UUID;
 import java.util.concurrent.ConcurrentHashMap;
 
+import org.joda.time.DateTime;
+
 import com.ning.billing.account.api.Account;
 import com.ning.billing.invoice.api.Invoice;
 import com.ning.billing.payment.api.Either;
@@ -32,10 +34,20 @@ import com.ning.billing.payment.api.PaypalPaymentMethodInfo;
 
 public class MockPaymentProviderPlugin implements PaymentProviderPlugin {
     private final Map<String, PaymentInfo> payments = new ConcurrentHashMap<String, PaymentInfo>();
+    private final Map<String, PaymentProviderAccount> accounts = new ConcurrentHashMap<String, PaymentProviderAccount>();
 
     @Override
     public Either<PaymentError, PaymentInfo> processInvoice(Account account, Invoice invoice) {
-        PaymentInfo payment = new PaymentInfo.Builder().setId(UUID.randomUUID().toString()).build();
+        PaymentInfo payment = new PaymentInfo.Builder().setId(UUID.randomUUID().toString())
+                                            .setAmount(invoice.getAmountOutstanding())
+                                            .setStatus("Processed")
+                                            .setBankIdentificationNumber("1234")
+                                            .setCreatedDate(new DateTime())
+                                            .setEffectiveDate(new DateTime())
+                                            .setPaymentNumber("12345")
+                                            .setReferenceId("12345")
+                                            .setType("Electronic")
+                                            .build();
 
         payments.put(payment.getId(), payment);
         return Either.right(payment);
@@ -55,8 +67,18 @@ public class MockPaymentProviderPlugin implements PaymentProviderPlugin {
 
     @Override
     public Either<PaymentError, PaymentProviderAccount> createPaymentProviderAccount(Account account) {
-        // TODO
-        return Either.left(new PaymentError("unknown", "Not implemented"));
+        if (account != null) {
+            PaymentProviderAccount paymentProviderAccount = accounts.put(account.getExternalKey(),
+                                                                         new PaymentProviderAccount.Builder().setAccountName(account.getExternalKey())
+                                                                                                             .setAccountNumber(account.getExternalKey())
+                                                                                                             .setId(account.getId().toString())
+                                                                                                             .build());
+
+            return Either.right(paymentProviderAccount);
+        }
+        else {
+            return Either.left(new PaymentError("unknown", "Did not get account to create payment provider account"));
+        }
     }
 
     @Override
diff --git a/payment/src/test/java/com/ning/billing/payment/TestNotifyInvoicePaymentApi.java b/payment/src/test/java/com/ning/billing/payment/TestNotifyInvoicePaymentApi.java
new file mode 100644
index 0000000..0867f01
--- /dev/null
+++ b/payment/src/test/java/com/ning/billing/payment/TestNotifyInvoicePaymentApi.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.payment;
+
+import static org.testng.Assert.assertNotNull;
+
+import java.util.UUID;
+
+import org.testng.annotations.Guice;
+import org.testng.annotations.Test;
+
+import com.ning.billing.account.api.Account;
+import com.ning.billing.invoice.api.Invoice;
+import com.ning.billing.payment.api.InvoicePayment;
+import com.ning.billing.payment.setup.PaymentTestModule;
+
+@Guice(modules = PaymentTestModule.class)
+public class TestNotifyInvoicePaymentApi extends TestPaymentProvider {
+
+    @Test
+    public void testNotifyPaymentSuccess() {
+        final Account account = createTestAccount();
+        final Invoice invoice = createTestInvoice(account);
+
+        PaymentAttempt paymentAttempt = new PaymentAttempt(UUID.randomUUID(), invoice);
+        invoicePaymentApi.paymentSuccessful(invoice.getId(),
+                                     invoice.getAmountOutstanding(),
+                                     invoice.getCurrency(),
+                                     paymentAttempt.getPaymentAttemptId(),
+                                     paymentAttempt.getPaymentAttemptDate());
+
+        InvoicePayment invoicePayment = invoicePaymentApi.getInvoicePayment(invoice.getId(), paymentAttempt.getPaymentAttemptId());
+
+        assertNotNull(invoicePayment);
+    }
+
+    @Test
+    public void testNotifyPaymentFailure() {
+        final Account account = createTestAccount();
+        final Invoice invoice = createTestInvoice(account);
+
+        PaymentAttempt paymentAttempt = new PaymentAttempt(UUID.randomUUID(), invoice);
+        invoicePaymentApi.paymentFailed(invoice.getId(),
+                                 paymentAttempt.getPaymentAttemptId(),
+                                 paymentAttempt.getPaymentAttemptDate());
+
+        InvoicePayment invoicePayment = invoicePaymentApi.getInvoicePayment(invoice.getId(), paymentAttempt.getPaymentAttemptId());
+
+        assertNotNull(invoicePayment);
+    }
+
+}
diff --git a/payment/src/test/java/com/ning/billing/payment/TestPaymentProvider.java b/payment/src/test/java/com/ning/billing/payment/TestPaymentProvider.java
index 9be2dc1..5ceba69 100644
--- a/payment/src/test/java/com/ning/billing/payment/TestPaymentProvider.java
+++ b/payment/src/test/java/com/ning/billing/payment/TestPaymentProvider.java
@@ -58,7 +58,7 @@ public class TestPaymentProvider {
     @Inject
     protected MockAccountUserApi accountUserApi;
     @Inject
-    protected MockInvoicePaymentApi invoiceApi;
+    protected MockInvoicePaymentApi invoicePaymentApi;
 
     private MockPaymentInfoReceiver paymentInfoReceiver;
 
@@ -76,18 +76,67 @@ public class TestPaymentProvider {
         eventBus.stop();
     }
 
-    protected Account createAccount() {
+    protected Account createTestAccount() {
         String name = "First" + RandomStringUtils.random(5) + " " + "Last" + RandomStringUtils.random(5);
         String externalKey = "12345";
         return accountUserApi.createAccount(UUID.randomUUID(), externalKey, "user@example.com", name, name.length(), "123-456-7890", Currency.USD, 1, null, BigDecimal.ZERO);
     }
 
+    @Test
+    public void testSimpleInvoice() throws Exception {
+        final Account account = createTestAccount();
+        final Invoice invoice = createTestInvoice(account);
+
+        eventBus.post(createNotificationFor(invoice));
+        await().atMost(1, MINUTES).until(new Callable<Boolean>() {
+            @Override
+            public Boolean call() throws Exception {
+                List<PaymentInfo> processedPayments = paymentInfoReceiver.getProcessedPayments();
+                List<PaymentError> errors = paymentInfoReceiver.getErrors();
+
+                return processedPayments.size() == 1 || errors.size() == 1;
+            }
+        });
+
+        assertFalse(paymentInfoReceiver.getProcessedPayments().isEmpty());
+        assertTrue(paymentInfoReceiver.getErrors().isEmpty());
+
+        final PaymentInfo paymentInfo = paymentInfoReceiver.getProcessedPayments().get(0);
+        final PaymentInfoRequest paymentInfoRequest = new PaymentInfoRequest(account.getId(), paymentInfo.getId());
+
+        paymentInfoReceiver.clear();
+        eventBus.post(paymentInfoRequest);
+        await().atMost(5, MINUTES).until(new Callable<Boolean>() {
+            @Override
+            public Boolean call() throws Exception {
+                List<PaymentInfo> processedPayments = paymentInfoReceiver.getProcessedPayments();
+                List<PaymentError> errors = paymentInfoReceiver.getErrors();
+
+                return processedPayments.size() == 1 || errors.size() == 1;
+            }
+        });
+
+        assertFalse(paymentInfoReceiver.getProcessedPayments().isEmpty());
+        assertTrue(paymentInfoReceiver.getErrors().isEmpty());
+        assertEquals(paymentInfoReceiver.getProcessedPayments().get(0), paymentInfo);
+    }
+
+    protected Invoice createTestInvoice(final Account account) {
+        final DateTime now = new DateTime();
+        final UUID subscriptionId = UUID.randomUUID();
+        final BigDecimal amount = new BigDecimal("10.00");
+        final Invoice invoice = createInvoice(account, now, Currency.USD);
+
+        invoice.add(new DefaultInvoiceItem(invoice.getId(), subscriptionId, now, now.plusMonths(1), "Test", amount, new BigDecimal("1.0"), Currency.USD));
+        return invoice;
+    }
+
     protected Invoice createInvoice(Account account,
                                     DateTime targetDate,
                                     Currency currency) {
         Invoice invoice = new DefaultInvoice(account.getId(), targetDate, currency);
 
-        invoiceApi.add(invoice);
+        invoicePaymentApi.add(invoice);
         return invoice;
     }
 
@@ -120,47 +169,4 @@ public class TestPaymentProvider {
         };
     }
 
-    @Test
-    public void testSimpleInvoice() throws Exception {
-        final Account account = createAccount();
-        final DateTime now = new DateTime();
-        final UUID subscriptionId = UUID.randomUUID();
-        final BigDecimal amount = new BigDecimal("10.00");
-        final Invoice invoice = createInvoice(account, now, Currency.USD);
-
-        invoice.add(new DefaultInvoiceItem(invoice.getId(), subscriptionId, now, now.plusMonths(1), "Test", amount, new BigDecimal("1.0"), Currency.USD));
-
-        eventBus.post(createNotificationFor(invoice));
-        await().atMost(1, MINUTES).until(new Callable<Boolean>() {
-            @Override
-            public Boolean call() throws Exception {
-                List<PaymentInfo> processedPayments = paymentInfoReceiver.getProcessedPayments();
-                List<PaymentError> errors = paymentInfoReceiver.getErrors();
-
-                return processedPayments.size() == 1 || errors.size() == 1;
-            }
-        });
-
-        assertFalse(paymentInfoReceiver.getProcessedPayments().isEmpty());
-        assertTrue(paymentInfoReceiver.getErrors().isEmpty());
-
-        final PaymentInfo paymentInfo = paymentInfoReceiver.getProcessedPayments().get(0);
-        final PaymentInfoRequest paymentInfoRequest = new PaymentInfoRequest(account.getId(), paymentInfo.getId());
-
-        paymentInfoReceiver.clear();
-        eventBus.post(paymentInfoRequest);
-        await().atMost(5, MINUTES).until(new Callable<Boolean>() {
-            @Override
-            public Boolean call() throws Exception {
-                List<PaymentInfo> processedPayments = paymentInfoReceiver.getProcessedPayments();
-                List<PaymentError> errors = paymentInfoReceiver.getErrors();
-
-                return processedPayments.size() == 1 || errors.size() == 1;
-            }
-        });
-
-        assertFalse(paymentInfoReceiver.getProcessedPayments().isEmpty());
-        assertTrue(paymentInfoReceiver.getErrors().isEmpty());
-        assertEquals(paymentInfoReceiver.getProcessedPayments().get(0), paymentInfo);
-    }
 }

pom.xml 2(+1 -1)

diff --git a/pom.xml b/pom.xml
index 1554d9b..20dcd7c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -17,7 +17,7 @@
     <groupId>com.ning.billing</groupId>
     <artifactId>killbill</artifactId>
     <packaging>pom</packaging>
-    <version>0.0.20-SNAPSHOT</version>
+    <version>0.0.21-SNAPSHOT</version>
     <name>killbill</name>
     <description>Library for managing recurring subscriptions and the associated billing</description>
     <url>http://github.com/ning/killbill</url>

util/pom.xml 2(+1 -1)

diff --git a/util/pom.xml b/util/pom.xml
index f6cc6f9..5152ab7 100644
--- a/util/pom.xml
+++ b/util/pom.xml
@@ -13,7 +13,7 @@
     <parent>
         <groupId>com.ning.billing</groupId>
         <artifactId>killbill</artifactId>
-        <version>0.0.20-SNAPSHOT</version>
+        <version>0.0.21-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-util</artifactId>
diff --git a/util/src/main/java/com/ning/billing/util/clock/DefaultClock.java b/util/src/main/java/com/ning/billing/util/clock/DefaultClock.java
index 5db2ac3..8280a15 100644
--- a/util/src/main/java/com/ning/billing/util/clock/DefaultClock.java
+++ b/util/src/main/java/com/ning/billing/util/clock/DefaultClock.java
@@ -41,21 +41,21 @@ public class DefaultClock implements Clock {
         return input.minus(input.getMillisOfSecond());
     }
 
-    public static DateTime addDuration(DateTime input, List<Duration> durations) {
 
+    public static DateTime addOrRemoveDuration(DateTime input, List<Duration> durations, boolean add) {
         DateTime result = input;
         for (Duration cur : durations) {
             switch (cur.getUnit()) {
             case DAYS:
-                result = result.plusDays(cur.getNumber());
+                result = add ? result.plusDays(cur.getNumber()) : result.minusDays(cur.getNumber());
                 break;
 
             case MONTHS:
-                result = result.plusMonths(cur.getNumber());
+                result = add ? result.plusMonths(cur.getNumber()) : result.minusMonths(cur.getNumber());
                 break;
 
             case YEARS:
-                result = result.plusYears(cur.getNumber());
+                result = add ? result.plusYears(cur.getNumber()) : result.minusYears(cur.getNumber());
                 break;
             case UNLIMITED:
             default:
@@ -65,9 +65,23 @@ public class DefaultClock implements Clock {
         return result;
     }
 
+    public static DateTime addDuration(DateTime input, List<Duration> durations) {
+        return addOrRemoveDuration(input, durations, true);
+    }
+
+    public static DateTime removeDuration(DateTime input, List<Duration> durations) {
+        return addOrRemoveDuration(input, durations, false);
+    }
+
     public static DateTime addDuration(DateTime input, Duration duration) {
         List<Duration> list = new ArrayList<Duration>();
         list.add(duration);
-        return addDuration(input, list);
+        return addOrRemoveDuration(input, list, true);
+    }
+
+    public static DateTime removeDuration(DateTime input, Duration duration) {
+        List<Duration> list = new ArrayList<Duration>();
+        list.add(duration);
+        return addOrRemoveDuration(input, list, false);
     }
 }