killbill-aplcache

Details

diff --git a/catalog/src/main/java/org/killbill/billing/catalog/api/user/DefaultCatalogUserApi.java b/catalog/src/main/java/org/killbill/billing/catalog/api/user/DefaultCatalogUserApi.java
index 2f66d29..38fcb86 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/api/user/DefaultCatalogUserApi.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/api/user/DefaultCatalogUserApi.java
@@ -19,15 +19,22 @@ package org.killbill.billing.catalog.api.user;
 import java.io.ByteArrayInputStream;
 import java.io.InputStream;
 import java.net.URI;
+import java.nio.charset.Charset;
 
 import javax.inject.Inject;
 
+import org.joda.time.DateTime;
 import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.catalog.CatalogUpdater;
 import org.killbill.billing.catalog.StandaloneCatalog;
+import org.killbill.billing.catalog.api.BillingMode;
 import org.killbill.billing.catalog.api.Catalog;
 import org.killbill.billing.catalog.api.CatalogApiException;
 import org.killbill.billing.catalog.api.CatalogService;
 import org.killbill.billing.catalog.api.CatalogUserApi;
+import org.killbill.billing.catalog.api.MutableStaticCatalog;
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.SimplePlanDescriptor;
 import org.killbill.billing.catalog.api.StaticCatalog;
 import org.killbill.billing.catalog.caching.CatalogCache;
 import org.killbill.billing.tenant.api.TenantApiException;
@@ -37,6 +44,9 @@ import org.killbill.billing.util.callcontext.CallContext;
 import org.killbill.billing.util.callcontext.InternalCallContextFactory;
 import org.killbill.billing.util.callcontext.TenantContext;
 import org.killbill.xmlloader.XMLLoader;
+import org.killbill.xmlloader.XMLWriter;
+
+import com.google.common.collect.ImmutableList;
 
 public class DefaultCatalogUserApi implements CatalogUserApi {
 
@@ -45,6 +55,7 @@ public class DefaultCatalogUserApi implements CatalogUserApi {
     private final TenantUserApi tenantApi;
     private final CatalogCache catalogCache;
 
+
     @Inject
     public DefaultCatalogUserApi(final CatalogService catalogService,
                                  final TenantUserApi tenantApi,
@@ -85,6 +96,26 @@ public class DefaultCatalogUserApi implements CatalogUserApi {
         }
     }
 
+    @Override
+    public void addSimplePlan(final SimplePlanDescriptor descriptor, final DateTime effectiveDate, final CallContext callContext) throws CatalogApiException {
+
+        try {
+            final StandaloneCatalog currentCatalog = (StandaloneCatalog) getCurrentCatalog("dummy", callContext);
+            final CatalogUpdater catalogUpdater = (currentCatalog != null) ?
+                                                  new CatalogUpdater(currentCatalog) :
+                                                  new CatalogUpdater("dummy", BillingMode.IN_ARREAR, effectiveDate, descriptor.getCurrency());
+
+            catalogUpdater.addSimplePlanDescriptor(descriptor);
+
+            final InternalTenantContext internalTenantContext = createInternalTenantContext(callContext);
+            catalogCache.clearCatalog(internalTenantContext);
+            tenantApi.addTenantKeyValue(TenantKey.CATALOG.toString(), catalogUpdater.getCatalogXML(), callContext);
+        } catch (TenantApiException e) {
+            throw new CatalogApiException(e);
+        }
+    }
+
+
     private InternalTenantContext createInternalTenantContext(final TenantContext tenantContext) {
         // Only tenantRecordId will be populated -- this is important to always create the (ehcache) key the same way
         return internalCallContextFactory.createInternalTenantContextWithoutAccountRecordId(tenantContext);
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/api/user/DefaultSimplePlanDescriptor.java b/catalog/src/main/java/org/killbill/billing/catalog/api/user/DefaultSimplePlanDescriptor.java
new file mode 100644
index 0000000..feffdd2
--- /dev/null
+++ b/catalog/src/main/java/org/killbill/billing/catalog/api/user/DefaultSimplePlanDescriptor.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
+ *
+ * The Billing Project 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 org.killbill.billing.catalog.api.user;
+
+import java.math.BigDecimal;
+
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.catalog.api.SimplePlanDescriptor;
+import org.killbill.billing.catalog.api.TimeUnit;
+
+public class DefaultSimplePlanDescriptor implements SimplePlanDescriptor {
+
+    private final String planId;
+    private final String productName;
+    private final Currency currency;
+    private final BigDecimal amount;
+    private final BillingPeriod billingPeriod;
+    private final int trialLength;
+    private final TimeUnit trialTimeUnit;
+
+    public DefaultSimplePlanDescriptor(final String planId,
+                                       final String productName,
+                                       final Currency currency,
+                                       final BigDecimal amount,
+                                       final BillingPeriod billingPeriod,
+                                       final int trialLength,
+                                       final TimeUnit trialTimeUnit) {
+        this.planId = planId;
+        this.productName = productName;
+        this.currency = currency;
+        this.amount = amount;
+        this.billingPeriod = billingPeriod;
+        this.trialLength = trialLength;
+        this.trialTimeUnit = trialTimeUnit;
+    }
+
+    @Override
+    public String getPlanId() {
+        return planId;
+    }
+
+    @Override
+    public String getProductName() {
+        return productName;
+    }
+
+    @Override
+    public Currency getCurrency() {
+        return currency;
+    }
+
+    @Override
+    public BigDecimal getAmount() {
+        return amount;
+    }
+
+    @Override
+    public BillingPeriod getBillingPeriod() {
+        return billingPeriod;
+    }
+
+    @Override
+    public int getTrialLength() {
+        return trialLength;
+    }
+
+    @Override
+    public TimeUnit getTrialTimeUnit() {
+        return trialTimeUnit;
+    }
+
+}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/CatalogUpdater.java b/catalog/src/main/java/org/killbill/billing/catalog/CatalogUpdater.java
new file mode 100644
index 0000000..5390adc
--- /dev/null
+++ b/catalog/src/main/java/org/killbill/billing/catalog/CatalogUpdater.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright 2016 Groupon, Inc
+ * Copyright 2016 The Billing Project, LLC
+ *
+ * The Billing Project 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 org.killbill.billing.catalog;
+
+import java.math.BigDecimal;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import org.joda.time.DateTime;
+import org.killbill.billing.catalog.api.BillingActionPolicy;
+import org.killbill.billing.catalog.api.BillingAlignment;
+import org.killbill.billing.catalog.api.BillingMode;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.catalog.api.PhaseType;
+import org.killbill.billing.catalog.api.PlanAlignmentChange;
+import org.killbill.billing.catalog.api.PlanAlignmentCreate;
+import org.killbill.billing.catalog.api.PriceListSet;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.catalog.api.SimplePlanDescriptor;
+import org.killbill.billing.catalog.api.TimeUnit;
+import org.killbill.billing.catalog.rules.DefaultCaseBillingAlignment;
+import org.killbill.billing.catalog.rules.DefaultCaseCancelPolicy;
+import org.killbill.billing.catalog.rules.DefaultCaseChangePlanAlignment;
+import org.killbill.billing.catalog.rules.DefaultCaseChangePlanPolicy;
+import org.killbill.billing.catalog.rules.DefaultCaseCreateAlignment;
+import org.killbill.billing.catalog.rules.DefaultCasePriceList;
+import org.killbill.billing.catalog.rules.DefaultPlanRules;
+import org.killbill.xmlloader.XMLWriter;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+
+public class CatalogUpdater {
+
+    private static URI DUMMY_URI;
+
+    {
+        try {
+            DUMMY_URI = new URI("dummy");
+        } catch (URISyntaxException e) {
+        }
+    }
+
+    ;
+
+    private static final DefaultPriceList DEFAULT_PRICELIST = new DefaultPriceList().setName(PriceListSet.DEFAULT_PRICELIST_NAME);
+
+    private final DefaultMutableStaticCatalog catalog;
+
+    public CatalogUpdater(final StandaloneCatalog standaloneCatalog) {
+        this.catalog = new DefaultMutableStaticCatalog(standaloneCatalog);
+    }
+
+    public CatalogUpdater(final String catalogName, final BillingMode billingMode, final DateTime effectiveDate, final Currency... currencies) {
+        final StandaloneCatalog tmp = new StandaloneCatalog()
+                .setCatalogName(catalogName)
+                .setEffectiveDate(effectiveDate.toDate())
+                .setSupportedCurrencies(currencies)
+                .setRecurringBillingMode(billingMode)
+                .setProducts(new DefaultProduct[0])
+                .setPlans(new DefaultPlan[0])
+                .setPriceLists(new DefaultPriceListSet(DEFAULT_PRICELIST, new DefaultPriceList[0]))
+                .setPlanRules(getSaneDefaultPlanRules());
+        tmp.initialize(tmp, DUMMY_URI);
+
+        this.catalog = new DefaultMutableStaticCatalog(tmp);
+    }
+
+    public StandaloneCatalog getCatalog() {
+        return catalog;
+    }
+
+    public String getCatalogXML() throws CatalogApiException {
+        try {
+            return XMLWriter.writeXML(catalog, StandaloneCatalog.class);
+        } catch (final Exception e) {
+            // TODO
+            throw new RuntimeException(e);
+        }
+    }
+
+    public void addSimplePlanDescriptor(final SimplePlanDescriptor desc) throws CatalogApiException {
+
+        validateSimplePlanDescriptor(desc);
+
+        DefaultProduct product = getExistingProduct(desc.getProductName());
+        if (product == null) {
+            product = new DefaultProduct();
+            product.setName(desc.getProductName());
+            product.setCatagory(ProductCategory.BASE); // TODO
+            product.initialize(catalog, DUMMY_URI);
+            catalog.addProduct(product);
+        }
+
+        if (!isCurrencySupported(desc.getCurrency())) {
+            catalog.addCurrency(desc.getCurrency());
+        }
+
+        DefaultPlan plan = getExistingPlan(desc.getPlanId());
+        if (plan == null) {
+            plan = new DefaultPlan();
+            plan.setName(desc.getPlanId());
+            plan.setPriceListName(DEFAULT_PRICELIST.getName());
+            plan.setProduct(product);
+
+            if (desc.getTrialLength() > 0 && desc.getTrialTimeUnit() != TimeUnit.UNLIMITED) {
+                final DefaultPlanPhase trialPhase = new DefaultPlanPhase();
+                trialPhase.setPhaseType(PhaseType.TRIAL);
+                trialPhase.setDuration(new DefaultDuration().setUnit(desc.getTrialTimeUnit()).setNumber(desc.getTrialLength()));
+                trialPhase.setFixed(new DefaultFixed().setFixedPrice(new DefaultInternationalPrice().setPrices(new DefaultPrice[]{new DefaultPrice().setCurrency(desc.getCurrency()).setValue(BigDecimal.ZERO)})));
+                plan.setInitialPhases(new DefaultPlanPhase[]{trialPhase});
+            }
+        } else {
+            validateExistingPlan(plan, desc);
+        }
+
+        DefaultPlanPhase evergreenPhase = plan.getFinalPhase();
+        if (evergreenPhase == null) {
+            evergreenPhase = new DefaultPlanPhase();
+            evergreenPhase.setPhaseType(PhaseType.EVERGREEN);
+            evergreenPhase.setDuration(new DefaultDuration()
+                                               .setUnit(TimeUnit.UNLIMITED));
+            plan.setFinalPhase(evergreenPhase);
+        }
+
+        DefaultRecurring recurring = (DefaultRecurring) evergreenPhase.getRecurring();
+        if (recurring == null) {
+            recurring = new DefaultRecurring();
+            recurring.setBillingPeriod(desc.getBillingPeriod());
+            recurring.setRecurringPrice(new DefaultInternationalPrice().setPrices(new DefaultPrice[0]));
+            evergreenPhase.setRecurring(recurring);
+        }
+
+        try {
+            recurring.getRecurringPrice().getPrice(desc.getCurrency());
+        } catch (CatalogApiException ignore) {
+            catalog.addRecurringPriceToPlan(recurring.getRecurringPrice(), new DefaultPrice().setCurrency(desc.getCurrency()).setValue(desc.getAmount()));
+        }
+
+        // TODO Ordering breaks
+        catalog.addPlan(plan);
+        plan.initialize(catalog, DUMMY_URI);
+    }
+
+    private void validateExistingPlan(final DefaultPlan plan, final SimplePlanDescriptor desc) {
+
+        boolean failedValidation = false;
+
+        //
+        // TRIAL VALIDATION
+        //
+        // We only support adding new Plan with NO TRIAL or $0 TRIAL. Existing Plan not matching such criteria are incompatible
+        if (plan.getInitialPhases().length > 1 ||
+            (plan.getInitialPhases().length == 1 &&
+             (plan.getInitialPhases()[0].getPhaseType() != PhaseType.TRIAL || !plan.getInitialPhases()[0].getFixed().getPrice().isZero()))) {
+            failedValidation = true;
+        } else {
+
+            final boolean isDescConfiguredWithTrial = desc.getTrialLength() > 0 && desc.getTrialTimeUnit() != TimeUnit.UNLIMITED;
+            final boolean isPlanConfiguredWithTrial = plan.getInitialPhases().length == 1;
+            // Current plan has trial and desc does not or reverse
+            if ((isDescConfiguredWithTrial && !isPlanConfiguredWithTrial) ||
+                (!isDescConfiguredWithTrial && isPlanConfiguredWithTrial)) {
+                failedValidation = true;
+            // Both have trials , check they match
+            } else if (isDescConfiguredWithTrial && isPlanConfiguredWithTrial) {
+                if (plan.getInitialPhases()[0].getDuration().getUnit() != desc.getTrialTimeUnit() ||
+                    plan.getInitialPhases()[0].getDuration().getNumber() != desc.getTrialLength()) {
+                    failedValidation = true;
+                }
+            }
+        }
+
+        //
+        // RECURRING VALIDATION
+        //
+        if (!failedValidation) {
+            // Desc only supports EVERGREEN Phase
+            if (plan.getFinalPhase().getPhaseType() != PhaseType.EVERGREEN) {
+                failedValidation = true;
+            } else {
+
+                // Should be same recurring BillingPeriod
+                if (plan.getFinalPhase().getRecurring().getBillingPeriod() != desc.getBillingPeriod()) {
+                    failedValidation = true;
+                } else {
+                    try {
+                        final BigDecimal currentAmount = plan.getFinalPhase().getRecurring().getRecurringPrice().getPrice(desc.getCurrency());
+                        if (currentAmount.compareTo(desc.getAmount()) != 0) {
+                            failedValidation = true;
+                        }
+                    } catch (CatalogApiException ignoreIfCurrencyIsCurrentlyUndefined) {}
+                }
+            }
+        }
+
+
+        if (failedValidation) {
+            // TODO
+            throw new RuntimeException("Incompatible plan ");
+        }
+    }
+
+
+
+    private boolean isCurrencySupported(final Currency targetCurrency) {
+        return Iterables.any(ImmutableList.copyOf(catalog.getCurrentSupportedCurrencies()), new Predicate<Currency>() {
+            @Override
+            public boolean apply(final Currency input) {
+                return input.equals(targetCurrency);
+            }
+        });
+    }
+
+    private void validateSimplePlanDescriptor(final SimplePlanDescriptor descriptor) throws CatalogApiException {
+        if (getExistingPlan(descriptor.getPlanId()) != null) {
+            // TODO
+            throw new RuntimeException("Existing plan" + descriptor.getPlanId());
+        }
+    }
+
+    private DefaultProduct getExistingProduct(final String productName) {
+        return Iterables.tryFind(ImmutableList.copyOf(catalog.getCurrentProducts()), new Predicate<DefaultProduct>() {
+            @Override
+            public boolean apply(final DefaultProduct input) {
+                return input.getName().equals(productName);
+            }
+        }).orNull();
+    }
+
+    private DefaultPlan getExistingPlan(final String planName) {
+        return Iterables.tryFind(ImmutableList.copyOf(catalog.getCurrentPlans()), new Predicate<DefaultPlan>() {
+            @Override
+            public boolean apply(final DefaultPlan input) {
+                return input.getName().equals(planName);
+            }
+        }).orNull();
+    }
+
+    private DefaultPlanRules getSaneDefaultPlanRules() {
+
+        final DefaultCaseChangePlanPolicy[] changePolicy = new DefaultCaseChangePlanPolicy[1];
+        changePolicy[0] = new DefaultCaseChangePlanPolicy();
+        changePolicy[0].setPolicy(BillingActionPolicy.IMMEDIATE);
+
+        final DefaultCaseChangePlanAlignment[] changeAlignment = new DefaultCaseChangePlanAlignment[1];
+        changeAlignment[0] = new DefaultCaseChangePlanAlignment();
+        changeAlignment[0].setAlignment(PlanAlignmentChange.START_OF_BUNDLE);
+
+        final DefaultCaseCancelPolicy[] cancelPolicy = new DefaultCaseCancelPolicy[1];
+        cancelPolicy[0] = new DefaultCaseCancelPolicy();
+        cancelPolicy[0].setPolicy(BillingActionPolicy.IMMEDIATE);
+
+        final DefaultCaseCreateAlignment[] createAlignment = new DefaultCaseCreateAlignment[1];
+        createAlignment[0] = new DefaultCaseCreateAlignment();
+        createAlignment[0].setAlignment(PlanAlignmentCreate.START_OF_BUNDLE);
+
+        final DefaultCaseBillingAlignment[] billingAlignmentCase = new DefaultCaseBillingAlignment[1];
+        billingAlignmentCase[0] = new DefaultCaseBillingAlignment();
+        billingAlignmentCase[0].setAlignment(BillingAlignment.ACCOUNT);
+
+        final DefaultCasePriceList[] priceList = new DefaultCasePriceList[1];
+        priceList[0] = new DefaultCasePriceList();
+        priceList[0].setToPriceList(DEFAULT_PRICELIST);
+
+        return new DefaultPlanRules()
+                .setChangeCase(changePolicy)
+                .setChangeAlignmentCase(changeAlignment)
+                .setCancelCase(cancelPolicy)
+                .setCreateAlignmentCase(createAlignment)
+                .setBillingAlignmentCase(billingAlignmentCase)
+                .setPriceListCase(priceList);
+    }
+
+}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultMutableStaticCatalog.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultMutableStaticCatalog.java
new file mode 100644
index 0000000..98ce0da
--- /dev/null
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultMutableStaticCatalog.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
+ *
+ * The Billing Project 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 org.killbill.billing.catalog;
+
+import java.lang.reflect.Array;
+import java.util.Date;
+
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.CatalogEntity;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.catalog.api.InternationalPrice;
+import org.killbill.billing.catalog.api.MutableStaticCatalog;
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PlanPhasePriceOverridesWithCallContext;
+import org.killbill.billing.catalog.api.PlanSpecifier;
+import org.killbill.billing.catalog.api.Price;
+import org.killbill.billing.catalog.api.PriceList;
+import org.killbill.billing.catalog.api.Product;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+
+public class DefaultMutableStaticCatalog extends StandaloneCatalog implements MutableStaticCatalog {
+
+    public DefaultMutableStaticCatalog() {
+    }
+
+    public DefaultMutableStaticCatalog(final Date effectiveDate) {
+        super(effectiveDate);
+    }
+
+    public DefaultMutableStaticCatalog(final StandaloneCatalog input) {
+        this.setCatalogName(input.getCatalogName())
+            .setEffectiveDate(input.getEffectiveDate())
+            .setSupportedCurrencies(input.getCurrentSupportedCurrencies())
+            .setUnits(input.getCurrentUnits())
+            .setProducts(input.getCurrentProducts())
+            .setPlans(input.getCurrentPlans())
+            .setRecurringBillingMode(input.getRecurringBillingMode())
+            .setPlanRules(input.getPlanRules())
+            .setPriceLists(input.getPriceLists());
+        initialize(this, null);
+    }
+
+
+    @Override
+    public void addCurrency(final Currency currency) throws CatalogApiException {
+        final Currency [] newEntries = allocateNewEntries(getCurrentSupportedCurrencies(), currency);
+        setSupportedCurrencies(newEntries);
+    }
+
+    @Override
+    public void addProduct(final Product product) throws CatalogApiException {
+        final Product[] newEntries = allocateNewEntries(getCurrentProducts(), product);
+        setProducts((DefaultProduct[]) newEntries);
+    }
+
+    @Override
+    public void addPlan(final Plan plan) throws CatalogApiException {
+        final Plan[] newEntries = allocateNewEntries(getCurrentPlans(), plan);
+        setPlans((DefaultPlan[]) newEntries);
+
+        final DefaultPriceList priceList = getPriceLists().findPriceListFrom(plan.getPriceListName());
+        priceList.setPlans((DefaultPlan[])newEntries);
+    }
+
+    @Override
+    public void addPriceList(final PriceList priceList) throws CatalogApiException {
+        final PriceList[] newEntries = allocateNewEntries(getPriceLists().getChildPriceLists(), priceList);
+        final DefaultPriceListSet priceListSet = new DefaultPriceListSet((PriceListDefault) getPriceLists().getDefaultPricelist(), (DefaultPriceList[]) newEntries);
+        setPriceLists(priceListSet);
+    }
+
+
+    public void addRecurringPriceToPlan(final DefaultInternationalPrice currentPrices, final Price newPrice) throws CatalogApiException {
+        final Price [] newEntries = allocateNewEntries(currentPrices.getPrices(), newPrice);
+        currentPrices.setPrices((DefaultPrice []) newEntries);
+    }
+
+    private <T> T [] allocateNewEntries(T [] existingEntries, T newEntry) throws CatalogApiException  {
+        if (newEntry instanceof CatalogEntity) {
+            final CatalogEntity newCatalogEntity = (CatalogEntity) newEntry;
+            final ImmutableList<CatalogEntity> existingCatalogEntries = ImmutableList.<CatalogEntity>copyOf((CatalogEntity []) existingEntries);
+            final CatalogEntity existing = Iterables.tryFind(existingCatalogEntries, new Predicate<CatalogEntity>() {
+                @Override
+                public boolean apply(final CatalogEntity input) {
+                    return input.getName().equals(newCatalogEntity.getName());
+                }
+            }).orNull();
+
+            if (existing != null) {
+                //throw new CatalogApiException();
+                throw new IllegalStateException("Already existing " + newCatalogEntity.getName());
+            }
+        } else if (newEntry instanceof Currency) {
+            // TODO
+        }
+        final T [] newEntries = (T[]) Array.newInstance(newEntry.getClass(), existingEntries.length + 1);
+        for (int i = 0 ; i < newEntries.length + 1; i++) {
+            if (i < newEntries.length - 1) {
+                newEntries[i] = existingEntries[i];
+            } else {
+                newEntries[newEntries.length - 1] = newEntry;
+            }
+        }
+        return newEntries;
+    }
+
+}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultPriceListSet.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPriceListSet.java
index 765d56a..236cdef 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/DefaultPriceListSet.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPriceListSet.java
@@ -48,7 +48,7 @@ public class DefaultPriceListSet extends ValidatingConfig<StandaloneCatalog> imp
         }
     }
 
-    public DefaultPriceListSet(final PriceListDefault defaultPricelist, final DefaultPriceList[] childPriceLists) {
+    public DefaultPriceListSet(final DefaultPriceList defaultPricelist, final DefaultPriceList[] childPriceLists) {
         this.defaultPricelist = defaultPricelist;
         this.childPriceLists = childPriceLists != null ? childPriceLists : new DefaultPriceList[0];
     }
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/io/TestXMLWriter.java b/catalog/src/test/java/org/killbill/billing/catalog/io/TestXMLWriter.java
new file mode 100644
index 0000000..5d89b63
--- /dev/null
+++ b/catalog/src/test/java/org/killbill/billing/catalog/io/TestXMLWriter.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
+ *
+ * The Billing Project 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 org.killbill.billing.catalog.io;
+
+import java.io.ByteArrayInputStream;
+import java.math.BigDecimal;
+import java.net.URI;
+import java.nio.charset.Charset;
+
+import org.killbill.billing.catalog.CatalogTestSuiteNoDB;
+import org.killbill.billing.catalog.DefaultDuration;
+import org.killbill.billing.catalog.DefaultFixed;
+import org.killbill.billing.catalog.DefaultInternationalPrice;
+import org.killbill.billing.catalog.DefaultMutableStaticCatalog;
+import org.killbill.billing.catalog.DefaultPlan;
+import org.killbill.billing.catalog.DefaultPlanPhase;
+import org.killbill.billing.catalog.DefaultPrice;
+import org.killbill.billing.catalog.DefaultPriceListSet;
+import org.killbill.billing.catalog.DefaultProduct;
+import org.killbill.billing.catalog.DefaultRecurring;
+import org.killbill.billing.catalog.StandaloneCatalog;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.catalog.api.MutableStaticCatalog;
+import org.killbill.billing.catalog.api.PhaseType;
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.catalog.api.TimeUnit;
+import org.killbill.xmlloader.XMLLoader;
+import org.killbill.xmlloader.XMLWriter;
+import org.testng.annotations.Test;
+
+import com.google.common.io.Resources;
+
+import static org.testng.Assert.assertEquals;
+
+public class TestXMLWriter extends CatalogTestSuiteNoDB {
+
+    // Verify we can marshall/unmarshall a (fairly complex catalog) catalog and get back the same result (Required to support catalog update)
+    @Test(groups = "fast")
+    public void testMarshallUnmarshall() throws Exception {
+        final StandaloneCatalog catalog = XMLLoader.getObjectFromString(Resources.getResource("SpyCarAdvanced.xml").toExternalForm(), StandaloneCatalog.class);
+        final String oldCatalogStr = XMLWriter.writeXML(catalog, StandaloneCatalog.class);
+
+        //System.err.println(oldCatalogStr);
+
+        final StandaloneCatalog oldCatalog = XMLLoader.getObjectFromStream(new URI("dummy"), new ByteArrayInputStream(oldCatalogStr.getBytes(Charset.forName("UTF-8"))), StandaloneCatalog.class);
+        final String oldCatalogStr2 = XMLWriter.writeXML(oldCatalog, StandaloneCatalog.class);
+        assertEquals(oldCatalogStr2, oldCatalogStr);
+    }
+
+
+    @Test(groups = "fast")
+    public void testAddPlan() throws Exception {
+        final StandaloneCatalog catalog = XMLLoader.getObjectFromString(Resources.getResource("SpyCarBasic.xml").toExternalForm(), StandaloneCatalog.class);
+
+        final MutableStaticCatalog mutableCatalog = new DefaultMutableStaticCatalog(catalog);
+
+        final DefaultProduct newProduct = new DefaultProduct();
+        newProduct.setName("Dynamic");
+        newProduct.setCatagory(ProductCategory.BASE);
+        newProduct.initialize((StandaloneCatalog) mutableCatalog, new URI("dummy"));
+
+        mutableCatalog.addProduct(newProduct);
+
+        final DefaultPlanPhase trialPhase = new DefaultPlanPhase();
+        trialPhase.setPhaseType(PhaseType.TRIAL);
+        trialPhase.setDuration(new DefaultDuration().setUnit(TimeUnit.DAYS).setNumber(14));
+        trialPhase.setFixed(new DefaultFixed().setFixedPrice(new DefaultInternationalPrice().setPrices(new DefaultPrice[]{new DefaultPrice().setCurrency(Currency.USD).setValue(BigDecimal.ZERO)})));
+
+        final DefaultPlanPhase evergreenPhase = new DefaultPlanPhase();
+        evergreenPhase.setPhaseType(PhaseType.EVERGREEN);
+        evergreenPhase.setDuration(new DefaultDuration().setUnit(TimeUnit.MONTHS).setNumber(1));
+        evergreenPhase.setRecurring(new DefaultRecurring().setBillingPeriod(BillingPeriod.MONTHLY).setRecurringPrice(new DefaultInternationalPrice().setPrices(new DefaultPrice[]{new DefaultPrice().setCurrency(Currency.USD).setValue(BigDecimal.TEN)})));
+
+        final DefaultPlan newPlan = new DefaultPlan();
+        newPlan.setName("dynamic-monthly");
+        newPlan.setPriceListName(DefaultPriceListSet.DEFAULT_PRICELIST_NAME);
+        newPlan.setProduct(newProduct);
+        newPlan.setInitialPhases(new DefaultPlanPhase[]{trialPhase});
+        newPlan.setFinalPhase(evergreenPhase);
+        // TODO Ordering breaks
+        mutableCatalog.addPlan(newPlan);
+        newPlan.initialize((StandaloneCatalog) mutableCatalog, new URI("dummy"));
+
+        final String newCatalogStr = XMLWriter.writeXML((StandaloneCatalog) mutableCatalog, StandaloneCatalog.class);
+        final StandaloneCatalog newCatalog = XMLLoader.getObjectFromStream(new URI("dummy"), new ByteArrayInputStream(newCatalogStr.getBytes(Charset.forName("UTF-8"))), StandaloneCatalog.class);
+        assertEquals(newCatalog.getCurrentPlans().length, catalog.getCurrentPlans().length + 1);
+
+        final Plan plan = newCatalog.findCurrentPlan("dynamic-monthly");
+        assertEquals(plan.getName(), "dynamic-monthly");
+        assertEquals(plan.getPriceListName(), DefaultPriceListSet.DEFAULT_PRICELIST_NAME);
+        assertEquals(plan.getProduct().getName(), "Dynamic");
+        assertEquals(plan.getProduct().getCategory(), ProductCategory.BASE);
+        assertEquals(plan.getInitialPhases().length, 1);
+        assertEquals(plan.getInitialPhases()[0].getName(), "dynamic-monthly-trial");
+        assertEquals(plan.getInitialPhases()[0].getPhaseType(), PhaseType.TRIAL);
+        assertEquals(plan.getInitialPhases()[0].getFixed().getPrice().getPrices().length, 1);
+        assertEquals(plan.getInitialPhases()[0].getFixed().getPrice().getPrices()[0].getCurrency(), Currency.USD);
+        assertEquals(plan.getInitialPhases()[0].getFixed().getPrice().getPrices()[0].getValue(), BigDecimal.ZERO);
+
+        assertEquals(plan.getFinalPhase().getName(), "dynamic-monthly-evergreen");
+        assertEquals(plan.getFinalPhase().getPhaseType(), PhaseType.EVERGREEN);
+        assertEquals(plan.getFinalPhase().getRecurring().getBillingPeriod(), BillingPeriod.MONTHLY);
+        assertEquals(plan.getFinalPhase().getRecurring().getRecurringPrice().getPrices().length, 1);
+        assertEquals(plan.getFinalPhase().getRecurring().getRecurringPrice().getPrices()[0].getCurrency(), Currency.USD);
+        assertEquals(plan.getFinalPhase().getRecurring().getRecurringPrice().getPrices()[0].getValue(), BigDecimal.TEN);
+    }
+
+}
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/TestCatalogUpdater.java b/catalog/src/test/java/org/killbill/billing/catalog/TestCatalogUpdater.java
new file mode 100644
index 0000000..9c98a8d
--- /dev/null
+++ b/catalog/src/test/java/org/killbill/billing/catalog/TestCatalogUpdater.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2014-2016 Groupon, Inc
+ * Copyright 2014-2016 The Billing Project, LLC
+ *
+ * The Billing Project 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 org.killbill.billing.catalog;
+
+import java.math.BigDecimal;
+
+import org.joda.time.DateTime;
+import org.killbill.billing.catalog.api.BillingMode;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.catalog.api.PhaseType;
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PriceList;
+import org.killbill.billing.catalog.api.Product;
+import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.catalog.api.SimplePlanDescriptor;
+import org.killbill.billing.catalog.api.TimeUnit;
+import org.killbill.billing.catalog.api.user.DefaultSimplePlanDescriptor;
+import org.killbill.xmlloader.XMLLoader;
+import org.testng.annotations.Test;
+
+import com.google.common.io.Resources;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+
+public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
+
+    @Test(groups = "fast")
+    public void testAddNoTrialPlanOnFirstCatalog() throws CatalogApiException {
+
+        final DateTime now = clock.getUTCNow();
+        final SimplePlanDescriptor desc = new DefaultSimplePlanDescriptor("foo-monthly", "Foo", Currency.EUR, BigDecimal.TEN, BillingPeriod.MONTHLY, 0, TimeUnit.UNLIMITED);
+
+        final CatalogUpdater catalogUpdater = new CatalogUpdater("dummy", BillingMode.IN_ARREAR, now, desc.getCurrency());
+
+        catalogUpdater.addSimplePlanDescriptor(desc);
+
+        final StandaloneCatalog catalog = catalogUpdater.getCatalog();
+
+        assertEquals(catalog.getCurrentProducts().length, 1);
+
+        final Product product = catalog.getCurrentProducts()[0];
+        assertEquals(product.getName(), "Foo");
+        assertEquals(product.getCategory(), ProductCategory.BASE);
+
+        assertEquals(catalog.getCurrentPlans().length, 1);
+
+        final Plan plan = catalog.findCurrentPlan("foo-monthly");
+        assertEquals(plan.getName(), "foo-monthly");
+
+        assertEquals(plan.getInitialPhases().length, 0);
+        assertEquals(plan.getFinalPhase().getPhaseType(), PhaseType.EVERGREEN);
+        assertNull(plan.getFinalPhase().getFixed());
+        assertEquals(plan.getFinalPhase().getName(), "foo-monthly-evergreen");
+
+        assertEquals(plan.getFinalPhase().getRecurring().getBillingPeriod(), BillingPeriod.MONTHLY);
+        assertEquals(plan.getFinalPhase().getRecurring().getRecurringPrice().getPrices().length, 1);
+        assertEquals(plan.getFinalPhase().getRecurring().getRecurringPrice().getPrices()[0].getValue(), BigDecimal.TEN);
+        assertEquals(plan.getFinalPhase().getRecurring().getRecurringPrice().getPrices()[0].getCurrency(), Currency.EUR);
+
+        assertEquals(catalog.getPriceLists().getAllPriceLists().size(), 1);
+        final PriceList priceList = catalog.getPriceLists().getAllPriceLists().get(0);
+        assertEquals(priceList.getName(), new PriceListDefault().getName());
+        assertEquals(priceList.getPlans().length, 1);
+        assertEquals(priceList.getPlans()[0].getName(), "foo-monthly");
+    }
+
+
+    @Test(groups = "fast")
+    public void testAddTrialPlanOnFirstCatalog() throws CatalogApiException {
+
+        final DateTime now = clock.getUTCNow();
+        final SimplePlanDescriptor desc = new DefaultSimplePlanDescriptor("foo-monthly", "Foo", Currency.EUR, BigDecimal.TEN, BillingPeriod.MONTHLY, 14, TimeUnit.DAYS);
+
+        final CatalogUpdater catalogUpdater = new CatalogUpdater("dummy", BillingMode.IN_ARREAR, now, desc.getCurrency());
+
+        catalogUpdater.addSimplePlanDescriptor(desc);
+
+        final StandaloneCatalog catalog = catalogUpdater.getCatalog();
+
+        assertEquals(catalog.getCurrentProducts().length, 1);
+
+        final Product product = catalog.getCurrentProducts()[0];
+        assertEquals(product.getName(), "Foo");
+        assertEquals(product.getCategory(), ProductCategory.BASE);
+
+        assertEquals(catalog.getCurrentPlans().length, 1);
+
+        final Plan plan = catalog.findCurrentPlan("foo-monthly");
+        assertEquals(plan.getName(), "foo-monthly");
+
+        assertEquals(plan.getInitialPhases().length, 1);
+        assertEquals(plan.getInitialPhases()[0].getPhaseType(), PhaseType.TRIAL);
+        assertEquals(plan.getInitialPhases()[0].getFixed().getPrice().getPrices().length, 1);
+        assertEquals(plan.getInitialPhases()[0].getFixed().getPrice().getPrices()[0].getCurrency(), Currency.EUR);
+        assertEquals(plan.getInitialPhases()[0].getFixed().getPrice().getPrices()[0].getValue(), BigDecimal.ZERO);
+        assertEquals(plan.getInitialPhases()[0].getName(), "foo-monthly-trial");
+
+        assertEquals(plan.getFinalPhase().getPhaseType(), PhaseType.EVERGREEN);
+        assertNull(plan.getFinalPhase().getFixed());
+        assertEquals(plan.getFinalPhase().getName(), "foo-monthly-evergreen");
+
+        assertEquals(plan.getFinalPhase().getRecurring().getBillingPeriod(), BillingPeriod.MONTHLY);
+        assertEquals(plan.getFinalPhase().getRecurring().getRecurringPrice().getPrices().length, 1);
+        assertEquals(plan.getFinalPhase().getRecurring().getRecurringPrice().getPrices()[0].getValue(), BigDecimal.TEN);
+        assertEquals(plan.getFinalPhase().getRecurring().getRecurringPrice().getPrices()[0].getCurrency(), Currency.EUR);
+
+        assertEquals(catalog.getPriceLists().getAllPriceLists().size(), 1);
+        final PriceList priceList = catalog.getPriceLists().getAllPriceLists().get(0);
+        assertEquals(priceList.getName(), new PriceListDefault().getName());
+        assertEquals(priceList.getPlans().length, 1);
+        assertEquals(priceList.getPlans()[0].getName(), "foo-monthly");
+    }
+
+
+
+    @Test(groups = "fast")
+    public void testAddPlanOnExistingCatalog() throws Exception {
+
+        final StandaloneCatalog originalCatalog = XMLLoader.getObjectFromString(Resources.getResource("SpyCarBasic.xml").toExternalForm(), StandaloneCatalog.class);
+        assertEquals(originalCatalog.getPriceLists().getAllPriceLists().size(), 1);
+        assertEquals(originalCatalog.getPriceLists().getAllPriceLists().get(0).getName(), new PriceListDefault().getName());
+        assertEquals(originalCatalog.getPriceLists().getAllPriceLists().get(0).getPlans().length, 3);
+
+        final CatalogUpdater catalogUpdater = new CatalogUpdater(originalCatalog);
+
+        final SimplePlanDescriptor desc = new DefaultSimplePlanDescriptor("standard-annual", "Standard", Currency.USD, BigDecimal.TEN, BillingPeriod.MONTHLY, 0, TimeUnit.UNLIMITED);
+        catalogUpdater.addSimplePlanDescriptor(desc);
+
+        final StandaloneCatalog catalog = catalogUpdater.getCatalog();
+
+        final Plan plan = catalog.findCurrentPlan("standard-annual");
+        assertEquals(plan.getName(), "standard-annual");
+
+        assertEquals(plan.getInitialPhases().length, 0);
+        assertEquals(plan.getFinalPhase().getPhaseType(), PhaseType.EVERGREEN);
+        assertNull(plan.getFinalPhase().getFixed());
+        assertEquals(plan.getFinalPhase().getName(), "standard-annual-evergreen");
+
+        assertEquals(plan.getFinalPhase().getRecurring().getBillingPeriod(), BillingPeriod.MONTHLY);
+        assertEquals(plan.getFinalPhase().getRecurring().getRecurringPrice().getPrices().length, 1);
+        assertEquals(plan.getFinalPhase().getRecurring().getRecurringPrice().getPrices()[0].getValue(), BigDecimal.TEN);
+        assertEquals(plan.getFinalPhase().getRecurring().getRecurringPrice().getPrices()[0].getCurrency(), Currency.USD);
+
+        assertEquals(catalog.getPriceLists().getAllPriceLists().size(), 1);
+        final PriceList priceList = catalog.getPriceLists().getAllPriceLists().get(0);
+        assertEquals(priceList.getName(), new PriceListDefault().getName());
+        assertEquals(priceList.getPlans().length, 4);
+
+        //System.err.println(catalogUpdater.getCatalogXML());
+    }
+
+
+
+
+}
\ No newline at end of file