DPD Claim Workflow Refactor Plan
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<TState><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:#e1f5feLifecycle 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()
endCurrent State
DPDClaimWorkflow (1,105 lines) extends ClaimWorkflowBase directly with:
- Manual
_stateshadowing +base._state = _state - Full
RunAsyncwith entire lifecycle inlined (init → investigation wait → reminder loop → completion) CheckAndSendRemindersAsyncoverride (200 lines) duplicating base logic + DPD timing- Inline email templates, manual state management, own wait methods
- Hardcoded
DPDWorkflowConstantsinstead of configurable intervals
Target architecture (as used by Asendia, DHL, InPost, RoyalMail):
- Extends
ClaimWorkflow<TState>(typed state, no manual shadowing) - One-liner
RunAsync→base.RunAsync()(template method handles lifecycle) ReminderHandlersubclass for courier-specific reminder behavior- Lifecycle hooks for courier-specific logic
AwaitResponseAsync(intervals, onElapsed)for waiting patterns
DPD's Unique Concepts
These must survive the refactor:
- Investigation period: DPD says "we'll investigate for X days" — first reminder waits for this
- Acknowledge-only email processing: DPD doesn't classify emails — existing automation handles that
- Courier response detection: If courier responds after investigation, workflow completes early
- Investigation-based reminder timing: First reminder = investigation due date, subsequent = interval
- Escalation routing: 3rd reminder goes to multiple escalation contacts with CC
- 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:#e8f5e9Changes:
- Apply stashed changes to
ClaimWorkflowBase:- Template
RunAsyncwith lifecycle phases (Submitting → WaitingForOutcome → Completed) RunEmailLoopAsync(private email/reminder loop)AwaitResponseAsync(intervals, onElapsed)reusable wait patternIWorkflowContextinterface forReminderHandlercomposition- Lifecycle hooks (
OnSubmissionAsync,OnPostSubmissionAsync,OnPreWorkflowCompleteAsync, etc.) - Properties:
Config,ShouldComplete,CompletionReason,EmailQueue
- Template
- Apply stashed changes to already-migrated couriers (Asendia, DHL, InPost, RoyalMail)
- Verify
ReminderHandler.csandDPDReminderHandler.csare present (committed on branch) - Build solution:
dotnet build Claimit.sln - Run existing tests to confirm no regressions
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<DPDClaimWorkflowState>"]
A2 --> A3["DPDClaimWorkflow<br/>(typed State property)"]
end
style B2 fill:#fce4ec
style A3 fill:#e8f5e9
style A2 fill:#e1f5feChanges:
- Change
DPDClaimWorkflow : ClaimWorkflowBase→DPDClaimWorkflow : ClaimWorkflow<DPDClaimWorkflowState> - Remove
private new DPDClaimWorkflowState _state = new()→ useStateproperty - Replace all
_state.references withState.in DPDClaimWorkflow - Remove
base._state = _statefrom RunAsync (generic handles this) - Remove
CreateDPDInitialState→ overrideCreateStateinstead - Override
CreateContinueAsNewException -
RunWithStateAsync→ResumeAfterContinueAsNewAsync(state, config) - Build and verify
Verification:
-
dotnet build backend/Claimit.slncompiles clean - DPD's custom
RunAsyncstill works (behavior unchanged, just state access changed)
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:#e8f5e9Changes:
- Override
CreateReminderHandlerin DPDClaimWorkflow to returnDPDReminderHandler - Delete from DPDClaimWorkflow (moved to DPDReminderHandler):
-
GetReminderEmailSubject(int)→ already inDPDReminderHandler.GetSubject -
GetReminderEmailBody(int)→ already inDPDReminderHandler.GetBody -
CalculateNextReminderDueDate()→ already inDPDReminderHandler.CalculateNextDueDate
-
- Delete
CheckAndSendRemindersAsyncoverride from DPDClaimWorkflow - Verify escalation routing works:
- May need to override
SendEmailAsyncin DPDReminderHandler for CC/additional-TO logic - Check: Does base
SendReminderEmailRequestsupport CC recipients?
- May need to override
- Build and verify
Verification:
- Build compiles
- Reminder timing logic matches: investigation period → first reminder, interval → subsequent
- Email templates match exactly (HTML content, subject line RE: prefix)
- Escalation routing preserved (3rd reminder → multiple contacts)
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:
-
OnSubmissionAsync: Investigation period initialization- Move
InitializeInvestigationPeriodAsync()here - Move early-exit for
CourierRespondedAfterInvestigation
- Move
-
OnPostSubmissionAsync: Investigation period wait- Move
WaitForInvestigationPeriodAsync()here - If courier responded: set
ShouldComplete = true, return → base skips email loop
- Move
- Override
OnProcessEmailAsync: Acknowledge-only- Replace full classification pipeline with
AcknowledgeEmailReceivedlogic - Check
IsCourierResponseAfterInvestigation→ setShouldComplete
- Replace full classification pipeline with
- Slim
RunAsyncto one-liner:=> await base.RunAsync(request) - Delete from DPDClaimWorkflow:
-
WaitForInvestigationPeriodAsync()→ moved toOnPostSubmissionAsync -
WaitForCourierResponseAsync()→ replaced by base email loop -
AcknowledgeEmailReceived()→ moved toOnProcessEmailAsyncoverride - Inline reminder loop (the entire
while (!_shouldComplete)block) -
RunWithStateAsync→ simplify toResumeAfterContinueAsNewAsync
-
Verification:
- Build compiles
- Test: Backfill flow still works (investigation init + wait + reminders)
- Test: Email signal during investigation → acknowledged, response detected
- Test: Email signal during reminder loop → acknowledged, response detected
- Test: Max reminders → workflow completes
- Test: Continue-as-new works
Key behavioral requirement: The refactored DPD workflow MUST:
- Wait for investigation period before first reminder (DPD-specific)
- Complete early if courier responds at any point
- Send max 3 reminders with escalation on 3rd
- Acknowledge (not classify) all emails
- Track investigation metadata in search attributes
Phase 5: Test, Clean Up, and Verify
Goal: Everything works, dead code removed, tests pass.
Changes:
- Remove any unused DPD methods/fields
- Remove
IsCourierResponseAfterInvestigationif logic folded intoOnProcessEmailAsync - Verify search attribute updates still work (phase tracking, next reminder due)
- Run full test suite
- Manual check: DPD workflow line count target < 300 lines (from 1,105)
Verification:
dotnet build backend/Claimit.sln
dotnet test backend/Temporal/EmailProcessing/EmailProcessing.Temporal.Tests/- Review final DPDClaimWorkflow.cs for dead code
- Compare behavior with test scenarios against pre-refactor
Risks and Open Questions
Escalation CC routing: Base
SendReminderEmailRequestmay not support CC recipients and multiple TO addresses. DPD 3rd+ reminders useccRecipients+additionalTo. May need to extend the request model or overrideSendEmailAsyncinDPDReminderHandler.Investigation wait in lifecycle hooks: The "wait for first email" edge case (non-backfill workflow with no emails) needs a home. Could be in
OnSubmissionAsyncorOnPostSubmissionAsync.Phase tracking: DPD uses
DPDWorkflowPhaseenum for detailed Temporal UI visibility. The base usesClaimWorkflowPhase. Need to map DPD phases to base phases or allow courier-specific phase tracking.Email loop behavior difference: Base
RunEmailLoopAsyncprocesses one email per loop iteration. DPD currently drains the queue. Should DPD override to drain, or is one-per-iteration sufficient?Auto-approve reminders: DPD has
_autoApproveReminderslogic. VerifyDPDReminderHandlerrespectsAutoApproveRemindersconfig (baseReminderHandler.RequestApprovalAndSendAsyncmay 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) |