git.iko.soy/typeable/connexpay
on master help sign in
tmp
commit8f541e0da60e58b6f1c3071d6cd76f843d602cd5 authorNick <nick@typeable.io> dateFri 22 Sep 2023, 10:22 (3 years ago) parentade5e0e
5 files changed+96−42
connexpay.cabal+1 src/Tool.hs+41−5 src/Web/Connexpay.hs+1 src/Web/Connexpay/Data.hs+27−26 src/Web/Connexpay/Payments.hs+26−11
connexpay.cabal+1
@@ -58,6 +58,7 @@ executable connexpay-tool
5858 , http-client
5959 , http-client-tls
6060 , mtl
61+ , optparse-applicative
6162 , req
6263 , text
6364 , uuid
src/Tool.hs+41−5
@@ -1,14 +1,19 @@
1+{-# LANGUAGE ApplicativeDo #-}
12{-# LANGUAGE OverloadedStrings #-}
23module Main where
34
5+import Control.Monad
6+import Control.Monad.IO.Class
47import Data.Aeson
8+import Data.Fixed
9+import Data.Money
510import Data.Text (Text)
611import Data.Text.IO qualified as Text
712import Data.UUID
8−import Data.Yaml
13+import Data.Yaml (decodeFileThrow)
914import GHC.Generics
1015import Network.HTTP.Client.TLS
11−import System.Environment
16+import Options.Applicative
1217import Web.Connexpay
1318
1419data Config = Config { login :: Text
@@ -19,14 +24,45 @@ data Config = Config { login :: Text
1924
2025instance FromJSON Config
2126
27+data Command = AuthSale CreditCard Centi
28+ | VoidSale SaleGuid
29+ | CaptureSale SaleGuid
30+
31+data CmdLine = CmdLine { configPath :: FilePath
32+ , operation :: Command
33+ }
34+
35+cmdParser :: Parser CmdLine
36+cmdParser = CmdLine <$> strOption (short 'c' <> metavar "FILE" <> help "Configuration file path")
37+ <*> subparser operation
38+ where operation = command "auth" (info (AuthSale <$> cc <*> amt) (progDesc "Authorise payment"))
39+ <> command "void" (info (VoidSale <$> guid) (progDesc "Void payment"))
40+ <> command "capture" (info (CaptureSale <$> guid) (progDesc "Void payment"))
41+ amt = argument auto (metavar "Payment amount")
42+ cc = CreditCard <$> argument str mempty
43+ <*> fmap pure (argument str mempty)
44+ <*> argument expdate mempty
45+ <*> fmap pure (argument str mempty)
46+ expdate = do s <- str
47+ guard (length s == 4)
48+ pure (read (take 2 s), read (drop 2 s))
49+ guid = argument auto (metavar "Payment UUID")
50+
51+
52+
53+
2254writeLog :: Text -> IO ()
2355writeLog msg = Text.putStrLn ("Connexpay log: " <> msg)
2456
2557main :: IO ()
26−main = do cmdLine <- getArgs
27− cnf :: Config <- decodeFileThrow (head cmdLine)
58+main = do cmdLine <- execParser (info cmdParser mempty)
59+ cnf :: Config <- decodeFileThrow cmdLine.configPath
2860 mgr <- newTlsManager
2961 res <- initConnexpay writeLog mgr cnf.device_guid cnf.host cnf.login cnf.password
3062 case res of
3163 Left err -> putStrLn ("Error: " <> show err)
32− Right _ -> pure ()
64+ Right cpi -> print =<< runConnexpay cpi (doThing cmdLine.operation)
65+
66+doThing :: Command -> ConnexpayM ()
67+doThing (AuthSale cc amt) = liftIO . print =<< authorisePayment cc usd
68+ where usd = Money amt
src/Web/Connexpay.hs+1
@@ -2,4 +2,5 @@ module Web.Connexpay ( module Export
22 ) where
33
44import Web.Connexpay.Init as Export
5+import Web.Connexpay.Payments as Export
56import Web.Connexpay.Types as Export
src/Web/Connexpay/Data.hs+27−26
@@ -1,31 +1,32 @@
1−module Web.Connexpay.Data where
1+{-# LANGUAGE OverloadedStrings #-}
2+module Web.Connexpay.Data ( TransactionStatus (..)
3+ ) where
24
3−import Web.Connexpay.Types
4−
5−import Data.Money (Money, USD)
5+import Data.Aeson
6+import Data.Aeson.Types
7+import Data.Maybe (fromMaybe)
68import Data.Text (Text)
79
8−-- | Create sale request type.
9−-- This is far from the complete description, but should be enough for our cause.
10−-- See documentation here: https://docs.connexpay.com/reference/getting-started-with-your-api
11−data CreateSale = CreateSale { deviceGuid :: DeviceGuid
12− , amount :: Money USD
13− , sequenceCode :: Text
14− , orderCode :: Text
15− , statementDesc :: Maybe Text
16− } deriving (Show)
17−
18−data CreateSaleResponse = CreateSaleResponse { -- Fill this later as needed
19− } deriving (Show)
10+-- | Transaction status in Connexpay
11+-- The list is taken from https://docs.connexpay.com/reference/search-sales
12+data TransactionStatus = TransactionApproved -- ^ Obvious
13+ | TransactionDeclined -- ^ Also obvious
14+ | TransactionCreatedLocal -- ^ Seems to only exist in a test environment, indicates success.
15+ | TransactionCreatedProcNotReached -- ^ Communication error between Connexpay and Card Processor
16+ | TransactionCreatedProcError -- ^ Processor errored out
17+ | TransactionApprovedWarning -- ^ Wut 0__o FIXME: figure out what this is
18+ | TransactionOther Text -- ^ In case they return something unexpected
19+ deriving (Show)
2020
21−-- | Void a previously create sale
22−-- Documentation: https://docs.connexpay.com/reference/void
23−data VoidSale = VoidSale { deviceGuid :: DeviceGuid
24− , saleGuid :: SaleGuid
25− , reason :: Maybe Text
26− , amount :: Maybe (Money USD)
27− , sequenceCode :: Text
28− } deriving (Show)
21+statuses :: [(Text, TransactionStatus)]
22+statuses = [ ( "Transaction - Approved", TransactionApproved )
23+ , ( "Transaction - Declined", TransactionDeclined )
24+ , ( "Transaction - CreatedLocal", TransactionCreatedLocal )
25+ , ( "Transaction - Created - Error: Processor not reached", TransactionCreatedProcNotReached )
26+ , ( "Transaction - Processor Error", TransactionCreatedProcError )
27+ , ( "Transaction - Approved - Warning", TransactionApprovedWarning )
28+ ]
2929
30−data VoidSaleResponse = VoidSaleResponse { -- Fill this later as needed
31− } deriving (Show)
30+instance FromJSON TransactionStatus where
31+ parseJSON (String s) = pure (fromMaybe (TransactionOther s) (lookup s statuses))
32+ parseJSON v = typeMismatch "TransactionStatus" v
src/Web/Connexpay/Payments.hs+26−11
@@ -1,19 +1,24 @@
11{-# LANGUAGE DataKinds #-}
22{-# LANGUAGE OverloadedStrings #-}
33{-# LANGUAGE UndecidableInstances #-}
4−module Web.Connexpay.Payments where
4+module Web.Connexpay.Payments ( CreditCard(..)
5+ , AuthResponse(..)
6+ , authorisePayment
7+ ) where
58
69import Control.Monad.Reader (asks)
710import Control.Monad.Writer.Strict
811import Data.Aeson
12+import Data.Aeson.Types (typeMismatch)
913import Data.Money
1014import Data.Text (Text)
1115import Data.Text qualified as Text
1216import Data.Text.Encoding qualified as Text
1317import GHC.TypeError as TypeError
1418import Network.HTTP.Req
19+
20+import Web.Connexpay.Data
1521import Web.Connexpay.Types
16−import Data.Aeson.Types (typeMismatch)
1722
1823-- | Credit card info
1924-- No 'Show' instance should be made for this type
@@ -30,13 +35,19 @@ instance TypeError ShowError => Show CreditCard where
3035 show = error "UNREACHABLE"
3136
3237instance ToJSON CreditCard where
33− toJSON cc = object $ execWriter $ do tell ["CardNumber" .= cc.number]
34− whenJust cc.cardholder $ \name ->
35− tell ["CardHolderName" .= name]
36− whenJust cc.cvv $ \cvv ->
37− tell ["Cvv2" .= cvv]
38− let expDate = tshow (snd cc.expiration) <> tshow (fst cc.expiration)
39− tell ["ExpirationDate" .= expDate]
38+ toJSON cc = object
39+ $ execWriter
40+ $ do tell ["CardNumber" .= cc.number]
41+ whenJust cc.cardholder $ \name ->
42+ tell ["CardHolderName" .= name]
43+ whenJust cc.cvv $ \cvv ->
44+ tell ["Cvv2" .= cvv]
45+ let expDate = padDate (tshow (snd cc.expiration)) <> padDate (tshow (fst cc.expiration))
46+ tell ["ExpirationDate" .= expDate]
47+
48+padDate :: Text -> Text
49+padDate t | Text.length t == 1 = "0" <> t
50+ | otherwise = t
4051
4152whenJust :: Monad m => Maybe a -> (a -> m ()) -> m ()
4253whenJust (Just x) f = f x
@@ -46,12 +57,16 @@ tshow :: Show a => a -> Text
4657tshow = Text.pack . show
4758
4859data AuthResponse = AuthResponse { paymentGuid :: Text
49− , status :: Text
50− }
60+ , status :: TransactionStatus
61+ , processorStatusCode :: Maybe Text
62+ , processorMessage :: Maybe Text
63+ } deriving (Show)
5164
5265instance FromJSON AuthResponse where
5366 parseJSON (Object o) = AuthResponse <$> o .: "guid"
5467 <*> o .: "status"
68+ <*> o .:? "processorStatusCode"
69+ <*> o .:? "processorResponseMessage"
5570 parseJSON v = typeMismatch "AuthReponse" v
5671
5772authorisePayment :: CreditCard -> Money USD -> ConnexpayM AuthResponse