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