-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathBankingWorkflow.cs
More file actions
236 lines (207 loc) · 9.55 KB
/
BankingWorkflow.cs
File metadata and controls
236 lines (207 loc) · 9.55 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
using Trellis.Primitives;
namespace BankingExample.Workflows;
using BankingExample.Aggregates;
using BankingExample.Events;
using BankingExample.Services;
using BankingExample.ValueObjects;
using Trellis;
/// <summary>
/// Orchestrates banking operations with fraud detection, validation, and domain event publishing.
/// </summary>
public class BankingWorkflow
{
private readonly FraudDetectionService _fraudDetection;
public BankingWorkflow(FraudDetectionService fraudDetection)
{
_fraudDetection = fraudDetection;
}
/// <summary>
/// Processes a secure withdrawal with fraud detection.
/// Demonstrates: Ensure, Bind, EnsureAsync, TapAsync, RecoverOnFailure, Domain Events
/// </summary>
public async Task<Result<BankAccount>> ProcessSecureWithdrawalAsync(
BankAccount account,
Money amount,
string verificationCode,
CancellationToken cancellationToken = default)
{
async Task<Result<BankAccount>> PerformChecks(BankAccount acc)
{
// Fraud detection
var fraudResult = await _fraudDetection.AnalyzeTransactionAsync(acc, amount, "withdrawal", cancellationToken);
if (fraudResult.IsFailure)
return fraudResult.Error;
// Require MFA for large withdrawals
if (amount.Amount > 1000)
{
var mfaResult = await _fraudDetection.VerifyCustomerIdentityAsync(
acc.CustomerId,
verificationCode,
cancellationToken
);
if (mfaResult.IsFailure)
return mfaResult.Error;
}
return acc.ToResult();
}
return await account.ToResult()
.BindAsync((Func<BankAccount, Task<Result<BankAccount>>>)PerformChecks)
.BindAsync(acc => Task.FromResult(acc.Withdraw(amount, "ATM Withdrawal")))
.TapAsync((Func<BankAccount, Task>)(acc => PublishEventsAndAcceptChangesAsync(acc, cancellationToken)))
.RecoverOnFailureAsync(
predicate: error => error.Code == "fraud.detected",
funcAsync: async error =>
{
await Task.FromResult(account.Freeze("Suspicious activity detected"));
await PublishEventsAndAcceptChangesAsync(account, cancellationToken);
await NotifySecurityTeamAsync(account.CustomerId, error, cancellationToken);
return error; // Still return error after recovery
}
);
}
/// <summary>
/// Processes a transfer between accounts with full validation.
/// Demonstrates: Complex workflow with multiple validations, parallel operations using ParallelAsync, and domain events.
/// </summary>
public async Task<Result<(BankAccount From, BankAccount To)>> ProcessTransferAsync(
BankAccount fromAccount,
BankAccount toAccount,
Money amount,
string description,
CancellationToken cancellationToken = default)
{
// Validate both accounts in parallel using Result.ParallelAsync
var validationResult = await Result.ParallelAsync(
() => _fraudDetection.AnalyzeTransactionAsync(fromAccount, amount, "transfer-out", cancellationToken),
() => _fraudDetection.AnalyzeTransactionAsync(toAccount, amount, "transfer-in", cancellationToken)
).WhenAllAsync();
if (validationResult.IsFailure)
return validationResult.Error;
// Perform transfer
return await fromAccount.TransferTo(toAccount, amount, description)
.TapAsync((Func<(BankAccount From, BankAccount To), Task>)(async accounts =>
{
// Publish events from both accounts
await PublishEventsAndAcceptChangesAsync(accounts.From, cancellationToken);
await PublishEventsAndAcceptChangesAsync(accounts.To, cancellationToken);
}))
.TapAsync((Func<(BankAccount From, BankAccount To), Task>)(accounts => NotifyTransferCompleteAsync(
fromAccount.CustomerId,
toAccount.CustomerId,
amount,
cancellationToken
)));
}
/// <summary>
/// Processes daily interest payment for savings accounts.
/// Demonstrates: Domain events for interest payments.
/// </summary>
public async Task<Result<BankAccount>> ProcessInterestPaymentAsync(
BankAccount account,
decimal interestRate,
CancellationToken cancellationToken = default)
{
return await Task.FromResult(account.ToResult()
.Ensure(acc => acc.AccountType == AccountType.Savings,
Error.Domain("Interest is only paid on savings accounts"))
.Ensure(acc => acc.Status == AccountStatus.Active,
Error.Domain($"Cannot process interest for {account.Status} account"))
.Ensure(acc => acc.Balance.Amount > 0,
Error.Domain("No interest on accounts with zero or negative balance"))
.Bind(acc =>
{
var interestAmount = acc.Balance.Amount * (interestRate / 365m); // Daily interest
return Money.TryCreate(interestAmount, "USD");
}))
.BindAsync(async interest =>
{
await Task.Delay(50, cancellationToken);
return account.Deposit(interest, $"Daily interest at {interestRate:P2} APR");
})
.TapAsync((Func<BankAccount, Task>)(acc => PublishEventsAndAcceptChangesAsync(acc, cancellationToken)));
}
/// <summary>
/// Processes multiple transactions in batch with rollback on any failure.
/// Demonstrates: Transaction-like behavior with recovery and event handling.
/// </summary>
public async Task<Result<BankAccount>> ProcessBatchTransactionsAsync(
BankAccount account,
List<(Money Amount, string Description)> transactions,
CancellationToken cancellationToken = default)
{
var processedCount = 0;
foreach (var (amount, description) in transactions)
{
var result = account.Deposit(amount, description);
if (result.IsFailure)
{
// Don't accept changes - events will be discarded
Console.WriteLine($"? Transaction failed at item {processedCount + 1}, discarding {account.UncommittedEvents().Count} uncommitted events");
return Error.Domain($"Batch transaction failed at item {processedCount + 1}: {result.Error.Detail}");
}
processedCount++;
}
// All successful - publish events and accept changes
await PublishEventsAndAcceptChangesAsync(account, cancellationToken);
Console.WriteLine($"? Successfully processed {processedCount} transactions");
return account;
}
/// <summary>
/// Demonstrates the repository pattern with domain event publishing.
/// This simulates what a real repository would do.
/// </summary>
private static async Task PublishEventsAndAcceptChangesAsync(
BankAccount account,
CancellationToken cancellationToken)
{
// 1. Get uncommitted events before accepting changes
var events = account.UncommittedEvents();
if (events.Count == 0)
return;
// 2. Simulate persisting the aggregate (in real code, this would save to database)
await Task.Delay(20, cancellationToken);
// 3. Publish each domain event
foreach (var domainEvent in events)
{
await PublishEventAsync(domainEvent, cancellationToken);
}
// 4. Accept changes - clears the uncommitted events list
account.AcceptChanges();
Console.WriteLine($"📢 Published {events.Count} domain event(s) for account {account.Id}");
}
private static async Task PublishEventAsync(IDomainEvent domainEvent, CancellationToken cancellationToken)
{
await Task.Delay(10, cancellationToken);
// Log the event type and key information
var eventInfo = domainEvent switch
{
AccountOpened e => $"AccountOpened: {e.AccountId}, Type: {e.AccountType}, Balance: {e.InitialBalance}",
MoneyDeposited e => $"MoneyDeposited: {e.Amount} -> Balance: {e.NewBalance}",
MoneyWithdrawn e => $"MoneyWithdrawn: {e.Amount} -> Balance: {e.NewBalance}",
TransferCompleted e => $"TransferCompleted: {e.Amount} from {e.FromAccountId} to {e.ToAccountId}",
AccountFrozen e => $"AccountFrozen: {e.AccountId}, Reason: {e.Reason}",
AccountUnfrozen e => $"AccountUnfrozen: {e.AccountId}",
AccountClosed e => $"AccountClosed: {e.AccountId}",
InterestPaid e => $"InterestPaid: {e.InterestAmount} at {e.AnnualRate:P2}",
_ => domainEvent.GetType().Name
};
Console.WriteLine($" 📋 Event: {eventInfo}");
}
private static async Task NotifyTransferCompleteAsync(
CustomerId fromCustomerId,
CustomerId toCustomerId,
Money amount,
CancellationToken cancellationToken)
{
await Task.Delay(50, cancellationToken);
Console.WriteLine($"📧 Transfer notification sent: {amount} from {fromCustomerId} to {toCustomerId}");
}
private static async Task NotifySecurityTeamAsync(
CustomerId customerId,
Error error,
CancellationToken cancellationToken)
{
await Task.Delay(100, cancellationToken);
Console.WriteLine($"🚨 Security alert for customer {customerId}: {error.Detail}");
}
}