Add configurable discount on sunrise domain creates (#2056)

Previously we had a 15% discount applied at invoicing time. We got rid of
that inadvertently in 2022 and we want to add it back, but instead of
being applied at invoicing time we'll just apply it directly to the
creation cost when creating the billing events.

Note: previous behavior didn't care about standard vs premium pricing so
we don't either

https://buganizer.corp.google.com/issues/287070313 is a bug for the
issue, and
https://github.com/google/nomulus/pull/1710/files#diff-5097b0ef57578718444ea6b9d4c6cb32f655686a37e2ca3dd96ad2db86a77f06L151-L170
is the section of the pull request that inadvertently removed it
This commit is contained in:
gbrodman 2023-06-27 18:58:44 -04:00 committed by GitHub
parent fdfbb9572d
commit a4540a847a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 112 additions and 64 deletions

View file

@ -1576,6 +1576,11 @@ public final class RegistryConfig {
return Duration.standardDays(CONFIG_SETTINGS.get().registryPolicy.contactAutomaticTransferDays);
}
/** A discount for all sunrise domain creates, between 0.0 (no discount) and 1.0 (free). */
public static double getSunriseDomainCreateDiscount() {
return CONFIG_SETTINGS.get().registryPolicy.sunriseDomainCreateDiscount;
}
/**
* Memoizes loading of the {@link RegistryConfigSettings} POJO.
*

View file

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

View file

@ -180,6 +180,11 @@ registryPolicy:
# should generally be true for production environments, for added security.
requireSslCertificates: true
# A fractional discount, if any, to be provided to all sunrise domain creates.
# 0 means no discount will be applied, and 1 means that all sunrise creates
# will be free.
sunriseDomainCreateDiscount: 0.15
hibernate:
# Make 'SERIALIZABLE' the default isolation level to ensure correctness.
#

View file

@ -337,7 +337,8 @@ public final class DomainCreateFlow implements TransactionalFlow {
Optional<FeeCreateCommandExtension> feeCreate =
eppInput.getSingleExtension(FeeCreateCommandExtension.class);
FeesAndCredits feesAndCredits =
pricingLogic.getCreatePrice(tld, targetId, now, years, isAnchorTenant, allocationToken);
pricingLogic.getCreatePrice(
tld, targetId, now, years, isAnchorTenant, isSunriseCreate, allocationToken);
validateFeeChallenge(feeCreate, feesAndCredits, defaultTokenUsed);
Optional<SecDnsCreateExtension> secDnsCreate =
validateSecDnsExtension(eppInput.getSingleExtension(SecDnsCreateExtension.class));

View file

@ -690,6 +690,7 @@ public class DomainFlowUtils {
now,
years,
isAnchorTenant(domainName, allocationToken, Optional.empty()),
isSunrise,
allocationToken)
.getFees();
}

View file

@ -22,6 +22,7 @@ import static google.registry.util.DomainNameUtils.getTldFromDomainName;
import static google.registry.util.PreconditionsUtils.checkArgumentPresent;
import com.google.common.net.InternetDomainName;
import google.registry.config.RegistryConfig;
import google.registry.flows.EppException;
import google.registry.flows.EppException.CommandUseErrorException;
import google.registry.flows.custom.DomainPricingCustomLogic;
@ -72,26 +73,33 @@ public final class DomainPricingLogic {
DateTime dateTime,
int years,
boolean isAnchorTenant,
boolean isSunriseCreate,
Optional<AllocationToken> allocationToken)
throws EppException {
CurrencyUnit currency = tld.getCurrency();
BaseFee createFeeOrCredit;
BaseFee createFee;
// Domain create cost is always zero for anchor tenants
if (isAnchorTenant) {
createFeeOrCredit = Fee.create(zeroInCurrency(currency), FeeType.CREATE, false);
createFee = Fee.create(zeroInCurrency(currency), FeeType.CREATE, false);
} else {
DomainPrices domainPrices = getPricesForDomainName(domainName, dateTime);
Money domainCreateCost =
getDomainCreateCostWithDiscount(domainPrices, years, allocationToken);
createFeeOrCredit =
// Apply a sunrise discount if configured and applicable
if (isSunriseCreate) {
domainCreateCost =
domainCreateCost.multipliedBy(
1.0d - RegistryConfig.getSunriseDomainCreateDiscount(), RoundingMode.HALF_EVEN);
}
createFee =
Fee.create(domainCreateCost.getAmount(), FeeType.CREATE, domainPrices.isPremium());
}
// Create fees for the cost and the EAP fee, if any.
Fee eapFee = tld.getEapFeeFor(dateTime);
FeesAndCredits.Builder feesBuilder =
new FeesAndCredits.Builder().setCurrency(currency).addFeeOrCredit(createFeeOrCredit);
new FeesAndCredits.Builder().setCurrency(currency).addFeeOrCredit(createFee);
// Don't charge anchor tenants EAP fees.
if (!isAnchorTenant && !eapFee.hasZeroCost()) {
feesBuilder.addFeeOrCredit(eapFee);

View file

@ -276,16 +276,24 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
boolean isAnchorTenant = expectedBillingFlags.contains(ANCHOR_TENANT);
// Set up the creation cost.
BigDecimal createCost =
isDomainPremium(getUniqueIdFromCommand(), clock.nowUtc())
? BigDecimal.valueOf(200)
: BigDecimal.valueOf(26);
if (isAnchorTenant) {
createCost = BigDecimal.ZERO;
}
if (expectedBillingFlags.contains(SUNRISE)) {
createCost =
createCost.multiply(
BigDecimal.valueOf(1 - RegistryConfig.getSunriseDomainCreateDiscount()));
}
FeesAndCredits feesAndCredits =
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(
Fee.create(
isAnchorTenant
? BigDecimal.valueOf(0)
: isDomainPremium(getUniqueIdFromCommand(), clock.nowUtc())
? BigDecimal.valueOf(200)
: BigDecimal.valueOf(26),
createCost,
FeeType.CREATE,
isDomainPremium(getUniqueIdFromCommand(), clock.nowUtc())))
.build();

View file

@ -19,6 +19,7 @@ import static google.registry.model.billing.BillingBase.Flag.AUTO_RENEW;
import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.DEFAULT;
import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.NONPREMIUM;
import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.SPECIFIED;
import static google.registry.model.domain.fee.BaseFee.FeeType.CREATE;
import static google.registry.model.domain.fee.BaseFee.FeeType.RENEW;
import static google.registry.model.domain.token.AllocationToken.TokenType.SINGLE_USE;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_CREATE;
@ -28,7 +29,6 @@ import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.joda.money.CurrencyUnit.USD;
import static org.joda.time.DateTimeZone.UTC;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.collect.ImmutableSet;
@ -49,6 +49,7 @@ import google.registry.model.domain.fee.Fee;
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.eppinput.EppInput;
import google.registry.model.tld.Tld;
import google.registry.model.tld.Tld.TldState;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.testing.DatabaseHelper;
@ -72,11 +73,11 @@ public class DomainPricingLogicTest {
final JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
@Inject Clock clock = new FakeClock(DateTime.now(UTC));
@Inject Clock clock = new FakeClock(DateTime.parse("2023-05-13T00:00:00.000Z"));
@Mock EppInput eppInput;
SessionMetadata sessionMetadata;
@Mock FlowMetadata flowMetadata;
Tld registry;
Tld tld;
Domain domain;
@BeforeEach
@ -86,7 +87,7 @@ public class DomainPricingLogicTest {
domainPricingLogic =
new DomainPricingLogic(
new DomainPricingCustomLogic(eppInput, sessionMetadata, flowMetadata));
registry =
tld =
persistResource(
Tld.get("example")
.asBuilder()
@ -133,12 +134,32 @@ public class DomainPricingLogicTest {
return billingRecurrence;
}
@Test
void testGetDomainCreatePrice_sunrise_appliesDiscount() throws EppException {
ImmutableSortedMap<DateTime, TldState> transitions =
ImmutableSortedMap.<DateTime, TldState>naturalOrder()
.put(START_OF_TIME, TldState.PREDELEGATION)
.put(clock.nowUtc().minusHours(1), TldState.START_DATE_SUNRISE)
.put(clock.nowUtc().plusHours(1), TldState.GENERAL_AVAILABILITY)
.build();
Tld sunriseTld = createTld("sunrise", transitions);
assertThat(
domainPricingLogic.getCreatePrice(
sunriseTld, "domain.sunrise", clock.nowUtc(), 2, false, true, Optional.empty()))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
// 13 * 2 * 0.85 == 22.1
.addFeeOrCredit(Fee.create(Money.of(USD, 22.1).getAmount(), CREATE, false))
.build());
}
@Test
void testGetDomainRenewPrice_oneYear_standardDomain_noBilling_isStandardPrice()
throws EppException {
assertThat(
domainPricingLogic.getRenewPrice(
registry, "standard.example", clock.nowUtc(), 1, null, Optional.empty()))
tld, "standard.example", clock.nowUtc(), 1, null, Optional.empty()))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
@ -151,7 +172,7 @@ public class DomainPricingLogicTest {
throws EppException {
assertThat(
domainPricingLogic.getRenewPrice(
registry, "standard.example", clock.nowUtc(), 5, null, Optional.empty()))
tld, "standard.example", clock.nowUtc(), 5, null, Optional.empty()))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
@ -164,7 +185,7 @@ public class DomainPricingLogicTest {
throws EppException {
assertThat(
domainPricingLogic.getRenewPrice(
registry, "premium.example", clock.nowUtc(), 1, null, Optional.empty()))
tld, "premium.example", clock.nowUtc(), 1, null, Optional.empty()))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
@ -177,7 +198,7 @@ public class DomainPricingLogicTest {
throws EppException {
assertThat(
domainPricingLogic.getRenewPrice(
registry, "premium.example", clock.nowUtc(), 5, null, Optional.empty()))
tld, "premium.example", clock.nowUtc(), 5, null, Optional.empty()))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
@ -189,7 +210,7 @@ public class DomainPricingLogicTest {
void testGetDomainRenewPrice_oneYear_premiumDomain_default_isPremiumPrice() throws EppException {
assertThat(
domainPricingLogic.getRenewPrice(
registry,
tld,
"premium.example",
clock.nowUtc(),
1,
@ -215,7 +236,7 @@ public class DomainPricingLogicTest {
.build());
assertThat(
domainPricingLogic.getRenewPrice(
registry,
tld,
"premium.example",
clock.nowUtc(),
1,
@ -244,7 +265,7 @@ public class DomainPricingLogicTest {
AllocationTokenInvalidForPremiumNameException.class,
() ->
domainPricingLogic.getRenewPrice(
registry,
tld,
"premium.example",
clock.nowUtc(),
1,
@ -256,7 +277,7 @@ public class DomainPricingLogicTest {
void testGetDomainRenewPrice_multiYear_premiumDomain_default_isPremiumCost() throws EppException {
assertThat(
domainPricingLogic.getRenewPrice(
registry,
tld,
"premium.example",
clock.nowUtc(),
5,
@ -283,7 +304,7 @@ public class DomainPricingLogicTest {
.build());
assertThat(
domainPricingLogic.getRenewPrice(
registry,
tld,
"premium.example",
clock.nowUtc(),
5,
@ -313,7 +334,7 @@ public class DomainPricingLogicTest {
AllocationTokenInvalidForPremiumNameException.class,
() ->
domainPricingLogic.getRenewPrice(
registry,
tld,
"premium.example",
clock.nowUtc(),
5,
@ -326,7 +347,7 @@ public class DomainPricingLogicTest {
throws EppException {
assertThat(
domainPricingLogic.getRenewPrice(
registry,
tld,
"standard.example",
clock.nowUtc(),
1,
@ -352,7 +373,7 @@ public class DomainPricingLogicTest {
.build());
assertThat(
domainPricingLogic.getRenewPrice(
registry,
tld,
"standard.example",
clock.nowUtc(),
1,
@ -370,7 +391,7 @@ public class DomainPricingLogicTest {
throws EppException {
assertThat(
domainPricingLogic.getRenewPrice(
registry,
tld,
"standard.example",
clock.nowUtc(),
5,
@ -397,7 +418,7 @@ public class DomainPricingLogicTest {
.build());
assertThat(
domainPricingLogic.getRenewPrice(
registry,
tld,
"standard.example",
clock.nowUtc(),
5,
@ -415,7 +436,7 @@ public class DomainPricingLogicTest {
throws EppException {
assertThat(
domainPricingLogic.getRenewPrice(
registry,
tld,
"premium.example",
clock.nowUtc(),
1,
@ -442,7 +463,7 @@ public class DomainPricingLogicTest {
.build());
assertThat(
domainPricingLogic.getRenewPrice(
registry,
tld,
"premium.example",
clock.nowUtc(),
1,
@ -460,7 +481,7 @@ public class DomainPricingLogicTest {
throws EppException {
assertThat(
domainPricingLogic.getRenewPrice(
registry,
tld,
"premium.example",
clock.nowUtc(),
5,
@ -488,7 +509,7 @@ public class DomainPricingLogicTest {
.build());
assertThat(
domainPricingLogic.getRenewPrice(
registry,
tld,
"premium.example",
clock.nowUtc(),
5,
@ -506,7 +527,7 @@ public class DomainPricingLogicTest {
throws EppException {
assertThat(
domainPricingLogic.getRenewPrice(
registry,
tld,
"standard.example",
clock.nowUtc(),
1,
@ -524,7 +545,7 @@ public class DomainPricingLogicTest {
throws EppException {
assertThat(
domainPricingLogic.getRenewPrice(
registry,
tld,
"standard.example",
clock.nowUtc(),
5,
@ -542,7 +563,7 @@ public class DomainPricingLogicTest {
throws EppException {
assertThat(
domainPricingLogic.getRenewPrice(
registry,
tld,
"standard.example",
clock.nowUtc(),
1,
@ -570,7 +591,7 @@ public class DomainPricingLogicTest {
.build());
assertThat(
domainPricingLogic.getRenewPrice(
registry,
tld,
"standard.example",
clock.nowUtc(),
1,
@ -601,7 +622,7 @@ public class DomainPricingLogicTest {
.build());
assertThat(
domainPricingLogic.getRenewPrice(
registry,
tld,
"standard.example",
clock.nowUtc(),
1,
@ -626,7 +647,7 @@ public class DomainPricingLogicTest {
throws EppException {
assertThat(
domainPricingLogic.getRenewPrice(
registry,
tld,
"standard.example",
clock.nowUtc(),
5,
@ -654,7 +675,7 @@ public class DomainPricingLogicTest {
.build());
assertThat(
domainPricingLogic.getRenewPrice(
registry,
tld,
"standard.example",
clock.nowUtc(),
5,
@ -673,7 +694,7 @@ public class DomainPricingLogicTest {
throws EppException {
assertThat(
domainPricingLogic.getRenewPrice(
registry,
tld,
"premium.example",
clock.nowUtc(),
1,
@ -692,7 +713,7 @@ public class DomainPricingLogicTest {
throws EppException {
assertThat(
domainPricingLogic.getRenewPrice(
registry,
tld,
"premium.example",
clock.nowUtc(),
5,
@ -713,15 +734,14 @@ public class DomainPricingLogicTest {
IllegalArgumentException.class,
() ->
domainPricingLogic.getRenewPrice(
registry, "standard.example", clock.nowUtc(), -1, null, Optional.empty()));
tld, "standard.example", clock.nowUtc(), -1, null, Optional.empty()));
assertThat(thrown).hasMessageThat().isEqualTo("Number of years must be positive");
}
@Test
void testGetDomainTransferPrice_standardDomain_default_noBilling_defaultRenewalPrice()
throws EppException {
assertThat(
domainPricingLogic.getTransferPrice(registry, "standard.example", clock.nowUtc(), null))
assertThat(domainPricingLogic.getTransferPrice(tld, "standard.example", clock.nowUtc(), null))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
@ -732,8 +752,7 @@ public class DomainPricingLogicTest {
@Test
void testGetDomainTransferPrice_premiumDomain_default_noBilling_premiumRenewalPrice()
throws EppException {
assertThat(
domainPricingLogic.getTransferPrice(registry, "premium.example", clock.nowUtc(), null))
assertThat(domainPricingLogic.getTransferPrice(tld, "premium.example", clock.nowUtc(), null))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
@ -745,7 +764,7 @@ public class DomainPricingLogicTest {
void testGetDomainTransferPrice_standardDomain_default_defaultRenewalPrice() throws EppException {
assertThat(
domainPricingLogic.getTransferPrice(
registry,
tld,
"standard.example",
clock.nowUtc(),
persistDomainAndSetRecurrence("standard.example", DEFAULT, Optional.empty())))
@ -760,7 +779,7 @@ public class DomainPricingLogicTest {
void testGetDomainTransferPrice_premiumDomain_default_premiumRenewalPrice() throws EppException {
assertThat(
domainPricingLogic.getTransferPrice(
registry,
tld,
"premium.example",
clock.nowUtc(),
persistDomainAndSetRecurrence("premium.example", DEFAULT, Optional.empty())))
@ -776,7 +795,7 @@ public class DomainPricingLogicTest {
throws EppException {
assertThat(
domainPricingLogic.getTransferPrice(
registry,
tld,
"standard.example",
clock.nowUtc(),
persistDomainAndSetRecurrence("standard.example", NONPREMIUM, Optional.empty())))
@ -792,7 +811,7 @@ public class DomainPricingLogicTest {
throws EppException {
assertThat(
domainPricingLogic.getTransferPrice(
registry,
tld,
"premium.example",
clock.nowUtc(),
persistDomainAndSetRecurrence("premium.example", NONPREMIUM, Optional.empty())))
@ -808,7 +827,7 @@ public class DomainPricingLogicTest {
throws EppException {
assertThat(
domainPricingLogic.getTransferPrice(
registry,
tld,
"standard.example",
clock.nowUtc(),
persistDomainAndSetRecurrence(
@ -825,7 +844,7 @@ public class DomainPricingLogicTest {
throws EppException {
assertThat(
domainPricingLogic.getTransferPrice(
registry,
tld,
"premium.example",
clock.nowUtc(),
persistDomainAndSetRecurrence(

View file

@ -55,7 +55,7 @@
<fee:currency>USD</fee:currency>
<fee:command>create</fee:command>
<fee:period unit="y">1</fee:period>
<fee:fee description="create">13.00</fee:fee>
<fee:fee description="create">11.05</fee:fee>
</fee:cd>
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:name>allowedinsunrise.tld</fee:name>
@ -83,7 +83,7 @@
<fee:currency>USD</fee:currency>
<fee:command>create</fee:command>
<fee:period unit="y">1</fee:period>
<fee:fee description="create">13.00</fee:fee>
<fee:fee description="create">11.05</fee:fee>
<fee:class>collision</fee:class>
</fee:cd>
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
@ -115,7 +115,7 @@
<fee:currency>USD</fee:currency>
<fee:command>create</fee:command>
<fee:period unit="y">1</fee:period>
<fee:fee description="create">70.00</fee:fee>
<fee:fee description="create">59.50</fee:fee>
<fee:class>premium-collision</fee:class>
</fee:cd>
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">

View file

@ -59,7 +59,7 @@
<fee:currency>USD</fee:currency>
<fee:command>create</fee:command>
<fee:period unit="y">1</fee:period>
<fee:fee description="create">13.00</fee:fee>
<fee:fee description="create">11.05</fee:fee>
</fee:cd>
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:name>allowedinsunrise.tld</fee:name>
@ -88,7 +88,7 @@
<fee:currency>USD</fee:currency>
<fee:command>create</fee:command>
<fee:period unit="y">1</fee:period>
<fee:fee description="create">13.00</fee:fee>
<fee:fee description="create">11.05</fee:fee>
<fee:class>collision</fee:class>
</fee:cd>
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
@ -121,7 +121,7 @@
<fee:currency>USD</fee:currency>
<fee:command>create</fee:command>
<fee:period unit="y">1</fee:period>
<fee:fee description="create">70.00</fee:fee>
<fee:fee description="create">59.50</fee:fee>
<fee:class>premium-collision</fee:class>
</fee:cd>
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">

View file

@ -40,7 +40,7 @@
<fee:command>create</fee:command>
<fee:currency>USD</fee:currency>
<fee:period unit="y">1</fee:period>
<fee:fee description="create">13.00</fee:fee>
<fee:fee description="create">11.05</fee:fee>
</fee:cd>
<fee:cd avail="1">
<fee:object>
@ -49,7 +49,7 @@
<fee:command>create</fee:command>
<fee:currency>USD</fee:currency>
<fee:period unit="y">1</fee:period>
<fee:fee description="create">13.00</fee:fee>
<fee:fee description="create">11.05</fee:fee>
<fee:class>collision</fee:class>
</fee:cd>
<fee:cd avail="1">
@ -59,7 +59,7 @@
<fee:command>create</fee:command>
<fee:currency>USD</fee:currency>
<fee:period unit="y">1</fee:period>
<fee:fee description="create">70.00</fee:fee>
<fee:fee description="create">59.50</fee:fee>
<fee:class>premium-collision</fee:class>
</fee:cd>
</fee:chkData>

View file

@ -66,7 +66,7 @@
</fee:object>
<fee:command name="create">
<fee:period unit="y">1</fee:period>
<fee:fee description="create">13.00</fee:fee>
<fee:fee description="create">11.05</fee:fee>
</fee:command>
</fee:cd>
<fee:cd>
@ -102,7 +102,7 @@
</fee:object>
<fee:command name="create">
<fee:period unit="y">1</fee:period>
<fee:fee description="create">13.00</fee:fee>
<fee:fee description="create">11.05</fee:fee>
<fee:class>collision</fee:class>
</fee:command>
</fee:cd>
@ -142,7 +142,7 @@
</fee:object>
<fee:command name="create">
<fee:period unit="y">1</fee:period>
<fee:fee description="create">70.00</fee:fee>
<fee:fee description="create">59.50</fee:fee>
<fee:class>premium-collision</fee:class>
</fee:command>
</fee:cd>