diff --git a/core/src/main/java/google/registry/batch/BatchModule.java b/core/src/main/java/google/registry/batch/BatchModule.java index a29a54d20..1272684c5 100644 --- a/core/src/main/java/google/registry/batch/BatchModule.java +++ b/core/src/main/java/google/registry/batch/BatchModule.java @@ -105,10 +105,22 @@ public class BatchModule { } @Provides - @Parameter(ExpandRecurringBillingEventsAction.PARAM_CURSOR_TIME) - static Optional provideCursorTime(HttpServletRequest req) { + @Parameter(ExpandRecurringBillingEventsAction.PARAM_START_TIME) + static Optional provideStartTime(HttpServletRequest req) { return extractOptionalDatetimeParameter( - req, ExpandRecurringBillingEventsAction.PARAM_CURSOR_TIME); + req, ExpandRecurringBillingEventsAction.PARAM_START_TIME); + } + + @Provides + @Parameter(ExpandRecurringBillingEventsAction.PARAM_END_TIME) + static Optional provideEndTime(HttpServletRequest req) { + return extractOptionalDatetimeParameter(req, ExpandRecurringBillingEventsAction.PARAM_END_TIME); + } + + @Provides + @Parameter(ExpandRecurringBillingEventsAction.PARAM_ADVANCE_CURSOR) + static boolean provideAdvanceCursor(HttpServletRequest req) { + return extractBooleanParameter(req, ExpandRecurringBillingEventsAction.PARAM_ADVANCE_CURSOR); } @Provides diff --git a/core/src/main/java/google/registry/batch/ExpandRecurringBillingEventsAction.java b/core/src/main/java/google/registry/batch/ExpandRecurringBillingEventsAction.java index 19d75dbf1..a5e8068de 100644 --- a/core/src/main/java/google/registry/batch/ExpandRecurringBillingEventsAction.java +++ b/core/src/main/java/google/registry/batch/ExpandRecurringBillingEventsAction.java @@ -15,58 +15,39 @@ package google.registry.batch; import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.collect.ImmutableSet.toImmutableSet; -import static com.google.common.collect.Sets.difference; -import static com.google.common.collect.Sets.newHashSet; import static google.registry.batch.BatchModule.PARAM_DRY_RUN; +import static google.registry.beam.BeamUtils.createJobName; import static google.registry.model.common.Cursor.CursorType.RECURRING_BILLING; -import static google.registry.model.domain.Period.Unit.YEARS; -import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_AUTORENEW; -import static google.registry.persistence.transaction.QueryComposer.Comparator.EQ; import static google.registry.persistence.transaction.TransactionManagerFactory.tm; -import static google.registry.util.CollectionUtils.union; import static google.registry.util.DateTimeUtils.START_OF_TIME; -import static google.registry.util.DateTimeUtils.earliestOf; -import static google.registry.util.DomainNameUtils.getTldFromDomainName; +import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; +import static javax.servlet.http.HttpServletResponse.SC_OK; -import com.google.auto.value.AutoValue; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Range; -import com.google.common.collect.Streams; +import com.google.api.services.dataflow.Dataflow; +import com.google.api.services.dataflow.model.LaunchFlexTemplateParameter; +import com.google.api.services.dataflow.model.LaunchFlexTemplateRequest; +import com.google.api.services.dataflow.model.LaunchFlexTemplateResponse; +import com.google.common.collect.ImmutableMap; import com.google.common.flogger.FluentLogger; +import google.registry.beam.billing.ExpandRecurringBillingEventsPipeline; import google.registry.config.RegistryConfig.Config; -import google.registry.flows.domain.DomainPricingLogic; -import google.registry.model.ImmutableObject; -import google.registry.model.billing.BillingEvent; -import google.registry.model.billing.BillingEvent.Flag; +import google.registry.config.RegistryEnvironment; import google.registry.model.billing.BillingEvent.OneTime; import google.registry.model.billing.BillingEvent.Recurring; import google.registry.model.common.Cursor; -import google.registry.model.domain.Domain; -import google.registry.model.domain.DomainHistory; -import google.registry.model.domain.Period; -import google.registry.model.reporting.DomainTransactionRecord; -import google.registry.model.reporting.DomainTransactionRecord.TransactionReportField; -import google.registry.model.tld.Registry; -import google.registry.persistence.VKey; import google.registry.request.Action; import google.registry.request.Parameter; import google.registry.request.Response; import google.registry.request.auth.Auth; import google.registry.util.Clock; -import java.util.List; +import java.io.IOException; import java.util.Optional; -import java.util.Set; -import java.util.concurrent.TimeUnit; import javax.inject.Inject; import org.joda.time.DateTime; /** - * An action that expands {@link Recurring} billing events into synthetic {@link OneTime} events. - * - *

The cursor used throughout this action (overridden if necessary using the parameter {@code - * cursorTime}) represents the inclusive lower bound on the range of billing times that will be - * expanded as a result of the job (the exclusive upper bound being the execution time of the job). + * An action that kicks off a {@link ExpandRecurringBillingEventsPipeline} dataflow job to expand + * {@link Recurring} billing events into synthetic {@link OneTime} events. */ @Action( service = Action.Service.BACKEND, @@ -74,303 +55,109 @@ import org.joda.time.DateTime; auth = Auth.AUTH_INTERNAL_OR_ADMIN) public class ExpandRecurringBillingEventsAction implements Runnable { - public static final String PARAM_CURSOR_TIME = "cursorTime"; + public static final String PARAM_START_TIME = "startTime"; + public static final String PARAM_END_TIME = "endTime"; + public static final String PARAM_ADVANCE_CURSOR = "advanceCursor"; + + private static final String PIPELINE_NAME = "expand_recurring_billing_events_pipeline"; private static final FluentLogger logger = FluentLogger.forEnclosingClass(); @Inject Clock clock; @Inject - @Config("jdbcBatchSize") - int batchSize; + @Parameter(PARAM_DRY_RUN) + boolean isDryRun; - @Inject @Parameter(PARAM_DRY_RUN) boolean isDryRun; - @Inject @Parameter(PARAM_CURSOR_TIME) Optional cursorTimeParam; + @Inject + @Parameter(PARAM_ADVANCE_CURSOR) + boolean advanceCursor; + + @Inject + @Parameter(PARAM_START_TIME) + Optional startTimeParam; + + @Inject + @Parameter(PARAM_END_TIME) + Optional endTimeParam; + + @Inject + @Config("projectId") + String projectId; + + @Inject + @Config("defaultJobRegion") + String jobRegion; + + @Inject + @Config("beamStagingBucketUrl") + String stagingBucketUrl; + + @Inject Dataflow dataflow; - @Inject DomainPricingLogic domainPricingLogic; @Inject Response response; - @Inject ExpandRecurringBillingEventsAction() {} + + @Inject + ExpandRecurringBillingEventsAction() {} @Override public void run() { - DateTime executeTime = clock.nowUtc(); - DateTime persistedCursorTime = - tm().transact( - () -> - tm().loadByKeyIfPresent(Cursor.createGlobalVKey(RECURRING_BILLING)) - .orElse(Cursor.createGlobal(RECURRING_BILLING, START_OF_TIME)) - .getCursorTime()); - DateTime cursorTime = cursorTimeParam.orElse(persistedCursorTime); + checkArgument(!(isDryRun && advanceCursor), "Cannot advance the cursor in a dry run."); + DateTime endTime = endTimeParam.orElse(clock.nowUtc()); checkArgument( - cursorTime.isBefore(executeTime), "Cursor time must be earlier than execution time."); - logger.atInfo().log( - "Running Recurring billing event expansion for billing time range [%s, %s).", - cursorTime, executeTime); - expandSqlBillingEventsInBatches(executeTime, cursorTime, persistedCursorTime); - } - - private void expandSqlBillingEventsInBatches( - DateTime executeTime, DateTime cursorTime, DateTime persistedCursorTime) { - int totalBillingEventsSaved = 0; - long maxProcessedRecurrenceId = 0; - SqlBatchResults sqlBatchResults; - - do { - final long prevMaxProcessedRecurrenceId = maxProcessedRecurrenceId; - sqlBatchResults = - tm().transact( - () -> { - Set expandedDomains = newHashSet(); - int batchBillingEventsSaved = 0; - long maxRecurrenceId = prevMaxProcessedRecurrenceId; - List recurrings = - tm().query( - "FROM BillingRecurrence " - + "WHERE eventTime <= :executeTime " - + "AND eventTime < recurrenceEndTime " - + "AND id > :maxProcessedRecurrenceId " - + "AND recurrenceEndTime > :adjustedCursorTime " - + "ORDER BY id ASC", - Recurring.class) - .setParameter("executeTime", executeTime) - .setParameter("maxProcessedRecurrenceId", prevMaxProcessedRecurrenceId) - .setParameter( - "adjustedCursorTime", - cursorTime.minus(Registry.DEFAULT_AUTO_RENEW_GRACE_PERIOD)) - .setMaxResults(batchSize) - .getResultList(); - for (Recurring recurring : recurrings) { - if (expandedDomains.contains(recurring.getTargetId())) { - // On the off chance this batch contains multiple recurrences for the same - // domain (which is actually possible if a given domain is quickly renewed - // multiple times in a row), then short-circuit after the first one is - // processed that involves actually expanding a billing event. This is - // necessary because otherwise we get an "Inserted/updated object reloaded" - // error from Hibernate when those billing events would be loaded - // inside a transaction where they were already written. Note, there is no - // actual further work to be done in this case anyway, not unless it has - // somehow been over a year since this action last ran successfully (and if - // that were somehow true, the remaining billing events would still be - // expanded on subsequent runs). - continue; - } - int billingEventsSaved = - expandBillingEvent( - recurring, executeTime, cursorTime, isDryRun, domainPricingLogic); - batchBillingEventsSaved += billingEventsSaved; - if (billingEventsSaved > 0) { - expandedDomains.add(recurring.getTargetId()); - } - maxRecurrenceId = Math.max(maxRecurrenceId, recurring.getId()); - } - return SqlBatchResults.create( - batchBillingEventsSaved, - maxRecurrenceId, - maxRecurrenceId > prevMaxProcessedRecurrenceId); - }); - totalBillingEventsSaved += sqlBatchResults.batchBillingEventsSaved(); - maxProcessedRecurrenceId = sqlBatchResults.maxProcessedRecurrenceId(); - if (sqlBatchResults.batchBillingEventsSaved() > 0) { - logger.atInfo().log( - "Saved %d billing events in batch (%d total) with max recurrence id %d.", - sqlBatchResults.batchBillingEventsSaved(), - totalBillingEventsSaved, - maxProcessedRecurrenceId); - } else { - // If we're churning through a lot of no-op recurrences that don't need expanding (yet?), - // then only log one no-op every so often as a good balance between letting the user track - // that the action is still running while also not spamming the logs incessantly. - logger.atInfo().atMostEvery(3, TimeUnit.MINUTES).log( - "Processed up to max recurrence id %d (no billing events saved recently).", - maxProcessedRecurrenceId); - } - } while (sqlBatchResults.shouldContinue()); - - if (!isDryRun) { - logger.atInfo().log("Saved %d total OneTime billing events.", totalBillingEventsSaved); - } else { - logger.atInfo().log( - "Generated %d total OneTime billing events (dry run).", totalBillingEventsSaved); - } - logger.atInfo().log( - "Recurring event expansion %s complete for billing event range [%s, %s).", - isDryRun ? "(dry run) " : "", cursorTime, executeTime); - tm().transact( - () -> { - // Check for the unlikely scenario where the cursor has been altered during the - // expansion. - DateTime currentCursorTime = - tm().loadByKeyIfPresent(Cursor.createGlobalVKey(RECURRING_BILLING)) - .orElse(Cursor.createGlobal(RECURRING_BILLING, START_OF_TIME)) - .getCursorTime(); - if (!currentCursorTime.equals(persistedCursorTime)) { - throw new IllegalStateException( + !endTime.isAfter(clock.nowUtc()), "End time (%s) must be at or before now", endTime); + DateTime startTime = + startTimeParam.orElse( + tm().transact( + () -> + tm().loadByKeyIfPresent(Cursor.createGlobalVKey(RECURRING_BILLING)) + .orElse(Cursor.createGlobal(RECURRING_BILLING, START_OF_TIME)) + .getCursorTime())); + checkArgument( + startTime.isBefore(endTime), + String.format("Start time (%s) must be before end time (%s)", startTime, endTime)); + LaunchFlexTemplateParameter launchParameter = + new LaunchFlexTemplateParameter() + .setJobName( + createJobName( String.format( - "Current cursor position %s does not match persisted cursor position %s.", - currentCursorTime, persistedCursorTime)); - } - if (!isDryRun) { - tm().put(Cursor.createGlobal(RECURRING_BILLING, executeTime)); - } - }); - } - - @AutoValue - abstract static class SqlBatchResults { - abstract int batchBillingEventsSaved(); - - abstract long maxProcessedRecurrenceId(); - - abstract boolean shouldContinue(); - - static SqlBatchResults create( - int batchBillingEventsSaved, long maxProcessedRecurrenceId, boolean shouldContinue) { - return new AutoValue_ExpandRecurringBillingEventsAction_SqlBatchResults( - batchBillingEventsSaved, maxProcessedRecurrenceId, shouldContinue); + "expand-billing-%s", startTime.toString("yyyy-MM-dd't'HH-mm-ss'z'")), + clock)) + .setContainerSpecGcsPath( + String.format("%s/%s_metadata.json", stagingBucketUrl, PIPELINE_NAME)) + .setParameters( + new ImmutableMap.Builder() + .put("registryEnvironment", RegistryEnvironment.get().name()) + .put("startTime", startTime.toString("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")) + .put("endTime", endTime.toString("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")) + .put("isDryRun", Boolean.toString(isDryRun)) + .put("advanceCursor", Boolean.toString(advanceCursor)) + .build()); + logger.atInfo().log( + "Launching recurring billing event expansion pipeline for event time range [%s, %s)%s.", + startTime, + endTime, + isDryRun ? " in dry run mode" : advanceCursor ? "" : " without advancing the cursor"); + try { + LaunchFlexTemplateResponse launchResponse = + dataflow + .projects() + .locations() + .flexTemplates() + .launch( + projectId, + jobRegion, + new LaunchFlexTemplateRequest().setLaunchParameter(launchParameter)) + .execute(); + logger.atInfo().log("Got response: %s", launchResponse.getJob().toPrettyString()); + response.setStatus(SC_OK); + response.setPayload( + String.format( + "Launched recurring billing event expansion pipeline: %s", + launchResponse.getJob().getId())); + } catch (IOException e) { + logger.atWarning().withCause(e).log("Pipeline Launch failed"); + response.setStatus(SC_INTERNAL_SERVER_ERROR); + response.setPayload(String.format("Pipeline launch failed: %s", e.getMessage())); } } - - private static int expandBillingEvent( - Recurring recurring, - DateTime executeTime, - DateTime cursorTime, - boolean isDryRun, - DomainPricingLogic domainPricingLogic) { - ImmutableSet.Builder syntheticOneTimesBuilder = new ImmutableSet.Builder<>(); - final Registry tld = Registry.get(getTldFromDomainName(recurring.getTargetId())); - - // Determine the complete set of times at which this recurring event should - // occur (up to and including the runtime of the action). - Iterable eventTimes = - recurring - .getRecurrenceTimeOfYear() - .getInstancesInRange( - Range.closed( - recurring.getEventTime(), - earliestOf(recurring.getRecurrenceEndTime(), executeTime))); - - // Convert these event times to billing times - final ImmutableSet billingTimes = - getBillingTimesInScope(eventTimes, cursorTime, executeTime, tld); - - VKey domainKey = VKey.create(Domain.class, recurring.getDomainRepoId()); - Iterable oneTimesForDomain; - oneTimesForDomain = - tm().createQueryComposer(OneTime.class) - .where("domainRepoId", EQ, recurring.getDomainRepoId()) - .list(); - - // Determine the billing times that already have OneTime events persisted. - ImmutableSet existingBillingTimes = - getExistingBillingTimes(oneTimesForDomain, recurring); - - ImmutableSet.Builder historyEntriesBuilder = new ImmutableSet.Builder<>(); - // Create synthetic OneTime events for all billing times that do not yet have - // an event persisted. - for (DateTime billingTime : difference(billingTimes, existingBillingTimes)) { - // Construct a new HistoryEntry that parents over the OneTime - DomainHistory historyEntry = - new DomainHistory.Builder() - .setBySuperuser(false) - .setRegistrarId(recurring.getRegistrarId()) - .setModificationTime(tm().getTransactionTime()) - .setDomain(tm().loadByKey(domainKey)) - .setPeriod(Period.create(1, YEARS)) - .setReason("Domain autorenewal by ExpandRecurringBillingEventsAction") - .setRequestedByRegistrar(false) - .setType(DOMAIN_AUTORENEW) - // Note: the following statement seems to not be entirely correct as manual renewal - // during the autorenew grace period also closes out the existing recurrence, but in - // that instance the autorenew history entry should still have the transaction records - // for obvious reasons. It can be argued the history entry should always have the - // transaction record, regardless of what happens afterward. If the domain is deleted - // later during the autorenew grace period, another history entry for the delete would - // record that mutation separately, but the previous autorenew should not have its - // history entry retroactively altered, or in this case have the transaction records - // omitted when its created belatedly (when billing time is in scope). However, since - // we will be rewriting this action and only want to do the absolute minimum change to - // fix it for now, we will leave the current logic in place to avoid any unnecessary - // complications. - // - // Don't write a domain transaction record if the recurrence was - // ended prior to the billing time (i.e. a domain was deleted - // during the autorenew grace period). - .setDomainTransactionRecords( - recurring.getRecurrenceEndTime().isBefore(billingTime) - ? ImmutableSet.of() - : ImmutableSet.of( - DomainTransactionRecord.create( - tld.getTldStr(), - // We report this when the autorenew grace period - // ends - billingTime, - TransactionReportField.netRenewsFieldFromYears(1), - 1))) - .build(); - historyEntriesBuilder.add(historyEntry); - - DateTime eventTime = billingTime.minus(tld.getAutoRenewGracePeriodLength()); - - syntheticOneTimesBuilder.add( - new OneTime.Builder() - .setBillingTime(billingTime) - .setRegistrarId(recurring.getRegistrarId()) - // Determine the cost for a one-year renewal. - .setCost( - domainPricingLogic - .getRenewPrice(tld, recurring.getTargetId(), eventTime, 1, recurring) - .getRenewCost()) - .setEventTime(eventTime) - .setFlags(union(recurring.getFlags(), Flag.SYNTHETIC)) - .setDomainHistory(historyEntry) - .setPeriodYears(1) - .setReason(recurring.getReason()) - .setSyntheticCreationTime(executeTime) - .setCancellationMatchingBillingEvent(recurring) - .setTargetId(recurring.getTargetId()) - .build()); - } - Set historyEntries = historyEntriesBuilder.build(); - Set syntheticOneTimes = syntheticOneTimesBuilder.build(); - if (!isDryRun) { - ImmutableSet entitiesToSave = - new ImmutableSet.Builder() - .addAll(historyEntries) - .addAll(syntheticOneTimes) - .build(); - tm().putAll(entitiesToSave); - } - return syntheticOneTimes.size(); - } - - /** - * Filters a set of {@link DateTime}s down to event times that are in scope for a particular - * action run, given the cursor time and the action execution time. - */ - protected static ImmutableSet getBillingTimesInScope( - Iterable eventTimes, - DateTime cursorTime, - DateTime executeTime, - final Registry tld) { - return Streams.stream(eventTimes) - .map(eventTime -> eventTime.plus(tld.getAutoRenewGracePeriodLength())) - .filter(Range.closedOpen(cursorTime, executeTime)) - .collect(toImmutableSet()); - } - - /** - * Determines an {@link ImmutableSet} of {@link DateTime}s that have already been persisted for a - * given recurring billing event. - */ - private static ImmutableSet getExistingBillingTimes( - Iterable oneTimesForDomain, - final BillingEvent.Recurring recurringEvent) { - return Streams.stream(oneTimesForDomain) - .filter( - billingEvent -> - recurringEvent - .createVKey() - .equals(billingEvent.getCancellationMatchingBillingEvent())) - .map(OneTime::getBillingTime) - .collect(toImmutableSet()); - } } diff --git a/core/src/main/java/google/registry/beam/billing/ExpandRecurringBillingEventsPipeline.java b/core/src/main/java/google/registry/beam/billing/ExpandRecurringBillingEventsPipeline.java index 6418e0177..327ab7f33 100644 --- a/core/src/main/java/google/registry/beam/billing/ExpandRecurringBillingEventsPipeline.java +++ b/core/src/main/java/google/registry/beam/billing/ExpandRecurringBillingEventsPipeline.java @@ -148,7 +148,7 @@ public class ExpandRecurringBillingEventsPipeline implements Serializable { endTime = DateTime.parse(options.getEndTime()); checkArgument( !endTime.isAfter(clock.nowUtc()), - String.format("End time %s must be on or before now.", endTime)); + String.format("End time %s must be at or before now.", endTime)); checkArgument( startTime.isBefore(endTime), String.format("[%s, %s) is not a valid window of operation.", startTime, endTime)); diff --git a/core/src/main/java/google/registry/env/alpha/default/WEB-INF/cron.xml b/core/src/main/java/google/registry/env/alpha/default/WEB-INF/cron.xml index 4295c253e..9f5c720f5 100644 --- a/core/src/main/java/google/registry/env/alpha/default/WEB-INF/cron.xml +++ b/core/src/main/java/google/registry/env/alpha/default/WEB-INF/cron.xml @@ -89,7 +89,7 @@ - + This job runs an action that creates synthetic OneTime billing events from Recurring billing events. Events are created for all instances of Recurring billing events that should exist diff --git a/core/src/main/java/google/registry/env/production/default/WEB-INF/cron.xml b/core/src/main/java/google/registry/env/production/default/WEB-INF/cron.xml index 37f83415f..ffcb25c6e 100644 --- a/core/src/main/java/google/registry/env/production/default/WEB-INF/cron.xml +++ b/core/src/main/java/google/registry/env/production/default/WEB-INF/cron.xml @@ -132,7 +132,7 @@ - + This job runs an action that creates synthetic OneTime billing events from Recurring billing events. Events are created for all instances of Recurring billing events that should exist diff --git a/core/src/main/java/google/registry/env/sandbox/default/WEB-INF/cron.xml b/core/src/main/java/google/registry/env/sandbox/default/WEB-INF/cron.xml index 5becbb1c5..1ca4ae6d9 100644 --- a/core/src/main/java/google/registry/env/sandbox/default/WEB-INF/cron.xml +++ b/core/src/main/java/google/registry/env/sandbox/default/WEB-INF/cron.xml @@ -107,7 +107,7 @@ - + This job runs an action that creates synthetic OneTime billing events from Recurring billing events. Events are created for all instances of Recurring billing events that should exist diff --git a/core/src/main/java/google/registry/reporting/billing/GenerateInvoicesAction.java b/core/src/main/java/google/registry/reporting/billing/GenerateInvoicesAction.java index a2f66cb26..7528b762a 100644 --- a/core/src/main/java/google/registry/reporting/billing/GenerateInvoicesAction.java +++ b/core/src/main/java/google/registry/reporting/billing/GenerateInvoicesAction.java @@ -15,10 +15,7 @@ package google.registry.reporting.billing; import static google.registry.beam.BeamUtils.createJobName; -import static google.registry.model.common.Cursor.CursorType.RECURRING_BILLING; -import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.request.Action.Method.POST; -import static google.registry.util.DateTimeUtils.START_OF_TIME; import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; import static javax.servlet.http.HttpServletResponse.SC_OK; @@ -32,7 +29,6 @@ import com.google.common.flogger.FluentLogger; import com.google.common.net.MediaType; import google.registry.config.RegistryConfig.Config; import google.registry.config.RegistryEnvironment; -import google.registry.model.common.Cursor; import google.registry.persistence.PersistenceModule; import google.registry.reporting.ReportingModule; import google.registry.request.Action; @@ -44,7 +40,6 @@ import google.registry.util.Clock; import google.registry.util.CloudTasksUtils; import java.io.IOException; import javax.inject.Inject; -import org.joda.time.DateTime; import org.joda.time.Duration; import org.joda.time.YearMonth; @@ -113,19 +108,6 @@ public class GenerateInvoicesAction implements Runnable { response.setContentType(MediaType.PLAIN_TEXT_UTF_8); logger.atInfo().log("Launching invoicing pipeline for %s.", yearMonth); try { - DateTime currentCursorTime = - tm().transact( - () -> - tm().loadByKeyIfPresent(Cursor.createGlobalVKey(RECURRING_BILLING)) - .orElse(Cursor.createGlobal(RECURRING_BILLING, START_OF_TIME)) - .getCursorTime()); - - if (!YearMonth.fromDateFields(currentCursorTime.toDate()).isAfter(yearMonth)) { - throw new IllegalStateException( - "Latest billing events expansion cycle hasn't finished yet, terminating invoicing" - + " pipeline"); - } - LaunchFlexTemplateParameter parameter = new LaunchFlexTemplateParameter() .setJobName(createJobName("invoicing", clock)) diff --git a/core/src/test/java/google/registry/batch/ExpandRecurringBillingEventsActionTest.java b/core/src/test/java/google/registry/batch/ExpandRecurringBillingEventsActionTest.java index f45a0114e..1b63afd21 100644 --- a/core/src/test/java/google/registry/batch/ExpandRecurringBillingEventsActionTest.java +++ b/core/src/test/java/google/registry/batch/ExpandRecurringBillingEventsActionTest.java @@ -15,1130 +15,169 @@ package google.registry.batch; import static com.google.common.truth.Truth.assertThat; -import static google.registry.model.billing.BillingEvent.RenewalPriceBehavior.NONPREMIUM; -import static google.registry.model.billing.BillingEvent.RenewalPriceBehavior.SPECIFIED; -import static google.registry.model.common.Cursor.CursorType.RECURRING_BILLING; -import static google.registry.model.domain.Period.Unit.YEARS; -import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_AUTORENEW; -import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_CREATE; import static google.registry.persistence.transaction.TransactionManagerFactory.tm; -import static google.registry.testing.DatabaseHelper.assertBillingEventsForResource; -import static google.registry.testing.DatabaseHelper.createTld; -import static google.registry.testing.DatabaseHelper.getHistoryEntriesOfType; -import static google.registry.testing.DatabaseHelper.getOnlyHistoryEntryOfType; -import static google.registry.testing.DatabaseHelper.persistDeletedDomain; -import static google.registry.testing.DatabaseHelper.persistPremiumList; -import static google.registry.testing.DatabaseHelper.persistResource; -import static google.registry.util.DateTimeUtils.END_OF_TIME; -import static google.registry.util.DateTimeUtils.START_OF_TIME; -import static org.joda.money.CurrencyUnit.USD; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.ImmutableSortedMap; -import com.google.common.collect.Iterables; -import google.registry.flows.custom.DomainPricingCustomLogic; -import google.registry.flows.domain.DomainPricingLogic; -import google.registry.model.billing.BillingEvent; -import google.registry.model.billing.BillingEvent.Flag; -import google.registry.model.billing.BillingEvent.OneTime; -import google.registry.model.billing.BillingEvent.Reason; +import com.google.api.services.dataflow.model.LaunchFlexTemplateRequest; +import google.registry.beam.BeamActionTestBase; import google.registry.model.common.Cursor; -import google.registry.model.domain.Domain; -import google.registry.model.domain.DomainHistory; -import google.registry.model.domain.Period; -import google.registry.model.reporting.DomainTransactionRecord; -import google.registry.model.reporting.DomainTransactionRecord.TransactionReportField; -import google.registry.model.reporting.HistoryEntry; -import google.registry.model.tld.Registry; +import google.registry.model.common.Cursor.CursorType; import google.registry.persistence.transaction.JpaTestExtensions; import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension; -import google.registry.testing.DatabaseHelper; import google.registry.testing.FakeClock; -import google.registry.testing.FakeResponse; -import java.util.ArrayList; -import java.util.List; +import java.io.IOException; +import java.util.HashMap; import java.util.Optional; -import org.joda.money.Money; import org.joda.time.DateTime; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.ArgumentCaptor; /** Unit tests for {@link ExpandRecurringBillingEventsAction}. */ -public class ExpandRecurringBillingEventsActionTest { +public class ExpandRecurringBillingEventsActionTest extends BeamActionTestBase { + + private final DateTime cursorTime = DateTime.parse("2020-02-01T00:00:00Z"); + private final DateTime now = DateTime.parse("2020-02-02T00:00:00Z"); + + private final FakeClock clock = new FakeClock(now); + private final ExpandRecurringBillingEventsAction action = + new ExpandRecurringBillingEventsAction(); + private final HashMap expectedParameters = new HashMap<>(); + + private final ArgumentCaptor launchRequest = + ArgumentCaptor.forClass(LaunchFlexTemplateRequest.class); @RegisterExtension final JpaIntegrationTestExtension jpa = - new JpaTestExtensions.Builder().buildIntegrationTestExtension(); - - private DateTime currentTestTime = DateTime.parse("1999-01-05T00:00:00Z"); - private final FakeClock clock = new FakeClock(currentTestTime); - - private ExpandRecurringBillingEventsAction action; - private Domain domain; - private DomainHistory historyEntry; - private BillingEvent.Recurring recurring; + new JpaTestExtensions.Builder().withClock(clock).buildIntegrationTestExtension(); @BeforeEach - void beforeEach() { - action = new ExpandRecurringBillingEventsAction(); + void before() { action.clock = clock; - action.cursorTimeParam = Optional.empty(); - action.batchSize = 2; - action.domainPricingLogic = - new DomainPricingLogic(new DomainPricingCustomLogic(null, null, null)); - createTld("tld"); - domain = - persistResource( - DatabaseHelper.newDomain("example.tld") - .asBuilder() - .setCreationTimeForTest(DateTime.parse("1999-01-05T00:00:00Z")) - .build()); - historyEntry = - persistResource( - new DomainHistory.Builder() - .setRegistrarId(domain.getCreationRegistrarId()) - .setType(HistoryEntry.Type.DOMAIN_CREATE) - .setModificationTime(DateTime.parse("1999-01-05T00:00:00Z")) - .setDomain(domain) - .build()); - recurring = - new BillingEvent.Recurring.Builder() - .setDomainHistory(historyEntry) - .setRegistrarId(domain.getCreationRegistrarId()) - .setEventTime(DateTime.parse("2000-01-05T00:00:00Z")) - .setFlags(ImmutableSet.of(Flag.AUTO_RENEW)) - .setId(2L) - .setReason(Reason.RENEW) - .setRecurrenceEndTime(END_OF_TIME) - .setTargetId(domain.getDomainName()) - .build(); - currentTestTime = clock.nowUtc(); - clock.setTo(DateTime.parse("2000-10-02T00:00:00Z")); + action.isDryRun = false; + action.advanceCursor = true; + action.startTimeParam = Optional.empty(); + action.endTimeParam = Optional.empty(); + action.projectId = "projectId"; + action.jobRegion = "jobRegion"; + action.stagingBucketUrl = "test-bucket"; + action.dataflow = dataflow; + action.response = response; + expectedParameters.put("registryEnvironment", "UNITTEST"); + expectedParameters.put("startTime", "2020-02-01T00:00:00.000Z"); + expectedParameters.put("endTime", "2020-02-02T00:00:00.000Z"); + expectedParameters.put("isDryRun", "false"); + expectedParameters.put("advanceCursor", "true"); + tm().transact(() -> tm().put(Cursor.createGlobal(CursorType.RECURRING_BILLING, cursorTime))); } - private void saveCursor(final DateTime cursorTime) { - tm().transact(() -> tm().put(Cursor.createGlobal(RECURRING_BILLING, cursorTime))); - } - - private void runAction() throws Exception { - action.response = new FakeResponse(); + @Test + void testSuccess() throws Exception { action.run(); - // Need to save the current test time before running the action, which increments the clock. - // The execution time (e.g. transaction time) is captured when the action starts running so - // the passage of time afterward does not affect the timestamp stored in the billing events. - currentTestTime = clock.nowUtc(); - } - - private void assertCursorAt(DateTime expectedCursorTime) { - Cursor cursor = tm().transact(() -> tm().loadByKey(Cursor.createGlobalVKey(RECURRING_BILLING))); - assertThat(cursor).isNotNull(); - assertThat(cursor.getCursorTime()).isEqualTo(expectedCursorTime); - } - - private void assertHistoryEntryMatches( - Domain domain, - DomainHistory actual, - String registrarId, - DateTime billingTime, - boolean shouldHaveTxRecord) { - assertThat(actual.getBySuperuser()).isFalse(); - assertThat(actual.getRegistrarId()).isEqualTo(registrarId); - assertThat(actual.getRepoId()).isEqualTo(domain.getRepoId()); - assertThat(actual.getPeriod()).isEqualTo(Period.create(1, YEARS)); - assertThat(actual.getReason()) - .isEqualTo("Domain autorenewal by ExpandRecurringBillingEventsAction"); - assertThat(actual.getRequestedByRegistrar()).isFalse(); - assertThat(actual.getType()).isEqualTo(DOMAIN_AUTORENEW); - if (shouldHaveTxRecord) { - assertThat(actual.getDomainTransactionRecords()) - .containsExactly( - DomainTransactionRecord.create( - "tld", billingTime, TransactionReportField.NET_RENEWS_1_YR, 1)); - } else { - assertThat(actual.getDomainTransactionRecords()).isEmpty(); - } - } - - private OneTime.Builder defaultOneTimeBuilder() { - return new BillingEvent.OneTime.Builder() - .setBillingTime(DateTime.parse("2000-02-19T00:00:00Z")) - .setRegistrarId("TheRegistrar") - .setCost(Money.of(USD, 11)) - .setEventTime(DateTime.parse("2000-01-05T00:00:00Z")) - .setFlags(ImmutableSet.of(Flag.AUTO_RENEW, Flag.SYNTHETIC)) - .setPeriodYears(1) - .setReason(Reason.RENEW) - .setSyntheticCreationTime(currentTestTime) - .setCancellationMatchingBillingEvent(recurring) - .setTargetId(domain.getDomainName()); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getPayload()) + .isEqualTo("Launched recurring billing event expansion pipeline: jobid"); + verify(templates, times(1)).launch(eq("projectId"), eq("jobRegion"), launchRequest.capture()); + assertThat(launchRequest.getValue().getLaunchParameter().getParameters()) + .containsExactlyEntriesIn(expectedParameters); } @Test - void testSuccess_expandSingleEvent() throws Exception { - persistResource(recurring); - action.cursorTimeParam = Optional.of(START_OF_TIME); - runAction(); - DomainHistory persistedEntry = - getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class); - assertHistoryEntryMatches( - domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"), true); - BillingEvent.OneTime expected = - defaultOneTimeBuilder().setDomainHistory(persistedEntry).build(); - assertBillingEventsForResource(domain, expected, recurring); - assertCursorAt(currentTestTime); + void testSuccess_provideEndTime() throws Exception { + action.endTimeParam = Optional.of(DateTime.parse("2020-02-01T12:00:00.001Z")); + expectedParameters.put("endTime", "2020-02-01T12:00:00.001Z"); + action.run(); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getPayload()) + .isEqualTo("Launched recurring billing event expansion pipeline: jobid"); + verify(templates, times(1)).launch(eq("projectId"), eq("jobRegion"), launchRequest.capture()); + assertThat(launchRequest.getValue().getLaunchParameter().getParameters()) + .containsExactlyEntriesIn(expectedParameters); } @Test - void testSuccess_expandSingleEvent_deletedDomain() throws Exception { - DateTime deletionTime = DateTime.parse("2000-08-01T00:00:00Z"); - Domain deletedDomain = persistDeletedDomain("deleted.tld", deletionTime); - historyEntry = - persistResource( - new DomainHistory.Builder() - .setDomain(deletedDomain) - .setRegistrarId(deletedDomain.getCreationRegistrarId()) - .setModificationTime(deletedDomain.getCreationTime()) - .setType(DOMAIN_CREATE) - .build()); - recurring = - persistResource( - new BillingEvent.Recurring.Builder() - .setDomainHistory(historyEntry) - .setRegistrarId(deletedDomain.getCreationRegistrarId()) - .setEventTime(DateTime.parse("2000-01-05T00:00:00Z")) - .setFlags(ImmutableSet.of(Flag.AUTO_RENEW)) - .setId(2L) - .setReason(Reason.RENEW) - .setRecurrenceEndTime(deletionTime) - .setTargetId(deletedDomain.getDomainName()) - .build()); - action.cursorTimeParam = Optional.of(START_OF_TIME); - runAction(); - DomainHistory persistedEntry = - getOnlyHistoryEntryOfType(deletedDomain, DOMAIN_AUTORENEW, DomainHistory.class); - assertHistoryEntryMatches( - deletedDomain, - persistedEntry, - "TheRegistrar", - DateTime.parse("2000-02-19T00:00:00Z"), - true); - BillingEvent.OneTime expected = - defaultOneTimeBuilder() - .setDomainHistory(persistedEntry) - .setTargetId(deletedDomain.getDomainName()) - .build(); - assertBillingEventsForResource(deletedDomain, expected, recurring); - assertCursorAt(currentTestTime); + void testSuccess_provideStartTime() throws Exception { + action.startTimeParam = Optional.of(DateTime.parse("2020-01-01T12:00:00.001Z")); + expectedParameters.put("startTime", "2020-01-01T12:00:00.001Z"); + action.run(); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getPayload()) + .isEqualTo("Launched recurring billing event expansion pipeline: jobid"); + verify(templates, times(1)).launch(eq("projectId"), eq("jobRegion"), launchRequest.capture()); + assertThat(launchRequest.getValue().getLaunchParameter().getParameters()) + .containsExactlyEntriesIn(expectedParameters); } @Test - void testSuccess_expandSingleEvent_idempotentForDuplicateRuns() throws Exception { - persistResource(recurring); - action.cursorTimeParam = Optional.of(START_OF_TIME); - runAction(); - DomainHistory persistedEntry = - getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class); - assertHistoryEntryMatches( - domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"), true); - BillingEvent.OneTime expected = - defaultOneTimeBuilder().setDomainHistory(persistedEntry).build(); - assertCursorAt(currentTestTime); - DateTime beginningOfSecondRun = clock.nowUtc(); - action.response = new FakeResponse(); - runAction(); - assertCursorAt(beginningOfSecondRun); - assertBillingEventsForResource(domain, expected, recurring); + void testSuccess_doesNotAdvanceCursor() throws Exception { + action.advanceCursor = false; + expectedParameters.put("advanceCursor", "false"); + action.run(); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getPayload()) + .isEqualTo("Launched recurring billing event expansion pipeline: jobid"); + verify(templates, times(1)).launch(eq("projectId"), eq("jobRegion"), launchRequest.capture()); + assertThat(launchRequest.getValue().getLaunchParameter().getParameters()) + .containsExactlyEntriesIn(expectedParameters); } @Test - void testSuccess_expandSingleEvent_idempotentForExistingOneTime() throws Exception { - persistResource(recurring); - BillingEvent.OneTime persisted = - persistResource(defaultOneTimeBuilder().setDomainHistory(historyEntry).build()); - action.cursorTimeParam = Optional.of(START_OF_TIME); - runAction(); - // No new history entries should be generated - assertThat(getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW)).isEmpty(); - assertCursorAt(currentTestTime); - // No additional billing events should be generated - assertBillingEventsForResource(domain, persisted, recurring); - } - - @Test - void testSuccess_expandSingleEvent_notIdempotentForDifferentBillingTime() throws Exception { - persistResource(recurring); - action.cursorTimeParam = Optional.of(START_OF_TIME); - runAction(); - DomainHistory persistedEntry = - getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class); - assertHistoryEntryMatches( - domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"), true); - BillingEvent.OneTime expected = - defaultOneTimeBuilder().setDomainHistory(persistedEntry).build(); - // Persist an otherwise identical billing event that differs only in billing time (and ID). - BillingEvent.OneTime persisted = - persistResource( - expected - .asBuilder() - .setId(15891L) - .setBillingTime(DateTime.parse("1999-02-19T00:00:00Z")) - .setEventTime(DateTime.parse("1999-01-05T00:00:00Z")) - .build()); - assertCursorAt(currentTestTime); - assertBillingEventsForResource(domain, persisted, expected, recurring); - } - - @Test - void testSuccess_expandSingleEvent_notIdempotentForDifferentRecurring() throws Exception { - persistResource(recurring); - BillingEvent.Recurring recurring2 = persistResource(recurring.asBuilder().setId(3L).build()); - action.cursorTimeParam = Optional.of(START_OF_TIME); - runAction(); - List persistedEntries = - getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class); - for (DomainHistory persistedEntry : persistedEntries) { - assertHistoryEntryMatches( - domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"), true); - } - assertThat(persistedEntries).hasSize(2); - BillingEvent.OneTime expected = - defaultOneTimeBuilder().setDomainHistory(persistedEntries.get(0)).build(); - // Persist an otherwise identical billing event that differs only in recurring event key. - BillingEvent.OneTime persisted = - expected - .asBuilder() - .setDomainHistory(persistedEntries.get(1)) - .setCancellationMatchingBillingEvent(recurring2) - .build(); - assertCursorAt(currentTestTime); - assertBillingEventsForResource(domain, persisted, expected, recurring, recurring2); - } - - @Test - void testSuccess_ignoreRecurringBeforeWindow() throws Exception { - recurring = - persistResource( - recurring - .asBuilder() - .setEventTime(DateTime.parse("1997-01-05T00:00:00Z")) - .setRecurrenceEndTime(DateTime.parse("1999-10-05T00:00:00Z")) - .build()); - action.cursorTimeParam = Optional.of(DateTime.parse("2000-01-01T00:00:00Z")); - runAction(); - // No new history entries should be generated - assertThat(getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW)).isEmpty(); - assertBillingEventsForResource(domain, recurring); - assertCursorAt(currentTestTime); - } - - @Test - void testSuccess_ignoreRecurringAfterWindow() throws Exception { - recurring = - persistResource(recurring.asBuilder().setEventTime(clock.nowUtc().plusYears(2)).build()); - action.cursorTimeParam = Optional.of(START_OF_TIME); - runAction(); - // No new history entries should be generated - assertThat(getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW)).isEmpty(); - assertBillingEventsForResource(domain, recurring); - } - - @Test - void testSuccess_expandSingleEvent_billingTimeAtCursorTime() throws Exception { - persistResource(recurring); - action.cursorTimeParam = Optional.of(DateTime.parse("2000-02-19T00:00:00Z")); - runAction(); - DomainHistory persistedEntry = - getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class); - assertHistoryEntryMatches( - domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"), true); - BillingEvent.OneTime expected = - defaultOneTimeBuilder().setDomainHistory(persistedEntry).build(); - assertBillingEventsForResource(domain, expected, recurring); - assertCursorAt(currentTestTime); - } - - @Test - void testSuccess_expandSingleEvent_cursorTimeBetweenEventAndBillingTime() throws Exception { - persistResource(recurring); - action.cursorTimeParam = Optional.of(DateTime.parse("2000-01-12T00:00:00Z")); - runAction(); - DomainHistory persistedEntry = - getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class); - assertHistoryEntryMatches( - domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"), true); - BillingEvent.OneTime expected = - defaultOneTimeBuilder().setDomainHistory(persistedEntry).build(); - assertBillingEventsForResource(domain, expected, recurring); - assertCursorAt(currentTestTime); - } - - @Test - void testSuccess_expandSingleEvent_billingTimeAtExecutionTime() throws Exception { - clock.setTo(currentTestTime); - persistResource(recurring); - action.cursorTimeParam = Optional.of(START_OF_TIME); - clock.setTo(DateTime.parse("2000-02-19T00:00:00Z")); - runAction(); - // No new history entries should be generated - assertThat(getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW)).isEmpty(); - // A candidate billing event is set to be billed exactly on 2/19/00 @ 00:00, - // but these should not be generated as the interval is closed on cursorTime, open on - // executeTime. - assertBillingEventsForResource(domain, recurring); - assertCursorAt(currentTestTime); - } - - @Test - void testSuccess_expandSingleEvent_multipleYearCreate() throws Exception { - action.cursorTimeParam = Optional.of(recurring.getEventTime()); - recurring = - persistResource( - recurring.asBuilder().setEventTime(recurring.getEventTime().plusYears(2)).build()); - clock.setTo(DateTime.parse("2002-10-02T00:00:00Z")); - runAction(); - DomainHistory persistedEntry = - getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class); - assertHistoryEntryMatches( - domain, persistedEntry, "TheRegistrar", DateTime.parse("2002-02-19T00:00:00Z"), true); - BillingEvent.OneTime expected = - defaultOneTimeBuilder() - .setBillingTime(DateTime.parse("2002-02-19T00:00:00Z")) - .setEventTime(DateTime.parse("2002-01-05T00:00:00Z")) - .setDomainHistory(persistedEntry) - .build(); - assertBillingEventsForResource(domain, expected, recurring); - assertCursorAt(currentTestTime); - } - - @Test - void testSuccess_expandSingleEvent_withCursor() throws Exception { - persistResource(recurring); - saveCursor(START_OF_TIME); - runAction(); - DomainHistory persistedEntry = - getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class); - assertHistoryEntryMatches( - domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"), true); - BillingEvent.OneTime expected = - defaultOneTimeBuilder().setDomainHistory(persistedEntry).build(); - assertBillingEventsForResource(domain, expected, recurring); - assertCursorAt(currentTestTime); - } - - @Test - void testSuccess_expandSingleEvent_withCursorPastExpected() throws Exception { - persistResource(recurring); - // Simulate a quick second run of the action (this should be a no-op). - saveCursor(clock.nowUtc().minusSeconds(1)); - runAction(); - // No new history entries should be generated - assertThat(getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW)).isEmpty(); - assertBillingEventsForResource(domain, recurring); - assertCursorAt(currentTestTime); - } - - @Test - void testSuccess_expandSingleEvent_recurrenceEndBeforeEvent() throws Exception { - // This can occur when a domain is transferred or deleted before a domain comes up for renewal. - recurring = - persistResource( - recurring - .asBuilder() - .setRecurrenceEndTime(recurring.getEventTime().minusDays(5)) - .build()); - action.cursorTimeParam = Optional.of(START_OF_TIME); - runAction(); - // No new history entries should be generated - assertThat(getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW)).isEmpty(); - assertBillingEventsForResource(domain, recurring); - assertCursorAt(currentTestTime); - } - - @Test - void testSuccess_noEventExpanded_recurrenceEndAfterEvent_cursorTimeTooLate() throws Exception { - // This can occur when a domain is transferred/renewed/deleted during autorenew grace peroid. - recurring = - persistResource( - recurring - .asBuilder() - .setRecurrenceEndTime(recurring.getEventTime().plusDays(5)) - .build()); - action.cursorTimeParam = Optional.of(recurring.getRecurrenceEndTime().plusDays(46)); - runAction(); - // No new history entries should be generated because cursor time is more than 45 days after - // recurrence end time. - assertThat(getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW)).isEmpty(); - assertBillingEventsForResource(domain, recurring); - assertCursorAt(currentTestTime); - } - - @Test - void testSuccess_expandSingleEvent_recurrenceEndAfterEvent() throws Exception { - // This can occur when a domain is transferred/renewed/deleted during autorenew grace peroid. - recurring = - persistResource( - recurring - .asBuilder() - .setRecurrenceEndTime(recurring.getEventTime().plusDays(5)) - .build()); - action.cursorTimeParam = Optional.of(recurring.getRecurrenceEndTime().plusDays(35)); - runAction(); - DomainHistory persistedEntry = - getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class); - assertHistoryEntryMatches( - domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"), false); - BillingEvent.OneTime expected = - defaultOneTimeBuilder().setDomainHistory(persistedEntry).build(); - assertBillingEventsForResource(domain, expected, recurring); - assertCursorAt(currentTestTime); - } - - @Test - void testSuccess_expandSingleEvent_dryRun() throws Exception { - persistResource(recurring); + void testSuccess_dryRun() throws Exception { action.isDryRun = true; - saveCursor(START_OF_TIME); // Need a saved cursor to verify that it didn't move. - runAction(); - // No new history entries should be generated - assertThat(getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW)).isEmpty(); - assertBillingEventsForResource(domain, recurring); - assertCursorAt(START_OF_TIME); // Cursor doesn't move on a dry run. + action.advanceCursor = false; + expectedParameters.put("isDryRun", "true"); + expectedParameters.put("advanceCursor", "false"); + action.run(); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getPayload()) + .isEqualTo("Launched recurring billing event expansion pipeline: jobid"); + verify(templates, times(1)).launch(eq("projectId"), eq("jobRegion"), launchRequest.capture()); + assertThat(launchRequest.getValue().getLaunchParameter().getParameters()) + .containsExactlyEntriesIn(expectedParameters); } @Test - void testSuccess_expandSingleEvent_multipleYears() throws Exception { - clock.setTo(clock.nowUtc().plusYears(5)); - List expectedEvents = new ArrayList<>(); - expectedEvents.add(persistResource(recurring)); - action.cursorTimeParam = Optional.of(START_OF_TIME); - runAction(); - List persistedEntries = - getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class); - assertThat(persistedEntries).hasSize(6); - DateTime eventDate = DateTime.parse("2000-01-05T00:00:00Z"); - DateTime billingDate = DateTime.parse("2000-02-19T00:00:00Z"); - // Expecting events for '00, '01, '02, '03, '04, '05. - for (int year = 0; year < 6; year++) { - assertHistoryEntryMatches( - domain, persistedEntries.get(year), "TheRegistrar", billingDate.plusYears(year), true); - expectedEvents.add( - defaultOneTimeBuilder() - .setBillingTime(billingDate.plusYears(year)) - .setEventTime(eventDate.plusYears(year)) - .setDomainHistory(persistedEntries.get(year)) - .build()); - } - assertBillingEventsForResource(domain, Iterables.toArray(expectedEvents, BillingEvent.class)); - assertCursorAt(currentTestTime); + void testFailure_endTimeAfterNow() throws Exception { + action.endTimeParam = Optional.of(DateTime.parse("2020-02-03T00:00:00Z")); + IllegalArgumentException thrown = + assertThrows(IllegalArgumentException.class, () -> action.run()); + assertThat(thrown.getMessage()).contains("must be at or before now"); + verifyNoInteractions(templates); } @Test - void testSuccess_expandSingleEvent_multipleYears_cursorInBetweenYears() throws Exception { - clock.setTo(clock.nowUtc().plusYears(5)); - List expectedEvents = new ArrayList<>(); - expectedEvents.add(persistResource(recurring)); - saveCursor(DateTime.parse("2003-10-02T00:00:00Z")); - runAction(); - List persistedEntries = - getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class); - assertThat(persistedEntries).hasSize(2); - DateTime eventDate = DateTime.parse("2004-01-05T00:00:00Z"); - DateTime billingDate = DateTime.parse("2004-02-19T00:00:00Z"); - // Only expect the last two years' worth of billing events. - for (int year = 0; year < 2; year++) { - assertHistoryEntryMatches( - domain, persistedEntries.get(year), "TheRegistrar", billingDate.plusYears(year), true); - expectedEvents.add( - defaultOneTimeBuilder() - .setBillingTime(billingDate.plusYears(year)) - .setDomainHistory(persistedEntries.get(year)) - .setEventTime(eventDate.plusYears(year)) - .build()); - } - assertBillingEventsForResource(domain, Iterables.toArray(expectedEvents, BillingEvent.class)); - assertCursorAt(currentTestTime); + void testFailure_startTimeAfterEndTime() throws Exception { + action.startTimeParam = Optional.of(DateTime.parse("2020-02-03T00:00:00Z")); + IllegalArgumentException thrown = + assertThrows(IllegalArgumentException.class, () -> action.run()); + assertThat(thrown.getMessage()).contains("must be before end time"); + verifyNoInteractions(templates); } @Test - void testSuccess_singleEvent_beforeRenewal() throws Exception { - // Need to restore to the time before the clock was advanced so that the commit log's timestamp - // is not inverted when the clock is later reverted. - clock.setTo(currentTestTime); - persistResource(recurring); - clock.setTo(DateTime.parse("2000-01-04T00:00:00Z")); - action.cursorTimeParam = Optional.of(START_OF_TIME); - runAction(); - // No new history entries should be generated - assertThat(getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW)).isEmpty(); - assertBillingEventsForResource(domain, recurring); - assertCursorAt(currentTestTime); + void testFailure_AdvanceCursorInDryRun() throws Exception { + action.isDryRun = true; + action.advanceCursor = true; + IllegalArgumentException thrown = + assertThrows(IllegalArgumentException.class, () -> action.run()); + assertThat(thrown.getMessage()).contains("Cannot advance the cursor in a dry run"); + verifyNoInteractions(templates); } @Test - void testSuccess_singleEvent_afterRecurrenceEnd_inAutorenewGracePeriod() throws Exception { - // The domain creation date is 1999-01-05, and the first renewal date is thus 2000-01-05. - clock.setTo(DateTime.parse("2001-02-06T00:00:00Z")); - recurring = - persistResource( - recurring - .asBuilder() - // The domain deletion date is 2000-01-29, which is within the 45 day autorenew - // grace period - // from the renewal date. - .setRecurrenceEndTime(DateTime.parse("2000-01-29T00:00:00Z")) - .setEventTime(domain.getCreationTime().plusYears(1)) - .build()); - action.cursorTimeParam = Optional.of(START_OF_TIME); - runAction(); - DomainHistory persistedEntry = - getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class); - assertHistoryEntryMatches( - domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"), false); - BillingEvent.OneTime expected = - defaultOneTimeBuilder() - .setBillingTime(DateTime.parse("2000-02-19T00:00:00Z")) - .setDomainHistory(persistedEntry) - .build(); - assertBillingEventsForResource(domain, recurring, expected); - assertCursorAt(currentTestTime); - } - - @Test - void testSuccess_singleEvent_afterRecurrenceEnd_outsideAutorenewGracePeriod() throws Exception { - // The domain creation date is 1999-01-05, and the first renewal date is thus 2000-01-05. - clock.setTo(DateTime.parse("2001-02-06T00:00:00Z")); - recurring = - persistResource( - recurring - .asBuilder() - // The domain deletion date is 2000-04-05, which is not within the 45 day autorenew - // grace - // period from the renewal date. - .setRecurrenceEndTime(DateTime.parse("2000-04-05T00:00:00Z")) - .setEventTime(domain.getCreationTime().plusYears(1)) - .build()); - action.cursorTimeParam = Optional.of(START_OF_TIME); - runAction(); - DomainHistory persistedEntry = - getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class); - assertHistoryEntryMatches( - domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"), true); - BillingEvent.OneTime expected = - defaultOneTimeBuilder() - .setBillingTime(DateTime.parse("2000-02-19T00:00:00Z")) - .setDomainHistory(persistedEntry) - .build(); - assertBillingEventsForResource(domain, recurring, expected); - assertCursorAt(currentTestTime); - } - - @Test - void testSuccess_expandSingleEvent_billingTimeOnLeapYear() throws Exception { - recurring = - persistResource( - recurring.asBuilder().setEventTime(DateTime.parse("2000-01-15T00:00:00Z")).build()); - action.cursorTimeParam = Optional.of(START_OF_TIME); - runAction(); - DomainHistory persistedEntry = - getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class); - assertHistoryEntryMatches( - domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-29T00:00:00Z"), true); - BillingEvent.OneTime expected = - defaultOneTimeBuilder() - .setBillingTime(DateTime.parse("2000-02-29T00:00:00Z")) - .setEventTime(DateTime.parse("2000-01-15T00:00:00Z")) - .setDomainHistory(persistedEntry) - .build(); - assertBillingEventsForResource(domain, expected, recurring); - assertCursorAt(currentTestTime); - } - - @Test - void testSuccess_expandSingleEvent_billingTimeNotOnLeapYear() throws Exception { - recurring = - persistResource( - recurring.asBuilder().setEventTime(DateTime.parse("2001-01-15T00:00:00Z")).build()); - action.cursorTimeParam = Optional.of(START_OF_TIME); - clock.setTo(DateTime.parse("2001-12-01T00:00:00Z")); - runAction(); - DomainHistory persistedEntry = - getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class); - assertHistoryEntryMatches( - domain, persistedEntry, "TheRegistrar", DateTime.parse("2001-03-01T00:00:00Z"), true); - BillingEvent.OneTime expected = - defaultOneTimeBuilder() - .setBillingTime(DateTime.parse("2001-03-01T00:00:00Z")) - .setEventTime(DateTime.parse("2001-01-15T00:00:00Z")) - .setDomainHistory(persistedEntry) - .build(); - assertBillingEventsForResource(domain, expected, recurring); - assertCursorAt(currentTestTime); - } - - @Test - void testSuccess_expandMultipleEvents() throws Exception { - persistResource(recurring); - Domain domain2 = - persistResource( - DatabaseHelper.newDomain("example2.tld") - .asBuilder() - .setCreationTimeForTest(DateTime.parse("1999-04-05T00:00:00Z")) - .build()); - DomainHistory historyEntry2 = - persistResource( - new DomainHistory.Builder() - .setRegistrarId(domain2.getCreationRegistrarId()) - .setType(HistoryEntry.Type.DOMAIN_CREATE) - .setModificationTime(DateTime.parse("1999-04-05T00:00:00Z")) - .setDomain(domain2) - .build()); - BillingEvent.Recurring recurring2 = - persistResource( - new BillingEvent.Recurring.Builder() - .setDomainHistory(historyEntry2) - .setRegistrarId(domain2.getCreationRegistrarId()) - .setEventTime(DateTime.parse("2000-04-05T00:00:00Z")) - .setFlags(ImmutableSet.of(Flag.AUTO_RENEW)) - .setReason(Reason.RENEW) - .setRecurrenceEndTime(END_OF_TIME) - .setTargetId(domain2.getDomainName()) - .build()); - Domain domain3 = - persistResource( - DatabaseHelper.newDomain("example3.tld") - .asBuilder() - .setCreationTimeForTest(DateTime.parse("1999-06-05T00:00:00Z")) - .build()); - DomainHistory historyEntry3 = - persistResource( - new DomainHistory.Builder() - .setRegistrarId(domain3.getCreationRegistrarId()) - .setType(HistoryEntry.Type.DOMAIN_CREATE) - .setModificationTime(DateTime.parse("1999-06-05T00:00:00Z")) - .setDomain(domain3) - .build()); - BillingEvent.Recurring recurring3 = - persistResource( - new BillingEvent.Recurring.Builder() - .setDomainHistory(historyEntry3) - .setRegistrarId(domain3.getCreationRegistrarId()) - .setEventTime(DateTime.parse("2000-06-05T00:00:00Z")) - .setFlags(ImmutableSet.of(Flag.AUTO_RENEW)) - .setReason(Reason.RENEW) - .setRecurrenceEndTime(END_OF_TIME) - .setTargetId(domain3.getDomainName()) - .build()); - action.cursorTimeParam = Optional.of(START_OF_TIME); - runAction(); - - DomainHistory persistedHistory1 = - getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class); - assertHistoryEntryMatches( - domain, persistedHistory1, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"), true); - BillingEvent.OneTime expected = - defaultOneTimeBuilder() - .setDomainHistory(persistedHistory1) - .setCancellationMatchingBillingEvent(recurring) - .build(); - DomainHistory persistedHistory2 = - getOnlyHistoryEntryOfType(domain2, DOMAIN_AUTORENEW, DomainHistory.class); - assertHistoryEntryMatches( - domain2, persistedHistory2, "TheRegistrar", DateTime.parse("2000-05-20T00:00:00Z"), true); - BillingEvent.OneTime expected2 = - defaultOneTimeBuilder() - .setBillingTime(DateTime.parse("2000-05-20T00:00:00Z")) - .setEventTime(DateTime.parse("2000-04-05T00:00:00Z")) - .setDomainHistory(persistedHistory2) - .setTargetId(domain2.getDomainName()) - .setCancellationMatchingBillingEvent(recurring2) - .build(); - DomainHistory persistedHistory3 = - getOnlyHistoryEntryOfType(domain3, DOMAIN_AUTORENEW, DomainHistory.class); - assertHistoryEntryMatches( - domain3, persistedHistory3, "TheRegistrar", DateTime.parse("2000-07-20T00:00:00Z"), true); - BillingEvent.OneTime expected3 = - defaultOneTimeBuilder() - .setBillingTime(DateTime.parse("2000-07-20T00:00:00Z")) - .setEventTime(DateTime.parse("2000-06-05T00:00:00Z")) - .setTargetId(domain3.getDomainName()) - .setDomainHistory(persistedHistory3) - .setCancellationMatchingBillingEvent(recurring3) - .build(); - assertBillingEventsForResource(domain, expected, recurring); - assertBillingEventsForResource(domain2, expected2, recurring2); - assertBillingEventsForResource(domain3, expected3, recurring3); - assertCursorAt(currentTestTime); - } - - @Test - void testSuccess_expandMultipleEvents_anchorTenant() throws Exception { - persistResource( - Registry.get("tld") - .asBuilder() - .setPremiumList(persistPremiumList("tld2", USD, "example,USD 100")) - .build()); - recurring = persistResource(recurring.asBuilder().setRenewalPriceBehavior(NONPREMIUM).build()); - BillingEvent.Recurring recurring2 = - persistResource( - recurring - .asBuilder() - .setEventTime(recurring.getEventTime().plusMonths(3)) - .setId(3L) - .build()); - action.cursorTimeParam = Optional.of(START_OF_TIME); - runAction(); - List persistedEntries = - getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class); - assertThat(persistedEntries).hasSize(2); - assertHistoryEntryMatches( - domain, - persistedEntries.get(0), - "TheRegistrar", - DateTime.parse("2000-02-19T00:00:00Z"), - true); - BillingEvent.OneTime expected = - defaultOneTimeBuilder() - .setDomainHistory(persistedEntries.get(0)) - .setCancellationMatchingBillingEvent(recurring) - .build(); - assertHistoryEntryMatches( - domain, - persistedEntries.get(1), - "TheRegistrar", - DateTime.parse("2000-05-20T00:00:00Z"), - true); - BillingEvent.OneTime expected2 = - defaultOneTimeBuilder() - .setBillingTime(DateTime.parse("2000-05-20T00:00:00Z")) - .setEventTime(DateTime.parse("2000-04-05T00:00:00Z")) - .setDomainHistory(persistedEntries.get(1)) - .setCancellationMatchingBillingEvent(recurring2) - .build(); - assertBillingEventsForResource(domain, expected, expected2, recurring, recurring2); - assertCursorAt(currentTestTime); - } - - @Test - void testSuccess_expandMultipleEvents_premiumDomain_internalRegistration() throws Exception { - persistResource( - Registry.get("tld") - .asBuilder() - .setPremiumList(persistPremiumList("tld2", USD, "example,USD 100")) - .build()); - recurring = - persistResource( - recurring - .asBuilder() - .setRenewalPriceBehavior(SPECIFIED) - .setRenewalPrice(Money.of(USD, 4)) - .build()); - BillingEvent.Recurring recurring2 = - persistResource( - recurring - .asBuilder() - .setEventTime(recurring.getEventTime().plusMonths(3)) - .setId(3L) - .setRenewalPriceBehavior(SPECIFIED) - .setRenewalPrice(Money.of(USD, 4)) - .build()); - action.cursorTimeParam = Optional.of(START_OF_TIME); - runAction(); - List persistedEntries = - getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class); - assertThat(persistedEntries).hasSize(2); - assertHistoryEntryMatches( - domain, - persistedEntries.get(0), - "TheRegistrar", - DateTime.parse("2000-02-19T00:00:00Z"), - true); - BillingEvent.OneTime expected = - defaultOneTimeBuilder() - .setCost(Money.of(USD, 4)) - .setDomainHistory(persistedEntries.get(0)) - .setCancellationMatchingBillingEvent(recurring) - .build(); - assertHistoryEntryMatches( - domain, - persistedEntries.get(1), - "TheRegistrar", - DateTime.parse("2000-05-20T00:00:00Z"), - true); - BillingEvent.OneTime expected2 = - defaultOneTimeBuilder() - .setCost(Money.of(USD, 4)) - .setBillingTime(DateTime.parse("2000-05-20T00:00:00Z")) - .setEventTime(DateTime.parse("2000-04-05T00:00:00Z")) - .setDomainHistory(persistedEntries.get(1)) - .setCancellationMatchingBillingEvent(recurring2) - .build(); - assertBillingEventsForResource(domain, expected, expected2, recurring, recurring2); - assertCursorAt(currentTestTime); - } - - @Test - void testSuccess_premiumDomain() throws Exception { - persistResource( - Registry.get("tld") - .asBuilder() - .setPremiumList(persistPremiumList("tld2", USD, "example,USD 100")) - .build()); - persistResource(recurring); - action.cursorTimeParam = Optional.of(START_OF_TIME); - runAction(); - DomainHistory persistedEntry = - getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class); - assertHistoryEntryMatches( - domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"), true); - BillingEvent.OneTime expected = - defaultOneTimeBuilder() - .setDomainHistory(persistedEntry) - .setCost(Money.of(USD, 100)) - .build(); - assertBillingEventsForResource(domain, expected, recurring); - assertCursorAt(currentTestTime); - } - - @Test - void testSuccess_premiumDomain_forAnchorTenant() throws Exception { - persistResource( - Registry.get("tld") - .asBuilder() - .setPremiumList(persistPremiumList("tld2", USD, "example,USD 100")) - .build()); - recurring = persistResource(recurring.asBuilder().setRenewalPriceBehavior(NONPREMIUM).build()); - action.cursorTimeParam = Optional.of(START_OF_TIME); - runAction(); - DomainHistory persistedEntry = - getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class); - assertHistoryEntryMatches( - domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"), true); - BillingEvent.OneTime expected = - defaultOneTimeBuilder().setDomainHistory(persistedEntry).setCost(Money.of(USD, 11)).build(); - assertBillingEventsForResource(domain, expected, recurring); - assertCursorAt(currentTestTime); - } - - @Test - void testSuccess_standardDomain_forAnchorTenant() throws Exception { - recurring = persistResource(recurring.asBuilder().setRenewalPriceBehavior(NONPREMIUM).build()); - action.cursorTimeParam = Optional.of(START_OF_TIME); - runAction(); - DomainHistory persistedEntry = - getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class); - assertHistoryEntryMatches( - domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"), true); - BillingEvent.OneTime expected = - defaultOneTimeBuilder().setDomainHistory(persistedEntry).build(); - assertBillingEventsForResource(domain, expected, recurring); - assertCursorAt(currentTestTime); - } - - @Test - void testSuccess_premiumDomain_forInternalRegistration() throws Exception { - persistResource( - Registry.get("tld") - .asBuilder() - .setPremiumList(persistPremiumList("tld2", USD, "example,USD 100")) - .build()); - recurring = - persistResource( - recurring - .asBuilder() - .setRenewalPriceBehavior(SPECIFIED) - .setRenewalPrice(Money.of(USD, 20)) - .build()); - action.cursorTimeParam = Optional.of(START_OF_TIME); - runAction(); - DomainHistory persistedEntry = - getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class); - assertHistoryEntryMatches( - domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"), true); - BillingEvent.OneTime expected = - defaultOneTimeBuilder().setDomainHistory(persistedEntry).setCost(Money.of(USD, 20)).build(); - assertBillingEventsForResource(domain, expected, recurring); - assertCursorAt(currentTestTime); - } - - @Test - void testSuccess_standardDomain_forInternalRegistration() throws Exception { - recurring = - persistResource( - recurring - .asBuilder() - .setRenewalPriceBehavior(SPECIFIED) - .setRenewalPrice(Money.of(USD, 2)) - .build()); - action.cursorTimeParam = Optional.of(START_OF_TIME); - runAction(); - DomainHistory persistedEntry = - getOnlyHistoryEntryOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class); - assertHistoryEntryMatches( - domain, persistedEntry, "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"), true); - BillingEvent.OneTime expected = - defaultOneTimeBuilder().setDomainHistory(persistedEntry).setCost(Money.of(USD, 2)).build(); - assertBillingEventsForResource(domain, expected, recurring); - assertCursorAt(currentTestTime); - } - - @Test - void testSuccess_varyingRenewPrices() throws Exception { - clock.setTo(currentTestTime); - persistResource( - Registry.get("tld") - .asBuilder() - .setRenewBillingCostTransitions( - ImmutableSortedMap.of( - START_OF_TIME, - Money.of(USD, 8), - DateTime.parse("2000-06-01T00:00:00Z"), - Money.of(USD, 10))) - .build()); - clock.setTo(DateTime.parse("2001-10-02T00:00:00Z")); - persistResource(recurring); - action.cursorTimeParam = Optional.of(START_OF_TIME); - runAction(); - List persistedEntries = - getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class); - assertThat(persistedEntries).hasSize(2); - DateTime eventDate = DateTime.parse("2000-01-05T00:00:00Z"); - DateTime billingDate = DateTime.parse("2000-02-19T00:00:00Z"); - assertHistoryEntryMatches(domain, persistedEntries.get(0), "TheRegistrar", billingDate, true); - BillingEvent.OneTime cheaper = - defaultOneTimeBuilder() - .setBillingTime(billingDate) - .setEventTime(eventDate) - .setDomainHistory(persistedEntries.get(0)) - .setCost(Money.of(USD, 8)) - .build(); - assertHistoryEntryMatches( - domain, persistedEntries.get(1), "TheRegistrar", billingDate.plusYears(1), true); - BillingEvent.OneTime expensive = - cheaper - .asBuilder() - .setCost(Money.of(USD, 10)) - .setBillingTime(billingDate.plusYears(1)) - .setEventTime(eventDate.plusYears(1)) - .setDomainHistory(persistedEntries.get(1)) - .build(); - assertBillingEventsForResource(domain, recurring, cheaper, expensive); - assertCursorAt(currentTestTime); - } - - @Test - void testSuccess_varyingRenewPrices_anchorTenant() throws Exception { - clock.setTo(currentTestTime); - persistResource( - Registry.get("tld") - .asBuilder() - .setPremiumList(persistPremiumList("tld2", USD, "example,USD 100")) - .setRenewBillingCostTransitions( - ImmutableSortedMap.of( - START_OF_TIME, - Money.of(USD, 8), - DateTime.parse("2000-06-01T00:00:00Z"), - Money.of(USD, 10))) - .build()); - clock.setTo(DateTime.parse("2001-10-02T00:00:00Z")); - recurring = persistResource(recurring.asBuilder().setRenewalPriceBehavior(NONPREMIUM).build()); - action.cursorTimeParam = Optional.of(START_OF_TIME); - runAction(); - List persistedEntries = - getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class); - assertThat(persistedEntries).hasSize(2); - DateTime eventDate = DateTime.parse("2000-01-05T00:00:00Z"); - DateTime billingDate = DateTime.parse("2000-02-19T00:00:00Z"); - assertHistoryEntryMatches(domain, persistedEntries.get(0), "TheRegistrar", billingDate, true); - BillingEvent.OneTime cheaper = - defaultOneTimeBuilder() - .setBillingTime(billingDate) - .setEventTime(eventDate) - .setDomainHistory(persistedEntries.get(0)) - .setCost(Money.of(USD, 8)) - .build(); - assertHistoryEntryMatches( - domain, persistedEntries.get(1), "TheRegistrar", billingDate.plusYears(1), true); - BillingEvent.OneTime expensive = - cheaper - .asBuilder() - .setCost(Money.of(USD, 10)) - .setBillingTime(billingDate.plusYears(1)) - .setEventTime(eventDate.plusYears(1)) - .setDomainHistory(persistedEntries.get(1)) - .build(); - assertBillingEventsForResource(domain, recurring, cheaper, expensive); - assertCursorAt(currentTestTime); - } - - @Test - void testSuccess_varyingRenewPrices_internalRegistration() throws Exception { - clock.setTo(currentTestTime); - persistResource( - Registry.get("tld") - .asBuilder() - .setPremiumList(persistPremiumList("tld2", USD, "example,USD 100")) - .setRenewBillingCostTransitions( - ImmutableSortedMap.of( - START_OF_TIME, - Money.of(USD, 8), - DateTime.parse("2000-06-01T00:00:00Z"), - Money.of(USD, 10))) - .build()); - clock.setTo(DateTime.parse("2001-10-02T00:00:00Z")); - recurring = - persistResource( - recurring - .asBuilder() - .setRenewalPriceBehavior(SPECIFIED) - .setRenewalPrice(Money.of(USD, 5)) - .build()); - action.cursorTimeParam = Optional.of(START_OF_TIME); - runAction(); - List persistedEntries = - getHistoryEntriesOfType(domain, DOMAIN_AUTORENEW, DomainHistory.class); - assertThat(persistedEntries).hasSize(2); - DateTime eventDate = DateTime.parse("2000-01-05T00:00:00Z"); - DateTime billingDate = DateTime.parse("2000-02-19T00:00:00Z"); - assertHistoryEntryMatches(domain, persistedEntries.get(0), "TheRegistrar", billingDate, true); - BillingEvent.OneTime cheaper = - defaultOneTimeBuilder() - .setBillingTime(billingDate) - .setEventTime(eventDate) - .setDomainHistory(persistedEntries.get(0)) - .setCost(Money.of(USD, 5)) - .build(); - assertHistoryEntryMatches( - domain, persistedEntries.get(1), "TheRegistrar", billingDate.plusYears(1), true); - BillingEvent.OneTime expensive = - cheaper - .asBuilder() - .setCost(Money.of(USD, 5)) - .setBillingTime(billingDate.plusYears(1)) - .setEventTime(eventDate.plusYears(1)) - .setDomainHistory(persistedEntries.get(1)) - .build(); - assertBillingEventsForResource(domain, recurring, cheaper, expensive); - assertCursorAt(currentTestTime); - } - - @Test - void testFailure_cursorAfterExecutionTime() { - action.cursorTimeParam = Optional.of(clock.nowUtc().plusYears(1)); - IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, this::runAction); - assertThat(thrown) - .hasMessageThat() - .contains("Cursor time must be earlier than execution time."); - } - - @Test - void testFailure_cursorAtExecutionTime() { - // The clock advances one milli on run. - action.cursorTimeParam = Optional.of(clock.nowUtc().plusMillis(1)); - IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, this::runAction); - assertThat(thrown) - .hasMessageThat() - .contains("Cursor time must be earlier than execution time."); + void testFailure_launchError() throws Exception { + when(launch.execute()).thenThrow(new IOException("cannot launch")); + action.run(); + assertThat(response.getStatus()).isEqualTo(500); + assertThat(response.getPayload()).isEqualTo("Pipeline launch failed: cannot launch"); + verify(templates, times(1)).launch(eq("projectId"), eq("jobRegion"), launchRequest.capture()); + assertThat(launchRequest.getValue().getLaunchParameter().getParameters()) + .containsExactlyEntriesIn(expectedParameters); } } diff --git a/core/src/test/java/google/registry/beam/billing/ExpandRecurringBillingEventsPipelineTest.java b/core/src/test/java/google/registry/beam/billing/ExpandRecurringBillingEventsPipelineTest.java index e1a6018f7..b53213849 100644 --- a/core/src/test/java/google/registry/beam/billing/ExpandRecurringBillingEventsPipelineTest.java +++ b/core/src/test/java/google/registry/beam/billing/ExpandRecurringBillingEventsPipelineTest.java @@ -126,7 +126,7 @@ public class ExpandRecurringBillingEventsPipelineTest { assertThrows(IllegalArgumentException.class, this::runPipeline); assertThat(thrown) .hasMessageThat() - .contains("End time 2021-02-02T00:00:05.001Z must be on or before now"); + .contains("End time 2021-02-02T00:00:05.001Z must be at or before now"); } @Test diff --git a/core/src/test/java/google/registry/reporting/billing/GenerateInvoicesActionTest.java b/core/src/test/java/google/registry/reporting/billing/GenerateInvoicesActionTest.java index 476ff8499..41c241028 100644 --- a/core/src/test/java/google/registry/reporting/billing/GenerateInvoicesActionTest.java +++ b/core/src/test/java/google/registry/reporting/billing/GenerateInvoicesActionTest.java @@ -15,8 +15,6 @@ package google.registry.reporting.billing; import static com.google.common.truth.Truth.assertThat; -import static google.registry.model.common.Cursor.CursorType.RECURRING_BILLING; -import static google.registry.testing.DatabaseHelper.persistResource; import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; import static javax.servlet.http.HttpServletResponse.SC_OK; import static org.mockito.Mockito.mock; @@ -26,7 +24,6 @@ import static org.mockito.Mockito.when; import com.google.cloud.tasks.v2.HttpMethod; import com.google.common.net.MediaType; import google.registry.beam.BeamActionTestBase; -import google.registry.model.common.Cursor; import google.registry.persistence.transaction.JpaTestExtensions; import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension; import google.registry.reporting.ReportingModule; @@ -35,10 +32,8 @@ import google.registry.testing.CloudTasksHelper.TaskMatcher; import google.registry.testing.FakeClock; import google.registry.util.CloudTasksUtils; import java.io.IOException; -import org.joda.time.DateTime; import org.joda.time.Duration; import org.joda.time.YearMonth; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -55,13 +50,6 @@ class GenerateInvoicesActionTest extends BeamActionTestBase { private CloudTasksUtils cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils(); private GenerateInvoicesAction action; - @BeforeEach - @Override - protected void beforeEach() throws Exception { - super.beforeEach(); - persistResource(Cursor.createGlobal(RECURRING_BILLING, DateTime.parse("2017-11-30TZ"))); - } - @Test void testLaunchTemplateJob_withPublish() throws Exception { action = @@ -142,55 +130,4 @@ class GenerateInvoicesActionTest extends BeamActionTestBase { verify(emailUtils).sendAlertEmail("Pipeline Launch failed due to Pipeline error"); cloudTasksHelper.assertNoTasksEnqueued("beam-reporting"); } - - @Test - void testFailsToGenerateInvoicesNotExpandedBillingEvents() throws Exception { - persistResource(Cursor.createGlobal(RECURRING_BILLING, DateTime.parse("2017-10-30TZ"))); - action = - new GenerateInvoicesAction( - "test-project", - "test-region", - "staging_bucket", - "billing_bucket", - "REG-INV", - false, - new YearMonth(2017, 10), - emailUtils, - cloudTasksUtils, - clock, - response, - dataflow); - action.run(); - assertThat(response.getContentType()).isEqualTo(MediaType.PLAIN_TEXT_UTF_8); - assertThat(response.getStatus()).isEqualTo(SC_INTERNAL_SERVER_ERROR); - assertThat(response.getPayload()) - .isEqualTo( - "Pipeline launch failed: Latest billing events expansion cycle hasn't finished yet," - + " terminating invoicing pipeline"); - cloudTasksHelper.assertNoTasksEnqueued("beam-reporting"); - } - - @Test - void testSucceedsToGenerateInvoicesFirstDayOfTheYear() throws Exception { - persistResource(Cursor.createGlobal(RECURRING_BILLING, DateTime.parse("2017-01-01T13:15:00Z"))); - action = - new GenerateInvoicesAction( - "test-project", - "test-region", - "staging_bucket", - "billing_bucket", - "REG-INV", - false, - new YearMonth(2016, 12), - emailUtils, - cloudTasksUtils, - clock, - response, - dataflow); - action.run(); - assertThat(response.getContentType()).isEqualTo(MediaType.PLAIN_TEXT_UTF_8); - assertThat(response.getStatus()).isEqualTo(SC_OK); - assertThat(response.getPayload()).isEqualTo("Launched invoicing pipeline: jobid"); - cloudTasksHelper.assertNoTasksEnqueued("beam-reporting"); - } }