{{ theme.skipToContentLabel || 'Skip to content' }}

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 templates

Module Details

1. types.ts

All TypeScript type definitions extracted from lines 16-93:

  • RecurrenceType
  • DateRangeType
  • FlowMeterDataSource
  • AttachmentDataSource
  • EmailScheduleAttachmentRow
  • GeneratedAttachment
  • EmailScheduleRow
  • EmailSenderRow
  • EmailRequestPayload
  • SendEmailResult

Dependencies: None (leaf module)

2. content.ts

Email content building utilities:

  • parseRecipientInput() — Parse comma/semicolon/newline-separated emails
  • escapeHtml() — HTML entity escaping
  • ensureEmailList() — Filter valid email addresses
  • buildEmailContent() — Convert plain text to HTML
  • downloadImageAsBase64() — Download images from Supabase storage
  • convertStoragePathsToBase64() — 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 acquisition
  • buildRecipientList() — Format recipients for Graph API
  • sendEmailViaGraph() — 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 UTC
  • parseTime() — Parse HH:MM:SS format
  • getDaysInMonth() — UTC-safe month length
  • formatDateForDisplay() — AWST-aware date formatting
  • formatDateTimeForDisplay() — AWST-aware datetime formatting
  • PERTH_OFFSET_MS constant

Date range:

  • calculateDateRange() — Calculate date ranges (last_week, last_month, week_to_date, custom)

Recurrence:

  • calculateNextOccurrence() — Next send time for daily/weekly/monthly
  • isRecurrenceComplete() — Check if recurrence end date passed

DB operations:

  • insertEmailLog() — Log email send attempts
  • getRequester() — Extract user from auth token
  • isUserAdmin() — Check admin role
  • getSenderById() — Fetch specific email sender
  • getDefaultSender() — Get user's default sender
  • resolveFromAddress() — Determine "from" address
  • updateScheduleStatus() — Update schedule status and metadata
  • lockScheduleForSend() — Atomic lock for concurrent send prevention
  • fetchScheduleAttachments() — Get attachment configs
  • generateAttachments() — 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 database
  • generateFlowMeterCSV() — Create CSV from records
  • generateFilename() — Template-based filename generation
  • aggregateDailySummaries() — Group records by day
  • getRecentEvents() — Extract last 10 dispensing events
  • generateFlowMeterAttachment() — Orchestrate CSV generation
  • generateDailyUsageChartUrl() — QuickChart.io URL for bar chart
  • generateDailyUsageChartHTML() — HTML table-based daily usage visualization
  • generateSiteDetailHTML() — Detailed site section with charts and events
  • formatSummariesAsHTML() — 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 levels
  • calculateSvgTankValues() — SVG tank visualization calculations
  • formatK() — Format numbers to k units
  • formatTankLevelAsHTML() — Template variable replacement for tank cards
  • groupTanksBySite() — Group tanks by site name
  • generateTankSiteDetailHTML() — Site-specific tank detail section
  • formatTankAlertAsHTML() — 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 schedules
  • serve() handler — Manual send + cron dispatch, wrapped with withHandler()

Dependencies: All other modules

Implementation Order

Execute in dependency order (leaf modules first):

  1. types.ts — No dependencies
  2. content.ts — Depends on types
  3. template.ts — No dependencies
  4. graph-api.ts — Depends on types
  5. flow-meter-report.ts — Depends on types, schedule (circular — use forward reference or pass functions as params)
  6. tank-level-alert.ts — Depends on types, content
  7. schedule.ts — Depends on types, content, flow-meter-report
  8. index.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.ts

This 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)