diff --git a/core/src/main/java/google/registry/flows/domain/DomainRenewFlow.java b/core/src/main/java/google/registry/flows/domain/DomainRenewFlow.java index 88b8a313c..ade96a4d4 100644 --- a/core/src/main/java/google/registry/flows/domain/DomainRenewFlow.java +++ b/core/src/main/java/google/registry/flows/domain/DomainRenewFlow.java @@ -30,6 +30,7 @@ import static google.registry.flows.domain.DomainFlowUtils.validateFeeChallenge; import static google.registry.flows.domain.DomainFlowUtils.validateRegistrationPeriod; import static google.registry.flows.domain.DomainFlowUtils.verifyRegistrarIsActive; import static google.registry.flows.domain.DomainFlowUtils.verifyUnitIsYears; +import static google.registry.flows.domain.token.AllocationTokenFlowUtils.maybeApplyPackageRemovalToken; import static google.registry.flows.domain.token.AllocationTokenFlowUtils.verifyTokenAllowedOnDomain; import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_RENEW; import static google.registry.persistence.transaction.TransactionManagerFactory.tm; @@ -180,6 +181,10 @@ public final class DomainRenewFlow implements TransactionalFlow { now, eppInput.getSingleExtension(AllocationTokenExtension.class)); verifyRenewAllowed(authInfo, existingDomain, command, allocationToken); + + // If client passed an applicable static token this updates the domain + existingDomain = maybeApplyPackageRemovalToken(existingDomain, allocationToken); + int years = command.getPeriod().getValue(); DateTime newExpirationTime = leapSafeAddYears(existingDomain.getRegistrationExpirationTime(), years); // Uncapped @@ -315,14 +320,14 @@ public final class DomainRenewFlow implements TransactionalFlow { throws EppException { verifyOptionalAuthInfo(authInfo, existingDomain); verifyNoDisallowedStatuses(existingDomain, RENEW_DISALLOWED_STATUSES); - // We only allow __REMOVE_PACKAGE__ token on promo package domains for now - verifyTokenAllowedOnDomain(existingDomain, allocationToken); if (!isSuperuser) { verifyResourceOwnership(registrarId, existingDomain); checkAllowedAccessToTld(registrarId, existingDomain.getTld()); checkHasBillingAccount(registrarId, existingDomain.getTld()); } verifyUnitIsYears(command.getPeriod()); + // We only allow __REMOVE_PACKAGE__ token on promo package domains for now + verifyTokenAllowedOnDomain(existingDomain, allocationToken); // If the date they specify doesn't match the expiration, fail. (This is an idempotence check). if (!command.getCurrentExpirationDate().equals( existingDomain.getRegistrationExpirationTime().toLocalDate())) { @@ -344,7 +349,11 @@ public final class DomainRenewFlow implements TransactionalFlow { .setPeriodYears(years) .setCost(renewCost) .setEventTime(now) - .setAllocationToken(allocationToken.map(AllocationToken::createVKey).orElse(null)) + .setAllocationToken( + allocationToken + .filter(t -> AllocationToken.TokenBehavior.DEFAULT.equals(t.getTokenBehavior())) + .map(AllocationToken::createVKey) + .orElse(null)) .setBillingTime(now.plus(Registry.get(tld).getRenewGracePeriodLength())) .setDomainHistoryId( new DomainHistoryId(domainHistoryKey.getParent().getName(), domainHistoryKey.getId())) diff --git a/core/src/main/java/google/registry/flows/domain/token/AllocationTokenFlowUtils.java b/core/src/main/java/google/registry/flows/domain/token/AllocationTokenFlowUtils.java index 02690f570..54a74368d 100644 --- a/core/src/main/java/google/registry/flows/domain/token/AllocationTokenFlowUtils.java +++ b/core/src/main/java/google/registry/flows/domain/token/AllocationTokenFlowUtils.java @@ -15,6 +15,7 @@ package google.registry.flows.domain.token; import static com.google.common.base.Preconditions.checkArgument; +import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm; import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import com.google.common.base.Strings; @@ -26,6 +27,8 @@ import google.registry.flows.EppException; import google.registry.flows.EppException.AssociationProhibitsOperationException; import google.registry.flows.EppException.AuthorizationErrorException; import google.registry.flows.EppException.StatusProhibitsOperationException; +import google.registry.model.billing.BillingEvent.Recurring; +import google.registry.model.billing.BillingEvent.RenewalPriceBehavior; import google.registry.model.domain.Domain; import google.registry.model.domain.DomainCommand; import google.registry.model.domain.token.AllocationToken; @@ -214,6 +217,34 @@ public class AllocationTokenFlowUtils { } } + public static Domain maybeApplyPackageRemovalToken( + Domain domain, Optional allocationToken) { + if (!allocationToken.isPresent() + || !TokenBehavior.REMOVE_PACKAGE.equals(allocationToken.get().getTokenBehavior())) { + return domain; + } + + Recurring newRecurringBillingEvent = + tm().loadByKey(domain.getAutorenewBillingEvent()) + .asBuilder() + .setRenewalPriceBehavior(RenewalPriceBehavior.DEFAULT) + .setRenewalPrice(null) + .build(); + + // the Recurring billing event is reloaded later in the renew flow, so we synchronize changed + // RecurringBillingEvent with storage manually + tm().put(newRecurringBillingEvent); + jpaTm().getEntityManager().flush(); + jpaTm().getEntityManager().clear(); + + // Remove current package token + return domain + .asBuilder() + .setCurrentPackageToken(null) + .setAutorenewBillingEvent(newRecurringBillingEvent.createVKey()) + .build(); + } + // Note: exception messages should be <= 32 characters long for domain check results /** The allocation token is not currently valid. */ diff --git a/core/src/main/java/google/registry/model/domain/token/AllocationToken.java b/core/src/main/java/google/registry/model/domain/token/AllocationToken.java index eb80e87d8..9f0bcd94e 100644 --- a/core/src/main/java/google/registry/model/domain/token/AllocationToken.java +++ b/core/src/main/java/google/registry/model/domain/token/AllocationToken.java @@ -292,6 +292,10 @@ public class AllocationToken extends BackupGroupRoot implements Buildable { @Override public VKey createVKey() { + if (!AllocationToken.TokenBehavior.DEFAULT.equals(getTokenBehavior())) { + throw new IllegalArgumentException( + String.format("%s tokens are not stored in the database", getTokenBehavior())); + } return VKey.create(AllocationToken.class, getToken(), Key.create(this)); } diff --git a/core/src/test/java/google/registry/flows/domain/DomainRenewFlowTest.java b/core/src/test/java/google/registry/flows/domain/DomainRenewFlowTest.java index 0c52a0f7f..b842d101f 100644 --- a/core/src/test/java/google/registry/flows/domain/DomainRenewFlowTest.java +++ b/core/src/test/java/google/registry/flows/domain/DomainRenewFlowTest.java @@ -49,6 +49,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedMap; +import com.google.common.truth.Truth8; import com.googlecode.objectify.Key; import google.registry.flows.EppException; import google.registry.flows.EppRequestSource; @@ -1261,4 +1262,36 @@ class DomainRenewFlowTest extends ResourceFlowTestCase assertThrows(RemovePackageTokenOnNonPackageDomainException.class, this::runFlow); assertAboutEppExceptions().that(thrown).marshalsToXml(); } + + @Test + void testSuccesfullyAppliesRemovePackageToken() throws Exception { + AllocationToken token = + persistResource( + new AllocationToken.Builder() + .setToken("abc123") + .setTokenType(PACKAGE) + .setAllowedRegistrarIds(ImmutableSet.of("TheRegistrar")) + .setAllowedTlds(ImmutableSet.of("tld")) + .setRenewalPriceBehavior(SPECIFIED) + .build()); + persistDomain(SPECIFIED, Money.of(USD, 2)); + persistResource( + reloadResourceByForeignKey() + .asBuilder() + .setCurrentPackageToken(token.createVKey()) + .build()); + setEppInput( + "domain_renew_allocationtoken.xml", + ImmutableMap.of("DOMAIN", "example.tld", "YEARS", "2", "TOKEN", "__REMOVEPACKAGE__")); + + doSuccessfulTest( + "domain_renew_response.xml", + 2, + ImmutableMap.of("DOMAIN", "example.tld", "EXDATE", "2002-04-03T22:00:00Z")); + + // We still need to verify that package token is removed as it's not being tested as a part of + // doSuccessfulTest + Domain domain = reloadResourceByForeignKey(); + Truth8.assertThat(domain.getCurrentPackageToken()).isEmpty(); + } }