diff options
author | Bjørn Christian Seime <bjorncs@vespa.ai> | 2023-10-13 15:44:47 +0200 |
---|---|---|
committer | Bjørn Christian Seime <bjorncs@vespa.ai> | 2023-10-16 10:23:06 +0200 |
commit | ce9556b4cdb280e621f3710b93755469c5e3627a (patch) | |
tree | 3606d206b30e722a402c7893872fd7e438b099ab | |
parent | 4e98a3c331bb2b036c7a64f40fbf4251063294c1 (diff) |
Add prototype email notification for trial account events
3 files changed, 98 insertions, 2 deletions
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java index 6c50afe7fb2..ed8cd3f54f8 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java @@ -7,20 +7,33 @@ import com.yahoo.vespa.flags.ListFlag; import com.yahoo.vespa.flags.PermanentFlags; import com.yahoo.vespa.hosted.controller.Controller; import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId; +import com.yahoo.vespa.hosted.controller.notification.Notification; +import com.yahoo.vespa.hosted.controller.notification.NotificationSource; +import com.yahoo.vespa.hosted.controller.persistence.TrialNotifications; import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo; import com.yahoo.vespa.hosted.controller.tenant.Tenant; import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Optional; import java.util.function.Predicate; +import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; +import static com.yahoo.vespa.hosted.controller.persistence.TrialNotifications.State.EXPIRED; +import static com.yahoo.vespa.hosted.controller.persistence.TrialNotifications.State.EXPIRES_IMMEDIATELY; +import static com.yahoo.vespa.hosted.controller.persistence.TrialNotifications.State.EXPIRES_SOON; +import static com.yahoo.vespa.hosted.controller.persistence.TrialNotifications.State.MID_CHECK_IN; +import static com.yahoo.vespa.hosted.controller.persistence.TrialNotifications.State.SIGNED_UP; +import static com.yahoo.vespa.hosted.controller.persistence.TrialNotifications.State.UNKNOWN; + /** * Expires unused tenants from Vespa Cloud. * <p> - * TODO: Should support sending notifications some time before the various expiry events happen. * * @author ogronnesby */ @@ -40,7 +53,8 @@ public class CloudTrialExpirer extends ControllerMaintainer { protected double maintain() { var a = tombstoneNonePlanTenants(); var b = moveInactiveTenantsToNonePlan(); - return (a ? 0.0 : -0.5) + (b ? 0.0 : -0.5); + var c = notifyTenants(); + return (a ? 0.0 : -(1D/3)) + (b ? 0.0 : -(1D/3) + (c ? 0.0 : -(1D/3))); } private boolean moveInactiveTenantsToNonePlan() { @@ -76,6 +90,84 @@ public class CloudTrialExpirer extends ControllerMaintainer { return tombstoneTenants(idleOldPlanTenants); } + private boolean notifyTenants() { + try { + // TODO Introduce tenant specific feature flag + + var currentStatus = controller().curator().readTrialNotifications() + .map(TrialNotifications::tenants).orElse(List.of()); + log.fine(() -> "Current: %s".formatted(currentStatus)); + var currentStatusByTenant = new HashMap<TenantName, TrialNotifications.Status>(); + currentStatus.forEach(status -> currentStatusByTenant.put(status.tenant(), status)); + var updatedStatus = new ArrayList<TrialNotifications.Status>(); + var now = controller().clock().instant(); + + for (var tenant : controller().tenants().asList()) { + var status = currentStatusByTenant.get(tenant.name()); + var state = status == null ? UNKNOWN : status.state(); + var plan = controller().serviceRegistry().billingController().getPlan(tenant.name()).value(); + var ageInDays = Duration.between(tenant.createdAt(), now).toDays(); + + // TODO Replace stubs with proper email content stored in templates. + if (!List.of("none", "trial").contains(plan)) { + // Ignore tenants that are on a paid plan and skip from inclusion in updated data structure + } else if (status == null && "trial".equals(plan) && ageInDays <= 1) { + updatedStatus.add(updatedStatus(tenant, now, SIGNED_UP)); + queueNotification(tenant, "Welcome to Vespa Cloud", "Welcome to Vespa Cloud", + "Welcome to Vespa Cloud! We hope you will enjoy your trial. " + + "Please reach out to us if you have any questions or feedback."); + } else if ("none".equals(plan) && !List.of(EXPIRED).contains(state)) { + updatedStatus.add(updatedStatus(tenant, now, EXPIRED)); + queueNotification(tenant, "Your Vespa Cloud trial has expired", "Your Vespa Cloud trial has expired", + "Your Vespa Cloud trial has expired. " + + "Please reach out to us if you have any questions or feedback."); + } else if ("trial".equals(plan) && ageInDays >= 13 + && !List.of(EXPIRES_IMMEDIATELY, EXPIRED).contains(state)) { + updatedStatus.add(updatedStatus(tenant, now, EXPIRES_IMMEDIATELY)); + queueNotification(tenant, "Your Vespa Cloud trial expires tomorrow", "Your Vespa Cloud trial expires tomorrow", + "Your Vespa Cloud trial expires tomorrow. " + + "Please reach out to us if you have any questions or feedback."); + } else if ("trial".equals(plan) && ageInDays >= 12 + && !List.of(EXPIRES_SOON, EXPIRES_IMMEDIATELY, EXPIRED).contains(state)) { + updatedStatus.add(updatedStatus(tenant, now, EXPIRES_SOON)); + queueNotification(tenant, "Your Vespa Cloud trial expires in 2 days", "Your Vespa Cloud trial expires in 2 days", + "Your Vespa Cloud trial expires in 2 days. " + + "Please reach out to us if you have any questions or feedback."); + } else if ("trial".equals(plan) && ageInDays >= 7 + && !List.of(MID_CHECK_IN, EXPIRES_SOON, EXPIRES_IMMEDIATELY, EXPIRED).contains(state)) { + updatedStatus.add(updatedStatus(tenant, now, MID_CHECK_IN)); + queueNotification(tenant, "How is your Vespa Cloud trial going?", "How is your Vespa Cloud trial going?", + "How is your Vespa Cloud trial going? " + + "Please reach out to us if you have any questions or feedback."); + } else { + updatedStatus.add(status); + } + } + log.fine(() -> "Updated: %s".formatted(updatedStatus)); + controller().curator().writeTrialNotifications(new TrialNotifications(updatedStatus)); + return true; + } catch (Exception e) { + log.log(Level.WARNING, "Failed to process trial notifications", e); + return false; + } + } + + private void queueNotification(Tenant tenant, String consoleMsg, String emailSubject, String emailMsg) { + var mail = Optional.of(Notification.MailContent.fromTemplate("default-mail-content") + .subject(emailSubject) + .with("mailMessageTemplate", "cloud-trial-notification") + .with("cloudTrialMessage", emailMsg) + .build()); + var source = NotificationSource.from(tenant.name()); + // Remove previous notification to ensure new notification is sent by email + controller().notificationsDb().removeNotification(source, Notification.Type.account); + controller().notificationsDb().setNotification( + source, Notification.Type.account, Notification.Level.info, List.of(consoleMsg), mail); + } + + private static TrialNotifications.Status updatedStatus(Tenant t, Instant i, TrialNotifications.State s) { + return new TrialNotifications.Status(t.name(), s, i); + } private boolean tenantIsCloudTenant(Tenant tenant) { return tenant.type() == Tenant.Type.cloud; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notifier.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notifier.java index c1e1f075552..6468a4c397b 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notifier.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notifier.java @@ -72,6 +72,7 @@ public class Notifier { registerTemplate(repo, "mail"); registerTemplate(repo, "default-mail-content"); registerTemplate(repo, "notification-message"); + registerTemplate(repo, "cloud-trial-notification"); return v; } diff --git a/controller-server/src/main/resources/mail/cloud-trial-notification.vm b/controller-server/src/main/resources/mail/cloud-trial-notification.vm new file mode 100644 index 00000000000..27bc9b1ad1b --- /dev/null +++ b/controller-server/src/main/resources/mail/cloud-trial-notification.vm @@ -0,0 +1,3 @@ +<p> + $esc.html($cloudTrialMessage): +</p>
\ No newline at end of file |