@startuml business_flow_technical
title AutoResponder — Technical Flow (engineering view)
' Engineering counterpart to business_flow_simplified.puml.
' Same decisions, outcomes, owners, and routing; adds:
'   • connector / repository names (MSSQL, CUPOLA, Hodor, Multipub, Salesforce, SFMC)
'   • class and method entry points (EmailProcessor, AutoResponseDetermination,
'     ContactLookup, ActionEngine, Notifier)
'   • run artifacts produced alongside each step (processing_report_*,
'     output_document_*, cupola_audit_log*, classifier_output, stage_execution)
'   • notify_* functions and their recipients
' Content is kept in sync with business_flow_simplified.puml:
'   Tarun → automated upload endpoint emails
'   Client Services, per-run impact report, active-only automatic writes,
'   OUT-OF-OFFICE tracked as its own outcome.
' Plus Venu's CUPOLA stored procedures
'   (""IP4SP_AutoBounce_MarkPersonInactive"",
'    ""IP4SP_AutoBounce_OrgPersonUpdates"")
' and Max's Hodor Prospect-Import clarification: Hodor is NOT written
' directly — each CUPOLA AUTO write produces a row in a Prospect Import
' file (SAC data-push template) so Hodor's de-dup on Last Name / Email /
' Phone stays intact; the file is imported via the standard path.

skinparam defaultFontSize 13
skinparam activity {
  BackgroundColor #F6F9FF
  BorderColor #2C3E8C
  DiamondBackgroundColor #FFF7E0
  DiamondBorderColor #C58B00
}
skinparam note {
  BackgroundColor #FFFBEA
  BorderColor #C58B00
}

start

'============================================================
'  STEP 1 — extract raw auto-reply emails
'============================================================
:**STEP 1 — Extract auto-reply emails**

Source: MSSQL ""CBI_EmailExtraction.dbo.Emails""
Class:  **EmailRepository**
Order:  most recent N by ""ReceivedDate DESC""

Dedup by ""MessageId"":
  **EmailProcessor.deduplicate_emails()**;<<#F0F8FF>>

'============================================================
'  STEP 2 — classify each email (LLM + QA)
'============================================================
:**STEP 2 — Run the determination pipeline**

Class: **AutoResponseDetermination**
Entry: **AutoResponseDetermination.determine()**

Pipeline:
  1. Classification agent (LLM)
  2. QA agent (second-pass label check)
  3. Validate + clean extracted emails
  4. ""normalize_llm_category"" (compound strings)
  5. ""map_category_to_determination""
     + ""create_determination_result""
  6. ""identify_auto_response_source_email""
     (resolve true sender behind relays)
  7. Post-processing:
     • drop alt contacts matching sender
     • REPLACEMENT with no alts → INACTIVE / UNKNOWN

Outcomes (Determination):
  • ACTIVE  (still at firm / out-of-office)
  • INACTIVE  (left / retired / deceased)
  • EMAIL_UPDATE / TITLE_UPDATE
  • REPLACEMENT  ('contact X instead')
  • BOUNCE  (undeliverable)
  • UNKNOWN  (unclear / not handled)
  • OUT_OF_OFFICE  (tracked separately);<<#F0F8FF>>

'============================================================
'  UNKNOWN short-circuit — no lookup, no actions
'============================================================
if (Determination == UNKNOWN?) then (yes)
  :**Skip — no lookup, no actions**

  ""skipped_unknown_determination++""
  Row still lands in the master
  Processing Report with outcome UNKNOWN.;<<#FFF0F0>>
  stop
else (no)
endif

'============================================================
'  STEP 3 — contact lookup across systems
'============================================================
:**STEP 3 — Contact lookup**

Class: **ContactLookup**
Entry: **ContactLookup.lookup_with_enrichment()**

Resolved lookup email = identified source
(fallback to raw sender).

Sources queried:
  • **CUPOLA** (system of record)
  • **Hodor**
  • **Multipub**
  • **Salesforce**
  • **SFMC** suppression feeds

Dry-run: stub Contact (no DB query).
Live: if zero records → skip
      (""skipped_no_contact++"").;<<#F0F8FF>>

'============================================================
'  STEP 4 — ActionEngine per determination
'============================================================
:**STEP 4 — ActionEngine.execute()**

Class: **ActionEngine**
Dispatched by determination below.;<<#F0F8FF>>

switch ( Determination )

'------------------------------------------------------------
'  ACTIVE — still at firm / OOO on an active record
'------------------------------------------------------------
case ( ACTIVE\n(100% confident\nactive in CUPOLA) )
  :**AUTOMATIC UPDATE PATH**
**ActionEngine._handle_active()**

CUPOLA current status = ACTIVE →
  no write required; confirmation only.
Hodor: no action (no change to mirror —
  Prospect Import file only emitted on writes).
Multipub: NEVER touched (marketing owns data).

Outputs:
  • ""processing_report_master.csv / .json""
  • ""classifier_output.json / .csv""
  • ""stage_execution.log"";<<#DCF3DC>>

'------------------------------------------------------------
'  INACTIVE — currently active record, allowed to auto-write
'------------------------------------------------------------
case ( INACTIVE\n(active in CUPOLA) )
  :**AUTOMATIC UPDATE PATH**
**ActionEngine._handle_inactive()**

PRECONDITION: person must be CURRENTLY
ACTIVE with this org in CUPOLA, else
record-only (""skipped_already_inactive_at_org++"").

1. Multipub validation cross-check
   (active sub, expired within 12 months,
    single-issue within 12 months).
2. CUPOLA: ""IP4SP_AutoBounce_MarkPersonInactive
   @OrgPersonID, @Result"" (handles status +
   hidden fields, stamps ""status_change_date"").
3. Hodor: Prospect Import row (SAC-push template)
   — no direct DB write, preserves de-dup on
   Last Name / Email / Phone.
4. Multipub: flagged only — no write.
5. Salesforce: skip if Multipub-linked,
   else mark INACTIVE.

Audit:
  • ""cupola_audit_log.csv / .json""
  • ""cupola_audit_log_rollback_plan.csv""
  • ""hodor_pending_actions.csv""
    (Prospect Import file — SAC-push template,
     imported via standard Prospect Import path,
     no direct Hodor DB writes)
Notify: **notify_venu_cupola_audit_files**;<<#DCF3DC>>

'------------------------------------------------------------
'  EMAIL_UPDATE / TITLE_UPDATE on an active record
'------------------------------------------------------------
case ( EMAIL_UPDATE / TITLE_UPDATE\n(active in CUPOLA) )
  :**AUTOMATIC UPDATE PATH**
**ActionEngine._handle_title_update()**

Applies only when CUPOLA status = ACTIVE.
CUPOLA write via ""IP4SP_AutoBounce_OrgPersonUpdates
  @OrgPersonID, @Position, @Email, @Result"".
If ""sender_new_email"" set → update email
  (skip Multipub, skip SF if Multipub-linked).
If ""updated_title"" set → update title with
  the same skip rules.
Hodor: Prospect Import row (SAC-push template)
  — no direct Hodor DB write.

Outputs:
  • ""output_document_email_update_requests"" (SFMC)
  • ""hodor_pending_actions.csv"" (Prospect Import)
  • ""processing_report_master""
  • ""cupola_audit_log"" + rollback plan;<<#DCF3DC>>

'------------------------------------------------------------
'  REPLACEMENT — original contact only
'------------------------------------------------------------
case ( REPLACEMENT\n(original contact) )
  :**AUTOMATIC UPDATE PATH**
**ActionEngine._handle_replacement()**
→ ""_handle_inactive()"" for the original

Mark original INACTIVE with full
Multipub validation + currently-active
precondition (same rule as INACTIVE flow).

Replacement details (name / email / title)
captured on ""AlternateContactRecord"".;<<#DCF3DC>>

'------------------------------------------------------------
'  MANUAL REVIEW BY IP4 — new contacts
'------------------------------------------------------------
case ( ACTIVE — new contact\n(not in CUPOLA) )
  :**MANUAL REVIEW BY IP4**

No CUPOLA record. Automation does NOT
auto-create. Row emitted for manual
verification via the official website.

Output:
  • ""output_document_inactive_no_cupola_match""
Notify: **notify_sai_action_items** (catalog N05/N06)

**Owner: Sai Teja / IP4**;<<#FFF0F0>>

'------------------------------------------------------------
'  MANUAL REVIEW BY IP4 — reactivation (inactive in CUPOLA)
'------------------------------------------------------------
case ( ACTIVE — reactivation\n(inactive in CUPOLA) )
  :**MANUAL REVIEW BY IP4**

CUPOLA says INACTIVE but email says active.
Automation does NOT auto-reactivate.
IP4 confirms, then creates a NEW entry
(no old-record reactivation).

Output:
  • ""output_document_inactive_no_cupola_match""
Notify: **notify_sai_action_items** (catalog N05/N06)

**Owner: Sai Teja / IP4**;<<#FFF0F0>>

'------------------------------------------------------------
'  MANUAL REVIEW BY IP4 — email/title change on inactive record
'------------------------------------------------------------
case ( EMAIL_UPDATE / TITLE_UPDATE\n(inactive in CUPOLA) )
  :**MANUAL REVIEW BY IP4**

CUPOLA record is inactive; automation only
updates ACTIVE records. Routed for review.

Output:
  • ""output_document_human_review""
Notify: **notify_sai_action_items** (+ Venu audit HR metadata)

**Owner: Sai Teja / IP4**;<<#FFF0F0>>

'------------------------------------------------------------
'  MANUAL REVIEW BY IP4 — moved to a different org
'------------------------------------------------------------
case ( Moved to a different org )
  :**MANUAL REVIEW BY IP4 (research step)**

Person left and joined a different
organisation (not retired / deceased).
Research whether the new organisation
needs to be added to CUPOLA before any
contact is created.

Output:
  • ""output_document_inactive_new_org""
Notify: **notify_sai_action_items** (catalog N05/N06)

**Owner: Sai Teja / IP4**;<<#FFF0F0>>

'------------------------------------------------------------
'  MANUAL REVIEW BY IP4 — REPLACEMENT new-person add
'------------------------------------------------------------
case ( REPLACEMENT\n(new person add) )
  :**MANUAL REVIEW BY IP4**

New-person replacements are NOT auto-added
to CUPOLA. Row is
emitted for IP4 / research-team entry.

Output:
  • ""output_document_alternate_contacts""
Notify: **notify_sai_action_items** (catalog N05 — replacements)

**Owner: Sai Teja / IP4 (research team)**;<<#FFF0F0>>

'------------------------------------------------------------
'  MULTIPUB AUDIT — ambiguous / unmatched, Tarun loop
'------------------------------------------------------------
case ( Multipub unmatched\nor ambiguous )
  :**MULTIPUB AUDIT PATH**

Inactive-candidate has an active or
recently expired Multipub subscription,
OR the Multipub match is ambiguous.

Automation does NOT change Multipub.
File is sent to Tarun for verification.
Tarun uploads the reviewed file through
the new upload endpoint, which
automatically emails the results back
to Client Services for action.

Output:
  • ""output_document_multipub_audit""
Notify:
  • **notify_multipub_subscriber_followup_batch** (Angel + Yogesh)
  • **notify_tarun_undetermined_sender_review** (when sender unknown)
  • **notify_multipub_subscriber_followup_from_upload** (Tarun upload Yes)

**Owners: Tarun (undetermined sender) → Client Services (Angel/Yogesh)**;<<#E8F1FF>>

'------------------------------------------------------------
'  OUT-OF-OFFICE — tracked separately
'------------------------------------------------------------
case ( OUT_OF_OFFICE )
  :**OUT-OF-OFFICE — tracked separately**

Not classified as inactive. Today these
are routed for human review inside the
main output; a separate extraction
process is planned.

Output:
  • ""output_document_human_review""
    (interim bucket)
Notify: **notify_sai_action_items** (+ Venu audit HR metadata)

**Owner: Human review (interim) → dedicated out-of-office process (planned)**;<<#FFF5E0>>

'------------------------------------------------------------
'  BOUNCE — pending rule, SFMC suppression
'------------------------------------------------------------
case ( BOUNCE )
  :**BOUNCE — pending rule**

Every bounce is sent for human review
and to Marketing for SFMC suppression.

Output:
  • ""output_document_undeliverables""
  • ""output_document_human_review""
Notify:
  • **notify_sai_action_items**
  • **notify_marketing_suppression** (marketing team mailbox)

**Owners: Max + Vish (rule), Marketing / SFMC (suppression)**;<<#FFF0F0>>

endswitch

'============================================================
'  STEP 5 — impact report (every run)
'============================================================
:**STEP 5 — Impact report (every run)**

Short summary assembled after every run
so leadership can see tangible progress:
  • **Total emails processed**
  • **Records deactivated**
  • **New records added**

Source: run summary counters
(""classifier_output"" + ""processing_report"").
Used for org-wide status updates and to
surface automation opportunities.;<<#F0F8FF>>

'============================================================
'  STEP 6 — outputs & notifications at end of run
'============================================================
:**STEP 6 — Outputs & notifications at end of run**

This section answers:
**which artifacts ship where, and via which notifier call.**;

partition "Notifier dispatch — live methods" {

  :**1. Automatic updates in systems**
   • CUPOLA — audited status / email SPs
   • Hodor — Prospect Import SAC file only
   • Multipub — never written by automation
   → ""cupola_audit_log"" + rollback plan
   → ""hodor_prospect_import_verification.csv"" when queued;

  :**2. Multipub subscriber follow-up**
   Fn: **notify_multipub_subscriber_followup_batch**
   → **To:** Angel + Yogesh, **Cc:** Matt;

  :**3. Tarun undetermined-sender review**
   Fn: **notify_tarun_undetermined_sender_review**
   Upload **Yes** → **notify_multipub_subscriber_followup_from_upload**;

  :**4. Sai consolidated action items**
   Fn: **notify_sai_action_items** → Sai Teja (IP4);

  :**5. Hodor Prospect Import**
   Fn: **notify_hodor_prospect_import**;

  :**6. Run audit → Sai**
   Fn: **notify_run_audit_for_ip4**
   (master + IP4 CSV, impact inline);

  :**7. Cupola audit → Venu**
   Fn: **notify_venu_cupola_audit_files**
   (audit log + HR metadata CSV);

  :**8. Marketing suppression → marketing team**
   Fn: **notify_marketing_suppression**
   (inactive people CSV only);

  :Global Cc via **_send_notification_email** (Max + Vish).;
}

stop

legend bottom
  **Verification:** every run output is verified by Sai Teja;
  edge cases are taken offline. Every run also emits impact
  counts (processed / deactivated / added) for leadership.
  This diagram mirrors ""business_flow_simplified.puml"" and
  is the engineering view (classes, methods, artifacts, notify_*).
endlegend

@enduml
