-
Notifications
You must be signed in to change notification settings - Fork 7
/
payment_handler.ts
104 lines (88 loc) · 3.65 KB
/
payment_handler.ts
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
/*
* Copyright (c) 2024 - Restate Software, Inc., Restate GmbH
*
* This file is part of the Restate SDK for Node.js/TypeScript,
* which is released under the MIT license.
*
* You can find a copy of the license in file LICENSE in the root
* directory of this repository or package, or at
* https://github.com/restatedev/sdk-typescript/blob/main/LICENSE
*/
import * as restate from "@restatedev/restate-sdk";
import * as stripe_utils from "./utils/stripe_utils";
import { verifyPaymentRequest } from "./utils/utils";
import Stripe from "stripe";
//
// The payment handler that issues calls to Stripe.
// - the result often comes synchronously as a response API call.
// - some requests (and some payment methods) only return "processing" and
// notify later via a webhook.
//
// This example combines both paths in a single function that reliably waits for both
// paths, if needed, thus giving you a single long-running synchronous function.
// Durable execution and the persistent awakeable promises combine this into a single
// reliably promise/async-function.
//
// See README on how to run this example (needs a Stripe test account).
//
type PaymentRequest = {
amount: number;
paymentMethodId: string;
delayedStatus?: boolean;
};
async function processPayment(ctx: restate.Context, request: PaymentRequest) {
verifyPaymentRequest(request);
// Generate a deterministic idempotency key
const idempotencyKey = ctx.rand.uuidv4();
// Initiate a listener for external calls for potential webhook callbacks
const webhookPromise = ctx.awakeable<Stripe.PaymentIntent>();
// Make a synchronous call to the payment service
const paymentIntent = await ctx.run("stripe call", () =>
stripe_utils.createPaymentIntent({
paymentMethodId: request.paymentMethodId,
amount: request.amount,
idempotencyKey,
webhookPromiseId: webhookPromise.id,
delayedStatus: request.delayedStatus,
})
);
if (paymentIntent.status !== "processing") {
// The synchronous call to Stripe had already been completed.
// That was fast :)
ctx.console.log(`Request ${idempotencyKey} was processed synchronously!`);
stripe_utils.ensureSuccess(paymentIntent.status);
return;
}
// We did not get the response on the synchronous path, talking to Stripe.
// No worries, Stripe will let us know when it is done processing via a webhook.
ctx.console.log(
`Synchronous response for ${idempotencyKey} yielded 'processing', awaiting webhook call...`
);
// We will now wait for the webhook call to complete this promise.
// Check out the handler below.
const processedPaymentIntent = await webhookPromise.promise;
console.log(`Webhook call for ${idempotencyKey} received!`);
stripe_utils.ensureSuccess(processedPaymentIntent.status);
}
async function processWebhook(ctx: restate.Context) {
const req = ctx.request();
const sig = req.headers.get("stripe-signature");
const event = stripe_utils.parseWebhookCall(req.body, sig);
if (!stripe_utils.isPaymentIntent(event)) {
ctx.console.log(`Unhandled event type ${event.type}`);
return { received: true };
}
const paymentIntent = event.data.object as Stripe.PaymentIntent;
ctx.console.log(JSON.stringify(paymentIntent));
const webhookPromise =
paymentIntent.metadata[stripe_utils.RESTATE_CALLBACK_ID];
if (!webhookPromise) {
throw new restate.TerminalError(
"Missing callback property: " + stripe_utils.RESTATE_CALLBACK_ID,
{ errorCode: 404 }
);
}
ctx.resolveAwakeable(webhookPromise, paymentIntent);
return { received: true };
}
restate.endpoint().bind(restate.service({name: "payments", handlers: { processPayment, processWebhook }})).listen(9080);