Extract premium pricing to a PricingEngine interface

This refactors the existing premium list functionality into the new
class StaticPremiumListPricingEngine, which implements PricingEngine.
A backfill @OnLoad is provided to default existing Registry entities
into the static implementation.  For now there is just this one
implementation.  Dagger map multibinding is used to generate the total
set of allowed pricing engines, and allows other parties to plug in
their own implementations.

The pricing engine is a required field on the Registry object.  If you
don't want a particular Registry to actually have a premium list, then
use the static pricing engine but don't actually set a premium list.

A subsequent CL will refactor the Key<PremiumList> field on the
Registry entity class to be handled solely by the
StaticPremiumListPricingEngine implementation.  Going forward, all
configuration and implementation details that are specific to a given
pricing engine should be handled by that pricing engine, and not as
fields on the Registry object.
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=121850176
This commit is contained in:
mcilwain 2016-05-09 10:30:13 -07:00 committed by Justine Tunney
parent 87961fbb12
commit 047bbf186e
32 changed files with 659 additions and 200 deletions

View file

@ -0,0 +1,31 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// Licensed 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 google.registry.model.pricing;
import com.google.common.base.Optional;
import org.joda.money.Money;
import org.joda.time.DateTime;
/** A plugin interface for premium pricing engines. */
public interface PricingEngine {
/**
* Returns the premium price for the given second-level domain name at the given time for the
* given registrar, or absent if the domain name isn't premium.
*/
public Optional<Money> getPremiumPrice(
String secondLevelDomainName, DateTime priceTime, String clientIdentifier);
}

View file

@ -0,0 +1,53 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// Licensed 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 google.registry.model.pricing;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static google.registry.util.DomainNameUtils.getTldFromSld;
import com.google.common.base.Optional;
import com.google.common.net.InternetDomainName;
import google.registry.model.registry.Registry;
import google.registry.model.registry.label.PremiumList;
import org.joda.money.Money;
import org.joda.time.DateTime;
import javax.inject.Inject;
/** A premium list pricing engine that stores static pricing information in Datastore entities. */
public final class StaticPremiumListPricingEngine implements PricingEngine {
@Inject StaticPremiumListPricingEngine() {}
@Override
public Optional<Money> getPremiumPrice(
String secondLevelDomainName, DateTime priceTime, String clientIdentifier) {
// Note that clientIdentifier and priceTime are not used for determining premium pricing for
// static premium lists.
String tld = getTldFromSld(secondLevelDomainName);
Registry registry = Registry.get(checkNotNull(tld, "tld"));
if (registry.getPremiumList() == null) {
return Optional.<Money>absent();
}
String listName = registry.getPremiumList().getName();
Optional<PremiumList> premiumList = PremiumList.get(listName);
checkState(premiumList.isPresent(), "Could not load premium list: %s", listName);
String label = InternetDomainName.from(secondLevelDomainName).parts().get(0);
return premiumList.get().getPremiumPrice(label);
}
}

View file

@ -21,7 +21,6 @@ import static com.google.common.base.Predicates.not;
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.model.ofy.Ofy.RECOMMENDED_MEMCACHE_EXPIRATION;
import static google.registry.model.registry.label.PremiumList.getPremiumPrice;
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
@ -47,6 +46,7 @@ import com.googlecode.objectify.annotation.Embed;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Id;
import com.googlecode.objectify.annotation.Mapify;
import com.googlecode.objectify.annotation.OnLoad;
import com.googlecode.objectify.annotation.OnSave;
import com.googlecode.objectify.annotation.Parent;
@ -57,6 +57,8 @@ import google.registry.model.ImmutableObject;
import google.registry.model.common.EntityGroupRoot;
import google.registry.model.common.TimedTransitionProperty;
import google.registry.model.common.TimedTransitionProperty.TimedTransition;
import google.registry.model.pricing.PricingEngine;
import google.registry.model.pricing.StaticPremiumListPricingEngine;
import google.registry.model.registry.label.PremiumList;
import google.registry.model.registry.label.ReservedList;
import google.registry.util.Idn;
@ -238,6 +240,27 @@ public class Registry extends ImmutableObject implements Buildable {
CACHE.invalidate(tldStr);
}
/**
* Backfill the Registry entities that were saved before this field was added.
*
* <p>Note that this defaults to the {@link StaticPremiumListPricingEngine}.
*/
// TODO(b/26901539): Remove this backfill once it is populated on all Registry entities.
@OnLoad
void backfillPricingEngine() {
if (pricingEngineClassName == null) {
pricingEngineClassName = StaticPremiumListPricingEngine.class.getCanonicalName();
}
}
/**
* The fully qualified canonical classname of the pricing engine that this TLD uses.
*
* <p>This must be a valid key for the map of pricing engines injected by
* <code>@Inject Map<String, PricingEngine></code>
*/
String pricingEngineClassName;
/**
* The unicode-aware representation of the TLD associated with this {@link Registry}.
* <p>
@ -271,7 +294,7 @@ public class Registry extends ImmutableObject implements Buildable {
return nullToEmptyImmutableCopy(reservedLists);
}
/** The {@link PremiumList} for this TLD. */
/** The static {@link PremiumList} for this TLD, if there is one. */
Key<PremiumList> premiumList;
/** Should RDE upload a nightly escrow deposit for this TLD? */
@ -442,7 +465,10 @@ public class Registry extends ImmutableObject implements Buildable {
return currency;
}
/** Use {@link #getDomainCreateCost} instead of this to find the cost for a domain create. */
/**
* Use <code>PricingUtils.getDomainCreateCost</code> instead of this to find the cost for a
* domain create.
*/
@VisibleForTesting
public Money getStandardCreateCost() {
return createBillingCost;
@ -457,8 +483,9 @@ public class Registry extends ImmutableObject implements Buildable {
}
/**
* Use {@link #getDomainRenewCost} instead of this to find the cost for a domain renew, and all
* derived costs (i.e. autorenews, transfers, and the per-domain part of a restore cost).
* Use <code>PricingUtils.getDomainRenewCost</code> instead of this to find the cost for a domain
* renewal, and all derived costs (i.e. autorenews, transfers, and the per-domain part of a
* restore cost).
*/
@VisibleForTesting
public Money getStandardRenewCost(DateTime now) {
@ -480,47 +507,6 @@ public class Registry extends ImmutableObject implements Buildable {
return renewBillingCostTransitions.toValueMap();
}
private Optional<Money> getPremiumPriceForSld(String sldName) {
return getPremiumPriceForSld(InternetDomainName.from(sldName));
}
private Optional<Money> getPremiumPriceForSld(InternetDomainName domainName) {
checkArgument(getTld().equals(domainName.parent()),
"Domain name %s is not an SLD for TLD %s", domainName.toString(), tldStr);
String label = domainName.parts().get(0);
return getPremiumPrice(label, tldStr);
}
/** Returns true if the given domain name is on the premium price list. */
public boolean isPremiumName(String domainName, DateTime priceTime, String clientIdentifier) {
return isPremiumName(InternetDomainName.from(domainName), priceTime, clientIdentifier);
}
/** Returns true if the given domain name is on the premium price list. */
@SuppressWarnings("unused")
public boolean isPremiumName(
InternetDomainName domainName, DateTime priceTime, String clientIdentifier) {
return getPremiumPriceForSld(domainName).isPresent();
}
/** Returns the billing cost for registering the specified domain name for this many years. */
@SuppressWarnings("unused")
public Money getDomainCreateCost(
String domainName, DateTime priceTime, String clientIdentifier, int years) {
checkArgument(years > 0, "Number of years must be positive");
Money annualCost = getPremiumPriceForSld(domainName).or(getStandardCreateCost());
return annualCost.multipliedBy(years);
}
/** Returns the billing cost for renewing the specified domain name for this many years. */
@SuppressWarnings("unused")
public Money getDomainRenewCost(
String domainName, DateTime priceTime, String clientIdentifier, int years) {
checkArgument(years > 0, "Number of years must be positive");
Money annualCost = getPremiumPriceForSld(domainName).or(getStandardRenewCost(priceTime));
return annualCost.multipliedBy(years);
}
public String getLordnUsername() {
return lordnUsername;
}
@ -529,6 +515,10 @@ public class Registry extends ImmutableObject implements Buildable {
return claimsPeriodEnd;
}
public String getPricingEngineClassName() {
return pricingEngineClassName;
}
public ImmutableSet<String> getAllowedRegistrantContactIds() {
return nullToEmptyImmutableCopy(allowedRegistrantContactIds);
}
@ -595,6 +585,12 @@ public class Registry extends ImmutableObject implements Buildable {
return this;
}
public Builder setPricingEngineClass(Class<? extends PricingEngine> pricingEngineClass) {
getInstance().pricingEngineClassName =
checkArgumentNotNull(pricingEngineClass).getCanonicalName();
return this;
}
public Builder setAddGracePeriodLength(Duration addGracePeriodLength) {
checkArgument(addGracePeriodLength.isLongerThan(Duration.ZERO),
"addGracePeriodLength must be non-zero");
@ -800,6 +796,8 @@ public class Registry extends ImmutableObject implements Buildable {
return money.getCurrencyUnit().equals(instance.currency);
}}),
"Renew cost must be in the registry's currency");
checkArgumentNotNull(
instance.pricingEngineClassName, "All registries must have a configured pricing engine");
instance.tldStrId = tldName;
instance.tldUnicode = Idn.toUnicode(tldName);
return super.build();