1. fetcher: GET /messages?$filter=isRead eq false and contains(subject, 'Metaweave Forms')
→ returns FetchedEmail(message_id, subject, body_text, received_datetime, vessel_name, report_type_raw, report_date)
2. parser.parse_email_body(body_text)
→ extracts BEGIN/END markers
→ decrypts AES-128-CBC payload
→ returns ParseResult(form_data: dict, report_type_raw, form_version)
3. mapper.map_report(parse_result)
→ coerces 92 scalar fields, builds 11 child arrays
→ returns dict with vessel_info, voyage_number, Report instance, child lists
4. writer.write_report(session, mapped, email_message_id)
→ upsert Vessel by IMO
→ upsert Voyage by (vessel_id, voyage_number)
→ delete existing Report by (vessel_id, report_type, report_datetime_utc) — CASCADE drops children
→ insert new Report + 11 child arrays
→ flush to get report_id
5. session.commit() # per email; rollback on exception
6. fetcher: PATCH /messages/{id} isRead=true