Allow domain transfers with 0 period and in auto-renew grace period

Normally, if a domain is in the auto-renew grace period, a transfer will cancel the auto-renew billing event. In the event of a transfer with no change to registration end date, the auto-renew billing event should not be cancelled and the gaining registrar should not be charged for the transfer.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=170576726
This commit is contained in:
bbilbo 2017-09-30 01:29:01 -07:00 committed by Ben McIlwain
parent 3c0b17dc6f
commit 7aa5629517
9 changed files with 120 additions and 50 deletions

View file

@ -898,7 +898,6 @@ new ones with the correct approval time).
* Resource with this id does not exist. * Resource with this id does not exist.
* 2304 * 2304
* Resource status prohibits this operation. * Resource status prohibits this operation.
* Superuser extensions cannot be used during autorenew grace periods.
* Domain transfer period cannot be zero when using the fee transfer * Domain transfer period cannot be zero when using the fee transfer
extension. extension.
* The requested domain name is on the premium price list, and this * The requested domain name is on the premium price list, and this

View file

@ -138,9 +138,16 @@ public final class DomainTransferApproveFlow implements TransactionalFlow {
int extraYears = transferData.getTransferPeriod().getValue(); int extraYears = transferData.getTransferPeriod().getValue();
if (autorenewGrace != null) { if (autorenewGrace != null) {
extraYears = 0; extraYears = 0;
// During a normal transfer, if the domain is in the auto-renew grace period, the auto-renew
// billing event is cancelled and the gaining registrar is charged for the one year renewal.
// But, if the superuser extension is used to request a transfer without an additional year
// then the gaining registrar is not charged for the one year renewal and the losing registrar
// still needs to be charged for the auto-renew.
if (billingEvent.isPresent()) {
ofy().save().entity( ofy().save().entity(
BillingEvent.Cancellation.forGracePeriod(autorenewGrace, historyEntry, targetId)); BillingEvent.Cancellation.forGracePeriod(autorenewGrace, historyEntry, targetId));
} }
}
// Close the old autorenew event and poll message at the transfer time (aka now). This may end // Close the old autorenew event and poll message at the transfer time (aka now). This may end
// up deleting the poll message. // up deleting the poll message.
updateAutorenewRecurrenceEndTime(existingDomain, now); updateAutorenewRecurrenceEndTime(existingDomain, now);

View file

@ -46,7 +46,6 @@ import google.registry.flows.annotations.ReportingSpec;
import google.registry.flows.exceptions.AlreadyPendingTransferException; import google.registry.flows.exceptions.AlreadyPendingTransferException;
import google.registry.flows.exceptions.InvalidTransferPeriodValueException; import google.registry.flows.exceptions.InvalidTransferPeriodValueException;
import google.registry.flows.exceptions.ObjectAlreadySponsoredException; import google.registry.flows.exceptions.ObjectAlreadySponsoredException;
import google.registry.flows.exceptions.SuperuserExtensionAndAutorenewGracePeriodException;
import google.registry.flows.exceptions.TransferPeriodMustBeOneYearException; import google.registry.flows.exceptions.TransferPeriodMustBeOneYearException;
import google.registry.flows.exceptions.TransferPeriodZeroAndFeeTransferExtensionException; import google.registry.flows.exceptions.TransferPeriodZeroAndFeeTransferExtensionException;
import google.registry.model.domain.DomainCommand.Transfer; import google.registry.model.domain.DomainCommand.Transfer;
@ -99,7 +98,6 @@ import org.joda.time.DateTime;
* @error {@link google.registry.flows.exceptions.MissingTransferRequestAuthInfoException} * @error {@link google.registry.flows.exceptions.MissingTransferRequestAuthInfoException}
* @error {@link google.registry.flows.exceptions.ObjectAlreadySponsoredException} * @error {@link google.registry.flows.exceptions.ObjectAlreadySponsoredException}
* @error {@link google.registry.flows.exceptions.ResourceStatusProhibitsOperationException} * @error {@link google.registry.flows.exceptions.ResourceStatusProhibitsOperationException}
* @error {@link google.registry.flows.exceptions.SuperuserExtensionAndAutorenewGracePeriodException}
* @error {@link google.registry.flows.exceptions.TransferPeriodMustBeOneYearException} * @error {@link google.registry.flows.exceptions.TransferPeriodMustBeOneYearException}
* @error {@link InvalidTransferPeriodValueException} * @error {@link InvalidTransferPeriodValueException}
* @error {@link google.registry.flows.exceptions.TransferPeriodZeroAndFeeTransferExtensionException} * @error {@link google.registry.flows.exceptions.TransferPeriodZeroAndFeeTransferExtensionException}
@ -184,10 +182,6 @@ public final class DomainTransferRequestFlow implements TransactionalFlow {
DomainResource domainAtTransferTime = DomainResource domainAtTransferTime =
existingDomain.cloneProjectedAtTime(automaticTransferTime); existingDomain.cloneProjectedAtTime(automaticTransferTime);
if (!domainAtTransferTime.getGracePeriodsOfType(GracePeriodStatus.AUTO_RENEW).isEmpty()) { if (!domainAtTransferTime.getGracePeriodsOfType(GracePeriodStatus.AUTO_RENEW).isEmpty()) {
if (superuserExtension != null) {
// We don't allow the superuser extension for domains in the auto renew grace period
throw new SuperuserExtensionAndAutorenewGracePeriodException();
}
extraYears = 0; extraYears = 0;
} }
// The new expiration time if there is a server approval. // The new expiration time if there is a server approval.

View file

@ -119,7 +119,7 @@ public final class DomainTransferUtils {
return builder return builder
.addAll( .addAll(
createOptionalAutorenewCancellation( createOptionalAutorenewCancellation(
automaticTransferTime, historyEntry, targetId, existingDomain) automaticTransferTime, historyEntry, targetId, existingDomain, transferCost)
.asSet()) .asSet())
.add( .add(
createGainingClientAutorenewEvent( createGainingClientAutorenewEvent(
@ -229,7 +229,9 @@ public final class DomainTransferUtils {
* expiration time. Since the gaining registrar will still be billed for the transfer's 1-year * expiration time. Since the gaining registrar will still be billed for the transfer's 1-year
* renewal, we must issue a cancellation for the autorenew, so that the losing registrar will not * renewal, we must issue a cancellation for the autorenew, so that the losing registrar will not
* be charged (essentially, the gaining registrar takes on the cost of the year of registration * be charged (essentially, the gaining registrar takes on the cost of the year of registration
* that the autorenew just added). * that the autorenew just added). But, if the superuser extension is used to request a transfer
* without an additional year then the gaining registrar is not charged for the one year renewal
* and the losing registrar still needs to be charged for the auto-renew.
* *
* <p>For details on the policy justification, see b/19430703#comment17 and <a * <p>For details on the policy justification, see b/19430703#comment17 and <a
* href="https://www.icann.org/news/advisory-2002-06-06-en">this ICANN advisory</a>. * href="https://www.icann.org/news/advisory-2002-06-06-en">this ICANN advisory</a>.
@ -238,13 +240,14 @@ public final class DomainTransferUtils {
DateTime automaticTransferTime, DateTime automaticTransferTime,
HistoryEntry historyEntry, HistoryEntry historyEntry,
String targetId, String targetId,
DomainResource existingDomain) { DomainResource existingDomain,
Optional<Money> transferCost) {
DomainResource domainAtTransferTime = DomainResource domainAtTransferTime =
existingDomain.cloneProjectedAtTime(automaticTransferTime); existingDomain.cloneProjectedAtTime(automaticTransferTime);
GracePeriod autorenewGracePeriod = GracePeriod autorenewGracePeriod =
getOnlyElement( getOnlyElement(
domainAtTransferTime.getGracePeriodsOfType(GracePeriodStatus.AUTO_RENEW), null); domainAtTransferTime.getGracePeriodsOfType(GracePeriodStatus.AUTO_RENEW), null);
if (autorenewGracePeriod != null) { if (autorenewGracePeriod != null && transferCost.isPresent()) {
return Optional.of( return Optional.of(
BillingEvent.Cancellation.forGracePeriod(autorenewGracePeriod, historyEntry, targetId) BillingEvent.Cancellation.forGracePeriod(autorenewGracePeriod, historyEntry, targetId)
.asBuilder() .asBuilder()

View file

@ -1,25 +0,0 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.flows.exceptions;
import google.registry.flows.EppException.StatusProhibitsOperationException;
/** Superuser extensions cannot be used during autorenew grace periods. */
public class SuperuserExtensionAndAutorenewGracePeriodException extends
StatusProhibitsOperationException {
public SuperuserExtensionAndAutorenewGracePeriodException() {
super("Superuser extensions cannot be used during autorenew grace periods.");
}
}

View file

@ -42,6 +42,7 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Ordering; import com.google.common.collect.Ordering;
import com.googlecode.objectify.Key;
import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException; import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException;
import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException; import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException; import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException;
@ -52,6 +53,7 @@ import google.registry.model.billing.BillingEvent.Cancellation;
import google.registry.model.billing.BillingEvent.Cancellation.Builder; import google.registry.model.billing.BillingEvent.Cancellation.Builder;
import google.registry.model.billing.BillingEvent.OneTime; import google.registry.model.billing.BillingEvent.OneTime;
import google.registry.model.billing.BillingEvent.Reason; import google.registry.model.billing.BillingEvent.Reason;
import google.registry.model.billing.BillingEvent.Recurring;
import google.registry.model.contact.ContactAuthInfo; import google.registry.model.contact.ContactAuthInfo;
import google.registry.model.domain.DomainAuthInfo; import google.registry.model.domain.DomainAuthInfo;
import google.registry.model.domain.DomainResource; import google.registry.model.domain.DomainResource;
@ -568,7 +570,6 @@ public class DomainTransferApproveFlowTest
previousSuccessRecord.asBuilder().setReportAmount(-1).build(), previousSuccessRecord.asBuilder().setReportAmount(-1).build(),
DomainTransactionRecord.create( DomainTransactionRecord.create(
"tld", clock.nowUtc().plusDays(3), TRANSFER_SUCCESSFUL, 1)); "tld", clock.nowUtc().plusDays(3), TRANSFER_SUCCESSFUL, 1));
} }
@Test @Test
@ -589,4 +590,37 @@ public class DomainTransferApproveFlowTest
domain.getRegistrationExpirationTime().plusYears(0)); domain.getRegistrationExpirationTime().plusYears(0));
assertHistoryEntriesDoNotContainTransferBillingEventsOrGracePeriods(); assertHistoryEntriesDoNotContainTransferBillingEventsOrGracePeriods();
} }
@Test
public void testSuccess_superuserExtension_transferPeriodZero_autorenewGraceActive()
throws Exception {
DomainResource domain = reloadResourceByForeignKey();
Key<Recurring> existingAutorenewEvent = domain.getAutorenewBillingEvent();
// Set domain to have auto-renewed just before the transfer request, so that it will have an
// active autorenew grace period spanning the entire transfer window.
DateTime autorenewTime = clock.nowUtc().minusDays(1);
DateTime expirationTime = autorenewTime.plusYears(1);
TransferData.Builder transferDataBuilder = domain.getTransferData().asBuilder();
domain =
persistResource(
domain
.asBuilder()
.setTransferData(
transferDataBuilder.setTransferPeriod(Period.create(0, Unit.YEARS)).build())
.setRegistrationExpirationTime(expirationTime)
.addGracePeriod(
GracePeriod.createForRecurring(
GracePeriodStatus.AUTO_RENEW,
autorenewTime.plus(Registry.get("tld").getAutoRenewGracePeriodLength()),
"TheRegistrar",
existingAutorenewEvent))
.build());
clock.advanceOneMilli();
runSuccessfulFlowWithAssertions(
"tld",
"domain_transfer_approve.xml",
"domain_transfer_approve_response_zero_period_autorenew_grace.xml",
domain.getRegistrationExpirationTime());
assertHistoryEntriesDoNotContainTransferBillingEventsOrGracePeriods();
}
} }

View file

@ -60,7 +60,6 @@ import google.registry.flows.exceptions.InvalidTransferPeriodValueException;
import google.registry.flows.exceptions.MissingTransferRequestAuthInfoException; import google.registry.flows.exceptions.MissingTransferRequestAuthInfoException;
import google.registry.flows.exceptions.ObjectAlreadySponsoredException; import google.registry.flows.exceptions.ObjectAlreadySponsoredException;
import google.registry.flows.exceptions.ResourceStatusProhibitsOperationException; import google.registry.flows.exceptions.ResourceStatusProhibitsOperationException;
import google.registry.flows.exceptions.SuperuserExtensionAndAutorenewGracePeriodException;
import google.registry.flows.exceptions.TransferPeriodMustBeOneYearException; import google.registry.flows.exceptions.TransferPeriodMustBeOneYearException;
import google.registry.flows.exceptions.TransferPeriodZeroAndFeeTransferExtensionException; import google.registry.flows.exceptions.TransferPeriodZeroAndFeeTransferExtensionException;
import google.registry.model.billing.BillingEvent; import google.registry.model.billing.BillingEvent;
@ -713,20 +712,33 @@ public class DomainTransferRequestFlowTest
} }
@Test @Test
public void testFailure_superuserExtension_duringAutorenewGracePeriod() throws Exception { public void testSuccess_superuserExtension_zeroPeriod_autorenewGraceActive()
setupDomain("example", "tld"); throws Exception {
eppRequestSource = EppRequestSource.TOOL; eppRequestSource = EppRequestSource.TOOL;
DomainResource domain = reloadResourceByForeignKey(); setupDomain("example", "tld");
DateTime oldExpirationTime = clock.nowUtc().minusDays(1); Key<BillingEvent.Recurring> existingAutorenewEvent =
persistResource(domain.asBuilder() domain.getAutorenewBillingEvent();
.setRegistrationExpirationTime(oldExpirationTime) // Set domain to have auto-renewed just before the transfer request, so that it will have an
// active autorenew grace period spanning the entire transfer window.
DateTime autorenewTime = clock.nowUtc().minusDays(1);
DateTime expirationTime = autorenewTime.plusYears(1);
domain = persistResource(domain.asBuilder()
.setRegistrationExpirationTime(expirationTime)
.addGracePeriod(GracePeriod.createForRecurring(
GracePeriodStatus.AUTO_RENEW,
autorenewTime.plus(Registry.get("tld").getAutoRenewGracePeriodLength()),
"TheRegistrar",
existingAutorenewEvent))
.build()); .build());
clock.advanceOneMilli(); clock.advanceOneMilli();
thrown.expect(SuperuserExtensionAndAutorenewGracePeriodException.class); doSuccessfulSuperuserExtensionTest(
runTest(
"domain_transfer_request_superuser_extension.xml", "domain_transfer_request_superuser_extension.xml",
UserPrivileges.SUPERUSER, "domain_transfer_request_response_su_ext_zero_period_autorenew_grace.xml",
ImmutableMap.of("PERIOD", "1", "AUTOMATIC_TRANSFER_LENGTH", "5")); domain.getRegistrationExpirationTime(),
ImmutableMap.of("PERIOD", "0", "AUTOMATIC_TRANSFER_LENGTH", "0"),
Optional.<Money>absent(),
Period.create(0, Unit.YEARS),
Duration.standardDays(0));
} }
@Test @Test

View file

@ -0,0 +1,23 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response>
<result code="1000">
<msg>Command completed successfully</msg>
</result>
<resData>
<domain:trnData
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>example.tld</domain:name>
<domain:trStatus>clientApproved</domain:trStatus>
<domain:reID>NewRegistrar</domain:reID>
<domain:reDate>2000-06-06T22:00:00.0Z</domain:reDate>
<domain:acID>TheRegistrar</domain:acID>
<domain:acDate>2000-06-09T22:00:00.0Z</domain:acDate>
<domain:exDate>2001-06-08T22:00:00.0Z</domain:exDate>
</domain:trnData>
</resData>
<trID>
<clTRID>ABC-12345</clTRID>
<svTRID>server-trid</svTRID>
</trID>
</response>
</epp>

View file

@ -0,0 +1,23 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response>
<result code="1001">
<msg>Command completed successfully; action pending</msg>
</result>
<resData>
<domain:trnData
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>example.tld</domain:name>
<domain:trStatus>pending</domain:trStatus>
<domain:reID>NewRegistrar</domain:reID>
<domain:reDate>2000-06-09T22:00:00.0Z</domain:reDate>
<domain:acID>TheRegistrar</domain:acID>
<domain:acDate>2000-06-09T22:00:00.0Z</domain:acDate>
<domain:exDate>2001-06-08T22:00:00.0Z</domain:exDate>
</domain:trnData>
</resData>
<trID>
<clTRID>ABC-12345</clTRID>
<svTRID>server-trid</svTRID>
</trID>
</response>
</epp>