Record domain transaction for domain deletes

This is the third of many cls adding explicit logging in all our domain
mutation flows to facilitate transaction reporting.

We add a +1 counter for either grace or nograce deletes, based on the grace period status of the domain. We then search back in time for DOMAIN_CREATE, DOMAIN_RENEW and DOMAIN_AUTORENEW HistoryEntries off the same resource that happened in their corresponding grace periods (5, 5 and 45 days respectively). All transaction records for these events are then given -1 counters to properly account for cancellations in the NET_CREATE and NET_RENEW fields.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=166506010
This commit is contained in:
larryruili 2017-08-25 11:55:39 -07:00 committed by Ben McIlwain
parent d011b8e073
commit 7ee8bc9070
4 changed files with 350 additions and 60 deletions

View file

@ -24,15 +24,20 @@ import static google.registry.flows.ResourceFlowUtils.verifyNoDisallowedStatuses
import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfo;
import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership;
import static google.registry.flows.domain.DomainFlowUtils.checkAllowedAccessToTld;
import static google.registry.flows.domain.DomainFlowUtils.createCancellingRecords;
import static google.registry.flows.domain.DomainFlowUtils.updateAutorenewRecurrenceEndTime;
import static google.registry.flows.domain.DomainFlowUtils.verifyNotInPredelegation;
import static google.registry.model.eppoutput.Result.Code.SUCCESS;
import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_ACTION_PENDING;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.model.reporting.DomainTransactionRecord.TransactionReportField.isAddsField;
import static google.registry.model.reporting.DomainTransactionRecord.TransactionReportField.isRenewsField;
import static google.registry.pricing.PricingEngineProxy.getDomainRenewCost;
import static google.registry.util.CollectionUtils.nullToEmpty;
import static google.registry.util.CollectionUtils.union;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.googlecode.objectify.Key;
@ -77,14 +82,18 @@ import google.registry.model.poll.PendingActionNotificationResponse.DomainPendin
import google.registry.model.poll.PollMessage;
import google.registry.model.poll.PollMessage.OneTime;
import google.registry.model.registry.Registry;
import google.registry.model.reporting.DomainTransactionRecord;
import google.registry.model.reporting.DomainTransactionRecord.TransactionReportField;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import google.registry.model.transfer.TransferStatus;
import java.util.Collections;
import java.util.Set;
import javax.annotation.Nullable;
import javax.inject.Inject;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.joda.time.Duration;
/**
* An EPP flow that deletes a domain.
@ -133,24 +142,30 @@ public final class DomainDeleteFlow implements TransactionalFlow {
customLogic.afterValidation(
AfterValidationParameters.newBuilder().setExistingDomain(existingDomain).build());
ImmutableSet.Builder<ImmutableObject> entitiesToSave = new ImmutableSet.Builder<>();
HistoryEntry historyEntry = buildHistoryEntry(existingDomain, now);
Builder builder = existingDomain.getStatusValues().contains(StatusValue.PENDING_TRANSFER)
? ResourceFlowUtils.<DomainResource, DomainResource.Builder>resolvePendingTransfer(
existingDomain, TransferStatus.SERVER_CANCELLED, now)
: existingDomain.asBuilder();
builder.setDeletionTime(now).setStatusValues(null);
// If the domain is in the Add Grace Period, we delete it immediately, which is already
// reflected in the builder we just prepared. Otherwise we give it a PENDING_DELETE status.
if (!existingDomain.getGracePeriodStatuses().contains(GracePeriodStatus.ADD)) {
// By default, this should be 30 days of grace, and 5 days of pending delete.
DateTime deletionTime = now
.plus(registry.getRedemptionGracePeriodLength())
.plus(registry.getPendingDeleteLength());
boolean inAddGracePeriod =
existingDomain.getGracePeriodStatuses().contains(GracePeriodStatus.ADD);
// If the domain is in the Add Grace Period, we delete it immediately.
// Otherwise, we give it a PENDING_DELETE status.
Duration durationUntilDelete =
inAddGracePeriod
? Duration.ZERO
// By default, this should be 30 days of grace, and 5 days of pending delete.
: registry.getRedemptionGracePeriodLength().plus(registry.getPendingDeleteLength());
HistoryEntry historyEntry = buildHistoryEntry(
existingDomain, registry, now, durationUntilDelete, inAddGracePeriod);
if (inAddGracePeriod) {
builder.setDeletionTime(now).setStatusValues(null);
} else {
DateTime deletionTime = now.plus(durationUntilDelete);
PollMessage.OneTime deletePollMessage =
createDeletePollMessage(existingDomain, historyEntry, deletionTime);
entitiesToSave.add(deletePollMessage);
builder.setStatusValues(ImmutableSet.of(StatusValue.PENDING_DELETE))
.setDeletionTime(deletionTime)
builder.setDeletionTime(deletionTime)
.setStatusValues(ImmutableSet.of(StatusValue.PENDING_DELETE))
// Clear out all old grace periods and add REDEMPTION, which does not include a key to a
// billing event because there isn't one for a domain delete.
.setGracePeriods(ImmutableSet.of(GracePeriod.createWithoutBillingEvent(
@ -218,11 +233,43 @@ public final class DomainDeleteFlow implements TransactionalFlow {
}
}
private HistoryEntry buildHistoryEntry(DomainResource existingResource, DateTime now) {
private HistoryEntry buildHistoryEntry(
DomainResource existingResource,
Registry registry,
DateTime now,
Duration durationUntilDelete,
boolean inAddGracePeriod) {
Duration maxGracePeriod = Collections.max(
ImmutableSet.of(
registry.getAddGracePeriodLength(),
registry.getAutoRenewGracePeriodLength(),
registry.getRenewGracePeriodLength()));
ImmutableSet<DomainTransactionRecord> cancelledRecords =
createCancellingRecords(
existingResource,
now,
maxGracePeriod,
new Predicate<DomainTransactionRecord>() {
@Override
public boolean apply(@Nullable DomainTransactionRecord domainTransactionRecord) {
return isAddsField(domainTransactionRecord.getReportField())
|| isRenewsField(domainTransactionRecord.getReportField());
}
});
return historyBuilder
.setType(HistoryEntry.Type.DOMAIN_DELETE)
.setModificationTime(now)
.setParent(Key.create(existingResource))
.setDomainTransactionRecords(
union(
cancelledRecords,
DomainTransactionRecord.create(
existingResource.getTld(),
now.plus(durationUntilDelete),
inAddGracePeriod
? TransactionReportField.DELETED_DOMAINS_GRACE
: TransactionReportField.DELETED_DOMAINS_NOGRACE,
1)))
.build();
}

View file

@ -42,7 +42,9 @@ import static google.registry.util.FormattingLogger.getLoggerForCallerClass;
import com.google.common.base.CharMatcher;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.base.Splitter;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
@ -105,6 +107,7 @@ import google.registry.model.registry.Registry;
import google.registry.model.registry.Registry.TldState;
import google.registry.model.registry.label.ReservationType;
import google.registry.model.registry.label.ReservedList;
import google.registry.model.reporting.DomainTransactionRecord;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.tmch.ClaimsListShard;
import google.registry.util.FormattingLogger;
@ -118,6 +121,7 @@ import javax.annotation.Nullable;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.joda.time.Duration;
/** Static utility functions for domain flows. */
public class DomainFlowUtils {
@ -909,6 +913,56 @@ public class DomainFlowUtils {
return builder.build();
}
/**
* Returns a set of DomainTransactionRecords which negate the most recent HistoryEntry's records.
*
* <p>Domain deletes and transfers use this function to account for previous records negated by
* their flow. For example, if a grace period delete occurs, we must add -1 counters for the
* associated NET_ADDS_#_YRS field, if it exists.
*
* <p>The steps are as follows: 1. Find all HistoryEntries under the domain modified in the past,
* up to the maxSearchPeriod. 2. Only keep HistoryEntries with a DomainTransactionRecord that a)
* hasn't been reported yet and b) matches the predicate 3. Return the transactionRecords under
* the most recent HistoryEntry that fits the above criteria, with negated reportAmounts.
*/
static ImmutableSet<DomainTransactionRecord> createCancellingRecords(
DomainResource domainResource,
final DateTime now,
Duration maxSearchPeriod,
final Predicate<DomainTransactionRecord> isCancellable) {
List<HistoryEntry> recentHistoryEntries = ofy().load()
.type(HistoryEntry.class)
.ancestor(domainResource)
.filter("modificationTime >=", now.minus(maxSearchPeriod))
.order("modificationTime")
.list();
Optional<HistoryEntry> entryToCancel = FluentIterable.from(recentHistoryEntries)
.filter(
new Predicate<HistoryEntry>() {
@Override
public boolean apply(HistoryEntry historyEntry) {
// Look for add and renew transaction records that have yet to be reported
for (DomainTransactionRecord record : historyEntry.getDomainTransactionRecords()) {
if (isCancellable.apply(record) && record.getReportingTime().isAfter(now)) {
return true;
}
}
return false;
}
})
// We only want to cancel out the most recent add or renewal
.last();
ImmutableSet.Builder<DomainTransactionRecord> recordsBuilder = new ImmutableSet.Builder<>();
if (entryToCancel.isPresent()) {
for (DomainTransactionRecord record : entryToCancel.get().getDomainTransactionRecords()) {
int cancelledAmount = -1 * record.getReportAmount();
recordsBuilder.add(record.asBuilder().setReportAmount(cancelledAmount).build());
}
}
return recordsBuilder.build();
}
/** Resource linked to this domain does not exist. */
static class LinkedResourcesDoNotExistException extends ObjectDoesNotExistException {
public LinkedResourcesDoNotExistException(Class<?> type, ImmutableSet<String> resourceIds) {

View file

@ -17,6 +17,7 @@ package google.registry.model.reporting;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.google.common.collect.ImmutableSet;
import com.googlecode.objectify.annotation.Embed;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
@ -114,6 +115,42 @@ public class DomainTransactionRecord extends ImmutableObject implements Buildabl
return nameToField("NET_RENEWS_%d_YR", years);
}
private static final ImmutableSet<TransactionReportField> ADD_FIELDS =
ImmutableSet.of(
NET_ADDS_1_YR,
NET_ADDS_2_YR,
NET_ADDS_3_YR,
NET_ADDS_4_YR,
NET_ADDS_5_YR,
NET_ADDS_6_YR,
NET_ADDS_7_YR,
NET_ADDS_8_YR,
NET_ADDS_9_YR,
NET_ADDS_10_YR);
private static final ImmutableSet<TransactionReportField> RENEW_FIELDS =
ImmutableSet.of(
NET_RENEWS_1_YR,
NET_RENEWS_2_YR,
NET_RENEWS_3_YR,
NET_RENEWS_4_YR,
NET_RENEWS_5_YR,
NET_RENEWS_6_YR,
NET_RENEWS_7_YR,
NET_RENEWS_8_YR,
NET_RENEWS_9_YR,
NET_RENEWS_10_YR);
/** Boilerplate to determine if a field is one of the NET_ADDS fields. */
public static boolean isAddsField(TransactionReportField field) {
return ADD_FIELDS.contains(field);
}
/** Boilerplate to determine if a field is one of the NET_ADDS fields. */
public static boolean isRenewsField(TransactionReportField field) {
return RENEW_FIELDS.contains(field);
}
private static TransactionReportField nameToField(String enumTemplate, int years) {
checkArgument(
years >= 1 && years <= 10, "domain add and renew years must be between 1 and 10");