From 8088efb25c884d2cb3460623744b2de02f685456 Mon Sep 17 00:00:00 2001 From: Lai Jiang Date: Mon, 23 Jan 2023 19:08:04 -0500 Subject: [PATCH] Update ExpandRecurringBillingEventsAction to use the beam pipeline (#1907) Due to the way the beam pipeline is designed, it will expand an recurring billing event when its event time is in scope for expansion, instead of billing time. This means that the one time will be generated 45 days earlier. This would negate the need to check if the expansion is finished when generating monthly invoices. We will need to backfill the past 45 days of onetimes before the new code is deployed. As an illustration, with the old code, a cursor time of 2023-01-17 means that all auto-renewals whose billing time is before 2023-01-17 were created, which corresponds to an effective cursor time of 2022-12-03 (45 days before 2023-01-17) for event time. This cursor will need to be brought to 2023-01-17 to ensure that there is no gap in generated event times when switching to use the new code. --- .../google/registry/batch/BatchModule.java | 18 +- .../ExpandRecurringBillingEventsAction.java | 419 ++---- .../ExpandRecurringBillingEventsPipeline.java | 2 +- .../env/alpha/default/WEB-INF/cron.xml | 2 +- .../env/production/default/WEB-INF/cron.xml | 2 +- .../env/sandbox/default/WEB-INF/cron.xml | 2 +- .../billing/GenerateInvoicesAction.java | 18 - ...xpandRecurringBillingEventsActionTest.java | 1193 ++--------------- ...andRecurringBillingEventsPipelineTest.java | 2 +- .../billing/GenerateInvoicesActionTest.java | 63 - 10 files changed, 239 insertions(+), 1482 deletions(-) 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"); - } }