Skip to content

Commit 45c8b81

Browse files
authored
Map token renewal behavior directly onto BillingRecurrence (#2635)
Instead of using a separate RenewalPriceInfo object, just map the behavior (if it exists) onto the BillingRecurrence with a special carve-out, as always, for anchor tenants (note: this shouldn't matter much since anchor tenants *should* use NONPREMIUM renewal tokens anyway, but just in case, double-check). This also fixes DomainPricingLogic to treat a multiyear create as a one-year-create + n-minus-1-year-renewal for cases where either the creation or the renewal (or both) are nonpremium.
1 parent 4cfcc60 commit 45c8b81

File tree

4 files changed

+202
-129
lines changed

4 files changed

+202
-129
lines changed

core/src/main/java/google/registry/flows/domain/DomainCreateFlow.java

Lines changed: 14 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
package google.registry.flows.domain;
1616

17-
import static com.google.common.base.Preconditions.checkArgument;
1817
import static com.google.common.collect.ImmutableSet.toImmutableSet;
1918
import static google.registry.dns.DnsUtils.requestDomainDnsRefresh;
2019
import static google.registry.flows.FlowUtils.persistEntityChanges;
@@ -120,9 +119,7 @@
120119
import google.registry.model.tmch.ClaimsListDao;
121120
import google.registry.tmch.LordnTaskUtils.LordnPhase;
122121
import java.util.Optional;
123-
import javax.annotation.Nullable;
124122
import javax.inject.Inject;
125-
import org.joda.money.Money;
126123
import org.joda.time.DateTime;
127124
import org.joda.time.Duration;
128125

@@ -363,9 +360,7 @@ public EppResponse run() throws EppException {
363360
// Create a new autorenew billing event and poll message starting at the expiration time.
364361
BillingRecurrence autorenewBillingEvent =
365362
createAutorenewBillingEvent(
366-
domainHistoryId,
367-
registrationExpirationTime,
368-
getRenewalPriceInfo(isAnchorTenant, allocationToken));
363+
domainHistoryId, registrationExpirationTime, isAnchorTenant, allocationToken);
369364
PollMessage.Autorenew autorenewPollMessage =
370365
createAutorenewPollMessage(domainHistoryId, registrationExpirationTime);
371366
ImmutableSet.Builder<ImmutableObject> entitiesToSave = new ImmutableSet.Builder<>();
@@ -625,7 +620,17 @@ private BillingEvent createBillingEvent(
625620
private BillingRecurrence createAutorenewBillingEvent(
626621
HistoryEntryId domainHistoryId,
627622
DateTime registrationExpirationTime,
628-
RenewalPriceInfo renewalpriceInfo) {
623+
boolean isAnchorTenant,
624+
Optional<AllocationToken> allocationToken) {
625+
// Non-standard renewal behaviors can occur for anchor tenants (always NONPREMIUM pricing) or if
626+
// explicitly configured in the token (either NONPREMIUM or directly SPECIFIED). Use DEFAULT if
627+
// none is configured.
628+
RenewalPriceBehavior renewalPriceBehavior =
629+
isAnchorTenant
630+
? RenewalPriceBehavior.NONPREMIUM
631+
: allocationToken
632+
.map(AllocationToken::getRenewalPriceBehavior)
633+
.orElse(RenewalPriceBehavior.DEFAULT);
629634
return new BillingRecurrence.Builder()
630635
.setReason(Reason.RENEW)
631636
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
@@ -634,8 +639,8 @@ private BillingRecurrence createAutorenewBillingEvent(
634639
.setEventTime(registrationExpirationTime)
635640
.setRecurrenceEndTime(END_OF_TIME)
636641
.setDomainHistoryId(domainHistoryId)
637-
.setRenewalPriceBehavior(renewalpriceInfo.renewalPriceBehavior())
638-
.setRenewalPrice(renewalpriceInfo.renewalPrice())
642+
.setRenewalPriceBehavior(renewalPriceBehavior)
643+
.setRenewalPrice(allocationToken.flatMap(AllocationToken::getRenewalPrice).orElse(null))
639644
.build();
640645
}
641646

@@ -679,41 +684,6 @@ private static PollMessage.OneTime createNameCollisionOneTimePollMessage(
679684
.build();
680685
}
681686

682-
/**
683-
* Determines the {@link RenewalPriceBehavior} and the renewal price that needs be stored in the
684-
* {@link BillingRecurrence} billing events.
685-
*
686-
* <p>By default, the renewal price is calculated during the process of renewal. Renewal price
687-
* should be the createCost if and only if the renewal price behavior in the {@link
688-
* AllocationToken} is 'SPECIFIED'.
689-
*/
690-
static RenewalPriceInfo getRenewalPriceInfo(
691-
boolean isAnchorTenant, Optional<AllocationToken> allocationToken) {
692-
if (isAnchorTenant) {
693-
allocationToken.ifPresent(
694-
token ->
695-
checkArgument(
696-
token.getRenewalPriceBehavior() != RenewalPriceBehavior.SPECIFIED,
697-
"Renewal price behavior cannot be SPECIFIED for anchor tenant"));
698-
return RenewalPriceInfo.create(RenewalPriceBehavior.NONPREMIUM, null);
699-
} else if (allocationToken.isPresent()
700-
&& allocationToken.get().getRenewalPriceBehavior() == RenewalPriceBehavior.SPECIFIED) {
701-
return RenewalPriceInfo.create(
702-
RenewalPriceBehavior.SPECIFIED, allocationToken.get().getRenewalPrice().get());
703-
} else {
704-
return RenewalPriceInfo.create(RenewalPriceBehavior.DEFAULT, null);
705-
}
706-
}
707-
708-
/** A record to store renewal info used in {@link BillingRecurrence} billing events. */
709-
public record RenewalPriceInfo(
710-
RenewalPriceBehavior renewalPriceBehavior, @Nullable Money renewalPrice) {
711-
static RenewalPriceInfo create(
712-
RenewalPriceBehavior renewalPriceBehavior, @Nullable Money renewalPrice) {
713-
return new RenewalPriceInfo(renewalPriceBehavior, renewalPrice);
714-
}
715-
}
716-
717687
private static ImmutableList<FeeTransformResponseExtension> createResponseExtensions(
718688
Optional<FeeCreateCommandExtension> feeCreate, FeesAndCredits feesAndCredits) {
719689
return feeCreate

core/src/main/java/google/registry/flows/domain/DomainPricingLogic.java

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,14 +85,10 @@ FeesAndCredits getCreatePrice(
8585
createFee = Fee.create(zeroInCurrency(currency), FeeType.CREATE, false);
8686
} else {
8787
DomainPrices domainPrices = getPricesForDomainName(domainName, dateTime);
88-
if (allocationToken.isPresent()
89-
&& allocationToken
90-
.get()
91-
.getRegistrationBehavior()
92-
.equals(RegistrationBehavior.NONPREMIUM_CREATE)) {
88+
if (allocationToken.isPresent()) {
89+
// Handle any special NONPREMIUM / SPECIFIED cases configured in the token
9390
domainPrices =
94-
DomainPrices.create(
95-
false, tld.getCreateBillingCost(dateTime), domainPrices.getRenewCost());
91+
applyTokenToDomainPrices(domainPrices, tld, dateTime, years, allocationToken.get());
9692
}
9793
Money domainCreateCost =
9894
getDomainCreateCostWithDiscount(domainPrices, years, allocationToken, tld);
@@ -357,6 +353,27 @@ private Money getDomainCostWithDiscount(
357353
return totalDomainFlowCost;
358354
}
359355

356+
private DomainPrices applyTokenToDomainPrices(
357+
DomainPrices domainPrices, Tld tld, DateTime dateTime, int years, AllocationToken token) {
358+
// Convert to nonpremium iff no premium charges are included (either in create or any renewal)
359+
boolean convertToNonPremium =
360+
token.getRegistrationBehavior().equals(RegistrationBehavior.NONPREMIUM_CREATE)
361+
&& (years == 1
362+
|| !token.getRenewalPriceBehavior().equals(RenewalPriceBehavior.DEFAULT));
363+
boolean isPremium = domainPrices.isPremium() && !convertToNonPremium;
364+
Money createCost =
365+
token.getRegistrationBehavior().equals(RegistrationBehavior.NONPREMIUM_CREATE)
366+
? tld.getCreateBillingCost(dateTime)
367+
: domainPrices.getCreateCost();
368+
Money renewCost =
369+
token.getRenewalPriceBehavior().equals(RenewalPriceBehavior.NONPREMIUM)
370+
? tld.getStandardRenewCost(dateTime)
371+
: token.getRenewalPriceBehavior().equals(RenewalPriceBehavior.SPECIFIED)
372+
? token.getRenewalPrice().get()
373+
: domainPrices.getRenewCost();
374+
return DomainPrices.create(isPremium, createCost, renewCost);
375+
}
376+
360377
/** An allocation token was provided that is invalid for premium domains. */
361378
public static class AllocationTokenInvalidForPremiumNameException
362379
extends CommandUseErrorException {

core/src/test/java/google/registry/flows/domain/DomainCreateFlowTest.java

Lines changed: 76 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import static google.registry.model.billing.BillingBase.Flag.ANCHOR_TENANT;
2323
import static google.registry.model.billing.BillingBase.Flag.RESERVED;
2424
import static google.registry.model.billing.BillingBase.Flag.SUNRISE;
25-
import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.DEFAULT;
2625
import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.NONPREMIUM;
2726
import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.SPECIFIED;
2827
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_OPTIONAL;
@@ -87,7 +86,6 @@
8786
import google.registry.flows.domain.DomainCreateFlow.MustHaveSignedMarksInCurrentPhaseException;
8887
import google.registry.flows.domain.DomainCreateFlow.NoGeneralRegistrationsInCurrentPhaseException;
8988
import google.registry.flows.domain.DomainCreateFlow.NoTrademarkedRegistrationsBeforeSunriseException;
90-
import google.registry.flows.domain.DomainCreateFlow.RenewalPriceInfo;
9189
import google.registry.flows.domain.DomainCreateFlow.SignedMarksOnlyDuringSunriseException;
9290
import google.registry.flows.domain.DomainFlowTmchUtils.FoundMarkExpiredException;
9391
import google.registry.flows.domain.DomainFlowTmchUtils.FoundMarkNotYetValidException;
@@ -303,10 +301,8 @@ private void assertSuccessfulCreate(
303301

304302
boolean isAnchorTenant = expectedBillingFlags.contains(ANCHOR_TENANT);
305303
// Set up the creation cost.
306-
BigDecimal createCost =
307-
isDomainPremium(getUniqueIdFromCommand(), clock.nowUtc())
308-
? BigDecimal.valueOf(200)
309-
: BigDecimal.valueOf(24);
304+
boolean isDomainPremium = isDomainPremium(getUniqueIdFromCommand(), clock.nowUtc());
305+
BigDecimal createCost = isDomainPremium ? BigDecimal.valueOf(200) : BigDecimal.valueOf(24);
310306
if (isAnchorTenant) {
311307
createCost = BigDecimal.ZERO;
312308
}
@@ -315,6 +311,26 @@ private void assertSuccessfulCreate(
315311
createCost.multiply(
316312
BigDecimal.valueOf(1 - RegistryConfig.getSunriseDomainCreateDiscount()));
317313
}
314+
if (allocationToken != null) {
315+
if (allocationToken
316+
.getRegistrationBehavior()
317+
.equals(RegistrationBehavior.NONPREMIUM_CREATE)) {
318+
createCost =
319+
createCost.subtract(
320+
BigDecimal.valueOf(isDomainPremium ? 87 : 0)); // premium is 100, standard 13
321+
}
322+
if (allocationToken.getRenewalPriceBehavior().equals(NONPREMIUM)) {
323+
createCost =
324+
createCost.subtract(
325+
BigDecimal.valueOf(isDomainPremium ? 89 : 0)); // premium is 100, standard 11
326+
}
327+
if (allocationToken.getRenewalPriceBehavior().equals(SPECIFIED)) {
328+
createCost =
329+
createCost
330+
.subtract(BigDecimal.valueOf(isDomainPremium ? 100 : 11))
331+
.add(allocationToken.getRenewalPrice().get().getAmount());
332+
}
333+
}
318334
FeesAndCredits feesAndCredits =
319335
new FeesAndCredits.Builder()
320336
.setCurrency(USD)
@@ -343,8 +359,12 @@ private void assertSuccessfulCreate(
343359
.hasType(HistoryEntry.Type.DOMAIN_CREATE)
344360
.and()
345361
.hasPeriodYears(2);
346-
RenewalPriceInfo renewalPriceInfo =
347-
DomainCreateFlow.getRenewalPriceInfo(isAnchorTenant, Optional.ofNullable(allocationToken));
362+
RenewalPriceBehavior expectedRenewalPriceBehavior =
363+
isAnchorTenant
364+
? RenewalPriceBehavior.NONPREMIUM
365+
: Optional.ofNullable(allocationToken)
366+
.map(AllocationToken::getRenewalPriceBehavior)
367+
.orElse(RenewalPriceBehavior.DEFAULT);
348368
// There should be one bill for the create and one for the recurrence autorenew event.
349369
BillingEvent createBillingEvent =
350370
new BillingEvent.Builder()
@@ -369,8 +389,11 @@ private void assertSuccessfulCreate(
369389
.setEventTime(domain.getRegistrationExpirationTime())
370390
.setRecurrenceEndTime(END_OF_TIME)
371391
.setDomainHistory(historyEntry)
372-
.setRenewalPriceBehavior(renewalPriceInfo.renewalPriceBehavior())
373-
.setRenewalPrice(renewalPriceInfo.renewalPrice())
392+
.setRenewalPriceBehavior(expectedRenewalPriceBehavior)
393+
.setRenewalPrice(
394+
Optional.ofNullable(allocationToken)
395+
.flatMap(AllocationToken::getRenewalPrice)
396+
.orElse(null))
374397
.build();
375398

376399
ImmutableSet.Builder<BillingBase> expectedBillingEvents =
@@ -3187,85 +3210,62 @@ void testEppMetric_isSuccessfullyCreated() throws Exception {
31873210
}
31883211

31893212
@Test
3190-
void testGetRenewalPriceInfo_isAnchorTenantWithoutToken_returnsNonPremiumAndNullPrice() {
3191-
assertThat(DomainCreateFlow.getRenewalPriceInfo(true, Optional.empty()))
3192-
.isEqualTo(RenewalPriceInfo.create(NONPREMIUM, null));
3193-
}
3194-
3195-
@Test
3196-
void testGetRenewalPriceInfo_isAnchorTenantWithDefaultToken_returnsNonPremiumAndNullPrice() {
3197-
assertThat(DomainCreateFlow.getRenewalPriceInfo(true, Optional.of(allocationToken)))
3198-
.isEqualTo(RenewalPriceInfo.create(NONPREMIUM, null));
3199-
}
3200-
3201-
@Test
3202-
void testGetRenewalPriceInfo_isNotAnchorTenantWithDefaultToken_returnsDefaultAndNullPrice() {
3203-
assertThat(DomainCreateFlow.getRenewalPriceInfo(false, Optional.of(allocationToken)))
3204-
.isEqualTo(RenewalPriceInfo.create(DEFAULT, null));
3213+
void testSuccess_anchorTenant_nonPremiumRenewal() throws Exception {
3214+
AllocationToken token =
3215+
persistResource(
3216+
new AllocationToken.Builder()
3217+
.setToken("abc123")
3218+
.setTokenType(SINGLE_USE)
3219+
.setDomainName("example.tld")
3220+
.setRegistrationBehavior(RegistrationBehavior.ANCHOR_TENANT)
3221+
.build());
3222+
persistContactsAndHosts();
3223+
setEppInput(
3224+
"domain_create_allocationtoken.xml",
3225+
ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2"));
3226+
runFlow();
3227+
assertSuccessfulCreate("tld", ImmutableSet.of(ANCHOR_TENANT), token);
32053228
}
32063229

32073230
@Test
3208-
void testGetRenewalPriceInfo_isNotAnchorTenantWithoutToken_returnsDefaultAndNullPrice() {
3209-
assertThat(DomainCreateFlow.getRenewalPriceInfo(false, Optional.empty()))
3210-
.isEqualTo(RenewalPriceInfo.create(DEFAULT, null));
3231+
void testSuccess_nonAnchorTenant_nonPremiumRenewal() throws Exception {
3232+
createTld("example");
3233+
AllocationToken token =
3234+
persistResource(
3235+
new AllocationToken.Builder()
3236+
.setToken("abc123")
3237+
.setTokenType(SINGLE_USE)
3238+
.setDomainName("rich.example")
3239+
.setRenewalPriceBehavior(NONPREMIUM)
3240+
.build());
3241+
persistContactsAndHosts();
3242+
// Creation is still $100 but it'll create a NONPREMIUM renewal
3243+
setEppInput(
3244+
"domain_create_premium_allocationtoken.xml",
3245+
ImmutableMap.of("YEARS", "2", "FEE", "111.00"));
3246+
runFlow();
3247+
assertSuccessfulCreate("example", ImmutableSet.of(), token);
32113248
}
32123249

32133250
@Test
3214-
void
3215-
testGetRenewalPriceInfo_isNotAnchorTenantWithSpecifiedInToken_returnsSpecifiedAndCreatePrice() {
3251+
void testSuccess_specifiedRenewalPriceToken_specifiedRecurrencePrice() throws Exception {
3252+
createTld("example");
32163253
AllocationToken token =
32173254
persistResource(
32183255
new AllocationToken.Builder()
32193256
.setToken("abc123")
32203257
.setTokenType(SINGLE_USE)
3258+
.setDomainName("rich.example")
32213259
.setRenewalPriceBehavior(SPECIFIED)
3222-
.setRenewalPrice(Money.of(USD, 5))
3260+
.setRenewalPrice(Money.of(USD, 1))
32233261
.build());
3224-
assertThat(DomainCreateFlow.getRenewalPriceInfo(false, Optional.of(token)))
3225-
.isEqualTo(RenewalPriceInfo.create(SPECIFIED, Money.of(USD, 5)));
3226-
}
3227-
3228-
@Test
3229-
void testGetRenewalPriceInfo_isAnchorTenantWithSpecifiedStateInToken_throwsError() {
3230-
IllegalArgumentException thrown =
3231-
assertThrows(
3232-
IllegalArgumentException.class,
3233-
() ->
3234-
DomainCreateFlow.getRenewalPriceInfo(
3235-
true,
3236-
Optional.of(
3237-
persistResource(
3238-
new AllocationToken.Builder()
3239-
.setToken("abc123")
3240-
.setTokenType(SINGLE_USE)
3241-
.setRenewalPriceBehavior(SPECIFIED)
3242-
.setRenewalPrice(Money.of(USD, 0))
3243-
.build()))));
3244-
assertThat(thrown)
3245-
.hasMessageThat()
3246-
.isEqualTo("Renewal price behavior cannot be SPECIFIED for anchor tenant");
3247-
}
3248-
3249-
@Test
3250-
void testGetRenewalPriceInfo_withInvalidRenewalPriceBehavior_throwsError() {
3251-
IllegalArgumentException thrown =
3252-
assertThrows(
3253-
IllegalArgumentException.class,
3254-
() ->
3255-
DomainCreateFlow.getRenewalPriceInfo(
3256-
true,
3257-
Optional.of(
3258-
persistResource(
3259-
new AllocationToken.Builder()
3260-
.setToken("abc123")
3261-
.setTokenType(SINGLE_USE)
3262-
.setRenewalPriceBehavior(RenewalPriceBehavior.valueOf("INVALID"))
3263-
.build()))));
3264-
assertThat(thrown)
3265-
.hasMessageThat()
3266-
.isEqualTo(
3267-
"No enum constant"
3268-
+ " google.registry.model.billing.BillingBase.RenewalPriceBehavior.INVALID");
3262+
persistContactsAndHosts();
3263+
// Creation is still $100 but it'll create a $1 renewal
3264+
setEppInput(
3265+
"domain_create_premium_allocationtoken.xml",
3266+
ImmutableMap.of("YEARS", "2", "FEE", "101.00"));
3267+
runFlow();
3268+
assertSuccessfulCreate("example", ImmutableSet.of(), token);
32693269
}
32703270

32713271
@Test

0 commit comments

Comments
 (0)