diff --git a/java/google/registry/model/reporting/DomainTransactionRecord.java b/java/google/registry/model/reporting/DomainTransactionRecord.java new file mode 100644 index 000000000..87c7b922e --- /dev/null +++ b/java/google/registry/model/reporting/DomainTransactionRecord.java @@ -0,0 +1,175 @@ +// 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.model.reporting; + +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; +import java.util.Set; +import org.joda.time.DateTime; + +/** + * The record of the mutations which contribute to transaction reporting. + * + *

This will only be constructed for a HistoryEntry which contributes to the transaction report, + * i.e. only domain mutations. + * + *

The registrar accredited with this transaction is the enclosing HistoryEntry.clientId. The + * only exception is for reportField = TRANSFER_LOSING_SUCCESSFUL or TRANSFER_LOSING_NACKED, which + * uses HistoryEntry.otherClientId because the losing party in a transfer is always the otherClient. + */ +@Embed +public class DomainTransactionRecord extends ImmutableObject implements Buildable { + + /** + * The time this Transaction takes effect (counting grace periods and other nuances). + * + *

Net adds, renews and transfers are modificationTime + 5 days for the grace period, while + * Autorenews have a 45 day grace period. For deletions, this is the purge date of the domain. And + * for restored names, this is the modificationTime, if done in the 30 day redemption period. + * + * @see + * Grace period spec + */ + DateTime reportingTime; + + /** The TLD this record operates on. */ + String tld; + + /** The fields affected by this transaction, and the amounts they're affected by. */ + Set transactionFieldAmounts; + + /** A tuple that encapsulates an amount to add to a specified field in the report. */ + @Embed + public static class TransactionFieldAmount extends ImmutableObject { + + public static TransactionFieldAmount create( + TransactionReportField reportField, int reportAmount) { + TransactionFieldAmount instance = new TransactionFieldAmount(); + instance.reportField = reportField; + instance.reportAmount = reportAmount; + return instance; + } + + /** The transaction report field we add reportAmount to for this registrar. */ + TransactionReportField reportField; + + /** + * The amount this record increases or decreases a registrar's report field. + * + *

For adds, renews, deletes, and restores, this is +1. For their respective cancellations, + * this is -1. + * + *

For transfers, the gaining party gets a +1 for TRANSFER_GAINING_SUCCESSFUL, whereas the + * losing party gets a +1 for TRANSFER_LOSING_SUCCESSFUL. Nacks result in +1 for + * TRANSFER_GAINING_NACKED and TRANSFER_LOSING_NACKED, as well as -1 entries to cancel out the + * original SUCCESSFUL transfer counters. Finally, if we explicitly allow a transfer, the report + * amount is 0, as we've already counted the transfer in the original request. + */ + int reportAmount; + + /** + * The field added to by reportAmount within the transaction report. + * + *

The reportField specifies which column the reportAmount contributes to in the overall + * report. ICANN wants a column for every add/renew broken down by number of years, so we have + * the NET_ADDS_#_YR and NET_RENEWS_#_YR boilerplate to facilitate report generation. + */ + public enum TransactionReportField { + 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, + 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, + TRANSFER_GAINING_SUCCESSFUL, + TRANSFER_GAINING_NACKED, + TRANSFER_LOSING_SUCCESSFUL, + TRANSFER_LOSING_NACKED, + DELETED_DOMAINS_GRACE, + DELETED_DOMAINS_NOGRACE, + RESTORED_DOMAINS + } + } + + public DateTime getReportingTime() { + return reportingTime; + } + + public String getTld() { + return tld; + } + + @Override + public Builder asBuilder() { + return new Builder(clone(this)); + } + + /** A builder for {@link DomainTransactionRecord} since it is immutable. */ + public static class Builder extends Buildable.Builder { + + public Builder() {} + + public Builder(DomainTransactionRecord instance) { + super(instance); + checkArgumentNotNull(instance, "DomainTransactionRecord instance must not be null"); + } + + public Builder setReportingTime(DateTime reportingTime) { + checkArgumentNotNull(reportingTime, "reportingTime must not be mull"); + getInstance().reportingTime = reportingTime; + return this; + } + + public Builder setTld(String tld) { + checkArgumentNotNull(tld, "tld must not be null"); + getInstance().tld = tld; + return this; + } + + public Builder setTransactionFieldAmounts( + ImmutableSet transactionFieldAmounts) { + getInstance().transactionFieldAmounts = transactionFieldAmounts; + return this; + } + + @Override + public DomainTransactionRecord build() { + checkArgumentNotNull(getInstance().reportingTime, "reportingTime must not be null"); + checkArgumentNotNull(getInstance().tld, "tld must not be null"); + checkArgumentNotNull( + getInstance().transactionFieldAmounts, "transactionFieldAmounts must not be null"); + return super.build(); + } + } +} diff --git a/java/google/registry/model/reporting/HistoryEntry.java b/java/google/registry/model/reporting/HistoryEntry.java index 24f6154b3..eea84ef9e 100644 --- a/java/google/registry/model/reporting/HistoryEntry.java +++ b/java/google/registry/model/reporting/HistoryEntry.java @@ -27,6 +27,7 @@ import google.registry.model.ImmutableObject; import google.registry.model.annotations.ReportedOn; import google.registry.model.domain.Period; import google.registry.model.eppcommon.Trid; +import javax.annotation.Nullable; import org.joda.time.DateTime; /** A record of an EPP command that mutated a resource. */ @@ -124,6 +125,16 @@ public class HistoryEntry extends ImmutableObject implements Buildable { /** Whether this change was requested by a registrar. */ Boolean requestedByRegistrar; + /** + * Logging field for transaction reporting. + * + *

This will be null for any HistoryEntry generated before this field was added. This will + * also be null if the HistoryEntry refers to an EPP mutation that does not affect domain + * transaction counts (such as contact or host mutations). + */ + @Nullable + DomainTransactionRecord domainTransactionRecord; + public Key getParent() { return parent; } @@ -168,6 +179,11 @@ public class HistoryEntry extends ImmutableObject implements Buildable { return requestedByRegistrar; } + @Nullable + public DomainTransactionRecord getDomainTransactionRecord() { + return domainTransactionRecord; + } + @Override public Builder asBuilder() { return new Builder(clone(this)); @@ -240,5 +256,10 @@ public class HistoryEntry extends ImmutableObject implements Buildable { getInstance().requestedByRegistrar = requestedByRegistrar; return this; } + + public Builder setDomainTransactionRecord(DomainTransactionRecord domainTransactionRecord) { + getInstance().domainTransactionRecord = domainTransactionRecord; + return this; + } } } diff --git a/javatests/google/registry/model/reporting/HistoryEntryTest.java b/javatests/google/registry/model/reporting/HistoryEntryTest.java index 1aa64e30c..de782007d 100644 --- a/javatests/google/registry/model/reporting/HistoryEntryTest.java +++ b/javatests/google/registry/model/reporting/HistoryEntryTest.java @@ -16,14 +16,17 @@ package google.registry.model.reporting; import static com.google.common.truth.Truth.assertThat; import static google.registry.model.ofy.ObjectifyService.ofy; +import static google.registry.model.reporting.DomainTransactionRecord.TransactionFieldAmount.TransactionReportField.NET_ADDS_1_YR; import static google.registry.testing.DatastoreHelper.createTld; import static google.registry.testing.DatastoreHelper.newDomainResource; import static google.registry.testing.DatastoreHelper.persistResource; import static java.nio.charset.StandardCharsets.UTF_8; +import com.google.common.collect.ImmutableSet; import google.registry.model.EntityTestCase; import google.registry.model.domain.Period; import google.registry.model.eppcommon.Trid; +import google.registry.model.reporting.DomainTransactionRecord.TransactionFieldAmount; import org.junit.Before; import org.junit.Test; @@ -35,22 +38,30 @@ public class HistoryEntryTest extends EntityTestCase { @Before public void setUp() throws Exception { createTld("foobar"); + DomainTransactionRecord transactionRecord = + new DomainTransactionRecord.Builder() + .setTld("foobar") + .setReportingTime(clock.nowUtc()) + .setTransactionFieldAmounts( + ImmutableSet.of(TransactionFieldAmount.create(NET_ADDS_1_YR, 1))) + .build(); // Set up a new persisted HistoryEntry entity. - historyEntry = new HistoryEntry.Builder() - .setParent(newDomainResource("foo.foobar")) - .setType(HistoryEntry.Type.DOMAIN_CREATE) - .setPeriod(Period.create(1, Period.Unit.YEARS)) - .setXmlBytes("".getBytes(UTF_8)) - .setModificationTime(clock.nowUtc()) - .setClientId("foo") - .setOtherClientId("otherClient") - .setTrid(Trid.create("ABC-123", "server-trid")) - .setBySuperuser(false) - .setReason("reason") - .setRequestedByRegistrar(false) - .build(); + historyEntry = + new HistoryEntry.Builder() + .setParent(newDomainResource("foo.foobar")) + .setType(HistoryEntry.Type.DOMAIN_CREATE) + .setPeriod(Period.create(1, Period.Unit.YEARS)) + .setXmlBytes("".getBytes(UTF_8)) + .setModificationTime(clock.nowUtc()) + .setClientId("foo") + .setOtherClientId("otherClient") + .setTrid(Trid.create("ABC-123", "server-trid")) + .setBySuperuser(false) + .setReason("reason") + .setRequestedByRegistrar(false) + .setDomainTransactionRecord(transactionRecord) + .build(); persistResource(historyEntry); - } @Test diff --git a/javatests/google/registry/model/schema.txt b/javatests/google/registry/model/schema.txt index 50554cd90..61e41e533 100644 --- a/javatests/google/registry/model/schema.txt +++ b/javatests/google/registry/model/schema.txt @@ -781,6 +781,44 @@ class google.registry.model.registry.label.ReservedList$ReservedListEntry { java.lang.String authCode; java.lang.String comment; } +class google.registry.model.reporting.DomainTransactionRecord { + java.lang.String tld; + java.util.Set transactionFieldAmounts; + org.joda.time.DateTime reportingTime; +} +class google.registry.model.reporting.DomainTransactionRecord$TransactionFieldAmount { + google.registry.model.reporting.DomainTransactionRecord$TransactionFieldAmount$TransactionReportField reportField; + int reportAmount; +} +enum google.registry.model.reporting.DomainTransactionRecord$TransactionFieldAmount$TransactionReportField { + DELETED_DOMAINS_GRACE; + DELETED_DOMAINS_NOGRACE; + NET_ADDS_10_YR; + 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_RENEWS_10_YR; + 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; + RESTORED_DOMAINS; + TRANSFER_GAINING_NACKED; + TRANSFER_GAINING_SUCCESSFUL; + TRANSFER_LOSING_NACKED; + TRANSFER_LOSING_SUCCESSFUL; +} class google.registry.model.reporting.HistoryEntry { @Id long id; @Parent com.googlecode.objectify.Key parent; @@ -788,6 +826,7 @@ class google.registry.model.reporting.HistoryEntry { byte[] xmlBytes; google.registry.model.domain.Period period; google.registry.model.eppcommon.Trid trid; + google.registry.model.reporting.DomainTransactionRecord domainTransactionRecord; google.registry.model.reporting.HistoryEntry$Type type; java.lang.Boolean requestedByRegistrar; java.lang.String clientId;