diff options
Diffstat (limited to 'Haskell/merchant-api-tutorial/taler-merchant-api-tutorial.hs')
-rw-r--r-- | Haskell/merchant-api-tutorial/taler-merchant-api-tutorial.hs | 329 |
1 files changed, 329 insertions, 0 deletions
diff --git a/Haskell/merchant-api-tutorial/taler-merchant-api-tutorial.hs b/Haskell/merchant-api-tutorial/taler-merchant-api-tutorial.hs new file mode 100644 index 0000000..56b668c --- /dev/null +++ b/Haskell/merchant-api-tutorial/taler-merchant-api-tutorial.hs @@ -0,0 +1,329 @@ +-- SPDX-FileCopyrightText: 2023 Vint Leenaars <vl.software@leenaa.rs> +-- SPDX-License-Identifier: Apache-2.0 +-- SPDX-License-Identifier: EUPL-1.2 +-- SPDX-License-Identifier: MPL-2.0 +-- SPDX-License-Identifier: AGPL-3.0-or-later + +-- See https://docs.taler.net/taler-merchant-api-tutorial.html for full tutorial +-- +-- GNU Taler is an open protocol for an electronic payment system with a free +-- software reference implementation. GNU Taler offers secure, fast and easy +-- payment processing using well understood cryptographic techniques. GNU Taler +-- allows customers to remain anonymous, while ensuring that merchants can be +-- held accountable by governments. Hence, GNU Taler is compatible with +-- anti-money-laundering (AML) and know-your-customer (KYC) regulation, as well +-- as data protection regulation (such as GDPR). + +{-# LANGUAGE OverloadedStrings, DeriveGeneric #-} + +-- IMPORTS -- +import Data.Aeson (FromJSON (..), (.=), object, encode, decode) +import Network.HTTP +import Network.HTTP.Client +import Network.HTTP.Client.TLS +import Network.HTTP.Types +import Network.URI + +import Data.ByteString.Lazy as BL +import Data.ByteString as B + +import Control.Monad (when) + +import GHC.Generics + +-- VARIABLES + +-- Domain name of the sandbox backend +domainName :: String +domainName = "https://backend.demo.taler.net/private/" + +-- Domain name of the sandbox backends orders +domainNameOrder :: String +domainNameOrder = "https://backend.demo.taler.net/private/orders" + +-- Key of the Authorization header ("Bearer secret-token:$KEY") +secretToken :: B.ByteString +secretToken = "sandbox" + +-- 4.1.4 Public Sandbox Backend and Authentication +-- https://docs.taler.net/taler-merchant-api-tutorial.html#public-sandbox-backend-and-authentication + +-- The public sandbox backend https://backend.demo.taler.net/ uses an API key +-- in the Authorization header. The value of this header must be Bearer +-- secret-token:secret for the public sandbox backend. + +-- The function 'authenticationTest' tests if authenticating to the backend works: + +authenticationTest :: Manager -> IO () +authenticationTest manager = do + initialized_request <- parseRequest domainNameOrder + + response <- httpLbs initialized_request { requestHeaders = [(hAuthorization, B.append "Bearer secret-token:" secretToken)] } manager + + let status_code = statusCode $ responseStatus response + + print $ "Function 'authenticationTest' returned status code: " ++ show status_code + + when (not $ status_code == 200) $ printHint response + +-- This should return HTTP status code 200. + + + +-- 4.2.1: Creating an Order for a Payment +-- https://docs.taler.net/taler-merchant-api-tutorial.html#creating-an-order-for-a-payment + +-- Payments in Taler revolve around an order, which is a machine-readable +-- description of the business transaction for which the payment is to be made. +-- Before accepting a Taler payment as a merchant you must create such an +-- order. + +-- The function 'createOrder' creates a minimal order and returns a response: + +createOrder :: Manager -> IO BL.ByteString +createOrder manager = do + initialized_request <- parseRequest domainNameOrder + + let order = object [ "create_token" .= False + , "order" .= object [ "amount" .= ("KUDOS:10" :: String) + , "summary" .= ("Donation" :: String) + , "fulfillment_url" .= ("https://example.com/thanks.html" :: String) ] ] + + let request = initialized_request { requestHeaders = [(hAuthorization, B.append "Bearer secret-token:" secretToken )] + , method = "POST" + , requestBody = RequestBodyLBS $ encode order } + + response <- httpLbs request manager + + let status_code = statusCode $ responseStatus response + + print $ "Function 'createOrder' returned status code: " ++ show status_code + + if status_code == 200 + then print $ "Created an order with ID: " ++ (order_id $ responseToOrderID $ responseBody response) + else printHint response + + return $ responseBody response + + +-- The backend will fill in some details missing in the order, such as the +-- address of the merchant instance. The full details are called the contract +-- terms. + +-- After successfully POSTing to /private/orders, a JSON with just an order_id +-- field with a string representing the order ID will be returned. + + + +-- 4.2.2: Checking Payment Status +-- https://docs.taler.net/taler-merchant-api-tutorial.html#checking-payment-status-and-prompting-for-payment + +-- Given the order ID, the status of a payment can be checked with the +-- /private/orders/$ORDER_ID endpoint. If the payment is yet to be completed by +-- the customer, /private/orders/$ORDER_ID will give the frontend a URL (under +-- the name payment_redirect_url) that will trigger the customer’s wallet to +-- execute the payment. + +-- The function 'checkPaymentStatus' checks what the current status of the +-- payment with given order_id is: + +checkPaymentStatus :: Manager -> String -> IO () +checkPaymentStatus manager order_id = do + + initialized_request <- parseRequest ( domainNameOrder ++ "/" ++ order_id ) + response <- httpLbs ( initialized_request { requestHeaders = [(hAuthorization, B.append "Bearer secret-token:" secretToken)] } ) manager + + let status_code = statusCode $ responseStatus response + + print $ "Function 'checkPaymentStatus' returned status code: " ++ show status_code + + if status_code == 200 + then print $ responseBody response + else printHint response + + +-- If the order_status field in the response is paid, you will not get a +-- payment_redirect_url and instead information about the payment status, +-- including contract_terms (the full contract terms of the order), +-- refunded (true if a (possibly partial) refund was granted for this purchase) +-- and refunded_amount (amount that was refunded). + + + +-- 4.3: Giving Refunds +-- https://docs.taler.net/taler-merchant-api-tutorial.html#index-9 + +-- A refund in GNU Taler is a way to “undo” a payment. It needs to be +-- authorized by the merchant. Refunds can be for any fraction of the original +-- amount paid, but they cannot exceed the original payment. Refunds are +-- time-limited and can only happen while the exchange holds funds for a +-- particular payment in escrow. The time during which a refund is possible can +-- be controlled by setting the refund_deadline in an order. The default value +-- for this refund deadline is specified in the configuration of the merchant’s +-- backend. +-- +-- The frontend can instruct the merchant backend to authorize a refund by +-- POSTing to the /private/orders/$ORDER_ID/refund endpoint. + +-- The function 'requestRefund' requests for a refund: + +requestRefund :: Manager -> String -> IO () +requestRefund manager order_id = do + + initialized_request <- parseRequest ( domainNameOrder ++ "/" ++ order_id ++ "/refund" ) + + let order_refund = object [ "refund" .= ("KUDOS:10" :: String) + , "reason" .= ("Customer did not like the product" :: String) ] + + let refund_request = initialized_request { requestHeaders = [(hAuthorization, B.append "Bearer secret-token:" secretToken)] + , method = "POST" + , requestBody = RequestBodyLBS $ encode order_refund } + + response <- httpLbs refund_request manager + + let status_code = statusCode $ responseStatus response + + print $ "Function 'requestRefund' returned status code: " ++ show status_code + + if status_code == 200 + then print $ "Succesfully created request for refund, send customer to: " + ++ (taler_refund_uri $ responseToRefundURI $ responseBody response) + else printHint response + + +-- If the request is successful (indicated by HTTP status code 200), the +-- response includes a taler_refund_uri. The frontend must redirect the +-- customer’s browser to that URL to allow the refund to be processed by the +-- wallet. + + + +-- 4.5 Giving Customers Rewards +-- https://docs.taler.net/taler-merchant-api-tutorial.html#index-11 + +-- GNU Taler allows Web sites to grant digital cash directly to a visitor. The +-- idea is that some sites may want incentivize actions such as filling out a +-- survey or trying a new feature. It is important to note that receiving +-- rewards is not enforceable for the visitor, as there is no contract. It is +-- simply a voluntary gesture of appreciation of the site to its visitor. +-- However, once a reward has been granted, the visitor obtains full control +-- over the funds provided by the site. + +-- The function 'checkReserves' checks if rewards are confiured properly and +-- if there are sufficient funds avaliable for granting rewards: + +checkReserves :: Manager -> IO () +checkReserves manager = do + + initialized_request <- parseRequest $ domainName ++ "reserves" + + response <- httpLbs ( initialized_request { requestHeaders = [(hAuthorization, B.append "Bearer secret-token:" secretToken)] } ) manager + + let status_code = statusCode $ responseStatus response + + print $ "Function 'checkReserves' returned status code: " ++ show status_code + + if status_code == 200 + + -- Check that a reserve exists where the merchant_initial_amount is below the + -- committed_amount and that the reserve is active. + then print $ BL.append "Reserves found: " (responseBody response) + + else printHint response + +-- The response from the backend contains a taler_reward_url. The customer’s +-- browser must be redirected to this URL for the wallet to pick up the reward. + + + +-- The function 'giveReward' illustrates giving a reward +giveReward :: Manager -> IO () +giveReward manager = do + + initialized_request <- parseRequest $ domainName ++ "rewards" + + let order = object [ "amount" .= ("KUDOS:0.5" :: String) + , "justification" .= ("User filled out survey" :: String) + , "next_url" .= ("https://merchant.com/thanks.html" :: String) ] + + let reward_request = initialized_request { requestHeaders = [(hAuthorization, B.append "Bearer secret-token:" secretToken)] + , method = methodPost + , requestBody = RequestBodyLBS $ encode order } + + response <- httpLbs reward_request manager + + print response + + let status_code = statusCode $ responseStatus response + + print $ "Function 'giveReward' returned status code: " ++ show status_code + + if status_code == 200 + then print $ "Reward created, redirect customer to: " + ++ (taler_reward_url $ responseToRewardURL $ responseBody response) + else printHint response + + +-- HELPER FUNCTIONS & CUSTOM DATA TYPES + +printHint :: Network.HTTP.Client.Response BL.ByteString -> IO () +printHint response = do + print $ B.append "Status message: " (statusMessage $ responseStatus response) + case decode $ responseBody response :: Maybe FailedResponse of + Just help -> print $ Prelude.concat [ "Hint ", "(code ", show $ code help, "): ", hint help ] + Nothing -> return () + +data ResponseWithID = ResponseWithID { + order_id :: String } deriving (Show, Eq, Generic) + +data ResponseWithRefundURI = ResponseWithRefundURI { + taler_refund_uri :: String } deriving (Show, Eq, Generic) + +data ResponseWithRewardURL = ResponseWithRewardURL { + taler_reward_url :: String } deriving (Show, Eq, Generic) + +data FailedResponse = FailedResponse + { hint :: String + , code :: Int } deriving (Show, Eq, Generic) + +instance FromJSON ResponseWithID +instance FromJSON ResponseWithRefundURI +instance FromJSON ResponseWithRewardURL +instance FromJSON FailedResponse + +responseToOrderID :: BL.ByteString -> ResponseWithID +responseToOrderID input = case decode input of + Just r -> r + Nothing -> ResponseWithID { order_id = "NO ORDER ID FOUND" } + +responseToRefundURI :: BL.ByteString -> ResponseWithRefundURI +responseToRefundURI input = case decode input of + Just r -> r + Nothing -> ResponseWithRefundURI { taler_refund_uri = "NO REFUND URI FOUND" } + +responseToRewardURL :: BL.ByteString -> ResponseWithRewardURL +responseToRewardURL input = case decode input of + Just r -> r + Nothing -> ResponseWithRewardURL { taler_reward_url = "NO REWARD URL FOUND" } + + + +-- MAIN -- +main :: IO () +main = do + let settings = managerSetProxy (proxyEnvironment Nothing) tlsManagerSettings + manager <- newTlsManagerWith settings + + authenticationTest manager + + response <- createOrder manager + + let orderId = order_id $ responseToOrderID response + + checkPaymentStatus manager orderId + + requestRefund manager orderId + + checkReserves manager + + giveReward manager |