Add renewal support for AT and internal registrations in DomainCreateFlow (#1591)

* Add renewal info to create flow

* Improve PR
This commit is contained in:
Rachel Guan 2022-05-13 10:04:15 -04:00 committed by GitHub
parent d0ebbde9ad
commit fd9fe398ae
2 changed files with 229 additions and 10 deletions

View file

@ -14,6 +14,7 @@
package google.registry.flows.domain;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.flows.FlowUtils.persistEntityChanges;
import static google.registry.flows.FlowUtils.validateRegistrarIsLoggedIn;
@ -54,6 +55,7 @@ import static google.registry.persistence.transaction.TransactionManagerFactory.
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.DateTimeUtils.leapSafeAddYears;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.net.InternetDomainName;
@ -80,6 +82,7 @@ import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Flag;
import google.registry.model.billing.BillingEvent.Reason;
import google.registry.model.billing.BillingEvent.Recurring;
import google.registry.model.billing.BillingEvent.RenewalPriceBehavior;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainCommand;
import google.registry.model.domain.DomainCommand.Create;
@ -115,7 +118,9 @@ import google.registry.model.tld.Registry.TldType;
import google.registry.model.tld.label.ReservationType;
import google.registry.tmch.LordnTaskUtils;
import java.util.Optional;
import javax.annotation.Nullable;
import javax.inject.Inject;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.joda.time.Duration;
@ -327,7 +332,10 @@ public final class DomainCreateFlow implements TransactionalFlow {
now);
// Create a new autorenew billing event and poll message starting at the expiration time.
BillingEvent.Recurring autorenewBillingEvent =
createAutorenewBillingEvent(domainHistoryKey, registrationExpirationTime);
createAutorenewBillingEvent(
domainHistoryKey,
registrationExpirationTime,
getRenewalPriceInfo(isAnchorTenant, allocationToken, feesAndCredits));
PollMessage.Autorenew autorenewPollMessage =
createAutorenewPollMessage(domainHistoryKey, registrationExpirationTime);
ImmutableSet.Builder<ImmutableObject> entitiesToSave = new ImmutableSet.Builder<>();
@ -546,7 +554,9 @@ public final class DomainCreateFlow implements TransactionalFlow {
}
private Recurring createAutorenewBillingEvent(
Key<DomainHistory> domainHistoryKey, DateTime registrationExpirationTime) {
Key<DomainHistory> domainHistoryKey,
DateTime registrationExpirationTime,
RenewalPriceInfo renewalpriceInfo) {
return new BillingEvent.Recurring.Builder()
.setReason(Reason.RENEW)
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
@ -555,6 +565,8 @@ public final class DomainCreateFlow implements TransactionalFlow {
.setEventTime(registrationExpirationTime)
.setRecurrenceEndTime(END_OF_TIME)
.setParent(domainHistoryKey)
.setRenewalPriceBehavior(renewalpriceInfo.renewalPriceBehavior())
.setRenewalPrice(renewalpriceInfo.renewalPrice())
.build();
}
@ -611,6 +623,48 @@ public final class DomainCreateFlow implements TransactionalFlow {
}
}
/**
* Determines the {@link RenewalPriceBehavior} and the renewal price that needs be stored in the
* {@link Recurring} billing events.
*
* <p>By default, the renewal price is calculated during the process of renewal. Renewal price
* should be the createCost if and only if the renewal price behavior in the {@link
* AllocationToken} is 'SPECIFIED'.
*/
static RenewalPriceInfo getRenewalPriceInfo(
boolean isAnchorTenant,
Optional<AllocationToken> allocationToken,
FeesAndCredits feesAndCredits) {
if (isAnchorTenant) {
if (allocationToken.isPresent()) {
checkArgument(
allocationToken.get().getRenewalPriceBehavior() != RenewalPriceBehavior.SPECIFIED,
"Renewal price behavior cannot be SPECIFIED for anchor tenant");
}
return RenewalPriceInfo.create(RenewalPriceBehavior.NONPREMIUM, null);
} else if (allocationToken.isPresent()
&& allocationToken.get().getRenewalPriceBehavior() == RenewalPriceBehavior.SPECIFIED) {
return RenewalPriceInfo.create(
RenewalPriceBehavior.SPECIFIED, feesAndCredits.getCreateCost());
} else {
return RenewalPriceInfo.create(RenewalPriceBehavior.DEFAULT, null);
}
}
/** A class to store renewal info used in {@link Recurring} billing events. */
@AutoValue
public abstract static class RenewalPriceInfo {
static DomainCreateFlow.RenewalPriceInfo create(
RenewalPriceBehavior renewalPriceBehavior, @Nullable Money renewalPrice) {
return new AutoValue_DomainCreateFlow_RenewalPriceInfo(renewalPriceBehavior, renewalPrice);
}
public abstract RenewalPriceBehavior renewalPriceBehavior();
@Nullable
public abstract Money renewalPrice();
}
private static ImmutableList<FeeTransformResponseExtension> createResponseExtensions(
Optional<FeeCreateCommandExtension> feeCreate, FeesAndCredits feesAndCredits) {
return feeCreate.isPresent()

View file

@ -21,6 +21,9 @@ import static google.registry.flows.FlowTestCase.UserPrivileges.SUPERUSER;
import static google.registry.model.billing.BillingEvent.Flag.ANCHOR_TENANT;
import static google.registry.model.billing.BillingEvent.Flag.RESERVED;
import static google.registry.model.billing.BillingEvent.Flag.SUNRISE;
import static google.registry.model.billing.BillingEvent.RenewalPriceBehavior.DEFAULT;
import static google.registry.model.billing.BillingEvent.RenewalPriceBehavior.NONPREMIUM;
import static google.registry.model.billing.BillingEvent.RenewalPriceBehavior.SPECIFIED;
import static google.registry.model.domain.fee.Fee.FEE_EXTENSION_URIS;
import static google.registry.model.domain.token.AllocationToken.TokenType.SINGLE_USE;
import static google.registry.model.domain.token.AllocationToken.TokenType.UNLIMITED_USE;
@ -83,6 +86,7 @@ import google.registry.flows.ResourceFlowTestCase;
import google.registry.flows.domain.DomainCreateFlow.AnchorTenantCreatePeriodException;
import google.registry.flows.domain.DomainCreateFlow.MustHaveSignedMarksInCurrentPhaseException;
import google.registry.flows.domain.DomainCreateFlow.NoGeneralRegistrationsInCurrentPhaseException;
import google.registry.flows.domain.DomainCreateFlow.RenewalPriceInfo;
import google.registry.flows.domain.DomainCreateFlow.SignedMarksOnlyDuringSunriseException;
import google.registry.flows.domain.DomainFlowTmchUtils.FoundMarkExpiredException;
import google.registry.flows.domain.DomainFlowTmchUtils.FoundMarkNotYetValidException;
@ -149,9 +153,12 @@ import google.registry.flows.exceptions.ResourceCreateContentionException;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Flag;
import google.registry.model.billing.BillingEvent.Reason;
import google.registry.model.billing.BillingEvent.RenewalPriceBehavior;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.fee.BaseFee.FeeType;
import google.registry.model.domain.fee.Fee;
import google.registry.model.domain.launch.LaunchNotice;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.domain.secdns.DelegationSignerData;
@ -177,6 +184,7 @@ import google.registry.testing.TestOfyAndSql;
import google.registry.testing.TestOfyOnly;
import java.math.BigDecimal;
import java.util.Map;
import java.util.Optional;
import javax.annotation.Nullable;
import org.joda.money.Money;
import org.joda.time.DateTime;
@ -258,13 +266,20 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
DomainBase domain = reloadResourceByForeignKey();
boolean isAnchorTenant = expectedBillingFlags.contains(ANCHOR_TENANT);
// Calculate the total creation cost.
Money creationCost =
// Set up the creation cost.
FeesAndCredits feesAndCredits =
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(
Fee.create(
isAnchorTenant
? Money.of(USD, 0)
? BigDecimal.valueOf(0)
: isDomainPremium(getUniqueIdFromCommand(), clock.nowUtc())
? Money.of(USD, 200)
: Money.of(USD, 26);
? BigDecimal.valueOf(200)
: BigDecimal.valueOf(26),
FeeType.CREATE,
isDomainPremium(getUniqueIdFromCommand(), clock.nowUtc())))
.build();
Money eapFee =
Money.of(
Registry.get(domainTld).getCurrency(),
@ -284,13 +299,16 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
.hasType(HistoryEntry.Type.DOMAIN_CREATE)
.and()
.hasPeriodYears(2);
RenewalPriceInfo renewalPriceInfo =
DomainCreateFlow.getRenewalPriceInfo(
isAnchorTenant, Optional.ofNullable(allocationToken), feesAndCredits);
// There should be one bill for the create and one for the recurring autorenew event.
BillingEvent.OneTime createBillingEvent =
new BillingEvent.OneTime.Builder()
.setReason(Reason.CREATE)
.setTargetId(getUniqueIdFromCommand())
.setRegistrarId("TheRegistrar")
.setCost(creationCost)
.setCost(feesAndCredits.getCreateCost())
.setPeriodYears(2)
.setEventTime(clock.nowUtc())
.setBillingTime(billingTime)
@ -308,6 +326,8 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
.setEventTime(domain.getRegistrationExpirationTime())
.setRecurrenceEndTime(END_OF_TIME)
.setParent(historyEntry)
.setRenewalPriceBehavior(renewalPriceInfo.renewalPriceBehavior())
.setRenewalPrice(renewalPriceInfo.renewalPrice())
.build();
ImmutableSet.Builder<BillingEvent> expectedBillingEvents =
@ -1159,6 +1179,28 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
assertAllocationTokenWasRedeemed("abcDEF23456");
}
@TestOfyAndSql
void testSuccess_internalRegistrationWithSpecifiedRenewalPrice() throws Exception {
allocationToken =
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDomainName("resdom.tld")
.setRenewalPriceBehavior(SPECIFIED)
.build());
// Despite the domain being FULLY_BLOCKED, the non-superuser create succeeds the domain is also
// RESERVED_FOR_SPECIFIC_USE and the correct allocation token is passed.
setEppInput(
"domain_create_allocationtoken.xml", ImmutableMap.of("DOMAIN", "resdom.tld", "YEARS", "2"));
persistContactsAndHosts();
runFlowAssertResponse(
loadFile("domain_create_response.xml", ImmutableMap.of("DOMAIN", "resdom.tld")));
assertSuccessfulCreate("tld", ImmutableSet.of(RESERVED), allocationToken);
assertNoLordn();
assertAllocationTokenWasRedeemed("abc123");
}
@TestOfyAndSql
void testFailure_anchorTenant_notTwoYearPeriod() {
setEppInput("domain_create_anchor_tenant_invalid_years.xml");
@ -2594,4 +2636,127 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
assertAboutEppExceptions().that(thrown).marshalsToXml();
DatabaseHelper.removeDatabaseMigrationSchedule();
}
@TestOfyAndSql
void testGetRenewalPriceInfo_isAnchorTenantWithoutToken_returnsNonPremiumAndNullPrice() {
assertThat(
DomainCreateFlow.getRenewalPriceInfo(
true,
Optional.empty(),
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(BigDecimal.valueOf(0), FeeType.CREATE, false))
.build()))
.isEqualTo(RenewalPriceInfo.create(NONPREMIUM, null));
}
@TestOfyAndSql
void testGetRenewalPriceInfo_isAnchorTenantWithDefaultToken_returnsNonPremiumAndNullPrice() {
assertThat(
DomainCreateFlow.getRenewalPriceInfo(
true,
Optional.of(allocationToken),
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(BigDecimal.valueOf(0), FeeType.CREATE, false))
.build()))
.isEqualTo(RenewalPriceInfo.create(NONPREMIUM, null));
}
@TestOfyAndSql
void testGetRenewalPriceInfo_isNotAnchorTenantWithDefaultToken_returnsDefaultAndNullPrice() {
assertThat(
DomainCreateFlow.getRenewalPriceInfo(
false,
Optional.of(allocationToken),
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(BigDecimal.valueOf(100), FeeType.CREATE, false))
.build()))
.isEqualTo(RenewalPriceInfo.create(DEFAULT, null));
}
@TestOfyAndSql
void testGetRenewalPriceInfo_isNotAnchorTenantWithoutToken_returnsDefaultAndNullPrice() {
assertThat(
DomainCreateFlow.getRenewalPriceInfo(
false,
Optional.empty(),
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(BigDecimal.valueOf(100), FeeType.CREATE, false))
.build()))
.isEqualTo(RenewalPriceInfo.create(DEFAULT, null));
}
@TestOfyAndSql
void
testGetRenewalPriceInfo_isNotAnchorTenantWithSpecifiedInToken_returnsSpecifiedAndCreatePrice() {
AllocationToken token =
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setRenewalPriceBehavior(SPECIFIED)
.build());
assertThat(
DomainCreateFlow.getRenewalPriceInfo(
false,
Optional.of(token),
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(BigDecimal.valueOf(100), FeeType.CREATE, false))
.build()))
.isEqualTo(RenewalPriceInfo.create(SPECIFIED, Money.of(USD, 100)));
}
@TestOfyAndSql
void testGetRenewalPriceInfo_isAnchorTenantWithSpecifiedStateInToken_throwsError() {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() ->
DomainCreateFlow.getRenewalPriceInfo(
true,
Optional.of(
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setRenewalPriceBehavior(SPECIFIED)
.build())),
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(BigDecimal.valueOf(0), FeeType.CREATE, true))
.build()));
assertThat(thrown)
.hasMessageThat()
.isEqualTo("Renewal price behavior cannot be SPECIFIED for anchor tenant");
}
@TestOfyAndSql
void testGetRenewalPriceInfo_withInvalidRenewalPriceBehavior_throwsError() {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() ->
DomainCreateFlow.getRenewalPriceInfo(
true,
Optional.of(
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setRenewalPriceBehavior(RenewalPriceBehavior.valueOf("INVALID"))
.build())),
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(BigDecimal.valueOf(0), FeeType.CREATE, true))
.build()));
assertThat(thrown)
.hasMessageThat()
.isEqualTo(
"No enum constant"
+ " google.registry.model.billing.BillingEvent.RenewalPriceBehavior.INVALID");
}
}