diff --git a/java/google/registry/flows/domain/DomainTransferApproveFlow.java b/java/google/registry/flows/domain/DomainTransferApproveFlow.java index 022296acd..7a898928f 100644 --- a/java/google/registry/flows/domain/DomainTransferApproveFlow.java +++ b/java/google/registry/flows/domain/DomainTransferApproveFlow.java @@ -14,7 +14,6 @@ package google.registry.flows.domain; -import static com.google.common.collect.Iterables.filter; import static com.google.common.collect.Iterables.getOnlyElement; import static google.registry.flows.FlowUtils.validateClientIsLoggedIn; import static google.registry.flows.ResourceFlowUtils.approvePendingTransfer; @@ -32,7 +31,6 @@ import static google.registry.pricing.PricingEngineProxy.getDomainRenewCost; import static google.registry.util.DateTimeUtils.END_OF_TIME; import com.google.common.base.Optional; -import com.google.common.base.Predicate; import com.google.common.collect.ImmutableSet; import com.googlecode.objectify.Key; import google.registry.flows.EppException; @@ -123,16 +121,9 @@ public final class DomainTransferApproveFlow implements TransactionalFlow { .setParent(historyEntry) .build(); // If we are within an autorenew grace period, cancel the autorenew billing event and reduce - // the number of years to extend the registration by one. - GracePeriod autorenewGrace = getOnlyElement( - filter( - existingDomain.getGracePeriods(), - new Predicate() { - @Override - public boolean apply(GracePeriod gracePeriod) { - return GracePeriodStatus.AUTO_RENEW.equals(gracePeriod.getType()); - }}), - null); + // the number of years to extend the registration by one to "subsume" the autorenew. + GracePeriod autorenewGrace = + getOnlyElement(existingDomain.getGracePeriodsOfType(GracePeriodStatus.AUTO_RENEW), null); if (autorenewGrace != null) { extraYears--; ofy().save().entity( diff --git a/java/google/registry/flows/domain/DomainTransferRequestFlow.java b/java/google/registry/flows/domain/DomainTransferRequestFlow.java index a2f8eddc1..58e0d6eb6 100644 --- a/java/google/registry/flows/domain/DomainTransferRequestFlow.java +++ b/java/google/registry/flows/domain/DomainTransferRequestFlow.java @@ -50,6 +50,7 @@ import google.registry.model.domain.Period; import google.registry.model.domain.fee.FeeTransferCommandExtension; import google.registry.model.domain.fee.FeeTransformResponseExtension; import google.registry.model.domain.metadata.MetadataExtension; +import google.registry.model.domain.rgp.GracePeriodStatus; import google.registry.model.eppcommon.AuthInfo; import google.registry.model.eppcommon.StatusValue; import google.registry.model.eppcommon.Trid; @@ -138,9 +139,27 @@ public final class DomainTransferRequestFlow implements TransactionalFlow { validateFeeChallenge(targetId, tld, now, feeTransfer, feesAndCredits); HistoryEntry historyEntry = buildHistory(period, existingDomain, now); DateTime automaticTransferTime = now.plus(registry.getAutomaticTransferLength()); + + // If the domain will be in the auto-renew grace period at the moment of transfer, the transfer + // will subsume the autorenew, so we reduce by 1 the number of years to extend the registration. + // Note that the regular "years" remains the same since it affects the transfer charge amount. + // The gaining registrar is still billed for the full years; the losing registrar will get a + // cancellation for the autorenew written out within createTransferServerApproveEntities(). + // + // See b/19430703#comment17 and https://www.icann.org/news/advisory-2002-06-06-en for the + // policy documentation for transfers subsuming autorenews within the autorenew grace period. + int registrationExtensionYears = years; + DomainResource domainAtTransferTime = + existingDomain.cloneProjectedAtTime(automaticTransferTime); + if (!domainAtTransferTime.getGracePeriodsOfType(GracePeriodStatus.AUTO_RENEW).isEmpty()) { + registrationExtensionYears--; + } // The new expiration time if there is a server approval. - DateTime serverApproveNewExpirationTime = extendRegistrationWithCap( - automaticTransferTime, existingDomain.getRegistrationExpirationTime(), years); + DateTime serverApproveNewExpirationTime = + extendRegistrationWithCap( + automaticTransferTime, + domainAtTransferTime.getRegistrationExpirationTime(), + registrationExtensionYears); // Create speculative entities in anticipation of an automatic server approval. ImmutableSet serverApproveEntities = createTransferServerApproveEntities( diff --git a/java/google/registry/flows/domain/DomainTransferUtils.java b/java/google/registry/flows/domain/DomainTransferUtils.java index dc550e602..b4cc21876 100644 --- a/java/google/registry/flows/domain/DomainTransferUtils.java +++ b/java/google/registry/flows/domain/DomainTransferUtils.java @@ -26,6 +26,8 @@ import google.registry.model.billing.BillingEvent; import google.registry.model.billing.BillingEvent.Flag; import google.registry.model.billing.BillingEvent.Reason; import google.registry.model.domain.DomainResource; +import google.registry.model.domain.GracePeriod; +import google.registry.model.domain.rgp.GracePeriodStatus; import google.registry.model.eppcommon.Trid; import google.registry.model.poll.PendingActionNotificationResponse.DomainPendingActionNotificationResponse; import google.registry.model.poll.PollMessage; @@ -39,7 +41,6 @@ import google.registry.model.transfer.TransferStatus; import javax.annotation.Nullable; import org.joda.money.Money; import org.joda.time.DateTime; -import org.joda.time.Duration; /** * Utility logic for facilitating domain transfers. @@ -217,34 +218,33 @@ public final class DomainTransferUtils { /** * Creates an optional autorenew cancellation if one would apply to the server-approved transfer. * - *

If there will be an autorenew between now and the automatic transfer time, and if the - * autorenew grace period length is long enough that the domain will still be within it at the - * automatic transfer time, then the transfer will subsume the autorenew and we need to write out - * a cancellation for it. + *

If the domain will be in the auto-renew grace period at the automatic transfer time, then + * the transfer will subsume the autorenew. This means that we "cancel" the 1-year extension of + * the autorenew before applying the extra transfer years, which in effect means reducing the + * transfer extended registration years by one. Since the gaining registrar will still be billed + * for the full extended registration years, 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 that the autorenew just added). + * + *

For details on the policy justification, see b/19430703#comment17 and + * this ICANN advisory. */ - // TODO(b/19430703): the above logic is incomplete; it doesn't handle a grace period that started - // before the transfer was requested and continues through the automatic transfer time. private static Optional createOptionalAutorenewCancellation( DateTime automaticTransferTime, HistoryEntry historyEntry, String targetId, DomainResource existingDomain) { - Registry registry = Registry.get(existingDomain.getTld()); - DateTime oldExpirationTime = existingDomain.getRegistrationExpirationTime(); - Duration autoRenewGracePeriodLength = registry.getAutoRenewGracePeriodLength(); - if (automaticTransferTime.isAfter(oldExpirationTime) - && automaticTransferTime.isBefore(oldExpirationTime.plus(autoRenewGracePeriodLength))) { - return Optional.of(new BillingEvent.Cancellation.Builder() - .setReason(Reason.RENEW) - .setFlags(ImmutableSet.of(Flag.AUTO_RENEW)) - .setTargetId(targetId) - .setClientId(existingDomain.getCurrentSponsorClientId()) - .setEventTime(automaticTransferTime) - .setBillingTime(existingDomain.getRegistrationExpirationTime() - .plus(registry.getAutoRenewGracePeriodLength())) - .setRecurringEventKey(existingDomain.getAutorenewBillingEvent()) - .setParent(historyEntry) - .build()); + DomainResource domainAtTransferTime = + existingDomain.cloneProjectedAtTime(automaticTransferTime); + GracePeriod autorenewGracePeriod = + getOnlyElement( + domainAtTransferTime.getGracePeriodsOfType(GracePeriodStatus.AUTO_RENEW), null); + if (autorenewGracePeriod != null) { + return Optional.of( + BillingEvent.Cancellation.forGracePeriod(autorenewGracePeriod, historyEntry, targetId) + .asBuilder() + .setEventTime(automaticTransferTime) + .build()); } return Optional.absent(); } diff --git a/javatests/google/registry/flows/domain/DomainTransferRequestFlowTest.java b/javatests/google/registry/flows/domain/DomainTransferRequestFlowTest.java index 3bfe4385d..27349d3ce 100644 --- a/javatests/google/registry/flows/domain/DomainTransferRequestFlowTest.java +++ b/javatests/google/registry/flows/domain/DomainTransferRequestFlowTest.java @@ -39,6 +39,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.Iterables; +import com.googlecode.objectify.Key; import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException; import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException; import google.registry.flows.domain.DomainFlowUtils.BadPeriodUnitException; @@ -55,7 +56,6 @@ import google.registry.flows.exceptions.ObjectAlreadySponsoredException; import google.registry.flows.exceptions.ResourceStatusProhibitsOperationException; import google.registry.model.billing.BillingEvent; import google.registry.model.billing.BillingEvent.Cancellation.Builder; -import google.registry.model.billing.BillingEvent.Flag; import google.registry.model.billing.BillingEvent.Reason; import google.registry.model.contact.ContactAuthInfo; import google.registry.model.domain.DomainAuthInfo; @@ -579,28 +579,97 @@ public class DomainTransferRequestFlowTest } @Test - public void testSuccess_autorenewBeforeAutomaticTransfer() throws Exception { + public void testSuccess_autorenewGraceActive_onlyAtTransferRequestTime() throws Exception { setupDomain("example", "tld"); - DomainResource oldResource = persistResource(reloadResourceByForeignKey().asBuilder() - .setRegistrationExpirationTime(clock.nowUtc().plusDays(1).plus(1)) + // Set the domain to have auto-renewed long enough ago that it is still in the autorenew grace + // period at the transfer request time, but will have exited it by the automatic transfer time. + DateTime autorenewTime = + clock.nowUtc().minus(Registry.get("tld").getAutoRenewGracePeriodLength()).plusDays(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", + domain.getAutorenewBillingEvent())) .build()); clock.advanceOneMilli(); - // The autorenew should be subsumed into the transfer resulting in 2 years of renewal in total. + // Since the autorenew grace period will have ended by the automatic transfer time, subsuming + // will not occur in the server-approve case, so the transfer will add 1 year to the current + // expiration time as usual, and no Cancellation will be issued. Note however that if the + // transfer were to be manually approved before the autorenew grace period ends, then the + // DomainTransferApproveFlow will still issue a Cancellation. doSuccessfulTest( - "domain_transfer_request_2_years.xml", - "domain_transfer_request_response_2_years.xml", - clock.nowUtc().plusDays(1).plusYears(2), + "domain_transfer_request.xml", + "domain_transfer_request_response_autorenew_grace_at_request_only.xml", + expirationTime.plusYears(1)); + } + + @Test + public void testSuccess_autorenewGraceActive_throughoutTransferWindow() throws Exception { + setupDomain("example", "tld"); + Key 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); + domain = persistResource(domain.asBuilder() + .setRegistrationExpirationTime(expirationTime) + .addGracePeriod(GracePeriod.createForRecurring( + GracePeriodStatus.AUTO_RENEW, + autorenewTime.plus(Registry.get("tld").getAutoRenewGracePeriodLength()), + "TheRegistrar", + existingAutorenewEvent)) + .build()); + clock.advanceOneMilli(); + // The transfer will subsume the recent autorenew, so there will be no net change in expiration + // time caused by the transfer, but we must write a Cancellation. + doSuccessfulTest( + "domain_transfer_request.xml", + "domain_transfer_request_response_autorenew_grace_throughout_transfer_window.xml", + expirationTime, new BillingEvent.Cancellation.Builder() .setReason(Reason.RENEW) - .setFlags(ImmutableSet.of(Flag.AUTO_RENEW)) .setTargetId("example.tld") .setClientId("TheRegistrar") // The cancellation happens at the moment of transfer. .setEventTime(clock.nowUtc().plus(Registry.get("tld").getAutomaticTransferLength())) - .setBillingTime(oldResource.getRegistrationExpirationTime().plus( + .setBillingTime(autorenewTime.plus( Registry.get("tld").getAutoRenewGracePeriodLength())) // The cancellation should refer to the old autorenew billing event. - .setRecurringEventKey(oldResource.getAutorenewBillingEvent())); + .setRecurringEventKey(existingAutorenewEvent)); + } + + @Test + public void testSuccess_autorenewGraceActive_onlyAtAutomaticTransferTime() throws Exception { + setupDomain("example", "tld"); + Key existingAutorenewEvent = + domain.getAutorenewBillingEvent(); + // Set domain to expire in 1 day, so that it will be in the autorenew grace period by the + // automatic transfer time, even though it isn't yet. + DateTime expirationTime = clock.nowUtc().plusDays(1); + domain = persistResource(domain.asBuilder() + .setRegistrationExpirationTime(expirationTime) + .build()); + clock.advanceOneMilli(); + // The transfer will subsume the future autorenew, meaning that the expected server-approve + // expiration time will be 1 year beyond the current one, and we must write a Cancellation. + doSuccessfulTest( + "domain_transfer_request.xml", + "domain_transfer_request_response_autorenew_grace_at_transfer_only.xml", + expirationTime.plusYears(1), + new BillingEvent.Cancellation.Builder() + .setReason(Reason.RENEW) + .setTargetId("example.tld") + .setClientId("TheRegistrar") + // The cancellation happens at the moment of transfer. + .setEventTime(clock.nowUtc().plus(Registry.get("tld").getAutomaticTransferLength())) + .setBillingTime( + expirationTime.plus(Registry.get("tld").getAutoRenewGracePeriodLength())) + // The cancellation should refer to the old autorenew billing event. + .setRecurringEventKey(existingAutorenewEvent)); } @Test diff --git a/javatests/google/registry/flows/domain/testdata/domain_transfer_request_response_2_years.xml b/javatests/google/registry/flows/domain/testdata/domain_transfer_request_response_autorenew_grace_at_request_only.xml similarity index 92% rename from javatests/google/registry/flows/domain/testdata/domain_transfer_request_response_2_years.xml rename to javatests/google/registry/flows/domain/testdata/domain_transfer_request_response_autorenew_grace_at_request_only.xml index bfc917b8b..e430886f7 100644 --- a/javatests/google/registry/flows/domain/testdata/domain_transfer_request_response_2_years.xml +++ b/javatests/google/registry/flows/domain/testdata/domain_transfer_request_response_autorenew_grace_at_request_only.xml @@ -12,7 +12,7 @@ 2000-06-09T22:00:00.0Z TheRegistrar 2000-06-14T22:00:00.0Z - 2002-06-10T22:00:00.0Z + 2002-04-26T22:00:00.0Z diff --git a/javatests/google/registry/flows/domain/testdata/domain_transfer_request_response_autorenew_grace_at_transfer_only.xml b/javatests/google/registry/flows/domain/testdata/domain_transfer_request_response_autorenew_grace_at_transfer_only.xml new file mode 100644 index 000000000..fa357264c --- /dev/null +++ b/javatests/google/registry/flows/domain/testdata/domain_transfer_request_response_autorenew_grace_at_transfer_only.xml @@ -0,0 +1,23 @@ + + + + Command completed successfully; action pending + + + + example.tld + pending + NewRegistrar + 2000-06-09T22:00:00.0Z + TheRegistrar + 2000-06-14T22:00:00.0Z + 2001-06-10T22:00:00.0Z + + + + ABC-12345 + server-trid + + + diff --git a/javatests/google/registry/flows/domain/testdata/domain_transfer_request_response_autorenew_grace_throughout_transfer_window.xml b/javatests/google/registry/flows/domain/testdata/domain_transfer_request_response_autorenew_grace_throughout_transfer_window.xml new file mode 100644 index 000000000..4e0689ae8 --- /dev/null +++ b/javatests/google/registry/flows/domain/testdata/domain_transfer_request_response_autorenew_grace_throughout_transfer_window.xml @@ -0,0 +1,24 @@ + + + + Command completed successfully; action pending + + + + example.tld + pending + NewRegistrar + 2000-06-09T22:00:00.0Z + TheRegistrar + 2000-06-14T22:00:00.0Z + + 2002-06-08T22:00:00.0Z + + + + ABC-12345 + server-trid + + +