TECH-589 ClaimWorkflowBase Encapsulation Refactor
What Changed
This refactor replaces direct field access across courier workflows with property accessors and removes EmailData from hook method signatures. The goal: tighten encapsulation on ClaimWorkflowBase so subclasses interact through a well-defined surface area instead of reaching into internals.
Before → After
graph LR
subgraph Before["Before: Direct Field Access"]
direction TB
B1["_config"] -.-> C1["protected field"]
B2["_shouldComplete"] -.-> C2["protected field"]
B3["_completionReason"] -.-> C3["protected field"]
B4["_emailQueue"] -.-> C4["protected field"]
B5["_isBackfill"] -.-> C5["protected field"]
end
subgraph After["After: Property Accessors"]
direction TB
A1["Config"] --> P1["private _config"]
A2["ShouldComplete"] --> P2["auto-property"]
A3["CompletionReason"] --> P3["auto-property"]
A4["EmailQueue"] --> P4["private _emailQueue"]
A5["CurrentEmail"] --> P5["private _currentEmail"]
end
style Before fill:#fce4ec
style After fill:#e8f5e9Field → Accessor Mapping
| Old (protected field) | New (accessor) | Access | Notes |
|---|---|---|---|
_config |
Config { get; private protected set; } |
Read everywhere, write from base + derived | Setter needed for continue-as-new |
_shouldComplete |
ShouldComplete { get; set; } |
Read + Write | Auto-property replaces field |
_completionReason |
CompletionReason { get; set; } |
Read + Write | Auto-property replaces field |
_emailQueue |
EmailQueue (readonly property) |
Read + Dequeue | Backing field stays private readonly |
_isBackfill |
(no accessor) | Private only | Not accessed by any courier |
| (new) | CurrentEmail |
Read only | Throws if accessed outside email processing |
Visibility Changes
Two methods that were unnecessarily protected virtual are now correctly scoped:
| Method | Before | After | Reason |
|---|---|---|---|
RunEmailLoopAsync() |
protected virtual |
private |
No courier overrides this — they either use the base template or own their entire loop |
IsFinalClassification() |
protected virtual |
private |
Classification finality is a base concern, not courier-specific |
Hook Signature Cleanup
The 4 email processing hooks previously passed EmailData through their signatures. Since the base already tracks which email is being processed, hooks can access it via CurrentEmail instead.
sequenceDiagram
participant Base as ClaimWorkflowBase
participant Hook as Courier Override
Note over Base: _currentEmail = email
Base->>Hook: OnPreEmailProcessedAsync()
Note over Hook: Access via CurrentEmail if needed
Base->>Base: Classify + Process
Base->>Hook: OnPreClaimStatusUpdateAsync(classification, processingData)
Note over Hook: DHL uses CurrentEmail here
Base->>Base: Update claim status
Base->>Hook: OnPostClaimStatusUpdateAsync(classification, result)
Base->>Hook: OnPostEmailProcessedAsync(classification)
Note over Base: _currentEmail = null (finally block)Before
protected virtual Task OnPreEmailProcessedAsync(EmailData email) { ... }
protected virtual Task OnPostEmailProcessedAsync(EmailData email, ClassificationResult classification) { ... }
protected virtual Task OnPreClaimStatusUpdateAsync(EmailData email, ClassificationResult classification, ProcessingResult processingData) { ... }
protected virtual Task OnPostClaimStatusUpdateAsync(EmailData email, ClassificationResult classification, UpdateClaimStatusResponse result) { ... }After
protected virtual Task OnPreEmailProcessedAsync() { ... }
protected virtual Task OnPostEmailProcessedAsync(ClassificationResult classification) { ... }
protected virtual Task OnPreClaimStatusUpdateAsync(ClassificationResult classification, ProcessingResult processingData) { ... }
protected virtual Task OnPostClaimStatusUpdateAsync(ClassificationResult classification, UpdateClaimStatusResponse result) { ... }DHL Override (only real override)
// Before
protected override Task OnPreClaimStatusUpdateAsync(
EmailData email, ClassificationResult classification, ProcessingResult processingData)
{
if (classification.PredictedCategory.Equals("Credit", ...))
CheckForBankDetailsRequest(email, processingData); // ← email parameter
return Task.CompletedTask;
}
// After
protected override Task OnPreClaimStatusUpdateAsync(
ClassificationResult classification, ProcessingResult processingData)
{
if (classification.PredictedCategory.Equals("Credit", ...))
CheckForBankDetailsRequest(CurrentEmail, processingData); // ← property
return Task.CompletedTask;
}Files Changed
All paths relative to backend/Temporal/EmailProcessing/EmailProcessing.Workflows/Couriers/
graph TD
subgraph Base["Common/"]
CWB["ClaimWorkflowBase.cs<br/>+accessors, +CurrentEmail<br/>hook signatures, _field→Property"]
end
subgraph Couriers["Courier Workflows"]
DHL["DHLClaimWorkflow.cs<br/>hook signature + field renames"]
DPD["DPD/DPDClaimWorkflow.cs<br/>field renames"]
IP["InPostClaimWorkflow.cs<br/>field renames"]
RM["RoyalMail/RoyalMailClaimWorkflow.cs<br/>field renames"]
AS["AsendiaClaimWorkflow.cs<br/>field renames"]
end
CWB --> DHL
CWB --> DPD
CWB --> IP
CWB --> RM
CWB --> AS
style CWB fill:#e1f5fe
style DHL fill:#fff3e0
style DPD fill:#e8f5e9
style IP fill:#e8f5e9
style RM fill:#e8f5e9
style AS fill:#e8f5e9Change summary per file
| File | What changed |
|---|---|
| ClaimWorkflowBase.cs | Fields → private + accessors, CurrentEmail property, hook signatures cleaned, _config. → Config. throughout, _shouldComplete → ShouldComplete, _completionReason → CompletionReason, RunEmailLoopAsync / IsFinalClassification → private |
| DHLClaimWorkflow.cs | Hook signature change (email param removed), email → CurrentEmail, _emailQueue → EmailQueue, _shouldComplete → ShouldComplete, _completionReason → CompletionReason, _config → Config |
| DPDClaimWorkflow.cs | _emailQueue → EmailQueue, _shouldComplete → ShouldComplete, _completionReason → CompletionReason, _config → Config |
| InPostClaimWorkflow.cs | Same field renames as DPD |
| RoyalMailClaimWorkflow.cs | _config → Config, _shouldComplete → ShouldComplete, _completionReason → CompletionReason |
| AsendiaClaimWorkflow.cs | _config → Config |
What's Next
Planned: AwaitResponseAsync primitive
A general-purpose "wait for reply, escalate if none" method to be added to ClaimWorkflowBase:
protected async Task<bool> AwaitResponseAsync(
TimeSpan[] intervals, Func<ReminderContext, Task> onElapsed)This gives couriers a declarative way to express chase patterns in their submission hooks:
// In AsendiaClaimWorkflow.OnSubmissionAsync, after submission:
await AwaitResponseAsync(
_reminderHandler.GetIntervals(),
async reminder => await _reminderHandler.RequestApprovalAndSendAsync(reminder.Number));RunEmailLoopAsync stays as-is — AwaitResponseAsync is additive, not a replacement.
Verification
dotnet build Claimit.sln— 0 errorsdotnet test --filter "AsendiaClaimWorkflow"— 41 passed, 9 skippeddotnet test --filter "RoyalMailClaimWorkflow"— 12 passed- Grep for
_config\.,_shouldComplete,_completionReason,_emailQueuein courier files — 0 hits