diff --git a/java/google/registry/beam/BUILD b/java/google/registry/beam/BUILD index 757553669..99a1367ff 100644 --- a/java/google/registry/beam/BUILD +++ b/java/google/registry/beam/BUILD @@ -9,6 +9,7 @@ java_library( srcs = glob(["*.java"]), resources = glob(["sql/*"]), deps = [ + "//java/google/registry/billing", "//java/google/registry/config", "//java/google/registry/util", "@com_google_apis_google_api_services_bigquery", diff --git a/java/google/registry/beam/BillingEvent.java b/java/google/registry/beam/BillingEvent.java index 8bcf5d836..73559bec0 100644 --- a/java/google/registry/beam/BillingEvent.java +++ b/java/google/registry/beam/BillingEvent.java @@ -18,6 +18,7 @@ import com.google.auto.value.AutoValue; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; +import google.registry.billing.BillingModule; import google.registry.util.FormattingLogger; import java.io.IOException; import java.io.InputStream; @@ -173,7 +174,8 @@ public abstract class BillingEvent implements Serializable { * filepath with the arguments, such as "../sensitive_info". */ String toFilename(String yearMonth) { - return String.format("invoice_details_%s_%s_%s", yearMonth, registrarId(), tld()); + return String.format( + "%s_%s_%s_%s", BillingModule.DETAIL_REPORT_PREFIX, yearMonth, registrarId(), tld()); } /** Generates a CSV representation of this {@code BillingEvent}. */ diff --git a/java/google/registry/beam/InvoicingPipeline.java b/java/google/registry/beam/InvoicingPipeline.java index 3ec043cdf..9fb3d7df2 100644 --- a/java/google/registry/beam/InvoicingPipeline.java +++ b/java/google/registry/beam/InvoicingPipeline.java @@ -16,6 +16,7 @@ package google.registry.beam; import google.registry.beam.BillingEvent.InvoiceGroupingKey; import google.registry.beam.BillingEvent.InvoiceGroupingKey.InvoiceGroupingKeyCoder; +import google.registry.billing.BillingModule; import google.registry.config.RegistryConfig.Config; import java.io.Serializable; import javax.inject.Inject; @@ -130,7 +131,11 @@ public class InvoicingPipeline implements Serializable { .to( NestedValueProvider.of( yearMonthProvider, - yearMonth -> String.format("%s/results/CRR-INV-%s", beamBucket, yearMonth))) + // TODO(larryruili): Replace with billing bucket after verifying 2017-12 output. + yearMonth -> + String.format( + "%s/results/%s-%s", + beamBucket, BillingModule.OVERALL_INVOICE_PREFIX, yearMonth))) .withHeader(InvoiceGroupingKey.invoiceHeader()) .withoutSharding() .withSuffix(".csv"); @@ -140,6 +145,7 @@ public class InvoicingPipeline implements Serializable { private TextIO.TypedWrite writeDetailReports( ValueProvider yearMonthProvider) { return TextIO.writeCustomType() + // TODO(larryruili): Replace with billing bucket/yyyy-MM after verifying 2017-12 output. .to( InvoicingUtils.makeDestinationFunction(beamBucket + "/results", yearMonthProvider), InvoicingUtils.makeEmptyDestinationParams(beamBucket + "/results")) diff --git a/java/google/registry/beam/InvoicingUtils.java b/java/google/registry/beam/InvoicingUtils.java index ea2abbb59..daa864ed5 100644 --- a/java/google/registry/beam/InvoicingUtils.java +++ b/java/google/registry/beam/InvoicingUtils.java @@ -68,7 +68,7 @@ public class InvoicingUtils { return new Params() .withBaseFilename( FileBasedSink.convertToFileResourceIfPossible( - String.format("%s/%s", outputBucket, "failed"))); + String.format("%s/%s", outputBucket, "FAILURES"))); } /** diff --git a/java/google/registry/billing/BUILD b/java/google/registry/billing/BUILD index a601a7052..121f04a10 100644 --- a/java/google/registry/billing/BUILD +++ b/java/google/registry/billing/BUILD @@ -12,12 +12,16 @@ java_library( ], deps = [ "//java/google/registry/config", + "//java/google/registry/gcs", + "//java/google/registry/model", "//java/google/registry/request", "//java/google/registry/request/auth", + "//java/google/registry/storage/drive", "//java/google/registry/util", "@com_google_api_client_appengine", "@com_google_apis_google_api_services_dataflow", "@com_google_appengine_api_1_0_sdk", + "@com_google_appengine_tools_appengine_gcs_client", "@com_google_dagger", "@com_google_guava", "@com_google_http_client", diff --git a/java/google/registry/billing/BillingEmailUtils.java b/java/google/registry/billing/BillingEmailUtils.java new file mode 100644 index 000000000..347520fcb --- /dev/null +++ b/java/google/registry/billing/BillingEmailUtils.java @@ -0,0 +1,85 @@ +// Copyright 2017 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.billing; + +import com.google.common.collect.ImmutableList; +import google.registry.config.RegistryConfig.Config; +import google.registry.util.FormattingLogger; +import google.registry.util.SendEmailService; +import javax.inject.Inject; +import javax.mail.Message; +import javax.mail.Message.RecipientType; +import javax.mail.MessagingException; +import javax.mail.internet.InternetAddress; +import org.joda.time.YearMonth; + +/** Utility functions for sending emails involving monthly invoices. */ +class BillingEmailUtils { + + private final SendEmailService emailService; + private final YearMonth yearMonth; + private final String alertSenderAddress; + private final ImmutableList invoiceEmailRecipients; + // TODO(larryruili): Replace this bucket after verifying 2017-12 output. + private final String beamBucketUrl; + + @Inject + BillingEmailUtils( + SendEmailService emailService, + YearMonth yearMonth, + @Config("alertSenderEmailAddress") String alertSenderAddress, + @Config("invoiceEmailRecipients") ImmutableList invoiceEmailRecipients, + @Config("apacheBeamBucketUrl") String beamBucketUrl) { + this.emailService = emailService; + this.yearMonth = yearMonth; + this.alertSenderAddress = alertSenderAddress; + this.invoiceEmailRecipients = invoiceEmailRecipients; + this.beamBucketUrl = beamBucketUrl; + } + + private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass(); + + /** + * Sends a link to the generated overall invoice in GCS. + * + *

Note the users receiving the e-mail should have access to the object or bucket, via an + * authorization mechanism such as IAM. + */ + void emailInvoiceLink() { + // TODO(larryruili): Add read permissions for appropriate buckets. + try { + String beamBucket = beamBucketUrl.replaceFirst("gs://", ""); + Message msg = emailService.createMessage(); + msg.setFrom(new InternetAddress(alertSenderAddress)); + for (String recipient : invoiceEmailRecipients) { + msg.addRecipient(RecipientType.TO, new InternetAddress(recipient)); + } + msg.setSubject(String.format("Domain Registry invoice data %s", yearMonth.toString())); + msg.setText( + String.format( + "Link to invoice on GCS:\nhttps://storage.cloud.google.com/%s/%s", + beamBucket, + String.format( + "%s%s-%s.csv", + BillingModule.RESULTS_DIRECTORY_PREFIX, + BillingModule.OVERALL_INVOICE_PREFIX, + yearMonth.toString()))); + emailService.sendMessage(msg); + } catch (MessagingException e) { + // TODO(larryruili): Replace with retrier with final failure email settings. + logger.warning(e, "E-mail service failed due to %s"); + } + } +} diff --git a/java/google/registry/billing/BillingModule.java b/java/google/registry/billing/BillingModule.java index 4f585d181..c15fd9c55 100644 --- a/java/google/registry/billing/BillingModule.java +++ b/java/google/registry/billing/BillingModule.java @@ -33,10 +33,18 @@ import javax.servlet.http.HttpServletRequest; @Module public final class BillingModule { + public static final String DETAIL_REPORT_PREFIX = "invoice_details"; + public static final String OVERALL_INVOICE_PREFIX = "CRR-INV"; + + static final String PARAM_JOB_ID = "jobId"; + static final String PARAM_DIRECTORY_PREFIX = "directoryPrefix"; + static final String BILLING_QUEUE = "billing"; + static final String CRON_QUEUE = "retryable-cron-tasks"; + // TODO(larryruili): Replace with invoices/yyyy-MM after verifying 2017-12 invoice. + static final String RESULTS_DIRECTORY_PREFIX = "results/"; + private static final String CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform"; - static final String BILLING_QUEUE = "billing"; - static final String PARAM_JOB_ID = "jobId"; /** Provides the invoicing Dataflow jobId enqueued by {@link GenerateInvoicesAction}. */ @Provides @@ -45,6 +53,13 @@ public final class BillingModule { return extractRequiredParameter(req, PARAM_JOB_ID); } + /** Provides the subdirectory under a GCS bucket that we copy detail reports from. */ + @Provides + @Parameter(PARAM_DIRECTORY_PREFIX) + static String provideDirectoryPrefix(HttpServletRequest req) { + return extractRequiredParameter(req, PARAM_DIRECTORY_PREFIX); + } + /** Constructs a {@link Dataflow} API client with default settings. */ @Provides static Dataflow provideDataflow( diff --git a/java/google/registry/billing/CopyDetailReportsAction.java b/java/google/registry/billing/CopyDetailReportsAction.java new file mode 100644 index 000000000..33a170779 --- /dev/null +++ b/java/google/registry/billing/CopyDetailReportsAction.java @@ -0,0 +1,129 @@ +// Copyright 2017 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.billing; + +import static google.registry.request.Action.Method.POST; +import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; +import static javax.servlet.http.HttpServletResponse.SC_OK; + +import com.google.appengine.tools.cloudstorage.GcsFilename; +import com.google.common.collect.ImmutableList; +import com.google.common.io.ByteStreams; +import com.google.common.net.MediaType; +import google.registry.config.RegistryConfig.Config; +import google.registry.gcs.GcsUtils; +import google.registry.model.registrar.Registrar; +import google.registry.request.Action; +import google.registry.request.Parameter; +import google.registry.request.Response; +import google.registry.request.auth.Auth; +import google.registry.storage.drive.DriveConnection; +import google.registry.util.FormattingLogger; +import google.registry.util.Retrier; +import java.io.IOException; +import java.io.InputStream; +import java.util.Optional; +import javax.inject.Inject; + +/** Copy all registrar detail reports in a given bucket's subdirectory from GCS to Drive. */ +@Action(path = CopyDetailReportsAction.PATH, method = POST, auth = Auth.AUTH_INTERNAL_OR_ADMIN) +public final class CopyDetailReportsAction implements Runnable { + + public static final String PATH = "/_dr/task/copyDetailReports"; + + private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass(); + + // TODO(larryruili): Replace this bucket with the billing bucket after verifying 2017-12 output. + private final String beamBucketUrl; + private final String folderPrefix; + private final DriveConnection driveConnection; + private final GcsUtils gcsUtils; + private final Retrier retrier; + private final Response response; + + @Inject + CopyDetailReportsAction( + @Config("apacheBeamBucketUrl") String beamBucketUrl, + @Parameter(BillingModule.PARAM_DIRECTORY_PREFIX) String folderPrefix, + DriveConnection driveConnection, + GcsUtils gcsUtils, + Retrier retrier, + Response response) { + this.beamBucketUrl = beamBucketUrl; + this.folderPrefix = folderPrefix; + this.driveConnection = driveConnection; + this.gcsUtils = gcsUtils; + this.retrier = retrier; + this.response = response; + } + + @Override + public void run() { + // Strip the URL prefix from the beam bucket + String beamBucket = beamBucketUrl.replace("gs://", ""); + ImmutableList detailReportObjectNames; + try { + detailReportObjectNames = + gcsUtils + .listFolderObjects(beamBucket, folderPrefix) + .stream() + .filter(objectName -> objectName.startsWith(BillingModule.DETAIL_REPORT_PREFIX)) + .collect(ImmutableList.toImmutableList()); + } catch (IOException e) { + logger.severefmt("Copy failed due to %s", e.getMessage()); + response.setStatus(SC_INTERNAL_SERVER_ERROR); + response.setContentType(MediaType.PLAIN_TEXT_UTF_8); + response.setPayload(String.format("Failure, encountered %s", e.getMessage())); + return; + } + for (String detailReportName : detailReportObjectNames) { + // The standard report format is "invoice_details_yyyy-MM_registrarId_tld.csv + // TODO(larryruili): Determine a safer way of enforcing this. + String registrarId = detailReportName.split("_")[3]; + Optional registrar = Registrar.loadByClientId(registrarId); + // TODO(larryruili): Send an email alert if any report fails to be copied for any reason. + if (!registrar.isPresent()) { + logger.warningfmt( + "Registrar %s not found in database for file %s", registrar, detailReportName); + continue; + } + String driveFolderId = registrar.get().getDriveFolderId(); + if (driveFolderId == null) { + logger.warningfmt("Drive folder id not found for registrar %s", registrarId); + continue; + } + // Attempt to copy each detail report to its associated registrar's drive folder. + retrier.callWithRetry( + () -> { + try (InputStream input = + gcsUtils.openInputStream( + new GcsFilename(beamBucket, folderPrefix + detailReportName))) { + driveConnection.createFile( + detailReportName, + MediaType.CSV_UTF_8, + driveFolderId, + ByteStreams.toByteArray(input)); + logger.infofmt( + "Published detail report for %s to folder %s using GCS file gs://%s/%s.", + registrarId, driveFolderId, beamBucket, detailReportName); + } + }, + IOException.class); + } + response.setStatus(SC_OK); + response.setContentType(MediaType.PLAIN_TEXT_UTF_8); + response.setPayload("Copied detail reports."); + } +} diff --git a/java/google/registry/billing/GenerateInvoicesAction.java b/java/google/registry/billing/GenerateInvoicesAction.java index 8c75b54ed..0bd06ee4d 100644 --- a/java/google/registry/billing/GenerateInvoicesAction.java +++ b/java/google/registry/billing/GenerateInvoicesAction.java @@ -49,23 +49,28 @@ public class GenerateInvoicesAction implements Runnable { private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass(); - @Inject - @Config("projectId") - String projectId; - - @Inject - @Config("apacheBeamBucketUrl") - String beamBucketUrl; - - @Inject YearMonth yearMonth; - @Inject Dataflow dataflow; - @Inject Response response; - - @Inject - GenerateInvoicesAction() {} - static final String PATH = "/_dr/task/generateInvoices"; + private final String projectId; + private final String beamBucketUrl; + private final YearMonth yearMonth; + private final Dataflow dataflow; + private final Response response; + + @Inject + GenerateInvoicesAction( + @Config("projectId") String projectId, + @Config("apacheBeamBucketUrl") String beamBucketUrl, + YearMonth yearMonth, + Dataflow dataflow, + Response response) { + this.projectId = projectId; + this.beamBucketUrl = beamBucketUrl; + this.yearMonth = yearMonth; + this.dataflow = dataflow; + this.response = response; + } + @Override public void run() { logger.infofmt("Launching invoicing pipeline for %s", yearMonth); @@ -87,13 +92,7 @@ public class GenerateInvoicesAction implements Runnable { .execute(); logger.infofmt("Got response: %s", launchResponse.getJob().toPrettyString()); String jobId = launchResponse.getJob().getId(); - TaskOptions uploadTask = - TaskOptions.Builder.withUrl(PublishInvoicesAction.PATH) - .method(TaskOptions.Method.POST) - // Dataflow jobs tend to take about 10 minutes to complete. - .countdownMillis(Duration.standardMinutes(10).getMillis()) - .param(BillingModule.PARAM_JOB_ID, jobId); - QueueFactory.getQueue(BillingModule.BILLING_QUEUE).add(uploadTask); + enqueuePublishTask(jobId); } catch (IOException e) { logger.warningfmt("Template Launch failed due to: %s", e.getMessage()); response.setStatus(SC_INTERNAL_SERVER_ERROR); @@ -105,4 +104,14 @@ public class GenerateInvoicesAction implements Runnable { response.setContentType(MediaType.PLAIN_TEXT_UTF_8); response.setPayload("Launched dataflow template."); } + + private void enqueuePublishTask(String jobId) { + TaskOptions publishTask = + TaskOptions.Builder.withUrl(PublishInvoicesAction.PATH) + .method(TaskOptions.Method.POST) + // Dataflow jobs tend to take about 10 minutes to complete. + .countdownMillis(Duration.standardMinutes(10).getMillis()) + .param(BillingModule.PARAM_JOB_ID, jobId); + QueueFactory.getQueue(BillingModule.BILLING_QUEUE).add(publishTask); + } } diff --git a/java/google/registry/billing/PublishInvoicesAction.java b/java/google/registry/billing/PublishInvoicesAction.java index 0e4b61f06..b3343912e 100644 --- a/java/google/registry/billing/PublishInvoicesAction.java +++ b/java/google/registry/billing/PublishInvoicesAction.java @@ -22,6 +22,8 @@ import static javax.servlet.http.HttpServletResponse.SC_OK; import com.google.api.services.dataflow.Dataflow; import com.google.api.services.dataflow.model.Job; +import com.google.appengine.api.taskqueue.QueueFactory; +import com.google.appengine.api.taskqueue.TaskOptions; import com.google.common.net.MediaType; import google.registry.config.RegistryConfig.Config; import google.registry.request.Action; @@ -48,31 +50,45 @@ public class PublishInvoicesAction implements Runnable { private static final String JOB_DONE = "JOB_STATE_DONE"; private static final String JOB_FAILED = "JOB_STATE_FAILED"; - @Inject @Config("projectId") String projectId; - @Inject @Parameter(BillingModule.PARAM_JOB_ID) String jobId; - @Inject Dataflow dataflow; - @Inject Response response; - @Inject PublishInvoicesAction() {} + private final String projectId; + private final String jobId; + private final BillingEmailUtils emailUtils; + private final Dataflow dataflow; + private final Response response; + + @Inject + PublishInvoicesAction( + @Config("projectId") String projectId, + @Parameter(BillingModule.PARAM_JOB_ID) String jobId, + BillingEmailUtils emailUtils, + Dataflow dataflow, + Response response) { + this.projectId = projectId; + this.jobId = jobId; + this.emailUtils = emailUtils; + this.dataflow = dataflow; + this.response = response; + } static final String PATH = "/_dr/task/publishInvoices"; @Override public void run() { - logger.info("Starting publish job."); try { + logger.info("Starting publish job."); Job job = dataflow.projects().jobs().get(projectId, jobId).execute(); String state = job.getCurrentState(); switch (state) { case JOB_DONE: - logger.infofmt("Dataflow job %s finished successfully.", jobId); + logger.infofmt("Dataflow job %s finished successfully, publishing results.", jobId); response.setStatus(SC_OK); - // TODO(larryruili): Implement upload logic. + enqueueCopyDetailReportsTask(); + emailUtils.emailInvoiceLink(); break; case JOB_FAILED: logger.severefmt("Dataflow job %s finished unsuccessfully.", jobId); - // Return a 'success' code to stop task queue retry. response.setStatus(SC_NO_CONTENT); - // TODO(larryruili): Implement failure response. + // TODO(larryruili): Email failure message break; default: logger.infofmt("Job in non-terminal state %s, retrying:", state); @@ -80,10 +96,18 @@ public class PublishInvoicesAction implements Runnable { break; } } catch (IOException e) { - logger.warningfmt("Template Launch failed due to: %s", e.getMessage()); response.setStatus(SC_INTERNAL_SERVER_ERROR); response.setContentType(MediaType.PLAIN_TEXT_UTF_8); response.setPayload(String.format("Template launch failed: %s", e.getMessage())); } } + + private static void enqueueCopyDetailReportsTask() { + TaskOptions copyDetailTask = + TaskOptions.Builder.withUrl(CopyDetailReportsAction.PATH) + .method(TaskOptions.Method.POST) + .param( + BillingModule.PARAM_DIRECTORY_PREFIX, BillingModule.RESULTS_DIRECTORY_PREFIX); + QueueFactory.getQueue(BillingModule.CRON_QUEUE).add(copyDetailTask); + } } diff --git a/java/google/registry/config/RegistryConfig.java b/java/google/registry/config/RegistryConfig.java index e1751f952..7bce00e41 100644 --- a/java/google/registry/config/RegistryConfig.java +++ b/java/google/registry/config/RegistryConfig.java @@ -509,27 +509,15 @@ public final class RegistryConfig { } /** - * Returns the email address from which we send ICANN reporting email summaries from. + * Returns the list of addresses that receive monthly invoicing emails. * - * @see google.registry.reporting.ReportingEmailUtils + * @see google.registry.billing.BillingEmailUtils */ @Provides - @Config("icannReportingSenderEmailAddress") - public static String provideIcannReportingEmailSenderAddress( - @Config("projectId") String projectId, RegistryConfigSettings config) { - return String.format( - "%s@%s", projectId, config.icannReporting.icannReportingEmailSenderDomain); - } - - /** - * Returns the email address from which we send ICANN reporting email summaries to. - * - * @see google.registry.reporting.ReportingEmailUtils - */ - @Provides - @Config("icannReportingRecipientEmailAddress") - public static String provideIcannReportingEmailRecipientAddress(RegistryConfigSettings config) { - return config.icannReporting.icannReportingEmailRecipient; + @Config("invoiceEmailRecipients") + public static ImmutableList provideInvoiceEmailRecipients( + RegistryConfigSettings config) { + return ImmutableList.copyOf(config.billing.invoiceEmailRecipients); } /** @@ -712,6 +700,35 @@ public final class RegistryConfig { return Optional.ofNullable(config.misc.sheetExportId); } + /** + * Returns the email address we send various alert e-mails to. + * + *

This allows us to easily verify the success or failure of periodic tasks by passively + * checking e-mail. + * + * @see google.registry.reporting.ReportingEmailUtils + * @see google.registry.billing.BillingEmailUtils + */ + @Provides + @Config("alertRecipientEmailAddress") + public static String provideAlertRecipientEmailAddress(RegistryConfigSettings config) { + return config.misc.alertRecipientEmailAddress; + } + + /** + * Returns the email address we send emails from. + * + * @see google.registry.reporting.ReportingEmailUtils + * @see google.registry.billing.BillingEmailUtils + */ + + @Provides + @Config("alertSenderEmailAddress") + public static String provideAlertSenderEmailAddress( + @Config("projectId") String projectId, RegistryConfigSettings config) { + return String.format("%s@%s", projectId, config.misc.alertEmailSenderDomain); + } + /** * Returns SSH client connection and read timeout. * diff --git a/java/google/registry/config/RegistryConfigSettings.java b/java/google/registry/config/RegistryConfigSettings.java index 0a3e5060b..2fbf1008b 100644 --- a/java/google/registry/config/RegistryConfigSettings.java +++ b/java/google/registry/config/RegistryConfigSettings.java @@ -28,6 +28,7 @@ public class RegistryConfigSettings { public CloudDns cloudDns; public Caching caching; public IcannReporting icannReporting; + public Billing billing; public Rde rde; public RegistrarConsole registrarConsole; public Monitoring monitoring; @@ -114,8 +115,11 @@ public class RegistryConfigSettings { public static class IcannReporting { public String icannTransactionsReportingUploadUrl; public String icannActivityReportingUploadUrl; - public String icannReportingEmailSenderDomain; - public String icannReportingEmailRecipient; + } + + /** Configuration for monthly invoices. */ + public static class Billing { + public List invoiceEmailRecipients; } /** Configuration for Registry Data Escrow (RDE). */ @@ -145,6 +149,8 @@ public class RegistryConfigSettings { /** Miscellaneous configuration that doesn't quite fit in anywhere else. */ public static class Misc { public String sheetExportId; + public String alertRecipientEmailAddress; + public String alertEmailSenderDomain; } /** Configuration for Braintree credit card payment processing. */ diff --git a/java/google/registry/config/files/default-config.yaml b/java/google/registry/config/files/default-config.yaml index 63c63d045..336527aac 100644 --- a/java/google/registry/config/files/default-config.yaml +++ b/java/google/registry/config/files/default-config.yaml @@ -160,11 +160,8 @@ icannReporting: # URL we PUT monthly ICANN activity reports to. icannActivityReportingUploadUrl: https://ry-api.icann.org/report/registry-functions-activity - # Domain for the email address we send reporting pipeline summary emails from. - icannReportingEmailSenderDomain: appspotmail.com - - # Address we send reporting pipeline summary emails to. - icannReportingEmailRecipient: email@example.com +billing: + invoiceEmailRecipients: [] rde: # URL prefix of ICANN's server to upload RDE reports to. Nomulus adds /TLD/ID @@ -215,6 +212,12 @@ misc: # to. Leave this null to disable syncing. sheetExportId: null + # Address we send alert summary emails to. + alertRecipientEmailAddress: email@example.com + + # Domain for the email address we send alert summary emails from. + alertEmailSenderDomain: appspotmail.com + # Braintree is a credit card payment processor that is used on the registrar # console to allow registrars to pay their invoices. braintree: diff --git a/java/google/registry/env/common/backend/WEB-INF/web.xml b/java/google/registry/env/common/backend/WEB-INF/web.xml index 44d879c4b..99e856f1b 100644 --- a/java/google/registry/env/common/backend/WEB-INF/web.xml +++ b/java/google/registry/env/common/backend/WEB-INF/web.xml @@ -77,6 +77,14 @@ /_dr/task/publishInvoices + + + backend-servlet + /_dr/task/copyDetailReports + +