Make domain transfers use (and retain) the renewal price/behavior (#1701)

* Use the new renewal price logic in transfer flow

* Fix build

* Add renewal handling on all transfer flows

* Merge branch 'master' into transfer-retain-renewal-price

* Merge branch 'master' into transfer-retain-renewal-price

* Add more tests
This commit is contained in:
Ben McIlwain 2022-08-05 15:53:27 -04:00 committed by GitHub
parent 6370baf9de
commit 53426a34d1
15 changed files with 409 additions and 30 deletions

View file

@ -64,6 +64,7 @@ import google.registry.flows.custom.DomainDeleteFlowCustomLogic.BeforeSaveParame
import google.registry.flows.custom.EntityChanges; import google.registry.flows.custom.EntityChanges;
import google.registry.model.ImmutableObject; import google.registry.model.ImmutableObject;
import google.registry.model.billing.BillingEvent; import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Recurring;
import google.registry.model.domain.Domain; import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory; import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.DomainHistory.DomainHistoryId; import google.registry.model.domain.DomainHistory.DomainHistoryId;
@ -260,8 +261,9 @@ public final class DomainDeleteFlow implements TransactionalFlow {
handlePendingTransferOnDelete(existingDomain, newDomain, now, domainHistory); handlePendingTransferOnDelete(existingDomain, newDomain, now, domainHistory);
// Close the autorenew billing event and poll message. This may delete the poll message. Store // Close the autorenew billing event and poll message. This may delete the poll message. Store
// the updated recurring billing event, we'll need it later and can't reload it. // the updated recurring billing event, we'll need it later and can't reload it.
Recurring existingRecurring = tm().loadByKey(existingDomain.getAutorenewBillingEvent());
BillingEvent.Recurring recurringBillingEvent = BillingEvent.Recurring recurringBillingEvent =
updateAutorenewRecurrenceEndTime(existingDomain, now); updateAutorenewRecurrenceEndTime(existingDomain, existingRecurring, now);
// If there's a pending transfer, the gaining client's autorenew billing // If there's a pending transfer, the gaining client's autorenew billing
// event and poll message will already have been deleted in // event and poll message will already have been deleted in
// ResourceDeleteFlow since it's listed in serverApproveEntities. // ResourceDeleteFlow since it's listed in serverApproveEntities.

View file

@ -583,7 +583,8 @@ public class DomainFlowUtils {
* *
* <p>Returns the new autorenew recurring billing event. * <p>Returns the new autorenew recurring billing event.
*/ */
public static Recurring updateAutorenewRecurrenceEndTime(Domain domain, DateTime newEndTime) { public static Recurring updateAutorenewRecurrenceEndTime(
Domain domain, Recurring existingRecurring, DateTime newEndTime) {
Optional<PollMessage.Autorenew> autorenewPollMessage = Optional<PollMessage.Autorenew> autorenewPollMessage =
tm().loadByKeyIfPresent(domain.getAutorenewPollMessage()); tm().loadByKeyIfPresent(domain.getAutorenewPollMessage());
@ -611,13 +612,9 @@ public class DomainFlowUtils {
tm().put(updatedAutorenewPollMessage); tm().put(updatedAutorenewPollMessage);
} }
Recurring recurring = Recurring newRecurring = existingRecurring.asBuilder().setRecurrenceEndTime(newEndTime).build();
tm().loadByKey(domain.getAutorenewBillingEvent()) tm().put(newRecurring);
.asBuilder() return newRecurring;
.setRecurrenceEndTime(newEndTime)
.build();
tm().put(recurring);
return recurring;
} }
/** /**
@ -708,7 +705,10 @@ public class DomainFlowUtils {
throw new TransfersAreAlwaysForOneYearException(); throw new TransfersAreAlwaysForOneYearException();
} }
builder.setAvailIfSupported(true); builder.setAvailIfSupported(true);
fees = pricingLogic.getTransferPrice(registry, domainNameString, now).getFees(); fees =
pricingLogic
.getTransferPrice(registry, domainNameString, now, recurringBillingEvent)
.getFees();
break; break;
case UPDATE: case UPDATE:
builder.setAvailIfSupported(true); builder.setAvailIfSupported(true);
@ -767,7 +767,7 @@ public class DomainFlowUtils {
final Optional<? extends FeeTransformCommandExtension> feeCommand, final Optional<? extends FeeTransformCommandExtension> feeCommand,
FeesAndCredits feesAndCredits) FeesAndCredits feesAndCredits)
throws EppException { throws EppException {
if (isDomainPremium(domainName, priceTime) && !feeCommand.isPresent()) { if (feesAndCredits.hasAnyPremiumFees() && !feeCommand.isPresent()) {
throw new FeesRequiredForPremiumNameException(); throw new FeesRequiredForPremiumNameException();
} }
validateFeesAckedIfPresent(feeCommand, feesAndCredits); validateFeesAckedIfPresent(feeCommand, feesAndCredits);

View file

@ -195,9 +195,14 @@ public final class DomainPricingLogic {
} }
/** Returns a new transfer price for the pricer. */ /** Returns a new transfer price for the pricer. */
FeesAndCredits getTransferPrice(Registry registry, String domainName, DateTime dateTime) FeesAndCredits getTransferPrice(
Registry registry,
String domainName,
DateTime dateTime,
@Nullable Recurring recurringBillingEvent)
throws EppException { throws EppException {
DomainPrices domainPrices = getPricesForDomainName(domainName, dateTime); FeesAndCredits renewPrice =
getRenewPrice(registry, domainName, dateTime, 1, recurringBillingEvent);
return customLogic.customizeTransferPrice( return customLogic.customizeTransferPrice(
TransferPriceParameters.newBuilder() TransferPriceParameters.newBuilder()
.setFeesAndCredits( .setFeesAndCredits(
@ -205,9 +210,9 @@ public final class DomainPricingLogic {
.setCurrency(registry.getCurrency()) .setCurrency(registry.getCurrency())
.addFeeOrCredit( .addFeeOrCredit(
Fee.create( Fee.create(
domainPrices.getRenewCost().getAmount(), renewPrice.getRenewCost().getAmount(),
FeeType.RENEW, FeeType.RENEW,
domainPrices.isPremium())) renewPrice.hasAnyPremiumFees()))
.build()) .build())
.setRegistry(registry) .setRegistry(registry)
.setDomainName(InternetDomainName.from(domainName)) .setDomainName(InternetDomainName.from(domainName))

View file

@ -222,7 +222,8 @@ public final class DomainRenewFlow implements TransactionalFlow {
domainHistoryKey.getParent().getName(), domainHistoryKey.getId())) domainHistoryKey.getParent().getName(), domainHistoryKey.getId()))
.build(); .build();
// End the old autorenew billing event and poll message now. This may delete the poll message. // End the old autorenew billing event and poll message now. This may delete the poll message.
updateAutorenewRecurrenceEndTime(existingDomain, now); Recurring existingRecurring = tm().loadByKey(existingDomain.getAutorenewBillingEvent());
updateAutorenewRecurrenceEndTime(existingDomain, existingRecurring, now);
Domain newDomain = Domain newDomain =
existingDomain existingDomain
.asBuilder() .asBuilder()

View file

@ -31,7 +31,6 @@ import static google.registry.model.ResourceTransferUtils.approvePendingTransfer
import static google.registry.model.reporting.DomainTransactionRecord.TransactionReportField.TRANSFER_SUCCESSFUL; import static google.registry.model.reporting.DomainTransactionRecord.TransactionReportField.TRANSFER_SUCCESSFUL;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_TRANSFER_APPROVE; import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_TRANSFER_APPROVE;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.pricing.PricingEngineProxy.getDomainRenewCost;
import static google.registry.util.CollectionUtils.union; import static google.registry.util.CollectionUtils.union;
import static google.registry.util.DateTimeUtils.END_OF_TIME; import static google.registry.util.DateTimeUtils.END_OF_TIME;
@ -49,6 +48,7 @@ import google.registry.model.ImmutableObject;
import google.registry.model.billing.BillingEvent; import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Flag; import google.registry.model.billing.BillingEvent.Flag;
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.domain.Domain; import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory; import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.DomainHistory.DomainHistoryId; import google.registry.model.domain.DomainHistory.DomainHistoryId;
@ -96,6 +96,8 @@ public final class DomainTransferApproveFlow implements TransactionalFlow {
@Inject @Superuser boolean isSuperuser; @Inject @Superuser boolean isSuperuser;
@Inject DomainHistory.Builder historyBuilder; @Inject DomainHistory.Builder historyBuilder;
@Inject EppResponse.Builder responseBuilder; @Inject EppResponse.Builder responseBuilder;
@Inject DomainPricingLogic pricingLogic;
@Inject DomainTransferApproveFlow() {} @Inject DomainTransferApproveFlow() {}
/** /**
@ -120,6 +122,7 @@ public final class DomainTransferApproveFlow implements TransactionalFlow {
String gainingRegistrarId = transferData.getGainingRegistrarId(); String gainingRegistrarId = transferData.getGainingRegistrarId();
// Create a transfer billing event for 1 year, unless the superuser extension was used to set // Create a transfer billing event for 1 year, unless the superuser extension was used to set
// the transfer period to zero. There is not a transfer cost if the transfer period is zero. // the transfer period to zero. There is not a transfer cost if the transfer period is zero.
Recurring existingRecurring = tm().loadByKey(existingDomain.getAutorenewBillingEvent());
Key<DomainHistory> domainHistoryKey = createHistoryKey(existingDomain, DomainHistory.class); Key<DomainHistory> domainHistoryKey = createHistoryKey(existingDomain, DomainHistory.class);
historyBuilder.setId(domainHistoryKey.getId()); historyBuilder.setId(domainHistoryKey.getId());
Optional<BillingEvent.OneTime> billingEvent = Optional<BillingEvent.OneTime> billingEvent =
@ -131,7 +134,14 @@ public final class DomainTransferApproveFlow implements TransactionalFlow {
.setTargetId(targetId) .setTargetId(targetId)
.setRegistrarId(gainingRegistrarId) .setRegistrarId(gainingRegistrarId)
.setPeriodYears(1) .setPeriodYears(1)
.setCost(getDomainRenewCost(targetId, transferData.getTransferRequestTime(), 1)) .setCost(
pricingLogic
.getTransferPrice(
Registry.get(tld),
targetId,
transferData.getTransferRequestTime(),
existingRecurring)
.getRenewCost())
.setEventTime(now) .setEventTime(now)
.setBillingTime(now.plus(Registry.get(tld).getTransferGracePeriodLength())) .setBillingTime(now.plus(Registry.get(tld).getTransferGracePeriodLength()))
.setDomainHistoryId( .setDomainHistoryId(
@ -161,7 +171,7 @@ public final class DomainTransferApproveFlow implements TransactionalFlow {
} }
// 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, existingRecurring, now);
DateTime newExpirationTime = DateTime newExpirationTime =
computeExDateForApprovalTime(existingDomain, now, transferData.getTransferPeriod()); computeExDateForApprovalTime(existingDomain, now, transferData.getTransferPeriod());
// Create a new autorenew event starting at the expiration time. // Create a new autorenew event starting at the expiration time.
@ -172,6 +182,8 @@ public final class DomainTransferApproveFlow implements TransactionalFlow {
.setTargetId(targetId) .setTargetId(targetId)
.setRegistrarId(gainingRegistrarId) .setRegistrarId(gainingRegistrarId)
.setEventTime(newExpirationTime) .setEventTime(newExpirationTime)
.setRenewalPriceBehavior(existingRecurring.getRenewalPriceBehavior())
.setRenewalPrice(existingRecurring.getRenewalPrice().orElse(null))
.setRecurrenceEndTime(END_OF_TIME) .setRecurrenceEndTime(END_OF_TIME)
.setDomainHistoryId( .setDomainHistoryId(
new DomainHistoryId( new DomainHistoryId(

View file

@ -40,6 +40,7 @@ import google.registry.flows.FlowModule.Superuser;
import google.registry.flows.FlowModule.TargetId; import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.TransactionalFlow; import google.registry.flows.TransactionalFlow;
import google.registry.flows.annotations.ReportingSpec; import google.registry.flows.annotations.ReportingSpec;
import google.registry.model.billing.BillingEvent.Recurring;
import google.registry.model.domain.Domain; import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory; import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.metadata.MetadataExtension; import google.registry.model.domain.metadata.MetadataExtension;
@ -114,7 +115,8 @@ public final class DomainTransferCancelFlow implements TransactionalFlow {
targetId, newDomain.getTransferData(), null, domainHistoryKey)); targetId, newDomain.getTransferData(), null, domainHistoryKey));
// Reopen the autorenew event and poll message that we closed for the implicit transfer. This // Reopen the autorenew event and poll message that we closed for the implicit transfer. This
// may recreate the autorenew poll message if it was deleted when the transfer request was made. // may recreate the autorenew poll message if it was deleted when the transfer request was made.
updateAutorenewRecurrenceEndTime(existingDomain, END_OF_TIME); Recurring existingRecurring = tm().loadByKey(existingDomain.getAutorenewBillingEvent());
updateAutorenewRecurrenceEndTime(existingDomain, existingRecurring, END_OF_TIME);
// Delete the billing event and poll messages that were written in case the transfer would have // Delete the billing event and poll messages that were written in case the transfer would have
// been implicitly server approved. // been implicitly server approved.
tm().delete(existingDomain.getTransferData().getServerApproveEntities()); tm().delete(existingDomain.getTransferData().getServerApproveEntities());

View file

@ -42,6 +42,7 @@ import google.registry.flows.FlowModule.Superuser;
import google.registry.flows.FlowModule.TargetId; import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.TransactionalFlow; import google.registry.flows.TransactionalFlow;
import google.registry.flows.annotations.ReportingSpec; import google.registry.flows.annotations.ReportingSpec;
import google.registry.model.billing.BillingEvent.Recurring;
import google.registry.model.domain.Domain; import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory; import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.metadata.MetadataExtension; import google.registry.model.domain.metadata.MetadataExtension;
@ -115,7 +116,8 @@ public final class DomainTransferRejectFlow implements TransactionalFlow {
targetId, newDomain.getTransferData(), null, now, domainHistoryKey)); targetId, newDomain.getTransferData(), null, now, domainHistoryKey));
// Reopen the autorenew event and poll message that we closed for the implicit transfer. This // Reopen the autorenew event and poll message that we closed for the implicit transfer. This
// may end up recreating the poll message if it was deleted upon the transfer request. // may end up recreating the poll message if it was deleted upon the transfer request.
updateAutorenewRecurrenceEndTime(existingDomain, END_OF_TIME); Recurring existingRecurring = tm().loadByKey(existingDomain.getAutorenewBillingEvent());
updateAutorenewRecurrenceEndTime(existingDomain, existingRecurring, END_OF_TIME);
// Delete the billing event and poll messages that were written in case the transfer would have // Delete the billing event and poll messages that were written in case the transfer would have
// been implicitly server approved. // been implicitly server approved.
tm().delete(existingDomain.getTransferData().getServerApproveEntities()); tm().delete(existingDomain.getTransferData().getServerApproveEntities());

View file

@ -52,6 +52,7 @@ import google.registry.flows.exceptions.InvalidTransferPeriodValueException;
import google.registry.flows.exceptions.ObjectAlreadySponsoredException; import google.registry.flows.exceptions.ObjectAlreadySponsoredException;
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.Recurring;
import google.registry.model.domain.Domain; import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainCommand.Transfer; import google.registry.model.domain.DomainCommand.Transfer;
import google.registry.model.domain.DomainHistory; import google.registry.model.domain.DomainHistory;
@ -168,10 +169,12 @@ public final class DomainTransferRequestFlow implements TransactionalFlow {
throw new TransferPeriodZeroAndFeeTransferExtensionException(); throw new TransferPeriodZeroAndFeeTransferExtensionException();
} }
// If the period is zero, then there is no fee for the transfer. // If the period is zero, then there is no fee for the transfer.
Recurring existingRecurring = tm().loadByKey(existingDomain.getAutorenewBillingEvent());
Optional<FeesAndCredits> feesAndCredits = Optional<FeesAndCredits> feesAndCredits =
(period.getValue() == 0) (period.getValue() == 0)
? Optional.empty() ? Optional.empty()
: Optional.of(pricingLogic.getTransferPrice(registry, targetId, now)); : Optional.of(
pricingLogic.getTransferPrice(registry, targetId, now, existingRecurring));
if (feesAndCredits.isPresent()) { if (feesAndCredits.isPresent()) {
validateFeeChallenge(targetId, now, feeTransfer, feesAndCredits.get()); validateFeeChallenge(targetId, now, feeTransfer, feesAndCredits.get());
} }
@ -201,6 +204,7 @@ public final class DomainTransferRequestFlow implements TransactionalFlow {
serverApproveNewExpirationTime, serverApproveNewExpirationTime,
domainHistoryKey, domainHistoryKey,
existingDomain, existingDomain,
existingRecurring,
trid, trid,
gainingClientId, gainingClientId,
feesAndCredits.map(FeesAndCredits::getTotalCost), feesAndCredits.map(FeesAndCredits::getTotalCost),
@ -230,7 +234,7 @@ public final class DomainTransferRequestFlow implements TransactionalFlow {
// the poll message if it has no events left. Note that if the automatic transfer succeeds, then // the poll message if it has no events left. Note that if the automatic transfer succeeds, then
// cloneProjectedAtTime() will replace these old autorenew entities with the server approve ones // cloneProjectedAtTime() will replace these old autorenew entities with the server approve ones
// that we've created in this flow and stored in pendingTransferData. // that we've created in this flow and stored in pendingTransferData.
updateAutorenewRecurrenceEndTime(existingDomain, automaticTransferTime); updateAutorenewRecurrenceEndTime(existingDomain, existingRecurring, automaticTransferTime);
Domain newDomain = Domain newDomain =
existingDomain existingDomain
.asBuilder() .asBuilder()

View file

@ -24,6 +24,7 @@ import com.googlecode.objectify.Key;
import google.registry.model.billing.BillingEvent; import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Flag; import google.registry.model.billing.BillingEvent.Flag;
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.domain.Domain; import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory; import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.DomainHistory.DomainHistoryId; import google.registry.model.domain.DomainHistory.DomainHistoryId;
@ -110,6 +111,7 @@ public final class DomainTransferUtils {
DateTime serverApproveNewExpirationTime, DateTime serverApproveNewExpirationTime,
Key<DomainHistory> domainHistoryKey, Key<DomainHistory> domainHistoryKey,
Domain existingDomain, Domain existingDomain,
Recurring existingRecurring,
Trid trid, Trid trid,
String gainingRegistrarId, String gainingRegistrarId,
Optional<Money> transferCost, Optional<Money> transferCost,
@ -144,7 +146,11 @@ public final class DomainTransferUtils {
return builder return builder
.add( .add(
createGainingClientAutorenewEvent( createGainingClientAutorenewEvent(
serverApproveNewExpirationTime, domainHistoryKey, targetId, gainingRegistrarId)) existingRecurring,
serverApproveNewExpirationTime,
domainHistoryKey,
targetId,
gainingRegistrarId))
.add( .add(
createGainingClientAutorenewPollMessage( createGainingClientAutorenewPollMessage(
serverApproveNewExpirationTime, domainHistoryKey, targetId, gainingRegistrarId)) serverApproveNewExpirationTime, domainHistoryKey, targetId, gainingRegistrarId))
@ -239,6 +245,7 @@ public final class DomainTransferUtils {
} }
private static BillingEvent.Recurring createGainingClientAutorenewEvent( private static BillingEvent.Recurring createGainingClientAutorenewEvent(
Recurring existingRecurring,
DateTime serverApproveNewExpirationTime, DateTime serverApproveNewExpirationTime,
Key<DomainHistory> domainHistoryKey, Key<DomainHistory> domainHistoryKey,
String targetId, String targetId,
@ -250,6 +257,8 @@ public final class DomainTransferUtils {
.setRegistrarId(gainingRegistrarId) .setRegistrarId(gainingRegistrarId)
.setEventTime(serverApproveNewExpirationTime) .setEventTime(serverApproveNewExpirationTime)
.setRecurrenceEndTime(END_OF_TIME) .setRecurrenceEndTime(END_OF_TIME)
.setRenewalPriceBehavior(existingRecurring.getRenewalPriceBehavior())
.setRenewalPrice(existingRecurring.getRenewalPrice().orElse(null))
.setDomainHistoryId( .setDomainHistoryId(
new DomainHistoryId(domainHistoryKey.getParent().getName(), domainHistoryKey.getId())) new DomainHistoryId(domainHistoryKey.getParent().getName(), domainHistoryKey.getId()))
.build(); .build();

View file

@ -32,6 +32,7 @@ import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import google.registry.model.billing.BillingEvent; import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Recurring;
import google.registry.model.domain.Domain; import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory; import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.Period; import google.registry.model.domain.Period;
@ -215,7 +216,8 @@ class UnrenewDomainCommand extends ConfirmingCommand implements CommandWithRemot
.setHistoryEntry(domainHistory) .setHistoryEntry(domainHistory)
.build(); .build();
// End the old autorenew billing event and poll message now. // End the old autorenew billing event and poll message now.
updateAutorenewRecurrenceEndTime(domain, now); Recurring existingRecurring = tm().loadByKey(domain.getAutorenewBillingEvent());
updateAutorenewRecurrenceEndTime(domain, existingRecurring, now);
Domain newDomain = Domain newDomain =
domain domain
.asBuilder() .asBuilder()

View file

@ -399,4 +399,128 @@ public class DomainPricingLogicTest {
registry, "standard.example", clock.nowUtc(), -1, null)); registry, "standard.example", clock.nowUtc(), -1, null));
assertThat(thrown).hasMessageThat().isEqualTo("Number of years must be positive"); assertThat(thrown).hasMessageThat().isEqualTo("Number of years must be positive");
} }
@Test
void testGetDomainTransferPrice_standardDomain_default_noBilling_defaultRenewalPrice()
throws EppException {
assertThat(
domainPricingLogic.getTransferPrice(registry, "standard.example", clock.nowUtc(), null))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 10).getAmount(), RENEW, false))
.build());
}
@Test
void testGetDomainTransferPrice_premiumDomain_default_noBilling_premiumRenewalPrice()
throws EppException {
assertThat(
domainPricingLogic.getTransferPrice(registry, "premium.example", clock.nowUtc(), null))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 100).getAmount(), RENEW, true))
.build());
}
@Test
void testGetDomainTransferPrice_standardDomain_default_defaultRenewalPrice() throws EppException {
assertThat(
domainPricingLogic.getTransferPrice(
registry,
"standard.example",
clock.nowUtc(),
persistDomainAndSetRecurringBillingEvent(
"standard.example", DEFAULT, Optional.empty())))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 10).getAmount(), RENEW, false))
.build());
}
@Test
void testGetDomainTransferPrice_premiumDomain_default_premiumRenewalPrice() throws EppException {
assertThat(
domainPricingLogic.getTransferPrice(
registry,
"premium.example",
clock.nowUtc(),
persistDomainAndSetRecurringBillingEvent(
"premium.example", DEFAULT, Optional.empty())))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 100).getAmount(), RENEW, true))
.build());
}
@Test
void testGetDomainTransferPrice_standardDomain_nonPremium_nonPremiumRenewalPrice()
throws EppException {
assertThat(
domainPricingLogic.getTransferPrice(
registry,
"standard.example",
clock.nowUtc(),
persistDomainAndSetRecurringBillingEvent(
"standard.example", NONPREMIUM, Optional.empty())))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 10).getAmount(), RENEW, false))
.build());
}
@Test
void testGetDomainTransferPrice_premiumDomain_nonPremium_nonPremiumRenewalPrice()
throws EppException {
assertThat(
domainPricingLogic.getTransferPrice(
registry,
"premium.example",
clock.nowUtc(),
persistDomainAndSetRecurringBillingEvent(
"premium.example", NONPREMIUM, Optional.empty())))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 10).getAmount(), RENEW, false))
.build());
}
@Test
void testGetDomainTransferPrice_standardDomain_specified_specifiedRenewalPrice()
throws EppException {
assertThat(
domainPricingLogic.getTransferPrice(
registry,
"standard.example",
clock.nowUtc(),
persistDomainAndSetRecurringBillingEvent(
"standard.example", SPECIFIED, Optional.of(Money.of(USD, 1.23)))))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 1.23).getAmount(), RENEW, false))
.build());
}
@Test
void testGetDomainTransferPrice_premiumDomain_specified_specifiedRenewalPrice()
throws EppException {
assertThat(
domainPricingLogic.getTransferPrice(
registry,
"premium.example",
clock.nowUtc(),
persistDomainAndSetRecurringBillingEvent(
"premium.example", SPECIFIED, Optional.of(Money.of(USD, 1.23)))))
.isEqualTo(
new FeesAndCredits.Builder()
.setCurrency(USD)
.addFeeOrCredit(Fee.create(Money.of(USD, 1.23).getAmount(), RENEW, false))
.build());
}
} }

View file

@ -27,6 +27,7 @@ import static google.registry.testing.DatabaseHelper.deleteTestDomain;
import static google.registry.testing.DatabaseHelper.getOnlyHistoryEntryOfType; import static google.registry.testing.DatabaseHelper.getOnlyHistoryEntryOfType;
import static google.registry.testing.DatabaseHelper.getOnlyPollMessage; import static google.registry.testing.DatabaseHelper.getOnlyPollMessage;
import static google.registry.testing.DatabaseHelper.getPollMessages; import static google.registry.testing.DatabaseHelper.getPollMessages;
import static google.registry.testing.DatabaseHelper.loadByEntity;
import static google.registry.testing.DatabaseHelper.loadByKey; import static google.registry.testing.DatabaseHelper.loadByKey;
import static google.registry.testing.DatabaseHelper.loadRegistrar; import static google.registry.testing.DatabaseHelper.loadRegistrar;
import static google.registry.testing.DatabaseHelper.persistResource; import static google.registry.testing.DatabaseHelper.persistResource;
@ -53,6 +54,7 @@ import google.registry.model.billing.BillingEvent;
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.billing.BillingEvent.Recurring;
import google.registry.model.billing.BillingEvent.RenewalPriceBehavior;
import google.registry.model.contact.ContactAuthInfo; import google.registry.model.contact.ContactAuthInfo;
import google.registry.model.domain.Domain; import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainAuthInfo; import google.registry.model.domain.DomainAuthInfo;
@ -69,10 +71,13 @@ import google.registry.model.poll.PollMessage;
import google.registry.model.reporting.DomainTransactionRecord; import google.registry.model.reporting.DomainTransactionRecord;
import google.registry.model.reporting.HistoryEntry; import google.registry.model.reporting.HistoryEntry;
import google.registry.model.tld.Registry; import google.registry.model.tld.Registry;
import google.registry.model.tld.label.PremiumList;
import google.registry.model.tld.label.PremiumListDao;
import google.registry.model.transfer.DomainTransferData; import google.registry.model.transfer.DomainTransferData;
import google.registry.model.transfer.TransferResponse.DomainTransferResponse; import google.registry.model.transfer.TransferResponse.DomainTransferResponse;
import google.registry.model.transfer.TransferStatus; import google.registry.model.transfer.TransferStatus;
import google.registry.persistence.VKey; import google.registry.persistence.VKey;
import java.math.BigDecimal;
import java.util.Arrays; import java.util.Arrays;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.joda.money.Money; import org.joda.money.Money;
@ -415,6 +420,103 @@ class DomainTransferApproveFlowTest
.setRecurringEventKey(domain.getAutorenewBillingEvent())); .setRecurringEventKey(domain.getAutorenewBillingEvent()));
} }
@Test
void testSuccess_nonpremiumPriceRenewalBehavior_carriesOver() throws Exception {
PremiumList pl =
PremiumListDao.save(
new PremiumList.Builder()
.setCurrency(USD)
.setName("tld")
.setLabelsToPrices(ImmutableMap.of("example", new BigDecimal("67.89")))
.build());
persistResource(Registry.get("tld").asBuilder().setPremiumList(pl).build());
setupDomainWithPendingTransfer("example", "tld");
domain = loadByEntity(domain);
persistResource(
loadByKey(domain.getAutorenewBillingEvent())
.asBuilder()
.setRenewalPriceBehavior(RenewalPriceBehavior.NONPREMIUM)
.build());
setEppInput("domain_transfer_approve_wildcard.xml", ImmutableMap.of("DOMAIN", "example.tld"));
DateTime now = clock.nowUtc();
runFlowAssertResponse(loadFile("domain_transfer_approve_response.xml"));
domain = reloadResourceByForeignKey();
DomainHistory acceptHistory =
getOnlyHistoryEntryOfType(domain, DOMAIN_TRANSFER_APPROVE, DomainHistory.class);
assertBillingEventsForResource(
domain,
new BillingEvent.OneTime.Builder()
.setBillingTime(now.plusDays(5))
.setEventTime(now)
.setRegistrarId("NewRegistrar")
.setCost(Money.of(USD, new BigDecimal("11.00")))
.setDomainHistory(acceptHistory)
.setReason(Reason.TRANSFER)
.setPeriodYears(1)
.setTargetId("example.tld")
.build(),
getGainingClientAutorenewEvent()
.asBuilder()
.setRenewalPriceBehavior(RenewalPriceBehavior.NONPREMIUM)
.setDomainHistory(acceptHistory)
.build(),
getLosingClientAutorenewEvent()
.asBuilder()
.setRecurrenceEndTime(now)
.setRenewalPriceBehavior(RenewalPriceBehavior.NONPREMIUM)
.build());
}
@Test
void testSuccess_specifiedPriceRenewalBehavior_carriesOver() throws Exception {
PremiumList pl =
PremiumListDao.save(
new PremiumList.Builder()
.setCurrency(USD)
.setName("tld")
.setLabelsToPrices(ImmutableMap.of("example", new BigDecimal("67.89")))
.build());
persistResource(Registry.get("tld").asBuilder().setPremiumList(pl).build());
setupDomainWithPendingTransfer("example", "tld");
domain = loadByEntity(domain);
persistResource(
loadByKey(domain.getAutorenewBillingEvent())
.asBuilder()
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(USD, new BigDecimal("43.10")))
.build());
setEppInput("domain_transfer_approve_wildcard.xml", ImmutableMap.of("DOMAIN", "example.tld"));
DateTime now = clock.nowUtc();
runFlowAssertResponse(loadFile("domain_transfer_approve_response.xml"));
domain = reloadResourceByForeignKey();
DomainHistory acceptHistory =
getOnlyHistoryEntryOfType(domain, DOMAIN_TRANSFER_APPROVE, DomainHistory.class);
assertBillingEventsForResource(
domain,
new BillingEvent.OneTime.Builder()
.setBillingTime(now.plusDays(5))
.setEventTime(now)
.setRegistrarId("NewRegistrar")
.setCost(Money.of(USD, new BigDecimal("43.10")))
.setDomainHistory(acceptHistory)
.setReason(Reason.TRANSFER)
.setPeriodYears(1)
.setTargetId("example.tld")
.build(),
getGainingClientAutorenewEvent()
.asBuilder()
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(USD, new BigDecimal("43.10")))
.setDomainHistory(acceptHistory)
.build(),
getLosingClientAutorenewEvent()
.asBuilder()
.setRecurrenceEndTime(now)
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(USD, new BigDecimal("43.10")))
.build());
}
@Test @Test
void testFailure_badContactPassword() { void testFailure_badContactPassword() {
// Change the contact's password so it does not match the password in the file. // Change the contact's password so it does not match the password in the file.

View file

@ -28,11 +28,13 @@ import static google.registry.model.tld.Registry.TldState.QUIET_PERIOD;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.DatabaseHelper.assertBillingEvents; import static google.registry.testing.DatabaseHelper.assertBillingEvents;
import static google.registry.testing.DatabaseHelper.assertBillingEventsEqual; import static google.registry.testing.DatabaseHelper.assertBillingEventsEqual;
import static google.registry.testing.DatabaseHelper.assertBillingEventsForResource;
import static google.registry.testing.DatabaseHelper.assertPollMessagesEqual; import static google.registry.testing.DatabaseHelper.assertPollMessagesEqual;
import static google.registry.testing.DatabaseHelper.createTld; import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.getOnlyHistoryEntryOfType; import static google.registry.testing.DatabaseHelper.getOnlyHistoryEntryOfType;
import static google.registry.testing.DatabaseHelper.getOnlyPollMessage; import static google.registry.testing.DatabaseHelper.getOnlyPollMessage;
import static google.registry.testing.DatabaseHelper.getPollMessages; import static google.registry.testing.DatabaseHelper.getPollMessages;
import static google.registry.testing.DatabaseHelper.loadByEntity;
import static google.registry.testing.DatabaseHelper.loadByKey; import static google.registry.testing.DatabaseHelper.loadByKey;
import static google.registry.testing.DatabaseHelper.loadByKeys; import static google.registry.testing.DatabaseHelper.loadByKeys;
import static google.registry.testing.DatabaseHelper.loadRegistrar; import static google.registry.testing.DatabaseHelper.loadRegistrar;
@ -83,6 +85,7 @@ 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;
import google.registry.model.billing.BillingEvent.Reason; import google.registry.model.billing.BillingEvent.Reason;
import google.registry.model.billing.BillingEvent.RenewalPriceBehavior;
import google.registry.model.contact.ContactAuthInfo; import google.registry.model.contact.ContactAuthInfo;
import google.registry.model.domain.Domain; import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainAuthInfo; import google.registry.model.domain.DomainAuthInfo;
@ -101,11 +104,14 @@ import google.registry.model.registrar.Registrar.State;
import google.registry.model.reporting.DomainTransactionRecord; import google.registry.model.reporting.DomainTransactionRecord;
import google.registry.model.reporting.HistoryEntry; import google.registry.model.reporting.HistoryEntry;
import google.registry.model.tld.Registry; import google.registry.model.tld.Registry;
import google.registry.model.tld.label.PremiumList;
import google.registry.model.tld.label.PremiumListDao;
import google.registry.model.transfer.DomainTransferData; import google.registry.model.transfer.DomainTransferData;
import google.registry.model.transfer.TransferResponse; import google.registry.model.transfer.TransferResponse;
import google.registry.model.transfer.TransferStatus; import google.registry.model.transfer.TransferStatus;
import google.registry.persistence.VKey; import google.registry.persistence.VKey;
import google.registry.testing.CloudTasksHelper.TaskMatcher; import google.registry.testing.CloudTasksHelper.TaskMatcher;
import java.math.BigDecimal;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -1202,6 +1208,117 @@ class DomainTransferRequestFlowTest
runTest("domain_transfer_request_fee.xml", UserPrivileges.SUPERUSER, RICH_DOMAIN_MAP); runTest("domain_transfer_request_fee.xml", UserPrivileges.SUPERUSER, RICH_DOMAIN_MAP);
} }
@Test
void testSuccess_nonPremiumRenewalPrice_isReflectedInTransferCostAndCarriesOver()
throws Exception {
setupDomain("example", "tld");
PremiumList pl =
PremiumListDao.save(
new PremiumList.Builder()
.setCurrency(USD)
.setName("tld")
.setLabelsToPrices(ImmutableMap.of("example", new BigDecimal("67.89")))
.build());
persistResource(Registry.get("tld").asBuilder().setPremiumList(pl).build());
domain = loadByEntity(domain);
persistResource(
loadByKey(domain.getAutorenewBillingEvent())
.asBuilder()
.setRenewalPriceBehavior(RenewalPriceBehavior.NONPREMIUM)
.build());
DateTime now = clock.nowUtc();
// This ensures that the transfer has non-premium cost, as otherwise, the fee extension would be
// required to ack the premium price.
setEppInput("domain_transfer_request.xml");
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
runFlowAssertResponse(loadFile("domain_transfer_request_response.xml"));
domain = loadByEntity(domain);
DomainHistory requestHistory =
getOnlyHistoryEntryOfType(domain, DOMAIN_TRANSFER_REQUEST, DomainHistory.class);
// Check that the server approve billing recurrence (which will reify after 5 days if the
// transfer is not explicitly acked) maintains the non-premium behavior.
assertBillingEventsForResource(
domain,
new BillingEvent.OneTime.Builder()
.setBillingTime(now.plusDays(10)) // 5 day pending transfer + 5 day billing grace period
.setEventTime(now.plusDays(5))
.setRegistrarId("NewRegistrar")
.setCost(Money.of(USD, new BigDecimal("11.00")))
.setDomainHistory(requestHistory)
.setReason(Reason.TRANSFER)
.setPeriodYears(1)
.setTargetId("example.tld")
.build(),
getGainingClientAutorenewEvent()
.asBuilder()
.setRenewalPriceBehavior(RenewalPriceBehavior.NONPREMIUM)
.setDomainHistory(requestHistory)
.build(),
getLosingClientAutorenewEvent()
.asBuilder()
.setRecurrenceEndTime(now.plusDays(5))
.setRenewalPriceBehavior(RenewalPriceBehavior.NONPREMIUM)
.build());
}
@Test
void testSuccess_specifiedRenewalPrice_isReflectedInTransferCostAndCarriesOver()
throws Exception {
setupDomain("example", "tld");
PremiumList pl =
PremiumListDao.save(
new PremiumList.Builder()
.setCurrency(USD)
.setName("tld")
.setLabelsToPrices(ImmutableMap.of("example", new BigDecimal("67.89")))
.build());
persistResource(Registry.get("tld").asBuilder().setPremiumList(pl).build());
domain = loadByEntity(domain);
persistResource(
loadByKey(domain.getAutorenewBillingEvent())
.asBuilder()
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(USD, new BigDecimal("18.79")))
.build());
DateTime now = clock.nowUtc();
setEppInput("domain_transfer_request.xml");
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
runFlowAssertResponse(loadFile("domain_transfer_request_response.xml"));
domain = loadByEntity(domain);
DomainHistory requestHistory =
getOnlyHistoryEntryOfType(domain, DOMAIN_TRANSFER_REQUEST, DomainHistory.class);
// Check that the server approve billing recurrence (which will reify after 5 days if the
// transfer is not explicitly acked) maintains the non-premium behavior.
assertBillingEventsForResource(
domain,
new BillingEvent.OneTime.Builder()
.setBillingTime(now.plusDays(10)) // 5 day pending transfer + 5 day billing grace period
.setEventTime(now.plusDays(5))
.setRegistrarId("NewRegistrar")
.setCost(Money.of(USD, new BigDecimal("18.79")))
.setDomainHistory(requestHistory)
.setReason(Reason.TRANSFER)
.setPeriodYears(1)
.setTargetId("example.tld")
.build(),
getGainingClientAutorenewEvent()
.asBuilder()
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(USD, new BigDecimal("18.79")))
.setDomainHistory(requestHistory)
.build(),
getLosingClientAutorenewEvent()
.asBuilder()
.setRecurrenceEndTime(now.plusDays(5))
.setRenewalPriceBehavior(RenewalPriceBehavior.SPECIFIED)
.setRenewalPrice(Money.of(USD, new BigDecimal("18.79")))
.build());
}
private void runWrongCurrencyTest(Map<String, String> substitutions) { private void runWrongCurrencyTest(Map<String, String> substitutions) {
Map<String, String> fullSubstitutions = Maps.newHashMap(); Map<String, String> fullSubstitutions = Maps.newHashMap();
fullSubstitutions.putAll(substitutions); fullSubstitutions.putAll(substitutions);

View file

@ -33,10 +33,7 @@
<fee:currency>USD</fee:currency> <fee:currency>USD</fee:currency>
<fee:command>transfer</fee:command> <fee:command>transfer</fee:command>
<fee:period unit="y">1</fee:period> <fee:period unit="y">1</fee:period>
<!-- TODO(mcilwain): This should be non-premium once transfer flow <fee:fee description="renew">%RENEWPRICE%</fee:fee>
changes are made. -->
<fee:fee description="renew">100.00</fee:fee>
<fee:class>premium</fee:class>
</fee:cd> </fee:cd>
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6"> <fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:name>rich.example</fee:name> <fee:name>rich.example</fee:name>

View file

@ -3,7 +3,7 @@
<transfer op="approve"> <transfer op="approve">
<domain:transfer <domain:transfer
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0"> xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>example.extra</domain:name> <domain:name>%DOMAIN%</domain:name>
</domain:transfer> </domain:transfer>
</transfer> </transfer>
<clTRID>ABC-12345</clTRID> <clTRID>ABC-12345</clTRID>