paynow-zw (empty) → 0.1.0.0
raw patch · 7 files changed
+638/−0 lines, 7 filesdep +HTTPdep +basedep +bytestringsetup-changed
Dependencies added: HTTP, base, bytestring, containers, cryptohash, hspec, http-conduit, http-types, paynow-zw, text, unliftio
Files
- CHANGELOG.md +11/−0
- LICENSE +30/−0
- README.md +62/−0
- Setup.hs +2/−0
- paynow-zw.cabal +61/−0
- src/Network/Payments/Paynow.hs +333/−0
- test/Spec.hs +139/−0
+ CHANGELOG.md view
@@ -0,0 +1,11 @@+# Changelog for `paynow-zw`++All notable changes to this project will be documented in this file.++The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),+and this project adheres to the+[Haskell Package Versioning Policy](https://pvp.haskell.org/).++## Unreleased++## 0.1.0.0 - YYYY-MM-DD
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Author name here (c) 2022++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Author name here nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,62 @@+# Paynow Zimbabwe Haskell SDK++Haskell SDK for Paynow Zimbabwe's API++# Prerequisites++This library has a set of prerequisites that must be met for it to work++```Haskell+{-# LANGUAGE OverloadedStrings #-}+import Network.Payments.Paynow as Paynow++ # Do stuff+```++---++# Usage example++```Haskell+defaultConfig :: PaynowConfig+defaultConfig = defaultProdConfig "1201"+ "3e9fed89-60e1-4ce5-ab6e-6b1eb2d4f977"+ "http://localhost:3000/result"+ "http://localhost:3000/return"++....+client <- newPaynowClient config+payment <- newExpressCheckout (Ecocash "0779800700") "45" 37.50 "email" Nothing+result <- processTx payment+case result of+ Left e => //handle err+ Right r => //handle result r :: PaynowTxResult+```++Create a new payment passing in the reference for that payment (e.g invoice id, or anything that you can use to identify the transaction and the user's email address++---++> Express Transactions++```Haskell+testVMCDetails :: VMC+testVMCDetails = VMC (Card "4111111111111111" "588" "2023" "Trevor Sibanda") (BillingAddress "Address Line 1" "Line 2" "City" "Zimbabwe" Nothing)+++payment <- newExpressCheckout client (OneMoney "0783102754") testRef testAmount testEmail Nothing++payment <- newExpressCheckout client (Ecocash "0715900800") testRef testAmount testEmail Nothing++payment <- newExpressCheckout client (VisaMastercard testVMCDetails) testRef testAmount testEmail Nothing++```++# Checking transaction status++The SDK exposes a handy method that you can use to check the status of a transaction. Once you have instantiated the Paynow class.++```Haskell+tx <- newPollTransaction "https://www.paynow.co.zw/Interface/CheckPayment/?guid=dece867e-5a40-4961-bf0e-5d691c2a97f8"+processtx tx >>= print+```
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ paynow-zw.cabal view
@@ -0,0 +1,61 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.34.4.+--+-- see: https://github.com/sol/hpack++name: paynow-zw+version: 0.1.0.0+synopsis: A Haskell wrapper for the Paynow payment gateway+description: Please see the README on GitHub at <https://github.com/trevorsibanda/paynow-zw#readme>+category: payments+homepage: https://github.com/trevorsibanda/paynow-zw#readme+bug-reports: https://github.com/trevorsibanda/paynow-zw/issues+author: Trevor Sibanda+maintainer: sibandatrevor@gmail.com+copyright: 2022 Trevor Sibanda+license: BSD3+license-file: LICENSE+build-type: Simple+extra-source-files:+ README.md+ CHANGELOG.md++source-repository head+ type: git+ location: https://github.com/trevorsibanda/paynow-zw++library+ exposed-modules:+ Network.Payments.Paynow+ other-modules:+ Paths_paynow_zw+ hs-source-dirs:+ src+ ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints+ build-depends:+ HTTP >=4000.4.1 && <4000.5+ , base >=4.7 && <5+ , bytestring >=0.10.12 && <0.11+ , containers >=0.6.5 && <0.7+ , cryptohash >=0.11.9 && <0.12+ , http-conduit >=2.3.8 && <2.4+ , http-types >=0.12.3 && <0.13+ , text >=1.2.4 && <1.3+ , unliftio >=0.2.22 && <0.3+ default-language: Haskell2010++test-suite paynow-zw-test+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ other-modules:+ Paths_paynow_zw+ hs-source-dirs:+ test+ ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N+ build-depends:+ base >=4.7 && <5+ , hspec+ , paynow-zw+ , unliftio+ default-language: Haskell2010
+ src/Network/Payments/Paynow.hs view
@@ -0,0 +1,333 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE FlexibleContexts #-}++module Network.Payments.Paynow+ ( + newPaynowClient,+ defaultProdConfig,+ PaynowConfig (..),+ PaynowClient (..),+ PaynowTransaction (..),+ Checkout (..),+ PaynowTXResult (..),+ PaymentMethod (..),+ TxData (..),+ PaynowError(..),+ PaynowTXPayload (..),+ Status(..),+ BillingAddress (..),+ Card (..),+ PhoneNumber,+ Email,+ Amount,+ Ref,+ PaynowRef,+ PaynowIntegrationID,+ PaynowIntegrationKey,+ PaynowResultURL,+ PaynowReturnURL,+ Hash,+ RedirectURL,+ PollURL,+ VMC (..),+ serverResponseToResult,+ sha1Hex,+ )+where++import UnliftIO.Exception+import UnliftIO+import qualified Crypto.Hash as C+import qualified Data.ByteString.Lazy.Char8 as BS+import qualified Data.Map as M+import Data.List (sort)+import Data.Maybe (catMaybes, fromMaybe, isJust)+import Data.Text hiding (zip, filter, foldr)+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import Network.HTTP.Base (urlDecode, urlEncodeVars)+import Network.HTTP.Types.Header (hContentType)+import Network.HTTP.Simple++prodPaynowEndpoint :: Text+prodPaynowEndpoint = "https://www.paynow.co.zw/interface/"+++-- | Additional data to be sent with the transaction+data TxData = TxData+ { tAdditionalInfo :: Maybe Text,+ tResultURL :: Maybe PaynowResultURL,+ tReturnURL :: Maybe PaynowReturnURL+ }++instance ToKeyMap (Maybe TxData) where+ toKeyMap Nothing = []+ toKeyMap (Just (TxData {..})) = foldr + (\(k, v) accum -> if (isJust v) then (k, fromMaybe "" v) : accum else accum )+ []+ [("additionalinfo", tAdditionalInfo), ("returnurl", tReturnURL), ("resulturl", tResultURL)]++class ToKeyMap a where+ toKeyMap :: a -> [(Text, Text)]+++-- Type aliases used to make documentation more readable+type PhoneNumber = Text+type Email = Text+type Amount = Double+type PaynowRef = Text+type Instructions = Text++data Status = Created | Error | Cancelled | Paid | Okay | Refunded | Failed | OtherStatus Text deriving (Show, Eq)+type PollURL = Text+type RedirectURL = Text+type Hash = Text+type Ref = Text++type PaynowIntegrationID = Text+type PaynowIntegrationKey = Text+type PaynowResultURL = Text+type PaynowReturnURL = Text+++-- | Card Details+data Card = Card {cardNumber :: !Text, cardCVV :: !Text, cardExpiry :: !Text, cardName :: !Text} deriving (Show)++-- | ToKeyMap typeclass+instance ToKeyMap Card where+ toKeyMap Card {..} = [("cardnumber", cardNumber), ("cardname", cardName), ("cardcvv", cardCVV), ("cardexpiry", cardExpiry)]++-- | Billing Address+data BillingAddress = BillingAddress {baAddressLine1 :: !Text, baAddressLine2 :: !Text, baCity :: !Text, baCountry :: !Text, baProvince :: Maybe Text} deriving (Show)++instance ToKeyMap BillingAddress where+ toKeyMap BillingAddress {..} =+ [("billingline1", baAddressLine1), ("billingline2", baAddressLine2), ("billingcity", baCity), ("billingprovince", fromMaybe "" baProvince), ("billingcountry", baCountry)]++-- | Visa Mastercard Details+data VMC = VMC Card BillingAddress deriving (Show)++instance ToKeyMap VMC where+ toKeyMap (VMC card address) = (toKeyMap card) <> (toKeyMap address)++-- | Ecocash, OneMoney, VisaMastercard payment methods+data PaymentMethod = Ecocash PhoneNumber | OneMoney PhoneNumber | VisaMastercard VMC deriving (Show)++instance ToKeyMap PaymentMethod where+ toKeyMap (Ecocash phone) = [("phone", phone), ("method", "ecocash")]+ toKeyMap (OneMoney phone) = [("phone", phone), ("method", "onemoney")]+ toKeyMap (VisaMastercard vmcd) = toKeyMap vmcd++-- | Checkout type+data Checkout = ExpressCheckout PaymentMethod | ClassicCheckout deriving (Show)++instance ToKeyMap Checkout where+ toKeyMap ClassicCheckout = []+ toKeyMap (ExpressCheckout method) = toKeyMap method++data PaynowError = ServerError | InternalError | UnHandledException SomeException | OtherError Text deriving (Show)++-- |+-- | PaynowTXResult is the result of a transaction+-- | +data PaynowTXResult = + InitExpressPaymentSuccess Status PaynowRef Instructions PollURL Hash | + InitClassicCheckoutSuccess Status RedirectURL PollURL Hash |+ PollPaymentSuccess Status PaynowRef Ref Amount PollURL Hash |+ VerifySignatureSuccess deriving (Show, Eq)++-- |+-- PaynowTransaction is the type of transaction to be performed.+--+-- InitPayment: Initialize a payment+-- PollPayment: Poll for the status of a payment+-- VerifyStatusUpdate: Verify the authenticity of a status update+data PaynowTransaction = InitPayment Ref Amount Email Checkout (Maybe TxData) | PollPayment PollURL | VerifyStatusUpdate [(Text, Text)]++statusMessage :: [(Text, Text)]+statusMessage = [("status", "Message")]++-- | Ordering of keys for hashing and form body+txOrdering :: [Text]+txOrdering = ["resulturl", "returnurl", "reference", "amount", "id", "additionalinfo", "authemail", "phone", "method", "status", "hash"]++orderTXKeyMap :: [(Text, Text)] -> [(Text, Text)]+orderTXKeyMap l =+ catMaybes $ (\k -> (k,) <$> M.lookup k m) <$> txOrdering+ where+ m :: M.Map Text Text+ m = M.fromList l++instance ToKeyMap PaynowTransaction where+ toKeyMap (InitPayment ref amount email checkout mTxData) =+ orderTXKeyMap $ statusMessage <> [("reference", ref), ("amount", T.pack $ show amount), ("authemail", email)] <> (toKeyMap checkout) <> (toKeyMap mTxData)+ toKeyMap (PollPayment _) = statusMessage+ toKeyMap _ = statusMessage++class PaynowTXPayload a where+ urlParamsWithHash :: a -> PaynowConfig -> (Hash, Text)+ hash :: a -> PaynowConfig -> Text++urlEncodeT :: [(Text, Text)] -> Text+urlEncodeT tple = T.pack $ urlEncodeVars $ (\(k, v) -> (T.unpack k, T.unpack v)) <$> tple++sha1Hex :: Text -> Hash+sha1Hex t =+ T.toUpper $ T.decodeUtf8 $ (C.digestToHexByteString (C.hash (T.encodeUtf8 t) :: C.Digest C.SHA512))++instance PaynowTXPayload PaynowTransaction where+ hash tx config =+ sha1Hex $ (intercalate "" ((snd) <$> (orderTXKeyMap $ toKeyMap config <> toKeyMap tx))) <> (pncIntegrationKey config)++ urlParamsWithHash tx config =+ (hashed, urlEncodeT $ orderTXKeyMap ((toKeyMap config <> toKeyMap tx <> [("hash", hashed)])))+ where+ hashed :: Text+ hashed = hash tx config++-- |+-- PaynowConfig is the configuration for the Paynow API.+-- For more details, see https://www.paynow.co.zw/docs/api/+--+-- pncEndpoint: The endpoint to use for the API+-- pncIntegrationID: The integration ID for the API+-- pncIntegrationKey: The integration key for the API+-- pncResultURL: The URL to redirect to after a transaction is completed+-- pncReturnURL: The URL to redirect to after a transaction is completed+data PaynowConfig = PaynowConfig+ { pncEndpoint :: !Text,+ pncIntegrationID :: !PaynowIntegrationID,+ pncIntegrationKey :: !PaynowIntegrationKey,+ pncResultURL :: !PaynowResultURL,+ pncReturnURL :: !PaynowReturnURL+ }+ deriving (Show)++checkoutEndpoint :: PaynowConfig -> Checkout -> Text+checkoutEndpoint PaynowConfig {..} ClassicCheckout =+ pncEndpoint <> "initiatetransaction"+checkoutEndpoint PaynowConfig {..} (ExpressCheckout _) =+ pncEndpoint <> "remotetransaction"++instance ToKeyMap PaynowConfig where+ toKeyMap PaynowConfig {..} =+ [("id", pncIntegrationID), ("resulturl", pncResultURL), ("returnurl", pncReturnURL)]++-- |+-- | A PaynowClient is a client for the Paynow API. It contains the following functions:+-- | - newExpressCheckout: Create a new express checkout transaction (mobile or visa mastercard)+-- | - newClassicCheckout: Create a new classic checkout transaction (user will be redirected to url to complete payment)+-- | - newPollPayment: Create a new poll payment transaction (poll for the status of a payment)+-- | - newVerifyStatusUpdate: Create a new verify status update transaction (verify the authenticity of a status update)+-- | - processTx: Process a transaction, sends an HTTP request to the Paynow API+-- | - txHash: Get the hash of a transaction+data PaynowClient m = PaynowClient+ { newExpressCheckout :: PaymentMethod -> Ref -> Amount -> Email -> Maybe TxData -> m PaynowTransaction,+ newClassicCheckout :: Ref -> Amount -> Email -> Maybe TxData -> m PaynowTransaction,+ newPollPayment :: PollURL -> m PaynowTransaction,+ newVerifyStatusUpdate :: [(Text, Text)] -> m PaynowTransaction,+ processTx :: PaynowTransaction -> m (Either PaynowError PaynowTXResult),+ txHash :: PaynowTransaction -> m Text+ }++-- | The default configuration for the Paynow production API+defaultProdConfig :: PaynowIntegrationID -> PaynowIntegrationKey -> PaynowResultURL -> PaynowReturnURL -> PaynowConfig+defaultProdConfig apiID apiSecret = PaynowConfig prodPaynowEndpoint apiID apiSecret++++-- |+-- Create a new PaynowClient with the given configuration.+-- @+-- client <- newPaynowClient config+-- payment <- newExpressCheckout (Ecocash "0779800700") "45" 37.50 Nothing+-- result <- processTx payment+-- case result of+-- PaynowError e => //handle err+-- PaynowTxResult r => //handle result+-- @+newPaynowClient :: forall m. (MonadUnliftIO m) => PaynowConfig -> (PaynowClient m)+newPaynowClient config =+ PaynowClient {..}+ where+ newExpressCheckout method ref amount email mTXData =+ pure $ InitPayment ref amount email (ExpressCheckout method) mTXData++ newClassicCheckout ref amount email mTXData =+ pure $ InitPayment ref amount email ClassicCheckout mTXData++ newPollPayment = pure . PollPayment++ newVerifyStatusUpdate = pure . VerifyStatusUpdate++ txHash = pure . fst . (`urlParamsWithHash` config)++ processTx tx = processTxHandler `handleAny` case tx of+ InitPayment _ _ _ checkout _ -> do+ r <- pure $ parseRequest_ $ T.unpack $ "POST " <> (checkoutEndpoint config checkout)+ let req = addRequestHeader hContentType "application/x-www-form-urlencoded" $ setRequestBodyLBS (BS.pack $ T.unpack $ snd $ urlParamsWithHash tx config) r+ response <- httpLBS req+ let body = T.pack . BS.unpack $ getResponseBody response+ case getResponseStatusCode response of+ 200 -> pure $ serverResponseToResult body+ _ -> pure $ Left $ ServerError+ PollPayment url -> do+ req <- pure $ parseRequest_ $ T.unpack $ "GET " <> url+ response <- httpLBS req+ case getResponseStatusCode response of+ 200 -> + pure $ serverResponseToResult (T.pack . BS.unpack $ getResponseBody response)+ _ -> pure $ Left $ ServerError+ VerifyStatusUpdate _ -> + pure $ Left $ OtherError "not yet implemented"++ + processTxHandler :: (MonadUnliftIO m) => SomeException -> m (Either PaynowError PaynowTXResult)+ processTxHandler ex = pure $ Left $ UnHandledException ex+++serverResponseToResult :: Text -> Either PaynowError PaynowTXResult+serverResponseToResult resp = case ( sort $ M.keys kvp) of+-- InitExpressPaymentSuccess Status PaynowRef Instructions PollURL Hash + ["hash", "instructions", "paynowreference", "pollurl", "status"] -> + Right $ InitExpressPaymentSuccess (ls "status") (l "paynowreference") (l "instructions") (l "pollurl") (l "hash")+-- InitClassicCheckoutSuccess Status RedirectURL PollURL Hash + ["browserurl", "hash", "pollurl", "status"] -> + Right $ InitClassicCheckoutSuccess (ls "status") (l "browserurl") (l "pollurl") (l "hash")+-- PollPaymentSuccess Status PaynowRef Ref Amount PollURL Hash + ["amount", "hash", "paynowreference", "pollurl", "reference", "status"] -> + Right $ PollPaymentSuccess (ls "status") (l "paynowreference") (l "reference") (la "amount") (l "pollurl") (l "hash")+ _ -> Left $ OtherError resp+ + where+ l :: Text -> Text+ l k = fromMaybe "" $ M.lookup k kvp++ la :: Text -> Amount+ la k = (read . T.unpack . fromMaybe "0" $ M.lookup k kvp) :: Amount++ ls :: Text -> Status+ ls t = case (T.toLower $ l t) of+ "paid" -> Paid+ "created" -> Created+ "ok" -> Okay+ "cancelled" -> Cancelled+ "failed" -> Failed+ "error" -> Error+ "refunded" -> Refunded+ other -> OtherStatus other+ ++ kvp :: M.Map Text Text+ kvp = M.fromList $ (\t -> explode $ splitOn "=" t) <$> splitOn "&" resp++ explode :: [Text] -> (Text, Text)+ explode [] = ("", "")+ explode [k] = (k, "")+ explode [k, v] = (k, T.pack . urlDecode $ T.unpack v)+ explode (k : vs) = (k, T.pack . urlDecode $ T.unpack $ T.unwords vs)
+ test/Spec.hs view
@@ -0,0 +1,139 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}++import Test.Hspec++import Data.Either+import UnliftIO+import Network.Payments.Paynow++-- | Run the tests for the paynow library+main :: IO ()+main = hspec $ do+ describe "PaynowTransaction" transactionSpec + describe "Transaction Hashing" hashingSpec+ describe "Response Parsing" responseSpec+ describe "Paynow Client" clientSpec++defaultConfig :: PaynowConfig+defaultConfig = defaultProdConfig "1201"+ "3e9fed89-60e1-4ce5-ab6e-6b1eb2d4f977"+ "http://localhost:3000/result"+ "http://localhost:3000/return"++runWithDefaultClientConfig :: (MonadUnliftIO m) => (PaynowClient m -> PaynowConfig -> m a) -> m a+runWithDefaultClientConfig f = do+ f client config+ where+ config = defaultConfig+ client = newPaynowClient config++runWithDefaultClient :: (MonadUnliftIO m) => (PaynowClient m -> m a) -> m a+runWithDefaultClient f = runWithDefaultClientConfig (\cl _ -> f cl)+++testRef :: Ref+testRef = "TEST REF"++testAmount :: Amount+testAmount = 99.99++testEmail :: Email+testEmail = "customer@gmail.com"++testVMCDetails :: VMC+testVMCDetails = VMC (Card "4111111111111111" "588" "2023" "Trevor Sibanda") (BillingAddress "Address Line 1" "Line 2" "City" "Zimbabwe" Nothing)++testPollResponse = "reference=01&paynowreference=15650881&amount=99.00&status=Cancelled&pollurl=https%3a%2f%2fwww.paynow.co.zw%2fInterface%2fCheckPayment%2f%3fguid%3dd80b830d-623b-4948-9ab1-aae4950929bd&hash=E4F6E55CF3408EBDF0F59951E9E7B74C17CD9798A8AE5DA7B5D68D04F014E40753C438AA55445EB42FD9E455EE7FFA7BF752AD6791DE878DAEF4B486A345EAC"++testClassicCheckoutResponse = "status=Ok&browserurl=https%3a%2f%2fwww.paynow.co.zw%2fPayment%2fConfirmPayment%2f15655543%2fsibandatrevor%40gmail.com%2f%2f&pollurl=https%3a%2f%2fwww.paynow.co.zw%2fInterface%2fCheckPayment%2f%3fguid%3ddece867e-5a40-4961-bf0e-5d691c2a97f8&hash=83B31644B176661DC643CEDD480ADF3C0DECC9E2A00B004B8DC37B6A88A2A246D1A3DE57D5BA41B61BBA2FE2580CEFDCDEE30698200B88CD85D62FE75E76CD4B"++testExpressCheckoutResponse = "status=Ok&instructions=Dial+*151*2*4%23+and+enter+your+EcoCash+PIN.+Once+you+have+authorized+the+payment+via+your+handset%2c+please+click+Check+For+Payment+below+to+conclude+this+transaction&paynowreference=15655549&pollurl=https%3a%2f%2fwww.paynow.co.zw%2fInterface%2fCheckPayment%2f%3fguid%3dc9f51998-3bbd-48b4-9107-819794c75ef8&hash=6DB1869C5C3961ADDF616B0476660E6D76E9071855ACAFA974F751A67600E0AD7416040518825BB235FF4AF3B57FD50A80884FB438DE8A2A6FAC62C71BECB51F"++clientSpec :: Spec+clientSpec = do+ it "should wrap exceptions in a PaynowError" $ do+ let client = newPaynowClient defaultConfig { pncEndpoint = "not-a-valid-url" }+ payment <- newClassicCheckout client testRef testAmount testEmail Nothing+ result <- processTx client payment+ result `shouldSatisfy` isLeft++responseSpec :: Spec+responseSpec = do+ it "should parse a successful express checkout response" $ do+ case serverResponseToResult testExpressCheckoutResponse of+ Right result -> + result `shouldBe` InitExpressPaymentSuccess Okay "15655549" "Dial+*151*2*4#+and+enter+your+EcoCash+PIN.+Once+you+have+authorized+the+payment+via+your+handset,+please+click+Check+For+Payment+below+to+conclude+this+transaction" "https://www.paynow.co.zw/Interface/CheckPayment/?guid=c9f51998-3bbd-48b4-9107-819794c75ef8" "6DB1869C5C3961ADDF616B0476660E6D76E9071855ACAFA974F751A67600E0AD7416040518825BB235FF4AF3B57FD50A80884FB438DE8A2A6FAC62C71BECB51F"+ _ -> expectationFailure "Expected a successful poll response" ++ it "should parse a successful classic checkout response" $ do+ case serverResponseToResult testClassicCheckoutResponse of+ Right result -> + result `shouldBe` InitClassicCheckoutSuccess Okay "https://www.paynow.co.zw/Payment/ConfirmPayment/15655543/sibandatrevor@gmail.com//" "https://www.paynow.co.zw/Interface/CheckPayment/?guid=dece867e-5a40-4961-bf0e-5d691c2a97f8" "83B31644B176661DC643CEDD480ADF3C0DECC9E2A00B004B8DC37B6A88A2A246D1A3DE57D5BA41B61BBA2FE2580CEFDCDEE30698200B88CD85D62FE75E76CD4B"+ _ -> expectationFailure "Expected a successful poll response"++ it "should successfully parse a poll response" $ do+ case serverResponseToResult testPollResponse of+ Right result -> + result `shouldBe` PollPaymentSuccess Cancelled "15650881" "01" 99.00 "https://www.paynow.co.zw/Interface/CheckPayment/?guid=d80b830d-623b-4948-9ab1-aae4950929bd" "E4F6E55CF3408EBDF0F59951E9E7B74C17CD9798A8AE5DA7B5D68D04F014E40753C438AA55445EB42FD9E455EE7FFA7BF752AD6791DE878DAEF4B486A345EAC"+ _ -> expectationFailure "Expected a successful poll response"++ it "should fail gracefully on bad input" $ do+ serverResponseToResult "bad input" `shouldSatisfy` isLeft+ + +transactionSpec :: Spec+transactionSpec = do++ it "should override the default return url" $ do+ runWithDefaultClientConfig $ \client config -> do+ let txdata = TxData Nothing Nothing (Just "https://api.com/override_return_url")+ payment <- newClassicCheckout client testRef testAmount testEmail (Just txdata)+ (snd $ urlParamsWithHash payment config) `shouldBe` "resulturl=http%3A%2F%2Flocalhost%3A3000%2Fresult&returnurl=https%3A%2F%2Fapi.com%2Foverride_return_url&reference=TEST%20REF&amount=99.99&id=1201&authemail=customer%40gmail.com&status=Message&hash=1C236C65D6DA213FE2F1F0B6472C6B15787CE1F5D9001ED698FEBDCC0FDFDBDFAD2310BDCB6ED8245E65BCA48D17E48F11666172BC894422DC3146582EBED2DC"++ it "should override the default result url" $ do+ runWithDefaultClientConfig $ \client config -> do+ let txdata = TxData Nothing (Just "https://api.com/override_result_url") Nothing+ payment <- newClassicCheckout client testRef testAmount testEmail (Just txdata)+ (snd $ urlParamsWithHash payment config) `shouldBe` "resulturl=https%3A%2F%2Fapi.com%2Foverride_result_url&returnurl=http%3A%2F%2Flocalhost%3A3000%2Freturn&reference=TEST%20REF&amount=99.99&id=1201&authemail=customer%40gmail.com&status=Message&hash=0AA2A87F4B0564341FCB47E9C3A0CCCECFE1E0F2A8DB6029081AB8D350CB189A631589279613B1A71EFA0511189C756368537EE717EC982BE8A1449AF44BC1D5"++ it "should include the additional info" $ do+ runWithDefaultClientConfig $ \client config -> do+ let txdata = TxData (Just "additional info to pass") Nothing Nothing+ payment <- newClassicCheckout client testRef testAmount testEmail (Just txdata)+ (snd $ urlParamsWithHash payment config) `shouldBe` "resulturl=http%3A%2F%2Flocalhost%3A3000%2Fresult&returnurl=http%3A%2F%2Flocalhost%3A3000%2Freturn&reference=TEST%20REF&amount=99.99&id=1201&additionalinfo=additional%20info%20to%20pass&authemail=customer%40gmail.com&status=Message&hash=2EE631E7F58FDCF10C3FC2602604DECE1E443A4F3A2D1FE05B9672C1A384FDA369FC4E529BDEDA30D651F974FFAFE9CDF494D2F176519F567E9675FE3804AEC1"+ +++hashingSpec :: Spec+hashingSpec = do+ -- See https://developers.paynow.co.zw/docs/generating_hash.html + it "should generate sha512 hash correctly" $ do+ let payload = "1201TEST REF99.99A test ticket transactionhttp://www.google.com/search?q=returnurlhttp://www.google.com/search?q=resulturlMessage3e9fed89-60e1-4ce5-ab6e-6b1eb2d4f977"+ expectedHash = "2A033FC38798D913D42ECB786B9B19645ADEDBDE788862032F1BD82CF3B92DEF84F316385D5B40DBB35F1A4FD7D5BFE73835174136463CDD48C9366B0749C689"+ sha1Hex payload `shouldBe` expectedHash+++ it "should generate a hash for a ClassicCheckout transaction" $ do+ runWithDefaultClient $ \client -> do+ payment <- newClassicCheckout client testRef testAmount testEmail Nothing+ hash <- txHash client payment+ hash `shouldBe` "79176952B0F3A8EA27B5FF22E1568B396A37017E65EFD1130A85175AB163C2894E5D97BC154184CE20CEC9C7B01456247FA7D942B583412DD42634579ABD9C33"++ it "should generate a hash for a Ecocash/ExpressCheckout transaction" $ do+ runWithDefaultClient $ \client -> do+ payment <- newExpressCheckout client (Ecocash "0783102754") testRef testAmount testEmail Nothing+ hash <- txHash client payment+ hash `shouldBe` "7FCBF1103BA1B6E6EA026DCE955200CEA23F69E42A023050457ED3E0B4489459FB4313ADD1446047F1AA9F3E0CE8E5FADCC3C4DAB386636A09DB560BD01506A3"++ it "should generate a hash for a OneMoney/ExpressCheckout transaction" $ do+ runWithDefaultClient $ \client -> do+ payment <- newExpressCheckout client (OneMoney "0783102754") testRef testAmount testEmail Nothing+ hash <- txHash client payment+ hash `shouldBe` "7E1770DC2A5BA3A9498B995F3096F9DCB83F6992816B7B1E418C0B1888ADF30252BF2175240209C53025E7025033202C6C07395707EF39C8B3349E136DE3CAEB"+ + it "should generate a hash for a VMC/ExpressCheckout transaction" $ do+ runWithDefaultClient $ \client -> do+ payment <- newExpressCheckout client (VisaMastercard testVMCDetails) testRef testAmount testEmail Nothing+ hash <- txHash client payment+ hash `shouldBe` "79176952B0F3A8EA27B5FF22E1568B396A37017E65EFD1130A85175AB163C2894E5D97BC154184CE20CEC9C7B01456247FA7D942B583412DD42634579ABD9C33"