@@ -8,11 +8,14 @@ import Control.Concurrent.MVar (MVar, readMVar)
88import Control.Monad.Except (MonadError, ExceptT, runExceptT, throwError)
99import Control.Monad.IO.Class
1010import Control.Monad.Reader
11+import Data.Aeson
12+import Data.Aeson.Types (typeMismatch)
1113import Data.Text (Text)
1214import Data.Text qualified as Text
1315import Data.UUID (UUID)
14−import Network.HTTP.Client (Manager, HttpException(..), HttpExceptionContent)
16+import Network.HTTP.Client
1517import Network.HTTP.Req
18+import Network.HTTP.Types
1619
1720type BearerToken = Text
1821type DeviceGuid = UUID
@@ -29,19 +32,47 @@ data Connexpay = Connexpay { logAction :: Text -> IO ()
2932 , password :: Text
3033 }
3134
32−data PaymentError = AuthFailure
33− | ParseError String
35+-- | Payment failure types.
36+-- This type describes failures that related to either credit card being invalid,
37+-- client account having insufficient funds, and other non-technical conditions.
38+-- FIXME: this list is not exhaustive. Add more values whenever we encounter them.
39+data PaymentFailure = CVVFailed -- ^ CVV verification failure
40+ | CardInvalid -- ^ Credit card details are invalid
41+ | InvalidAmount -- ^ Money amount is invalid
42+ | LocalTransaction -- ^ Special case for transactions that were registered but did't go through somehow.
43+ deriving (Eq, Show)
44+
45+-- | Guess failure type from HTTP code and supplied error string.
46+guessFailure :: Int -> Text -> Maybe PaymentFailure
47+guessFailure 422 "Error code D2020. CVV2 verification failed." = Just CVVFailed
48+guessFailure 422 "Error code D2005. Invalid Card." = Just CardInvalid
49+guessFailure 422 "Amount field don't allow a value greater than $999,999.99" = Just InvalidAmount
50+guessFailure _ _ = Nothing
51+
52+-- | Error response from Connexpay
53+data ErrorMessage = ErrorMessage { message :: Text
54+ , errorId :: Text }
55+
56+instance FromJSON ErrorMessage where
57+ parseJSON (Object o) = ErrorMessage <$> o .: "message"
58+ <*> o .: "errorId"
59+ parseJSON v = typeMismatch "ErrorMessage" v
60+
61+data PaymentError = ParseError String
3462 | InvalidUrl String String
3563 | HttpFailure HttpExceptionContent
64+ | PaymentFailure PaymentFailure
3665 deriving (Show)
3766
38−
3967newtype ConnexpayM a = ConnexpayM (ReaderT Connexpay (ExceptT PaymentError IO) a)
4068 deriving (Functor, Applicative, Monad, MonadIO, MonadReader Connexpay, MonadError PaymentError)
4169
4270instance MonadHttp ConnexpayM where
4371 handleHttpException (JsonHttpException e) = throwError (ParseError e)
4472 handleHttpException (VanillaHttpException (InvalidUrlException url why)) = throwError (InvalidUrl url why)
73+ handleHttpException (VanillaHttpException (HttpExceptionRequest _ (StatusCodeException resp bs)))
74+ | Just err <- decodeStrict @ErrorMessage bs
75+ , Just f <- guessFailure (statusCode $ responseStatus resp) err.message = throwError (PaymentFailure f)
4576 handleHttpException (VanillaHttpException (HttpExceptionRequest _ c)) = throwError (HttpFailure c)
4677
4778 getHttpConfig = do mgr <- asks (.manager)