diff --git a/java/google/registry/model/billing/BillingEvent.java b/java/google/registry/model/billing/BillingEvent.java index ca099c898..d55f88168 100644 --- a/java/google/registry/model/billing/BillingEvent.java +++ b/java/google/registry/model/billing/BillingEvent.java @@ -216,6 +216,15 @@ public abstract class BillingEvent extends ImmutableObject @IgnoreSave(IfNull.class) Integer periodYears = null; + /** + * For {@link Flag#SYNTHETIC} events, when this event was persisted to datastore (i.e. the + * cursor position at the time the recurrence expansion job was last run). In the event a job + * needs to be undone, a query on this field will return the complete set of potentially bad + * events. + */ + @Index + DateTime syntheticCreationTime; + public Money getCost() { return cost; } @@ -228,6 +237,10 @@ public abstract class BillingEvent extends ImmutableObject return periodYears; } + public DateTime getSyntheticCreationTime() { + return syntheticCreationTime; + } + @Override public Builder asBuilder() { return new Builder(clone(this)); @@ -259,6 +272,11 @@ public abstract class BillingEvent extends ImmutableObject return this; } + public Builder setSyntheticCreationTime(DateTime syntheticCreationTime) { + getInstance().syntheticCreationTime = syntheticCreationTime; + return this; + } + @Override public OneTime build() { OneTime instance = getInstance(); @@ -270,6 +288,10 @@ public abstract class BillingEvent extends ImmutableObject checkState( reasonsWithPeriods.contains(instance.reason) == (instance.periodYears != null), "Period years must be set if and only if reason is CREATE, RENEW, or TRANSFER."); + checkState( + instance.getFlags().contains(Flag.SYNTHETIC) + == (instance.syntheticCreationTime != null), + "Billing events with SYNTHETIC flag set must have a synthetic creation time."); return super.build(); } } diff --git a/javatests/google/registry/model/billing/BillingEventTest.java b/javatests/google/registry/model/billing/BillingEventTest.java index 7da3c7602..a067dd50e 100644 --- a/javatests/google/registry/model/billing/BillingEventTest.java +++ b/javatests/google/registry/model/billing/BillingEventTest.java @@ -55,6 +55,7 @@ public class BillingEventTest extends EntityTestCase { HistoryEntry historyEntry2; DomainResource domain; BillingEvent.OneTime oneTime; + BillingEvent.OneTime oneTimeSynthetic; BillingEvent.Recurring recurring; BillingEvent.Cancellation cancellationOneTime; BillingEvent.Cancellation cancellationRecurring; @@ -84,6 +85,16 @@ public class BillingEventTest extends EntityTestCase { .setCost(Money.of(USD, 1)) .setEventTime(now) .setBillingTime(now.plusDays(5)))); + oneTimeSynthetic = persistResource(commonInit( + new BillingEvent.OneTime.Builder() + .setParent(historyEntry) + .setReason(Reason.CREATE) + .setFlags(ImmutableSet.of(BillingEvent.Flag.ANCHOR_TENANT, BillingEvent.Flag.SYNTHETIC)) + .setSyntheticCreationTime(now.plusDays(10)) + .setPeriodYears(2) + .setCost(Money.of(USD, 1)) + .setEventTime(now) + .setBillingTime(now.plusDays(5)))); recurring = persistResource(commonInit( new BillingEvent.Recurring.Builder() .setParent(historyEntry) @@ -136,7 +147,7 @@ public class BillingEventTest extends EntityTestCase { // Note that these are all tested separately because BillingEvent is an abstract base class that // lacks the @Entity annotation, and thus we cannot call .type(BillingEvent.class) assertThat(ofy().load().type(BillingEvent.OneTime.class).ancestor(domain).list()) - .containsExactly(oneTime); + .containsExactly(oneTime, oneTimeSynthetic); assertThat(ofy().load().type(BillingEvent.Recurring.class).ancestor(domain).list()) .containsExactly(recurring); assertThat(ofy().load().type(BillingEvent.Cancellation.class).ancestor(domain).list()) @@ -144,7 +155,7 @@ public class BillingEventTest extends EntityTestCase { assertThat(ofy().load().type(BillingEvent.Modification.class).ancestor(domain).list()) .containsExactly(modification); assertThat(ofy().load().type(BillingEvent.OneTime.class).ancestor(historyEntry).list()) - .containsExactly(oneTime); + .containsExactly(oneTime, oneTimeSynthetic); assertThat(ofy().load().type(BillingEvent.Recurring.class).ancestor(historyEntry).list()) .containsExactly(recurring); assertThat(ofy().load().type(BillingEvent.Cancellation.class).ancestor(historyEntry2).list()) @@ -155,13 +166,35 @@ public class BillingEventTest extends EntityTestCase { @Test public void testIndexing() throws Exception { - verifyIndexing(oneTime, "clientId", "eventTime", "billingTime"); + verifyIndexing(oneTime, "clientId", "eventTime", "billingTime", "syntheticCreationTime"); + verifyIndexing( + oneTimeSynthetic, "clientId", "eventTime", "billingTime", "syntheticCreationTime"); verifyIndexing( recurring, "clientId", "eventTime", "recurrenceEndTime", "recurrenceTimeOfYear.timeString"); verifyIndexing(cancellationOneTime, "clientId", "eventTime", "billingTime"); verifyIndexing(modification, "clientId", "eventTime"); } + @Test + public void testFailure_syntheticFlagWithoutCreationTime() { + thrown.expect( + IllegalStateException.class, + "Billing events with SYNTHETIC flag set must have a synthetic creation time"); + oneTime.asBuilder() + .setFlags(ImmutableSet.of(BillingEvent.Flag.SYNTHETIC)) + .build(); + } + + @Test + public void testFailure_syntheticCreationTimeWithoutFlag() { + thrown.expect( + IllegalStateException.class, + "Billing events with SYNTHETIC flag set must have a synthetic creation time"); + oneTime.asBuilder() + .setSyntheticCreationTime(now.plusDays(10)) + .build(); + } + @Test public void testSuccess_cancellation_forGracePeriod_withOneTime() { BillingEvent.Cancellation newCancellation = BillingEvent.Cancellation.forGracePeriod( diff --git a/javatests/google/registry/tools/DeleteDomainCommandTest.java b/javatests/google/registry/tools/DeleteDomainCommandTest.java index 81f8e8d1c..ca5e13ecc 100644 --- a/javatests/google/registry/tools/DeleteDomainCommandTest.java +++ b/javatests/google/registry/tools/DeleteDomainCommandTest.java @@ -25,28 +25,28 @@ public class DeleteDomainCommandTest extends EppToolCommandTestCase extends C /** Helper to get a new {@link EppVerifier} instance. */ EppVerifier eppVerifier() { - return new EppVerifier(); + return new EppVerifier("NewRegistrar", false, false); } - /** Builder pattern class for verifying EPP commands sent to the server. */ + /** Class for verifying EPP commands sent to the server. */ class EppVerifier { - String clientIdentifier = "NewRegistrar"; - boolean superuser = false; - boolean dryRun = false; + private final String clientIdentifier; + private final boolean superuser; + private final boolean dryRun; + + private EppVerifier(String clientIdentifier, boolean superuser, boolean dryRun) { + this.clientIdentifier = clientIdentifier; + this.superuser = superuser; + this.dryRun = dryRun; + } EppVerifier setClientIdentifier(String clientIdentifier) { - this.clientIdentifier = clientIdentifier; - return this; + return new EppVerifier(clientIdentifier, superuser, dryRun); } EppVerifier asSuperuser() { - this.superuser = true; - return this; + return new EppVerifier(clientIdentifier, true, dryRun); } EppVerifier asDryRun() { - this.dryRun = true; - return this; + return new EppVerifier(clientIdentifier, superuser, true); } void verifySent(String... filesToMatch) throws Exception {