Skip to content

Commit cdda29a

Browse files
committed
Workflow works with failed and successful payments
1 parent 3d37173 commit cdda29a

File tree

10 files changed

+109
-43
lines changed

10 files changed

+109
-43
lines changed

.vscode/launch.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"request": "launch",
3131
"preLaunchTask": "checkout debug",
3232
"postDebugTask": "checkout daprd-down",
33-
"program": "${workspaceFolder}/CheckoutService/bin/Debug/net7.0/CheckoutWorkflowSample.dll",
33+
"program": "${workspaceFolder}/CheckoutService/bin/Debug/net7.0/CheckoutService.dll",
3434
"args": [],
3535
"cwd": "${workspaceFolder}/CheckoutService",
3636
"stopAtEntry": false,

.vscode/tasks.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"type": "process",
4444
"args": [
4545
"build",
46-
"${workspaceFolder}/CheckoutService/CheckoutWorkflowSample.csproj",
46+
"${workspaceFolder}/CheckoutService/CheckoutService.csproj",
4747
"/property:GenerateFullPaths=true",
4848
"/consoleloggerparameters:NoSummary"
4949
],
@@ -55,7 +55,7 @@
5555
"type": "process",
5656
"args": [
5757
"publish",
58-
"${workspaceFolder}/CheckoutService/CheckoutWorkflowSample.csproj",
58+
"${workspaceFolder}/CheckoutService/CheckoutService.csproj",
5959
"/property:GenerateFullPaths=true",
6060
"/consoleloggerparameters:NoSummary"
6161
],
@@ -69,7 +69,7 @@
6969
"watch",
7070
"run",
7171
"--project",
72-
"${workspaceFolder}/CheckoutService/CheckoutWorkflowSample.csproj"
72+
"${workspaceFolder}/CheckoutService/CheckoutService.csproj"
7373
],
7474
"problemMatcher": "$msCompile"
7575
},
Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
using Dapr.Workflow;
21
using CheckoutService.Models;
32
using Dapr.Client;
3+
using Dapr.Workflow;
44

55
namespace CheckoutService.Activities
66
{
7-
public class ProcessPaymentActivity : WorkflowActivity<PaymentRequest, object?>
7+
public class ProcessPaymentActivity : WorkflowActivity<PaymentRequest, PaymentResponse>
88
{
99
private readonly ILogger _logger;
1010
private readonly DaprClient _client;
@@ -15,25 +15,44 @@ public ProcessPaymentActivity(ILoggerFactory loggerFactory, DaprClient client)
1515
_client = client;
1616
}
1717

18-
public override async Task<object?> RunAsync(WorkflowActivityContext context, PaymentRequest req)
18+
public override async Task<PaymentResponse> RunAsync(WorkflowActivityContext context, PaymentRequest request)
1919
{
2020
_logger.LogInformation(
21-
"Processing payment: {requestId} for {name} at ${totalCost}",
22-
req.RequestId,
23-
req.Name,
24-
req.TotalCost);
21+
"Calling PaymentService for: {requestId} {name} at ${totalCost}",
22+
request.RequestId,
23+
request.Name,
24+
request.TotalCost);
2525

2626
// Simulate slow processing
2727
await Task.Delay(TimeSpan.FromSeconds(3));
2828

29-
var request = _client.CreateInvokeMethodRequest(HttpMethod.Post, "payment", "pay", req);
30-
await _client.InvokeMethodAsync(request);
29+
var methodRequest = _client.CreateInvokeMethodRequest(HttpMethod.Post, "payment", "pay", request);
30+
try
31+
{
32+
await _client.InvokeMethodAsync(methodRequest);
33+
_logger.LogInformation(
34+
"Payment for request ID '{requestId}' processed successfully",
35+
request.RequestId);
3136

32-
_logger.LogInformation(
33-
"Payment for request ID '{requestId}' processed successfully",
34-
req.RequestId);
37+
return new PaymentResponse(request.RequestId, true);
38+
}
39+
catch (Exception ex)
40+
{
41+
if (ex.InnerException != null && ex.InnerException.Message.Contains("500"))
42+
{
43+
// Throw internal server errors up to the workflow so
44+
// this activity can be retried.
45+
throw;
46+
}
47+
48+
// Any other exception is treated as a failed payment.
49+
_logger.LogWarning(
50+
"Payment for request ID '{requestId}' failed",
51+
request.RequestId);
52+
53+
return new PaymentResponse(request.RequestId, false);
3554

36-
return null;
55+
}
3756
}
3857
}
3958
}

CheckoutService/Activities/RefundPaymentActivity.cs

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
namespace CheckoutService.Activities
66
{
7-
public class RefundPaymentActivity : WorkflowActivity<PaymentRequest, object?>
7+
public class RefundPaymentActivity : WorkflowActivity<PaymentRequest, PaymentResponse>
88
{
99
private readonly ILogger _logger;
1010
private readonly DaprClient _client;
@@ -15,25 +15,33 @@ public RefundPaymentActivity(ILoggerFactory loggerFactory, DaprClient client)
1515
_logger = loggerFactory.CreateLogger<RefundPaymentActivity>();
1616
}
1717

18-
public override async Task<object?> RunAsync(WorkflowActivityContext context, PaymentRequest req)
18+
public override async Task<PaymentResponse> RunAsync(WorkflowActivityContext context, PaymentRequest request)
1919
{
2020
_logger.LogInformation(
2121
"Refunding payment: {requestId} for {name} at ${totalCost}",
22-
req.RequestId,
23-
req.Name,
24-
req.TotalCost);
22+
request.RequestId,
23+
request.Name,
24+
request.TotalCost);
2525

2626
// Simulate slow processing
2727
await Task.Delay(TimeSpan.FromSeconds(3));
2828

29-
var request = _client.CreateInvokeMethodRequest(HttpMethod.Post, "payment", "refund", req);
30-
await _client.InvokeMethodAsync(request);
31-
32-
_logger.LogInformation(
33-
"Payment for request ID '{requestId}' refunded successfully",
34-
req.RequestId);
35-
36-
return null;
29+
var methodRequest = _client.CreateInvokeMethodRequest(HttpMethod.Post, "payment", "refund", request);
30+
try
31+
{
32+
await _client.InvokeMethodAsync(methodRequest);
33+
_logger.LogInformation(
34+
"Refund for request ID '{requestId}' processed successfully",
35+
request.RequestId);
36+
return new PaymentResponse(request.RequestId, true);
37+
}
38+
catch (Exception)
39+
{
40+
_logger.LogWarning(
41+
"Refund for request ID '{requestId}' failed",
42+
request.RequestId);
43+
return new PaymentResponse(request.RequestId, false);
44+
}
3745
}
3846
}
3947
}

CheckoutService/Models.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ public record InventoryItem(int ProductId, string Name, double PerItemCost, int
66
public record InventoryRequest(string RequestId, string ItemName, int Quantity);
77
public record InventoryResult(bool InStock, InventoryItem? OrderPayload, double TotalCost);
88
public record PaymentRequest(string RequestId, string Name, double TotalCost);
9+
public record PaymentResponse(string RequestId, bool IsPaymentSuccess);
910
public record Notification(string Message);
1011
}

CheckoutService/Workflows/CheckoutWorkflow.cs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1+
using Microsoft.DurableTask;
12
using Dapr.Workflow;
2-
33
using CheckoutService.Activities;
44
using CheckoutService.Models;
5-
using Microsoft.DurableTask;
65

76
namespace CheckoutService.Workflows
87
{
@@ -19,7 +18,7 @@ await context.CallActivityAsync(
1918

2019
// Determine if there is enough of the item available for purchase by checking the inventory
2120
var inventoryRequest = new InventoryRequest(RequestId: orderId, order.Name, order.Quantity);
22-
InventoryResult inventoryResult = await context.CallActivityAsync<InventoryResult>(
21+
var inventoryResult = await context.CallActivityAsync<InventoryResult>(
2322
nameof(CheckInventoryActivity),
2423
inventoryRequest);
2524

@@ -35,13 +34,21 @@ await context.CallActivityAsync(
3534
return new CheckoutResult(Processed: false);
3635
}
3736

37+
// Create a RetryPolicy to retry calling the ProcessPaymentActivity in case it fails.
3838
var taskOptions = new TaskOptions(
3939
new TaskRetryOptions(
4040
new RetryPolicy(10, TimeSpan.FromSeconds(1), 2)));
41-
await context.CallActivityAsync(
41+
var paymentResponse = await context.CallActivityAsync<PaymentResponse>(
4242
nameof(ProcessPaymentActivity),
4343
new PaymentRequest(orderId, order.Name, inventoryResult.TotalCost), taskOptions);
4444

45+
if (!paymentResponse.IsPaymentSuccess)
46+
{
47+
context.SetCustomStatus("Stopped order process due to payment issue.");
48+
49+
return new CheckoutResult(Processed: false);
50+
}
51+
4552
try
4653
{
4754
await context.CallActivityAsync(

CheckoutService/checkout.http

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ POST {{dapr_url}}/v1.0-alpha1/workflows/dapr/CheckoutWorkflow/{{workflow_id}}/st
1818
Content-Type: application/json
1919

2020
{
21-
"input" : {"Name": "Paperclips", "Quantity": 100}
21+
"input" : {"Name": "Paperclips", "Quantity": 25}
2222
}
2323

2424
### Get status

PaymentService/Program.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@
2424
if (isPaymentSuccess)
2525
{
2626
Console.WriteLine("Payment successful : " + request.RequestId);
27+
2728
return Results.Accepted();
2829
}
2930
else
3031
{
3132
Console.WriteLine("Payment failed : " + request.RequestId);
33+
3234
return Results.BadRequest();
3335
}
3436
});
@@ -40,11 +42,13 @@
4042
if (isPaymentSuccess)
4143
{
4244
Console.WriteLine("Refund successful : " + request.RequestId);
45+
4346
return Results.Accepted();
4447
}
4548
else
4649
{
4750
Console.WriteLine("Refund failed : " + request.RequestId);
51+
4852
return Results.BadRequest();
4953
}
5054
});
@@ -61,11 +65,14 @@ async Task<bool> GetConfigItemAsync()
6165
{
6266
Console.WriteLine("Can't parse isPaymentSuccessItem to boolean.");
6367
}
68+
6469
return isPaymentSuccess;
6570
}
6671
else
6772
{
6873
Console.WriteLine("isPaymentSuccessItem not found");
74+
75+
//default to true so the happy path of the CheckoutWorkflow always works.
6976
return true;
7077
}
7178
}

README.md

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ graph TD
355355
}
356356
```
357357
358-
## CheckoutWorkflow sample
358+
## CheckoutService sample
359359
360360
```mermaid
361361
flowchart LR
@@ -366,7 +366,7 @@ A --> B
366366
A --> C
367367
```
368368
369-
The CheckoutWorkflowSample app contains workflow that processes an order. The workflow takes an `OrderItem` as input and returns a `CheckoutResult` as output. The `CheckoutWorkflow` workflow uses these activities:
369+
The CheckoutService app contains workflow that processes an order. The workflow takes an `OrderItem` as input and returns a `CheckoutResult` as output. The `CheckoutWorkflow` workflow uses these activities:
370370
371371
- `NotifyActivity`: Notifies the customer of the progress of the order.
372372
- `CheckInventoryActivity`: Checks if the inventory is sufficient.
@@ -410,12 +410,26 @@ The `InventoryController` also uses Dapr's state management building block.
410410
411411
### Run the PaymentService app
412412
413-
The CheckoutWorkflowSample app relies on the PaymentService app to process the payment. The PaymentService app is a small ASP.NET app that exposes two endpoints:
413+
The CheckoutService app relies on the PaymentService app to process the payment. The PaymentService app is a small ASP.NET app that exposes two endpoints:
414414
415415
- `/pay`: processes the payment
416416
- `/refund`: refunds the payment
417417
418-
This service will be started first before the CheckoutWorkflowSample app is started.
418+
The PaymentService app uses the Dapr Configuration API to read the `isPaymentSuccess` configuration item from the configuration store (Redis). If the item key is not present, or if the item value is set to the string "true", the payment will be successful. If the item value is set to the string "false", the payment will fail. Use this setting to simulate a failed payment and check the workflow result.
419+
420+
Setting the configuration item is done via the redis-cli in the dapr_redis docker container:
421+
422+
```bash
423+
docker exec dapr_redis redis-cli MSET isPaymentSuccess "true"
424+
```
425+
426+
To configure the PaymentService to return a failed payment response use:
427+
428+
```bash
429+
docker exec dapr_redis redis-cli MSET isPaymentSuccess "false"
430+
```
431+
432+
Set the `isPaymentSuccess` config item to "true" and start the PaymentService as follows:
419433
420434
1. Change to the PaymentService directory and build the ASP.NET app:
421435
@@ -427,10 +441,10 @@ This service will be started first before the CheckoutWorkflowSample app is star
427441
2. Run the app using the Dapr CLI:
428442
429443
```bash
430-
dapr run --app-id payment --app-port 5063 --dapr-http-port 3501 dotnet run
444+
dapr run --app-id payment --app-port 5063 --dapr-http-port 3501 --resources-path ./Resources dotnet run
431445
```
432446
433-
### Run the CheckoutWorkflowSample app
447+
### Run the CheckoutService app
434448
435449
1. Change to the CheckoutService directory and build the ASP.NET app:
436450
@@ -519,7 +533,7 @@ This service will be started first before the CheckoutWorkflowSample app is star
519533
```bash
520534
curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/CheckoutWorkflow/1234d/start \
521535
-H "Content-Type: application/json" \
522-
-d '{ "input" : {"Name": "Paperclips", "Quantity": 100}}'
536+
-d '{ "input" : {"Name": "Paperclips", "Quantity": 25}}'
523537
```
524538
525539
> Note that `1234d` in the URL is the workflow instance ID. This can be any string you want.
@@ -550,7 +564,7 @@ This service will be started first before the CheckoutWorkflowSample app is star
550564
"start_time": "2023-05-01T12:22:25Z",
551565
"metadata": {
552566
"dapr.workflow.custom_status": "",
553-
"dapr.workflow.input": "{\"Name\":\"Paperclips\",\"Quantity\":100}",
567+
"dapr.workflow.input": "{\"Name\":\"Paperclips\",\"Quantity\":25}",
554568
"dapr.workflow.last_updated": "2023-05-01T12:22:37Z",
555569
"dapr.workflow.name": "CheckoutWorkflow",
556570
"dapr.workflow.output": "{\"Processed\":true}",
@@ -563,6 +577,16 @@ This service will be started first before the CheckoutWorkflowSample app is star
563577
564578
![Checkout workflow in Zipkin](images/checkout_zipkin.png)
565579
580+
### Unhappy paths
581+
582+
Now try these different scenarios and check the workflow result.
583+
584+
Start the CheckoutWorkflow with:
585+
586+
1. Shutting down the CheckoutService app once the CheckoutWorkflow has started. Restart the CheckoutService and watch the logs.
587+
2. A failing payment (set the `isPaymentSuccess` configuration item to "false").
588+
3. Shut down the PaymentService completely (check the logs for the retry attempts).
589+
566590
## Resources
567591
568592
1. [Dapr Workflow overview](https://docs.dapr.io/developing-applications/building-blocks/workflow/workflow-overview/).

0 commit comments

Comments
 (0)