2026-06-23 20:36:07 -04:00
import { NextResponse } from "next/server"
import { and , eq , gte , lte } from "drizzle-orm"
import { db } from "@/lib/db"
import {
follow_up_rules ,
follow_up_log ,
rent_payments ,
maintenance_requests ,
leases ,
units ,
} from "@/lib/db/schema"
import { getSessionUser } from "@/lib/session"
import { logActivity } from "@/lib/activity"
2026-07-01 13:56:34 -04:00
import { sendEmail , escapeHtml } from "@/lib/email/send"
2026-06-23 20:36:07 -04:00
export async function POST() {
const user = await getSessionUser ()
if ( ! user ) return NextResponse . json ({ error : "Unauthorized" }, { status : 401 })
const rules = await db
. select ()
. from ( follow_up_rules )
. where ( and ( eq ( follow_up_rules . user_id , user . id ), eq ( follow_up_rules . is_active , true )))
if ( ! rules . length ) return NextResponse . json ({ sent : 0 , results : [] })
const now = new Date ()
const followUpsToLog : any [] = []
for ( const rule of rules ) {
const cutoff = new Date ( now )
cutoff . setDate ( cutoff . getDate () - rule . trigger_days )
if ( rule . type === "overdue_rent" ) {
const overdue = await db . query . rent_payments . findMany ({
where : and (
eq ( rent_payments . user_id , user . id ),
eq ( rent_payments . status , "overdue" ),
lte ( rent_payments . due_date , cutoff . toISOString (). slice ( 0 , 10 ))
),
columns : { id : true , amount : true , due_date : true },
with : {
tenant : { columns : { first_name : true , last_name : true , email : true } },
},
})
for ( const payment of overdue ) {
const tenant = payment . tenant
if ( ! tenant ? . email ) continue
const daysOverdue = Math . ceil (( now . getTime () - new Date ( payment . due_date ). getTime ()) / ( 1000 * 60 * 60 * 24 ))
followUpsToLog . push ({
user_id : user.id ,
rule_id : rule.id ,
type : "overdue_rent" ,
recipient_name : ` ${ tenant . first_name } ${ tenant . last_name } ` ,
recipient_email : tenant.email ,
subject : `Rent Payment Reminder — ${ daysOverdue } Days Overdue` ,
message : rule.message_template
?? `Dear ${ tenant . first_name } , your rent payment of $ ${ Number ( payment . amount ). toLocaleString () } was due on ${ payment . due_date } and is now ${ daysOverdue } days overdue. Please make your payment as soon as possible to avoid further action.` ,
status : "sent" ,
})
}
}
if ( rule . type === "maintenance_stale" ) {
const stale = await db . query . maintenance_requests . findMany ({
where : and (
eq ( maintenance_requests . user_id , user . id ),
eq ( maintenance_requests . status , "open" ),
lte ( maintenance_requests . created_at , cutoff . toISOString ())
),
columns : { id : true , title : true , priority : true , created_at : true },
with : {
tenant : { columns : { first_name : true , last_name : true , email : true } },
},
})
for ( const req of stale ) {
const tenant = req . tenant
const daysOpen = Math . ceil (( now . getTime () - new Date ( req . created_at ). getTime ()) / ( 1000 * 60 * 60 * 24 ))
followUpsToLog . push ({
user_id : user.id ,
rule_id : rule.id ,
type : "maintenance_stale" ,
recipient_name : tenant ? ` ${ tenant . first_name } ${ tenant . last_name } ` : "N/A" ,
recipient_email : tenant?.email ?? null ,
subject : `Maintenance Update: ${ req . title } ` ,
message : rule.message_template
?? `Your maintenance request " ${ req . title } " has been open for ${ daysOpen } days. We are working on resolving this as soon as possible and will update you shortly.` ,
status : "sent" ,
})
}
}
if ( rule . type === "lease_renewal" ) {
const renewalDate = new Date ( now )
renewalDate . setDate ( renewalDate . getDate () + rule . trigger_days )
const expiring = await db . query . leases . findMany ({
where : and (
eq ( leases . user_id , user . id ),
eq ( leases . status , "active" ),
lte ( leases . lease_end , renewalDate . toISOString (). slice ( 0 , 10 )),
gte ( leases . lease_end , now . toISOString (). slice ( 0 , 10 ))
),
columns : { id : true , lease_end : true , rent_amount : true },
with : {
tenant : { columns : { first_name : true , last_name : true , email : true } },
},
})
for ( const lease of expiring ) {
const tenant = lease . tenant
if ( ! tenant ? . email ) continue
const daysLeft = Math . ceil (( new Date ( lease . lease_end ). getTime () - now . getTime ()) / ( 1000 * 60 * 60 * 24 ))
followUpsToLog . push ({
user_id : user.id ,
rule_id : rule.id ,
type : "lease_renewal" ,
recipient_name : ` ${ tenant . first_name } ${ tenant . last_name } ` ,
recipient_email : tenant.email ,
subject : `Lease Renewal Notice — Expires in ${ daysLeft } Days` ,
message : rule.message_template
?? `Dear ${ tenant . first_name } , your lease expires on ${ lease . lease_end } ( ${ daysLeft } days from now). Please contact us to discuss renewal options and ensure continuity of your tenancy.` ,
status : "sent" ,
})
}
}
if ( rule . type === "vacant_unit" ) {
const vacant = await db . query . units . findMany ({
where : and ( eq ( units . user_id , user . id ), eq ( units . status , "vacant" )),
columns : { id : true , unit_number : true , rent_amount : true },
with : {
property : { columns : { name : true } },
},
})
for ( const unit of vacant ) {
const property = unit . property
followUpsToLog . push ({
user_id : user.id ,
rule_id : rule.id ,
type : "vacant_unit" ,
recipient_name : "You" ,
recipient_email : null ,
subject : `Vacant Unit Alert: ${ property ? . name ?? "" } — Unit ${ unit . unit_number } ` ,
message : rule.message_template
?? `Unit ${ unit . unit_number } at ${ property ? . name ?? "your property" } has been vacant. Consider reviewing your listing or adjusting the rent of $ ${ Number ( unit . rent_amount ). toLocaleString () } /month to attract tenants faster.` ,
status : "sent" ,
})
}
}
// Update last_run_at
await db
. update ( follow_up_rules )
. set ({ last_run_at : now.toISOString () })
. where ( and ( eq ( follow_up_rules . id , rule . id ), eq ( follow_up_rules . user_id , user . id )))
}
// Send actual emails for all follow-ups that have a recipient
for ( const log of followUpsToLog ) {
if ( log . recipient_email ) {
try {
await sendEmail ({
to : log.recipient_email ,
subject : log.subject ,
html : `<!DOCTYPE html>
<html>
<body style="font-family:sans-serif;background:#09090b;color:#fff;padding:40px 20px;max-width:560px;margin:0 auto;">
<div style="background:#16161f;border:1px solid rgba(255,255,255,0.08);border-radius:12px;padding:32px;">
2026-07-01 13:56:34 -04:00
<p style="font-size:15px;line-height:1.6;color:rgba(255,255,255,0.8);margin:0 0 24px;"> ${ escapeHtml ( log . message ). replace ( /\n/g , "<br/>" ) } </p>
2026-06-23 20:36:07 -04:00
<p style="color:rgba(255,255,255,0.3);font-size:11px;margin:24px 0 0;border-top:1px solid rgba(255,255,255,0.06);padding-top:16px;">
Property Management Network — Automated Follow-up System
</p>
</div>
</body>
</html>`
})
} catch {
log . status = "failed"
}
}
}
if ( followUpsToLog . length > 0 ) {
await db . insert ( follow_up_log ). values ( followUpsToLog )
}
await logActivity ({
userId : user.id ,
type : "ai_action" ,
title : `Follow-ups processed: ${ followUpsToLog . length } action ${ followUpsToLog . length !== 1 ? "s" : "" } triggered` ,
})
return NextResponse . json ({ sent : followUpsToLog.length , results : followUpsToLog })
}