Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
9a95dd9
restore old webhook endpoint
wuan Sep 15, 2022
26bd10f
update versions and add missing imports
wuan Sep 15, 2022
01fdeb1
reference updated version
wuan Sep 19, 2022
185298e
partial fix from meeting
wuan Sep 20, 2022
d74b049
fixed last compile error
wuan Sep 20, 2022
42d19c5
import cleanup
wuan Oct 11, 2022
1ca9928
update dependency
wuan Oct 11, 2022
2c4a791
Merge remote-tracking branch 'upstream/main' into new_payment
wuan Oct 11, 2022
dd25dc7
upgrade network to avoid compile errors
wuan Oct 12, 2022
2bf07c9
update materialized payment server
wuan Oct 12, 2022
7061a2a
Merge remote-tracking branch 'upstream/main' into new_payment
wuan Oct 12, 2022
8f76ae7
move webhook endpoint to stripe path and add some example payloads
wuan Oct 13, 2022
cc40dad
accept stripe signature header and plain text to prepare validation
wuan Oct 25, 2022
a50b0ce
packaging changes to get stripe-signature
hacklschorsch Oct 25, 2022
ec6584b
start writing a test for the webhook endpoint
hacklschorsch Oct 25, 2022
bb9a91e
the pretty version of the charge event json
hacklschorsch Oct 25, 2022
b734408
Merge remote-tracking branch 'wuan/new_payment_prepare_validation' in…
exarkun Oct 26, 2022
b0d4f8b
Test that demonstrates a 200 response for an Event-having request body
exarkun Oct 26, 2022
7fd0a0b
exercise and implement some more cases of the webhook
exarkun Oct 26, 2022
5e7813a
fix parsing of Stripe Events in the happy-path
exarkun Oct 26, 2022
85359d8
steps towards handling properly signed webhook requests
exarkun Oct 27, 2022
76ba261
Merge branch '125.give-up-chargeid' into 240.stripe-webhook-and-payme…
exarkun Oct 27, 2022
b23be3d
be explicit in what we are importing
exarkun Oct 27, 2022
35bf5b1
Be sure we can handle the other events Stripe might deliver to us
exarkun Oct 28, 2022
944ae74
Thread the webhook secret key from CLI to webhook implementation
exarkun Oct 28, 2022
8d86a2e
Fix the materialization
exarkun Oct 28, 2022
52335d5
This is redundant with the other example
exarkun Oct 28, 2022
83e9d46
We don't need a charge anymore and we have a payment_intent in the te…
exarkun Oct 28, 2022
9a18d35
Fix the tests :/
exarkun Oct 28, 2022
c820d5b
Merge remote-tracking branch 'origin/main' into 240.stripe-webhook-an…
exarkun Oct 28, 2022
341416c
regenerate materialization to account for main merge changes
exarkun Oct 28, 2022
5afd3b6
Merge branch '127.update-materialization' into 240.stripe-webhook-and…
exarkun Oct 28, 2022
6b74533
Some lint cleanup
exarkun Oct 28, 2022
f7be761
note about timestamp checking
exarkun Oct 28, 2022
ede0a1c
webhook is elsewhere now
exarkun Oct 28, 2022
68aac17
whitespace
exarkun Oct 28, 2022
529d29b
note about our stripe fork
exarkun Oct 28, 2022
f501f82
made a ticket for that one
exarkun Oct 28, 2022
61f4909
let's not even bother, source ip verification is amazingly weak form …
exarkun Oct 28, 2022
1ff1be9
Some minor test refactoring
exarkun Oct 28, 2022
ca664a9
Merge remote-tracking branch 'origin/main' into 240.stripe-webhook-an…
exarkun Oct 28, 2022
60631a8
Merge branch 'main' into 240.stripe-webhook-and-payment-intents
exarkun Oct 28, 2022
4684edf
Attempt to clarify the motivation behind handling of certain cases
exarkun Oct 31, 2022
5e374d7
add PaymentServer-complete-payment
exarkun Oct 31, 2022
9ad1adc
Update the dev docs a little bit
exarkun Oct 31, 2022
24a8209
Make non-checkout.session.completed events errors
exarkun Oct 31, 2022
1b5e83d
Build more pieces on CI
exarkun Oct 31, 2022
11df85f
document the stripe interaction a bit
exarkun Oct 31, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,10 @@ jobs:
--option extra-trusted-public-keys "${TRUSTED_PUBLIC_KEYS}" \
-j 4 \
./nix/ \
-A PaymentServer.components.exes."PaymentServer-exe"
-A PaymentServer.components.exes."PaymentServer-exe" \
-A PaymentServer.components.exes."PaymentServer-generate-key" \
-A PaymentServer.components.exes."PaymentServer-get-public-key" \
-A PaymentServer.components.exes."PaymentServer-complete-payment"

- run:
name: "Building Tests"
Expand Down
22 changes: 22 additions & 0 deletions PaymentServer.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,21 @@ library
, aeson
, bytestring
, utf8-string
, base16-bytestring
, servant
, servant-server
, http-types
, http-media
, wai
, wai-extra
, wai-cors
, data-default
, warp
, warp-tls
, stripe-concepts
, stripe-haskell
, stripe-core
, stripe-signature
, text
, containers
, cryptonite
Expand Down Expand Up @@ -78,6 +82,22 @@ executable PaymentServer-get-public-key
, PaymentServer
default-language: Haskell2010

executable PaymentServer-complete-payment
hs-source-dirs: complete-payment
main-is: Main.hs
ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wmissing-import-lists -Wunused-imports
build-depends: base
, time
, text
, bytestring
, optparse-applicative
, unix-compat
, http-client
, stripe-concepts
, raw-strings-qq
, PaymentServer
default-language: Haskell2010

test-suite PaymentServer-tests
type: exitcode-stdio-1.0
hs-source-dirs: test
Expand All @@ -91,6 +111,8 @@ test-suite PaymentServer-tests
build-depends: aeson
, base
, bytestring
, stripe-signature
, stripe-concepts
, text
, transformers
, raw-strings-qq
Expand Down
42 changes: 34 additions & 8 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ Get all the build dependencies with nix::
$ nix-shell PrivateStorageio/shell.nix # Might be needed depending on your system, see #88
$ nix-shell PaymentServer/shell.nix

Build using Stack::
Build using Nix::

$ nix-build nix/ -A PaymentServer.components.exes.PaymentServer-exe -o exe

Or using Stack::

$ stack build

Expand All @@ -24,15 +28,19 @@ Testing
You can perform manual integration testing against Stripe.
First, run the server::

$ stack run
$ ./exe/bin/PaymentServer-exe [arguments]

Then create a testing charge::
Or with stack::

$ curl \
http://<youraddress>:8081/v1/stripe/charge \
-X POST \
-H 'content-type: application/json' \
--data '{ "token":"tok_visa", "voucher":"abcdefg", "amount":"650", "currency":"USD" }'
$ stack run -- [arguments]

Then report that payment has been received for a given voucher:

$ stack run -- \
PaymentServer-complete-payment \
--voucher abcdefg \
--server-url http://localhost:8081/ \
--webhook-secret-path ../stripe.webhook-secret

The PaymentServer marks the voucher as paid in its database.
Then redeem the vouncher for tokens::
Expand All @@ -42,3 +50,21 @@ Then redeem the vouncher for tokens::
-X POST \
-H 'content-type: application/json' \
--data '{ "redeemVoucher": "abcdefg", "redeemTokens":[]}'

Stripe Integration
------------------

PaymentServer listens for Stripe events at a "webhook" endpoint.
The endpoint is at ``/v1/stripe/webhook``.
It handles only ``checkout.session.completed`` events.
These events must include a voucher in the ``client_reference_id`` field.
A voucher so referenced will be marked as paid when this event is processed.

The webhook must be correctly configured in the associated Stripe account.
One way to configure it is with a request like::

curl \
https://api.stripe.com/v1/webhook_endpoints \
-u sk_test_yourkey: \
-d url="https://serveraddress/v1/stripe/webhook" \
-d "enabled_events[]"="checkout.session.completed"
253 changes: 253 additions & 0 deletions complete-payment/Main.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE QuasiQuotes #-}

module Main
( main
) where

import Text.Printf
( printf
)

import Data.Text
( Text
, pack
, unpack
)

import GHC.Natural
( naturalFromInteger
)

import Data.Text.Encoding
( encodeUtf8
)

import Data.ByteString
( ByteString
, readFile
)
import Data.ByteString.Char8
( strip
)

import Text.RawString.QQ
( r
)

import Options.Applicative
( Parser
, ParserInfo
, strOption
, option
, auto
, long
, help
, showDefault
, value
, info
, (<**>)
, helper
, fullDesc
, progDesc
, header
, execParser
)

import Network.HTTP.Client
( Request(method, requestBody, requestHeaders, path)
, RequestBody(RequestBodyBS)
, parseRequest
, newManager
, defaultManagerSettings
, httpLbs
, responseStatus
, responseBody
)

import Data.Time.Clock.POSIX
( getPOSIXTime
)

import Stripe.Concepts
( WebhookSecretKey(WebhookSecretKey)
)

import PaymentServer.Processors.Stripe
( stripeSignature
)

data Config = Config
{ configServerURL :: Text
, configVoucher :: Text
, configWebhookSecretPath :: FilePath
}

config :: Parser Config
config = Config
<$> strOption
( long "server-url"
<> help "The root URL of the PaymentServer on which to complete the payment."
<> showDefault
<> value "http://localhost:8000/"
)
<*> strOption
( long "voucher"
<> help "The voucher for which to complete payment."
)
<*> strOption
( long "webhook-secret-path"
<> help "The path to a file containing the webhook secret to use to sign the request."
)

options :: ParserInfo Config
options = info (config <**> helper)
( fullDesc
<> progDesc ""
<> header ""
)

-- Construct the request body for a `checkout.session.complete` event
-- containing the given voucher.
completePaymentBody :: Text -> ByteString
completePaymentBody =
encodeUtf8 . pack . printf template
where
template = [r|
{
"id": "evt_1LxcsdBHXBAMm9bPSq6UWAZe",
"object": "event",
"api_version": "2019-11-05",
"created": 1666903247,
"data": {
"object": {
"id": "cs_test_a1kWLWGoXZPa6ywyVnuib8DPA3BqXCWZX5UEjLfKh7gLjdZy2LD3F5mEp3",
"object": "checkout.session",
"after_expiration": null,
"allow_promotion_codes": null,
"amount_subtotal": 3000,
"amount_total": 3000,
"automatic_tax": {
"enabled": false,
"status": null
},
"billing_address_collection": null,
"cancel_url": "https://httpbin.org/post",
"client_reference_id": "%s",
"consent": null,
"consent_collection": null,
"created": 1666903243,
"currency": "usd",
"customer": "cus_Mh0u62xtelUehD",
"customer_creation": "always",
"customer_details": {
"address": {
"city": null,
"country": null,
"line1": null,
"line2": null,
"postal_code": null,
"state": null
},
"email": "stripe@example.com",
"name": null,
"phone": null,
"tax_exempt": "none",
"tax_ids": [

]
},
"customer_email": null,
"display_items": [
{
"amount": 1500,
"currency": "usd",
"custom": {
"description": "comfortable cotton t-shirt",
"images": null,
"name": "t-shirt"
},
"quantity": 2,
"type": "custom"
}
],
"expires_at": 1666989643,
"livemode": false,
"locale": null,
"metadata": {
},
"mode": "payment",
"payment_intent": "pi_3LxcsZBHXBAMm9bP1daBGoPV",
"payment_link": null,
"payment_method_collection": "always",
"payment_method_options": {
},
"payment_method_types": [
"card"
],
"payment_status": "paid",
"phone_number_collection": {
"enabled": false
},
"recovered_from": null,
"setup_intent": null,
"shipping": null,
"shipping_address_collection": null,
"shipping_options": [

],
"shipping_rate": null,
"status": "complete",
"submit_type": null,
"subscription": null,
"success_url": "https://httpbin.org/post",
"total_details": {
"amount_discount": 0,
"amount_shipping": 0,
"amount_tax": 0
},
"url": null
}
},
"livemode": false,
"pending_webhooks": 2,
"request": {
"id": null,
"idempotency_key": null
},
"type": "checkout.session.completed"
}
|]


main :: IO ()
main = do
Config
{ configServerURL
, configVoucher
, configWebhookSecretPath
} <- execParser options

let body = completePaymentBody configVoucher
webhookSecret <- WebhookSecretKey . strip <$> Data.ByteString.readFile configWebhookSecretPath
now <- naturalFromInteger . truncate <$> getPOSIXTime

req <- parseRequest . unpack $ configServerURL
let req' = req
{ method = "POST"
, path = "/v1/stripe/webhook"
, requestBody = RequestBodyBS body
, requestHeaders =
[ ( "Stripe-Signature"
, stripeSignature webhookSecret now body
)
, ( "Content-Type"
, "application/json; charset=utf-8"
)
]
}

manager <- newManager defaultManagerSettings
response <- httpLbs req' manager
print ((responseStatus response), (responseBody response))
1 change: 1 addition & 0 deletions nix/materialized.paymentserver/.stack-to-nix.cache
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
https://github.com/PrivateStorageio/servant-prometheus.git 622eb77cb08c5f13729173b8feb123a6700ff91f . 09dcwj5ac2x2ilb4a0rai3qhl875vvvjr12af5plpz86faga3lnz servant-prometheus .stack-to-nix.cache.0
https://github.com/PrivateStorageio/stripe.git 6c340eea6dc23c4245762b937b9701a40761a5c9 stripe-core 1bhw1dcmr2fiphxjvjl80qwg1yrw1ifldm4w4s11q3z0a6yphl5r stripe-core .stack-to-nix.cache.1
Loading