From 9a95dd9d828c5cdff1a1bec3b7f4dc65215ea730 Mon Sep 17 00:00:00 2001 From: Andreas Wuerl Date: Thu, 15 Sep 2022 12:28:48 +0200 Subject: [PATCH 01/10] restore old webhook endpoint --- src/PaymentServer/Processors/Stripe.hs | 31 +++++++++++++++++++++++++- stack.yaml | 3 ++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/PaymentServer/Processors/Stripe.hs b/src/PaymentServer/Processors/Stripe.hs index 07f2556..bf5d3a6 100644 --- a/src/PaymentServer/Processors/Stripe.hs +++ b/src/PaymentServer/Processors/Stripe.hs @@ -106,7 +106,8 @@ instance ToJSON Acknowledgement where [ "success" .= True ] -type StripeAPI = ChargesAPI +type StripeAPI = WebhookAPI :<|> ChargesAPI +type WebhookAPI = "webhook" :> ReqBody '[JSON] Event :> Post '[JSON] Acknowledgement -- | getVoucher finds the metadata item with the key `"Voucher"` and returns -- the corresponding value, or Nothing. @@ -119,6 +120,31 @@ stripeServer :: VoucherDatabase d => StripeConfig -> d -> Server StripeAPI stripeServer stripeConfig d = withSuccessFailureMetrics chargeAttempts chargeSuccesses . charge stripeConfig d +--stripeServer :: VoucherDatabase d => StripeSecretKey -> d -> Server StripeAPI +--stripeServer key d = webhook d :<|> charge d key + +-- | Process charge succeeded events +webhook :: VoucherDatabase d => StripeConfig -> d -> Event -> Handler Acknowledgement +webhook stripeConfig d Event{eventId=Just (EventId eventId), eventType=ChargeSucceededEvent, eventData=(ChargeEvent charge)} = + case getVoucher $ chargeMetaData charge of + Nothing -> + -- TODO: Record the eventId somewhere. In all cases where we don't + -- associate the value of the charge with something in our system, we + -- probably need enough information to issue a refund. We're early + -- enough in the system here that refunds are possible and not even + -- particularly difficult. + return Ok + Just v -> do + -- TODO: What if it is a duplicate payment? payForVoucher should be + -- able to indicate error I guess. + () <- liftIO $ payForVoucher d v + return Ok + +-- Disregard anything else - but return success so that Stripe doesn't retry. +webhook d _ = + -- TODO: Record the eventId somewhere. + return Ok + -- | Browser facing API that takes token, voucher and a few other information -- and calls stripe charges API. If payment succeeds, then the voucher is stored -- in the voucher database. @@ -175,6 +201,9 @@ withSuccessFailureMetrics attemptCount successCount op = do -- and if the Charge is okay, then set the voucher as "paid" in the database. charge :: VoucherDatabase d => StripeConfig -> d -> Charges -> Handler Acknowledgement charge stripeConfig d (Charges token voucher 650 USD) = do + + # TODO verify the webhook request + result <- liftIO payForVoucher' case result of Left AlreadyPaid -> diff --git a/stack.yaml b/stack.yaml index d3514f5..400f37f 100644 --- a/stack.yaml +++ b/stack.yaml @@ -17,7 +17,8 @@ # # resolver: ./custom-snapshot.yaml # resolver: https://example.com/snapshots/2018-01-01.yaml -resolver: lts-14.2 +resolver: + url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/19/19.yaml # User packages to be built. # Various formats can be used as shown in the example below. From 26bd10f168ef440385f21b94789f37e296f2c2f5 Mon Sep 17 00:00:00 2001 From: Andreas Wuerl Date: Thu, 15 Sep 2022 14:53:24 +0200 Subject: [PATCH 02/10] update versions and add missing imports --- src/PaymentServer/Processors/Stripe.hs | 9 ++++++++- stack.yaml | 13 ++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/PaymentServer/Processors/Stripe.hs b/src/PaymentServer/Processors/Stripe.hs index bf5d3a6..4fa45e4 100644 --- a/src/PaymentServer/Processors/Stripe.hs +++ b/src/PaymentServer/Processors/Stripe.hs @@ -67,6 +67,13 @@ import Servant.API , JSON , Post , (:>) + , (:<|>)((:<|>)) + ) +import Web.Stripe.Event + ( Event(Event, eventId, eventType, eventData) + , EventId(EventId) + , EventType(ChargeSucceededEvent) + , EventData(ChargeEvent) ) import Web.Stripe.Error ( StripeError(StripeError, errorType, errorMsg) @@ -202,7 +209,7 @@ withSuccessFailureMetrics attemptCount successCount op = do charge :: VoucherDatabase d => StripeConfig -> d -> Charges -> Handler Acknowledgement charge stripeConfig d (Charges token voucher 650 USD) = do - # TODO verify the webhook request + -- TODO verify the webhook request as a first step result <- liftIO payForVoucher' case result of diff --git a/stack.yaml b/stack.yaml index 400f37f..552d5c6 100644 --- a/stack.yaml +++ b/stack.yaml @@ -17,8 +17,7 @@ # # resolver: ./custom-snapshot.yaml # resolver: https://example.com/snapshots/2018-01-01.yaml -resolver: - url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/19/19.yaml +resolver: lts-18.28 # User packages to be built. # Various formats can be used as shown in the example below. @@ -39,11 +38,11 @@ packages: # using the same syntax as the packages field. # (e.g., acme-missiles-0.3) extra-deps: - - "stripe-core-2.5.0" - - "stripe-haskell-2.5.0" - - "stripe-http-client-2.5.0" - - github: "PrivateStorageio/servant-prometheus" - commit: "b9461cbf689b47506b2eee973136706092b74968" + - "stripe-core-2.6.2" + - "stripe-haskell-2.6.2" + - "stripe-http-client-2.6.2" + - github: "wuan/servant-prometheus" + commit: "3eaceb378edc1f81abb4d6fbdb92c6172a09813d" # https://input-output-hk.github.io/haskell.nix/tutorials/source-repository-hashes/#stack # nix-sha256: 1gfslw670ri119bnq3szc8b08n504f8cnzs5cgk5qvfwvfmsr1xh From 01fdeb141768ac0bd94ad3fbf305d1a7c92e380e Mon Sep 17 00:00:00 2001 From: Andreas Wuerl Date: Mon, 19 Sep 2022 11:11:21 +0200 Subject: [PATCH 03/10] reference updated version --- stack.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stack.yaml b/stack.yaml index 552d5c6..84f1bfe 100644 --- a/stack.yaml +++ b/stack.yaml @@ -41,8 +41,8 @@ extra-deps: - "stripe-core-2.6.2" - "stripe-haskell-2.6.2" - "stripe-http-client-2.6.2" - - github: "wuan/servant-prometheus" - commit: "3eaceb378edc1f81abb4d6fbdb92c6172a09813d" + - github: "PrivateStorageio/servant-prometheus" + commit: "6c8430b802303f9b8a3d11acb0c212b80444ad7c" # https://input-output-hk.github.io/haskell.nix/tutorials/source-repository-hashes/#stack # nix-sha256: 1gfslw670ri119bnq3szc8b08n504f8cnzs5cgk5qvfwvfmsr1xh From 185298e5d5988a05b4df2767d92780855dfd1d98 Mon Sep 17 00:00:00 2001 From: Andreas Wuerl Date: Tue, 20 Sep 2022 18:01:07 +0200 Subject: [PATCH 04/10] partial fix from meeting --- src/PaymentServer/Processors/Stripe.hs | 16 +++++++++------- src/PaymentServer/Server.hs | 4 ++++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/PaymentServer/Processors/Stripe.hs b/src/PaymentServer/Processors/Stripe.hs index 4fa45e4..29fb5fa 100644 --- a/src/PaymentServer/Processors/Stripe.hs +++ b/src/PaymentServer/Processors/Stripe.hs @@ -6,10 +6,12 @@ module PaymentServer.Processors.Stripe ( StripeAPI + , WebhookAPI , Charges(Charges) , Acknowledgement(Ok) , Failure(Failure) , stripeServer + , webhookServer , getVoucher , charge ) where @@ -80,7 +82,7 @@ import Web.Stripe.Error , StripeErrorType(InvalidRequest, APIError, ConnectionFailure, CardError) ) import Web.Stripe.Types - ( Charge(Charge, chargeId) + ( Charge(Charge, chargeId, chargeMetaData) , MetaData(MetaData) , Currency(USD) ) @@ -113,8 +115,8 @@ instance ToJSON Acknowledgement where [ "success" .= True ] -type StripeAPI = WebhookAPI :<|> ChargesAPI -type WebhookAPI = "webhook" :> ReqBody '[JSON] Event :> Post '[JSON] Acknowledgement +type StripeAPI = ChargesAPI +type WebhookAPI = ReqBody '[JSON] Event :> Post '[JSON] Acknowledgement -- | getVoucher finds the metadata item with the key `"Voucher"` and returns -- the corresponding value, or Nothing. @@ -131,8 +133,8 @@ stripeServer stripeConfig d = --stripeServer key d = webhook d :<|> charge d key -- | Process charge succeeded events -webhook :: VoucherDatabase d => StripeConfig -> d -> Event -> Handler Acknowledgement -webhook stripeConfig d Event{eventId=Just (EventId eventId), eventType=ChargeSucceededEvent, eventData=(ChargeEvent charge)} = +webhookServer :: VoucherDatabase d => StripeConfig -> d -> Event -> Handler Acknowledgement +webhookServer stripeConfig d Event{eventId=Just (EventId eventId), eventType=ChargeSucceededEvent, eventData=(ChargeEvent charge)} = case getVoucher $ chargeMetaData charge of Nothing -> -- TODO: Record the eventId somewhere. In all cases where we don't @@ -144,11 +146,11 @@ webhook stripeConfig d Event{eventId=Just (EventId eventId), eventType=ChargeSuc Just v -> do -- TODO: What if it is a duplicate payment? payForVoucher should be -- able to indicate error I guess. - () <- liftIO $ payForVoucher d v + _ <- liftIO $ payForVoucher d v (return $ Right $ chargeId charge) return Ok -- Disregard anything else - but return success so that Stripe doesn't retry. -webhook d _ = +webhook _ d _ = -- TODO: Record the eventId somewhere. return Ok diff --git a/src/PaymentServer/Server.hs b/src/PaymentServer/Server.hs index 3c0c5ee..8f0c1ae 100644 --- a/src/PaymentServer/Server.hs +++ b/src/PaymentServer/Server.hs @@ -38,7 +38,9 @@ import Web.Stripe.Client import PaymentServer.Processors.Stripe ( StripeAPI + , WebhookAPI , stripeServer + , webhookServer ) import PaymentServer.Redemption ( RedemptionConfig(RedemptionConfig) @@ -60,12 +62,14 @@ import PaymentServer.Persistence type PaymentServerAPI = "v1" :> "stripe" :> StripeAPI :<|> "v1" :> "redeem" :> RedemptionAPI + :<|> "v1" :> "webhook" :> WebhookAPI :<|> MetricsAPI -- | Create a server which uses the given database. paymentServer :: VoucherDatabase d => StripeConfig -> RedemptionConfig -> d -> Server PaymentServerAPI paymentServer stripeConfig redemptionConfig database = stripeServer stripeConfig database + :<|> webhookServer stripeConfig database :<|> redemptionServer redemptionConfig database :<|> metricsServer From d74b04968d1214ddac307244bbb0e8b8c70ded17 Mon Sep 17 00:00:00 2001 From: Andreas Wuerl Date: Tue, 20 Sep 2022 18:02:24 +0200 Subject: [PATCH 05/10] fixed last compile error --- src/PaymentServer/Server.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PaymentServer/Server.hs b/src/PaymentServer/Server.hs index 8f0c1ae..4029f74 100644 --- a/src/PaymentServer/Server.hs +++ b/src/PaymentServer/Server.hs @@ -69,8 +69,8 @@ type PaymentServerAPI paymentServer :: VoucherDatabase d => StripeConfig -> RedemptionConfig -> d -> Server PaymentServerAPI paymentServer stripeConfig redemptionConfig database = stripeServer stripeConfig database - :<|> webhookServer stripeConfig database :<|> redemptionServer redemptionConfig database + :<|> webhookServer stripeConfig database :<|> metricsServer paymentServerAPI :: Proxy PaymentServerAPI From 42d19c5bc6c329cf1107bdac5504be8a4e71670d Mon Sep 17 00:00:00 2001 From: Andreas Wuerl Date: Tue, 11 Oct 2022 17:03:09 +0200 Subject: [PATCH 06/10] import cleanup --- nix/materialized.paymentserver/.stack-to-nix.cache | 2 +- src/PaymentServer/Processors/Stripe.hs | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/nix/materialized.paymentserver/.stack-to-nix.cache b/nix/materialized.paymentserver/.stack-to-nix.cache index 36bb67c..c641303 100644 --- a/nix/materialized.paymentserver/.stack-to-nix.cache +++ b/nix/materialized.paymentserver/.stack-to-nix.cache @@ -1 +1 @@ -https://github.com/PrivateStorageio/servant-prometheus.git b9461cbf689b47506b2eee973136706092b74968 . 1gfslw670ri119bnq3szc8b08n504f8cnzs5cgk5qvfwvfmsr1xh servant-prometheus .stack-to-nix.cache.0 +https://github.com/PrivateStorageio/servant-prometheus.git 6c8430b802303f9b8a3d11acb0c212b80444ad7c . 1gfslw670ri119bnq3szc8b08n504f8cnzs5cgk5qvfwvfmsr1xh servant-prometheus .stack-to-nix.cache.0 diff --git a/src/PaymentServer/Processors/Stripe.hs b/src/PaymentServer/Processors/Stripe.hs index 29fb5fa..294b385 100644 --- a/src/PaymentServer/Processors/Stripe.hs +++ b/src/PaymentServer/Processors/Stripe.hs @@ -31,12 +31,9 @@ import Control.Monad ) import Data.Text ( Text - , unpack , concat ) -import Text.Read - ( readMaybe - ) +import Text.Read() import Network.HTTP.Types ( Status(Status) @@ -69,7 +66,6 @@ import Servant.API , JSON , Post , (:>) - , (:<|>)((:<|>)) ) import Web.Stripe.Event ( Event(Event, eventId, eventType, eventData) From 1ca9928c93b1e9d2193fcaea11e4c76d9dd0fc29 Mon Sep 17 00:00:00 2001 From: Andreas Wuerl Date: Tue, 11 Oct 2022 18:24:52 +0200 Subject: [PATCH 07/10] update dependency --- nix/materialized.paymentserver/.stack-to-nix.cache | 2 +- stack.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nix/materialized.paymentserver/.stack-to-nix.cache b/nix/materialized.paymentserver/.stack-to-nix.cache index c641303..36bb67c 100644 --- a/nix/materialized.paymentserver/.stack-to-nix.cache +++ b/nix/materialized.paymentserver/.stack-to-nix.cache @@ -1 +1 @@ -https://github.com/PrivateStorageio/servant-prometheus.git 6c8430b802303f9b8a3d11acb0c212b80444ad7c . 1gfslw670ri119bnq3szc8b08n504f8cnzs5cgk5qvfwvfmsr1xh servant-prometheus .stack-to-nix.cache.0 +https://github.com/PrivateStorageio/servant-prometheus.git b9461cbf689b47506b2eee973136706092b74968 . 1gfslw670ri119bnq3szc8b08n504f8cnzs5cgk5qvfwvfmsr1xh servant-prometheus .stack-to-nix.cache.0 diff --git a/stack.yaml b/stack.yaml index 84f1bfe..63eb141 100644 --- a/stack.yaml +++ b/stack.yaml @@ -42,9 +42,9 @@ extra-deps: - "stripe-haskell-2.6.2" - "stripe-http-client-2.6.2" - github: "PrivateStorageio/servant-prometheus" - commit: "6c8430b802303f9b8a3d11acb0c212b80444ad7c" + commit: "622eb77cb08c5f13729173b8feb123a6700ff91f" # https://input-output-hk.github.io/haskell.nix/tutorials/source-repository-hashes/#stack - # nix-sha256: 1gfslw670ri119bnq3szc8b08n504f8cnzs5cgk5qvfwvfmsr1xh + # nix-sha256: 39KhnnIG/UtvcUqELPfe5SAK8YgqA0UWjaILporkrCU= # Override default flag values for local packages and extra-deps # flags: {} From dd25dc75a7d3ed1cd261d7d99fb7f37ff0ffe1cd Mon Sep 17 00:00:00 2001 From: Andreas Wuerl Date: Wed, 12 Oct 2022 08:20:14 +0200 Subject: [PATCH 08/10] upgrade network to avoid compile errors --- stack.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/stack.yaml b/stack.yaml index 193f5e4..58510fc 100644 --- a/stack.yaml +++ b/stack.yaml @@ -38,9 +38,7 @@ packages: # using the same syntax as the packages field. # (e.g., acme-missiles-0.3) extra-deps: - - "stripe-core-2.5.0" - - "stripe-haskell-2.5.0" - - "stripe-http-client-2.5.0" + - "network-3.1.2.7" - github: "PrivateStorageio/servant-prometheus" commit: "622eb77cb08c5f13729173b8feb123a6700ff91f" # https://input-output-hk.github.io/haskell.nix/tutorials/source-repository-hashes/#stack From 2bf07c9587f01c10f1d4ca6c766ec5a5824c16c4 Mon Sep 17 00:00:00 2001 From: Andreas Wuerl Date: Wed, 12 Oct 2022 15:21:35 +0200 Subject: [PATCH 09/10] update materialized payment server --- nix/materialized.paymentserver/.stack-to-nix.cache | 2 +- nix/materialized.paymentserver/.stack-to-nix.cache.0 | 2 +- nix/materialized.paymentserver/PaymentServer.nix | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/nix/materialized.paymentserver/.stack-to-nix.cache b/nix/materialized.paymentserver/.stack-to-nix.cache index 826f2b3..f0ac606 100644 --- a/nix/materialized.paymentserver/.stack-to-nix.cache +++ b/nix/materialized.paymentserver/.stack-to-nix.cache @@ -1 +1 @@ -https://github.com/PrivateStorageio/servant-prometheus.git 6c8430b802303f9b8a3d11acb0c212b80444ad7c . 09dcwj5ac2x2ilb4a0rai3qhl875vvvjr12af5plpz86faga3lnz servant-prometheus .stack-to-nix.cache.0 +https://github.com/PrivateStorageio/servant-prometheus.git 622eb77cb08c5f13729173b8feb123a6700ff91f . 09dcwj5ac2x2ilb4a0rai3qhl875vvvjr12af5plpz86faga3lnz servant-prometheus .stack-to-nix.cache.0 diff --git a/nix/materialized.paymentserver/.stack-to-nix.cache.0 b/nix/materialized.paymentserver/.stack-to-nix.cache.0 index 54253b1..086ae74 100644 --- a/nix/materialized.paymentserver/.stack-to-nix.cache.0 +++ b/nix/materialized.paymentserver/.stack-to-nix.cache.0 @@ -76,5 +76,5 @@ }; }; } // rec { - src = (pkgs.lib).mkDefault /nix/store/g62jlfm3vg9dld12fi12f5rkp9sjgbab-servant-prometheus-6c8430b; + src = (pkgs.lib).mkDefault /nix/store/r537z0w0swb6calr6vkk2ppnqd2bgv8z-servant-prometheus-622eb77; } diff --git a/nix/materialized.paymentserver/PaymentServer.nix b/nix/materialized.paymentserver/PaymentServer.nix index a5702ec..842157c 100644 --- a/nix/materialized.paymentserver/PaymentServer.nix +++ b/nix/materialized.paymentserver/PaymentServer.nix @@ -142,4 +142,6 @@ }; }; }; - } // rec { src = (pkgs.lib).mkDefault ./.; } \ No newline at end of file + } // rec { + src = (pkgs.lib).mkDefault ./.; + } \ No newline at end of file From 8f76ae702c290a78ed724bd75e5490c2cdffa64c Mon Sep 17 00:00:00 2001 From: Andreas Wuerl Date: Thu, 13 Oct 2022 15:57:08 +0200 Subject: [PATCH 10/10] move webhook endpoint to stripe path and add some example payloads --- misc/examples/events/charge.http | 10 + misc/examples/events/webhook.http | 292 +++++++++++++++++++++++++ src/PaymentServer/Processors/Stripe.hs | 19 +- src/PaymentServer/Server.hs | 12 +- 4 files changed, 320 insertions(+), 13 deletions(-) create mode 100644 misc/examples/events/charge.http create mode 100644 misc/examples/events/webhook.http diff --git a/misc/examples/events/charge.http b/misc/examples/events/charge.http new file mode 100644 index 0000000..7829a7a --- /dev/null +++ b/misc/examples/events/charge.http @@ -0,0 +1,10 @@ +### +POST http://localhost:8080/v1/stripe/charge +Content-Type: application/json + +{ + "token": "tok_visa", + "voucher": "abcg", + "amount": "650", + "currency": "USD" +} \ No newline at end of file diff --git a/misc/examples/events/webhook.http b/misc/examples/events/webhook.http new file mode 100644 index 0000000..a544d21 --- /dev/null +++ b/misc/examples/events/webhook.http @@ -0,0 +1,292 @@ +### +POST http://localhost:8080/v1/stripe/webhook +Content-Type: application/json + +{ + "id": "evt_3Ls83eLswFpehDNg0dmzogyf", + "object": "event", + "api_version": "2022-08-01", + "created": 1665593127, + "data": { + "object": { + "id": "ch_3Ls83eLswFpehDNg0WVw0vTa", + "object": "charge", + "amount": 250, + "amount_captured": 250, + "amount_refunded": 0, + "application": null, + "application_fee": null, + "application_fee_amount": null, + "balance_transaction": "txn_3Ls83eLswFpehDNg0dFvPaKv", + "billing_details": { + "address": { + "city": null, + "country": "DE", + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": "a@b.d", + "name": "asdfasf", + "phone": null + }, + "calculated_statement_descriptor": "Stripe", + "captured": true, + "created": 1665593127, + "currency": "usd", + "customer": null, + "description": null, + "destination": null, + "dispute": null, + "disputed": false, + "failure_balance_transaction": null, + "failure_code": null, + "failure_message": null, + "fraud_details": {}, + "invoice": null, + "livemode": false, + "metadata": {}, + "on_behalf_of": null, + "order": null, + "outcome": { + "network_status": "approved_by_network", + "reason": null, + "risk_level": "normal", + "risk_score": 21, + "seller_message": "Payment complete.", + "type": "authorized" + }, + "paid": true, + "payment_intent": "pi_3Ls83eLswFpehDNg0b2mAFUW", + "payment_method": "pm_1Ls83dLswFpehDNgpYAGL3j9", + "payment_method_details": { + "card": { + "brand": "mastercard", + "checks": { + "address_line1_check": null, + "address_postal_code_check": null, + "cvc_check": "pass" + }, + "country": "US", + "exp_month": 12, + "exp_year": 2023, + "fingerprint": "DoAWRfUcyOfJupbL", + "funding": "credit", + "installments": null, + "last4": "4444", + "mandate": null, + "network": "mastercard", + "three_d_secure": null, + "wallet": null + }, + "type": "card" + }, + "receipt_email": null, + "receipt_number": null, + "receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xTGZORGFMc3dGcGVoRE5nKKjem5oGMgZo4m-xDMM6LBadftys-t7FIeo23hfQKTAtYI3zpLwmJb_3-A6VqCpIGjfmpkWUwCDQC38M", + "refunded": false, + "refunds": { + "object": "list", + "data": [], + "has_more": false, + "total_count": 0, + "url": "/v1/charges/ch_3Ls83eLswFpehDNg0WVw0vTa/refunds" + }, + "review": null, + "shipping": null, + "source": null, + "source_transfer": null, + "statement_descriptor": null, + "statement_descriptor_suffix": null, + "status": "succeeded", + "transfer_data": null, + "transfer_group": null + } + }, + "livemode": false, + "pending_webhooks": 2, + "request": { + "id": "req_F8pjOORr12gJT9", + "idempotency_key": "8fdd25c9-cb73-4807-973f-f0b21d8bb7cc" + }, + "type": "charge.succeeded" +} + + +### +POST http://localhost:8080/v1/stripe/webhook +Content-Type: application/json + +{ + "id": "evt_3Ls83eLswFpehDNg0YbJevK2", + "object": "event", + "api_version": "2022-08-01", + "created": 1665593128, + "data": { + "object": { + "id": "pi_3Ls83eLswFpehDNg0b2mAFUW", + "object": "payment_intent", + "amount": 250, + "amount_capturable": 0, + "amount_details": { + "tip": {} + }, + "amount_received": 250, + "application": null, + "application_fee_amount": null, + "automatic_payment_methods": null, + "canceled_at": null, + "cancellation_reason": null, + "capture_method": "automatic", + "charges": { + "object": "list", + "data": [ + { + "id": "ch_3Ls83eLswFpehDNg0WVw0vTa", + "object": "charge", + "amount": 250, + "amount_captured": 250, + "amount_refunded": 0, + "application": null, + "application_fee": null, + "application_fee_amount": null, + "balance_transaction": "txn_3Ls83eLswFpehDNg0dFvPaKv", + "billing_details": { + "address": { + "city": null, + "country": "DE", + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": "a@b.d", + "name": "asdfasf", + "phone": null + }, + "calculated_statement_descriptor": "Stripe", + "captured": true, + "created": 1665593127, + "currency": "usd", + "customer": null, + "description": null, + "destination": null, + "dispute": null, + "disputed": false, + "failure_balance_transaction": null, + "failure_code": null, + "failure_message": null, + "fraud_details": {}, + "invoice": null, + "livemode": false, + "metadata": {}, + "on_behalf_of": null, + "order": null, + "outcome": { + "network_status": "approved_by_network", + "reason": null, + "risk_level": "normal", + "risk_score": 21, + "seller_message": "Payment complete.", + "type": "authorized" + }, + "paid": true, + "payment_intent": "pi_3Ls83eLswFpehDNg0b2mAFUW", + "payment_method": "pm_1Ls83dLswFpehDNgpYAGL3j9", + "payment_method_details": { + "card": { + "brand": "mastercard", + "checks": { + "address_line1_check": null, + "address_postal_code_check": null, + "cvc_check": "pass" + }, + "country": "US", + "exp_month": 12, + "exp_year": 2023, + "fingerprint": "DoAWRfUcyOfJupbL", + "funding": "credit", + "installments": null, + "last4": "4444", + "mandate": null, + "network": "mastercard", + "three_d_secure": null, + "wallet": null + }, + "type": "card" + }, + "receipt_email": null, + "receipt_number": null, + "receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xTGZORGFMc3dGcGVoRE5nKKjem5oGMgbvNEiopYo6LBaa9sMFbxpWzGb2WX9aOuc4LWzXH4hUaNoIgvdvHlJv5zChsx0FH0MJcj6b", + "refunded": false, + "refunds": { + "object": "list", + "data": [], + "has_more": false, + "total_count": 0, + "url": "/v1/charges/ch_3Ls83eLswFpehDNg0WVw0vTa/refunds" + }, + "review": null, + "shipping": null, + "source": null, + "source_transfer": null, + "statement_descriptor": null, + "statement_descriptor_suffix": null, + "status": "succeeded", + "transfer_data": null, + "transfer_group": null + } + ], + "has_more": false, + "total_count": 1, + "url": "/v1/charges?payment_intent=pi_3Ls83eLswFpehDNg0b2mAFUW" + }, + "client_secret": "pi_3Ls83eLswFpehDNg0b2mAFUW_secret_72eKxCUCTrTLH0E3dSvrlwmgO", + "confirmation_method": "automatic", + "created": 1665593126, + "currency": "usd", + "customer": null, + "description": null, + "invoice": null, + "last_payment_error": null, + "livemode": false, + "metadata": {}, + "next_action": null, + "on_behalf_of": null, + "payment_method": "pm_1Ls83dLswFpehDNgpYAGL3j9", + "payment_method_options": { + "card": { + "installments": null, + "mandate_options": null, + "network": null, + "request_three_d_secure": "automatic" + } + }, + "payment_method_types": [ + "card" + ], + "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 + } + }, + "livemode": false, + "pending_webhooks": 2, + "request": { + "id": "req_F8pjOORr12gJT9", + "idempotency_key": "8fdd25c9-cb73-4807-973f-f0b21d8bb7cc" + }, + "type": "payment_intent.succeeded" +} + + + diff --git a/src/PaymentServer/Processors/Stripe.hs b/src/PaymentServer/Processors/Stripe.hs index 294b385..bb6c982 100644 --- a/src/PaymentServer/Processors/Stripe.hs +++ b/src/PaymentServer/Processors/Stripe.hs @@ -5,12 +5,12 @@ {-# LANGUAGE ScopedTypeVariables #-} module PaymentServer.Processors.Stripe - ( StripeAPI + ( ChargesAPI , WebhookAPI , Charges(Charges) , Acknowledgement(Ok) , Failure(Failure) - , stripeServer + , chargeServer , webhookServer , getVoucher , charge @@ -42,6 +42,8 @@ import Network.HTTP.Types , status503 ) +import Data.ByteString (ByteString) + import Data.ByteString.UTF8 ( toString ) @@ -111,8 +113,7 @@ instance ToJSON Acknowledgement where [ "success" .= True ] -type StripeAPI = ChargesAPI -type WebhookAPI = ReqBody '[JSON] Event :> Post '[JSON] Acknowledgement +type WebhookAPI = "webhook" :> ReqBody '[JSON] Event :> Post '[JSON] Acknowledgement -- | getVoucher finds the metadata item with the key `"Voucher"` and returns -- the corresponding value, or Nothing. @@ -121,14 +122,18 @@ getVoucher (MetaData []) = Nothing getVoucher (MetaData (("Voucher", value):xs)) = Just value getVoucher (MetaData (x:xs)) = getVoucher (MetaData xs) -stripeServer :: VoucherDatabase d => StripeConfig -> d -> Server StripeAPI -stripeServer stripeConfig d = +chargeServer :: VoucherDatabase d => StripeConfig -> d -> Server ChargesAPI +chargeServer stripeConfig d = withSuccessFailureMetrics chargeAttempts chargeSuccesses . charge stripeConfig d --stripeServer :: VoucherDatabase d => StripeSecretKey -> d -> Server StripeAPI --stripeServer key d = webhook d :<|> charge d key --- | Process charge succeeded events +--webhookServer :: VoucherDatabase d => StripeConfig -> d -> ByteString -> Handler Acknowledgement +--webhookServer stripeConfig d payload = + + +-- | Process charge succeeded webhookServer :: VoucherDatabase d => StripeConfig -> d -> Event -> Handler Acknowledgement webhookServer stripeConfig d Event{eventId=Just (EventId eventId), eventType=ChargeSucceededEvent, eventData=(ChargeEvent charge)} = case getVoucher $ chargeMetaData charge of diff --git a/src/PaymentServer/Server.hs b/src/PaymentServer/Server.hs index 4029f74..342888f 100644 --- a/src/PaymentServer/Server.hs +++ b/src/PaymentServer/Server.hs @@ -37,9 +37,9 @@ import Web.Stripe.Client ) import PaymentServer.Processors.Stripe - ( StripeAPI + ( ChargesAPI , WebhookAPI - , stripeServer + , chargeServer , webhookServer ) import PaymentServer.Redemption @@ -60,17 +60,17 @@ import PaymentServer.Persistence -- | This is the complete type of the server API. type PaymentServerAPI - = "v1" :> "stripe" :> StripeAPI + = "v1" :> "stripe" :> ChargesAPI + :<|> "v1" :> "stripe" :> WebhookAPI :<|> "v1" :> "redeem" :> RedemptionAPI - :<|> "v1" :> "webhook" :> WebhookAPI :<|> MetricsAPI -- | Create a server which uses the given database. paymentServer :: VoucherDatabase d => StripeConfig -> RedemptionConfig -> d -> Server PaymentServerAPI paymentServer stripeConfig redemptionConfig database = - stripeServer stripeConfig database - :<|> redemptionServer redemptionConfig database + chargeServer stripeConfig database :<|> webhookServer stripeConfig database + :<|> redemptionServer redemptionConfig database :<|> metricsServer paymentServerAPI :: Proxy PaymentServerAPI