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(); + } +}