send-email Split Implementation Plan
Overview
Split send-email/index.ts (3,680 lines) into 7 sub-modules + orchestration index.
Target Structure
supabase/functions/send-email/
├── index.ts # HTTP handler + orchestration (~300 lines)
├── types.ts # All type definitions
├── content.ts # Email content building utilities
├── graph-api.ts # Microsoft Graph API OAuth + sending
├── template.ts # Template variable replacement
├── schedule.ts # Recurrence, timezone, DB operations
├── flow-meter-report.ts # Flow meter data, CSV, charts, HTML
└── tank-level-alert.ts # Tank alert data, SVG, HTML templatesModule Details
1. types.ts
All TypeScript type definitions extracted from lines 16-93:
RecurrenceTypeDateRangeTypeFlowMeterDataSourceAttachmentDataSourceEmailScheduleAttachmentRowGeneratedAttachmentEmailScheduleRowEmailSenderRowEmailRequestPayloadSendEmailResult
Dependencies: None (leaf module)
2. content.ts
Email content building utilities:
parseRecipientInput()— Parse comma/semicolon/newline-separated emailsescapeHtml()— HTML entity escapingensureEmailList()— Filter valid email addressesbuildEmailContent()— Convert plain text to HTMLdownloadImageAsBase64()— Download images from Supabase storageconvertStoragePathsToBase64()— Replace storage paths with base64 data URIs
Dependencies: types.ts (for SupabaseClient type)
3. graph-api.ts
Microsoft Graph API integration:
getGraphToken()— OAuth2 client credentials token acquisitionbuildRecipientList()— Format recipients for Graph APIsendEmailViaGraph()— Send email with attachments, CC, BCC- Environment variable reading for Azure config
Dependencies: types.ts (GeneratedAttachment, SendEmailResult)
4. template.ts
Template variable replacement:
replaceTemplateVariables()— Body template variables ({{summary_flow_meter}},{{summary_tank_level}}, etc.)replaceSubjectTemplateVariables()— Subject template variables
Dependencies: None (pure string functions)
5. schedule.ts
Schedule management, timezone, and DB operations:
Timezone utilities:
toAwstTime()— Convert UTC to Perth timezone (UTC+8)fromAwstTime()— Convert Perth timezone to UTCparseTime()— Parse HH:MM:SS formatgetDaysInMonth()— UTC-safe month lengthformatDateForDisplay()— AWST-aware date formattingformatDateTimeForDisplay()— AWST-aware datetime formattingPERTH_OFFSET_MSconstant
Date range:
calculateDateRange()— Calculate date ranges (last_week, last_month, week_to_date, custom)
Recurrence:
calculateNextOccurrence()— Next send time for daily/weekly/monthlyisRecurrenceComplete()— Check if recurrence end date passed
DB operations:
insertEmailLog()— Log email send attemptsgetRequester()— Extract user from auth tokenisUserAdmin()— Check admin rolegetSenderById()— Fetch specific email sendergetDefaultSender()— Get user's default senderresolveFromAddress()— Determine "from" addressupdateScheduleStatus()— Update schedule status and metadatalockScheduleForSend()— Atomic lock for concurrent send preventionfetchScheduleAttachments()— Get attachment configsgenerateAttachments()— Generate all attachments for a schedule
Dependencies: types.ts, content.ts (parseRecipientInput), flow-meter-report.ts (generateFlowMeterAttachment)
6. flow-meter-report.ts
Flow meter report generation:
fetchFlowMeterRecords()— Query flow meter data from databasegenerateFlowMeterCSV()— Create CSV from recordsgenerateFilename()— Template-based filename generationaggregateDailySummaries()— Group records by daygetRecentEvents()— Extract last 10 dispensing eventsgenerateFlowMeterAttachment()— Orchestrate CSV generationgenerateDailyUsageChartUrl()— QuickChart.io URL for bar chartgenerateDailyUsageChartHTML()— HTML table-based daily usage visualizationgenerateSiteDetailHTML()— Detailed site section with charts and eventsformatSummariesAsHTML()— Complete branded email template
Dependencies: types.ts, schedule.ts (calculateDateRange, formatDateForDisplay)
7. tank-level-alert.ts
Tank level alert generation:
Constants:
DEFAULT_TANK_THRESHOLD_LITRES(15,000L)CRITICAL_TANK_THRESHOLD_LITRES(5,000L)DEFAULT_TANK_CARD_TEMPLATE(SVG template)
Functions:
fetchTankLevelAlertData()— Fetch pending alerts, calculate current tank levelscalculateSvgTankValues()— SVG tank visualization calculationsformatK()— Format numbers to k unitsformatTankLevelAsHTML()— Template variable replacement for tank cardsgroupTanksBySite()— Group tanks by site namegenerateTankSiteDetailHTML()— Site-specific tank detail sectionformatTankAlertAsHTML()— Complete branded email template
Dependencies: types.ts, content.ts (escapeHtml)
8. index.ts (orchestration)
Main handler and cron processor:
processDueSchedules()— Cron handler: fetch and send all due schedulesserve()handler — Manual send + cron dispatch, wrapped withwithHandler()
Dependencies: All other modules
Implementation Order
Execute in dependency order (leaf modules first):
types.ts— No dependenciescontent.ts— Depends on typestemplate.ts— No dependenciesgraph-api.ts— Depends on typesflow-meter-report.ts— Depends on types, schedule (circular — use forward reference or pass functions as params)tank-level-alert.ts— Depends on types, contentschedule.ts— Depends on types, content, flow-meter-reportindex.ts— Depends on all modules, add withHandler wrapper
Circular Dependency Resolution
schedule.ts calls generateAttachments() which calls generateFlowMeterAttachment() from flow-meter-report.ts. flow-meter-report.ts calls calculateDateRange() and formatDateForDisplay() from schedule.ts.
Resolution: Move calculateDateRange(), formatDateForDisplay(), and other timezone utilities into a separate concern within schedule.ts that doesn't depend on flow-meter-report. Then generateAttachments() imports from flow-meter-report, and flow-meter-report imports timezone utils from schedule. The dependency becomes one-directional:
schedule.ts (timezone utils) ← flow-meter-report.ts
schedule.ts (generateAttachments) → flow-meter-report.tsThis is fine because schedule.ts is a single file — the timezone utils don't depend on generateAttachments.
Verification
After splitting:
- Each module should be independently importable
- No circular import errors
- All exports properly typed
- index.ts should be ~300 lines of orchestration
- Total line count across all files should roughly equal original (minus removed boilerplate)