By Patrick McCurley

DPD Claim Workflow Refactor Plan

By Patrick McCurley · Created Mar 6, 2026 public

Migrate DPDClaimWorkflow from the old inline architecture to the TECH-589 composable architecture (template RunAsync, lifecycle hooks, ReminderHandler).

Architecture Overview

graph TD
    subgraph "Current (Old Architecture)"
        CWB1[ClaimWorkflowBase] --> DPD1[DPDClaimWorkflow<br/>1,105 lines]
        DPD1 -.-> |"manual _state shadowing"| S1[DPDClaimWorkflowState]
        DPD1 -.-> |"inline 200 lines"| R1["CheckAndSendRemindersAsync<br/>(duplicated from base)"]
        DPD1 -.-> |"inline 200+ lines"| L1["RunAsync<br/>(full lifecycle inlined)"]
        DPD1 -.-> |"inline"| T1["Email templates<br/>Wait methods<br/>State management"]
    end

    subgraph "Target (TECH-589 Architecture)"
        CWB2[ClaimWorkflowBase<br/>Template RunAsync] --> CW2["ClaimWorkflow&lt;TState&gt;<br/>Typed state plumbing"]
        CW2 --> DPD2["DPDClaimWorkflow<br/>~250 lines"]
        DPD2 -.-> |"composition"| RH["DPDReminderHandler<br/>~180 lines"]
        DPD2 -.-> |"typed via generic"| S2[DPDClaimWorkflowState]
        RH --> RHB[ReminderHandler<br/>base class]
    end

    style DPD1 fill:#fce4ec
    style DPD2 fill:#e8f5e9
    style R1 fill:#fce4ec
    style T1 fill:#fce4ec
    style RH fill:#e8f5e9
    style RHB fill:#e1f5fe
    style CW2 fill:#e1f5fe

Lifecycle Flow Comparison

sequenceDiagram
    participant Old as DPD (Current)
    participant New as DPD (Target)
    participant Base as Base RunAsync

    Note over Old: Full lifecycle inlined in RunAsync

    rect rgb(252, 228, 236)
    Old->>Old: CreateDPDInitialState()
    Old->>Old: InitializeConfig()
    Old->>Old: base._state = _state
    Old->>Old: InitializeInvestigationPeriodAsync()
    Old->>Old: WaitForInvestigationPeriodAsync()
    Old->>Old: while loop: CheckAndSendRemindersAsync()
    Old->>Old: WaitForCourierResponseAsync()
    Old->>Old: FinalizeWorkflow()
    end

    Note over New,Base: Lifecycle hooks + base template

    rect rgb(232, 245, 233)
    New->>Base: RunAsync(request)
    Base->>New: CreateState() → DPD-specific state
    Base->>New: OnSubmissionAsync() → init investigation
    Base->>New: OnPostSubmissionAsync() → wait for investigation
    Base->>Base: RunEmailLoopAsync()
    Base->>New: OnProcessEmailAsync() → acknowledge only
    Base->>New: _reminderHandler.CheckAndSendAsync()
    Base->>Base: FinalizeWorkflow()
    end

Current State

DPDClaimWorkflow (1,105 lines) extends ClaimWorkflowBase directly with:

Target architecture (as used by Asendia, DHL, InPost, RoyalMail):

DPD's Unique Concepts

These must survive the refactor:

  1. Investigation period: DPD says "we'll investigate for X days" — first reminder waits for this
  2. Acknowledge-only email processing: DPD doesn't classify emails — existing automation handles that
  3. Courier response detection: If courier responds after investigation, workflow completes early
  4. Investigation-based reminder timing: First reminder = investigation due date, subsequent = interval
  5. Escalation routing: 3rd reminder goes to multiple escalation contacts with CC
  6. DPD-specific activities: SearchDPDEmails, ExtractInvestigationDays

Phase 1: Apply TECH-589 Base Infrastructure

Goal: Get the new architecture foundation in place. All existing couriers work.

graph LR
    subgraph "What gets applied"
        A["Template RunAsync"] --> B["RunEmailLoopAsync"]
        A --> C["AwaitResponseAsync"]
        A --> D["Lifecycle Hooks"]
        E["ReminderHandler"] --> F["IWorkflowContext"]
    end

    style A fill:#e1f5fe
    style B fill:#e1f5fe
    style C fill:#e1f5fe
    style D fill:#e1f5fe
    style E fill:#e8f5e9
    style F fill:#e8f5e9

Changes:

Verification:

dotnet build backend/Claimit.sln
dotnet test backend/Temporal/EmailProcessing/EmailProcessing.Temporal.Tests/

Risk: Merge conflicts between stash and current state. May need to manually reconcile.


Phase 2: Migrate DPD Inheritance

Goal: DPDClaimWorkflow uses the new type hierarchy. No behavior changes yet.

graph TD
    subgraph Before
        B1[ClaimWorkflowBase] --> B2["DPDClaimWorkflow<br/>(manual _state shadowing)"]
    end
    subgraph After
        A1[ClaimWorkflowBase] --> A2["ClaimWorkflow&lt;DPDClaimWorkflowState&gt;"]
        A2 --> A3["DPDClaimWorkflow<br/>(typed State property)"]
    end

    style B2 fill:#fce4ec
    style A3 fill:#e8f5e9
    style A2 fill:#e1f5fe

Changes:

Verification:

Does NOT change: RunAsync orchestration, reminder logic, email handling (all still inline).


Phase 3: Wire DPDReminderHandler

Goal: DPD's reminder timing, email templates, and intervention data use the handler pattern.

graph TD
    subgraph "Delete from DPDClaimWorkflow"
        D1["GetReminderEmailSubject()"]
        D2["GetReminderEmailBody()"]
        D3["CalculateNextReminderDueDate()"]
        D4["CheckAndSendRemindersAsync()"]
    end

    subgraph "Already in DPDReminderHandler"
        H1["GetSubject()"]
        H2["GetBody()"]
        H3["CalculateNextDueDate()"]
        H4["CheckAndSendAsync()"]
    end

    D1 -.->|"replaced by"| H1
    D2 -.->|"replaced by"| H2
    D3 -.->|"replaced by"| H3
    D4 -.->|"replaced by"| H4

    style D1 fill:#fce4ec
    style D2 fill:#fce4ec
    style D3 fill:#fce4ec
    style D4 fill:#fce4ec
    style H1 fill:#e8f5e9
    style H2 fill:#e8f5e9
    style H3 fill:#e8f5e9
    style H4 fill:#e8f5e9

Changes:

Verification:

Open question: Escalation CC/additional-TO routing may need SendReminderEmailRequest changes or a DPDReminderHandler.SendEmailAsync override.


Phase 4: Extract DPD Lifecycle into Hooks

Goal: DPD's unique initialization and investigation wait move to lifecycle hooks. RunAsync becomes a one-liner.

stateDiagram-v2
    [*] --> CreateState: base.RunAsync()
    CreateState --> OnSubmissionAsync: Phase: Submitting

    state OnSubmissionAsync {
        [*] --> SearchEmails: SearchDPDEmailsActivity
        SearchEmails --> ExtractDays: ExtractInvestigationDaysActivity
        ExtractDays --> CheckPriorResponse
        CheckPriorResponse --> [*]: courier responded → ShouldComplete
        CheckPriorResponse --> [*]: no response → continue
    }

    OnSubmissionAsync --> OnPostSubmissionAsync

    state OnPostSubmissionAsync {
        [*] --> WaitInvestigation: Wait for investigation period
        WaitInvestigation --> [*]: elapsed or email arrived
    }

    OnPostSubmissionAsync --> RunEmailLoopAsync: Phase: WaitingForOutcome

    state RunEmailLoopAsync {
        [*] --> WaitForSignal: Wait for email/timeout
        WaitForSignal --> AcknowledgeEmail: OnProcessEmailAsync (override)
        AcknowledgeEmail --> CheckReminder: _reminderHandler.CheckAndSendAsync
        CheckReminder --> WaitForSignal: loop
        WaitForSignal --> [*]: ShouldComplete or timeout
    }

    RunEmailLoopAsync --> FinalizeWorkflow: Phase: Completed
    FinalizeWorkflow --> [*]

Changes:

Verification:

Key behavioral requirement: The refactored DPD workflow MUST:

  1. Wait for investigation period before first reminder (DPD-specific)
  2. Complete early if courier responds at any point
  3. Send max 3 reminders with escalation on 3rd
  4. Acknowledge (not classify) all emails
  5. Track investigation metadata in search attributes

Phase 5: Test, Clean Up, and Verify

Goal: Everything works, dead code removed, tests pass.

Changes:

Verification:

dotnet build backend/Claimit.sln
dotnet test backend/Temporal/EmailProcessing/EmailProcessing.Temporal.Tests/

Risks and Open Questions

  1. Escalation CC routing: Base SendReminderEmailRequest may not support CC recipients and multiple TO addresses. DPD 3rd+ reminders use ccRecipients + additionalTo. May need to extend the request model or override SendEmailAsync in DPDReminderHandler.

  2. Investigation wait in lifecycle hooks: The "wait for first email" edge case (non-backfill workflow with no emails) needs a home. Could be in OnSubmissionAsync or OnPostSubmissionAsync.

  3. Phase tracking: DPD uses DPDWorkflowPhase enum for detailed Temporal UI visibility. The base uses ClaimWorkflowPhase. Need to map DPD phases to base phases or allow courier-specific phase tracking.

  4. Email loop behavior difference: Base RunEmailLoopAsync processes one email per loop iteration. DPD currently drains the queue. Should DPD override to drain, or is one-per-iteration sufficient?

  5. Auto-approve reminders: DPD has _autoApproveReminders logic. Verify DPDReminderHandler respects AutoApproveReminders config (base ReminderHandler.RequestApprovalAndSendAsync may need this).


Line Count Targets

File Before Target Notes
DPDClaimWorkflow.cs 1,105 ~250-300 Init hooks + email override + identity
DPDReminderHandler.cs 157 ~180 May grow slightly for escalation
ClaimWorkflowBase.cs 2,626 ~2,626 No changes (already has infrastructure)