From 99c16dc345e7cbfddd8d49bdc48c146ee6c5dcbf Mon Sep 17 00:00:00 2001 From: jianglai Date: Wed, 30 Nov 2016 10:01:59 -0800 Subject: [PATCH] Make domain pricing customizable By refactoring TldSpecificLogicProxy (and renaming it to DomainPricingLogic) into a FlowScope object that is injected into each flow, we can further inject a DomainPricingCustomLogic into the pricer, which has hooks into each pricing calls, e. g. getCreatePrice(), getRenewPrice(), etc. The benefit of these customization hooks is that methods like getCreatePrice() get called in multiple flows, such as DomainAllocateFlow, DomainApplicationCreateFlow, and DomainCreateFlow. By customizing the return value of getCreatePrice() itself, all of its callers gets the amended price. This CL only includes the basic infrastructure needed, with no actual hooks placed into each flow. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=140616990 --- .../flows/custom/CustomLogicFactory.java | 5 + .../flows/custom/CustomLogicModule.java | 6 + .../custom/DomainPricingCustomLogic.java | 81 +++++ .../flows/domain/DomainPricingLogic.java | 321 ++++++++++++++++++ 4 files changed, 413 insertions(+) create mode 100644 java/google/registry/flows/custom/DomainPricingCustomLogic.java create mode 100644 java/google/registry/flows/domain/DomainPricingLogic.java diff --git a/java/google/registry/flows/custom/CustomLogicFactory.java b/java/google/registry/flows/custom/CustomLogicFactory.java index c15d23c42..5610c9f9e 100644 --- a/java/google/registry/flows/custom/CustomLogicFactory.java +++ b/java/google/registry/flows/custom/CustomLogicFactory.java @@ -60,4 +60,9 @@ public class CustomLogicFactory { EppInput eppInput, SessionMetadata sessionMetadata) { return new DomainDeleteFlowCustomLogic(eppInput, sessionMetadata); } + + public DomainPricingCustomLogic forDomainPricing( + EppInput eppInput, SessionMetadata sessionMetadata) { + return new DomainPricingCustomLogic(eppInput, sessionMetadata); + } } diff --git a/java/google/registry/flows/custom/CustomLogicModule.java b/java/google/registry/flows/custom/CustomLogicModule.java index 8b514aa6a..27b5b614f 100644 --- a/java/google/registry/flows/custom/CustomLogicModule.java +++ b/java/google/registry/flows/custom/CustomLogicModule.java @@ -58,4 +58,10 @@ public class CustomLogicModule { CustomLogicFactory factory, EppInput eppInput, SessionMetadata sessionMetadata) { return factory.forDomainDeleteFlow(eppInput, sessionMetadata); } + + @Provides + static DomainPricingCustomLogic provideDomainPricingCustomLogic( + CustomLogicFactory factory, EppInput eppInput, SessionMetadata sessionMetadata) { + return factory.forDomainPricing(eppInput, sessionMetadata); + } } diff --git a/java/google/registry/flows/custom/DomainPricingCustomLogic.java b/java/google/registry/flows/custom/DomainPricingCustomLogic.java new file mode 100644 index 000000000..865e3f1e7 --- /dev/null +++ b/java/google/registry/flows/custom/DomainPricingCustomLogic.java @@ -0,0 +1,81 @@ +// Copyright 2016 The Nomulus 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.flows.custom; + +import com.google.auto.value.AutoValue; +import com.google.common.net.InternetDomainName; +import google.registry.flows.EppException; +import google.registry.flows.SessionMetadata; +import google.registry.flows.domain.DomainPricingLogic; +import google.registry.model.ImmutableObject; +import google.registry.model.domain.fee.BaseFee; +import google.registry.model.eppinput.EppInput; +import google.registry.model.registry.Registry; +import org.joda.time.DateTime; + +/** + * A no-op base class to customize {@link DomainPricingLogic}. + * + *

Extend this class and override the hook(s) to perform custom logic. + */ +public class DomainPricingCustomLogic extends BaseFlowCustomLogic { + + protected DomainPricingCustomLogic(EppInput eppInput, SessionMetadata sessionMetadata) { + super(eppInput, sessionMetadata); + } + + /** A hook that customizes create price. */ + @SuppressWarnings("unused") + public BaseFee customizeCreatePrice(CreatePriceParameters createPriceParameters) + throws EppException { + return createPriceParameters.createFee(); + } + + /** A class to encapsulate parameters for a call to {@link #customizeCreatePrice} . */ + @AutoValue + public abstract static class CreatePriceParameters extends ImmutableObject { + + public abstract BaseFee createFee(); + + public abstract Registry registry(); + + public abstract InternetDomainName domainName(); + + public abstract DateTime asOfDate(); + + public abstract int years(); + + public static Builder newBuilder() { + return new AutoValue_DomainPricingCustomLogic_CreatePriceParameters.Builder(); + } + + /** Builder for {@link CreatePriceParameters}. */ + @AutoValue.Builder + public abstract static class Builder { + + public abstract Builder setCreateFee(BaseFee createFee); + + public abstract Builder setRegistry(Registry registry); + + public abstract Builder setDomainName(InternetDomainName domainName); + + public abstract Builder setAsOfDate(DateTime asOfDate); + + public abstract Builder setYears(int years); + + public abstract CreatePriceParameters build(); + } + } +} diff --git a/java/google/registry/flows/domain/DomainPricingLogic.java b/java/google/registry/flows/domain/DomainPricingLogic.java new file mode 100644 index 000000000..eb7555060 --- /dev/null +++ b/java/google/registry/flows/domain/DomainPricingLogic.java @@ -0,0 +1,321 @@ +// Copyright 2016 The Nomulus 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.flows.domain; + +import static com.google.common.base.Preconditions.checkArgument; +import static google.registry.model.EppResourceUtils.loadByForeignKey; +import static google.registry.model.ofy.ObjectifyService.ofy; +import static google.registry.pricing.PricingEngineProxy.getDomainCreateCost; +import static google.registry.pricing.PricingEngineProxy.getDomainFeeClass; +import static google.registry.pricing.PricingEngineProxy.getDomainRenewCost; +import static google.registry.util.CollectionUtils.nullToEmpty; +import static google.registry.util.PreconditionsUtils.checkArgumentNotNull; + +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableList; +import com.google.common.net.InternetDomainName; +import com.googlecode.objectify.Key; +import google.registry.flows.EppException; +import google.registry.flows.FlowScope; +import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException; +import google.registry.flows.custom.DomainPricingCustomLogic; +import google.registry.flows.custom.DomainPricingCustomLogic.CreatePriceParameters; +import google.registry.model.ImmutableObject; +import google.registry.model.domain.DomainApplication; +import google.registry.model.domain.DomainResource; +import google.registry.model.domain.LrpTokenEntity; +import google.registry.model.domain.fee.BaseFee; +import google.registry.model.domain.fee.BaseFee.FeeType; +import google.registry.model.domain.fee.Credit; +import google.registry.model.domain.fee.Fee; +import google.registry.model.eppinput.EppInput; +import google.registry.model.registry.Registry; +import java.util.List; +import javax.inject.Inject; +import org.joda.money.CurrencyUnit; +import org.joda.money.Money; +import org.joda.time.DateTime; + +/** + * Provides pricing for create, renew, etc, operations, with call-outs that can be customized by + * providing a {@link DomainPricingCustomLogic} implementation that operates on cross-TLD or per-TLD + * logic. + */ +@FlowScope +public final class DomainPricingLogic { + + @Inject DomainPricingCustomLogic customLogic; + + @Inject + DomainPricingLogic() {} + + /** A collection of fees and credits for a specific EPP transform. */ + public static final class EppCommandOperations extends ImmutableObject { + private final CurrencyUnit currency; + private final ImmutableList fees; + private final ImmutableList credits; + + /** Constructs an EppCommandOperations object using separate lists of fees and credits. */ + EppCommandOperations( + CurrencyUnit currency, ImmutableList fees, ImmutableList credits) { + this.currency = + checkArgumentNotNull(currency, "Currency may not be null in EppCommandOperations."); + checkArgument(!fees.isEmpty(), "You must specify one or more fees."); + this.fees = checkArgumentNotNull(fees, "Fees may not be null in EppCommandOperations."); + this.credits = + checkArgumentNotNull(credits, "Credits may not be null in EppCommandOperations."); + } + + /** + * Constructs an EppCommandOperations object. The arguments are sorted into fees and credits. + */ + EppCommandOperations(CurrencyUnit currency, BaseFee... feesAndCredits) { + this.currency = + checkArgumentNotNull(currency, "Currency may not be null in EppCommandOperations."); + ImmutableList.Builder feeBuilder = new ImmutableList.Builder<>(); + ImmutableList.Builder creditBuilder = new ImmutableList.Builder<>(); + for (BaseFee feeOrCredit : feesAndCredits) { + if (feeOrCredit instanceof Credit) { + creditBuilder.add((Credit) feeOrCredit); + } else { + feeBuilder.add((Fee) feeOrCredit); + } + } + this.fees = feeBuilder.build(); + this.credits = creditBuilder.build(); + } + + private Money getTotalCostForType(FeeType type) { + Money result = Money.zero(currency); + checkArgumentNotNull(type); + for (Fee fee : fees) { + if (fee.getType() == type) { + result = result.plus(fee.getCost()); + } + } + return result; + } + + /** Returns the total cost of all fees and credits for the event. */ + public Money getTotalCost() { + Money result = Money.zero(currency); + for (Fee fee : fees) { + result = result.plus(fee.getCost()); + } + for (Credit credit : credits) { + result = result.plus(credit.getCost()); + } + return result; + } + + /** Returns the create cost for the event. */ + public Money getCreateCost() { + return getTotalCostForType(FeeType.CREATE); + } + + /** Returns the EAP cost for the event. */ + public Money getEapCost() { + return getTotalCostForType(FeeType.EAP); + } + + /** Returns the list of fees for the event. */ + public ImmutableList getFees() { + return fees; + } + + /** Returns the list of credits for the event. */ + public List getCredits() { + return nullToEmpty(credits); + } + + /** Returns the currency for all fees in the event. */ + public final CurrencyUnit getCurrency() { + return currency; + } + } + + /** Returns a new create price for the Pricer. */ + public EppCommandOperations getCreatePrice( + Registry registry, String domainName, DateTime date, int years) throws EppException { + CurrencyUnit currency = registry.getCurrency(); + + // Get the vanilla create cost. + BaseFee createFeeOrCredit = + Fee.create(getDomainCreateCost(domainName, date, years).getAmount(), FeeType.CREATE); + + // Apply custom logic to the create fee, if any. + createFeeOrCredit = + customLogic.customizeCreatePrice( + CreatePriceParameters.newBuilder() + .setCreateFee(createFeeOrCredit) + .setRegistry(registry) + .setDomainName(InternetDomainName.from(domainName)) + .setAsOfDate(date) + .setYears(years) + .build()); + + // Create fees for the cost and the EAP fee, if any. + Fee eapFee = registry.getEapFeeFor(date); + if (!eapFee.hasZeroCost()) { + return new EppCommandOperations(currency, createFeeOrCredit, eapFee); + } else { + return new EppCommandOperations(currency, createFeeOrCredit); + } + } + + // TODO: (b/33000134) clean up the rest of the pricing calls. + + /** + * Computes the renew fee or credit. This is called by other methods which use the renew fee + * (renew, restore, etc). + */ + static BaseFee getRenewFeeOrCredit( + Registry registry, + String domainName, + String clientId, + DateTime date, + int years, + EppInput eppInput) + throws EppException { + Optional extraFlowLogic = + RegistryExtraFlowLogicProxy.newInstanceForTld(registry.getTldStr()); + if (extraFlowLogic.isPresent()) { + // TODO: Consider changing the method definition to have the domain passed in to begin with. + DomainResource domain = loadByForeignKey(DomainResource.class, domainName, date); + if (domain == null) { + throw new ResourceDoesNotExistException(DomainResource.class, domainName); + } + return extraFlowLogic.get().getRenewFeeOrCredit(domain, clientId, date, years, eppInput); + } else { + return Fee.create(getDomainRenewCost(domainName, date, years).getAmount(), FeeType.RENEW); + } + } + + /** Returns a new renew price for the pricer. */ + public static EppCommandOperations getRenewPrice( + Registry registry, + String domainName, + String clientId, + DateTime date, + int years, + EppInput eppInput) + throws EppException { + return new EppCommandOperations( + registry.getCurrency(), + getRenewFeeOrCredit(registry, domainName, clientId, date, years, eppInput)); + } + + /** Returns a new restore price for the pricer. */ + public static EppCommandOperations getRestorePrice( + Registry registry, String domainName, String clientId, DateTime date, EppInput eppInput) + throws EppException { + return new EppCommandOperations( + registry.getCurrency(), + getRenewFeeOrCredit(registry, domainName, clientId, date, 1, eppInput), + Fee.create(registry.getStandardRestoreCost().getAmount(), FeeType.RESTORE)); + } + + /** Returns a new transfer price for the pricer. */ + public static EppCommandOperations getTransferPrice( + Registry registry, + String domainName, + String clientId, + DateTime transferDate, + int years, + EppInput eppInput) + throws EppException { + // Currently, all transfer prices = renew prices, so just pass through. + return getRenewPrice(registry, domainName, clientId, transferDate, years, eppInput); + } + + /** Returns a new update price for the pricer. */ + public static EppCommandOperations getUpdatePrice( + Registry registry, String domainName, String clientId, DateTime date, EppInput eppInput) + throws EppException { + CurrencyUnit currency = registry.getCurrency(); + + // If there is extra flow logic, it may specify an update price. Otherwise, there is none. + BaseFee feeOrCredit; + Optional extraFlowLogic = + RegistryExtraFlowLogicProxy.newInstanceForTld(registry.getTldStr()); + if (extraFlowLogic.isPresent()) { + // TODO: Consider changing the method definition to have the domain passed in to begin with. + DomainResource domain = loadByForeignKey(DomainResource.class, domainName, date); + if (domain == null) { + throw new ResourceDoesNotExistException(DomainResource.class, domainName); + } + feeOrCredit = extraFlowLogic.get().getUpdateFeeOrCredit(domain, clientId, date, eppInput); + } else { + feeOrCredit = Fee.create(Money.zero(registry.getCurrency()).getAmount(), FeeType.UPDATE); + } + + return new EppCommandOperations(currency, feeOrCredit); + } + + /** Returns a new domain application update price for the pricer. */ + public static EppCommandOperations getApplicationUpdatePrice( + Registry registry, + DomainApplication application, + String clientId, + DateTime date, + EppInput eppInput) + throws EppException { + CurrencyUnit currency = registry.getCurrency(); + + // If there is extra flow logic, it may specify an update price. Otherwise, there is none. + BaseFee feeOrCredit; + Optional extraFlowLogic = + RegistryExtraFlowLogicProxy.newInstanceForTld(registry.getTldStr()); + if (extraFlowLogic.isPresent()) { + feeOrCredit = + extraFlowLogic + .get() + .getApplicationUpdateFeeOrCredit(application, clientId, date, eppInput); + } else { + feeOrCredit = Fee.create(Money.zero(registry.getCurrency()).getAmount(), FeeType.UPDATE); + } + + return new EppCommandOperations(currency, feeOrCredit); + } + + /** Returns the fee class for a given domain and date. */ + public static Optional getFeeClass(String domainName, DateTime date) { + return getDomainFeeClass(domainName, date); + } + + /** + * Checks whether an LRP token String maps to a valid {@link LrpTokenEntity} for the domain name's + * TLD, and return that entity (wrapped in an {@link Optional}) if one exists. + * + *

This method has no knowledge of whether or not an auth code (interpreted here as an LRP + * token) has already been checked against the reserved list for QLP (anchor tenant), as auth + * codes are used for both types of registrations. + */ + public static Optional getMatchingLrpToken( + String lrpToken, InternetDomainName domainName) { + // Note that until the actual per-TLD logic is built out, what's being done here is a basic + // domain-name-to-assignee match. + if (!lrpToken.isEmpty()) { + LrpTokenEntity token = ofy().load().key(Key.create(LrpTokenEntity.class, lrpToken)).now(); + if (token != null + && token.getAssignee().equalsIgnoreCase(domainName.toString()) + && token.getRedemptionHistoryEntry() == null + && token.getValidTlds().contains(domainName.parent().toString())) { + return Optional.of(token); + } + } + return Optional.absent(); + } +}