diff --git a/core/src/main/java/google/registry/config/RegistryConfig.java b/core/src/main/java/google/registry/config/RegistryConfig.java index 653a9b070..6fd282867 100644 --- a/core/src/main/java/google/registry/config/RegistryConfig.java +++ b/core/src/main/java/google/registry/config/RegistryConfig.java @@ -1780,6 +1780,19 @@ public final class RegistryConfig { return CONFIG_SETTINGS.get().registryPolicy.sunriseDomainCreateDiscount; } + /** + * List of registrars for which we include a promotional price on domain checks if configured. + * + *

In these cases, when a default promotion is running for the domain+registrar combination in + * question (a DEFAULT_PROMO token is set on the TLD), the standard non-promotional price will be + * returned for that domain as the standard create price. We will then add an additional fee check + * response with the actual promotional price and a "STANDARD PROMOTION" class. + */ + public static ImmutableSet getTieredPricingPromotionRegistrarIds() { + return ImmutableSet.copyOf( + CONFIG_SETTINGS.get().registryPolicy.tieredPricingPromotionRegistrarIds); + } + /** * Memoizes loading of the {@link RegistryConfigSettings} POJO. * @@ -1790,8 +1803,6 @@ public final class RegistryConfig { public static final Supplier CONFIG_SETTINGS = memoize(RegistryConfig::getConfigSettings); - - private static InternetAddress parseEmailAddress(String email) { try { return new InternetAddress(email); diff --git a/core/src/main/java/google/registry/config/RegistryConfigSettings.java b/core/src/main/java/google/registry/config/RegistryConfigSettings.java index 2436f0efc..2385851ab 100644 --- a/core/src/main/java/google/registry/config/RegistryConfigSettings.java +++ b/core/src/main/java/google/registry/config/RegistryConfigSettings.java @@ -113,6 +113,7 @@ public class RegistryConfigSettings { public List spec11WebResources; public boolean requireSslCertificates; public double sunriseDomainCreateDiscount; + public Set tieredPricingPromotionRegistrarIds; } /** Configuration for Hibernate. */ diff --git a/core/src/main/java/google/registry/config/files/default-config.yaml b/core/src/main/java/google/registry/config/files/default-config.yaml index 592274db0..1c5ddc104 100644 --- a/core/src/main/java/google/registry/config/files/default-config.yaml +++ b/core/src/main/java/google/registry/config/files/default-config.yaml @@ -201,6 +201,15 @@ registryPolicy: # will be free. sunriseDomainCreateDiscount: 0.15 + # List of registrars participating in tiered pricing promotions that require + # non-standard responses to EPP domain:check and domain:create commands. + # When a promotion is active, we will set an additional STANDARD PROMOTION + # fee check response on any domain checks that corresponds to the actual + # promotional price (the regular response will be the non-promotional price). + # In addition, we will return the non-promotional (i.e. incorrect) price on + # domain create requests. + tieredPricingPromotionRegistrarIds: [] + hibernate: # If set to false, calls to tm().transact() cannot be nested. If set to true, # nested calls to tm().transact() are allowed, as long as they do not specify diff --git a/core/src/main/java/google/registry/config/files/nomulus-config-unittest.yaml b/core/src/main/java/google/registry/config/files/nomulus-config-unittest.yaml index 74a05ea89..fb288c3b3 100644 --- a/core/src/main/java/google/registry/config/files/nomulus-config-unittest.yaml +++ b/core/src/main/java/google/registry/config/files/nomulus-config-unittest.yaml @@ -9,6 +9,8 @@ registryPolicy: reservedTermsExportDisclaimer: | Disclaimer line 1. Line 2 is this 1. + tieredPricingPromotionRegistrarIds: + - NewRegistrar caching: singletonCacheRefreshSeconds: 0 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 22359959c..933dacded 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainCheckFlow.java +++ b/core/src/main/java/google/registry/flows/domain/DomainCheckFlow.java @@ -43,6 +43,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; import com.google.common.net.InternetDomainName; +import google.registry.config.RegistryConfig; import google.registry.config.RegistryConfig.Config; import google.registry.flows.EppException; import google.registry.flows.EppException.ParameterValuePolicyErrorException; @@ -70,6 +71,7 @@ import google.registry.model.domain.DomainCommand.Check; import google.registry.model.domain.fee.FeeCheckCommandExtension; import google.registry.model.domain.fee.FeeCheckCommandExtensionItem; import google.registry.model.domain.fee.FeeCheckResponseExtensionItem; +import google.registry.model.domain.fee.FeeQueryCommandExtensionItem; import google.registry.model.domain.fee06.FeeCheckCommandExtensionV06; import google.registry.model.domain.launch.LaunchCheckExtension; import google.registry.model.domain.token.AllocationToken; @@ -129,6 +131,9 @@ import org.joda.time.DateTime; @ReportingSpec(ActivityReportField.DOMAIN_CHECK) public final class DomainCheckFlow implements TransactionalFlow { + private static final String STANDARD_FEE_RESPONSE_CLASS = "STANDARD"; + private static final String STANDARD_PROMOTION_FEE_RESPONSE_CLASS = "STANDARD PROMOTION"; + @Inject ResourceCommand resourceCommand; @Inject ExtensionManager extensionManager; @Inject EppInput eppInput; @@ -300,6 +305,8 @@ public final class DomainCheckFlow implements TransactionalFlow { loadDomainsForChecks(feeCheck, domainNames, existingDomains); ImmutableMap recurrences = loadRecurrencesForDomains(domainObjs); + boolean shouldUseTieredPricingPromotion = + RegistryConfig.getTieredPricingPromotionRegistrarIds().contains(registrarId); for (FeeCheckCommandExtensionItem feeCheckItem : feeCheck.getItems()) { for (String domainName : getDomainNamesToCheckForFee(feeCheckItem, domainNames.keySet())) { Optional defaultToken = @@ -332,6 +339,44 @@ public final class DomainCheckFlow implements TransactionalFlow { allocationToken.isPresent() ? allocationToken : defaultToken, availableDomains.contains(domainName), recurrences.getOrDefault(domainName, null)); + // In the case of a registrar that is running a tiered pricing promotion, we issue two + // responses for the CREATE fee check command: one (the default response) with the + // non-promotional price, and one (an extra STANDARD PROMO response) with the actual + // promotional price. + if (defaultToken.isPresent() + && shouldUseTieredPricingPromotion + && feeCheckItem + .getCommandName() + .equals(FeeQueryCommandExtensionItem.CommandName.CREATE)) { + // First, set the promotional (real) price under the STANDARD PROMO class + builder + .setClass(STANDARD_PROMOTION_FEE_RESPONSE_CLASS) + .setCommand( + FeeQueryCommandExtensionItem.CommandName.CUSTOM, + feeCheckItem.getPhase(), + feeCheckItem.getSubphase()); + + // Next, get the non-promotional price and set it as the standard response to the CREATE + // fee check command + FeeCheckResponseExtensionItem.Builder nonPromotionalBuilder = + feeCheckItem.createResponseBuilder(); + handleFeeRequest( + feeCheckItem, + nonPromotionalBuilder, + domainNames.get(domainName), + domain, + feeCheck.getCurrency(), + now, + pricingLogic, + allocationToken, + availableDomains.contains(domainName), + recurrences.getOrDefault(domainName, null)); + responseItems.add( + nonPromotionalBuilder + .setClass(STANDARD_FEE_RESPONSE_CLASS) + .setDomainNameIfSupported(domainName) + .build()); + } responseItems.add(builder.setDomainNameIfSupported(domainName).build()); } catch (AllocationTokenInvalidForPremiumNameException | AllocationTokenNotValidForCommandException diff --git a/core/src/main/java/google/registry/model/domain/fee/FeeQueryCommandExtensionItem.java b/core/src/main/java/google/registry/model/domain/fee/FeeQueryCommandExtensionItem.java index 2d98090bd..bbb564d89 100644 --- a/core/src/main/java/google/registry/model/domain/fee/FeeQueryCommandExtensionItem.java +++ b/core/src/main/java/google/registry/model/domain/fee/FeeQueryCommandExtensionItem.java @@ -34,19 +34,26 @@ public abstract class FeeQueryCommandExtensionItem extends ImmutableObject { /** The name of a command that might have an associated fee. */ public enum CommandName { - UNKNOWN(false), - CREATE(false), - RENEW(true), - TRANSFER(true), - RESTORE(true), - UPDATE(false); + UNKNOWN(false, false), + CREATE(false, true), + RENEW(true, true), + TRANSFER(true, true), + RESTORE(true, true), + UPDATE(false, true), + /** + * We don't accept CUSTOM commands in requests but may issue them in responses. A CUSTOM command + * name is permitted in general per RFC 8748 section 3.1. + */ + CUSTOM(false, false); private final boolean loadDomainForCheck; + private final boolean acceptableInputAction; public static CommandName parseKnownCommand(String string) { try { CommandName command = valueOf(string); - checkArgument(!command.equals(UNKNOWN)); + checkArgument( + command.acceptableInputAction, "Command %s is not an acceptable input action", string); return command; } catch (IllegalArgumentException e) { throw new IllegalArgumentException( @@ -55,8 +62,9 @@ public abstract class FeeQueryCommandExtensionItem extends ImmutableObject { } } - CommandName(boolean loadDomainForCheck) { + CommandName(boolean loadDomainForCheck, boolean acceptableInputAction) { this.loadDomainForCheck = loadDomainForCheck; + this.acceptableInputAction = acceptableInputAction; } public boolean shouldLoadDomainForCheck() { 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 01db9f56d..6b626ce4c 100644 --- a/core/src/test/java/google/registry/flows/domain/DomainCheckFlowTest.java +++ b/core/src/test/java/google/registry/flows/domain/DomainCheckFlowTest.java @@ -918,24 +918,6 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase(Ordering.natural()) - .put(START_OF_TIME, Money.of(USD, 0)) - .put(clock.nowUtc().minusDays(1), Money.of(USD, 100)) - .put(clock.nowUtc().plusDays(1), Money.of(USD, 50)) - .put(clock.nowUtc().plusDays(2), Money.of(USD, 0)) - .build()) - .build()); - setEppInput(inputFile, ImmutableMap.of("CURRENCY", "USD")); - runFlowAssertResponse(loadFile(outputFile)); - } - @Test void testSuccess_eapFeeCheck_v06() throws Exception { runEapFeeCheckTest("domain_check_fee_v06.xml", "domain_check_eap_fee_response_v06.xml"); @@ -1836,6 +1800,43 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase(Ordering.natural()) + .put(START_OF_TIME, Money.of(USD, 0)) + .put(clock.nowUtc().minusDays(1), Money.of(USD, 100)) + .put(clock.nowUtc().plusDays(1), Money.of(USD, 50)) + .put(clock.nowUtc().plusDays(2), Money.of(USD, 0)) + .build()) + .build()); + setEppInput(inputFile, ImmutableMap.of("CURRENCY", "USD")); + runFlowAssertResponse(loadFile(outputFile)); + } + + private AllocationToken setUpDefaultToken() { + return setUpDefaultToken("TheRegistrar"); + } + + private AllocationToken setUpDefaultToken(String registrarId) { + AllocationToken defaultToken = + persistResource( + new AllocationToken.Builder() + .setToken("bbbbb") + .setTokenType(DEFAULT_PROMO) + .setAllowedRegistrarIds(ImmutableSet.of(registrarId)) + .setAllowedTlds(ImmutableSet.of("tld")) + .setAllowedEppActions(ImmutableSet.of(CommandName.CREATE)) + .setDiscountFraction(0.5) + .build()); + persistResource( + Tld.get("tld") + .asBuilder() + .setDefaultPromoTokens(ImmutableList.of(defaultToken.createVKey())) + .build()); + return defaultToken; + } } diff --git a/core/src/test/resources/google/registry/flows/domain/domain_check_tiered_promotion_fee_response_v12.xml b/core/src/test/resources/google/registry/flows/domain/domain_check_tiered_promotion_fee_response_v12.xml new file mode 100644 index 000000000..a5df0a6a7 --- /dev/null +++ b/core/src/test/resources/google/registry/flows/domain/domain_check_tiered_promotion_fee_response_v12.xml @@ -0,0 +1,91 @@ + + + + Command completed successfully + + + + + example1.tld + In use + + + example2.tld + + + example3.tld + + + + + + USD + + + example1.tld + + + 1 + 13.00 + STANDARD + + + + + example1.tld + + + 1 + 6.50 + STANDARD PROMOTION + + + + + example2.tld + + + 1 + 13.00 + STANDARD + + + + + example2.tld + + + 1 + 6.50 + STANDARD PROMOTION + + + + + example3.tld + + + 1 + 13.00 + STANDARD + + + + + example3.tld + + + 1 + 6.50 + STANDARD PROMOTION + + + + + + ABC-12345 + server-trid + + + \ No newline at end of file