/* * Copyright 2010-2013 Ning, Inc. * Copyright 2014-2017 Groupon, Inc * Copyright 2014-2017 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.io.Externalizable; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.Iterator; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlElementWrapper; import javax.xml.bind.annotation.XmlID; import javax.xml.bind.annotation.XmlIDREF; import org.joda.time.DateTime; import org.killbill.billing.ErrorCode; import org.killbill.billing.catalog.api.BillingPeriod; import org.killbill.billing.catalog.api.CatalogApiException; import org.killbill.billing.catalog.api.PhaseType; import org.killbill.billing.catalog.api.Plan; import org.killbill.billing.catalog.api.PlanPhase; import org.killbill.billing.catalog.api.PlanPhasePriceOverride; import org.killbill.billing.catalog.api.PriceList; import org.killbill.billing.catalog.api.Product; import org.killbill.billing.catalog.api.Recurring; import org.killbill.billing.catalog.api.TimeUnit; import org.killbill.billing.util.cache.ExternalizableInput; import org.killbill.billing.util.cache.ExternalizableOutput; import org.killbill.billing.util.cache.MapperHolder; import org.killbill.xmlloader.ValidatingConfig; import org.killbill.xmlloader.ValidationError; import org.killbill.xmlloader.ValidationErrors; @XmlAccessorType(XmlAccessType.NONE) public class DefaultPlan extends ValidatingConfig<StandaloneCatalog> implements Plan, Externalizable { private static final long serialVersionUID = -4159932819592790086L; @XmlAttribute(required = true) @XmlID private String name; @XmlElement(required = false) private Date effectiveDateForExistingSubscriptions; @XmlElement(required = true) @XmlIDREF private DefaultProduct product; @XmlElementWrapper(name = "initialPhases", required = false) @XmlElement(name = "phase", required = false) private DefaultPlanPhase[] initialPhases; @XmlElement(name = "finalPhase", required = true) private DefaultPlanPhase finalPhase; //If this is missing it defaults to 1 //No other value is allowed for BASE plans. //No other value is allowed for Tiered ADDONS //A value of -1 means unlimited @XmlElement(required = false) private Integer plansAllowedInBundle; private String priceListName; // For deserialization public DefaultPlan() { initialPhases = new DefaultPlanPhase[0]; } public DefaultPlan(final String planName, final DefaultPlan in, final PlanPhasePriceOverride[] overrides) { this.name = planName; this.effectiveDateForExistingSubscriptions = in.getEffectiveDateForExistingSubscriptions(); this.product = (DefaultProduct) in.getProduct(); this.initialPhases = new DefaultPlanPhase[in.getInitialPhases().length]; for (int i = 0; i < overrides.length - 1; i++) { final DefaultPlanPhase newPhase = new DefaultPlanPhase(this, in.getInitialPhases()[i], overrides[i]); initialPhases[i] = newPhase; } this.finalPhase = new DefaultPlanPhase(this, in.getFinalPhase(), overrides[overrides.length - 1]); this.priceListName = in.getPriceListName(); } @Override public Date getEffectiveDateForExistingSubscriptions() { return effectiveDateForExistingSubscriptions; } public void setEffectiveDateForExistingSubscriptions( final Date effectiveDateForExistingSubscriptions) { this.effectiveDateForExistingSubscriptions = effectiveDateForExistingSubscriptions; } @Override public DefaultPlanPhase[] getInitialPhases() { return initialPhases; } public DefaultPlan setInitialPhases(final DefaultPlanPhase[] phases) { this.initialPhases = phases; return this; } @Override public Product getProduct() { return product; } public DefaultPlan setProduct(final Product product) { this.product = (DefaultProduct) product; return this; } @Override public String getPriceListName() { return priceListName; } public DefaultPlan setPriceListName(final String priceListName) { this.priceListName = priceListName; return this; } @Override public String getName() { return name; } public DefaultPlan setName(final String name) { this.name = name; return this; } @Override public DefaultPlanPhase getFinalPhase() { return finalPhase; } public DefaultPlan setFinalPhase(final DefaultPlanPhase finalPhase) { this.finalPhase = finalPhase; return this; } @Override public PlanPhase[] getAllPhases() { final int length = initialPhases.length + 1; final PlanPhase[] allPhases = new DefaultPlanPhase[length]; int cnt = 0; if (length > 1) { for (final PlanPhase cur : initialPhases) { allPhases[cnt++] = cur; } } allPhases[cnt++] = finalPhase; return allPhases; } @Override public PlanPhase findPhase(final String name) throws CatalogApiException { for (final PlanPhase pp : getAllPhases()) { if (pp.getName().equals(name)) { return pp; } } throw new CatalogApiException(ErrorCode.CAT_NO_SUCH_PHASE, name); } @Override public BillingPeriod getRecurringBillingPeriod() { return finalPhase.getRecurring() != null ? finalPhase.getRecurring().getBillingPeriod() : BillingPeriod.NO_BILLING_PERIOD; } @Override public int getPlansAllowedInBundle() { return plansAllowedInBundle; } public DefaultPlan setPlansAllowedInBundle(final Integer plansAllowedInBundle) { this.plansAllowedInBundle = plansAllowedInBundle; return this; } @Override public Iterator<PlanPhase> getInitialPhaseIterator() { final Collection<PlanPhase> list = new ArrayList<PlanPhase>(); Collections.addAll(list, initialPhases); return list.iterator(); } @Override public void initialize(final StandaloneCatalog catalog, final URI sourceURI) { super.initialize(catalog, sourceURI); CatalogSafetyInitializer.initializeNonRequiredNullFieldsWithDefaultValue(this); if (finalPhase != null) { finalPhase.setPlan(this); finalPhase.initialize(catalog, sourceURI); } for (final DefaultPlanPhase p : initialPhases) { p.setPlan(this); p.initialize(catalog, sourceURI); } this.priceListName = this.priceListName != null ? this.priceListName : findPriceListForPlan(catalog); } @Override public ValidationErrors validate(final StandaloneCatalog catalog, final ValidationErrors errors) { if (effectiveDateForExistingSubscriptions != null && catalog.getEffectiveDate().getTime() > effectiveDateForExistingSubscriptions.getTime()) { errors.add(new ValidationError(String.format("Price effective date %s is before catalog effective date '%s'", effectiveDateForExistingSubscriptions, catalog.getEffectiveDate()), catalog.getCatalogURI(), DefaultPlan.class, "")); } if (product == null) { errors.add(new ValidationError(String.format("Invalid product for plan '%s'", name), catalog.getCatalogURI(), DefaultPlan.class, "")); } for (final DefaultPlanPhase cur : initialPhases) { cur.validate(catalog, errors); if (cur.getPhaseType() == PhaseType.EVERGREEN || cur.getPhaseType() == PhaseType.FIXEDTERM) { errors.add(new ValidationError(String.format("Initial Phase %s of plan %s cannot be of type %s", cur.getName(), name, cur.getPhaseType()), catalog.getCatalogURI(), DefaultPlan.class, "")); } } finalPhase.validate(catalog, errors); if (finalPhase.getPhaseType() == PhaseType.TRIAL || finalPhase.getPhaseType() == PhaseType.DISCOUNT) { errors.add(new ValidationError(String.format("Final Phase %s of plan %s cannot be of type %s", finalPhase.getName(), name, finalPhase.getPhaseType()), catalog.getCatalogURI(), DefaultPlan.class, "")); } // Safety check if (plansAllowedInBundle == null) { throw new IllegalStateException("plansAllowedInBundle should have been automatically been initialized with DEFAULT_NON_REQUIRED_INTEGER_FIELD_VALUE (-1)"); } return errors; } @Override public DateTime dateOfFirstRecurringNonZeroCharge(final DateTime subscriptionStartDate, final PhaseType initialPhaseType) { DateTime result = subscriptionStartDate; boolean skipPhase = initialPhaseType != null; for (final PlanPhase phase : getAllPhases()) { if (skipPhase) { if (phase.getPhaseType() != initialPhaseType) { continue; } else { skipPhase = false; } } final Recurring recurring = phase.getRecurring(); if (phase.getDuration().getUnit() != TimeUnit.UNLIMITED && (recurring == null || recurring.getRecurringPrice() == null || recurring.getRecurringPrice().isZero())) { try { result = phase.getDuration().addToDateTime(result); } catch (final CatalogApiException ignored) { } } else { break; } } return result; } @Override public boolean equals(final Object o) { if (this == o) { return true; } if (!(o instanceof DefaultPlan)) { return false; } final DefaultPlan that = (DefaultPlan) o; if (effectiveDateForExistingSubscriptions != null ? !effectiveDateForExistingSubscriptions.equals(that.effectiveDateForExistingSubscriptions) : that.effectiveDateForExistingSubscriptions != null) { return false; } if (finalPhase != null ? !finalPhase.equals(that.finalPhase) : that.finalPhase != null) { return false; } if (!Arrays.equals(initialPhases, that.initialPhases)) { return false; } if (name != null ? !name.equals(that.name) : that.name != null) { return false; } if (plansAllowedInBundle != null ? !plansAllowedInBundle.equals(that.plansAllowedInBundle) : that.plansAllowedInBundle != null) { return false; } if (product != null ? !product.equals(that.product) : that.product != null) { return false; } return true; } @Override public int hashCode() { int result = name != null ? name.hashCode() : 0; result = 31 * result + (effectiveDateForExistingSubscriptions != null ? effectiveDateForExistingSubscriptions.hashCode() : 0); result = 31 * result + (initialPhases != null ? Arrays.hashCode(initialPhases) : 0); result = 31 * result + (finalPhase != null ? finalPhase.hashCode() : 0); result = 31 * result + (plansAllowedInBundle != null ? plansAllowedInBundle.hashCode() : 0); return result; } @Override public String toString() { return "DefaultPlan [name=" + name + ", effectiveDateForExistingSubscriptions=" + effectiveDateForExistingSubscriptions + ", product=" + product.getName() + ", initialPhases=" + Arrays.toString(initialPhases) + ", finalPhase=" + finalPhase.getName() + ", plansAllowedInBundle=" + plansAllowedInBundle + "]"; } private String findPriceListForPlan(final StandaloneCatalog catalog) { for (final PriceList cur : catalog.getPriceLists().getAllPriceLists()) { final DefaultPriceList curDefaultPriceList = (DefaultPriceList) cur; if (curDefaultPriceList.findPlan(name) != null) { return curDefaultPriceList.getName(); } } throw new IllegalStateException("Cannot extract pricelist for plan " + name); } @Override public void readExternal(final ObjectInput in) throws IOException { MapperHolder.mapper().readerForUpdating(this).readValue(new ExternalizableInput(in)); } @Override public void writeExternal(final ObjectOutput oo) throws IOException { MapperHolder.mapper().writeValue(new ExternalizableOutput(oo), this); } }