Skip to content

Commit

Permalink
Change domain-check fee responses for registrars in tiered promos
Browse files Browse the repository at this point in the history
As requested, for registrars participaing in these tiered pricing promos
that wish to receive this type of response, we make the following
changes:

1. The non-promotional (i.e. incorrect) price is returned as the
   standard domain-create fee when running a domain check.
2. The promotional (i.e. correct) price is returned as a special custom
   command class with a name of "STANDARD PROMO" when running a domain
   check.
3. Domain creates will return the non-promotional (i.e. incorrect) price
   rather than the actual promotional price. This is not implemented in
   this PR.
  • Loading branch information
gbrodman committed Jul 9, 2024
1 parent 54c5a94 commit a8c82e1
Show file tree
Hide file tree
Showing 8 changed files with 255 additions and 46 deletions.
15 changes: 13 additions & 2 deletions core/src/main/java/google/registry/config/RegistryConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -1780,6 +1780,19 @@ public static double getSunriseDomainCreateDiscount() {
return CONFIG_SETTINGS.get().registryPolicy.sunriseDomainCreateDiscount;
}

/**
* List of registrars for which we include a promotional price on domain checks if configured.
*
* <p>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 ImmutableList<String> getTieredPricingPromotionRegistrarIds() {
return ImmutableList.copyOf(
CONFIG_SETTINGS.get().registryPolicy.tieredPricingPromotionRegistrarIds);
}

/**
* Memoizes loading of the {@link RegistryConfigSettings} POJO.
*
Expand All @@ -1790,8 +1803,6 @@ public static double getSunriseDomainCreateDiscount() {
public static final Supplier<RegistryConfigSettings> CONFIG_SETTINGS =
memoize(RegistryConfig::getConfigSettings);



private static InternetAddress parseEmailAddress(String email) {
try {
return new InternetAddress(email);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ public static class RegistryPolicy {
public List<String> spec11WebResources;
public boolean requireSslCertificates;
public double sunriseDomainCreateDiscount;
public List<String> tieredPricingPromotionRegistrarIds;
}

/** Configuration for Hibernate. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ registryPolicy:
reservedTermsExportDisclaimer: |
Disclaimer line 1.
Line 2 is this 1.
tieredPricingPromotionRegistrarIds:
- NewRegistrar

caching:
singletonCacheRefreshSeconds: 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
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;
Expand Down Expand Up @@ -70,6 +71,7 @@
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;
Expand Down Expand Up @@ -129,6 +131,9 @@
@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;
Expand Down Expand Up @@ -300,6 +305,8 @@ private ImmutableList<? extends ResponseExtension> getResponseExtensions(
loadDomainsForChecks(feeCheck, domainNames, existingDomains);
ImmutableMap<String, BillingRecurrence> recurrences = loadRecurrencesForDomains(domainObjs);

boolean shouldUseTieredPricingPromotion =
RegistryConfig.getTieredPricingPromotionRegistrarIds().contains(registrarId);
for (FeeCheckCommandExtensionItem feeCheckItem : feeCheck.getItems()) {
for (String domainName : getDomainNamesToCheckForFee(feeCheckItem, domainNames.keySet())) {
Optional<AllocationToken> defaultToken =
Expand Down Expand Up @@ -332,6 +339,44 @@ private ImmutableList<? extends ResponseExtension> getResponseExtensions(
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -55,8 +62,9 @@ public static CommandName parseKnownCommand(String string) {
}
}

CommandName(boolean loadDomainForCheck) {
CommandName(boolean loadDomainForCheck, boolean acceptableInputAction) {
this.loadDomainForCheck = loadDomainForCheck;
this.acceptableInputAction = acceptableInputAction;
}

public boolean shouldLoadDomainForCheck() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -918,24 +918,6 @@ void testFeeExtension_v06() throws Exception {
runFlowAssertResponse(loadFile("domain_check_fee_response_v06.xml"));
}

private void setUpDefaultToken() {
AllocationToken defaultToken =
persistResource(
new AllocationToken.Builder()
.setToken("bbbbb")
.setTokenType(DEFAULT_PROMO)
.setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar"))
.setAllowedTlds(ImmutableSet.of("tld"))
.setAllowedEppActions(ImmutableSet.of(CommandName.CREATE))
.setDiscountFraction(0.5)
.build());
persistResource(
Tld.get("tld")
.asBuilder()
.setDefaultPromoTokens(ImmutableList.of(defaultToken.createVKey()))
.build());
}

@Test
void testFeeExtension_defaultToken_v06() throws Exception {
setUpDefaultToken();
Expand Down Expand Up @@ -1782,24 +1764,6 @@ void testFeeExtension_invalidCommand_v12() {
assertAboutEppExceptions().that(thrown).marshalsToXml();
}

private void runEapFeeCheckTest(String inputFile, String outputFile) throws Exception {
clock.setTo(DateTime.parse("2010-01-01T10:00:00Z"));
persistActiveDomain("example1.tld");
persistResource(
Tld.get("tld")
.asBuilder()
.setEapFeeSchedule(
new ImmutableSortedMap.Builder<DateTime, Money>(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");
Expand Down Expand Up @@ -1836,6 +1800,43 @@ void testIcannActivityReportField_getsLogged() throws Exception {
assertTldsFieldLogged("com", "net", "org");
}

@Test
void testTieredPricingPromoResponse() throws Exception {
sessionMetadata.setRegistrarId("NewRegistrar");
setUpDefaultToken("NewRegistrar");
persistActiveDomain("example1.tld");
setEppInput("domain_check_fee_v12.xml");
runFlowAssertResponse(loadFile("domain_check_tiered_promotion_fee_response_v12.xml"));
}

@Test
void testTieredPricingPromo_registrarNotIncluded_standardResponse() throws Exception {
setUpDefaultToken("NewRegistrar");
persistActiveDomain("example1.tld");
setEppInput("domain_check_fee_v12.xml");
runFlowAssertResponse(loadFile("domain_check_fee_response_v12.xml"));
}

@Test
void testTieredPricingPromo_registrarIncluded_noTokenActive() throws Exception {
sessionMetadata.setRegistrarId("NewRegistrar");
persistActiveDomain("example1.tld");

persistResource(
setUpDefaultToken("NewRegistrar")
.asBuilder()
.setTokenStatusTransitions(
ImmutableSortedMap.of(
START_OF_TIME,
TokenStatus.NOT_STARTED,
clock.nowUtc().plusDays(1),
TokenStatus.VALID))
.build());

setEppInput("domain_check_fee_v12.xml");
runFlowAssertResponse(loadFile("domain_check_fee_response_v12.xml"));
}

private Domain persistPendingDeleteDomain(String domainName) {
Domain existingDomain =
persistResource(
Expand Down Expand Up @@ -1867,4 +1868,45 @@ private Domain persistPendingDeleteDomain(String domainName) {
return persistResource(
existingDomain.asBuilder().setAutorenewBillingEvent(renewEvent.createVKey()).build());
}

private void runEapFeeCheckTest(String inputFile, String outputFile) throws Exception {
clock.setTo(DateTime.parse("2010-01-01T10:00:00Z"));
persistActiveDomain("example1.tld");
persistResource(
Tld.get("tld")
.asBuilder()
.setEapFeeSchedule(
new ImmutableSortedMap.Builder<DateTime, Money>(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;
}
}
Loading

0 comments on commit a8c82e1

Please sign in to comment.