/** * Communications (Harvest Reach). * Source: `db/migrations/0001_init.sql`. */ import { pgTable, uuid, text, integer, boolean, timestamp, jsonb, index, uniqueIndex, } from "drizzle-orm/pg-core"; import { brands, adminUsers } from "./brands"; export const communicationSettings = pgTable( "communication_settings", { id: uuid("id").primaryKey().defaultRandom(), brandId: uuid("brand_id") .notNull() .unique() .references(() => brands.id, { onDelete: "cascade" }), defaultSenderEmail: text("default_sender_email"), defaultSenderName: text("default_sender_name"), replyToEmail: text("reply_to_email"), emailProvider: text("email_provider").notNull().default("resend"), emailFooterHtml: text("email_footer_html"), createdAt: timestamp("created_at", { withTimezone: true }) .notNull() .defaultNow(), updatedAt: timestamp("updated_at", { withTimezone: true }) .notNull() .defaultNow(), }, ); export const communicationTemplates = pgTable( "communication_templates", { id: uuid("id").primaryKey().defaultRandom(), brandId: uuid("brand_id") .notNull() .references(() => brands.id, { onDelete: "cascade" }), name: text("name").notNull(), subject: text("subject").notNull(), bodyText: text("body_text").notNull().default(""), bodyHtml: text("body_html"), templateType: text("template_type").notNull(), campaignType: text("campaign_type"), createdBy: uuid("created_by"), createdAt: timestamp("created_at", { withTimezone: true }) .notNull() .defaultNow(), updatedAt: timestamp("updated_at", { withTimezone: true }) .notNull() .defaultNow(), }, (t) => ({ brandIdx: index("communication_templates_brand_idx").on(t.brandId), }), ); export const communicationSegments = pgTable( "communication_segments", { id: uuid("id").primaryKey().defaultRandom(), brandId: uuid("brand_id") .notNull() .references(() => brands.id, { onDelete: "cascade" }), name: text("name").notNull(), description: text("description"), rules: jsonb("rules").notNull().default({}), createdBy: uuid("created_by"), createdAt: timestamp("created_at", { withTimezone: true }) .notNull() .defaultNow(), updatedAt: timestamp("updated_at", { withTimezone: true }) .notNull() .defaultNow(), }, (t) => ({ brandIdx: index("communication_segments_brand_idx").on(t.brandId), }), ); export const communicationCampaigns = pgTable( "communication_campaigns", { id: uuid("id").primaryKey().defaultRandom(), brandId: uuid("brand_id") .notNull() .references(() => brands.id, { onDelete: "cascade" }), name: text("name").notNull(), subject: text("subject"), bodyText: text("body_text"), bodyHtml: text("body_html"), templateId: uuid("template_id").references( () => communicationTemplates.id, { onDelete: "set null" }, ), campaignType: text("campaign_type").notNull(), status: text("status", { enum: ["draft", "scheduled", "sending", "sent", "canceled"], }).notNull().default("draft"), audienceRules: jsonb("audience_rules").notNull().default({}), brandName: text("brand_name"), scheduledAt: timestamp("scheduled_at", { withTimezone: true }), sentAt: timestamp("sent_at", { withTimezone: true }), recipientCount: integer("recipient_count").notNull().default(0), createdBy: uuid("created_by"), createdAt: timestamp("created_at", { withTimezone: true }) .notNull() .defaultNow(), updatedAt: timestamp("updated_at", { withTimezone: true }) .notNull() .defaultNow(), }, (t) => ({ brandIdx: index("communication_campaigns_brand_idx").on(t.brandId), statusIdx: index("communication_campaigns_status_idx").on( t.brandId, t.status, ), }), ); export const communicationContacts = pgTable( "communication_contacts", { id: uuid("id").primaryKey().defaultRandom(), brandId: uuid("brand_id") .notNull() .references(() => brands.id, { onDelete: "cascade" }), email: text("email"), phone: text("phone"), firstName: text("first_name"), lastName: text("last_name"), fullName: text("full_name"), source: text("source").notNull(), externalId: text("external_id"), customerId: uuid("customer_id"), emailOptIn: boolean("email_opt_in").notNull().default(true), smsOptIn: boolean("sms_opt_in").notNull().default(false), emailOptInAt: timestamp("email_opt_in_at", { withTimezone: true }), smsOptInAt: timestamp("sms_opt_in_at", { withTimezone: true }), unsubscribedAt: timestamp("unsubscribed_at", { withTimezone: true }), tags: text("tags").array().default([]), metadata: jsonb("metadata").default({}), createdAt: timestamp("created_at", { withTimezone: true }) .notNull() .defaultNow(), updatedAt: timestamp("updated_at", { withTimezone: true }) .notNull() .defaultNow(), }, (t) => ({ brandIdx: index("communication_contacts_brand_idx").on(t.brandId), emailIdx: index("communication_contacts_email_idx").on( t.brandId, t.email, ), }), ); export const communicationMessageLogs = pgTable( "communication_message_logs", { id: uuid("id").primaryKey().defaultRandom(), brandId: uuid("brand_id") .notNull() .references(() => brands.id, { onDelete: "cascade" }), campaignId: uuid("campaign_id").references( () => communicationCampaigns.id, { onDelete: "set null" }, ), contactId: uuid("contact_id").references( () => communicationContacts.id, { onDelete: "set null" }, ), customerEmail: text("customer_email"), deliveryMethod: text("delivery_method").notNull(), subject: text("subject"), bodyPreview: text("body_preview"), status: text("status").notNull(), sentAt: timestamp("sent_at", { withTimezone: true }), errorMessage: text("error_message"), eventType: text("event_type"), eventId: uuid("event_id"), createdAt: timestamp("created_at", { withTimezone: true }) .notNull() .defaultNow(), }, (t) => ({ brandIdx: index("communication_message_logs_brand_idx").on(t.brandId), campaignIdx: index( "communication_message_logs_campaign_idx", ).on(t.campaignId), }), ); export const customerCommunicationPreferences = pgTable( "customer_communication_preferences", { id: uuid("id").primaryKey().defaultRandom(), customerId: uuid("customer_id").notNull(), brandId: uuid("brand_id") .notNull() .references(() => brands.id, { onDelete: "cascade" }), emailOptIn: boolean("email_opt_in").notNull().default(true), smsOptIn: boolean("sms_opt_in").notNull().default(false), emailOptInAt: timestamp("email_opt_in_at", { withTimezone: true }), smsOptInAt: timestamp("sms_opt_in_at", { withTimezone: true }), createdAt: timestamp("created_at", { withTimezone: true }) .notNull() .defaultNow(), updatedAt: timestamp("updated_at", { withTimezone: true }) .notNull() .defaultNow(), }, (t) => ({ customerBrandIdx: uniqueIndex( "customer_communication_prefs_customer_brand_idx", ).on(t.customerId, t.brandId), }), ); // Email automation sequences export const abandonedCartRecovery = pgTable( "abandoned_cart_recovery", { id: uuid("id").primaryKey().defaultRandom(), brandId: uuid("brand_id") .notNull() .references(() => brands.id, { onDelete: "cascade" }), customerId: uuid("customer_id"), contactEmail: text("contact_email").notNull(), contactName: text("contact_name"), cartSnapshot: jsonb("cart_snapshot").notNull(), brandName: text("brand_name"), locale: text("locale").default("en"), sequenceStep: integer("sequence_step").default(0), lastEmailSentAt: timestamp("last_email_sent_at", { withTimezone: true, }), nextEmailAt: timestamp("next_email_at", { withTimezone: true }), status: text("status", { enum: ["active", "recovered", "expired", "manually_closed"], }).default("active"), recoveredOrderId: uuid("recovered_order_id"), recoveredAt: timestamp("recovered_at", { withTimezone: true }), expiredAt: timestamp("expired_at", { withTimezone: true }), manuallyClosedAt: timestamp("manually_closed_at", { withTimezone: true, }), manuallyClosedBy: uuid("manually_closed_by").references( () => adminUsers.id, ), createdAt: timestamp("created_at", { withTimezone: true }) .notNull() .defaultNow(), updatedAt: timestamp("updated_at", { withTimezone: true }) .notNull() .defaultNow(), }, ); export const welcomeEmailSequence = pgTable( "welcome_email_sequence", { id: uuid("id").primaryKey().defaultRandom(), brandId: uuid("brand_id") .notNull() .references(() => brands.id, { onDelete: "cascade" }), contactId: uuid("contact_id").references( () => communicationContacts.id, { onDelete: "cascade" }, ), contactEmail: text("contact_email").notNull(), contactName: text("contact_name"), brandName: text("brand_name"), locale: text("locale").default("en"), sequenceStep: integer("sequence_step").default(0), lastEmailSentAt: timestamp("last_email_sent_at", { withTimezone: true, }), nextEmailAt: timestamp("next_email_at", { withTimezone: true }), status: text("status", { enum: ["active", "completed", "unsubscribed", "bounced"], }).default("active"), completedAt: timestamp("completed_at", { withTimezone: true }), unsubscribedAt: timestamp("unsubscribed_at", { withTimezone: true }), bouncedAt: timestamp("bounced_at", { withTimezone: true }), createdAt: timestamp("created_at", { withTimezone: true }) .notNull() .defaultNow(), updatedAt: timestamp("updated_at", { withTimezone: true }) .notNull() .defaultNow(), }, ); export type CommunicationSettings = typeof communicationSettings.$inferSelect; export type CommunicationTemplate = typeof communicationTemplates.$inferSelect; export type CommunicationSegment = typeof communicationSegments.$inferSelect; export type CommunicationCampaign = typeof communicationCampaigns.$inferSelect; export type CommunicationContact = typeof communicationContacts.$inferSelect; export type CommunicationMessageLog = typeof communicationMessageLogs.$inferSelect; export type CustomerCommunicationPreference = typeof customerCommunicationPreferences.$inferSelect; export type AbandonedCartRecovery = typeof abandonedCartRecovery.$inferSelect; export type WelcomeEmailSequence = typeof welcomeEmailSequence.$inferSelect;