/*
* Copyright 2014-2018 Groupon, Inc
* Copyright 2014-2018 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.ErrorCode;
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.InternationalPrice;
import org.killbill.billing.catalog.api.PhaseType;
import org.killbill.billing.catalog.api.Plan;
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.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.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.collect.ImmutableList;
public class CatalogUpdater {
public static String DEFAULT_CATALOG_NAME = "DEFAULT";
private final DefaultMutableStaticCatalog catalog;
public CatalogUpdater(final StandaloneCatalog standaloneCatalog) {
this.catalog = new DefaultMutableStaticCatalog(standaloneCatalog);
this.catalog.setRecurringBillingMode(BillingMode.IN_ADVANCE);
}
public CatalogUpdater(final DateTime effectiveDate, final Currency... currencies) {
final DefaultPriceList defaultPriceList = new DefaultPriceList().setName(PriceListSet.DEFAULT_PRICELIST_NAME);
final StandaloneCatalog tmp = new StandaloneCatalog()
.setCatalogName(DEFAULT_CATALOG_NAME)
.setEffectiveDate(effectiveDate.toDate())
.setRecurringBillingMode(BillingMode.IN_ADVANCE)
.setProducts(ImmutableList.<Product>of())
.setPlans(ImmutableList.<Plan>of())
.setPriceLists(new DefaultPriceListSet(defaultPriceList, new DefaultPriceList[0]))
.setPlanRules(getSaneDefaultPlanRules(defaultPriceList));
if (currencies != null && currencies.length > 0) {
tmp.setSupportedCurrencies(currencies);
} else {
tmp.setSupportedCurrencies(new Currency[0]);
}
tmp.initialize(tmp);
this.catalog = new DefaultMutableStaticCatalog(tmp);
}
public StandaloneCatalog getCatalog() {
return catalog;
}
public String getCatalogXML() {
try {
return XMLWriter.writeXML(catalog, StandaloneCatalog.class);
} catch (final Exception e) {
throw new RuntimeException(e);
}
}
public void addSimplePlanDescriptor(final SimplePlanDescriptor desc) throws CatalogApiException {
// We need at least a planId
if (desc == null || desc.getPlanId() == null) {
throw new CatalogApiException(ErrorCode.CAT_INVALID_SIMPLE_PLAN_DESCRIPTOR, desc);
}
DefaultPlan plan = (DefaultPlan) getExistingPlan(desc.getPlanId());
if (plan == null && desc.getProductName() == null) {
throw new CatalogApiException(ErrorCode.CAT_INVALID_SIMPLE_PLAN_DESCRIPTOR, desc);
}
validateNewPlanDescriptor(desc);
DefaultProduct product = plan != null ? (DefaultProduct) plan.getProduct() : (DefaultProduct) getExistingProduct(desc.getProductName());
if (product == null) {
product = new DefaultProduct();
product.setName(desc.getProductName());
product.setCatagory(desc.getProductCategory());
product.initialize(catalog);
catalog.addProduct(product);
}
if (plan == null) {
plan = new DefaultPlan(catalog);
plan.setName(desc.getPlanId());
plan.setPriceListName(PriceListSet.DEFAULT_PRICELIST_NAME);
plan.setProduct(product);
plan.setRecurringBillingMode(catalog.getRecurringBillingMode());
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});
}
catalog.addPlan(plan);
} else {
validateExistingPlan(plan, desc);
}
//
// At this point we have an old or newly created **simple** Plan and we need to either create the recurring section or add a new currency.
//
if (!isCurrencySupported(desc.getCurrency())) {
catalog.addCurrency(desc.getCurrency());
// Reset the fixed price to null so the isZero() logic goes through new currencies and set the zero price for all
if (plan.getInitialPhases().length == 1) {
((DefaultInternationalPrice) plan.getInitialPhases()[0].getFixed().getPrice()).setPrices(null);
}
}
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);
}
if (!isPriceForCurrencyExists(recurring.getRecurringPrice(), desc.getCurrency())) {
catalog.addRecurringPriceToPlan(recurring.getRecurringPrice(), new DefaultPrice().setCurrency(desc.getCurrency()).setValue(desc.getAmount()));
}
if (desc.getProductCategory() == ProductCategory.ADD_ON) {
for (final String bp : desc.getAvailableBaseProducts()) {
final Product targetBasePlan = getExistingProduct(bp);
boolean found = false;
for (Product cur : targetBasePlan.getAvailable()) {
if (cur.getName().equals(product.getName())) {
found = true;
break;
}
}
if (!found) {
catalog.addProductAvailableAO(getExistingProduct(bp), product);
}
}
}
// Reinit catalog
catalog.initialize(catalog);
}
private boolean isPriceForCurrencyExists(final InternationalPrice price, final Currency currency) {
if (price.getPrices().length == 0) {
return false;
}
try {
price.getPrice(currency);
} catch (CatalogApiException ignore) {
return false;
}
return true;
}
private void validateExistingPlan(final DefaultPlan plan, final SimplePlanDescriptor desc) throws CatalogApiException {
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 if (desc.getTrialLength() != null && desc.getTrialTimeUnit() != null) { // If desc includes trial info we verify this is valid
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 (desc.getBillingPeriod() != null && plan.getFinalPhase().getRecurring().getBillingPeriod() != desc.getBillingPeriod()) {
failedValidation = true;
} else if (desc.getCurrency() != null && desc.getAmount() != null) {
try {
final BigDecimal currentAmount = plan.getFinalPhase().getRecurring().getRecurringPrice().getPrice(desc.getCurrency());
if (currentAmount.compareTo(desc.getAmount()) != 0) {
failedValidation = true;
}
} catch (CatalogApiException ignoreIfCurrencyIsCurrentlyUndefined) {
}
}
}
}
if (failedValidation) {
throw new CatalogApiException(ErrorCode.CAT_FAILED_SIMPLE_PLAN_VALIDATION, plan.toString(), desc.toString());
}
}
private boolean isCurrencySupported(final Currency targetCurrency) {
if (catalog.getCurrentSupportedCurrencies() != null) {
for (final Currency input : catalog.getCurrentSupportedCurrencies()) {
if (input.equals(targetCurrency)) {
return true;
}
}
}
return false;
}
private void validateNewPlanDescriptor(final SimplePlanDescriptor desc) throws CatalogApiException {
final boolean invalidPlan = desc.getPlanId() == null && (desc.getProductCategory() == null || desc.getBillingPeriod() == null);
final boolean invalidPrice = (desc.getAmount() == null || desc.getAmount().compareTo(BigDecimal.ZERO) < 0) ||
desc.getCurrency() == null;
if (invalidPlan || invalidPrice) {
throw new CatalogApiException(ErrorCode.CAT_INVALID_SIMPLE_PLAN_DESCRIPTOR, desc);
}
if (desc.getProductCategory() == ProductCategory.ADD_ON) {
if (desc.getAvailableBaseProducts() == null || desc.getAvailableBaseProducts().isEmpty()) {
throw new CatalogApiException(ErrorCode.CAT_INVALID_SIMPLE_PLAN_DESCRIPTOR, desc);
}
for (final String cur : desc.getAvailableBaseProducts()) {
if (getExistingProduct(cur) == null) {
throw new CatalogApiException(ErrorCode.CAT_INVALID_SIMPLE_PLAN_DESCRIPTOR, desc);
}
}
}
}
private Product getExistingProduct(final String productName) {
try {
return catalog.findCurrentProduct(productName);
} catch (final CatalogApiException e) {
return null;
}
}
private Plan getExistingPlan(final String planName) {
try {
return catalog.findCurrentPlan(planName);
} catch (CatalogApiException e) {
return null;
}
}
private DefaultPlanRules getSaneDefaultPlanRules(final DefaultPriceList defaultPriceList) {
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(defaultPriceList);
return new DefaultPlanRules()
.setChangeCase(changePolicy)
.setChangeAlignmentCase(changeAlignment)
.setCancelCase(cancelPolicy)
.setCreateAlignmentCase(createAlignment)
.setBillingAlignmentCase(billingAlignmentCase)
.setPriceListCase(priceList);
}
}