1
0
mirror of https://github.com/google/nomulus synced 2026-01-03 11:45:39 +00:00

Change domain-check fee responses for registrars in tiered promos (#2489)

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.
This commit is contained in:
gbrodman
2024-07-12 11:50:39 -04:00
committed by GitHub
parent d9c5d71f40
commit 04a4431d6a
8 changed files with 255 additions and 46 deletions

View File

@@ -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.
*
* <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 ImmutableSet<String> 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<RegistryConfigSettings> CONFIG_SETTINGS =
memoize(RegistryConfig::getConfigSettings);
private static InternetAddress parseEmailAddress(String email) {
try {
return new InternetAddress(email);

View File

@@ -113,6 +113,7 @@ public class RegistryConfigSettings {
public List<String> spec11WebResources;
public boolean requireSslCertificates;
public double sunriseDomainCreateDiscount;
public Set<String> tieredPricingPromotionRegistrarIds;
}
/** Configuration for Hibernate. */

View File

@@ -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

View File

@@ -9,6 +9,8 @@ registryPolicy:
reservedTermsExportDisclaimer: |
Disclaimer line 1.
Line 2 is this 1.
tieredPricingPromotionRegistrarIds:
- NewRegistrar
caching:
singletonCacheRefreshSeconds: 0

View File

@@ -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<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 =
@@ -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

View File

@@ -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() {

View File

@@ -918,24 +918,6 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
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();
@@ -1782,24 +1764,6 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
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");
@@ -1836,6 +1800,43 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
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(
@@ -1867,4 +1868,45 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
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;
}
}

View File

@@ -0,0 +1,91 @@
<epp xmlns:domain="urn:ietf:params:xml:ns:domain-1.0" xmlns:contact="urn:ietf:params:xml:ns:contact-1.0" xmlns:fee="urn:ietf:params:xml:ns:fee-0.6" xmlns="urn:ietf:params:xml:ns:epp-1.0" xmlns:rgp="urn:ietf:params:xml:ns:rgp-1.0" xmlns:bulkToken="urn:google:params:xml:ns:bulkToken-1.0" xmlns:fee11="urn:ietf:params:xml:ns:fee-0.11" xmlns:fee="urn:ietf:params:xml:ns:fee-0.12" xmlns:launch="urn:ietf:params:xml:ns:launch-1.0" xmlns:secDNS="urn:ietf:params:xml:ns:secDNS-1.1" xmlns:host="urn:ietf:params:xml:ns:host-1.0">
<response>
<result code="1000">
<msg>Command completed successfully</msg>
</result>
<resData>
<domain:chkData>
<domain:cd>
<domain:name avail="false">example1.tld</domain:name>
<domain:reason>In use</domain:reason>
</domain:cd>
<domain:cd>
<domain:name avail="true">example2.tld</domain:name>
</domain:cd>
<domain:cd>
<domain:name avail="true">example3.tld</domain:name>
</domain:cd>
</domain:chkData>
</resData>
<extension>
<fee:chkData xmlns:fee="urn:ietf:params:xml:ns:fee-0.12"
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<fee:currency>USD</fee:currency>
<fee:cd>
<fee:object>
<domain:name>example1.tld</domain:name>
</fee:object>
<fee:command name="create">
<fee:period unit="y">1</fee:period>
<fee:fee description="create">13.00</fee:fee>
<fee:class>STANDARD</fee:class>
</fee:command>
</fee:cd>
<fee:cd>
<fee:object>
<domain:name>example1.tld</domain:name>
</fee:object>
<fee:command name="custom">
<fee:period unit="y">1</fee:period>
<fee:fee description="create">6.50</fee:fee>
<fee:class>STANDARD PROMOTION</fee:class>
</fee:command>
</fee:cd>
<fee:cd>
<fee:object>
<domain:name>example2.tld</domain:name>
</fee:object>
<fee:command name="create">
<fee:period unit="y">1</fee:period>
<fee:fee description="create">13.00</fee:fee>
<fee:class>STANDARD</fee:class>
</fee:command>
</fee:cd>
<fee:cd>
<fee:object>
<domain:name>example2.tld</domain:name>
</fee:object>
<fee:command name="custom">
<fee:period unit="y">1</fee:period>
<fee:fee description="create">6.50</fee:fee>
<fee:class>STANDARD PROMOTION</fee:class>
</fee:command>
</fee:cd>
<fee:cd>
<fee:object>
<domain:name>example3.tld</domain:name>
</fee:object>
<fee:command name="create">
<fee:period unit="y">1</fee:period>
<fee:fee description="create">13.00</fee:fee>
<fee:class>STANDARD</fee:class>
</fee:command>
</fee:cd>
<fee:cd>
<fee:object>
<domain:name>example3.tld</domain:name>
</fee:object>
<fee:command name="custom">
<fee:period unit="y">1</fee:period>
<fee:fee description="create">6.50</fee:fee>
<fee:class>STANDARD PROMOTION</fee:class>
</fee:command>
</fee:cd>
</fee:chkData>
</extension>
<trID>
<clTRID>ABC-12345</clTRID>
<svTRID>server-trid</svTRID>
</trID>
</response>
</epp>