diff --git a/core/src/main/java/google/registry/flows/domain/DomainCheckFlow.java b/core/src/main/java/google/registry/flows/domain/DomainCheckFlow.java index 467147a29..79ff68108 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainCheckFlow.java +++ b/core/src/main/java/google/registry/flows/domain/DomainCheckFlow.java @@ -50,6 +50,7 @@ import google.registry.flows.annotations.ReportingSpec; import google.registry.flows.custom.DomainCheckFlowCustomLogic; import google.registry.flows.custom.DomainCheckFlowCustomLogic.BeforeResponseParameters; import google.registry.flows.custom.DomainCheckFlowCustomLogic.BeforeResponseReturnData; +import google.registry.flows.domain.DomainPricingLogic.AllocationTokenInvalidForPremiumNameException; import google.registry.flows.domain.token.AllocationTokenDomainCheckResults; import google.registry.flows.domain.token.AllocationTokenFlowUtils; import google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotInPromotionException; @@ -81,6 +82,7 @@ import google.registry.model.tld.Tld; import google.registry.model.tld.Tld.TldState; import google.registry.model.tld.label.ReservationType; import google.registry.persistence.VKey; +import google.registry.pricing.PricingEngineProxy; import google.registry.util.Clock; import java.util.HashSet; import java.util.Optional; @@ -291,6 +293,7 @@ public final class DomainCheckFlow implements Flow { allocationToken.get(), feeCheckItem.getCommandName(), registrarId, + PricingEngineProxy.isDomainPremium(domainName, now), now); } handleFeeRequest( @@ -305,7 +308,8 @@ public final class DomainCheckFlow implements Flow { availableDomains.contains(domainName), recurrences.getOrDefault(domainName, null)); responseItems.add(builder.setDomainNameIfSupported(domainName).build()); - } catch (AllocationTokenNotValidForCommandException + } catch (AllocationTokenInvalidForPremiumNameException + | AllocationTokenNotValidForCommandException | AllocationTokenNotValidForDomainException | AllocationTokenNotValidForRegistrarException | AllocationTokenNotValidForTldException 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 b3b208ed0..71dfc302b 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java +++ b/core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java @@ -74,6 +74,7 @@ import google.registry.flows.EppException.ParameterValueSyntaxErrorException; import google.registry.flows.EppException.RequiredParameterMissingException; import google.registry.flows.EppException.StatusProhibitsOperationException; import google.registry.flows.EppException.UnimplementedOptionException; +import google.registry.flows.domain.DomainPricingLogic.AllocationTokenInvalidForPremiumNameException; import google.registry.flows.domain.token.AllocationTokenFlowUtils; import google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException; import google.registry.model.EppResource; @@ -1217,8 +1218,15 @@ public class DomainFlowUtils { for (Optional token : tokenList) { try { AllocationTokenFlowUtils.validateToken( - InternetDomainName.from(domainName), token.get(), commandName, registrarId, now); - } catch (AssociationProhibitsOperationException | StatusProhibitsOperationException e) { + InternetDomainName.from(domainName), + token.get(), + commandName, + registrarId, + isDomainPremium(domainName, now), + now); + } catch (AssociationProhibitsOperationException + | StatusProhibitsOperationException + | AllocationTokenInvalidForPremiumNameException e) { // Allocation token was not valid for this registration, continue to check the next token in // the list continue; 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 4d81d14f9..b2a393fdf 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainPricingLogic.java +++ b/core/src/main/java/google/registry/flows/domain/DomainPricingLogic.java @@ -16,6 +16,7 @@ package google.registry.flows.domain; import static com.google.common.base.Preconditions.checkArgument; import static google.registry.flows.domain.DomainFlowUtils.zeroInCurrency; +import static google.registry.flows.domain.token.AllocationTokenFlowUtils.validateTokenForPossiblePremiumName; import static google.registry.pricing.PricingEngineProxy.getPricesForDomainName; import static google.registry.util.DomainNameUtils.getTldFromDomainName; import static google.registry.util.PreconditionsUtils.checkArgumentPresent; @@ -257,12 +258,7 @@ public final class DomainPricingLogic { private Money getDomainCostWithDiscount( boolean isPremium, int years, Optional allocationToken, Money oneYearCost) throws AllocationTokenInvalidForPremiumNameException { - if (allocationToken.isPresent() - && allocationToken.get().getDiscountFraction() != 0.0 - && isPremium - && !allocationToken.get().shouldDiscountPremiums()) { - throw new AllocationTokenInvalidForPremiumNameException(); - } + validateTokenForPossiblePremiumName(allocationToken, isPremium); Money totalDomainFlowCost = oneYearCost.multipliedBy(years); // Apply the allocation token discount, if applicable. @@ -281,8 +277,8 @@ public final class DomainPricingLogic { /** An allocation token was provided that is invalid for premium domains. */ public static class AllocationTokenInvalidForPremiumNameException extends CommandUseErrorException { - AllocationTokenInvalidForPremiumNameException() { - super("A nonzero discount code cannot be applied to premium domains"); + public AllocationTokenInvalidForPremiumNameException() { + super("Token not valid for premium name"); } } } diff --git a/core/src/main/java/google/registry/flows/domain/token/AllocationTokenFlowUtils.java b/core/src/main/java/google/registry/flows/domain/token/AllocationTokenFlowUtils.java index f0a0badda..ed51eb9a1 100644 --- a/core/src/main/java/google/registry/flows/domain/token/AllocationTokenFlowUtils.java +++ b/core/src/main/java/google/registry/flows/domain/token/AllocationTokenFlowUtils.java @@ -16,6 +16,7 @@ package google.registry.flows.domain.token; import static com.google.common.base.Preconditions.checkArgument; import static google.registry.persistence.transaction.TransactionManagerFactory.tm; +import static google.registry.pricing.PricingEngineProxy.isDomainPremium; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; @@ -26,6 +27,7 @@ import google.registry.flows.EppException; import google.registry.flows.EppException.AssociationProhibitsOperationException; import google.registry.flows.EppException.AuthorizationErrorException; import google.registry.flows.EppException.StatusProhibitsOperationException; +import google.registry.flows.domain.DomainPricingLogic.AllocationTokenInvalidForPremiumNameException; import google.registry.model.billing.BillingBase.RenewalPriceBehavior; import google.registry.model.billing.BillingRecurrence; import google.registry.model.domain.Domain; @@ -79,7 +81,13 @@ public class AllocationTokenFlowUtils { ImmutableMap.Builder resultsBuilder = new ImmutableMap.Builder<>(); for (InternetDomainName domainName : domainNames) { try { - validateToken(domainName, tokenEntity, CommandName.CREATE, registrarId, now); + validateToken( + domainName, + tokenEntity, + CommandName.CREATE, + registrarId, + isDomainPremium(domainName.toString(), now), + now); validDomainNames.add(domainName); } catch (EppException e) { resultsBuilder.put(domainName, e.getMessage()); @@ -105,7 +113,7 @@ public class AllocationTokenFlowUtils { /** * Validates a given token. The token could be invalid if it has allowed client IDs or TLDs that * do not include this client ID / TLD, or if the token has a promotion that is not currently - * running. + * running, or the token is not valid for a premium name when necessary. * * @throws EppException if the token is invalid in any way */ @@ -114,33 +122,48 @@ public class AllocationTokenFlowUtils { AllocationToken token, CommandName commandName, String registrarId, + boolean isPremium, DateTime now) throws EppException { // Only tokens with default behavior require validation - if (TokenBehavior.DEFAULT.equals(token.getTokenBehavior())) { - if (!token.getAllowedEppActions().isEmpty() - && !token.getAllowedEppActions().contains(commandName)) { - throw new AllocationTokenNotValidForCommandException(); - } - if (!token.getAllowedRegistrarIds().isEmpty() - && !token.getAllowedRegistrarIds().contains(registrarId)) { - throw new AllocationTokenNotValidForRegistrarException(); - } - if (!token.getAllowedTlds().isEmpty() - && !token.getAllowedTlds().contains(domainName.parent().toString())) { - throw new AllocationTokenNotValidForTldException(); - } - if (token.getDomainName().isPresent() - && !token.getDomainName().get().equals(domainName.toString())) { - throw new AllocationTokenNotValidForDomainException(); - } - // Tokens without status transitions will just have a single-entry NOT_STARTED map, so only - // check the status transitions map if it's non-trivial. - if (token.getTokenStatusTransitions().size() > 1 - && !TokenStatus.VALID.equals(token.getTokenStatusTransitions().getValueAtTime(now))) { - throw new AllocationTokenNotInPromotionException(); - } + if (!TokenBehavior.DEFAULT.equals(token.getTokenBehavior())) { + return; + } + validateTokenForPossiblePremiumName(Optional.of(token), isPremium); + if (!token.getAllowedEppActions().isEmpty() + && !token.getAllowedEppActions().contains(commandName)) { + throw new AllocationTokenNotValidForCommandException(); + } + if (!token.getAllowedRegistrarIds().isEmpty() + && !token.getAllowedRegistrarIds().contains(registrarId)) { + throw new AllocationTokenNotValidForRegistrarException(); + } + if (!token.getAllowedTlds().isEmpty() + && !token.getAllowedTlds().contains(domainName.parent().toString())) { + throw new AllocationTokenNotValidForTldException(); + } + if (token.getDomainName().isPresent() + && !token.getDomainName().get().equals(domainName.toString())) { + throw new AllocationTokenNotValidForDomainException(); + } + // Tokens without status transitions will just have a single-entry NOT_STARTED map, so only + // check the status transitions map if it's non-trivial. + if (token.getTokenStatusTransitions().size() > 1 + && !TokenStatus.VALID.equals(token.getTokenStatusTransitions().getValueAtTime(now))) { + throw new AllocationTokenNotInPromotionException(); + } + } + + /** Validates that the given token is valid for a premium name if the name is premium. */ + public static void validateTokenForPossiblePremiumName( + Optional token, boolean isPremium) + throws AllocationTokenInvalidForPremiumNameException { + if (token.isPresent() + && token.get().getDiscountFraction() != 0.0 + && isPremium + && !token.get().shouldDiscountPremiums()) { + throw new AllocationTokenInvalidForPremiumNameException(); } } @@ -187,6 +210,7 @@ public class AllocationTokenFlowUtils { tokenEntity, CommandName.CREATE, registrarId, + isDomainPremium(command.getDomainName(), now), now); return Optional.of(tokenCustomLogic.validateToken(command, tokenEntity, tld, registrarId, now)); } @@ -209,6 +233,7 @@ public class AllocationTokenFlowUtils { tokenEntity, commandName, registrarId, + isDomainPremium(existingDomain.getDomainName(), now), now); return Optional.of( tokenCustomLogic.validateToken(existingDomain, tokenEntity, tld, registrarId, now)); diff --git a/core/src/test/java/google/registry/flows/domain/DomainCheckFlowTest.java b/core/src/test/java/google/registry/flows/domain/DomainCheckFlowTest.java index 04c7fea95..cfc677408 100644 --- a/core/src/test/java/google/registry/flows/domain/DomainCheckFlowTest.java +++ b/core/src/test/java/google/registry/flows/domain/DomainCheckFlowTest.java @@ -407,6 +407,26 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase assertThat(billingEvent.getCost()).isEqualTo(Money.of(USD, 22)); } + @Test + void testSuccess_doesNotApplyNonPremiumDefaultTokenToPremiumName() throws Exception { + ImmutableMap customFeeMap = updateSubstitutions(FEE_06_MAP, "FEE", "500"); + setEppInput("domain_renew_fee.xml", customFeeMap); + persistDomain(); + AllocationToken defaultToken1 = + persistResource( + new AllocationToken.Builder() + .setToken("aaaaa") + .setTokenType(DEFAULT_PROMO) + .setDiscountFraction(0.5) + .setDiscountYears(1) + .setAllowedTlds(ImmutableSet.of("tld")) + .build()); + persistResource( + Tld.get("tld") + .asBuilder() + .setDefaultPromoTokens(ImmutableList.of(defaultToken1.createVKey())) + .setPremiumList(persistPremiumList("tld", USD, "example,USD 100")) + .build()); + runFlowAssertResponse( + loadFile( + "domain_renew_response_fee.xml", + ImmutableMap.of( + "NAME", + "example.tld", + "PERIOD", + "5", + "EX_DATE", + "2005-04-03T22:00:00.0Z", + "FEE", + "500.0", + "CURRENCY", + "USD", + "FEE_VERSION", + "0.6", + "FEE_NS", + "fee"))); + BillingEvent billingEvent = + Iterables.getOnlyElement(DatabaseHelper.loadAllOf(BillingEvent.class)); + assertThat(billingEvent.getTargetId()).isEqualTo("example.tld"); + assertThat(billingEvent.getAllocationToken()).isEmpty(); + } + @Test void testSuccess_onlyUsesFirstValidToken() throws Exception { setEppInput("domain_renew.xml", ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2")); diff --git a/core/src/test/resources/google/registry/flows/domain/domain_check_allocationtoken_multiname_promotion.xml b/core/src/test/resources/google/registry/flows/domain/domain_check_allocationtoken_multiname_promotion.xml new file mode 100644 index 000000000..973b9ad02 --- /dev/null +++ b/core/src/test/resources/google/registry/flows/domain/domain_check_allocationtoken_multiname_promotion.xml @@ -0,0 +1,20 @@ + + + + + example1.example + rich.example + example3.example + + + + + abc123 + + + ABC-12345 + + \ No newline at end of file