From f9e0908022b824ccfbe7a41df581508af1c48fe1 Mon Sep 17 00:00:00 2001 From: Pavlo Tkach <3469726+ptkach@users.noreply.github.com> Date: Thu, 25 Jan 2024 14:08:08 -0500 Subject: [PATCH] Replace invoice email attachement with bucket link (#2299) --- .../beam/billing/InvoicingPipeline.java | 7 ++- .../registry/config/RegistryConfig.java | 11 +++++ .../config/RegistryConfigSettings.java | 1 + .../registry/config/files/default-config.yaml | 1 + .../java/google/registry/gcs/GcsUtils.java | 8 +++ .../reporting/billing/BillingEmailUtils.java | 49 ++++++++++--------- .../billing/BillingEmailUtilsTest.java | 25 +++------- 7 files changed, 61 insertions(+), 41 deletions(-) diff --git a/core/src/main/java/google/registry/beam/billing/InvoicingPipeline.java b/core/src/main/java/google/registry/beam/billing/InvoicingPipeline.java index d3a4a7f0d..af199fb08 100644 --- a/core/src/main/java/google/registry/beam/billing/InvoicingPipeline.java +++ b/core/src/main/java/google/registry/beam/billing/InvoicingPipeline.java @@ -163,7 +163,12 @@ public class InvoicingPipeline implements Serializable { } } - /** Saves the billing events to a single overall invoice CSV file. */ + /** + * Saves the billing events to a single overall invoice CSV file. TextIO always produces the file + * of type text/plain, which we then update to desired text/csv before sending an email to billing + * team {@link google.registry.reporting.billing.BillingEmailUtils#emailOverallInvoice() + * emailOverallInvoice} + */ static void saveInvoiceCsv( PCollection billingEvents, InvoicingPipelineOptions options) { diff --git a/core/src/main/java/google/registry/config/RegistryConfig.java b/core/src/main/java/google/registry/config/RegistryConfig.java index 4e8f5af47..f2e5f9e2c 100644 --- a/core/src/main/java/google/registry/config/RegistryConfig.java +++ b/core/src/main/java/google/registry/config/RegistryConfig.java @@ -721,6 +721,17 @@ public final class RegistryConfig { return "gs://" + billingBucket; } + /** + * Returns origin part of the URL of the billing invoice file + * + * @see google.registry.beam.billing.InvoicingPipeline + */ + @Provides + @Config("billingInvoiceOriginUrl") + public static String provideBillingInvoiceOriginUrl(RegistryConfigSettings config) { + return config.billing.billingInvoiceOriginUrl; + } + /** * Returns whether or not we should publish invoices to partners automatically by default. * diff --git a/core/src/main/java/google/registry/config/RegistryConfigSettings.java b/core/src/main/java/google/registry/config/RegistryConfigSettings.java index eeca2d47b..70600e082 100644 --- a/core/src/main/java/google/registry/config/RegistryConfigSettings.java +++ b/core/src/main/java/google/registry/config/RegistryConfigSettings.java @@ -173,6 +173,7 @@ public class RegistryConfigSettings { public List invoiceEmailRecipients; public String invoiceReplyToEmailAddress; public String invoiceFilePrefix; + public String billingInvoiceOriginUrl; } /** Configuration for Registry Data Escrow (RDE). */ diff --git a/core/src/main/java/google/registry/config/files/default-config.yaml b/core/src/main/java/google/registry/config/files/default-config.yaml index c0d6310aa..764b703e0 100644 --- a/core/src/main/java/google/registry/config/files/default-config.yaml +++ b/core/src/main/java/google/registry/config/files/default-config.yaml @@ -381,6 +381,7 @@ billing: # Optional return address that overrides the default. invoiceReplyToEmailAddress: null invoiceFilePrefix: REG-INV + billingInvoiceOriginUrl: https://billing-origin-url/ rde: # URL prefix of ICANN's server to upload RDE reports to. Nomulus adds /TLD/ID diff --git a/core/src/main/java/google/registry/gcs/GcsUtils.java b/core/src/main/java/google/registry/gcs/GcsUtils.java index c3b6d1f27..95d090c83 100644 --- a/core/src/main/java/google/registry/gcs/GcsUtils.java +++ b/core/src/main/java/google/registry/gcs/GcsUtils.java @@ -118,6 +118,14 @@ public class GcsUtils implements Serializable { storage().delete(blobId); } + /** Update file content type on existing GCS file */ + public void updateContentType(BlobId blobId, String contentType) throws StorageException { + if (existsAndNotEmpty(blobId)) { + Blob blob = storage().get(blobId); + blob.toBuilder().setContentType(contentType).build().update(); + } + } + /** * Returns a list of all object names within a bucket for a given prefix. * diff --git a/core/src/main/java/google/registry/reporting/billing/BillingEmailUtils.java b/core/src/main/java/google/registry/reporting/billing/BillingEmailUtils.java index ce88c7be6..05493a82a 100644 --- a/core/src/main/java/google/registry/reporting/billing/BillingEmailUtils.java +++ b/core/src/main/java/google/registry/reporting/billing/BillingEmailUtils.java @@ -15,20 +15,17 @@ package google.registry.reporting.billing; import static com.google.common.base.Throwables.getRootCause; -import static java.nio.charset.StandardCharsets.UTF_8; import com.google.cloud.storage.BlobId; +import com.google.cloud.storage.StorageException; import com.google.common.collect.ImmutableList; -import com.google.common.io.CharStreams; +import com.google.common.flogger.FluentLogger; import com.google.common.net.MediaType; import google.registry.config.RegistryConfig.Config; import google.registry.gcs.GcsUtils; import google.registry.groups.GmailClient; import google.registry.reporting.billing.BillingModule.InvoiceDirectoryPrefix; import google.registry.util.EmailMessage; -import google.registry.util.EmailMessage.Attachment; -import java.io.InputStream; -import java.io.InputStreamReader; import java.util.Optional; import javax.inject.Inject; import javax.mail.internet.InternetAddress; @@ -37,6 +34,7 @@ import org.joda.time.YearMonth; /** Utility functions for sending emails involving monthly invoices. */ public class BillingEmailUtils { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); private final GmailClient gmailClient; private final YearMonth yearMonth; private final InternetAddress outgoingEmailAddress; @@ -46,6 +44,7 @@ public class BillingEmailUtils { private final String billingBucket; private final String invoiceFilePrefix; private final String invoiceDirectoryPrefix; + private final String billingInvoiceOriginUrl; private final GcsUtils gcsUtils; @Inject @@ -58,6 +57,7 @@ public class BillingEmailUtils { @Config("invoiceReplyToEmailAddress") Optional replyToEmailAddress, @Config("billingBucket") String billingBucket, @Config("invoiceFilePrefix") String invoiceFilePrefix, + @Config("billingInvoiceOriginUrl") String billingInvoiceOriginUrl, @InvoiceDirectoryPrefix String invoiceDirectoryPrefix, GcsUtils gcsUtils) { this.gmailClient = gmailClient; @@ -69,31 +69,36 @@ public class BillingEmailUtils { this.billingBucket = billingBucket; this.invoiceFilePrefix = invoiceFilePrefix; this.invoiceDirectoryPrefix = invoiceDirectoryPrefix; + this.billingInvoiceOriginUrl = billingInvoiceOriginUrl; this.gcsUtils = gcsUtils; } /** Sends an e-mail to all expected recipients with an attached overall invoice from GCS. */ - void emailOverallInvoice() { + public void emailOverallInvoice() { try { String invoiceFile = String.format("%s-%s.csv", invoiceFilePrefix, yearMonth); + String fileUrl = billingInvoiceOriginUrl + invoiceDirectoryPrefix + invoiceFile; BlobId invoiceFilename = BlobId.of(billingBucket, invoiceDirectoryPrefix + invoiceFile); - try (InputStream in = gcsUtils.openInputStream(invoiceFilename)) { - gmailClient.sendEmail( - EmailMessage.newBuilder() - .setSubject(String.format("Domain Registry invoice data %s", yearMonth)) - .setBody( - String.format("Attached is the %s invoice for the domain registry.", yearMonth)) - .setFrom(outgoingEmailAddress) - .setRecipients(invoiceEmailRecipients) - .setReplyToEmailAddress(replyToEmailAddress) - .setAttachment( - Attachment.newBuilder() - .setContent(CharStreams.toString(new InputStreamReader(in, UTF_8))) - .setContentType(MediaType.CSV_UTF_8) - .setFilename(invoiceFile) - .build()) - .build()); + try { + gcsUtils.updateContentType(invoiceFilename, "text/csv"); + } catch (StorageException e) { + // We want to continue with email anyway, it just will be less convenient for billing team + // to process the file. + logger.atWarning().withCause(e).log("Failed to update invoice file type"); } + gmailClient.sendEmail( + EmailMessage.newBuilder() + .setSubject(String.format("Domain Registry invoice data %s", yearMonth)) + .setBody( + String.format( + "

Use the following link to download %s invoice for the domain registry -" + + " invoice.

", + yearMonth, fileUrl)) + .setFrom(outgoingEmailAddress) + .setRecipients(invoiceEmailRecipients) + .setReplyToEmailAddress(replyToEmailAddress) + .setContentType(MediaType.HTML_UTF_8) + .build()); } catch (Throwable e) { // Strip one layer, because callWithRetry wraps in a RuntimeException sendAlertEmail( diff --git a/core/src/test/java/google/registry/reporting/billing/BillingEmailUtilsTest.java b/core/src/test/java/google/registry/reporting/billing/BillingEmailUtilsTest.java index 4872c5edc..f6895196d 100644 --- a/core/src/test/java/google/registry/reporting/billing/BillingEmailUtilsTest.java +++ b/core/src/test/java/google/registry/reporting/billing/BillingEmailUtilsTest.java @@ -21,17 +21,12 @@ import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import com.google.cloud.storage.BlobId; import com.google.common.collect.ImmutableList; import com.google.common.net.MediaType; import google.registry.gcs.GcsUtils; import google.registry.groups.GmailClient; import google.registry.util.EmailMessage; -import google.registry.util.EmailMessage.Attachment; -import java.io.ByteArrayInputStream; -import java.nio.charset.StandardCharsets; import java.util.Optional; import javax.mail.MessagingException; import javax.mail.internet.InternetAddress; @@ -45,18 +40,14 @@ class BillingEmailUtilsTest { private GmailClient gmailClient; private BillingEmailUtils emailUtils; - private GcsUtils gcsUtils; private ArgumentCaptor contentCaptor; + private GcsUtils gcsUtils; @BeforeEach void beforeEach() throws Exception { gmailClient = mock(GmailClient.class); - gcsUtils = mock(GcsUtils.class); - when(gcsUtils.openInputStream(BlobId.of("test-bucket", "results/REG-INV-2017-10.csv"))) - .thenReturn( - new ByteArrayInputStream("test,data\nhello,world".getBytes(StandardCharsets.UTF_8))); contentCaptor = ArgumentCaptor.forClass(EmailMessage.class); - + gcsUtils = mock(GcsUtils.class); emailUtils = getEmailUtils(Optional.of(new InternetAddress("reply-to@test.com"))); } @@ -72,6 +63,7 @@ class BillingEmailUtilsTest { replyToAddress, "test-bucket", "REG-INV", + "www.google.com/", "results/", gcsUtils); } @@ -89,14 +81,11 @@ class BillingEmailUtilsTest { ImmutableList.of( new InternetAddress("hello@world.com"), new InternetAddress("hola@mundo.com"))) .setSubject("Domain Registry invoice data 2017-10") - .setBody("Attached is the 2017-10 invoice for the domain registry.") + .setBody( + "

Use the following link to download 2017-10 invoice for the domain registry -" + + " invoice.

") .setReplyToEmailAddress(new InternetAddress("reply-to@test.com")) - .setAttachment( - Attachment.newBuilder() - .setContent("test,data\nhello,world") - .setContentType(MediaType.CSV_UTF_8) - .setFilename("REG-INV-2017-10.csv") - .build()) + .setContentType(MediaType.HTML_UTF_8) .build(); assertThat(emailMessage).isEqualTo(expectedContent); }