packages feed

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 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"