1+ /* eslint-disable import/prefer-default-export */
12import { Resend } from 'resend' ;
23import * as dotenv from 'dotenv' ;
34import fetch , { Headers , Response , Request } from 'node-fetch' ;
@@ -6,8 +7,8 @@ import React from 'react';
67import { ApartmentWithId } from '@common/types/db-types' ;
78import GenerateNewsletter from './templates/GenerateNewsletter' ;
89import { getUserBatches , USERS } from './helpers/firebase_users_loader' ;
10+ import { NewsletterRequest } from './templates/Types' ;
911
10- // Initialize fetch globals if needed
1112if ( ! global . fetch ) {
1213 global . fetch = fetch as unknown as typeof global . fetch ;
1314 global . Headers = Headers as unknown as typeof global . Headers ;
@@ -25,50 +26,31 @@ type EmailCampaignOptions = {
2526 recentAreaPropertyIDs ?: string [ ] ;
2627 lovedPropertyIDs ?: string [ ] ;
2728 reviewedPropertyIDs ?: string [ ] ;
29+ newsletterData ?: NewsletterRequest ;
2830} ;
2931
30- /**
31- * sendEmailCampaign
32- * Sends a marketing email campaign to batches of users featuring apartment properties.
33- *
34- * @param options - Configuration options for the email campaign
35- * @param options.subject - Email subject line (default: 'Check Out These New Apartments!')
36- * @param options.toEmail - Primary recipient email address (default: '[email protected] ') 37- * @param options.nearbyPropertyIDs - List of property IDs to feature as nearby available properties
38- * @param options.budgetPropertyIDs - List of property IDs to feature as budget-friendly properties
39- * @param options.recentAreaPropertyIDs - List of property IDs to feature as recent area properties
40- * @param options.lovedPropertyIDs - List of property IDs to feature as top loved properties
41- * @param options.reviewedPropertyIDs - List of property IDs to feature as most reviewed properties
42- * @returns Promise that resolves when all email batches have been sent
43- */
4432const sendEmailCampaign = async ( options : EmailCampaignOptions = { } ) : Promise < void > => {
45- const { subject
= 'Check Out These New Apartments!' , toEmail
= '[email protected] ' } = 46- options ;
33+ const {
34+ subject = 'Check Out These New Apartments!' ,
35+ 36+ newsletterData,
37+ } = options ;
4738
48- // Load environment variables
4939 dotenv . config ( { path : path . resolve ( process . cwd ( ) , '.env.dev' ) } ) ;
5040
51- const fromEmail = 'updates. cuapts.org' ;
41+ const fromEmail = 'updates@ cuapts.org' ;
5242 const fromName = 'CU Apts' ;
5343
5444 const apiKey = process . env . RESEND_API_KEY ;
5545 if ( ! apiKey ) {
56- console . error ( 'Missing RESEND_API_KEY in environment variables' ) ;
57- return ;
46+ throw new Error ( 'Missing RESEND_API_KEY in environment variables' ) ;
5847 }
5948
60- /**
61- * getPropertiesByIds
62- * Fetches apartment data for a given list of property IDs.
63- *
64- * @param ids - List of apartment IDs to fetch from backend API
65- * @returns List of ApartmentWithId objects
66- *
67- */
6849 const getPropertiesByIds = async ( ids : string [ ] ) : Promise < ApartmentWithId [ ] > => {
50+ if ( ! ids || ids . length === 0 ) return [ ] ;
51+
6952 try {
7053 const idParam = ids . join ( ',' ) ;
71-
7254 const response = await fetch ( `${ API_BASE_URL } /api/apts/${ idParam } ` ) ;
7355
7456 if ( ! response . ok ) {
@@ -83,7 +65,7 @@ const sendEmailCampaign = async (options: EmailCampaignOptions = {}): Promise<vo
8365 }
8466 } ;
8567
86- // Loads chosen properties
68+ // Load properties
8769 const nearbyProperties = options . nearbyPropertyIDs
8870 ? await getPropertiesByIds ( options . nearbyPropertyIDs )
8971 : [ ] ;
@@ -100,63 +82,50 @@ const sendEmailCampaign = async (options: EmailCampaignOptions = {}): Promise<vo
10082 ? await getPropertiesByIds ( options . reviewedPropertyIDs )
10183 : [ ] ;
10284
103- console . log ( `Fetched ${ nearbyProperties . length } nearby properties (recently released spotlight) ` ) ;
104- console . log ( `Fetched ${ budgetProperties . length } budget properties (recently released spotlight) ` ) ;
105- console . log ( `Fetched ${ recentAreaProperties . length } recent area properties (area spotlight) ` ) ;
106- console . log ( `Fetched ${ lovedProperties . length } loved properties (loved spotlight) ` ) ;
107- console . log ( `Fetched ${ reviewedProperties . length } reviewed properties (loved spotlight) ` ) ;
85+ console . log ( `Fetched ${ nearbyProperties . length } nearby properties` ) ;
86+ console . log ( `Fetched ${ budgetProperties . length } budget properties` ) ;
87+ console . log ( `Fetched ${ recentAreaProperties . length } recent area properties` ) ;
88+ console . log ( `Fetched ${ lovedProperties . length } loved properties` ) ;
89+ console . log ( `Fetched ${ reviewedProperties . length } reviewed properties` ) ;
10890
10991 const resend = new Resend ( apiKey ) ;
11092
111- /**
112- * BATCH PROCESSING AND EMAIL SENDING
113- * Processes users in batches and sends emails concurrently:
114- * - Creates batches of 50 users and maps over batches to send emails in parallel
115- * - Uses BCC to hide recipient emails from each other
116- *
117- * To use, uncomment line 191, comment out line 192, and run the file as normal.
118- */
119- // eslint-disable-next-line @typescript-eslint/no-unused-vars
12093 const sendBatchEmail = async ( ) => {
12194 try {
122- console . log ( `Total users available in database : ${ USERS . length } ` ) ;
95+ console . log ( `Total users available: ${ USERS . length } ` ) ;
12396 const validEmails = USERS . filter ( ( user ) => user . email && user . email . includes ( '@' ) ) ;
12497 console . log ( `Valid email addresses: ${ validEmails . length } ` ) ;
12598
12699 if ( validEmails . length === 0 ) {
127- console . error ( 'No valid email addresses found!' ) ;
128- return ;
100+ throw new Error ( 'No valid email addresses found!' ) ;
129101 }
130102
131103 const userBatches = await getUserBatches ( 50 ) ;
132- console . log (
133- `Preparing to send emails to ${ userBatches . length } batches of users (${ 50 } per batch)`
134- ) ;
104+ console . log ( `Sending to ${ userBatches . length } batches of users` ) ;
135105
136106 const emailPromises = userBatches . map ( async ( batch , i ) => {
137107 const bccEmails = batch . map ( ( user ) => user . email ) ;
138- console . log (
139- `Preparing batch ${ i + 1 } /${ userBatches . length } with ${ bccEmails . length } recipients`
140- ) ;
141108
142109 const { data, error } = await resend . emails . send ( {
143110 from : `${ fromName } <${ fromEmail } >` ,
144111 to : toEmail ,
145- // bcc: bccEmails,
112+ bcc : bccEmails ,
146113 subject,
147114 react : React . createElement ( GenerateNewsletter , {
148115 nearbyProperties,
149116 budgetProperties,
150117 recentAreaProperties,
151118 lovedProperties,
152119 reviewedProperties,
120+ newsletterData,
153121 } ) ,
154122 } ) ;
155123
156124 if ( error ) {
157125 console . error ( `Error sending batch ${ i + 1 } :` , error ) ;
126+ throw error ;
158127 } else {
159- console . log ( `Batch ${ i + 1 } sent successfully! ID:` , data ?. id || 'no ID returned' ) ;
128+ console . log ( `Batch ${ i + 1 } sent successfully! ID:` , data ?. id ) ;
160129 }
161130 } ) ;
162131
@@ -168,65 +137,46 @@ const sendEmailCampaign = async (options: EmailCampaignOptions = {}): Promise<vo
168137 }
169138 } ;
170139
171- /**
172- * SINGLE TEST EMAIL SENDING
173- * Sends an email to one person (useful for testing email templates).
174- * To use, uncomment line 192, comment out line 191, edit info below,
175- * and run the file as normal.
176- */
177140 const sendSingleTestEmail = async ( ) => {
178141 try {
179142 const { data, error } = await resend . emails . send ( {
180- 181- 143+ from : ` ${ fromName } < ${ fromEmail } >` ,
144+ to : toEmail ,
182145 subject,
183146 react : React . createElement ( GenerateNewsletter , {
184147 nearbyProperties,
185148 budgetProperties,
186149 recentAreaProperties,
187150 lovedProperties,
188151 reviewedProperties,
152+ newsletterData,
189153 } ) ,
190154 } ) ;
155+
191156 if ( error ) {
192157 console . error ( 'Error sending email:' , error ) ;
158+ throw error ;
193159 } else {
194- console . log ( 'Email sent successfully! ID:' , data ? data . id : ' no ID returned.' ) ;
160+ console . log ( 'Email sent successfully! ID:' , data ? .id ) ;
195161 }
196162 } catch ( err ) {
197163 console . error ( 'Exception when sending email:' , err ) ;
164+ throw err ;
198165 }
199166 } ;
200167
201- // sendBatchEmail();
202- sendSingleTestEmail ( ) ;
203- } ;
204-
205- /**
206- * Entry point function that executes the email campaign with default settings.
207- * Handles logging and error handling for the campaign process.
208- *
209- * @returns Promise that resolves when the campaign completes
210- */
211- async function main ( ) {
212- try {
213- console . log ( 'Starting email campaign...' ) ;
214-
215- // Customize email subject
216- await sendEmailCampaign ( {
217- subject : 'New Apartment Listings Available!' ,
218- recentAreaPropertyIDs : [ '12' , '2' , '24' ] ,
219- budgetPropertyIDs : [ '23' , '24' , '24' ] ,
220- nearbyPropertyIDs : [ '14' , '23' , '24' ] ,
221- reviewedPropertyIDs : [ '1' , '2' , '3' ] ,
222- lovedPropertyIDs : [ '4' , '5' , '6' ] ,
223- } ) ;
224-
225- console . log ( 'Campaign completed successfully!' ) ;
226- } catch ( error ) {
227- console . error ( 'Failed to send campaign:' , error ) ;
168+ // Determine which sending method to use based on sendToAll flag
169+ // If newsletterData exists and sendToAll is true, send to all users
170+ // Otherwise, send a single test email
171+ const shouldSendToAll = newsletterData ?. sendToAll === true ;
172+
173+ if ( shouldSendToAll ) {
174+ console . log ( 'Sending to all subscribers...' ) ;
175+ await sendBatchEmail ( ) ;
176+ } else {
177+ console . log ( `Sending test email to: ${ toEmail } ` ) ;
178+ await sendSingleTestEmail ( ) ;
228179 }
229- }
180+ } ;
230181
231- // Execute main function directly
232- main ( ) . catch ( console . error ) ;
182+ export { sendEmailCampaign } ;
0 commit comments