diff --git a/supabase/functions/alerts/alert_types/provided_payment_method.ts b/supabase/functions/alerts/alert_types/provided_payment_method.ts
new file mode 100644
index 0000000000..0487f39c0c
--- /dev/null
+++ b/supabase/functions/alerts/alert_types/provided_payment_method.ts
@@ -0,0 +1,50 @@
+import { AlertRecord, EmailConfig } from "../index.ts";
+import { commonTemplate } from "../template.ts";
+
+interface ProvidedPaymentMethod {
+ // This feels like it should apply to all alert types, and doesn't belong here..
+ recipients: {
+ email: string;
+ full_name: string | null;
+ }[];
+ trial_start: string;
+ trial_end: string;
+ tenant: string;
+ in_trial: boolean;
+ straight_from_free_tier: boolean;
+}
+
+type ProvidedPaymentMethodRecord = AlertRecord<"provided_payment_method", ProvidedPaymentMethod>;
+
+const ProvidedPaymentMethod = (req: ProvidedPaymentMethodRecord): EmailConfig[] => {
+ return req.arguments.recipients.map((recipient) => ({
+ emails: [recipient.email],
+ subject: `Estuary Flow: Payment Method Entered 🎉`,
+ content: commonTemplate(`
+ Dear ${recipient.full_name},
+ Thank you for entering your payment information. ${
+ req.arguments.in_trial
+ ? `After your free trial ends on ${req.arguments.trial_end}, your account will begin accruing usage. Your dataflows will not be interrupted.`
+ : req.arguments.straight_from_free_tier
+ ? `Your account will now begin accruing usage. Your dataflows will not be interrupted.`
+ : `Your account is now active.`
+ }
+ 📈 See your bill
+
+
+ Frequently Asked Questions
+
+ Where is my data stored?
+ By default, all collection data is stored in an Estuary-owned cloud storage bucket with a 30 day retention plicy. Now that you have a paid account, you can update this to store data in your own cloud storage bucket. We support GCS, S3, and Azure Blob storage.
+ `),
+ }));
+};
+
+export const ProvidedPaymentMethodEmail = (request: ProvidedPaymentMethodRecord): EmailConfig[] => {
+ if (request.resolved_at) {
+ // Do we want to send a "cc confirmed" email when this alert stops firing?
+ return [];
+ } else {
+ return ProvidedPaymentMethod(request);
+ }
+};
diff --git a/supabase/functions/alerts/index.ts b/supabase/functions/alerts/index.ts
index 3348812733..0eb6577afb 100644
--- a/supabase/functions/alerts/index.ts
+++ b/supabase/functions/alerts/index.ts
@@ -6,6 +6,7 @@ import { freeTierExceededEmail } from "./alert_types/free_tier_exceeded.ts";
import { freeTrialEndedEmail } from "./alert_types/free_trial_ended.ts";
import { freeTrialEndingEmail } from "./alert_types/free_trial_ending.ts";
import { freeTrialGracePeriodOverEmail } from "./alert_types/free_trial_grace_period_over.ts";
+import { ProvidedPaymentMethodEmail } from "./alert_types/provided_payment_method.ts";
export interface AlertRecord {
alert_type: T;
@@ -27,6 +28,7 @@ const emailTemplates = {
"free_trial_ended": freeTrialEndedEmail,
"free_trial_ending": freeTrialEndingEmail,
"free_trial_grace_period_over": freeTrialGracePeriodOverEmail,
+ "provided_payment_method": ProvidedPaymentMethodEmail,
};
// This is a temporary type guard for the POST request that provides shallow validation
@@ -159,6 +161,9 @@ serve(async (rawRequest: Request): Promise => {
case "free_trial_grace_period_over":
pendingEmails = emailTemplates[request.alert_type](request);
break;
+ case "provided_payment_method":
+ pendingEmails = emailTemplates[request.alert_type](request);
+ break;
default: {
// This checks that we have an exhaustive match. If this line has a
// type error, make sure you have a case above for every key in `emailTemplates`.
diff --git a/supabase/migrations/43_add_free_tier_exceeded_alert.sql b/supabase/migrations/43_payment_emails.sql
similarity index 83%
rename from supabase/migrations/43_add_free_tier_exceeded_alert.sql
rename to supabase/migrations/43_payment_emails.sql
index cd9dd48aa9..f995459b9b 100644
--- a/supabase/migrations/43_add_free_tier_exceeded_alert.sql
+++ b/supabase/migrations/43_payment_emails.sql
@@ -115,6 +115,34 @@ group by
customers.name,
users.raw_user_meta_data;
+-- Alert us internally when they go past 5 days over the trial
+create or replace view internal.provided_payment_method_firing as
+select
+ 'provided_payment_method' as alert_type,
+ tenants.tenant || 'alerts/provided_payment_method' as catalog_name,
+ alert_subscriptions.email,
+ auth.users.raw_user_meta_data->>'full_name' as full_name,
+ tenants.tenant,
+ tenants.trial_start,
+ tenants.trial_start + interval '1 month' as trial_end,
+ -- if tenants.trial_start is null, that means they entered their cc
+ -- while they're still in the free tier
+ coalesce((now() - tenants.trial_start) < interval '1 month', false) as in_trial,
+ tenants.trial_start is null as straight_from_free_tier
+from tenants
+ left join alert_subscriptions on alert_subscriptions.catalog_prefix ^@ tenants.tenant and email is not null
+ left join stripe.customers on stripe.customers."name" = tenants.tenant
+ -- Filter out sso users because auth.users is only guarinteed unique when that is false:
+ -- CREATE UNIQUE INDEX users_email_partial_key ON auth.users(email text_ops) WHERE is_sso_user = false;
+ left join auth.users on auth.users.email = alert_subscriptions.email and auth.users.is_sso_user is false
+where stripe.customers."invoice_settings/default_payment_method" is not null
+group by
+ tenants.tenant,
+ tenants.trial_start,
+ alert_subscriptions.email,
+ customers.name,
+ users.raw_user_meta_data;
+
-- Have to update this to join in auth.users for full_name support
-- Update to v2 because of the change from `emails` to `recipients`
create or replace view internal.alert_data_processing_firing_v2 as
@@ -254,12 +282,38 @@ free_trial_grace_period_over as (
trial_start,
trial_end,
has_credit_card
+),
+provided_payment_method_firing as (
+ select
+ catalog_name,
+ alert_type,
+ json_build_object(
+ 'tenant', tenant,
+ 'recipients', array_agg(json_build_object(
+ 'email', email,
+ 'full_name', full_name
+ )),
+ 'trial_start', trial_start,
+ 'trial_end', trial_end,
+ 'in_trial', in_trial,
+ 'straight_from_free_tier', straight_from_free_tier
+ ) as arguments
+ from internal.alert_free_trial_grace_period_over_firing
+ group by
+ catalog_name,
+ tenant,
+ alert_type,
+ trial_start,
+ trial_end,
+ in_trial
+ straight_from_free_tier
)
select * from data_processing
union all select * from free_tier_exceeded
union all select * from free_trial_ending
union all select * from free_trial_ended
union all select * from free_trial_grace_period_over
+union all select * from provided_payment_method_firing
order by catalog_name asc;
create or replace function internal.send_alerts()
@@ -277,7 +331,10 @@ if new.alert_type = 'data_not_processed_in_interval' then
to_jsonb(new.*),
headers:=format('{"Content-Type": "application/json", "Authorization": "Basic %s"}', token)::jsonb
);
-else
+-- Skip all of the past events that got triggered when we added these new event types
+-- NOTE: Change this so that the date is the day (or time) that it's deployed
+-- so that only "real" events that happen after deployment get sent
+else if new.fired_at > '2024-01-30'
perform
net.http_post(
'https://eyrcnmuzzyriypdajwdk.supabase.co/functions/v1/alerts',