From 8fc856e9ffe1caa87c08a2607eb9a680812ebf87 Mon Sep 17 00:00:00 2001 From: Giselle van Dongen Date: Fri, 20 Dec 2024 10:41:19 +0100 Subject: [PATCH] Fix Stripe payments example --- java/patterns-use-cases/README.md | 2 +- .../signalspayments/PaymentService.java | 2 +- python/patterns-use-cases/README.md | 62 ++++++++++---- python/patterns-use-cases/requirements.txt | 3 +- .../src/sagas/booking_workflow.py | 9 +- .../src/signalspayments/__init__.py | 0 .../src/signalspayments/payment_service.py | 79 ++++++++++++++++++ .../src/signalspayments/stripe_utils.py | 83 +++++++++++++++++++ .../async-tasks-payment-signals/README.md | 2 +- .../async-tasks-payment-signals/package.json | 2 - .../src/payment_handler.ts | 7 +- .../src/utils/stripe_utils.ts | 9 ++ .../src/utils/utils.ts | 21 ----- 13 files changed, 229 insertions(+), 52 deletions(-) create mode 100644 python/patterns-use-cases/src/signalspayments/__init__.py create mode 100644 python/patterns-use-cases/src/signalspayments/payment_service.py create mode 100644 python/patterns-use-cases/src/signalspayments/stripe_utils.py delete mode 100644 typescript/patterns-use-cases/async-tasks-payment-signals/src/utils/utils.ts diff --git a/java/patterns-use-cases/README.md b/java/patterns-use-cases/README.md index 120abebf..6aef1e9f 100644 --- a/java/patterns-use-cases/README.md +++ b/java/patterns-use-cases/README.md @@ -375,7 +375,7 @@ webhooks to your local machine. 7. Go to the Stripe UI and create a webhook. Select all _"Payment Intent"_ event types. Put the ngrok public URL + `/PaymentService/processWebhook` as the webhook URL (you need to update this whenever you stop/start ngrok). - Example: `https://.ngrok-free.app/PaymentService/processWebhooks` + Example: `https://.ngrok-free.app/PaymentService/processWebhook` 8. Put the webhook secret (`whsec_...`) to the [StripeUtils.java](src/main/java/my/example/signalspayment/StripeUtils.java) file. diff --git a/java/patterns-use-cases/src/main/java/my/example/signalspayments/PaymentService.java b/java/patterns-use-cases/src/main/java/my/example/signalspayments/PaymentService.java index 25f5d018..00cbbf18 100644 --- a/java/patterns-use-cases/src/main/java/my/example/signalspayments/PaymentService.java +++ b/java/patterns-use-cases/src/main/java/my/example/signalspayments/PaymentService.java @@ -78,7 +78,7 @@ public void processPayment(Context ctx, PaymentRequest request) { // 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. logger.info( - "Synchronous response for {} yielded 'processing', awaiting webhook call...", + "Payment intent for {} still 'processing', awaiting webhook call...", idempotencyKey); // We will now wait for the webhook call to complete this promise. diff --git a/python/patterns-use-cases/README.md b/python/patterns-use-cases/README.md index 284845da..968ca3d5 100644 --- a/python/patterns-use-cases/README.md +++ b/python/patterns-use-cases/README.md @@ -363,40 +363,72 @@ If you want to run everything locally, you also need a tool like _ngrok_ to forw webhooks to your local machine. 1. [Start the Restate Server](https://docs.restate.dev/develop/local_dev) in a separate shell: `restate-server` -2. Start the service: `./gradlew -PmainClass=my.example.signalspayments.PaymentService run` +2. Start the service: `python -m hypercorn --config hypercorn-config.toml src/signalspayments/payment_service:app` 3. Register the services (with `--force` to override the endpoint during **development**): `restate -y deployments register --force localhost:9080` 4. Create a free Stripe test account. This requires no verification, but you can only work with test data, not make real payments. Good enough for this example. -5. In the Stripe UI, go to "Developers" -> "API Keys" and copy the _secret key_ (`sk_test_...`). - Add it to the [StripeUtils.java](src/main/java/my/example/signalspayment/utils/StripeUtils.java) file. Because this is a dev-only +5. In the [Stripe UI](dashboard.stripe.com), go to ["Developers" -> "API Keys"](https://dashboard.stripe.com/test/apikeys) and copy the _secret key_ (`sk_test_...`). + Add it to the [stripe_utils.py](src/signalspayments/stripe_utils.py) file. Because this is a dev-only API key, it supports only test data, so it isn't super sensitive. -6. Run launch _ngrok_: Get a free account and download the binary, or launch a docker container. - Make it forward HTTP calls to local port `8080` - - `NGROK_AUTHTOKEN= ngrok http 8080` - - or `docker run --rm -it -e NGROK_AUTHTOKEN= --network host ngrok/ngrok http 8080` (on Linux command). - Copy the public URL that ngrok shows you: `https://.ngrok-free.app` +6. Run launch _ngrok_: + 1. [Get a free account](dashboard.ngrok.com) + 2. [Copy your auth token](https://dashboard.ngrok.com/get-started/your-authtoken) + 3. Download the binary, or launch a docker container. Make it forward HTTP calls to local port `8080`: + - `NGROK_AUTHTOKEN= ngrok http 8080` + - or `docker run --rm -it -e NGROK_AUTHTOKEN= --network host ngrok/ngrok http 8080` (on Linux command). + Copy the public URL that ngrok shows you: `https://.ngrok-free.app` -7. Go to the Stripe UI and create a webhook. Select all _"Payment Intent"_ event types. Put the ngrok - public URL + `/PaymentService/processWebhook` as the webhook URL (you need to update this whenever you stop/start ngrok). - Example: `https://.ngrok-free.app/PaymentService/processWebhooks` +7. Go to the Stripe UI and [create a webhook](https://dashboard.stripe.com/test/webhooks) + - Put the ngrok public URL + `/PaymentService/processWebhook` as the webhook URL (you need to update this whenever you stop/start ngrok). + Example: `https://.ngrok-free.app/payments/processWebhook` + - Select all _"Payment Intent"_ event types. -8. Put the webhook secret (`whsec_...`) to the [StripeUtils.java](src/main/java/my/example/signalspayment/StripeUtils.java) file. +8. Put the webhook secret (`whsec_...`) in the [stripe_utils.py](src/signalspayments/stripe_utils.py) file. +### Demo scenario Use as test data `pm_card_visa` for a successful payment and `pm_card_visa_chargeDeclined` for a declined payment. Because the test data rarely triggers an async response, this example's tools can mimic that if you add `"delayedStatus": true` to the request. ```shell -curl localhost:8080/PaymentService/processPayment -H 'content-type: application/json' -d '{ - "paymentMethodId": "pm_card_visa", +curl localhost:8080/payments/processPayment -H 'content-type: application/json' -d '{ + "payment_method_id": "pm_card_visa", "amount": 109, - "delayedStatus": true + "delayed_status": true }' ``` +You will see the synchronous response and the webhook call in the logs: +``` +[2024-12-20 09:34:39,136] [716785] [INFO] - message='Request to Stripe api' method=post url=https://api.stripe.com/v1/payment_intents +[2024-12-20 09:34:40,437] [716785] [INFO] - message='Stripe API response' path=https://api.stripe.com/v1/payment_intents response_code=200 +[2024-12-20 09:34:40,440] [716785] [INFO] - Payment intent for 6f8d16a5-d40c-4f9f-9c41-4da956ca795d still processing, awaiting webhook call... +[2024-12-20 09:34:40,963] [716784] [INFO] - Received webhook call for payment intent {"id": "pi_3QY...", "object": "payment_intent", "amount": 109, "amount_capturable": 0, "amount_details": {"tip": {}}, "amount_received": 109, "application": null, "application_fee_amount": null, "automatic_payment_methods": {"allow_redirects": "always", "enabled": true}, "canceled_at": null, "cancellation_reason": null, "capture_method": "automatic_async", "client_secret": "pi_3QY1fPG04wQ4kt1o0i25MBMQ_secret_V2RtPZSeeEIPlhgSlhJSzGMtC", "confirmation_method": "automatic", "created": 1734683679, "currency": "usd", "customer": null, "description": null, "invoice": null, "last_payment_error": null, "latest_charge": "ch_3QY1fPG04wQ4kt1o0p1gkSGB", "livemode": false, "metadata": {"restate_callback_id": "prom_1yCmagFOb6zIBk-M0WZWJmZVdqmDZf0gSAAAAAQ"}, "next_action": null, "on_behalf_of": null, "payment_method": "pm_1QY1fPG04wQ4kt1obj7uoLzU", "payment_method_configuration_details": {"id": "pmc_1QY1S3G04wQ4kt1oD2XuBNNT", "parent": null}, "payment_method_options": {"card": {"installments": null, "mandate_options": null, "network": null, "request_three_d_secure": "automatic"}, "link": {"persistent_token": null}}, "payment_method_types": ["card", "link"], "processing": null, "receipt_email": null, "review": null, "setup_future_usage": null, "shipping": null, "source": null, "statement_descriptor": null, "statement_descriptor_suffix": null, "status": "succeeded", "transfer_data": null, "transfer_group": null} +[2024-12-20 09:34:40,966] [716785] [INFO] - Webhook call for 6f8d16a5-d40c-4f9f-9c41-4da956ca795d received! +[2024-12-20 09:34:40,976] [716781] [INFO] - Received webhook call for payment intent {"id": "pi_3QY...", "object": "payment_intent", "amount": 109, "amount_capturable": 0, "amount_details": {"tip": {}}, "amount_received": 0, "application": null, "application_fee_amount": null, "automatic_payment_methods": {"allow_redirects": "always", "enabled": true}, "canceled_at": null, "cancellation_reason": null, "capture_method": "automatic_async", "client_secret": "pi_3QY1fPG04wQ4kt1o0i25MBMQ_secret_V2RtPZSeeEIPlhgSlhJSzGMtC", "confirmation_method": "automatic", "created": 1734683679, "currency": "usd", "customer": null, "description": null, "invoice": null, "last_payment_error": null, "latest_charge": null, "livemode": false, "metadata": {"restate_callback_id": "prom_1yCmagFOb6zIBk-M0WZWJmZVdqmDZf0gSAAAAAQ"}, "next_action": null, "on_behalf_of": null, "payment_method": null, "payment_method_configuration_details": {"id": "pmc_1QY1S3G04wQ4kt1oD2XuBNNT", "parent": null}, "payment_method_options": {"card": {"installments": null, "mandate_options": null, "network": null, "request_three_d_secure": "automatic"}, "link": {"persistent_token": null}}, "payment_method_types": ["card", "link"], "processing": null, "receipt_email": null, "review": null, "setup_future_usage": null, "shipping": null, "source": null, "statement_descriptor": null, "statement_descriptor_suffix": null, "status": "requires_payment_method", "transfer_data": null, "transfer_group": null} +``` + +And for declined payments +```shell +curl localhost:8080/payments/processPayment -H 'content-type: application/json' -d '{ + "payment_method_id": "pm_card_visa_chargeDeclined", + "amount": 109, + "delayed_status": true +}' +``` +``` +[2024-12-20 09:42:58,587] [718038] [INFO] - message='Request to Stripe api' method=post url=https://api.stripe.com/v1/payment_intents +[2024-12-20 09:42:59,655] [718038] [INFO] - message='Stripe API response' path=https://api.stripe.com/v1/payment_intents response_code=402 +[2024-12-20 09:42:59,655] [718038] [INFO] - error_code=card_declined error_message='Your card was declined.' error_param=None error_type=card_error message='Stripe v1 API error received' +[2024-12-20 09:42:59,657] [718038] [INFO] - Payment intent for 2d0239c9-5bd2-4d10-8c9d-3888b5c9a3c7 still processing, awaiting webhook call... +[2024-12-20 09:43:00,044] [718039] [INFO] - Received webhook call for payment intent {"id": "pi_3Q...", "object": "payment_intent", "amount": 109, "amount_capturable": 0, "amount_details": {"tip": {}}, "amount_received": 0, "application": null, "application_fee_amount": null, "automatic_payment_methods": {"allow_redirects": "always", "enabled": true}, "canceled_at": null, "cancellation_reason": null, "capture_method": "automatic_async", "client_secret": "pi_3QY1nSG04wQ4kt1o0LDvgKp2_secret_6u9ZCdKZODCKfs5TswZDEDqcc", "confirmation_method": "automatic", "created": 1734684178, "currency": "usd", "customer": null, "description": null, "invoice": null, "last_payment_error": null, "latest_charge": null, "livemode": false, "metadata": {"restate_callback_id": "prom_1WwmuXpSfrCwBk-M7-JLlV6QcnWZ7nyKlAAAAAQ"}, "next_action": null, "on_behalf_of": null, "payment_method": null, "payment_method_configuration_details": {"id": "pmc_1QY1S3G04wQ4kt1oD2XuBNNT", "parent": null}, "payment_method_options": {"card": {"installments": null, "mandate_options": null, "network": null, "request_three_d_secure": "automatic"}, "link": {"persistent_token": null}}, "payment_method_types": ["card", "link"], "processing": null, "receipt_email": null, "review": null, "setup_future_usage": null, "shipping": null, "source": null, "statement_descriptor": null, "statement_descriptor_suffix": null, "status": "requires_payment_method", "transfer_data": null, "transfer_group": null} +[2024-12-20 09:43:00,047] [718038] [INFO] - Webhook call for 2d0239c9-5bd2-4d10-8c9d-3888b5c9a3c7 received! +[2024-12-20 09:43:00,135] [718044] [INFO] - Received webhook call for payment intent {"id": "pi_3Q...", "object": "payment_intent", "amount": 109, "amount_capturable": 0, "amount_details": {"tip": {}}, "amount_received": 0, "application": null, "application_fee_amount": null, "automatic_payment_methods": {"allow_redirects": "always", "enabled": true}, "canceled_at": null, "cancellation_reason": null, "capture_method": "automatic_async", "client_secret": "pi_3QY1nSG04wQ4kt1o0LDvgKp2_secret_6u9ZCdKZODCKfs5TswZDEDqcc", "confirmation_method": "automatic", "created": 1734684178, "currency": "usd", "customer": null, "description": null, "invoice": null, "last_payment_error": {"advice_code": "try_again_later", "charge": "ch_3QY1nSG04wQ4kt1o0mEz8YHB", "code": "card_declined", "decline_code": "generic_decline", "doc_url": "https://stripe.com/docs/error-codes/card-declined", "message": "Your card was declined.", "payment_method": {"id": "pm_1QY1nSG04wQ4kt1oFaoJxf8z", "object": "payment_method", "allow_redisplay": "unspecified", "billing_details": {"address": {"city": null, "country": null, "line1": null, "line2": null, "postal_code": null, "state": null}, "email": null, "name": null, "phone": null}, "card": {"brand": "visa", "checks": {"address_line1_check": null, "address_postal_code_check": null, "cvc_check": "pass"}, "country": "US", "display_brand": "visa", "exp_month": 12, "exp_year": 2025, "fingerprint": "HgmUUSMwiOzktMXB", "funding": "credit", "generated_from": null, "last4": "0002", "networks": {"available": ["visa"], "preferred": null}, "regulated_status": "unregulated", "three_d_secure_usage": {"supported": true}, "wallet": null}, "created": 1734684178, "customer": null, "livemode": false, "metadata": {}, "type": "card"}, "type": "card_error"}, "latest_charge": "ch_3QY1nSG04wQ4kt1o0mEz8YHB", "livemode": false, "metadata": {"restate_callback_id": "prom_1WwmuXpSfrCwBk-M7-JLlV6QcnWZ7nyKlAAAAAQ"}, "next_action": null, "on_behalf_of": null, "payment_method": null, "payment_method_configuration_details": {"id": "pmc_1QY1S3G04wQ4kt1oD2XuBNNT", "parent": null}, "payment_method_options": {"card": {"installments": null, "mandate_options": null, "network": null, "request_three_d_secure": "automatic"}, "link": {"persistent_token": null}}, "payment_method_types": ["card", "link"], "processing": null, "receipt_email": null, "review": null, "setup_future_usage": null, "shipping": null, "source": null, "statement_descriptor": null, "statement_descriptor_suffix": null, "status": "requires_payment_method", "transfer_data": null, "transfer_group": null} +``` + A few notes: * You would usually submit payment calls through Restate also with an idempotency token, like: ` -H 'idempotency-key: my-id-token'` diff --git a/python/patterns-use-cases/requirements.txt b/python/patterns-use-cases/requirements.txt index 0a70572b..18e85e9d 100644 --- a/python/patterns-use-cases/requirements.txt +++ b/python/patterns-use-cases/requirements.txt @@ -1,4 +1,5 @@ hypercorn restate_sdk==0.4.1 pydantic -requests \ No newline at end of file +requests +stripe \ No newline at end of file diff --git a/python/patterns-use-cases/src/sagas/booking_workflow.py b/python/patterns-use-cases/src/sagas/booking_workflow.py index abbcb4b5..37a53f4c 100644 --- a/python/patterns-use-cases/src/sagas/booking_workflow.py +++ b/python/patterns-use-cases/src/sagas/booking_workflow.py @@ -46,7 +46,6 @@ async def run(ctx: restate.WorkflowContext, req: BookingRequest): # Reserve the flights and let Restate remember the reservation ID flight_booking_id = await ctx.service_call(flight_reserve, arg=req.flights) # Register the undo action for the flight reservation. - compensations.append(lambda: ctx.service_call(flight_cancel, arg=flight_booking_id)) # Reserve the car and let Restate remember the reservation ID @@ -60,24 +59,22 @@ async def run(ctx: restate.WorkflowContext, req: BookingRequest): # Register the refund as a compensation, using the idempotency key async def refund(): return await payment_client.refund(payment_id) - compensations.append(lambda: ctx.run("refund", refund)) # Do the payment using the idempotency key async def charge(): return await payment_client.charge(req.payment_info, payment_id) - await ctx.run("charge", charge) - # confirm the flight and car reservations + # Confirm the flight and car reservations await ctx.service_call(flight_confirm, arg=flight_booking_id) await ctx.service_call(car_rentals.confirm, arg=car_booking_id) except TerminalError as e: - # undo all the steps up to this point by running the compensations + # Undo all the steps up to this point by running the compensations for compensation in reversed(compensations): await compensation() - # rethrow error to fail this workflow + # Rethrow error to fail this workflow raise e diff --git a/python/patterns-use-cases/src/signalspayments/__init__.py b/python/patterns-use-cases/src/signalspayments/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/patterns-use-cases/src/signalspayments/payment_service.py b/python/patterns-use-cases/src/signalspayments/payment_service.py new file mode 100644 index 00000000..8a7507e2 --- /dev/null +++ b/python/patterns-use-cases/src/signalspayments/payment_service.py @@ -0,0 +1,79 @@ +import json +import logging +import uuid + +import restate +from restate import Context, Service +from restate.exceptions import TerminalError +import stripe_utils +from stripe_utils import (PaymentRequest, verify_payment_request, create_payment_intent, + RESTATE_CALLBACK_ID, is_payment_intent, parse_webhook_call) + +logging.basicConfig(level=logging.INFO, format='[%(asctime)s] [%(process)d] [%(levelname)s] - %(message)s') +logger = logging.getLogger(__name__) + +payment_service = Service("payments") + + +@payment_service.handler("processPayment") +async def process_payment(ctx: Context, req: PaymentRequest): + verify_payment_request(req) + + # Generate a deterministic idempotency key + idempotency_key = await ctx.run("idempotency key", lambda: str(uuid.uuid4())) + + # Initiate a listener for external calls for potential webhook callbacks + intent_webhook_id, intent_promise = ctx.awakeable() + + # Make a synchronous call to the payment service + async def payment_intent() -> dict: + return await create_payment_intent({ + 'payment_method_id': req.payment_method_id, + 'amount': req.amount, + 'idempotency_key': idempotency_key, + 'intent_webhook_id': intent_webhook_id, + 'delayed_status': req.delayed_status, + }) + + payment_intent = await ctx.run("stripe call", payment_intent) + + if payment_intent['status'] != "processing": + # The call to Stripe completed immediately / synchronously: processing done + logger.info(f"Request {idempotency_key} was processed synchronously!") + stripe_utils.ensure_success(payment_intent['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. + logger.info(f"Payment intent for {idempotency_key} still processing, awaiting webhook call...") + + # We will now wait for the webhook call to complete this promise. + # Check out the handler below. + processed_payment_intent = await intent_promise + + logger.info(f"Webhook call for {idempotency_key} received!") + stripe_utils.ensure_success(processed_payment_intent['status']) + + +@payment_service.handler("processWebhook") +async def process_webhook(ctx: Context): + req = ctx.request() + sig = req.headers.get("stripe-signature") + event = parse_webhook_call(req.body, sig) + + if not is_payment_intent(event): + logger.info(f"Unhandled event type {event['type']}") + return {'received': True} + + payment_intent = event['data']['object'] + logger.info("Received webhook call for payment intent %s", json.dumps(payment_intent)) + + webhook_promise = payment_intent['metadata'].get(RESTATE_CALLBACK_ID) + if not webhook_promise: + raise TerminalError(f"Missing callback property: {RESTATE_CALLBACK_ID}", status_code=404) + + ctx.resolve_awakeable(webhook_promise, payment_intent) + return {'received': True} + + +app = restate.app([payment_service]) diff --git a/python/patterns-use-cases/src/signalspayments/stripe_utils.py b/python/patterns-use-cases/src/signalspayments/stripe_utils.py new file mode 100644 index 00000000..57b0c370 --- /dev/null +++ b/python/patterns-use-cases/src/signalspayments/stripe_utils.py @@ -0,0 +1,83 @@ +import stripe +from pydantic import BaseModel +from restate.exceptions import TerminalError + +stripe_secret_key = "sk_test_..." +webhook_secret = "whsec_..." + +stripe.api_key = stripe_secret_key + +RESTATE_CALLBACK_ID = "restate_callback_id" + + +class PaymentRequest(BaseModel): + amount: int + payment_method_id: str + delayed_status: bool = False + + +def is_payment_intent(event: stripe.Event): + return event['type'].startswith("payment_intent") + + +def parse_webhook_call(request_body, signature): + if not signature: + raise TerminalError("Missing 'stripe-signature' header.", status_code=400) + try: + return stripe.Webhook.construct_event( + payload=request_body, + sig_header=signature, + secret=webhook_secret + ) + except Exception as err: + raise TerminalError(f"Webhook Error: {err}", status_code=400) + + +async def create_payment_intent(request) -> dict: + request_options = { + 'idempotency_key': request['idempotency_key'], + } + + try: + payment_intent = stripe.PaymentIntent.create( + amount=request['amount'], + currency="usd", + payment_method=request['payment_method_id'], + confirm=True, + confirmation_method="automatic", + return_url="https://restate.dev/", + metadata={ + RESTATE_CALLBACK_ID: request['intent_webhook_id'], + }, + **request_options + ) + + if request.get('delayed_status'): + payment_intent['status'] = "processing" + + return payment_intent + except stripe.error.CardError as error: + payment_intent = error.error.payment_intent + if request.get('delayed_status') and payment_intent: + payment_intent['status'] = "processing" + return payment_intent + else: + raise TerminalError(f"Payment declined: {payment_intent.get('status')} - {error.user_message}") + except Exception as error: + raise error + + +def ensure_success(status): + if status == "succeeded": + return + elif status in ["requires_payment_method", "canceled"]: + raise TerminalError(f"Payment declined: {status}") + else: + raise Exception(f"Unhandled status: {status}") + + +def verify_payment_request(request: PaymentRequest): + if not request.amount or request.amount == 0: + raise TerminalError("'amount' missing or zero in request") + if not request.payment_method_id or request.payment_method_id == "": + raise TerminalError("'paymentMethodId' missing in request") diff --git a/typescript/patterns-use-cases/async-tasks-payment-signals/README.md b/typescript/patterns-use-cases/async-tasks-payment-signals/README.md index 1bf58b7a..638d2dbc 100644 --- a/typescript/patterns-use-cases/async-tasks-payment-signals/README.md +++ b/typescript/patterns-use-cases/async-tasks-payment-signals/README.md @@ -41,7 +41,7 @@ webhooks to your local machine. 7. Go to the Stripe UI and create a webhook. Select all _"Payment Intent"_ event types. Put the ngrok public URL + `/payments/processWebhook` as the webhook URL (you need to update this whenever you stop/start ngrok). - Example: `https://.ngrok-free.app/payments/processWebhooks` + Example: `https://.ngrok-free.app/payments/processWebhook` 8. Put the webhook secret (`whsec_...`) to the [stripe_utils.ts](./src/utils/stripe_utils.ts) file. diff --git a/typescript/patterns-use-cases/async-tasks-payment-signals/package.json b/typescript/patterns-use-cases/async-tasks-payment-signals/package.json index cb89b369..a0da087f 100644 --- a/typescript/patterns-use-cases/async-tasks-payment-signals/package.json +++ b/typescript/patterns-use-cases/async-tasks-payment-signals/package.json @@ -13,8 +13,6 @@ "stripe": "^14.15.0" }, "devDependencies": { - "@restatedev/restate": "^1.1.0", - "@restatedev/restate-server": "^1.1.0", "@types/node": "^20.12.12", "ts-node-dev": "^1.1.1", "typescript": "^5.0.2" diff --git a/typescript/patterns-use-cases/async-tasks-payment-signals/src/payment_handler.ts b/typescript/patterns-use-cases/async-tasks-payment-signals/src/payment_handler.ts index b2a66eb7..e0bd6046 100644 --- a/typescript/patterns-use-cases/async-tasks-payment-signals/src/payment_handler.ts +++ b/typescript/patterns-use-cases/async-tasks-payment-signals/src/payment_handler.ts @@ -11,7 +11,7 @@ import * as restate from "@restatedev/restate-sdk"; import * as stripe_utils from "./utils/stripe_utils"; -import { verifyPaymentRequest } from "./utils/utils"; +import { verifyPaymentRequest } from "./utils/stripe_utils"; import Stripe from "stripe"; // @@ -67,7 +67,7 @@ async function processPayment(ctx: restate.Context, request: PaymentRequest) { // 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...` + `Payment intent for ${idempotencyKey} still 'processing', awaiting webhook call...` ); // We will now wait for the webhook call to complete this promise. @@ -89,7 +89,6 @@ async function processWebhook(ctx: restate.Context) { } const paymentIntent = event.data.object as Stripe.PaymentIntent; - ctx.console.log(JSON.stringify(paymentIntent)); const webhookPromise = paymentIntent.metadata[stripe_utils.RESTATE_CALLBACK_ID]; @@ -103,4 +102,4 @@ async function processWebhook(ctx: restate.Context) { return { received: true }; } -restate.endpoint().bind(restate.service({name: "payments", handlers: { processPayment, processWebhook }})).listen(9080); +restate.endpoint().bind(restate.service({name: "payments", handlers: { processPayment, processWebhooks }})).listen(9080); diff --git a/typescript/patterns-use-cases/async-tasks-payment-signals/src/utils/stripe_utils.ts b/typescript/patterns-use-cases/async-tasks-payment-signals/src/utils/stripe_utils.ts index 27cc79a2..88ecaa89 100644 --- a/typescript/patterns-use-cases/async-tasks-payment-signals/src/utils/stripe_utils.ts +++ b/typescript/patterns-use-cases/async-tasks-payment-signals/src/utils/stripe_utils.ts @@ -104,3 +104,12 @@ export function ensureSuccess(status: string) { } } +export function verifyPaymentRequest(request: any): void { + if (!request?.amount) { + throw new TerminalError("'amount' missing or zero in request"); + } + if (!request?.paymentMethodId) { + throw new TerminalError("'paymentMethodId' missing in request"); + } +} + diff --git a/typescript/patterns-use-cases/async-tasks-payment-signals/src/utils/utils.ts b/typescript/patterns-use-cases/async-tasks-payment-signals/src/utils/utils.ts deleted file mode 100644 index 214de2f9..00000000 --- a/typescript/patterns-use-cases/async-tasks-payment-signals/src/utils/utils.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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 { TerminalError } from "@restatedev/restate-sdk"; - -export function verifyPaymentRequest(request: any): void { - if (!request?.amount) { - throw new TerminalError("'amount' missing or zero in request"); - } - if (!request?.paymentMethodId) { - throw new TerminalError("'paymentMethodId' missing in request"); - } -}