mangopay (empty) → 1.0
raw patch · 28 files changed
+3413/−0 lines, 28 filesdep +HTFdep +HUnitdep +aesonsetup-changed
Dependencies added: HTF, HUnit, aeson, attoparsec, attoparsec-conduit, base, base16-bytestring, base64-bytestring, blaze-builder, bytestring, case-insensitive, conduit, data-default, http-conduit, http-types, lifted-base, mangopay, monad-control, resourcet, text, time, transformers, transformers-base, unordered-containers, utf8-string, wai, warp
Files
- LICENSE +27/−0
- Setup.hs +2/−0
- exe/Main.hs +28/−0
- mangopay.cabal +146/−0
- src/Web/MangoPay.hs +148/−0
- src/Web/MangoPay/Access.hs +35/−0
- src/Web/MangoPay/Accounts.hs +148/−0
- src/Web/MangoPay/Cards.hs +203/−0
- src/Web/MangoPay/Documents.hs +118/−0
- src/Web/MangoPay/Events.hs +213/−0
- src/Web/MangoPay/Monad.hs +349/−0
- src/Web/MangoPay/Payins.hs +200/−0
- src/Web/MangoPay/Payouts.hs +92/−0
- src/Web/MangoPay/Refunds.hs +97/−0
- src/Web/MangoPay/Types.hs +191/−0
- src/Web/MangoPay/Users.hs +274/−0
- src/Web/MangoPay/Wallets.hs +267/−0
- test/Web/MangoPay/AccessTest.hs +47/−0
- test/Web/MangoPay/AccountsTest.hs +29/−0
- test/Web/MangoPay/CardsTest.hs +50/−0
- test/Web/MangoPay/DocumentsTest.hs +34/−0
- test/Web/MangoPay/PayinsTest.hs +80/−0
- test/Web/MangoPay/PayoutsTest.hs +61/−0
- test/Web/MangoPay/RefundsTest.hs +121/−0
- test/Web/MangoPay/TestUtils.hs +234/−0
- test/Web/MangoPay/UsersTest.hs +67/−0
- test/Web/MangoPay/WalletsTest.hs +103/−0
- test/mangopay-tests.hs +49/−0
+ LICENSE view
@@ -0,0 +1,27 @@+Copyright (c) 2014, Prowdsponsor+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 the Prowdsponsor nor the names of its+ 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 HOLDER 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.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ exe/Main.hs view
@@ -0,0 +1,28 @@+{-# LANGUAGE OverloadedStrings #-}+module Main where++import Web.MangoPay++import Data.Aeson+import Data.Text+import Network.HTTP.Conduit+ +import System.Environment (getArgs)+import qualified Data.ByteString.Lazy as BS+import Data.Maybe (fromJust)++-- | get a new secret from the given client id and name+-- shows the secret on standard out+-- write the credentials in a JSON file called client.conf in current directory+main :: IO()+main = do+ args<-getArgs+ case args of+ [cid,name,email]->do+ let cred=Credentials (pack cid) (pack name) (pack email) Nothing+ cred2<-withManager (\mgr->+ runMangoPayT cred mgr Sandbox createCredentialsSecret)+ putStrLn $ unpack $ fromJust $ cClientSecret cred2+ BS.writeFile "client.conf" $ encode cred2+ return ()+ _ -> putStrLn "Usage: mangopay-passphrase clientid name email"
+ mangopay.cabal view
@@ -0,0 +1,146 @@+name: mangopay+version: 1.0+cabal-version: >= 1.8+build-type: Simple+author: JP Moresmau <jpmoresmau@gmail.com>+license: BSD3+license-file: LICENSE+copyright: (c) 2014 Prowdsponsor+category: Web+homepage: https://github.com/prowdsponsor/mangopay+maintainer: Prowdsponsor <opensource@prowdsponsor.com>+synopsis: Bindings to the MangoPay API++description:+ This package provides Haskell bindings to the+ <http://www.mangopay.com/ MangoPay> payment provider.+ .+ See also the @yesod-mangopay@ package.++source-repository head+ type: git+ location: git://github.com/prowdsponsor/mangopay.git++flag debug+ default: False+ description: Print debugging info.++library+ hs-source-dirs: src+ build-depends:+ base >= 4 && < 5+ , bytestring >= 0.9 && < 0.11+ , text == 0.11.*+ , transformers >= 0.2 && < 0.4+ , transformers-base+ , monad-control+ , resourcet+ , conduit == 1.0.*+ , http-types+ , http-conduit == 2.0.*+ , attoparsec >= 0.10 && < 0.12+ , attoparsec-conduit == 1.0.*+ , aeson >= 0.5 && < 0.8+ , time+ , data-default+ , lifted-base+ , unordered-containers+ , base16-bytestring+ , utf8-string >=0.3.7 && <0.4+ , base64-bytestring >=1.0.0 && <1.1+ , case-insensitive >=1.1.0 && <1.2+ ghc-options: -Wall+ other-modules:+ Web.MangoPay.Access,+ Web.MangoPay.Events,+ Web.MangoPay.Monad,+ Web.MangoPay.Types,+ Web.MangoPay.Users,+ Web.MangoPay.Wallets,+ Web.MangoPay.Documents,+ Web.MangoPay.Payins,+ Web.MangoPay.Cards,+ Web.MangoPay.Refunds,+ Web.MangoPay.Payouts,+ Web.MangoPay.Accounts++ if flag(debug)+ cpp-options: -DDEBUG+ exposed-modules: Web.MangoPay++executable mangopay-passphrase+ build-depends:+ base >= 4+ , mangopay+ , bytestring >= 0.9 && < 0.11+ , text == 0.11.*+ , transformers >= 0.2 && < 0.4+ , transformers-base+ , monad-control+ , resourcet+ , conduit == 1.0.*+ , http-types+ , http-conduit == 2.0.*+ , attoparsec >= 0.10 && < 0.12+ , attoparsec-conduit == 1.0.*+ , aeson >= 0.5 && < 0.8+ , time+ , data-default+ , lifted-base+ , unordered-containers+ , base16-bytestring+ , utf8-string >=0.3.7 && <0.4+ , case-insensitive >=1.1.0 && <1.2+ ghc-options: -Wall -rtsopts+ hs-source-dirs: exe+ main-is: Main.hs+ if flag(debug)+ cpp-options: -DDEBUG++test-suite mangopay-tests+ type: exitcode-stdio-1.0+ main-is: mangopay-tests.hs+ ghc-options: -Wall -rtsopts -threaded+ build-depends:+ base >= 4, HTF > 0.9+ , mangopay+ , bytestring >= 0.9 && < 0.11+ , text == 0.11.*+ , transformers >= 0.2 && < 0.4+ , transformers-base+ , monad-control+ , resourcet+ , conduit == 1.0.*+ , http-types+ , http-conduit == 2.0.*+ , attoparsec >= 0.10 && < 0.12+ , attoparsec-conduit == 1.0.*+ , aeson >= 0.5 && < 0.8+ , time+ , data-default+ , lifted-base+ , unordered-containers+ , base16-bytestring+ , HUnit >=1.2.5 && <1.3+ , utf8-string >=0.3.7 && <0.4+ , blaze-builder >=0.3.1 && <0.4+ , warp+ , wai+ , base64-bytestring >=1.0.0 && <1.1+ , case-insensitive >=1.1.0 && <1.2+ other-modules:+ Web.MangoPay.UsersTest,+ Web.MangoPay.TestUtils,+ Web.MangoPay.WalletsTest,+ Web.MangoPay.AccessTest,+ Web.MangoPay.DocumentsTest,+ Web.MangoPay.PayinsTest,+ Web.MangoPay.CardsTest,+ Web.MangoPay.RefundsTest,+ Web.MangoPay.AccountsTest,+ Web.MangoPay.PayoutsTest+ hs-source-dirs:+ test,+ src+ if flag(debug)+ cpp-options: -DDEBUG
+ src/Web/MangoPay.hs view
@@ -0,0 +1,148 @@+-- | API entry point+module Web.MangoPay (+ -- generic functions+ MangoPayT+ ,runMangoPayT+ ,runResourceInMp+ ,MpException+ ,getAll+ + -- useful types+ ,Credentials(..)+ ,AccessPoint(..)+ ,AccessToken(..)+ ,OAuthToken(..)+ ,Pagination(..)+ ,PagedList(..)+ + -- access+ ,createCredentialsSecret+ ,oauthLogin+ ,toAccessToken+ + -- Users+ ,NaturalUser(..)+ ,IncomeRange(..)+ ,NaturalUserID+ ,LegalUser(..)+ ,LegalUserType(..)+ ,LegalUserID+ ,UserRef(..)+ ,PersonType(..)+ ,AnyUserID+ ,storeNaturalUser+ ,fetchNaturalUser+ ,storeLegalUser+ ,fetchLegalUser+ ,getUser+ ,listUsers+ + -- Wallets+ ,Wallet(..)+ ,Amount(..)+ ,WalletID+ ,Currency+ ,storeWallet+ ,fetchWallet+ ,listWallets+ ,Transfer(..)+ ,TransferID+ ,TransferStatus(..)+ ,Transaction(..)+ ,TransactionID+ ,TransactionType(..)+ ,TransactionNature(..)+ ,createTransfer+ ,fetchTransfer+ ,listTransactions+ ,listTransactionsForUser+ + -- Events and Hooks+ ,Event(..)+ ,EventType(..)+ ,EventSearchParams(..)+ ,searchEvents+ ,HookStatus(..)+ ,HookValidity(..)+ ,HookID+ ,Hook(..)+ ,storeHook+ ,fetchHook+ ,listHooks+ ,eventFromQueryString+ ,eventFromQueryStringT+ + -- Documents and pages+ ,Document(..)+ ,DocumentID+ ,DocumentType(..)+ ,DocumentStatus(..)+ ,storeDocument+ ,fetchDocument+ ,storePage+ + -- Accounts+ ,BankAccount(..)+ ,BankAccountID+ ,BankAccountDetails(..)+ ,PaymentType(..)+ ,storeAccount+ ,fetchAccount+ ,listAccounts+ + -- Payins+ ,PaymentExecution(..)+ ,BankWireID+ ,BankWire(..)+ ,storeBankWire+ ,fetchBankWire+ ,mkBankWire+ ,CardPayinID+ ,CardPayin(..)+ ,storeCardPayin+ ,fetchCardPayin+ ,mkCardPayin+ + -- Payouts+ ,PayoutID+ ,Payout(..)+ ,mkPayout+ ,storePayout+ ,fetchPayout+ + -- Cards+ ,CardRegistration(..)+ ,CardRegistrationID+ ,CardID+ ,CardInfo(..)+ ,Card(..)+ ,CardValidity(..)+ ,mkCardRegistration+ ,storeCardRegistration+ ,registerCard+ ,fullRegistration+ ,fetchCard+ ,listCards+ + -- Refunds+ ,RefundID+ ,Refund(..)+ ,RefundRequest(..)+ ,refundTransfer+ ,refundPayin+ ,fetchRefund+)+where++import Web.MangoPay.Access+import Web.MangoPay.Accounts+import Web.MangoPay.Cards+import Web.MangoPay.Documents+import Web.MangoPay.Events+import Web.MangoPay.Monad+import Web.MangoPay.Payins+import Web.MangoPay.Payouts+import Web.MangoPay.Refunds+import Web.MangoPay.Users+import Web.MangoPay.Types+import Web.MangoPay.Wallets
+ src/Web/MangoPay/Access.hs view
@@ -0,0 +1,35 @@+{-# LANGUAGE FlexibleContexts, OverloadedStrings #-}+-- | access methods for login, creating clients...+module Web.MangoPay.Access +(+createCredentialsSecret+,oauthLogin+)+where++import Web.MangoPay.Monad+import Web.MangoPay.Types++import Data.Conduit+import Data.Text++import Network.HTTP.Conduit (applyBasicAuth)+import Control.Monad (liftM)+import qualified Network.HTTP.Types as HT+import qualified Data.Text.Encoding as TE+import Data.Maybe (isNothing)++-- | populate the passphrase for our clientId IFF we don't have one+createCredentialsSecret :: (MonadBaseControl IO m, MonadResource m) => MangoPayT m Credentials+createCredentialsSecret =do+ creds<- getCreds + if isNothing $ cClientSecret creds + then postExchange "/v2/clients" Nothing creds + else return creds++-- | login with given user name and password+-- returns the OAuth token that can be used to generate the opaque AccessToken and carries the expiration delay+oauthLogin :: (MonadBaseControl IO m, MonadResource m) => Text -> Text -> MangoPayT m OAuthToken+oauthLogin user pass = do+ req<- liftM (applyBasicAuth (TE.encodeUtf8 user) (TE.encodeUtf8 pass)) $ getPostRequest "/v2/oauth/token" Nothing ([("grant_type",Just "client_credentials")]::HT.Query)+ getJSONResponse req
+ src/Web/MangoPay/Accounts.hs view
@@ -0,0 +1,148 @@+{-# LANGUAGE DeriveDataTypeable, ScopedTypeVariables, OverloadedStrings, FlexibleContexts, FlexibleInstances, PatternGuards #-}+-- | handle bank accounts +module Web.MangoPay.Accounts where++import Web.MangoPay.Monad+import Web.MangoPay.Types+import Web.MangoPay.Users++import Data.Conduit+import Data.Text+import Data.Typeable (Typeable)+import Data.Aeson+import Data.Aeson.Types+import Data.Time.Clock.POSIX (POSIXTime)+import Control.Applicative+import qualified Network.HTTP.Types as HT++-- | create an account+storeAccount :: (MonadBaseControl IO m, MonadResource m) => BankAccount -> AccessToken -> MangoPayT m BankAccount+storeAccount ba at+ | Just uid<-baUserId ba= do+ url<-getClientURLMultiple ["/users/",uid,"/bankaccounts/",typeName $ baDetails ba]+ postExchange url (Just at) ba+ | otherwise=error "no user provided for account" ++-- | fetch an account from its ID+fetchAccount :: (MonadBaseControl IO m, MonadResource m) => AnyUserID -> BankAccountID -> AccessToken -> MangoPayT m BankAccount+fetchAccount uid aid at=do+ url<-getClientURLMultiple ["/users/",uid,"/bankaccounts/",aid]+ req<-getGetRequest url (Just at) ([]::HT.Query)+ getJSONResponse req ++-- | list all accounts for a given user +listAccounts :: (MonadBaseControl IO m, MonadResource m) => AnyUserID -> Maybe Pagination -> AccessToken -> MangoPayT m (PagedList BankAccount)+listAccounts uid mp at=do+ url<-getClientURLMultiple ["/users/",uid,"/bankaccounts/"]+ req<-getGetRequest url (Just at) (paginationAttributes mp)+ getJSONList req ++-- | account details, depending on the type+data BankAccountDetails=IBAN {+ atIBAN :: Text+ ,atBIC :: Text+ } | GB {+ atAccountNumber :: Text+ ,atSortCode :: Text+ } | US {+ atAccountNumber :: Text+ ,atABA :: Text+ } | CA {+ atAccountNumber :: Text+ ,atBankName :: Text+ ,atInstitutionNumber :: Text+ ,atBranchCode :: Text+ } | Other {+ atAccountNumber :: Text+ ,atBIC :: Text+ ,atCountry :: Text+ } deriving (Show,Read,Eq,Ord,Typeable)+ +-- | from json as per MangoPay format +instance FromJSON BankAccountDetails where+ parseJSON (Object v) =do + typ<-v .: "Type"+ case typ of+ "IBAN"->IBAN <$>+ v .: "IBAN" <*>+ v .: "BIC" + "GB"->GB <$>+ v .: "AccountNumber" <*>+ v .: "SortCode" + "US"->US <$>+ v .: "AccountNumber" <*>+ v .: "ABA"+ "CA"->CA <$>+ v .: "AccountNumber" <*>+ v .: "BankName" <*>+ v .: "InstitutionNumber" <*>+ v .: "BranchCode"+ "OTHER"->Other <$>+ v .: "AccountNumber" <*>+ v .: "BIC" <*>+ v .: "Country" + _->fail $ "BankAccountDetails: unknown type:" ++ typ+ parseJSON _=fail "BankAccountDetails" + +-- | type name for details+typeName :: BankAccountDetails -> Text+typeName (IBAN {})="IBAN"+typeName (GB {})="GB"+typeName (US {})="US"+typeName (CA {})="CA"+typeName (Other {})="OTHER"+ +-- | the details attribute to be added to the account object +toJSONPairs :: BankAccountDetails -> [Pair]+toJSONPairs (IBAN iban bic)=["IBAN" .= iban,"BIC" .= bic]+toJSONPairs (GB nb sc)=["AccountNumber" .= nb,"SortCode" .= sc] +toJSONPairs (US nb aba)=["AccountNumber" .= nb,"ABA" .= aba]+toJSONPairs (CA nb bn inb bc)=["AccountNumber" .= nb,"BankName" .= bn,"InstitutionNumber" .= inb, "BranchCode" .= bc]+toJSONPairs (Other nb bic c)=["AccountNumber" .= nb,"BIC" .= bic,"Country" .= c] + +-- | ID of a bank account +type BankAccountID = Text+ +-- | bank account details+data BankAccount = BankAccount {+ baId :: Maybe BankAccountID+ ,baCreationDate :: Maybe POSIXTime+ ,baUserId :: Maybe AnyUserID+ ,baTag :: Maybe Text+ ,baDetails :: BankAccountDetails+ ,baOwnerName :: Text+ ,baOwnerAddress :: Maybe Text+} deriving (Show,Eq,Ord,Typeable)++-- | to json as per MangoPay format +instance ToJSON BankAccount where+ toJSON ba=object $ ["OwnerName" .= baOwnerName ba,"Type" .= typeName (baDetails ba)+ ,"OwnerAddress" .= baOwnerAddress ba, "UserId" .= baUserId ba, "Tag" .= baTag ba]+ ++ toJSONPairs (baDetails ba)++-- | from json as per MangoPay format +instance FromJSON BankAccount where+ parseJSON o@(Object v) = + BankAccount <$>+ v .:? "Id" <*>+ v .:? "CreationDate" <*>+ v .:? "UserId" <*>+ v .:? "Tag" <*>+ parseJSON o <*>+ v .: "OwnerName" <*>+ v .:? "OwnerAddress"+ parseJSON _=fail "BankAccount"++-- | type of payment+data PaymentType = CARD | BANK_WIRE | AUTOMATIC_DEBIT | DIRECT_DEBIT+ deriving (Show,Read,Eq,Ord,Bounded,Enum,Typeable)++-- | to json as per MangoPay format+instance ToJSON PaymentType where+ toJSON =toJSON . show++-- | from json as per MangoPay format+instance FromJSON PaymentType where+ parseJSON (String s)+ | ((a,_):_)<-reads $ unpack s=pure a+ parseJSON _ =fail "PaymentType"
+ src/Web/MangoPay/Cards.hs view
@@ -0,0 +1,203 @@+{-# LANGUAGE DeriveDataTypeable, ScopedTypeVariables, OverloadedStrings, FlexibleContexts, FlexibleInstances, PatternGuards #-}+-- | handle cards+module Web.MangoPay.Cards where++import Web.MangoPay.Documents+import Web.MangoPay.Monad+import Web.MangoPay.Types+import Web.MangoPay.Users++import Data.Conduit+import Data.Text+import Data.Typeable (Typeable)+import Data.Aeson+import Data.Time.Clock.POSIX (POSIXTime, getPOSIXTime)+import Control.Applicative+import qualified Network.HTTP.Types as HT++import qualified Network.HTTP.Conduit as H+import Control.Monad.IO.Class (liftIO)+import qualified Data.Conduit.List as EL (consume)+import qualified Data.Text.Encoding as TE+import qualified Data.ByteString as BS++import qualified Data.HashMap.Lazy as HM+import Control.Exception.Base (throw)++-- | card registration ID+type CardRegistrationID=Text++-- | perform the full registration of a card+fullRegistration :: (MonadBaseControl IO m, MonadResource m) => AnyUserID -> Currency -> CardInfo -> AccessToken -> MangoPayT m CardRegistration+fullRegistration uid currency cardInfo at=do+ -- create registration+ let cr1=mkCardRegistration uid currency+ cr2<-storeCardRegistration cr1 at+ -- register it+ cr3<-registerCard cardInfo cr2+ -- save registered version+ storeCardRegistration cr3 at+ ++-- | create or edit a card registration+storeCardRegistration :: (MonadBaseControl IO m, MonadResource m) => CardRegistration -> AccessToken -> MangoPayT m CardRegistration+storeCardRegistration cr at= + case crId cr of+ Nothing-> do+ url<-getClientURL "/cardregistrations"+ postExchange url (Just at) cr+ Just i-> do+ url<-getClientURLMultiple ["/cardregistrations/",i]+ let Object m=toJSON cr+ putExchange url (Just at) $ Object $ HM.filterWithKey (\k _->k=="RegistrationData") m++-- | credit card information+data CardInfo = CardInfo {+ ciNumber :: Text+ ,ciExpire :: Text+ ,ciCSC :: Text+ } deriving (Show,Read,Eq,Ord,Typeable)++-- | register a card with the registration URL+registerCard :: (MonadBaseControl IO m, MonadResource m) => CardInfo -> CardRegistration -> MangoPayT m CardRegistration+registerCard ci cr |+ Just url <- crCardRegistrationURL cr,+ Just pre <- crPreregistrationData cr,+ Just ak <- crAccessKey cr=do+ req <-liftIO $ H.parseUrl $ unpack url + mgr<-getManager+ let b=HT.renderQuery False $ HT.toQuery [+ "accessKeyRef" ?+ ak+ ,"data" ?+ pre+ ,"cardNumber" ?+ ciNumber ci+ ,"cardExpirationDate" ?+ ciExpire ci+ ,"cardCvx" ?+ ciCSC ci]+ let req'=req {H.method=HT.methodPost+ , H.requestHeaders=[("content-type","application/x-www-form-urlencoded")]+ , H.requestBody=H.RequestBodyBS b} + res<- H.http req' mgr+ reg <- H.responseBody res $$+- EL.consume+ let t=TE.decodeUtf8 $ BS.concat reg+ if "data=" `isPrefixOf` t + then return cr{crRegistrationData=Just t}+ else do+ pt<-liftIO getPOSIXTime+ throw $ MpAppException $ MpError "" "RegistrationError" t $ Just pt +registerCard _ _=do+ pt<-liftIO getPOSIXTime+ throw $ MpAppException $ MpError "" "IllegalState" "CardRegistration not ready" $ Just pt + +-- | helper function to create a new card registration+mkCardRegistration :: AnyUserID -> Currency -> CardRegistration+mkCardRegistration uid currency=CardRegistration Nothing Nothing Nothing uid currency Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing++-- | a card registration+data CardRegistration = CardRegistration {+ crId :: Maybe CardRegistrationID -- ^ The Id of the object+ ,crCreationDate :: Maybe POSIXTime -- ^ The creation date of the object+ ,crTag :: Maybe Text -- ^ Custom data+ ,crUserId :: AnyUserID -- ^ The ID of the author+ ,crCurrency :: Currency -- ^ The currency of the card registrated+ ,crAccessKey :: Maybe Text -- ^ This key has to be sent with the card details and the PreregistrationData+ ,crPreregistrationData :: Maybe Text -- ^ This passphrase has to be sent with the card details and the AccessKey+ ,crCardRegistrationURL :: Maybe Text -- ^ The URL where to POST the card details, the AccessKey and PreregistrationData+ ,crRegistrationData :: Maybe Text -- ^ You get the CardRegistrationData once you posted the card details, the AccessKey and PreregistrationData+ ,crCardType :: Maybe Text -- ^ « CB_VISA_MASTERCARD » is the only value available yet+ ,crCardId :: Maybe CardID -- ^ You get the CardId (to process payments) once you edited the CardRegistration Object with the RegistrationData+ ,crResultCode :: Maybe Text -- ^ The result code of the object+ ,crResultMessage :: Maybe Text -- ^ The message explaining the result code+ ,crStatus :: Maybe DocumentStatus -- ^ The status of the object.+} deriving (Show,Eq,Ord,Typeable)+++-- | to json as per MangoPay format +instance ToJSON CardRegistration where+ toJSON cr=object ["Tag" .= crTag cr,"UserId" .= crUserId cr+ ,"Currency" .= crCurrency cr,"RegistrationData" .= crRegistrationData cr+ ,"CardRegistrationURL" .= crCardRegistrationURL cr]++-- | from json as per MangoPay format +instance FromJSON CardRegistration where+ parseJSON (Object v) =CardRegistration <$>+ v .: "Id" <*>+ v .: "CreationDate" <*>+ v .:? "Tag" <*>+ v .: "UserId" <*>+ v .: "Currency" <*>+ v .:? "AccessKey" <*>+ v .:? "PreregistrationData" <*>+ v .:? "CardRegistrationURL" <*>+ v .:? "RegistrationData" <*>+ v .:? "CardType" <*>+ v .:? "CardId" <*>+ v .:? "ResultCode" <*>+ v .:? "ResultMessage" <*>+ v .:? "Status" + parseJSON _=fail "CardRegistration" ++-- | fetch a card from its ID+fetchCard :: (MonadBaseControl IO m, MonadResource m) => CardID -> AccessToken -> MangoPayT m Card+fetchCard cid at=do+ url<-getClientURLMultiple ["/cards/",cid]+ req<-getGetRequest url (Just at) ([]::HT.Query)+ getJSONResponse req ++-- | list all cards for a given user +listCards :: (MonadBaseControl IO m, MonadResource m) => AnyUserID -> Maybe Pagination -> AccessToken -> MangoPayT m (PagedList Card)+listCards uid mp at=do+ url<-getClientURLMultiple ["/users/",uid,"/cards"]+ req<-getGetRequest url (Just at) (paginationAttributes mp)+ getJSONList req ++-- | validity of a card+data CardValidity=UNKNOWN | VALID | INVALID+ deriving (Show,Read,Eq,Ord,Bounded,Enum,Typeable)+++-- | to json as per MangoPay format+instance ToJSON CardValidity where+ toJSON =toJSON . show++-- | from json as per MangoPay format+instance FromJSON CardValidity where+ parseJSON (String s)=pure $ read $ unpack s+ parseJSON _ =fail "CardValidity"+++-- | a registered card+data Card=Card {+ cId :: CardID+ ,cCreationDate :: POSIXTime + ,cTag :: Maybe Text + ,cExpirationDate :: Text -- ^ MMYY+ ,cAlias :: Text -- ^ Example: 497010XXXXXX4414+ ,cCardProvider :: Text -- ^ The card provider, it could be « CB », « VISA », « MASTERCARD », etc.+ ,cCardType :: Text -- ^ « CB_VISA_MASTERCARD » is the only value available yet+ ,cProduct :: Maybe Text + ,cBankCode :: Maybe Text + ,cActive :: Bool+ ,cCurrency :: Currency+ ,cValidity :: CardValidity -- ^ Once we proceed (or attempted to process) a payment with the card we are able to indicate if it is « valid » or « invalid ». If we didn’t process a payment yet the « Validity » stay at « unknown ».+ ,cCountry :: Text+ ,cUserId :: AnyUserID+ } deriving (Show,Eq,Ord,Typeable)+ +-- | from json as per MangoPay format +instance FromJSON Card where+ parseJSON (Object v) =Card <$>+ v .: "Id" <*>+ v .: "CreationDate" <*>+ v .:? "Tag" <*>+ v .: "ExpirationDate" <*>+ v .: "Alias" <*>+ v .: "CardProvider" <*>+ v .: "CardType" <*>+ v .:? "Product" <*>+ v .:? "BankCode" <*>+ v .: "Active" <*>+ v .: "Currency" <*>+ v .: "Validity" <*>+ v .: "Country" <*>+ v .: "UserId" + parseJSON _=fail "Card" +
+ src/Web/MangoPay/Documents.hs view
@@ -0,0 +1,118 @@+{-# LANGUAGE DeriveDataTypeable, ScopedTypeVariables, OverloadedStrings, FlexibleContexts, FlexibleInstances #-}+-- | handle documents and pages+module Web.MangoPay.Documents where+++import Web.MangoPay.Monad+import Web.MangoPay.Types+import Web.MangoPay.Users++import Data.Conduit+import Data.Text+import Data.Typeable (Typeable)+import Data.Aeson+import Data.Time.Clock.POSIX (POSIXTime)+import Control.Applicative+import qualified Network.HTTP.Types as HT++import qualified Data.ByteString as BS+import qualified Data.ByteString.Base64 as BS++import qualified Data.Text.Encoding as TE++-- | create or edit a document+storeDocument :: (MonadBaseControl IO m, MonadResource m) => AnyUserID -> Document -> AccessToken -> MangoPayT m Document+storeDocument uid d at= + case dId d of+ Nothing-> do+ url<-getClientURLMultiple ["/users/",uid,"/KYC/documents/"]+ postExchange url (Just at) d+ Just i-> do+ url<-getClientURLMultiple ["/users/",uid,"/KYC/documents/",i]+ putExchange url (Just at) d+ ++-- | fetch a document from its ID+fetchDocument :: (MonadBaseControl IO m, MonadResource m) => AnyUserID -> DocumentID -> AccessToken -> MangoPayT m Document+fetchDocument uid did at=do+ url<-getClientURLMultiple ["/users/",uid,"/KYC/documents/",did]+ req<-getGetRequest url (Just at) ([]::HT.Query)+ getJSONResponse req ++-- | create a page+-- note that per the MangoPay API the document HAS to be in CREATED status+-- should we check it here? Since MangoPay returns a 500 Internal Server Error if the document is in another status...+storePage :: (MonadBaseControl IO m, MonadResource m) => AnyUserID -> DocumentID -> BS.ByteString -> AccessToken -> MangoPayT m ()+storePage uid did contents at=do+ let val=object ["File" .= TE.decodeLatin1 (BS.encode contents)]+ url<-getClientURLMultiple ["/users/",uid,"/KYC/documents/",did,"/pages"]+ postNoReply url (Just at) val++-- | ID of a document+type DocumentID = Text++-- | type of the document+data DocumentType= IDENTITY_PROOF -- ^ For legal and natural users+ | REGISTRATION_PROOF -- ^ Only for legal users+ | ARTICLES_OF_ASSOCIATION -- ^ Only for legal users+ | SHAREHOLDER_DECLARATION -- ^ Only for legal users+ | ADDRESS_PROOF -- ^ For legal and natural users+ deriving (Show,Read,Eq,Ord,Bounded,Enum,Typeable)++-- | to json as per MangoPay format+instance ToJSON DocumentType where+ toJSON =toJSON . show++-- | from json as per MangoPay format+instance FromJSON DocumentType where+ parseJSON (String s)=pure $ read $ unpack s+ parseJSON _ =fail "DocumentType"++-- | status of a document+data DocumentStatus=CREATED+ | VALIDATION_ASKED+ | VALIDATED+ | REFUSED + deriving (Show,Read,Eq,Ord,Bounded,Enum,Typeable)++-- | to json as per MangoPay format+instance ToJSON DocumentStatus where+ toJSON =toJSON . show++-- | from json as per MangoPay format+instance FromJSON DocumentStatus where+ parseJSON (String s)=pure $ read $ unpack s+ parseJSON _ =fail "DocumentStatus"++-- | a document+data Document = Document {+ dId :: Maybe DocumentID+ ,dCreationDate :: Maybe POSIXTime+ ,dTag :: Maybe Text -- ^ custom data for client+ ,dType :: DocumentType+ ,dStatus :: Maybe DocumentStatus+ ,dRefusedReasonType :: Maybe Text+ ,dRefusedReasonMessage :: Maybe Text+ } deriving (Show,Ord,Eq,Typeable)+ + +-- | to json as per MangoPay format +instance ToJSON Document where+ toJSON d=object ["Tag" .= dTag d,+ "Type" .= dType d,"Status" .= dStatus d]++-- | from json as per MangoPay format +instance FromJSON Document where+ parseJSON (Object v) =Document <$>+ v .: "Id" <*>+ v .: "CreationDate" <*>+ v .:? "Tag" <*>+ v .: "Type" <*>+ v .: "Status" <*>+ v .:? "RefusedReasonType" <*>+ v .:? "RefusedReasonMessage"+ parseJSON _=fail "Document"+ + + +
+ src/Web/MangoPay/Events.hs view
@@ -0,0 +1,213 @@+{-# LANGUAGE DeriveDataTypeable, ScopedTypeVariables, OverloadedStrings, FlexibleContexts, FlexibleInstances #-}+-- | handle events+-- <http://docs.mangopay.com/api-references/events/>+module Web.MangoPay.Events where++import Web.MangoPay.Monad+import Web.MangoPay.Types++import Data.Conduit+import Data.Text hiding (filter)+import Data.Typeable (Typeable)+import Data.Aeson+import Data.Time.Clock.POSIX (POSIXTime)+import Data.Default+import Control.Applicative+import qualified Network.HTTP.Types as HT+import Data.Maybe (isJust)+import qualified Data.HashMap.Lazy as HM (delete)+import qualified Data.Text.Encoding as TE+import Control.Monad (join)+import qualified Data.ByteString.Char8 as BS++-- | create or edit a natural user+searchEvents :: (MonadBaseControl IO m, MonadResource m) => EventSearchParams -> AccessToken -> MangoPayT m [Event]+searchEvents esp at=do+ url<-getClientURL "/events"+ req<-getGetRequest url (Just at) esp+ getJSONResponse req++-- | create or edit a hook+storeHook :: (MonadBaseControl IO m, MonadResource m) => Hook -> AccessToken -> MangoPayT m Hook+storeHook h at= + case hId h of+ Nothing-> do+ url<-getClientURL "/hooks"+ postExchange url (Just at) h+ Just i-> do+ url<-getClientURLMultiple ["/hooks/",i]+ let Object m=toJSON h+ putExchange url (Just at) (Object $ HM.delete "EventType" m)+ +-- | fetch a wallet from its ID+fetchHook :: (MonadBaseControl IO m, MonadResource m) => HookID -> AccessToken -> MangoPayT m Hook+fetchHook wid at=do+ url<-getClientURLMultiple ["/hooks/",wid]+ req<-getGetRequest url (Just at) ([]::HT.Query)+ getJSONResponse req ++-- | list all wallets for a given user +listHooks :: (MonadBaseControl IO m, MonadResource m) => Maybe Pagination -> AccessToken -> MangoPayT m (PagedList Hook)+listHooks mp at=do+ url<-getClientURL "/hooks"+ req<-getGetRequest url (Just at) (paginationAttributes mp)+ getJSONList req ++-- | Event type+data EventType=PAYIN_NORMAL_CREATED+ | PAYIN_NORMAL_SUCCEEDED+ | PAYIN_NORMAL_FAILED+ | PAYOUT_NORMAL_CREATED+ | PAYOUT_NORMAL_SUCCEEDED+ | PAYOUT_NORMAL_FAILED+ | TRANSFER_NORMAL_CREATED+ | TRANSFER_NORMAL_SUCCEEDED+ | TRANSFER_NORMAL_FAILED+ | PAYIN_REFUND_CREATED+ | PAYIN_REFUND_SUCCEEDED+ | PAYIN_REFUND_FAILED+ | PAYOUT_REFUND_CREATED+ | PAYOUT_REFUND_SUCCEEDED+ | PAYOUT_REFUND_FAILED+ | TRANSFER_REFUND_CREATED+ | TRANSFER_REFUND_SUCCEEDED+ | TRANSFER_REFUND_FAILED+ deriving (Show,Read,Eq,Ord,Bounded,Enum,Typeable)++instance ToHtQuery (Maybe EventType) where+ n ?+ d=n ?+ fmap show d++-- | to json as per MangoPay format+instance ToJSON EventType where+ toJSON =toJSON . show++-- | from json as per MangoPay format+instance FromJSON EventType where+ parseJSON (String s)=pure $ read $ unpack s+ parseJSON _ =fail "EventType"++-- | search parameters for events +data EventSearchParams=EventSearchParams{+ espEventType :: Maybe EventType+ ,espBeforeDate :: Maybe POSIXTime+ ,espAfterDate :: Maybe POSIXTime+ ,espPagination :: Maybe Pagination+ }+ deriving (Show,Eq,Ord,Typeable)+ +instance Default EventSearchParams where+ def=EventSearchParams Nothing Nothing Nothing Nothing+ +instance HT.QueryLike EventSearchParams where+ toQuery (EventSearchParams et bd ad p)=filter (isJust .snd) + ["eventtype" ?+ et+ ,"beforeDate" ?+ bd+ ,"afterDate" ?+ ad+ ] ++ paginationAttributes p + +--instance ToJSON EventSearchParams where+-- toJSON esp=object $ ["eventtype" .= espEventType esp,+-- "beforeDate" .= espBeforeDate esp, "afterDate" .= espAfterDate esp]+-- ++ paginationAttributes (espPagination esp)++-- | a event+data Event=Event {+ eResourceId :: Text+ ,eEventType :: EventType+ ,eDate :: POSIXTime+ }+ deriving (Show,Eq,Ord,Typeable)+ +-- | to json as per MangoPay format +instance ToJSON Event where+ toJSON e=object ["ResourceId" .= eResourceId e,"EventType" .= eEventType e,"Date" .= eDate e]++-- | from json as per MangoPay format +instance FromJSON Event where+ parseJSON (Object v) =Event <$>+ v .: "ResourceId" <*>+ v .: "EventType" <*>+ v .: "Date"+ parseJSON _=fail "Event"++-- | parse an event from the query string+-- the MangoPay is not very clear on notifications, but see v1 <http://docs.mangopay.com/v1-api-references/notifications/>+-- v2 works the same, the event is passed via parameters of the query string +eventFromQueryString :: HT.Query -> Maybe Event+eventFromQueryString q=do+ rid<-fmap TE.decodeUtf8 $ join $ findAssoc q "RessourceId" -- yes, two ss here+ et<-join $ fmap (maybeRead . BS.unpack) $ join $ findAssoc q "EventType"+ d<-fmap fromIntegral $ join $ fmap ((maybeRead :: String -> Maybe Integer). BS.unpack) $ join $ findAssoc q "Date"+ return $ Event rid et d+ ++-- | parse an event from the query string represented as Text+-- the MangoPay is not very clear on notifications, but see v1 <http://docs.mangopay.com/v1-api-references/notifications/>+-- v2 works the same, the event is passed via parameters of the query string +eventFromQueryStringT :: [(Text, Text)] -> Maybe Event+eventFromQueryStringT q=do+ rid<- findAssoc q "RessourceId" -- yes, two ss here+ et<-join $ fmap (maybeRead . unpack) $ findAssoc q "EventType"+ d<-fmap fromIntegral $ join $ fmap ((maybeRead :: String -> Maybe Integer). unpack) $ findAssoc q "Date"+ return $ Event rid et d ++-- | status of notification hook +data HookStatus=Enabled | Disabled+ deriving (Show,Read,Eq,Ord,Bounded,Enum,Typeable)+ +-- | to json as per MangoPay format+instance ToJSON HookStatus where+ toJSON Enabled="ENABLED"+ toJSON Disabled="DISABLED"+ +-- | from json as per MangoPay format+instance FromJSON HookStatus where+ parseJSON (String "ENABLED") =pure Enabled + parseJSON (String "DISABLED") =pure Disabled + parseJSON _= fail "HookStatus" ++-- | validity of notification hook +data HookValidity=Valid | Invalid+ deriving (Show,Read,Eq,Ord,Bounded,Enum,Typeable)+ +-- | to json as per MangoPay format+instance ToJSON HookValidity where+ toJSON Valid="VALID"+ toJSON Invalid="INVALID"+ +-- | from json as per MangoPay format+instance FromJSON HookValidity where+ parseJSON (String "VALID") =pure Valid + parseJSON (String "INVALID") =pure Invalid + parseJSON _= fail "HookValidity" + +-- | id for hook +type HookID=Text ++-- | a notification hook +data Hook=Hook {+ hId :: Maybe HookID -- ^ The Id of the hook details+ ,hCreationDate :: Maybe POSIXTime+ ,hTag :: Maybe Text -- ^ Custom data+ ,hUrl :: Text -- ^This is the URL where you receive notification for each EventType+ ,hStatus :: HookStatus+ ,hValidity :: Maybe HookValidity+ ,hEventType :: EventType+ } + deriving (Show,Eq,Ord,Typeable)+ +-- | to json as per MangoPay format +instance ToJSON Hook where+ toJSON h=object ["Tag" .= hTag h,"EventType" .= hEventType h,"Url" .= hUrl h,"Status" .= hStatus h]++-- | from json as per MangoPay format +instance FromJSON Hook where+ parseJSON (Object v) =Hook <$>+ v .: "Id" <*>+ v .: "CreationDate" <*>+ v .: "Tag" <*>+ v .: "Url" <*>+ v .: "Status" <*>+ v .: "Validity" <*>+ v .: "EventType" + parseJSON _=fail "Hook"
+ src/Web/MangoPay/Monad.hs view
@@ -0,0 +1,349 @@+{-# LANGUAGE DeriveDataTypeable, OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables, GeneralizedNewtypeDeriving, FlexibleInstances,+ MultiParamTypeClasses, UndecidableInstances, TypeFamilies,+ FlexibleContexts, RankNTypes,CPP #-}+-- | the utility monad and related functions, taking care of the HTTP, JSON, etc.+module Web.MangoPay.Monad where++import Web.MangoPay.Types++import Control.Applicative +import Control.Monad (MonadPlus, liftM, void, join)+import Control.Monad.Base (MonadBase(..))+import Control.Monad.Fix (MonadFix)+import Control.Monad.IO.Class (MonadIO, liftIO)+import Control.Monad.Trans.Class (MonadTrans(lift))+import Control.Monad.Trans.Control ( MonadTransControl(..), MonadBaseControl(..)+ , ComposeSt, defaultLiftBaseWith+ , defaultRestoreM )+import Control.Monad.Trans.Reader (ReaderT(..), ask, mapReaderT)+import Data.Typeable (Typeable)+import Data.Default+import qualified Control.Monad.Trans.Resource as R+import qualified Control.Exception.Lifted as L++import qualified Data.Conduit as C+import qualified Network.HTTP.Conduit as H+import qualified Network.HTTP.Types as HT+import Data.ByteString (ByteString)+import qualified Data.ByteString as BS+import qualified Data.ByteString.Char8 as BSC+import qualified Data.ByteString.Lazy.Char8 as BSLC+import Data.Aeson (json,fromJSON,Result(..),FromJSON, ToJSON,encode)+import Data.Conduit.Attoparsec (sinkParser, ParseError)+import Control.Exception.Base (throw)+import qualified Data.Text.Encoding as TE+import qualified Data.Text as T (Text)+import Data.Maybe (fromMaybe)+import Data.CaseInsensitive (CI)++#if DEBUG+import Data.Conduit.Binary (sinkHandle)+import System.IO (stdout)+import Data.Conduit.Util (zipSinks)+#endif+++-- | the mangopay monad transformer+-- this encapsulates the data necessary to pass the app credentials, etc+newtype MangoPayT m a = Mp { unIs :: ReaderT MpData m a }+ deriving ( Functor, Applicative, Alternative, Monad+ , MonadFix, MonadPlus, MonadIO, MonadTrans+ , R.MonadThrow, R.MonadActive, R.MonadResource )+ +instance MonadBase b m => MonadBase b (MangoPayT m) where+ liftBase = lift . liftBase++instance MonadTransControl MangoPayT where+ newtype StT MangoPayT a = MpStT { unMpStT :: StT (ReaderT MpData) a }+ liftWith f = Mp $ liftWith (\run -> f (liftM MpStT . run . unIs))+ restoreT = Mp . restoreT . liftM unMpStT++instance MonadBaseControl b m => MonadBaseControl b (MangoPayT m) where+ newtype StM (MangoPayT m) a = StMT {unStMT :: ComposeSt MangoPayT m a}+ liftBaseWith = defaultLiftBaseWith StMT+ restoreM = defaultRestoreM unStMT+ +-- | Run a computation in the 'MangoPayT' monad transformer with+-- your credentials.+runMangoPayT :: Credentials -- ^ Your app's credentials.+ -> H.Manager -- ^ Connection manager (see 'H.withManager').+ -> AccessPoint+ -> MangoPayT m a -- ^ the action to run+ -> m a -- ^ the result+runMangoPayT creds manager ap (Mp act) =+ runReaderT act (MpData creds manager ap) + +-- | Get the user's credentials.+getCreds :: Monad m => MangoPayT m Credentials+getCreds = mpCreds `liftM` Mp ask++-- | Get the MangoPay host+getHost :: Monad m => MangoPayT m ByteString+getHost = (getAccessPointURL . mpAccessPoint) `liftM` Mp ask++-- | build a post request to MangoPay+getPostRequest :: (Monad m,MonadIO m,HT.QueryLike q) => ByteString -- ^ the url path+ -> Maybe AccessToken+ -> q -- ^ the query parameters+ -> MangoPayT m H.Request -- ^ the properly configured request+getPostRequest path mat query=do+ host<-getHost+ let b=HT.renderQuery False $ HT.toQuery query +#if DEBUG+ liftIO $ BSC.putStrLn path+ liftIO $ BSC.putStrLn b+#endif + return $ def {+ H.secure=True+ , H.host = host+ , H.port = 443+ , H.path = path+ , H.method=HT.methodPost+ , H.requestHeaders=[("content-type","application/x-www-form-urlencoded")] +++ case mat of + Just (AccessToken at)->[("Authorization",at)]+ _->[] + , H.requestBody=H.RequestBodyBS b+ }++-- | build a get request to MangoPay+getGetRequest :: (Monad m,MonadIO m,HT.QueryLike q) => ByteString -- ^ the url path+ -> Maybe AccessToken+ -> q -- ^ the query parameters+ -> MangoPayT m H.Request -- ^ the properly configured request+getGetRequest path mat query=do+ host<-getHost+ let qs=HT.renderQuery True $ HT.toQuery query+#if DEBUG+ liftIO $ BSC.putStrLn $ BS.append path qs+#endif + return $ def {+ H.secure=True+ , H.host = host+ , H.port = 443+ , H.path = path+ , H.method=HT.methodGet+ , H.queryString=qs+ , H.requestHeaders=getJSONHeaders mat+ }+ +-- | build a delete request to MangoPay+getDeleteRequest :: (Monad m,MonadIO m,HT.QueryLike q) => ByteString -- ^ the url path+ -> Maybe AccessToken+ -> q -- ^ the query parameters+ -> MangoPayT m H.Request -- ^ the properly configured request+getDeleteRequest path mat query=do+ get<-getGetRequest path mat query+ return $ get {H.method=HT.methodDelete}++-- | get the url to use for our clientId+getClientURL :: (Monad m)=> ByteString -- ^ the url path+ -> MangoPayT m ByteString -- ^ the URL+getClientURL path=do+ cid<- liftM clientIDBS getCreds+ return $ BS.concat ["/v2/",cid,path]++-- | get the url to use for our clientId+getClientURLMultiple :: (Monad m)=> [T.Text] -- ^ the url components+ -> MangoPayT m ByteString -- ^ the URL+getClientURLMultiple path=do+ cid<- liftM clientIDBS getCreds+ return $ BS.concat $ ["/v2/",cid] ++ map TE.encodeUtf8 path++-- | build a URL for a get operation with a single query+getQueryURL :: (Monad m,HT.QueryLike q) => ByteString -- ^ the url path+ -> q -- ^ the query parameters + -> MangoPayT m ByteString -- ^ the URL+getQueryURL path query=do+ host<-getHost+ return $ BS.concat ["https://",host,path,HT.renderQuery True $ HT.toQuery query]++-- | perform a HTTP request and deal with the JSON result+-- The logic for errors is as follows: we have several cases:+-- If the HTTP request return OK and we can parse the proper result, we return it. +-- If we can't parse it into the data type we expect, we throw a MpJSONException: the server returned ok but we can't parse the result. +-- If we get an HTTP error code, we try to parse the result and send the proper exception: we have encountered probably a normal error, when the user has filled in incorrect data, etc. +-- If we can't even parse the result as JSON or if we can't understand the JSON error message, we throw an MpHttpException.+mpReq :: forall b (m :: * -> *) wrappedErr c .+ (MonadBaseControl IO m, C.MonadResource m,FromJSON b,FromJSON wrappedErr) =>+ H.Request+ -> (wrappedErr -> MpError) -- ^ extract the error from the JSON+ -> (HT.ResponseHeaders -> b -> c)+ -> MangoPayT m c+mpReq req extractError addHeaders=do+ -- we check the status ourselves+ let req' = req { H.checkStatus = \_ _ _ -> Nothing }+ mgr<-getManager+ res<-H.http req' mgr+ let status = H.responseStatus res+ headers = H.responseHeaders res+ cookies = H.responseCookieJar res+ ok=isOkay status+ err=H.StatusCodeException status headers cookies+ L.catch (do +#if DEBUG+ (value,_)<-H.responseBody res C.$$+- zipSinks (sinkParser json) (sinkHandle stdout)+ liftIO $ BSC.putStrLn ""+ liftIO $ print headers+#else + value<-H.responseBody res C.$$+- sinkParser json+#endif+ if ok+ then + -- parse response as the expected value+ case fromJSON value of+ Success ot->return $ addHeaders headers ot+ Error jerr->throw $ MpJSONException jerr -- got an ok response we couldn't parse+ else+ -- parse response as an error+ case fromJSON value of+ Success ise-> throw $ MpAppException $ extractError ise+ _ -> throw $ MpHttpException err $ Just value -- we can't even parse the error, throw the HTTP error inside our error type, but keep the JSON in case a human can make sens of it+ ) (\(_::ParseError)->throw $ MpHttpException err Nothing) -- the error body wasn't even json, throw the HTTP error inside our error type + +-- | get a JSON response from a request to MangoPay+-- MangoPay returns either a result, or an error+getJSONResponse :: forall (m :: * -> *) v.+ (MonadBaseControl IO m, C.MonadResource m,FromJSON v) =>+ H.Request+ -> MangoPayT+ m v+getJSONResponse req=mpReq req id (const id)++-- | get a PagedList from the JSON items+getJSONList:: forall (m :: * -> *) v.+ (MonadBaseControl IO m, C.MonadResource m,FromJSON v) =>+ H.Request+ -> MangoPayT+ m (PagedList v)+getJSONList req=mpReq req id buildList++-- | build a PagedList from the headers information+buildList :: HT.ResponseHeaders -> [b] -> PagedList b+buildList headers items=let+ cnt=fromMaybe (fromIntegral $ length items) $ getI "X-Number-Of-Items" -- yes, doc is wrong (they forgot the s)+ pgs=fromMaybe 1 $ getI "X-Number-Of-Pages"+ in PagedList items cnt pgs+ where+ getI :: CI ByteString -> Maybe Integer+ getI =join . fmap ((maybeRead :: String -> Maybe Integer). BSC.unpack) . findAssoc headers++-- | get all items, hiding the pagination system +getAll :: (MonadBaseControl IO m, C.MonadResource m,FromJSON v) => + (Maybe Pagination -> AccessToken -> MangoPayT m (PagedList v)) -> AccessToken -> + MangoPayT m [v]+getAll f at=readAll 1 []+ where + readAll p accum=do+ retL<-f (Just $ Pagination p 100) at+ let dts=accum ++ plData retL+ if plPageCount retL > p + then readAll (p + 1) dts+ else return dts+ +-- | get the headers necessary for a JSON call +getJSONHeaders :: Maybe AccessToken -> HT.RequestHeaders+getJSONHeaders mat= ("content-type", "application/json") :+ case mat of+ Just (AccessToken at) -> [("Authorization", at)]+ _ -> [] +++-- | send JSON via post, get JSON back +postExchange :: forall (m :: * -> *) v p.+ (MonadBaseControl IO m, C.MonadResource m,FromJSON v,ToJSON p) =>+ ByteString+ -> Maybe AccessToken+ -> p+ -> MangoPayT+ m v +postExchange=jsonExchange HT.methodPost++-- | send JSON via post, get JSON back +putExchange :: forall (m :: * -> *) v p.+ (MonadBaseControl IO m, C.MonadResource m,FromJSON v,ToJSON p) =>+ ByteString+ -> Maybe AccessToken+ -> p+ -> MangoPayT+ m v +putExchange=jsonExchange HT.methodPut+ +-- | send JSON, get JSON back +jsonExchange :: forall (m :: * -> *) v p.+ (MonadBaseControl IO m, C.MonadResource m,FromJSON v,ToJSON p) =>+ HT.Method+ -> ByteString+ -> Maybe AccessToken+ -> p+ -> MangoPayT+ m v +jsonExchange meth path mat p= getJSONRequest meth path mat p >>= getJSONResponse+ +-- | get JSON request +getJSONRequest :: forall (m :: * -> *) p.+ (MonadBaseControl IO m, C.MonadResource m,ToJSON p) =>+ HT.Method+ -> ByteString+ -> Maybe AccessToken+ -> p+ -> MangoPayT m H.Request -- ^ the properly configured request +getJSONRequest meth path mat p= do+ host<-getHost+#if DEBUG+ liftIO $ BSC.putStrLn path+ liftIO $ BSLC.putStrLn $ encode p+#endif + return def {+ H.secure=True+ , H.host = host+ , H.port = 443+ , H.path = path+ , H.method=meth+ , H.requestHeaders=getJSONHeaders mat+ , H.requestBody=H.RequestBodyLBS $ encode p+ } + +-- | post JSON content and ignore the reply +postNoReply :: forall (m :: * -> *) p.+ (MonadBaseControl IO m, C.MonadResource m,ToJSON p) =>+ ByteString+ -> Maybe AccessToken+ -> p+ -> MangoPayT+ m () +postNoReply path mat p= do+ req<- getJSONRequest HT.methodPost path mat p+ mgr<-getManager+ void $ H.http req mgr + +-- | Get the 'H.Manager'.+getManager :: Monad m => MangoPayT m H.Manager+getManager = mpManager `liftM` Mp ask++-- | Run a 'ResourceT' inside a 'MangoPayT'.+runResourceInMp :: (C.MonadResource m, MonadBaseControl IO m) =>+ MangoPayT (C.ResourceT m) a+ -> MangoPayT m a+runResourceInMp (Mp inner) = Mp $ ask >>= lift . C.runResourceT . runReaderT inner + +-- | Transform the computation inside a 'MangoPayT'.+mapMangoPayT :: (m a -> n b) -> MangoPayT m a -> MangoPayT n b+mapMangoPayT f = Mp . mapReaderT f . unIs + +-- | the data kept through the computations+data MpData = MpData {+ mpCreds::Credentials -- ^ app credentials+ ,mpManager::H.Manager -- ^ HTTP connection manager+ ,mpAccessPoint:: AccessPoint -- ^ access point+ } + deriving (Typeable)++-- | @True@ if the the 'Status' is ok (i.e. @2XX@).+isOkay :: HT.Status -> Bool+isOkay status =+ let sc = HT.statusCode status+ in 200 <= sc && sc < 300++
+ src/Web/MangoPay/Payins.hs view
@@ -0,0 +1,200 @@+{-# LANGUAGE DeriveDataTypeable, ScopedTypeVariables, OverloadedStrings, FlexibleContexts, FlexibleInstances #-}+-- | handle payins+module Web.MangoPay.Payins where++import Web.MangoPay.Accounts+import Web.MangoPay.Monad+import Web.MangoPay.Types+import Web.MangoPay.Users+import Web.MangoPay.Wallets++import Data.Conduit+import Data.Text+import Data.Typeable (Typeable)+import Data.Aeson+import Data.Time.Clock.POSIX (POSIXTime)+import Control.Applicative+import qualified Network.HTTP.Types as HT++-- | create or edit a bankwire+storeBankWire :: (MonadBaseControl IO m, MonadResource m) => BankWire -> AccessToken -> MangoPayT m BankWire+storeBankWire bw at= do+ url<-getClientURL "/payins/bankwire/direct" + postExchange url (Just at) bw+ +-- | fetch a bank wire from its ID+fetchBankWire :: (MonadBaseControl IO m, MonadResource m) => BankWireID -> AccessToken -> MangoPayT m BankWire+fetchBankWire bwid at=do+ url<-getClientURLMultiple ["/payins/",bwid]+ req<-getGetRequest url (Just at) ([]::HT.Query)+ getJSONResponse req ++-- | create or edit a direct card pay in+storeCardPayin :: (MonadBaseControl IO m, MonadResource m) => CardPayin -> AccessToken -> MangoPayT m CardPayin+storeCardPayin cp at= do+ url<-getClientURL "/payins/card/direct" + postExchange url (Just at) cp+ +-- | fetch a direct pay in from its ID+fetchCardPayin :: (MonadBaseControl IO m, MonadResource m) => CardPayinID -> AccessToken -> MangoPayT m CardPayin+fetchCardPayin cpid at=do+ url<-getClientURLMultiple ["/payins/",cpid]+ req<-getGetRequest url (Just at) ([]::HT.Query)+ getJSONResponse req + +data PaymentExecution = WEB -- ^ through a web interface+ | DIRECT -- ^ with a tokenized card+ deriving (Show,Read,Eq,Ord,Bounded,Enum,Typeable)++-- | to json as per MangoPay format+instance ToJSON PaymentExecution where+ toJSON =toJSON . show++-- | from json as per MangoPay format+instance FromJSON PaymentExecution where+ parseJSON (String s)=pure $ read $ unpack s+ parseJSON _ =fail "PaymentExecution"++-- | helper function to create a new bank wire with the needed information+mkBankWire :: AnyUserID -> AnyUserID -> WalletID -> Amount -> Amount -> BankWire+mkBankWire aid uid wid amount fees= BankWire Nothing Nothing Nothing aid uid Nothing+ wid Nothing Nothing Nothing amount fees Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing++-- | bankwire or card pay in+type AnyPayinID=Text++-- | id of a bankwire+type BankWireID=Text++-- | a bank wire+-- there are a lot of common fields between all kinds of payments+-- so this could probably become a "Payment" type+data BankWire=BankWire {+ bwId :: Maybe BankWireID+ ,bwCreationDate :: Maybe POSIXTime+ ,bwTag :: Maybe Text -- ^ custom data+ ,bwAuthorId :: AnyUserID -- ^ The user ID of the author+ ,bwCreditedUserId :: AnyUserID -- ^ It represents the amount credited on the targeted e-wallet.+ ,bwFees :: Maybe Amount -- ^ It represents your fees taken on the DebitedFundsDebitedFunds – Fees = CreditedFunds (amount received on wallet)+ ,bwCreditedWalletId :: WalletID -- ^ The ID of the credited wallet+ ,bwDebitedWalletId :: Maybe WalletID -- ^ The ID of the debited wallet+ ,bwDebitedFunds :: Maybe Amount -- ^ It represents the amount debited from the bank account.+ ,bwCreditedFunds :: Maybe Amount -- ^ It represents the amount credited on the targeted e-wallet.+ ,bwDeclaredDebitedFunds :: Amount -- ^ It represents the expected amount by the platform before that the user makes the payment.+ ,bwDeclaredFees :: Amount -- ^ It represents the expected fees amount by the platform before that the user makes the payment.+ ,bwWireReference :: Maybe Text -- ^ It is a reference generated by MANGOPAY and displayed to the user by the platform. The user have to indicate it into the bank wire.+ ,bwBankAccount :: Maybe BankAccount -- ^ The bank account is generated by MANGOPAY and displayed to the user.+ ,bwStatus :: Maybe TransferStatus -- ^ The status of the payment+ ,bwResultCode :: Maybe Text -- ^ The transaction result code+ ,bwResultMessage :: Maybe Text -- ^ The transaction result Message+ ,bwExecutionDate :: Maybe POSIXTime -- ^ The date when the payment is processed+ ,bwType :: Maybe TransactionType -- ^ The type of the transaction+ ,bwNature :: Maybe TransactionNature -- ^ The nature of the transaction:+ ,bwPaymentType :: Maybe PaymentType -- ^ The type of the payment (which type of mean of payment is used).+ ,bwExecutionType :: Maybe PaymentExecution -- ^ How the payment has been executed:+ } deriving (Show,Eq,Ord,Typeable)++-- | to json as per MangoPay format +instance ToJSON BankWire where+ toJSON bw=object ["Tag" .= bwTag bw,"AuthorId" .= bwAuthorId bw+ ,"CreditedUserId" .= bwCreditedUserId bw,"CreditedWalletId" .= bwCreditedWalletId bw+ ,"DeclaredDebitedFunds" .= bwDeclaredDebitedFunds bw,"DeclaredFees" .= bwDeclaredFees bw]++-- | from json as per MangoPay format +instance FromJSON BankWire where+ parseJSON (Object v) =BankWire <$>+ v .: "Id" <*>+ v .: "CreationDate" <*>+ v .:? "Tag" <*>+ v .: "AuthorId" <*>+ v .: "CreditedUserId" <*>+ v .:? "Fees" <*>+ v .: "CreditedWalletId" <*>+ v .:? "DebitedWalletId" <*>+ v .:? "DebitedFunds" <*>+ v .:? "CreditedFunds" <*>+ v .: "DeclaredDebitedFunds" <*>+ v .: "DeclaredFees" <*>+ v .:? "WireReference" <*>+ v .:? "BankAccount" <*>+ v .:? "Status" <*>+ v .:? "ResultCode" <*>+ v .:? "ResultMessage" <*>+ v .:? "ExecutionDate" <*>+ v .:? "Type" <*>+ v .:? "Nature" <*>+ v .:? "PaymentType" <*>+ v .:? "ExecutionType" + parseJSON _=fail "BankWire" + +-- | ID of a direct pay in+type CardPayinID=Text + +-- | helper function to create a new direct payin with the needed information+-- | the url is only used in secure mode but is REQUIRED by MangoPay+mkCardPayin :: AnyUserID -> AnyUserID -> WalletID -> Amount -> Amount -> Text -> CardID -> CardPayin+mkCardPayin aid uid wid amount fees url cid= CardPayin Nothing Nothing Nothing aid uid fees+ wid Nothing amount Nothing (Just url) Nothing Nothing cid Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing+ + +-- | direct pay in via registered card+data CardPayin=CardPayin {+ cpId :: Maybe CardPayinID+ ,cpCreationDate :: Maybe POSIXTime+ ,cpTag :: Maybe Text -- ^ custom data+ ,cpAuthorId :: AnyUserID -- ^ The user ID of the author+ ,cpCreditedUserId :: AnyUserID -- ^ The user ID of the owner of the credited wallet+ ,cpFees :: Amount -- ^ It represents your fees taken on the DebitedFundsDebitedFunds – Fees = CreditedFunds (amount received on wallet)+ ,cpCreditedWalletId :: WalletID -- ^ The ID of the credited wallet+ ,cpDebitedWalletId :: Maybe WalletID -- ^ The ID of the debited wallet+ ,cpDebitedFunds :: Amount -- ^ It represents the amount debited from the bank account.+ ,cpCreditedFunds :: Maybe Amount -- ^ It represents the amount credited on the targeted e-wallet.+ ,cpSecureModeReturnURL :: Maybe Text -- ^ This URL will be used in case the SecureMode is activated.+ ,cpSecureMode :: Maybe Text -- ^ The SecureMode correspond to « 3D secure » for CB Visa and MasterCard or « Amex Safe Key » for American Express. This field lets you activate it manually.+ ,cpSecureModeRedirectURL :: Maybe Text -- ^ This URL will be used in case the SecureMode is activated.+ ,cpCardId :: CardID -- ^ The ID of the registered card (Got through CardRegistration object)+ ,cpStatus :: Maybe TransferStatus -- ^ The status of the payment+ ,cpResultCode :: Maybe Text -- ^ The transaction result code+ ,cpResultMessage :: Maybe Text -- ^ The transaction result Message+ ,cpExecutionDate :: Maybe POSIXTime -- The date when the payment is processed+ ,cpType :: Maybe TransactionType -- ^ The type of the transaction+ ,cpNature :: Maybe TransactionNature -- ^ The nature of the transaction:+ ,cpPaymentType :: Maybe Text -- ^ The type of the payment (which type of mean of payment is used).+ ,cpExecutionType :: Maybe PaymentExecution -- ^ How the payment has been executed:+ } deriving (Show,Eq,Ord,Typeable)+ +-- | to json as per MangoPay format +instance ToJSON CardPayin where+ toJSON cp=object ["Tag" .= cpTag cp,"AuthorId" .= cpAuthorId cp+ ,"CreditedUserId" .= cpCreditedUserId cp,"CreditedWalletId" .= cpCreditedWalletId cp+ ,"DebitedFunds" .= cpDebitedFunds cp,"Fees" .= cpFees cp,"CardID" .= cpCardId cp+ ,"SecureModeReturnURL" .= cpSecureModeReturnURL cp+ ,"SecureMode" .= cpSecureMode cp]++-- | from json as per MangoPay format +instance FromJSON CardPayin where+ parseJSON (Object v) =CardPayin <$>+ v .: "Id" <*>+ v .: "CreationDate" <*>+ v .:? "Tag" <*>+ v .: "AuthorId" <*>+ v .: "CreditedUserId" <*>+ v .: "Fees" <*>+ v .: "CreditedWalletId" <*>+ v .:? "DebitedWalletId" <*>+ v .: "DebitedFunds" <*>+ v .:? "CreditedFunds" <*>+ v .:? "SecureModeReturnURL" <*>+ v .:? "SecureModeRedirectURL" <*>+ v .:? "SecureMode" <*>+ v .: "CardId" <*>+ v .:? "Status" <*>+ v .:? "ResultCode" <*>+ v .:? "ResultMessage" <*>+ v .:? "ExecutionDate" <*>+ v .:? "Type" <*>+ v .:? "Nature" <*>+ v .:? "PaymentType" <*>+ v .:? "ExecutionType" + parseJSON _=fail "CardPayin" +
+ src/Web/MangoPay/Payouts.hs view
@@ -0,0 +1,92 @@+{-# LANGUAGE DeriveDataTypeable, ScopedTypeVariables, OverloadedStrings, FlexibleContexts, FlexibleInstances #-}+-- | handle payouts+module Web.MangoPay.Payouts where++import Web.MangoPay.Accounts+import Web.MangoPay.Monad+import Web.MangoPay.Types+import Web.MangoPay.Users+import Web.MangoPay.Wallets++import Data.Conduit+import Data.Text+import Data.Typeable (Typeable)+import Data.Aeson+import Data.Time.Clock.POSIX (POSIXTime)+import Control.Applicative+import qualified Network.HTTP.Types as HT++-- | create a payout+storePayout :: (MonadBaseControl IO m, MonadResource m) => Payout -> AccessToken -> MangoPayT m Payout+storePayout pt at= do+ url<-getClientURL "/payouts/bankwire"+ postExchange url (Just at) pt++-- | fetch an payout from its ID+fetchPayout :: (MonadBaseControl IO m, MonadResource m) => PayoutID -> AccessToken -> MangoPayT m Payout+fetchPayout ptid at=do+ url<-getClientURLMultiple ["/payouts/",ptid]+ req<-getGetRequest url (Just at) ([]::HT.Query)+ getJSONResponse req ++-- | make a simplep payout for creation+mkPayout :: AnyUserID -> WalletID -> Amount -> Amount -> BankAccountID -> Payout+mkPayout aid wid fds fees bid=Payout Nothing Nothing Nothing aid wid fds fees bid Nothing Nothing Nothing Nothing Nothing+ Nothing Nothing Nothing Nothing Nothing++-- | id of payout+type PayoutID = Text++-- | payout+data Payout=Payout {+ ptId :: Maybe PayoutID+ ,ptCreationDate :: Maybe POSIXTime+ ,ptTag :: Maybe Text -- ^ custom data for client+ ,ptAuthorId :: AnyUserID -- ^ The user ID of the author+ ,ptDebitedWalletId :: WalletID+ ,ptDebitedFunds :: Amount+ ,ptFees :: Amount+ ,ptBankAccountId :: BankAccountID+ ,ptCreditedUserId :: Maybe AnyUserID+ ,ptCreditedFunds :: Maybe Amount+ ,ptStatus :: Maybe TransferStatus+ ,ptResultCode :: Maybe Text -- ^ The transaction result code+ ,ptResultMessage :: Maybe Text -- ^ The transaction result code+ ,ptExecutionDate :: Maybe POSIXTime+ ,ptType :: Maybe TransactionType+ ,ptNature :: Maybe TransactionNature+ ,ptPaymentType :: Maybe PaymentType+ ,ptMeanOfPaymentType :: Maybe PaymentType -- ^ « BANK_WIRE »,+ } deriving (Show,Eq,Ord,Typeable)+ ++-- | to json as per MangoPay format +instance ToJSON Payout where+ toJSON pt=object ["Tag" .= ptTag pt,"AuthorId" .= ptAuthorId pt+ ,"DebitedWalletId" .= ptDebitedWalletId pt+ ,"DebitedFunds" .= ptDebitedFunds pt,"Fees" .= ptFees pt,"BankAccountId" .= ptBankAccountId pt]++-- | from json as per MangoPay format +instance FromJSON Payout where+ parseJSON (Object v) =Payout <$>+ v .: "Id" <*>+ v .: "CreationDate" <*>+ v .:? "Tag" <*>+ v .: "AuthorId" <*>+ v .: "DebitedWalletId" <*>+ v .: "DebitedFunds" <*>+ v .: "Fees" <*>+ v .: "BankAccountId" <*>+ v .:? "CreditedUserId" <*>+ v .:? "CreditedFunds" <*>+ v .:? "Status" <*>+ v .:? "ResultCode" <*>+ v .:? "ResultMessage" <*>+ v .:? "ExecutionDate" <*>+ v .:? "Type" <*>+ v .:? "Nature" <*>+ v .:? "PaymentType" <*>+ v .:? "MeanOfPaymentType" + parseJSON _=fail "Payout" + +
+ src/Web/MangoPay/Refunds.hs view
@@ -0,0 +1,97 @@+{-# LANGUAGE DeriveDataTypeable, OverloadedStrings, FlexibleContexts #-}+-- | refunds on payins and transfers+module Web.MangoPay.Refunds where++import Web.MangoPay.Monad+import Web.MangoPay.Payins+import Web.MangoPay.Types+import Web.MangoPay.Users+import Web.MangoPay.Wallets++import Data.Conduit+import Data.Text+import Data.Typeable (Typeable)+import Data.Aeson+import Data.Time.Clock.POSIX (POSIXTime)+import Control.Applicative+import qualified Network.HTTP.Types as HT++-- | refund a transfer+refundTransfer :: (MonadBaseControl IO m, MonadResource m) => TransferID -> AnyUserID -> AccessToken -> MangoPayT m Refund+refundTransfer tid authID at= do+ url<-getClientURLMultiple ["/transfers/",tid,"/refunds"]+ postExchange url (Just at) (RefundRequest authID Nothing Nothing)++-- | refund a pay-in+refundPayin :: (MonadBaseControl IO m, MonadResource m) => AnyPayinID -> RefundRequest -> AccessToken -> MangoPayT m Refund+refundPayin pid rr at= do+ url<-getClientURLMultiple ["/payins/",pid,"/refunds"]+ postExchange url (Just at) rr++-- | fetch a refund from its ID+fetchRefund :: (MonadBaseControl IO m, MonadResource m) => RefundID -> AccessToken -> MangoPayT m Refund+fetchRefund rid at=do+ url<-getClientURLMultiple ["/refunds/",rid]+ req<-getGetRequest url (Just at) ([]::HT.Query)+ getJSONResponse req ++-- | refund request+data RefundRequest=RefundRequest{+ rrAuthorId :: AnyUserID -- ^ The user ID of the author+ ,rrDebitedFunds :: Maybe Amount -- ^ Strictly positive amount. In cents.+ ,rrFees :: Maybe Amount -- ^ In cents+ }deriving (Show,Eq,Ord,Typeable)++ -- | to json as per MangoPay format+instance ToJSON RefundRequest where+ toJSON rr=object ["AuthorId" .= rrAuthorId rr,"DebitedFunds" .= rrDebitedFunds rr,+ "Fees" .= rrFees rr] +++-- | id of a refund+type RefundID = Text++-- | refund of a transfer+data Refund=Refund{+ rId :: RefundID -- ^ Id of the refund+ ,rCreationDate :: POSIXTime+ ,rTag :: Maybe Text -- ^ Custom data+ ,rAuthorId :: AnyUserID -- ^ The user ID of the author+ ,rDebitedFunds :: Amount -- ^ Strictly positive amount. In cents.+ ,rFees :: Amount -- ^ In cents+ ,rCreditedFunds :: Amount -- ^ In cents+ ,rStatus :: TransferStatus + ,rResultCode :: Text -- ^ The transaction result code+ ,rResultMessage :: Maybe Text -- ^ The transaction result Message+ ,rExecutionDate :: POSIXTime + ,rType :: TransactionType+ ,rNature :: TransactionNature+ ,rCreditedUserId :: Maybe AnyUserID -- ^ Id of the user owner of the credited wallet+ ,rInitialTransactionId :: TransactionID -- ^ Id of the transaction being refunded+ ,rInitialTransactionType :: TransactionType -- ^ The type of the transaction before being refunded (PayIn, Refund)+ ,rDebitedWalletId :: WalletID -- ^ The Id of the debited Wallet+ ,rCreditedWalletID :: Maybe WalletID -- ^ The Id of the credited Wallet+ } deriving (Show,Eq,Ord,Typeable)+ + -- | from json as per MangoPay format +instance FromJSON Refund where+ parseJSON (Object v) =Refund <$>+ v .: "Id" <*>+ v .: "CreationDate" <*>+ v .:? "Tag" <*>+ v .: "AuthorId" <*>+ v .: "DebitedFunds" <*>+ v .: "Fees" <*>+ v .: "CreditedFunds" <*> + v .: "Status" <*> + v .: "ResultCode" <*>+ v .:? "ResultMessage" <*>+ v .: "ExecutionDate" <*>+ v .: "Type" <*>+ v .: "Nature" <*>+ v .:? "CreditedUserId" <*>+ v .: "InitialTransactionId" <*>+ v .: "InitialTransactionType" <*>+ v .: "DebitedWalletId" <*>+ v .:? "CreditedWalletID"+ parseJSON _=fail "Refund"
+ src/Web/MangoPay/Types.hs view
@@ -0,0 +1,191 @@+{-# LANGUAGE DeriveDataTypeable, OverloadedStrings, ScopedTypeVariables, TypeSynonymInstances, FlexibleInstances #-}+-- | useful types and simple accessor functions+module Web.MangoPay.Types where+++import Control.Applicative+import Control.Exception.Base (Exception)+import Data.Text as T+import Data.Typeable (Typeable)+import Data.ByteString (ByteString)+import Data.Time.Clock.POSIX (POSIXTime)+import Data.Aeson+import Data.Default++import qualified Data.Text.Encoding as TE+import qualified Data.ByteString.UTF8 as UTF8+import Data.Maybe (listToMaybe)+import Network.HTTP.Conduit (HttpException)++-- | the MangoPay access point+data AccessPoint = Sandbox | Production | Custom ByteString+ deriving (Show,Read,Eq,Ord,Typeable)+ +-- | get the real url for the given access point+getAccessPointURL :: AccessPoint -> ByteString+getAccessPointURL Sandbox="api.sandbox.mangopay.com"+getAccessPointURL Production="api.mangopay.com"+getAccessPointURL (Custom bs)=bs++-- | the app credentials+data Credentials = Credentials {+ cClientID :: Text -- ^ client id+ ,cName :: Text -- ^ the name+ ,cEmail :: Text -- ^ the email+ ,cClientSecret :: Maybe Text -- ^ client secret, maybe be Nothing if we haven't generated it+ }+ deriving (Show,Read,Eq,Ord,Typeable)+ +-- | to json as per MangoPay format +instance ToJSON Credentials where+ toJSON c=object ["ClientId" .= cClientID c, "Name" .= cName c , "Email" .= cEmail c,"Passphrase" .= cClientSecret c] ++-- | from json as per MangoPay format+instance FromJSON Credentials where+ parseJSON (Object v) =Credentials <$>+ v .: "ClientId" <*>+ v .: "Name" <*>+ v .: "Email" <*>+ v .: "Passphrase"+ parseJSON _= fail "Credentials" + +-- | get client id in ByteString form+clientIDBS :: Credentials -> ByteString+clientIDBS=TE.encodeUtf8 . cClientID+++-- | the access token is simply a Text+newtype AccessToken=AccessToken ByteString+ deriving (Eq, Ord, Read, Show, Typeable)+ + +-- | the oauth token returned after authentication+data OAuthToken = OAuthToken {+ oaAccessToken :: Text -- ^ the access token+ ,oaTokenType :: Text -- ^ the token type+ ,oaExpires :: Int -- ^ expiration+ }+ deriving (Show,Read,Eq,Ord,Typeable)++-- | to json as per MangoPay format+instance ToJSON OAuthToken where+ toJSON oa=object ["access_token" .= oaAccessToken oa, "token_type" .= oaTokenType oa, "expires_in" .= oaExpires oa] ++-- | from json as per MangoPay format +instance FromJSON OAuthToken where+ parseJSON (Object v) =OAuthToken <$>+ v .: "access_token" <*>+ v .: "token_type" <*>+ v .: "expires_in" + parseJSON _= fail "OAuthToken" + +-- | build the access token from the OAuthToken +toAccessToken :: OAuthToken -> AccessToken+toAccessToken oa=AccessToken $ TE.encodeUtf8 $ T.concat [oaTokenType oa, " ",oaAccessToken oa]+ +-- | an exception that a call to MangoPay may throw+data MpException = MpJSONException String -- ^ JSON parsingError+ | MpAppException MpError -- ^ application exception+ | MpHttpException HttpException (Maybe Value) -- ^ HTTP level exception, maybe with some JSON payload+ deriving (Show,Typeable)++-- | make our exception type a normal exception +instance Exception MpException +++-- | an error returned to us by MangoPay+data MpError = MpError {+ igeID :: Text+ ,igeType :: Text+ ,igeMessage :: Text+ ,igeDate :: Maybe POSIXTime+ }+ deriving (Show,Eq,Ord,Typeable)+ + + +-- | from json as per MangoPay format+instance FromJSON MpError where+ parseJSON (Object v) = MpError <$>+ v .: "Id" <*>+ v .: "Type" <*>+ v .: "Message" <*>+ v .: "Date"+ parseJSON _= fail "MpError"+ +instance FromJSON POSIXTime where+ parseJSON n@(Number _)=(fromIntegral . (round::Double -> Integer)) <$> parseJSON n+ parseJSON _ = fail "POSIXTime"+ +-- | to json as per MangoPay format+instance ToJSON POSIXTime where+ toJSON pt=toJSON (round pt :: Integer)+ +-- | Pagination info for searches+-- <http://docs.mangopay.com/api-references/pagination/>+data Pagination = Pagination {+ pPage :: Integer+ ,pPerPage :: Integer+ }+ deriving (Show,Read,Eq,Ord,Typeable)+ +instance Default Pagination where+ def=Pagination 1 10++-- | get pagination attributes for query +paginationAttributes :: Maybe Pagination -> [(ByteString,Maybe ByteString)]+paginationAttributes (Just p)=["page" ?+ pPage p, "per_page" ?+ pPerPage p]+paginationAttributes _=[]++data PagedList a= PagedList {+ plData :: [a]+ ,plItemCount :: Integer+ ,plPageCount :: Integer+ }+ deriving (Show,Read,Eq,Ord,Typeable)++-- | ID of a card+type CardID=Text++-- | alias for Currency+type Currency=Text++-- | simple class used to hide the serialization of parameters ansd simplify the calling code +class ToHtQuery a where+ (?+) :: ByteString -> a -> (ByteString,Maybe ByteString)++instance ToHtQuery Double where+ n ?+ d=n ?+ show d++instance ToHtQuery (Maybe Double) where+ n ?+ d=n ?+ fmap show d++instance ToHtQuery Integer where+ n ?+ d=n ?+ show d+ +instance ToHtQuery (Maybe Integer) where+ n ?+ d=n ?+ fmap show d+ +instance ToHtQuery (Maybe POSIXTime) where+ n ?+ d=n ?+ fmap (show . (round :: POSIXTime -> Integer)) d+ +instance ToHtQuery (Maybe T.Text) where+ n ?+ d=(n,fmap TE.encodeUtf8 d)++instance ToHtQuery T.Text where+ n ?+ d=(n,Just $ TE.encodeUtf8 d)+ ++instance ToHtQuery (Maybe String) where+ n ?+ d=(n,fmap UTF8.fromString d) ++instance ToHtQuery String where+ n ?+ d=(n,Just $ UTF8.fromString d) ++-- | find in assoc list+findAssoc :: Eq a=> [(a,b)] -> a -> Maybe b+findAssoc xs n=listToMaybe $ Prelude.map snd $ Prelude.filter ((n==) . fst) xs++-- | read an object or return Nothing+maybeRead :: Read a => String -> Maybe a+maybeRead = fmap fst . listToMaybe . reads
+ src/Web/MangoPay/Users.hs view
@@ -0,0 +1,274 @@+{-# LANGUAGE DeriveDataTypeable, ScopedTypeVariables, OverloadedStrings, FlexibleContexts, FlexibleInstances #-}+-- | handle users+module Web.MangoPay.Users where++import Web.MangoPay.Monad+import Web.MangoPay.Types++import Data.Conduit+import Data.Text+import Data.Typeable (Typeable)+import Data.Aeson+import Data.Time.Clock.POSIX (POSIXTime)+import Control.Applicative+import qualified Network.HTTP.Types as HT++-- | create or edit a natural user+storeNaturalUser :: (MonadBaseControl IO m, MonadResource m) => NaturalUser -> AccessToken -> MangoPayT m NaturalUser+storeNaturalUser u at= + case uId u of+ Nothing-> do+ url<-getClientURL "/users/natural"+ postExchange url (Just at) u + Just i-> do+ url<-getClientURLMultiple ["/users/natural/",i]+ putExchange url (Just at) u{uProofOfIdentity=Nothing,uProofOfAddress=Nothing} + ++-- | fetch a natural user from her ID+fetchNaturalUser :: (MonadBaseControl IO m, MonadResource m) => NaturalUserID -> AccessToken -> MangoPayT m NaturalUser+fetchNaturalUser uid at=do+ url<-getClientURLMultiple ["/users/natural/",uid]+ req<-getGetRequest url (Just at) ([]::HT.Query)+ getJSONResponse req ++-- | create or edit a natural user+storeLegalUser :: (MonadBaseControl IO m, MonadResource m) => LegalUser -> AccessToken -> MangoPayT m LegalUser+storeLegalUser u at= + case lId u of+ Nothing-> do+ url<-getClientURL "/users/legal"+ postExchange url (Just at) u + Just i-> do+ url<-getClientURLMultiple ["/users/legal/",i]+ putExchange url (Just at) u+ ++-- | fetch a natural user from her ID+fetchLegalUser :: (MonadBaseControl IO m, MonadResource m) => LegalUserID -> AccessToken -> MangoPayT m LegalUser+fetchLegalUser uid at=do+ url<-getClientURLMultiple ["/users/legal/",uid]+ req<-getGetRequest url (Just at) ([]::HT.Query)+ getJSONResponse req ++-- | get a user, natural or legal+getUser :: (MonadBaseControl IO m, MonadResource m) => AnyUserID -> AccessToken -> MangoPayT m (Either NaturalUser LegalUser)+getUser uid at=do+ url<-getClientURLMultiple ["/users/",uid]+ req<-getGetRequest url (Just at) ([]::HT.Query)+ getJSONResponse req++-- | list all user references+listUsers :: (MonadBaseControl IO m, MonadResource m) => Maybe Pagination -> AccessToken -> MangoPayT m (PagedList UserRef)+listUsers mp at=do+ url<-getClientURL "/users/"+ req<-getGetRequest url (Just at) (paginationAttributes mp)+ getJSONList req ++instance FromJSON (Either NaturalUser LegalUser) where+ parseJSON o@(Object v)=do+ pt::PersonType<-v .: "PersonType"+ case pt of+ Natural->Left <$> parseJSON o+ Legal->Right <$> parseJSON o+ parseJSON _=fail "EitherUsers"++-- not supported+--deleteNaturalUser :: (MonadBaseControl IO m, MonadResource m) => UserID -> AccessToken -> MangoPayT m ()+--deleteNaturalUser uid at=do+-- url<-getClientURL (TE.encodeUtf8 $ Data.Text.concat ["/users/natural/",uid])+-- req<-getDeleteRequest url (Just at) ([]::HT.Query)+-- _::Object<- getJSONResponse req +-- return ()++-- | ID for any kind of user+type AnyUserID = Text++-- | User ID+type NaturalUserID = Text++-- | supported income ranges+data IncomeRange=IncomeRange1 | IncomeRange2 | IncomeRange3 | IncomeRange4 | IncomeRange5 | IncomeRange6+ deriving (Show,Read,Eq,Ord,Bounded, Enum, Typeable) ++-- | to json as per MangoPay format+-- the samples do show string format when writing, integer format when reading...+instance ToJSON IncomeRange where+ toJSON IncomeRange1="1"+ toJSON IncomeRange2="2"+ toJSON IncomeRange3="3"+ toJSON IncomeRange4="4"+ toJSON IncomeRange5="5"+ toJSON IncomeRange6="6"+ +-- | from json as per MangoPay format+-- the samples do show string format when writing, integer format when reading...+instance FromJSON IncomeRange where+ parseJSON (String "1") =pure IncomeRange1 + parseJSON (String "2") =pure IncomeRange2 + parseJSON (String "3") =pure IncomeRange3 + parseJSON (String "4") =pure IncomeRange4 + parseJSON (String "5") =pure IncomeRange5 + parseJSON (String "6") =pure IncomeRange6 + parseJSON (Number 1) =pure IncomeRange1 + parseJSON (Number 2) =pure IncomeRange2 + parseJSON (Number 3) =pure IncomeRange3 + parseJSON (Number 4) =pure IncomeRange4 + parseJSON (Number 5) =pure IncomeRange5 + parseJSON (Number 6) =pure IncomeRange6 + parseJSON _= fail "IncomeRange" ++-- | a natural user+-- <http://docs.mangopay.com/api-references/users/natural-users/>+data NaturalUser=NaturalUser {+ uId :: Maybe NaturalUserID -- ^ The Id of the object+ ,uCreationDate :: Maybe POSIXTime -- ^ The creation date of the user object+ ,uEmail :: Text -- ^ User’s e-mail+ ,uFirstName :: Text -- ^ User’s firstname+ ,uLastName :: Text -- ^ User’s lastname+ ,uAddress :: Maybe Text -- ^ User’s address+ ,uBirthday :: POSIXTime -- ^ User’s birthdate+ ,uNationality :: Text -- ^ User’s Nationality+ ,uCountryOfResidence:: Text -- ^User’s country of residence+ ,uOccupation :: Maybe Text -- ^User’s occupation (ie. Work)+ ,uIncomeRange :: Maybe IncomeRange -- ^ User’s income range+ ,uTag :: Maybe Text -- ^ Custom data+ ,uProofOfIdentity :: Maybe Text -- ^ + ,uProofOfAddress :: Maybe Text -- ^ + }+ deriving (Show,Eq,Ord,Typeable)+ +-- | to json as per MangoPay format +instance ToJSON NaturalUser where+ toJSON u=object ["Tag" .= uTag u,"Email" .= uEmail u,"FirstName".= uFirstName u,"LastName" .= uLastName u,"Address" .= uAddress u, "Birthday" .= uBirthday u+ ,"Nationality" .= uNationality u,"CountryOfResidence" .= uCountryOfResidence u,"Occupation" .= uOccupation u, "IncomeRange" .= uIncomeRange u,"ProofOfIdentity" .= uProofOfIdentity u+ ,"ProofOfAddress" .= uProofOfAddress u,"PersonType" .= Natural] ++-- | from json as per MangoPay format+instance FromJSON NaturalUser where+ parseJSON (Object v) =NaturalUser <$>+ v .: "Id" <*>+ v .: "CreationDate" <*>+ v .: "Email" <*>+ v .: "FirstName" <*>+ v .: "LastName" <*>+ v .:? "Address" <*>+ v .: "Birthday" <*>+ v .: "Nationality" <*>+ v .: "CountryOfResidence" <*>+ v .:? "Occupation" <*>+ v .:? "IncomeRange" <*>+ v .:? "Tag" <*>+ v .:? "ProofOfIdentity" <*>+ v .:? "ProofOfAddress" + parseJSON _= fail "NaturalUser" ++-- | User ID+type LegalUserID = Text+ +-- | the type of legal user +data LegalUserType = Business | Organization+ deriving (Show,Read,Eq,Ord,Enum,Bounded,Typeable) + +-- | to json as per MangoPay format+instance ToJSON LegalUserType where+ toJSON Business="BUSINESS"+ toJSON Organization="ORGANIZATION"+ +-- | from json as per MangoPay format+instance FromJSON LegalUserType where+ parseJSON (String "BUSINESS") =pure Business + parseJSON (String "ORGANIZATION") =pure Organization + parseJSON _= fail "LegalUserType" + +-- | a legal user+-- <http://docs.mangopay.com/api-references/users/legal-users/> +data LegalUser=LegalUser {+ lId :: Maybe Text -- ^ The Id of the object+ ,lCreationDate :: Maybe POSIXTime -- ^ The creation date of the user object+ ,lEmail :: Text -- ^ The email of the company or the organization+ ,lName :: Text -- ^ The name of the company or the organization+ ,lLegalPersonType :: LegalUserType -- ^ The type of the legal user (‘BUSINESS’ or ’ORGANIZATION’)+ ,lHeadquartersAddress :: Maybe Text -- ^ The address of the company’s headquarters+ ,lLegalRepresentativeFirstName :: Text -- ^ The firstname of the company’s Legal representative person+ ,lLegalRepresentativeLastName :: Text -- ^ The lastname of the company’s Legal representative person+ ,lLegalRepresentativeAddress :: Maybe Text -- ^ The address of the company’s Legal representative person+ ,lLegalRepresentativeEmail :: Maybe Text -- ^ The email of the company’s Legal representative person+ ,lLegalRepresentativeBirthday :: POSIXTime -- ^ The birthdate of the company’s Legal representative person+ ,lLegalRepresentativeNationality :: Text -- ^ the nationality of the company’s Legal representative person+ ,lLegalRepresentativeCountryOfResidence :: Text -- ^ The country of residence of the company’s Legal representative person+ ,lStatute :: Maybe Text -- ^ The business statute of the company+ ,lTag :: Maybe Text -- ^ Custom data+ ,lProofOfRegistration :: Maybe Text -- ^ The proof of registration of the company+ ,lShareholderDeclaration :: Maybe Text -- ^ The shareholder declaration of the company+ }+ deriving (Show,Eq,Ord,Typeable)+ +-- | to json as per MangoPay format +instance ToJSON LegalUser where+ toJSON u=object ["Tag" .= lTag u,"Email" .= lEmail u,"Name".= lName u,"LegalPersonType" .= lLegalPersonType u,"HeadquartersAddress" .= lHeadquartersAddress u, "LegalRepresentativeFirstName" .= lLegalRepresentativeFirstName u+ ,"LegalRepresentativeLastName" .= lLegalRepresentativeLastName u,"LegalRepresentativeAddress" .= lLegalRepresentativeAddress u,"LegalRepresentativeEmail" .= lLegalRepresentativeEmail u, "LegalRepresentativeBirthday" .= lLegalRepresentativeBirthday u,"LegalRepresentativeNationality" .= lLegalRepresentativeNationality u+ ,"LegalRepresentativeCountryOfResidence" .= lLegalRepresentativeCountryOfResidence u,"Statute" .= lStatute u,"ProofOfRegistration" .=lProofOfRegistration u,"ShareholderDeclaration" .=lShareholderDeclaration u,"PersonType" .= Legal] + +-- | from json as per MangoPay format+instance FromJSON LegalUser where+ parseJSON (Object v) =LegalUser <$>+ v .: "Id" <*>+ v .: "CreationDate" <*>+ v .: "Email" <*>+ v .: "Name" <*>+ v .: "LegalPersonType" <*>+ v .:? "HeadquartersAddress" <*>+ v .: "LegalRepresentativeFirstName" <*>+ v .: "LegalRepresentativeLastName" <*>+ v .:? "LegalRepresentativeAddress" <*>+ v .:? "LegalRepresentativeEmail" <*>+ v .: "LegalRepresentativeBirthday" <*>+ v .: "LegalRepresentativeNationality" <*>+ v .: "LegalRepresentativeCountryOfResidence" <*>+ v .:? "Statute" <*>+ v .:? "Tag" <*>+ v .:? "ProofOfRegistration" <*>+ v .:? "ShareholderDeclaration" + parseJSON _= fail "NaturalUser" + +data PersonType = Natural | Legal+ deriving (Show,Read,Eq,Ord,Bounded,Enum,Typeable)+ +-- | to json as per MangoPay format+instance ToJSON PersonType where+ toJSON Natural="NATURAL"+ toJSON Legal="LEGAL"+ +-- | from json as per MangoPay format+instance FromJSON PersonType where+ parseJSON (String "NATURAL") =pure Natural + parseJSON (String "LEGAL") =pure Legal + parseJSON _= fail "PersonType" + +-- | a short user reference+data UserRef=UserRef {+ urId :: AnyUserID+ , urCreationDate :: POSIXTime+ , urPersonType :: PersonType+ , urEmail :: Text+ , urTag :: Maybe Text+ } + deriving (Show,Eq,Ord,Typeable)+ +-- | to json as per MangoPay format +instance ToJSON UserRef where+ toJSON ur=object [ "PersonType" .= urPersonType ur, "Email" .= urEmail ur,"Id" .= urId ur,+ "Tag" .= urTag ur,"CreationDate" .= urCreationDate ur]+ + +-- | from json as per MangoPay format+instance FromJSON UserRef where+ parseJSON (Object v) =UserRef <$>+ v .: "Id" <*>+ v .: "CreationDate"<*>+ v .: "PersonType" <*>+ v .: "Email" <*>+ v .:? "Tag" + parseJSON _=fail "UserRef"+
+ src/Web/MangoPay/Wallets.hs view
@@ -0,0 +1,267 @@+{-# LANGUAGE DeriveDataTypeable, ScopedTypeVariables, OverloadedStrings, FlexibleContexts, FlexibleInstances, PatternGuards #-}+-- | handle wallets+module Web.MangoPay.Wallets where+++import Web.MangoPay.Monad+import Web.MangoPay.Types+import Web.MangoPay.Users++import Data.Conduit+import Data.Text+import Data.Typeable (Typeable)+import Data.Aeson+import Data.Time.Clock.POSIX (POSIXTime)+import Control.Applicative+import qualified Network.HTTP.Types as HT+import qualified Data.HashMap.Lazy as HM (delete)++-- | create or edit a wallet+storeWallet :: (MonadBaseControl IO m, MonadResource m) => Wallet -> AccessToken -> MangoPayT m Wallet+storeWallet w at= + case wId w of+ Nothing-> do+ url<-getClientURL "/wallets"+ postExchange url (Just at) w+ Just i-> do+ url<-getClientURLMultiple ["/wallets/",i]+ let Object m=toJSON w+ putExchange url (Just at) (Object $ HM.delete "Currency" m)+ ++-- | fetch a wallet from its ID+fetchWallet :: (MonadBaseControl IO m, MonadResource m) => WalletID -> AccessToken -> MangoPayT m Wallet+fetchWallet wid at=do+ url<-getClientURLMultiple ["/wallets/",wid]+ req<-getGetRequest url (Just at) ([]::HT.Query)+ getJSONResponse req ++-- | list all wallets for a given user +listWallets :: (MonadBaseControl IO m, MonadResource m) => AnyUserID -> Maybe Pagination -> AccessToken -> MangoPayT m (PagedList Wallet)+listWallets uid mp at=do+ url<-getClientURLMultiple ["/users/",uid,"/wallets"]+ req<-getGetRequest url (Just at) (paginationAttributes mp)+ getJSONList req ++-- | create a new fund transfer +createTransfer :: (MonadBaseControl IO m, MonadResource m) => Transfer -> AccessToken -> MangoPayT m Transfer+createTransfer t at= do+ url<-getClientURL "/transfers"+ postExchange url (Just at) t + +-- | fetch a transfer from its ID+fetchTransfer :: (MonadBaseControl IO m, MonadResource m) => TransferID -> AccessToken -> MangoPayT m Transfer+fetchTransfer wid at=do+ url<-getClientURLMultiple ["/transfers/",wid]+ req<-getGetRequest url (Just at) ([]::HT.Query)+ getJSONResponse req ++-- | list transfers for a given wallet +listTransactions :: (MonadBaseControl IO m, MonadResource m) => WalletID -> Maybe Pagination -> AccessToken -> MangoPayT m (PagedList Transaction)+listTransactions wid mp at=do+ url<-getClientURLMultiple ["/wallets/",wid,"/transactions"]+ req<-getGetRequest url (Just at) (paginationAttributes mp)+ getJSONList req ++-- | list transfer for a given user+listTransactionsForUser :: (MonadBaseControl IO m, MonadResource m) => AnyUserID -> Maybe Pagination -> AccessToken -> MangoPayT m (PagedList Transaction)+listTransactionsForUser uid mp at=do+ url<-getClientURLMultiple ["/users/",uid,"/transactions"]+ req<-getGetRequest url (Just at) (paginationAttributes mp)+ getJSONList req ++-- | currency amount +data Amount=Amount {+ aCurrency :: Currency+ ,aAmount :: Integer -- ^ all amounts should be in cents!+ }+ deriving (Show,Read,Eq,Ord,Typeable)+ +-- | to json as per MangoPay format +instance ToJSON Amount where+ toJSON b=object ["Currency" .= aCurrency b,"Amount" .= aAmount b]++-- | from json as per MangoPay format +instance FromJSON Amount where+ parseJSON (Object v) =Amount <$>+ v .: "Currency" <*>+ v .: "Amount" + parseJSON _=fail "Amount"++-- | ID of a wallet+type WalletID=Text ++-- | a wallet +data Wallet = Wallet {+ wId:: Maybe WalletID -- ^ The Id of the wallet+ ,wCreationDate :: Maybe POSIXTime -- ^ The creation date of the object+ ,wTag :: Maybe Text -- ^ Custom data+ ,wOwners :: [Text] -- ^ The owner of the wallet+ ,wDescription :: Text -- ^ A description of the wallet+ ,wCurrency :: Currency -- ^ Currency of the wallet+ ,wBalance :: Maybe Amount -- ^ The amount held on the wallet+ }+ deriving (Show,Eq,Ord,Typeable)++-- | to json as per MangoPay format +instance ToJSON Wallet where+ toJSON w=object ["Tag" .= wTag w,"Owners" .= wOwners w,"Description" .= wDescription w,"Currency" .= wCurrency w]++-- | from json as per MangoPay format +instance FromJSON Wallet where+ parseJSON (Object v) =Wallet <$>+ v .: "Id" <*>+ v .: "CreationDate" <*>+ v .:? "Tag" <*>+ v .: "Owners" <*>+ v .: "Description" <*>+ v .: "Currency" <*>+ v .: "Balance" + parseJSON _=fail "Wallet"+ + +-- | ID of a transfer+type TransferID=Text + +-- | status of a transfer +data TransferStatus= Created | Succeeded | Failed + deriving (Show,Read,Eq,Ord,Bounded,Enum,Typeable)+ +-- | to json as per MangoPay format+instance ToJSON TransferStatus where+ toJSON Created="CREATED"+ toJSON Succeeded="SUCCEEDED"+ toJSON Failed="FAILED"+ +-- | from json as per MangoPay format+instance FromJSON TransferStatus where+ parseJSON (String "CREATED") =pure Created + parseJSON (String "SUCCEEDED") =pure Succeeded+ parseJSON (String "FAILED") =pure Failed + parseJSON _= fail "TransferStatus" + +-- | transfer between wallets+data Transfer = Transfer{+ tId :: Maybe TransferID -- ^ Id of the transfer+ ,tCreationDate :: Maybe POSIXTime -- ^ The creation date of the object+ ,tTag :: Maybe Text -- ^ Custom data+ ,tAuthorId :: AnyUserID -- ^ The Id of the author+ ,tCreditedUserId :: Maybe AnyUserID -- ^ The Id of the user owner of the credited wallet+ ,tDebitedFunds :: Amount -- ^ The funds debited from the « debited wallet »DebitedFunds – Fees = CreditedFunds (amount received on wallet)+ ,tFees :: Amount -- ^ The fees taken on the transfer.DebitedFunds – Fees = CreditedFunds (amount received on wallet)+ ,tDebitedWalletID :: WalletID -- ^ The debited wallet (where the funds are held before the transfer)+ ,tCreditedWalletID:: WalletID -- ^ The credited wallet (where the funds will be held after the transfer)+ ,tCreditedFunds :: Maybe Amount -- ^ The funds credited on the « credited wallet »DebitedFunds – Fees = CreditedFunds (amount received on wallet)+ ,tStatus :: Maybe TransferStatus -- ^ The status of the transfer:+ ,tResultCode :: Maybe Text -- ^ The transaction result code+ ,tResultMessage :: Maybe Text -- ^ The transaction result message+ ,tExecutionDate :: Maybe POSIXTime -- ^ The execution date of the transfer+ }+ deriving (Show,Eq,Ord,Typeable)+ +-- | to json as per MangoPay format+instance ToJSON Transfer where+ toJSON t=object ["AuthorId" .= tAuthorId t,"CreditedUserId" .= tCreditedUserId t,"DebitedFunds" .= tDebitedFunds t,+ "Fees" .= tFees t,"DebitedWalletID" .= tDebitedWalletID t,"CreditedWalletID" .= tCreditedWalletID t,+ "Tag" .= tTag t]+ + -- | from json as per MangoPay format +instance FromJSON Transfer where+ parseJSON (Object v) =Transfer <$>+ v .: "Id" <*>+ v .: "CreationDate" <*>+ v .:? "Tag" <*>+ v .: "AuthorId" <*>+ v .: "CreditedUserId" <*>+ v .: "DebitedFunds" <*>+ v .: "Fees" <*>+ v .: "DebitedWalletId" <*> -- yes, it's ID one way, Id the other+ v .: "CreditedWalletId" <*> -- yes, it's ID one way, Id the other+ v .:? "CreditedFunds" <*>+ v .:? "Status" <*>+ v .:? "ResultCode" <*>+ v .:? "ResultMessage" <*>+ v .:? "ExecutionDate" + parseJSON _=fail "Transfer" + +-- | type of transaction+data TransactionType = PAYIN + | PAYOUT+ | TRANSFER + deriving (Show,Read,Eq,Ord,Bounded,Enum,Typeable)++-- | to json as per MangoPay format+instance ToJSON TransactionType where+ toJSON =toJSON . show++-- | from json as per MangoPay format+instance FromJSON TransactionType where+ parseJSON (String s)+ | ((a,_):_)<-reads $ unpack s=pure a+ parseJSON _ =fail "TransactionType"++data TransactionNature = REGULAR -- ^ just as you created the object+ | REFUND -- ^ the transaction has been refunded+ | REPUDIATION -- ^ the transaction has been repudiated+ deriving (Show,Read,Eq,Ord,Bounded,Enum,Typeable)++-- | to json as per MangoPay format+instance ToJSON TransactionNature where+ toJSON =toJSON . show++-- | from json as per MangoPay format+instance FromJSON TransactionNature where+ parseJSON (String s)+ | ((a,_):_)<-reads $ unpack s=pure a+ parseJSON _ =fail "TransactionNature"+ + +type TransactionID = Text+ +-- | any transaction+data Transaction = Transaction{+ txId :: Maybe TransactionID -- ^ Id of the transfer+ ,txCreationDate :: Maybe POSIXTime -- ^ The creation date of the object+ ,txTag :: Maybe Text -- ^ Custom data+ ,txAuthorId :: AnyUserID -- ^ The Id of the author+ ,txCreditedUserId :: Maybe AnyUserID -- ^ The Id of the user owner of the credited wallet+ ,txDebitedFunds :: Amount -- ^ The funds debited from the « debited wallet »DebitedFunds – Fees = CreditedFunds (amount received on wallet)+ ,txFees :: Amount -- ^ The fees taken on the transfer.DebitedFunds – Fees = CreditedFunds (amount received on wallet)+ ,txDebitedWalletID :: Maybe WalletID -- ^ The debited wallet (where the funds are held before the transfer)+ ,txCreditedWalletID:: Maybe WalletID -- ^ The credited wallet (where the funds will be held after the transfer)+ ,txCreditedFunds :: Maybe Amount -- ^ The funds credited on the « credited wallet »DebitedFunds – Fees = CreditedFunds (amount received on wallet)+ ,txStatus :: Maybe TransferStatus -- ^ The status of the transfer:+ ,txResultCode :: Maybe Text -- ^ The transaction result code+ ,txResultMessage :: Maybe Text -- ^ The transaction result message+ ,txExecutionDate :: Maybe POSIXTime -- ^ The execution date of the transfer+ ,txType :: TransactionType -- ^ The type of the transaction+ ,txNature :: TransactionNature -- ^ The nature of the transaction:+ }+ deriving (Show,Eq,Ord,Typeable)+ +-- | to json as per MangoPay format+instance ToJSON Transaction where+ toJSON t=object ["AuthorId" .= txAuthorId t,"CreditedUserId" .= txCreditedUserId t,"DebitedFunds" .= txDebitedFunds t,+ "Fees" .= txFees t,"DebitedWalletID" .= txDebitedWalletID t,"CreditedWalletID" .= txCreditedWalletID t,+ "Tag" .= txTag t,"Type" .= txType t,"Nature" .= txNature t]+ + -- | from json as per MangoPay format +instance FromJSON Transaction where+ parseJSON (Object v) =Transaction <$>+ v .: "Id" <*>+ v .: "CreationDate" <*>+ v .:? "Tag" <*>+ v .: "AuthorId" <*>+ v .: "CreditedUserId" <*>+ v .: "DebitedFunds" <*>+ v .: "Fees" <*>+ v .:? "DebitedWalletId" <*> -- yes, it's ID one way, Id the other+ v .:? "CreditedWalletId" <*> -- yes, it's ID one way, Id the other+ v .:? "CreditedFunds" <*>+ v .:? "Status" <*>+ v .:? "ResultCode" <*>+ v .:? "ResultMessage" <*>+ v .:? "ExecutionDate" <*>+ v .: "Type" <*>+ v .: "Nature" + parseJSON _=fail "Transfer"
+ test/Web/MangoPay/AccessTest.hs view
@@ -0,0 +1,47 @@+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -F -pgmF htfpp #-}+-- | test access functions that can be tested+module Web.MangoPay.AccessTest where+++import Web.MangoPay+import Web.MangoPay.TestUtils++import Test.Framework+import Test.HUnit (Assertion)+import Data.Aeson+import qualified Data.Text as T+import Data.Maybe (fromJust, isJust)+import Data.ByteString.Lazy as BS+import Data.Time.Clock.POSIX (getPOSIXTime)+import Data.IORef (modifyIORef,readIORef)+import Control.Monad (liftM)+import Data.Conduit (runResourceT)++-- | Take the name/email from client.conf in the current directory+-- (this file can be generated by mangopay-passphrase)+-- generates a new id/name, keeps the same email and generates a new secret+-- saves the secret to use in other tests+test_CreateCredentials :: Assertion+test_CreateCredentials=do+ js<-BS.readFile "client.conf"+ let mcred=decode js+ assertBool (isJust mcred)+ ct<-getPOSIXTime+ -- max 20 characters in ClientID+ let suff=T.take 20 $ T.pack $ show $ round ct+ let creds=(fromJust mcred)+ let newCreds=creds{cClientSecret=Nothing,+ cClientID=T.append (cClientID creds) suff,+ cName=T.append (cName creds) suff}+ mgr<-liftM tsManager $ readIORef testState + creds2<-runResourceT $ runMangoPayT newCreds mgr Sandbox createCredentialsSecret+ assertBool (isJust $ cClientSecret creds2) + let s=fromJust $ cClientSecret creds2+ -- login once with our new credentials+ oat<-runResourceT $ runMangoPayT creds2 mgr Sandbox $+ oauthLogin (cClientID creds2) s + -- store access token and credentials + modifyIORef testState (\ts->ts{tsAccessToken=toAccessToken oat,tsCredentials=creds2})+ -- create hooks for all event types+ mapM_ createHook [minBound .. maxBound]
+ test/Web/MangoPay/AccountsTest.hs view
@@ -0,0 +1,29 @@+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -F -pgmF htfpp #-}+-- | test accounts+module Web.MangoPay.AccountsTest where++import Web.MangoPay+import Web.MangoPay.TestUtils++import Data.Maybe (isJust, fromJust)+import Test.Framework+import Test.HUnit (Assertion)++-- | test bank account creation+test_BankAccount :: Assertion+test_BankAccount=do+ usL<-testMP $ listUsers (Just $ Pagination 1 1)+ assertEqual 1 (length $ plData usL)+ let uid=urId $ head $ plData usL+ let details=IBAN "FR3020041010124530725S03383" "CRLYFRPP"+ let acc1=BankAccount Nothing Nothing (Just uid) Nothing details "JP Moresmau" (Just "Earth")+ acc2<-testMP $ storeAccount acc1+ assertBool $ isJust $ baId acc2+ assertBool $ isJust $ baCreationDate acc2+ assertEqual details $ baDetails acc2+ acc3<-testMP $ fetchAccount uid $ fromJust $ baId acc2+ assertEqual details $ baDetails acc3+ accs<-testMP $ listAccounts uid Nothing+ assertBool $ acc3 `elem` (plData accs)+
+ test/Web/MangoPay/CardsTest.hs view
@@ -0,0 +1,50 @@+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -F -pgmF htfpp #-}+-- | test cards+module Web.MangoPay.CardsTest where++import Web.MangoPay+import Web.MangoPay.TestUtils++import Data.Maybe (isJust, isNothing, fromJust)+import Test.Framework+import Test.HUnit (Assertion)++import qualified Data.Text as T++-- | test a card registration+test_Card :: Assertion+test_Card = do+ usL<-testMP $ listUsers (Just $ Pagination 1 1)+ assertEqual 1 (length $ plData usL)+ let uid=urId $ head $ plData usL+ let cr1=mkCardRegistration uid "EUR"+ cr2<-testMP $ storeCardRegistration cr1+ assertBool (isJust $ crId cr2)+ assertBool (isJust $ crCreationDate cr2)+ assertBool (isJust $ crCardRegistrationURL cr2)+ assertBool (isJust $ crAccessKey cr2)+ assertBool (isJust $ crPreregistrationData cr2) + assertBool (isNothing $ crRegistrationData cr2) + assertBool (isNothing $ crCardId cr2) + cr3<-testMP (\_->registerCard testCardInfo1 cr2)+ assertBool (isJust $ crRegistrationData cr3) + cr4<-testMP $ storeCardRegistration cr3+ assertBool (isJust $ crCardId cr4) + let cid=fromJust $ crCardId cr4+ c<-testMP $ fetchCard cid+ assertEqual cid $ cId c+ assertBool $ not $ T.null $ cAlias c + assertBool $ not $ T.null $ cCardProvider c+ assertBool $ not $ T.null $ cExpirationDate c+ assertEqual UNKNOWN $ cValidity c+ assertBool $ cActive c+ assertEqual uid $ cUserId c+ assertEqual "CB_VISA_MASTERCARD" $ cCardType c+ cs<-testMP $ getAll $ listCards uid+ assertBool $ not $ null cs+ assertBool $ any (\ c1 -> cId c == cid) cs+ + + +
+ test/Web/MangoPay/DocumentsTest.hs view
@@ -0,0 +1,34 @@+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -F -pgmF htfpp #-}+-- | test documents+module Web.MangoPay.DocumentsTest where++import Web.MangoPay+import Web.MangoPay.TestUtils++import Test.Framework+import Test.HUnit (Assertion)+import Data.Maybe (isJust, fromJust)+import qualified Data.ByteString as BS++-- | test document API+test_Document :: Assertion+test_Document=do+ usL<-testMP $ listUsers (Just $ Pagination 1 1)+ assertEqual 1 (length $ plData usL)+ let uid=urId $ head $ plData usL+ let d=Document Nothing Nothing Nothing IDENTITY_PROOF (Just CREATED) Nothing Nothing+ d2<-testMP $ storeDocument uid d+ assertBool (isJust $ dId d2)+ assertBool (isJust $ dCreationDate d2)+ assertEqual IDENTITY_PROOF (dType d2)+ tf<-BS.readFile "data/test.jpg"+ -- document has to be in CREATED status+ testMP $ storePage uid (fromJust $ dId d2) tf+ d3<-testMP $ storeDocument uid d2{dStatus=Just VALIDATION_ASKED}+ assertEqual (Just VALIDATION_ASKED) (dStatus d3)+ assertEqual (dId d2) (dId d3)+ d4<-testMP $ fetchDocument uid (fromJust $ dId d2)+ assertEqual (Just VALIDATION_ASKED) (dStatus d4)+ +
+ test/Web/MangoPay/PayinsTest.hs view
@@ -0,0 +1,80 @@+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -F -pgmF htfpp #-}+-- | test payins+module Web.MangoPay.PayinsTest where++import Web.MangoPay+import Web.MangoPay.TestUtils++import Data.Maybe (isJust, fromJust)+import Test.Framework+import Test.HUnit (Assertion)++-- | test bankwire+test_BankWire :: Assertion+test_BankWire=do+ usL<-testMP $ listUsers (Just $ Pagination 1 1)+ assertEqual 1 (length $ plData usL)+ let uid=urId $ head $ plData usL+ let w=Wallet Nothing Nothing (Just "custom") [uid] "my wallet" "EUR" Nothing + w2<-testMP $ storeWallet w+ assertBool (isJust $ wId w2)+ -- bank wire does not succeed since the real transfer needs to be performed afterwards+ testEventTypes [PAYIN_NORMAL_CREATED] $ do+ let bw1=mkBankWire uid uid (fromJust $ wId w2) (Amount "EUR" 100) (Amount "EUR" 1)+ bw2<-testMP $ storeBankWire bw1+ assertBool (isJust $ bwId bw2)+ assertBool (isJust $ bwBankAccount bw2)+ bw3<-testMP $ fetchBankWire (fromJust $ bwId bw2)+ assertEqual (bwId bw2) (bwId bw3)+ return $ bwId bw2+ +-- | test a successful card pay in+test_CardOK :: Assertion+test_CardOK = do+ usL<-testMP $ listUsers (Just $ Pagination 1 1)+ assertEqual 1 (length $ plData usL)+ let uid=urId $ head $ plData usL+ cr<-testMP $ fullRegistration uid "EUR" testCardInfo1+ assertBool (isJust $ crCardId cr)+ let cid=fromJust $ crCardId cr+ let w=Wallet Nothing Nothing (Just "custom") [uid] "my wallet" "EUR" Nothing + w2<-testMP $ storeWallet w+ assertBool (isJust $ wId w2)+ let wid=fromJust $ wId w2+ testEventTypes [PAYIN_NORMAL_CREATED,PAYIN_NORMAL_SUCCEEDED] $ do+ let cp=mkCardPayin uid uid wid (Amount "EUR" 333) (Amount "EUR" 1) "http://dummy" cid+ cp2<-testMP $ storeCardPayin cp+ assertBool (isJust $ cpId cp2)+ assertEqual (Just Succeeded) (cpStatus cp2)+ w3<-testMP $ fetchWallet wid+ assertEqual (Just $ Amount "EUR" 332) (wBalance w3)+ ts1 <- testMP $ listTransactions wid Nothing+ assertEqual 1 (length $ filter ((cpId cp2==) . txId) $ plData ts1)+ return $ cpId cp2+ +-- | test a failed card pay in+-- according to <http://docs.mangopay.com/api-references/test-payment/>+test_CardKO :: Assertion+test_CardKO = do+ usL<-testMP $ listUsers (Just $ Pagination 1 1)+ assertEqual 1 (length $ plData usL)+ let uid=urId $ head $ plData usL+ cr<-testMP $ fullRegistration uid "EUR" testCardInfo1+ assertBool (isJust $ crCardId cr)+ let cid=fromJust $ crCardId cr+ let w=Wallet Nothing Nothing (Just "custom") [uid] "my wallet" "EUR" Nothing + w2<-testMP $ storeWallet w+ assertBool (isJust $ wId w2)+ let wid=fromJust $ wId w2+ testEventTypes [PAYIN_NORMAL_CREATED,PAYIN_NORMAL_FAILED] $ do+ let cp=mkCardPayin uid uid wid (Amount "EUR" 33394) (Amount "EUR" 0) "http://dummy" cid+ cp2<-testMP $ storeCardPayin cp+ assertBool (isJust $ cpId cp2)+ assertEqual (Just Failed) (cpStatus cp2)+ assertEqual (Just "009199") (cpResultCode cp2)+ assertEqual (Just "PSP technical error") (cpResultMessage cp2)+ w3<-testMP $ fetchWallet wid+ assertEqual (Just $ Amount "EUR" 0) (wBalance w3)+ return $ cpId cp2+
+ test/Web/MangoPay/PayoutsTest.hs view
@@ -0,0 +1,61 @@+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -F -pgmF htfpp #-}+-- | test payouts+module Web.MangoPay.PayoutsTest where++import Web.MangoPay+import Web.MangoPay.TestUtils++import Data.Maybe (isJust, fromJust)+import Test.Framework+import Test.HUnit (Assertion)++-- | test successful payout+test_PayoutOK :: Assertion+test_PayoutOK=do+ usL<-testMP $ listUsers (Just $ Pagination 1 1)+ assertEqual 1 (length $ plData usL)+ let uid=urId $ head $ plData usL+ accs<-testMP $ listAccounts uid Nothing+ assertBool $ not $ null $ plData accs+ let aid=fromJust $ baId $ head $ plData accs + ws<- testMP $ listWallets uid Nothing+ assertBool $ not $ null $ plData ws+ let wid=fromJust $ wId $ head $ plData ws + let Just (Amount _ nb) =wBalance $ head $ plData ws+ assertBool $ nb >= 100+ -- no events triggered !!!+ --testEventTypes [PAYOUT_NORMAL_SUCCEEDED] $ do+ let pt1=mkPayout uid wid (Amount "EUR" 100) (Amount "EUR" 0) aid+ pt2<-testMP $ storePayout pt1+ assertBool $ isJust $ ptId pt2+ assertEqual (Just Created) (ptStatus pt2)+ assertEqual (Just (Amount "EUR" 100)) (ptCreditedFunds pt2)+ pt3 <- testMP $ fetchPayout $ fromJust $ ptId pt2+ assertEqual (Just BANK_WIRE) (ptPaymentType pt3)+ -- return $ ptId pt2+ +-- | test failing payout+test_PayoutKO :: Assertion+test_PayoutKO=do+ usL<-testMP $ listUsers (Just $ Pagination 1 1)+ assertEqual 1 (length $ plData usL)+ let uid=urId $ head $ plData usL+ accs<-testMP $ listAccounts uid Nothing+ assertBool $ not $ null $ plData accs+ let aid=fromJust $ baId $ head $ plData accs + ws<- testMP $ listWallets uid Nothing+ assertBool $ not $ null $ plData ws+ let wid=fromJust $ wId $ head $ plData ws + testEventTypes [PAYOUT_NORMAL_FAILED] $ do+ let pt1=mkPayout uid wid (Amount "EUR" 100000) (Amount "EUR" 0) aid+ pt2<-testMP $ storePayout pt1+ assertBool $ isJust $ ptId pt2+ assertEqual (Just Failed) (ptStatus pt2)+ pt3 <- testMP $ fetchPayout $ fromJust $ ptId pt2+ assertEqual (Just BANK_WIRE) (ptPaymentType pt3)+ assertEqual (Just "001001") (ptResultCode pt3)+ assertEqual (Just "Unsufficient wallet balance") (ptResultMessage pt3)+ return $ ptId pt2+ +
+ test/Web/MangoPay/RefundsTest.hs view
@@ -0,0 +1,121 @@+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -F -pgmF htfpp #-}+-- | test refunds+module Web.MangoPay.RefundsTest where++import Web.MangoPay+import Web.MangoPay.TestUtils++import Data.Maybe (isJust, fromJust)+import Test.Framework+import Test.HUnit (Assertion)++-- | test a successful card pay in + full refund+test_SimpleCardRefund :: Assertion+test_SimpleCardRefund = do+ usL<-testMP $ listUsers (Just $ Pagination 1 1)+ assertEqual 1 (length $ plData usL)+ let uid=urId $ head $ plData usL+ cr<-testMP $ fullRegistration uid "EUR" testCardInfo1+ assertBool (isJust $ crCardId cr)+ let cid=fromJust $ crCardId cr+ let w=Wallet Nothing Nothing (Just "custom") [uid] "my wallet" "EUR" Nothing + w2<-testMP $ storeWallet w+ assertBool (isJust $ wId w2)+ let wid=fromJust $ wId w2+ (Just cp)<-testEventTypes' [PAYIN_NORMAL_CREATED,PAYIN_NORMAL_SUCCEEDED] $ do+ let cp=mkCardPayin uid uid wid (Amount "EUR" 333) (Amount "EUR" 1) "http://dummy" cid+ cp2<-testMP $ storeCardPayin cp+ assertBool (isJust $ cpId cp2)+ assertEqual (Just Succeeded) (cpStatus cp2)+ w3<-testMP $ fetchWallet wid+ assertEqual (Just $ Amount "EUR" 332) (wBalance w3)+ return $ cpId cp2+ -- PAYIN_REFUND_CREATED not received ???+ testEventTypes [PAYIN_REFUND_SUCCEEDED] $ do+ let rr=RefundRequest uid Nothing Nothing+ r<-testMP $ refundPayin cp rr+ assertEqual PAYIN (rInitialTransactionType r)+ r2<-testMP $ fetchRefund (rId r)+ assertEqual cp $ rInitialTransactionId r2+ assertEqual (Amount "EUR" 333) $ rCreditedFunds r2+ return $ Just $ rId r+ +-- | test a successful card pay in + partial refund+test_AdvancedCardRefund :: Assertion+test_AdvancedCardRefund = do+ usL<-testMP $ listUsers (Just $ Pagination 1 1)+ assertEqual 1 (length $ plData usL)+ let uid=urId $ head $ plData usL+ cr<-testMP $ fullRegistration uid "EUR" testCardInfo1+ assertBool (isJust $ crCardId cr)+ let cid=fromJust $ crCardId cr+ let w=Wallet Nothing Nothing (Just "custom") [uid] "my wallet" "EUR" Nothing + w2<-testMP $ storeWallet w+ assertBool (isJust $ wId w2)+ let wid=fromJust $ wId w2+ (Just cp)<-testEventTypes' [PAYIN_NORMAL_CREATED,PAYIN_NORMAL_SUCCEEDED] $ do+ let cp=mkCardPayin uid uid wid (Amount "EUR" 333) (Amount "EUR" 1) "http://dummy" cid+ cp2<-testMP $ storeCardPayin cp+ assertBool (isJust $ cpId cp2)+ assertEqual (Just Succeeded) (cpStatus cp2)+ w3<-testMP $ fetchWallet wid+ assertEqual (Just $ Amount "EUR" 332) (wBalance w3)+ return $ cpId cp2+ -- PAYIN_REFUND_CREATED not received ??? + testEventTypes [PAYIN_REFUND_SUCCEEDED] $ do+ let rr=RefundRequest uid (Just $ Amount "EUR" 100) (Just $ Amount "EUR" 1) + r<-testMP $ refundPayin cp rr+ assertEqual PAYIN (rInitialTransactionType r)+ r2<-testMP $ fetchRefund (rId r)+ assertEqual cp $ rInitialTransactionId r2+ assertEqual (Amount "EUR" 99) $ rCreditedFunds r2+ return $ Just $ rId r + +-- | test transfer + full refund+test_TransferRefund :: Assertion+test_TransferRefund = do+ usL<-testMP $ listUsers (Just $ Pagination 1 2)+ assertEqual 2 (length $ plData usL)+ let [uid1,uid2] = map urId $ plData usL+ assertBool (uid1 /= uid2)+ let w1=Wallet Nothing Nothing (Just "custom") [uid1] "my wallet" "EUR" Nothing + w1'<-testMP $ storeWallet w1+ let uw1=fromJust $ wId w1'+ let w2=Wallet Nothing Nothing (Just "custom") [uid2] "my wallet" "EUR" Nothing + w2'<-testMP $ storeWallet w2+ let uw2=fromJust $ wId w2'+ assertBool (uw1 /= uw2)+ + cr<-testMP $ fullRegistration uid1 "EUR" testCardInfo1+ assertBool (isJust $ crCardId cr)+ let cid=fromJust $ crCardId cr+ testEventTypes [PAYIN_NORMAL_CREATED,PAYIN_NORMAL_SUCCEEDED] $ do+ let cp=mkCardPayin uid1 uid1 uw1 (Amount "EUR" 333) (Amount "EUR" 1) "http://dummy" cid+ cp2<-testMP $ storeCardPayin cp+ assertEqual (Just Succeeded) (cpStatus cp2)+ return $ cpId cp2++ (Just tr)<-testEventTypes' [TRANSFER_NORMAL_CREATED,TRANSFER_NORMAL_SUCCEEDED] $ do+ let t1=Transfer Nothing Nothing Nothing uid1 (Just uid2) (Amount "EUR" 100) (Amount "EUR" 1)+ uw1 uw2 Nothing Nothing Nothing Nothing Nothing+ t1'<-testMP $ createTransfer t1+ assertBool (isJust $ tId t1')+ assertEqual (Just $ Amount "EUR" 99) (tCreditedFunds t1')+ t2'<-testMP $ fetchTransfer (fromJust $ tId t1')+ assertEqual t1' t2'+ ts1 <- testMP $ listTransactions uw1 Nothing+ assertEqual 1 (length $ filter ((tId t1'==) . txId) $ plData ts1)+ ts2 <- testMP $ listTransactions uw2 Nothing+ assertEqual 1 (length $ filter ((tId t1'==) . txId) $ plData ts2)+ uts1 <- testMP $ listTransactionsForUser uid1 (Just $ Pagination 1 50)+ assertEqual 1 (length $ filter ((tId t1'==) . txId) $ plData uts1) + return $ tId t1' + + testEventTypes [TRANSFER_REFUND_CREATED,TRANSFER_REFUND_SUCCEEDED] $ do+ r<-testMP $ refundTransfer tr uid1+ assertEqual TRANSFER (rInitialTransactionType r)+ r2<-testMP $ fetchRefund (rId r)+ assertEqual tr $ rInitialTransactionId r2+ assertEqual (Amount "EUR" 100) $ rCreditedFunds r2+ return $ Just $ rId r
+ test/Web/MangoPay/TestUtils.hs view
@@ -0,0 +1,234 @@+{-# LANGUAGE RankNTypes,OverloadedStrings,DeriveDataTypeable #-}+{-# OPTIONS_GHC -F -pgmF htfpp #-}+module Web.MangoPay.TestUtils where++import Web.MangoPay++import Data.ByteString.Lazy as BS hiding (map,any,null)+import Network.HTTP.Conduit as H+import Data.Conduit+import Data.Maybe+import Test.Framework++import Network.Wai as W+import Network.Wai.Handler.Warp+import Network.HTTP.Types (status200)+import Blaze.ByteString.Builder (copyByteString)+import Data.Aeson as A+import Control.Concurrent (forkIO, ThreadId, threadDelay)+import Control.Concurrent.MVar (MVar, newMVar, putMVar, takeMVar)+import Control.Monad (when, void, liftM)+import Control.Monad.IO.Class (liftIO)++import Data.Text (Text)+import Data.List+import Data.Typeable+import Control.Applicative+import Test.HUnit (Assertion)+import Data.Default (def)+import Data.IORef +import System.IO.Unsafe (unsafePerformIO)++-- | a test card+testCardInfo1 :: CardInfo+testCardInfo1 = CardInfo "4970100000000154" "1220" "123"++-- | test MangoPay API call, logging in with the client credentials+-- expects a file called client.test.conf containing the JSON of client credentials+-- in the current directory +testMP :: forall b.+ (AccessToken -> MangoPayT (ResourceT IO) b)+ -> IO b+testMP f= do+ ior<-readIORef testState + let mgr=tsManager ior + let at=tsAccessToken ior+ let cred=tsCredentials ior+ runResourceT $ runMangoPayT cred mgr Sandbox $ f at++-- | the test state as a top level global variable+-- <http://www.haskell.org/haskellwiki/Top_level_mutable_state>+-- since HTF does not let us thread a parameter through tests+testState :: IORef TestState+{-# NOINLINE testState #-}+testState = unsafePerformIO (newIORef $ TestState zero zero zero zero zero)+ where zero=error "testState has not been initialized yet"+++-- | the test state we keep over all tests+data TestState=TestState {+ tsAccessToken :: AccessToken -- ^ the access token if we're logged in+ ,tsCredentials :: Credentials -- ^ the credentials+ ,tsManager :: H.Manager -- ^ the http manager+ ,tsHookEndPoint :: HookEndPoint -- ^ the end point for notifications+ ,tsReceivedEvents :: ReceivedEvents -- ^ the received events+ }+ ++-- | read end point information from hook.test.conf in current folder+getHookEndPoint :: IO HookEndPoint+getHookEndPoint = do+ js<-BS.readFile "hook.test.conf"+ let mhook=decode js + assertBool (isJust mhook)+ return $ fromJust mhook++-- | simple configuration to tell the tests what endpoint to use for notifications+data HookEndPoint = HookEndPoint{+ hepUrl :: Text+ ,hepPort :: Int+ } deriving (Show,Read,Eq,Ord,Typeable)++-- | to json +instance ToJSON HookEndPoint where+ toJSON h=object ["Url" .= hepUrl h,"Port" .= hepPort h]++-- | from json +instance FromJSON HookEndPoint where+ parseJSON (Object v) =HookEndPoint <$>+ v .: "Url" <*>+ v .: "Port" + parseJSON _=fail "HookEndPoint"+++-- | the events received via the notification hook+-- uses a MVar for storing events+data ReceivedEvents=ReceivedEvents{+ events::MVar [Either EventResult Event]+ }++-- | creates the new ReceivedEvents+newReceivedEvents :: IO ReceivedEvents+newReceivedEvents=do+ mv<-newMVar [] + return $ ReceivedEvents mv++-- | test an event, checking the resource id and the event type+testEvent :: Maybe Text -> EventType -> Event -> Bool+testEvent tid et evt= tid == (Just $ eResourceId evt) + && et == eEventType evt++-- | check that we're receiving events of the given type with the resource id returned by the passed test+testEventTypes :: [EventType] + -> IO (Maybe Text)+ -> Assertion+testEventTypes evtTs =void . testEventTypes' evtTs ++-- | check that we're receiving events of the given type with the resource id returned by the passed test+-- returns the result of the inner test+testEventTypes' :: [EventType] + -> IO (Maybe Text)+ -> IO (Maybe Text)+testEventTypes' evtTs ops=do+ res<-liftM tsReceivedEvents $ readIORef testState+ a<-ops+ mapM_ (testSearchEvent a) evtTs+ er<-waitForEvent res (map (testEvent a) evtTs) 5+ assertEqual EventsOK er+ return a++-- | assert that we find an event for the given resource id and event type+testSearchEvent :: Maybe Text -> EventType -> Assertion+testSearchEvent tid evtT=do+ es<-testMP $ searchEvents (def{espEventType=Just evtT})+ assertBool (not $ null es)+ assertBool (any ((tid ==) . Just . eResourceId) es)++-- | create a hook for a given event type +createHook :: EventType -> Assertion+createHook evtT= do+ hook<-liftM tsHookEndPoint $ readIORef testState+ h<-testMP $ storeHook (Hook Nothing Nothing Nothing (hepUrl hook) Enabled Nothing evtT)+ assertBool (isJust $ hId h)+ h2<-testMP $ fetchHook (fromJust $ hId h)+ assertEqual (hId h) (hId h2)+ assertEqual (Just Valid) (hValidity h)+ +-- | run a test with the notification server running+testEvents :: IO a -- ^ the test, returning a value+ -> [a -> Event -> Bool] -- ^ the test on the events, taking into account the returned value+ -> Assertion+testEvents ops tests=do+ res<-liftM tsReceivedEvents $ readIORef testState+ a<-ops+ er<-waitForEvent res (map ($ a) tests) 30+ assertEqual EventsOK er+ +-- | result of waiting for event+data EventResult = Timeout -- ^ didn't receive all expected events+ | EventsOK -- ^ OK: everything expected received, nothing unexpected+ | ExtraEvent Event -- ^ unexpected+ | UnhandledNotification String -- ^ notification we couldn't parse+ deriving (Show,Eq,Ord,Typeable)++-- | wait till we receive all the expected events, and none other, for a maximum number of seconds+waitForEvent :: ReceivedEvents + -> [Event -> Bool] -- ^ function on the expected event+ -> Integer -- ^ delay in seconds+ -> IO EventResult+waitForEvent _ _ del | del<=0=return Timeout+waitForEvent rc fs del=do+ mevt<-popReceivedEvent rc+ case (mevt,fs) of+ (Nothing,[])->return EventsOK -- nothing left to process+ (Just (Left er),_)->return er -- some notification we didn't understand+ (Just (Right evt),[])->return $ ExtraEvent evt -- an event that doesn't match+ (Nothing,_)->do -- no event yet+ threadDelay 1000000+ waitForEvent rc fs (del-1)+ (Just (Right evt),_)-> -- an event, does it match+ case Data.List.foldl' (match1 evt) ([],False) fs of+ (_,False)->return $ ExtraEvent evt -- doesn't match+ (fs2,_)-> waitForEvent rc fs2 del -- matched, either we have more to do or we need to check no unexpected event was found+ + where + -- | match the first event function and return all the non matching function, and a flag indicating if we matched+ match1 :: Event -> ([Event -> Bool],Bool) -> (Event -> Bool) -> ([Event -> Bool],Bool)+ match1 evt (nfs,False) f+ | f evt=(nfs,True)+ | otherwise=(f:nfs,False)+ match1 _ (nfs,True) f=(f:nfs,True)++-- | get one received event (and remove it from the underlying storage) +popReceivedEvent :: ReceivedEvents -> IO (Maybe (Either EventResult Event))+popReceivedEvent (ReceivedEvents mv)=do+ evts<-takeMVar mv + case evts of+ []->do+ putMVar mv []+ return Nothing+ (e:es)->do+ putMVar mv es+ return $ Just e++-- | get all received events (and remove them from the underlying storage) +popReceivedEvents :: ReceivedEvents -> IO [Either EventResult Event]+popReceivedEvents (ReceivedEvents mv)=do+ evts<-takeMVar mv + putMVar mv []+ return evts++-- | add a new event+pushReceivedEvent :: ReceivedEvents -> Either EventResult Event -> IO ()+pushReceivedEvent (ReceivedEvents mv) evt=do+ evts' <-takeMVar mv + -- we're getting events in duplicate ???+ let ns = if evt `Prelude.elem` evts' then evts' else evt:evts'+ putMVar mv ns++-- | start a HTTP server listening on the given port+-- if the path info is "mphook", then we'll push the received event +startHTTPServer :: Port -> ReceivedEvents -> IO ThreadId+startHTTPServer p revts= + forkIO $ run p app+ where+ app req = do+ when (pathInfo req == ["mphook"]) $ do+ let mevt=eventFromQueryString $ W.queryString req+ liftIO $ case mevt of+ Just evt->do+ pushReceivedEvent revts $ Right evt+ print evt+ Nothing->pushReceivedEvent revts $ Left $ UnhandledNotification $ show $ W.queryString req+ return $ W.responseBuilder status200 [("Content-Type", "text/plain")] $ copyByteString "noop"+
+ test/Web/MangoPay/UsersTest.hs view
@@ -0,0 +1,67 @@+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -F -pgmF htfpp #-}+module Web.MangoPay.UsersTest where++import Web.MangoPay+import Web.MangoPay.TestUtils++import Test.Framework+import Test.HUnit (Assertion)+import Data.Maybe (fromJust, isJust)++testNaturalUser :: NaturalUser+testNaturalUser=NaturalUser Nothing Nothing "jpmoresmau@gmail.com" "JP" "Moresmau" Nothing 11111 "FR" "FR" + (Just "Haskell contractor") (Just IncomeRange2) Nothing Nothing Nothing ++test_NaturalUser :: Assertion+test_NaturalUser = do+ u<-testMP $ storeNaturalUser testNaturalUser+ assertBool (isJust $ uId u)+ assertBool (isJust $ uCreationDate u)+ uf<-testMP $ fetchNaturalUser (fromJust $ uId u)+ assertEqual (uEmail testNaturalUser) (uEmail uf)+ assertEqual Nothing (uAddress uf)+ ue<-testMP $ storeNaturalUser (uf{uAddress=Just "St Guilhem"})+ assertEqual (Just "St Guilhem") (uAddress ue)+ assertEqual (Just IncomeRange2) (uIncomeRange ue)+ eu<-testMP $ getUser (fromJust $ uId u)+ assertEqual (Left ue) eu+ usL<-testMP $ listUsers (Just $ Pagination 1 100)+ assertEqual 1 (length $ filter (((fromJust $ uId u)==) . urId) $ plData usL)++testLegalUser :: LegalUser+testLegalUser = LegalUser Nothing Nothing "jpmoresmau@gmail.com" "JP Moresmau" Business Nothing+ "JP" "Moresmau" (Just "my house") Nothing 222222 "FR" "FR" Nothing Nothing Nothing Nothing+ +test_LegalUser :: Assertion+test_LegalUser = do+ l<-testMP $ storeLegalUser testLegalUser+ assertBool (isJust $ lId l)+ assertBool (isJust $ lCreationDate l)+ lf<-testMP $ fetchLegalUser (fromJust $ lId l)+ assertEqual (lEmail testLegalUser) (lEmail lf)+ assertEqual Nothing (lHeadquartersAddress lf)+ le<-testMP $ storeLegalUser (lf{lHeadquartersAddress=Just "St Guilhem"})+ assertEqual (Just "St Guilhem") (lHeadquartersAddress le)+ assertEqual "Moresmau" (lLegalRepresentativeLastName le)+ assertEqual (Just "my house") (lLegalRepresentativeAddress le) -- this is lost, see https://mangopay.desk.com/customer/portal/questions/5980417-legalrepresentativeaddress-in-legaluser-api+ assertEqual Nothing (lProofOfRegistration le)+ assertEqual Nothing (lShareholderDeclaration le)+ el<-testMP $ getUser (fromJust $ lId l)+ assertEqual (Right le) el+ usL<-testMP $ listUsers (Just $ Pagination 1 100)+ assertEqual 1 (length $ filter (((fromJust $ lId l)==) . urId) $ plData usL)+ +test_PaginationUsers :: Assertion+test_PaginationUsers = do+ usL<-testMP $ listUsers (Just $ Pagination 1 1)+ print usL+ assertEqual 1 (length $ plData usL)+ assertEqual 2 (plItemCount usL)+ assertEqual 2 (plPageCount usL)+ usL2<-testMP $ listUsers (Just $ Pagination 1 10)+ assertEqual 2 (length $ plData usL2)+ assertEqual 2 (plItemCount usL2)+ assertEqual 1 (plPageCount usL2)+ us<-testMP $ getAll listUsers+ assertEqual 2 (length us)
+ test/Web/MangoPay/WalletsTest.hs view
@@ -0,0 +1,103 @@+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -F -pgmF htfpp #-}+-- | test wallets and transfers+module Web.MangoPay.WalletsTest where++import Web.MangoPay+import Web.MangoPay.TestUtils++import Test.Framework+import Test.HUnit (Assertion)+import Data.Maybe (isJust, fromJust)++-- | test wallet API+test_Wallet :: Assertion+test_Wallet = do+ usL<-testMP $ listUsers (Just $ Pagination 1 1)+ assertEqual 1 (length $ plData usL)+ let uid=urId $ head $ plData usL+ let w=Wallet Nothing Nothing (Just "custom") [uid] "my wallet" "EUR" Nothing + w2<-testMP $ storeWallet w+ assertBool (isJust $ wId w2)+ assertBool (isJust $ wCreationDate w2)+ w3<-testMP $ storeWallet w2{wTag=Just "custom2"}+ assertEqual (Just "custom2") (wTag w3)+ assertEqual (wId w2) (wId w3)+ w4<-testMP $ fetchWallet (fromJust $ wId w2)+ assertEqual (Just "custom2") (wTag w4)+ assertEqual (wId w2) (wId w4)+ assertEqual (Just $ Amount "EUR" 0) (wBalance w4)+ ws<-testMP $ listWallets uid (Just $ Pagination 1 100)+ assertBool (not (null $ plData ws))+ assertEqual 1 (length $ filter ((wId w3 ==) . wId) $ plData ws)++-- | test transfer API + notifications on failure +test_FailedTransfer :: Assertion+test_FailedTransfer = do+ usL<-testMP $ listUsers (Just $ Pagination 1 2)+ assertEqual 2 (length $ plData usL)+ let [uid1,uid2] = map urId $ plData usL+ assertBool (uid1 /= uid2)+ ws<- testMP $ listWallets uid1 Nothing+ assertBool $ not $ null $ plData ws+ let uw1=fromJust $ wId $ head $ plData ws + let w2=Wallet Nothing Nothing (Just "custom") [uid2] "my wallet" "EUR" Nothing + w2'<-testMP $ storeWallet w2+ let uw2=fromJust $ wId w2'+ assertBool (uw1 /= uw2)+ -- transfer will fail since I have no money+ testEventTypes [TRANSFER_NORMAL_CREATED,TRANSFER_NORMAL_FAILED] $ do+ let t1=Transfer Nothing Nothing Nothing uid1 (Just uid2) (Amount "EUR" 100) (Amount "EUR" 1)+ uw1 uw2 Nothing Nothing Nothing Nothing Nothing+ t1'<-testMP $ createTransfer t1+ assertBool (isJust $ tId t1')+ assertEqual (Just $ Amount "EUR" 99) (tCreditedFunds t1')+ t2'<-testMP $ fetchTransfer (fromJust $ tId t1')+ assertEqual t1' t2'+ ts1 <- testMP $ listTransactions uw1 Nothing+ assertEqual 1 (length $ filter ((tId t1'==) . txId) $ plData ts1)+ ts2 <- testMP $ listTransactions uw2 Nothing+ assertEqual 1 (length $ filter ((tId t1'==) . txId) $ plData ts2)+ uts1 <- testMP $ listTransactionsForUser uid1 Nothing+ assertEqual 1 (length $ filter ((tId t1'==) . txId) $ plData uts1)+ return $ tId t1'+ +-- | test transfer API + notifications on success +test_SuccessfulTransfer :: Assertion+test_SuccessfulTransfer = do+ usL<-testMP $ listUsers (Just $ Pagination 1 2)+ assertEqual 2 (length $ plData usL)+ let [uid1,uid2] = map urId $ plData usL+ assertBool (uid1 /= uid2)+ ws<- testMP $ listWallets uid1 Nothing+ assertBool $ not $ null $ plData ws+ let uw1=fromJust $ wId $ head $ plData ws + let w2=Wallet Nothing Nothing (Just "custom") [uid2] "my wallet" "EUR" Nothing + w2'<-testMP $ storeWallet w2+ let uw2=fromJust $ wId w2'+ assertBool (uw1 /= uw2)+ + cr<-testMP $ fullRegistration uid1 "EUR" testCardInfo1+ assertBool (isJust $ crCardId cr)+ let cid=fromJust $ crCardId cr+ testEventTypes [PAYIN_NORMAL_CREATED,PAYIN_NORMAL_SUCCEEDED] $ do+ let cp=mkCardPayin uid1 uid1 uw1 (Amount "EUR" 333) (Amount "EUR" 1) "http://dummy" cid+ cp2<-testMP $ storeCardPayin cp+ assertEqual (Just Succeeded) (cpStatus cp2)+ return $ cpId cp2++ testEventTypes [TRANSFER_NORMAL_CREATED,TRANSFER_NORMAL_SUCCEEDED] $ do+ let t1=Transfer Nothing Nothing Nothing uid1 (Just uid2) (Amount "EUR" 100) (Amount "EUR" 1)+ uw1 uw2 Nothing Nothing Nothing Nothing Nothing+ t1'<-testMP $ createTransfer t1+ assertBool (isJust $ tId t1')+ assertEqual (Just $ Amount "EUR" 99) (tCreditedFunds t1')+ t2'<-testMP $ fetchTransfer (fromJust $ tId t1')+ assertEqual t1' t2'+ ts1 <- testMP $ listTransactions uw1 Nothing+ assertEqual 1 (length $ filter ((tId t1'==) . txId) $ plData ts1)+ ts2 <- testMP $ listTransactions uw2 Nothing+ assertEqual 1 (length $ filter ((tId t1'==) . txId) $ plData ts2)+ uts1 <- testMP $ listTransactionsForUser uid1 Nothing+ assertEqual 1 (length $ filter ((tId t1'==) . txId) $ plData uts1) + return $ tId t1'
+ test/mangopay-tests.hs view
@@ -0,0 +1,49 @@+{-# OPTIONS_GHC -F -pgmF htfpp #-}+-- | entry module for tests+module Main where+++import Test.Framework++import {-@ HTF_TESTS @-} Web.MangoPay.AccessTest+import {-@ HTF_TESTS @-} Web.MangoPay.UsersTest+import {-@ HTF_TESTS @-} Web.MangoPay.WalletsTest+import {-@ HTF_TESTS @-} Web.MangoPay.DocumentsTest+import {-@ HTF_TESTS @-} Web.MangoPay.PayinsTest+import {-@ HTF_TESTS @-} Web.MangoPay.CardsTest+import {-@ HTF_TESTS @-} Web.MangoPay.RefundsTest+import {-@ HTF_TESTS @-} Web.MangoPay.AccountsTest+import {-@ HTF_TESTS @-} Web.MangoPay.PayoutsTest++import Web.MangoPay.TestUtils++import Control.Exception (bracket)+import Network.HTTP.Conduit as H+import Control.Concurrent (killThread, threadDelay)+import Control.Monad.IO.Class (liftIO)+import Data.IORef (modifyIORef, readIORef)+import Test.HUnit (Assertion)+import Control.Monad (liftM)++-- | test entry point+main :: IO()+main = H.withManager (\mgr->liftIO $ do+ hook<-getHookEndPoint+ res<-newReceivedEvents+ -- initial state+ modifyIORef testState (\ts->ts{tsManager=mgr,tsHookEndPoint=hook,tsReceivedEvents=res})+ bracket + (startHTTPServer (hepPort hook) res)+ killThread+ (\_->htfMain $ htf_importedTests ++ [htf_thisModulesTests])+ )++-- | test there are no unprocessed events +test_Final :: Assertion+test_Final=do+ -- wait in case events still arrive+ threadDelay $ 5 * 1000000+ res<-liftM tsReceivedEvents $ readIORef testState + evts<-popReceivedEvents res+ assertEqual [] evts+