diff --git a/core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java b/core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java index db176f37b..5c8048264 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java +++ b/core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java @@ -690,7 +690,7 @@ public class DomainFlowUtils { List fees = feeCommand.get().getFees(); // The schema guarantees that at least one fee will be present. checkState(!fees.isEmpty()); - BigDecimal total = BigDecimal.ZERO; + BigDecimal total = zeroInCurrency(feeCommand.get().getCurrency()); for (Fee fee : fees) { if (!fee.hasDefaultAttributes()) { throw new UnsupportedFeeAttributeException(); @@ -938,6 +938,16 @@ public class DomainFlowUtils { } } + /** + * Returns zero for a specific currency. + * + *

{@link BigDecimal} has a concept of significant figures, so zero is not always zero. E.g. + * zero in USD is 0.00, whereas zero in Yen is 0, and zero in Dinars is 0.000 (!). + */ + static BigDecimal zeroInCurrency(CurrencyUnit currencyUnit) { + return Money.of(currencyUnit, BigDecimal.ZERO).getAmount(); + } + /** * Check that if there's a claims notice it's on the claims list, and that if there's not one it's * not on the claims list. diff --git a/core/src/main/java/google/registry/flows/domain/DomainPricingLogic.java b/core/src/main/java/google/registry/flows/domain/DomainPricingLogic.java index 22219c1a6..abb3b62d5 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainPricingLogic.java +++ b/core/src/main/java/google/registry/flows/domain/DomainPricingLogic.java @@ -14,8 +14,9 @@ package google.registry.flows.domain; +import static google.registry.flows.domain.DomainFlowUtils.zeroInCurrency; import static google.registry.pricing.PricingEngineProxy.getDomainFeeClass; -import static google.registry.pricing.PricingEngineProxy.getDomainRenewCost; +import static google.registry.pricing.PricingEngineProxy.getPricesForDomainName; import com.google.common.net.InternetDomainName; import google.registry.flows.EppException; @@ -33,7 +34,6 @@ import google.registry.model.domain.fee.Fee; import google.registry.model.domain.token.AllocationToken; import google.registry.model.pricing.PremiumPricingEngine.DomainPrices; import google.registry.model.registry.Registry; -import google.registry.pricing.PricingEngineProxy; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.Optional; @@ -64,21 +64,27 @@ public final class DomainPricingLogic { public FeesAndCredits getCreatePrice( Registry registry, String domainName, - DateTime date, + DateTime dateTime, int years, boolean isAnchorTenant, Optional allocationToken) throws EppException { CurrencyUnit currency = registry.getCurrency(); + + BaseFee createFeeOrCredit; // Domain create cost is always zero for anchor tenants - Money domainCreateCost = - isAnchorTenant - ? Money.of(currency, BigDecimal.ZERO) - : getDomainCreateCostWithDiscount(domainName, date, years, allocationToken); - BaseFee createFeeOrCredit = Fee.create(domainCreateCost.getAmount(), FeeType.CREATE); + if (isAnchorTenant) { + createFeeOrCredit = Fee.create(zeroInCurrency(currency), FeeType.CREATE, false); + } else { + DomainPrices domainPrices = getPricesForDomainName(domainName, dateTime); + Money domainCreateCost = + getDomainCreateCostWithDiscount(domainPrices, years, allocationToken); + createFeeOrCredit = + Fee.create(domainCreateCost.getAmount(), FeeType.CREATE, domainPrices.isPremium()); + } // Create fees for the cost and the EAP fee, if any. - Fee eapFee = registry.getEapFeeFor(date); + Fee eapFee = registry.getEapFeeFor(dateTime); FeesAndCredits.Builder feesBuilder = new FeesAndCredits.Builder().setCurrency(currency).addFeeOrCredit(createFeeOrCredit); // Don't charge anchor tenants EAP fees. @@ -92,7 +98,7 @@ public final class DomainPricingLogic { .setFeesAndCredits(feesBuilder.build()) .setRegistry(registry) .setDomainName(InternetDomainName.from(domainName)) - .setAsOfDate(date) + .setAsOfDate(dateTime) .setYears(years) .build()); } @@ -100,69 +106,73 @@ public final class DomainPricingLogic { /** Returns a new renew price for the pricer. */ @SuppressWarnings("unused") public FeesAndCredits getRenewPrice( - Registry registry, - String domainName, - DateTime date, - int years) - throws EppException { - Money renewCost = getDomainRenewCost(domainName, date, years); + Registry registry, String domainName, DateTime dateTime, int years) throws EppException { + DomainPrices domainPrices = getPricesForDomainName(domainName, dateTime); + BigDecimal renewCost = domainPrices.getRenewCost().multipliedBy(years).getAmount(); return customLogic.customizeRenewPrice( RenewPriceParameters.newBuilder() .setFeesAndCredits( new FeesAndCredits.Builder() .setCurrency(registry.getCurrency()) - .addFeeOrCredit(Fee.create(renewCost.getAmount(), FeeType.RENEW)) + .addFeeOrCredit(Fee.create(renewCost, FeeType.RENEW, domainPrices.isPremium())) .build()) .setRegistry(registry) .setDomainName(InternetDomainName.from(domainName)) - .setAsOfDate(date) + .setAsOfDate(dateTime) .setYears(years) .build()); } /** Returns a new restore price for the pricer. */ - public FeesAndCredits getRestorePrice(Registry registry, String domainName, DateTime date) + public FeesAndCredits getRestorePrice(Registry registry, String domainName, DateTime dateTime) throws EppException { + DomainPrices domainPrices = getPricesForDomainName(domainName, dateTime); FeesAndCredits feesAndCredits = new FeesAndCredits.Builder() .setCurrency(registry.getCurrency()) .addFeeOrCredit( - Fee.create(getDomainRenewCost(domainName, date, 1).getAmount(), FeeType.RENEW)) + Fee.create( + domainPrices.getRenewCost().getAmount(), + FeeType.RENEW, + domainPrices.isPremium())) .addFeeOrCredit( - Fee.create(registry.getStandardRestoreCost().getAmount(), FeeType.RESTORE)) + Fee.create(registry.getStandardRestoreCost().getAmount(), FeeType.RESTORE, false)) .build(); return customLogic.customizeRestorePrice( RestorePriceParameters.newBuilder() .setFeesAndCredits(feesAndCredits) .setRegistry(registry) .setDomainName(InternetDomainName.from(domainName)) - .setAsOfDate(date) + .setAsOfDate(dateTime) .build()); } /** Returns a new transfer price for the pricer. */ - public FeesAndCredits getTransferPrice(Registry registry, String domainName, DateTime date) + public FeesAndCredits getTransferPrice(Registry registry, String domainName, DateTime dateTime) throws EppException { - Money renewCost = getDomainRenewCost(domainName, date, 1); + DomainPrices domainPrices = getPricesForDomainName(domainName, dateTime); return customLogic.customizeTransferPrice( TransferPriceParameters.newBuilder() .setFeesAndCredits( new FeesAndCredits.Builder() .setCurrency(registry.getCurrency()) - .addFeeOrCredit(Fee.create(renewCost.getAmount(), FeeType.RENEW)) + .addFeeOrCredit( + Fee.create( + domainPrices.getRenewCost().getAmount(), + FeeType.RENEW, + domainPrices.isPremium())) .build()) .setRegistry(registry) .setDomainName(InternetDomainName.from(domainName)) - .setAsOfDate(date) + .setAsOfDate(dateTime) .build()); } /** Returns a new update price for the pricer. */ - public FeesAndCredits getUpdatePrice(Registry registry, String domainName, DateTime date) + public FeesAndCredits getUpdatePrice(Registry registry, String domainName, DateTime dateTime) throws EppException { CurrencyUnit currency = registry.getCurrency(); - BaseFee feeOrCredit = - Fee.create(Money.zero(registry.getCurrency()).getAmount(), FeeType.UPDATE); + BaseFee feeOrCredit = Fee.create(zeroInCurrency(currency), FeeType.UPDATE, false); return customLogic.customizeUpdatePrice( UpdatePriceParameters.newBuilder() .setFeesAndCredits( @@ -172,19 +182,19 @@ public final class DomainPricingLogic { .build()) .setRegistry(registry) .setDomainName(InternetDomainName.from(domainName)) - .setAsOfDate(date) + .setAsOfDate(dateTime) .build()); } /** Returns the fee class for a given domain and date. */ - public Optional getFeeClass(String domainName, DateTime date) { - return getDomainFeeClass(domainName, date); + public Optional getFeeClass(String domainName, DateTime dateTime) { + return getDomainFeeClass(domainName, dateTime); } + /** Returns the domain create cost with allocation-token-related discounts applied. */ private Money getDomainCreateCostWithDiscount( - String domainName, DateTime date, int years, Optional allocationToken) + DomainPrices domainPrices, int years, Optional allocationToken) throws EppException { - DomainPrices domainPrices = PricingEngineProxy.getPricesForDomainName(domainName, date); if (allocationToken.isPresent() && allocationToken.get().getDiscountFraction() != 0.0 && domainPrices.isPremium()) { diff --git a/core/src/main/java/google/registry/flows/domain/DomainRenewFlow.java b/core/src/main/java/google/registry/flows/domain/DomainRenewFlow.java index 39a11d3c4..d8871853a 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainRenewFlow.java +++ b/core/src/main/java/google/registry/flows/domain/DomainRenewFlow.java @@ -211,8 +211,7 @@ public final class DomainRenewFlow implements TransactionalFlow { BeforeResponseParameters.newBuilder() .setDomain(newDomain) .setResData(DomainRenewData.create(targetId, newExpirationTime)) - .setResponseExtensions( - createResponseExtensions(feesAndCredits.getTotalCost(), feeRenew)) + .setResponseExtensions(createResponseExtensions(feesAndCredits, feeRenew)) .build()); return responseBuilder .setResData(responseData.resData()) @@ -270,14 +269,19 @@ public final class DomainRenewFlow implements TransactionalFlow { } private ImmutableList createResponseExtensions( - Money renewCost, Optional feeRenew) { + FeesAndCredits feesAndCredits, Optional feeRenew) { return feeRenew.isPresent() ? ImmutableList.of( feeRenew .get() .createResponseBuilder() - .setCurrency(renewCost.getCurrencyUnit()) - .setFees(ImmutableList.of(Fee.create(renewCost.getAmount(), FeeType.RENEW))) + .setCurrency(feesAndCredits.getCurrency()) + .setFees( + ImmutableList.of( + Fee.create( + feesAndCredits.getRenewCost().getAmount(), + FeeType.RENEW, + feesAndCredits.hasPremiumFeesOfType(FeeType.RENEW)))) .build()) : ImmutableList.of(); } diff --git a/core/src/main/java/google/registry/flows/domain/DomainRestoreRequestFlow.java b/core/src/main/java/google/registry/flows/domain/DomainRestoreRequestFlow.java index d8f44b4f3..8e6fa7fad 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainRestoreRequestFlow.java +++ b/core/src/main/java/google/registry/flows/domain/DomainRestoreRequestFlow.java @@ -176,9 +176,7 @@ public final class DomainRestoreRequestFlow implements TransactionalFlow { ofy().delete().key(existingDomain.getDeletePollMessage()); dnsQueue.addDomainRefreshTask(existingDomain.getFullyQualifiedDomainName()); return responseBuilder - .setExtensions( - createResponseExtensions( - feesAndCredits.getRestoreCost(), feesAndCredits.getRenewCost(), feeUpdate)) + .setExtensions(createResponseExtensions(feesAndCredits, feeUpdate)) .build(); } @@ -265,17 +263,23 @@ public final class DomainRestoreRequestFlow implements TransactionalFlow { } private static ImmutableList createResponseExtensions( - Money restoreCost, Money renewCost, Optional feeUpdate) { + FeesAndCredits feesAndCredits, Optional feeUpdate) { return feeUpdate.isPresent() ? ImmutableList.of( feeUpdate .get() .createResponseBuilder() - .setCurrency(restoreCost.getCurrencyUnit()) + .setCurrency(feesAndCredits.getCurrency()) .setFees( ImmutableList.of( - Fee.create(restoreCost.getAmount(), FeeType.RESTORE), - Fee.create(renewCost.getAmount(), FeeType.RENEW))) + Fee.create( + feesAndCredits.getRestoreCost().getAmount(), + FeeType.RESTORE, + feesAndCredits.hasPremiumFeesOfType(FeeType.RESTORE)), + Fee.create( + feesAndCredits.getRenewCost().getAmount(), + FeeType.RENEW, + feesAndCredits.hasPremiumFeesOfType(FeeType.RENEW)))) .build()) : ImmutableList.of(); } diff --git a/core/src/main/java/google/registry/flows/domain/FeesAndCredits.java b/core/src/main/java/google/registry/flows/domain/FeesAndCredits.java index 8b85c0d89..65b5c905c 100644 --- a/core/src/main/java/google/registry/flows/domain/FeesAndCredits.java +++ b/core/src/main/java/google/registry/flows/domain/FeesAndCredits.java @@ -15,6 +15,7 @@ package google.registry.flows.domain; import static com.google.common.collect.ImmutableList.toImmutableList; +import static google.registry.flows.domain.DomainFlowUtils.zeroInCurrency; import static google.registry.util.CollectionUtils.nullToEmpty; import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy; import static google.registry.util.PreconditionsUtils.checkArgumentNotNull; @@ -27,6 +28,7 @@ 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 java.math.BigDecimal; import org.joda.money.CurrencyUnit; import org.joda.money.Money; @@ -39,26 +41,30 @@ public class FeesAndCredits extends ImmutableObject implements Buildable { private ImmutableList credits; 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; + return Money.of( + currency, + fees.stream() + .filter(f -> f.getType() == type) + .map(BaseFee::getCost) + .reduce(zeroInCurrency(currency), BigDecimal::add)); + } + + public boolean hasPremiumFeesOfType(FeeType type) { + return fees.stream().filter(f -> f.getType() == type).anyMatch(BaseFee::isPremium); } /** 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; + return Money.of( + currency, + Streams.concat(fees.stream(), credits.stream()) + .map(BaseFee::getCost) + .reduce(zeroInCurrency(currency), BigDecimal::add)); + } + + public boolean hasAnyPremiumFees() { + return fees.stream().anyMatch(BaseFee::isPremium); } /** Returns the create cost for the event. */ diff --git a/core/src/main/java/google/registry/model/domain/fee/BaseFee.java b/core/src/main/java/google/registry/model/domain/fee/BaseFee.java index a581b3c7a..954d9a027 100644 --- a/core/src/main/java/google/registry/model/domain/fee/BaseFee.java +++ b/core/src/main/java/google/registry/model/domain/fee/BaseFee.java @@ -104,6 +104,8 @@ public abstract class BaseFee extends ImmutableObject { @XmlTransient Range validDateRange; + @XmlTransient boolean isPremium; + public String getDescription() { return description; } @@ -120,6 +122,11 @@ public abstract class BaseFee extends ImmutableObject { return firstNonNull(refundable, true); } + /** Returns whether the fee in question is a premium price. */ + public boolean isPremium() { + return isPremium; + } + /** * According to the fee extension specification, a fee must always be non-negative, while a credit * must always be negative. Essentially, they are the same thing, just with different sign. diff --git a/core/src/main/java/google/registry/model/domain/fee/Fee.java b/core/src/main/java/google/registry/model/domain/fee/Fee.java index 58df5c8ea..a0bed40a4 100644 --- a/core/src/main/java/google/registry/model/domain/fee/Fee.java +++ b/core/src/main/java/google/registry/model/domain/fee/Fee.java @@ -31,25 +31,33 @@ import org.joda.time.DateTime; public class Fee extends BaseFee { /** Creates a Fee for the given cost and type with the default description. */ - public static Fee create(BigDecimal cost, FeeType type, Object... descriptionArgs) { + public static Fee create( + BigDecimal cost, FeeType type, boolean isPremium, Object... descriptionArgs) { checkArgumentNotNull(type, "Must specify the type of the fee"); - return createWithCustomDescription(cost, type, type.renderDescription(descriptionArgs)); + return createWithCustomDescription( + cost, type, isPremium, type.renderDescription(descriptionArgs)); } /** Creates a Fee for the given cost, type, and valid date range with the default description. */ public static Fee create( - BigDecimal cost, FeeType type, Range validDateRange, Object... descriptionArgs) { - Fee instance = create(cost, type, descriptionArgs); + BigDecimal cost, + FeeType type, + boolean isPremium, + Range validDateRange, + Object... descriptionArgs) { + Fee instance = create(cost, type, isPremium, descriptionArgs); instance.validDateRange = validDateRange; return instance; } /** Creates a Fee for the given cost and type with a custom description. */ - public static Fee createWithCustomDescription(BigDecimal cost, FeeType type, String description) { + private static Fee createWithCustomDescription( + BigDecimal cost, FeeType type, boolean isPremium, String description) { Fee instance = new Fee(); instance.cost = checkNotNull(cost); - checkArgument(instance.cost.signum() >= 0); + checkArgument(instance.cost.signum() >= 0, "Cost must be a positive number"); instance.type = checkNotNull(type); + instance.isPremium = isPremium; instance.description = description; return instance; } diff --git a/core/src/main/java/google/registry/model/domain/fee11/FeeCheckCommandExtensionV11.java b/core/src/main/java/google/registry/model/domain/fee11/FeeCheckCommandExtensionV11.java index 8116ad72e..086350b2c 100644 --- a/core/src/main/java/google/registry/model/domain/fee11/FeeCheckCommandExtensionV11.java +++ b/core/src/main/java/google/registry/model/domain/fee11/FeeCheckCommandExtensionV11.java @@ -56,7 +56,7 @@ public class FeeCheckCommandExtensionV11 extends ImmutableObject /** The period to check. */ Period period; - /** The class to check. */ + /** The fee class to check. */ @XmlElement(name = "class") String feeClass; diff --git a/core/src/main/java/google/registry/model/registry/Registry.java b/core/src/main/java/google/registry/model/registry/Registry.java index eba66765f..e5000cd3d 100644 --- a/core/src/main/java/google/registry/model/registry/Registry.java +++ b/core/src/main/java/google/registry/model/registry/Registry.java @@ -579,6 +579,8 @@ public class Registry extends ImmutableObject implements Buildable { return Fee.create( eapFeeSchedule.getValueAtTime(now).getAmount(), FeeType.EAP, + // An EAP fee counts as premium so the domain's overall Fee doesn't show as standard-priced. + true, validPeriod, validPeriod.upperEndpoint()); } diff --git a/core/src/test/java/google/registry/flows/custom/TestDomainPricingCustomLogic.java b/core/src/test/java/google/registry/flows/custom/TestDomainPricingCustomLogic.java index 84ce17cb9..432e6cf79 100644 --- a/core/src/test/java/google/registry/flows/custom/TestDomainPricingCustomLogic.java +++ b/core/src/test/java/google/registry/flows/custom/TestDomainPricingCustomLogic.java @@ -40,7 +40,7 @@ public class TestDomainPricingCustomLogic extends DomainPricingCustomLogic { public FeesAndCredits customizeRenewPrice(RenewPriceParameters priceParameters) { return priceParameters.domainName().toString().startsWith("costly-renew") ? addCustomFee( - priceParameters.feesAndCredits(), Fee.create(ONE_HUNDRED_BUCKS, FeeType.RENEW)) + priceParameters.feesAndCredits(), Fee.create(ONE_HUNDRED_BUCKS, FeeType.RENEW, true)) : priceParameters.feesAndCredits(); } @@ -48,7 +48,7 @@ public class TestDomainPricingCustomLogic extends DomainPricingCustomLogic { public FeesAndCredits customizeTransferPrice(TransferPriceParameters priceParameters) { return priceParameters.domainName().toString().startsWith("expensive") ? addCustomFee( - priceParameters.feesAndCredits(), Fee.create(ONE_HUNDRED_BUCKS, FeeType.TRANSFER)) + priceParameters.feesAndCredits(), Fee.create(ONE_HUNDRED_BUCKS, FeeType.TRANSFER, true)) : priceParameters.feesAndCredits(); } @@ -56,7 +56,7 @@ public class TestDomainPricingCustomLogic extends DomainPricingCustomLogic { public FeesAndCredits customizeUpdatePrice(UpdatePriceParameters priceParameters) { return priceParameters.domainName().toString().startsWith("non-free-update") ? addCustomFee( - priceParameters.feesAndCredits(), Fee.create(ONE_HUNDRED_BUCKS, FeeType.UPDATE)) + priceParameters.feesAndCredits(), Fee.create(ONE_HUNDRED_BUCKS, FeeType.UPDATE, true)) : priceParameters.feesAndCredits(); } diff --git a/core/src/test/java/google/registry/flows/domain/DomainTransferFlowTestCase.java b/core/src/test/java/google/registry/flows/domain/DomainTransferFlowTestCase.java index 9a2e7d9d3..93ff4292e 100644 --- a/core/src/test/java/google/registry/flows/domain/DomainTransferFlowTestCase.java +++ b/core/src/test/java/google/registry/flows/domain/DomainTransferFlowTestCase.java @@ -108,15 +108,16 @@ public class DomainTransferFlowTestCase clock.nowUtc(), DateTime.parse("1999-04-03T22:00:00.0Z"), REGISTRATION_EXPIRATION_TIME); - subordinateHost = persistResource( - new HostResource.Builder() - .setRepoId("2-".concat(Ascii.toUpperCase(tld))) - .setFullyQualifiedHostName("ns1." + label + "." + tld) - .setPersistedCurrentSponsorClientId("TheRegistrar") - .setCreationClientId("TheRegistrar") - .setCreationTimeForTest(DateTime.parse("1999-04-03T22:00:00.0Z")) - .setSuperordinateDomain(domain.createVKey()) - .build()); + subordinateHost = + persistResource( + new HostResource.Builder() + .setRepoId("2-".concat(Ascii.toUpperCase(tld))) + .setFullyQualifiedHostName("ns1." + label + "." + tld) + .setPersistedCurrentSponsorClientId("TheRegistrar") + .setCreationClientId("TheRegistrar") + .setCreationTimeForTest(DateTime.parse("1999-04-03T22:00:00.0Z")) + .setSuperordinateDomain(domain.createVKey()) + .build()); domain = persistResource( domain