monzo-0.4.0.0: src/Monzo/Types.hs
--------------------------------------------------------------------------------
-- Haskell bindings for the Monzo API --
-- Written by Michael B. Gale (michael.gale@cl.cam.ac.uk) --
--------------------------------------------------------------------------------
module Monzo.Types where
--------------------------------------------------------------------------------
import GHC.Generics
import Control.Monad (mzero)
import Data.Aeson
import Data.Aeson.Types (Parser, emptyObject)
import qualified Data.HashMap.Strict as H
import qualified Data.Text as T
import Data.Time.LocalTime
import Data.Time.RFC3339
import qualified Data.Map as M
import Data.Monoid ((<>))
import Servant.API
--------------------------------------------------------------------------------
-- | Retrieve the value associated with the given key of an `Object`. The result
-- is `Nothing` if the key is not present or if it is null, or `empty` if the
-- value cannot be converted to the desired type.
(.:??) :: FromJSON a => Object -> T.Text -> Parser (Maybe a)
obj .:?? key = case H.lookup key obj of
Nothing -> pure Nothing
Just Null -> pure Nothing
Just val -> parseJSON val
--------------------------------------------------------------------------------
lookupEither :: (Show a, Eq a) => a -> [(a,b)] -> Either String b
lookupEither x xs = maybe (Left $ "could not find key " <> show x) return (lookup x xs)
eitherOr :: Eq a => a -> [(a,b)] -> Either String b -> Either String b
eitherOr x xs f = maybe f return (lookup x xs)
--------------------------------------------------------------------------------
-- | An abstract representation of timestamps used by the Mondo API.
newtype Timestamp = Timestamp { timestamp :: ZonedTime }
deriving (Show)
instance Eq Timestamp where
(Timestamp x) == (Timestamp y) =
zonedTimeToUTC x == zonedTimeToUTC y
instance ToHttpApiData Timestamp where
toQueryParam (Timestamp time) = formatTimeRFC3339 time
instance FromHttpApiData Timestamp where
parseQueryParam v = case parseTimeRFC3339 v of
Nothing -> Left "Can't parse time"
Just t -> Right $ Timestamp t
instance FromJSON Timestamp where
parseJSON (String v) = case parseTimeRFC3339 v of
Nothing -> mzero
Just t -> pure $ Timestamp t
parseJSON _ = mzero
instance ToJSON Timestamp where
toJSON (Timestamp t) = String (formatTimeRFC3339 t)
--------------------------------------------------------------------------------
data Since = SinceTime Timestamp | SinceID String
instance ToHttpApiData Since where
toQueryParam (SinceTime time) = toQueryParam time
toQueryParam (SinceID id) = T.pack id
instance FromHttpApiData Since where
parseQueryParam v = case parseTimeRFC3339 v of
Just t -> Right $ SinceTime $ Timestamp t
Nothing -> Right $ SinceID $ T.unpack v
--------------------------------------------------------------------------------
data PageOptions = PageOptions {
limit :: Maybe Int,
since :: Maybe Since,
before :: Maybe ZonedTime
}
defaultPageOptions :: PageOptions
defaultPageOptions = PageOptions {
limit = Just 100,
since = Nothing,
before = Nothing
}
--------------------------------------------------------------------------------
data Empty = Empty
instance FromJSON Empty where
parseJSON val
| val == emptyObject = pure Empty
| otherwise = mzero
instance ToJSON Empty where
toJSON Empty = object []
toUnit :: Empty -> ()
toUnit _ = ()
--------------------------------------------------------------------------------
-- | The type of account IDs.
type AccountID = String
-- | The type of transaction IDs.
type TransactionID = String
-- | The type of webhook IDs.
type WebhookID = String
-- | The type of attachment IDs.
newtype AttachmentID = AttID T.Text
deriving Show
instance FromJSON AttachmentID where
parseJSON (String v) = pure $ AttID v
parseJSON _ = mzero
instance ToJSON AttachmentID where
toJSON (AttID v) = String v
instance ToFormUrlEncoded AttachmentID where
toFormUrlEncoded (AttID v) = [("id", v)]
instance FromFormUrlEncoded AttachmentID where
fromFormUrlEncoded dict = AttID <$> lookupEither "id" dict
--------------------------------------------------------------------------------
data AccountsResponse = AccountsResponse {
accounts :: [Account]
} deriving (Show, Generic)
instance FromJSON AccountsResponse
instance ToJSON AccountsResponse
data Account = Account {
accountID :: AccountID,
accountDescription :: String,
accountCreated :: Timestamp
} deriving (Eq, Show, Generic)
instance FromJSON Account where
parseJSON (Object v) =
Account <$> v .: "id"
<*> v .: "description"
<*> v .: "created"
parseJSON _ = mzero
instance ToJSON Account where
toJSON Account{..} =
object [ "id" .= accountID
, "description" .= accountDescription
, "created" .= accountCreated
]
data Balance = Balance {
balanceValue :: Integer,
balanceCurrency :: String,
balanceSpentToday :: Integer
} deriving (Eq, Show, Generic)
instance FromJSON Balance where
parseJSON (Object v) =
Balance <$> v .: "balance"
<*> v .: "currency"
<*> v .: "spend_today"
parseJSON _ = mzero
instance ToJSON Balance where
toJSON Balance{..} =
object [ "balance" .= balanceValue
, "currency" .= balanceCurrency
, "spend_today" .= balanceSpentToday
]
--------------------------------------------------------------------------------
-- | Enumerates reasons which cause transactions to be declined.
data DeclineReason
= InsufficientFunds
| CardInactive
| CardBlocked
| Other
deriving Show
instance FromJSON DeclineReason where
parseJSON (String "INSUFFICIENT_FUNDS") = pure InsufficientFunds
parseJSON (String "CARD_INACTIVE") = pure CardInactive
parseJSON (String "CARD_BLOCKED") = pure CardBlocked
parseJSON (String "OTHER") = pure Other
parseJSON _ = mzero
instance ToJSON DeclineReason where
toJSON InsufficientFunds = String "INSUFFICIENT_FUNDS"
toJSON CardInactive = String "CARD_INACTIVE"
toJSON CardBlocked = String "CARD_BLOCKED"
toJSON Other = String "OTHER"
data Address = Address {
addrAddress :: String,
addrCity :: String,
addrCountry :: String,
addrLatitude :: Double,
addrLongitude :: Double,
addrPostcode :: String,
addrRegion :: String
} deriving Show
instance FromJSON Address where
parseJSON (Object v) =
Address <$> v .: "address"
<*> v .: "city"
<*> v .: "country"
<*> v .: "latitude"
<*> v .: "longitude"
<*> v .: "postcode"
<*> v .: "region"
parseJSON _ = mzero
instance ToJSON Address where
toJSON Address{..} =
object [ "address" .= addrAddress
, "city" .= addrCity
, "country" .= addrCountry
, "latitude" .= addrLatitude
, "longitude" .= addrLongitude
, "postcode" .= addrPostcode
, "region" .= addrRegion
]
data Merchant = Merchant {
merchantAddress :: Maybe Address,
merchantCreated :: Maybe String,
merchantGroupID :: Maybe String,
merchantID :: String,
merchantLogo :: Maybe String,
merchantEmoji :: Maybe String,
merchantName :: Maybe String,
merchantCategory :: Maybe String
} deriving Show
newMerchant :: String -> Merchant
newMerchant id = Merchant {
merchantAddress = Nothing,
merchantCreated = Nothing,
merchantGroupID = Nothing,
merchantID = id,
merchantLogo = Nothing,
merchantEmoji = Nothing,
merchantName = Nothing,
merchantCategory = Nothing
}
instance FromJSON Merchant where
parseJSON (Object v) =
Merchant <$> v .:? "address"
<*> v .:? "created"
<*> v .:? "group_id"
<*> v .: "id"
<*> v .:? "logo"
<*> v .:? "emoji"
<*> v .:? "name"
<*> v .:? "category"
parseJSON (String v) =
pure (newMerchant $ T.unpack v)
parseJSON _ =
fail "Can't parse merchant."
instance ToJSON Merchant where
toJSON Merchant{..} =
object [ "address" .= merchantAddress
, "created" .= merchantCreated
, "group_id" .= merchantGroupID
, "id" .= merchantID
, "logo" .= merchantLogo
, "emoji" .= merchantEmoji
, "name" .= merchantName
, "category" .= merchantCategory
]
data Transactions = Transactions {
transactions :: [Transaction]
} deriving (Show, Generic)
instance FromJSON Transactions
instance ToJSON Transactions
data TransactionResponse = TransactionResponse {
transaction :: Transaction
} deriving (Show, Generic)
instance FromJSON TransactionResponse
instance ToJSON TransactionResponse
-- TODO: add metadata
data Transaction
= Transaction {
transactionAccountBalance :: Integer,
transactionAmount :: Integer,
transactionCreated :: Timestamp,
transactionCurrency :: String,
transactionDescription :: String,
transactionID :: TransactionID,
transactionDeclineReason :: Maybe DeclineReason,
transactionIsLoad :: Bool,
transactionSettled :: Maybe Timestamp,
transactionCategory :: Maybe String,
transactionMerchant :: Maybe Merchant,
transactionMetadata :: M.Map String String
} deriving Show
instance FromJSON Transaction where
parseJSON (Object v) =
Transaction <$> v .: "account_balance"
<*> v .: "amount"
<*> v .: "created"
<*> v .: "currency"
<*> v .: "description"
<*> v .: "id"
<*> v .:? "decline_reason"
<*> v .: "is_load"
<*> v .:? "settled"
<*> v .:? "category"
<*> v .:?? "merchant"
<*> v .: "metadata"
parseJSON v = fail "Can't parse transaction."
instance ToJSON Transaction where
toJSON Transaction{..} =
object [ "account_balance" .= transactionAccountBalance
, "amount" .= transactionAmount
, "created" .= transactionCreated
, "currency" .= transactionCurrency
, "description" .= transactionDescription
, "id" .= transactionID
, "decline_reason" .= transactionDeclineReason
, "is_load" .= transactionIsLoad
, "settled" .= transactionSettled
, "category" .= transactionSettled
, "merchant" .= transactionMerchant
, "metadata" .= transactionMetadata
]
data Metadata = Metadata { metadata :: M.Map String String }
instance ToFormUrlEncoded Metadata where
toFormUrlEncoded (Metadata d) =
[("metadata[" <> T.pack k <> "]", T.pack v) | (k,v) <- M.toList d]
instance FromFormUrlEncoded Metadata where
fromFormUrlEncoded dict =
return $ Metadata $ M.fromList [(T.unpack k, T.unpack v) | (k,v) <- dict]
--------------------------------------------------------------------------------
data FeedItemType
= BasicItem
instance Show FeedItemType where
show BasicItem = "basic"
instance Read FeedItemType where
readsPrec _ "basic" = [(BasicItem, "")]
instance ToJSON FeedItemType where
toJSON BasicItem = String "basic"
data FeedItemParams (k :: FeedItemType)
= BasicFeedItem {
itemTitle :: String,
itemImageURL :: String,
itemBody :: Maybe String,
itemBackgroundColour :: Maybe String,
itemTitleColour :: Maybe String,
itemBodyColour :: Maybe String
}
newBasicFeedItem :: String -> String -> FeedItemParams BasicItem
newBasicFeedItem title url = BasicFeedItem {
itemTitle = title,
itemImageURL = url,
itemBody = Nothing,
itemBackgroundColour = Nothing,
itemTitleColour = Nothing,
itemBodyColour = Nothing
}
instance ToFormUrlEncoded (FeedItemParams k) where
toFormUrlEncoded BasicFeedItem{..} =
[ ( "params[title]" , T.pack itemTitle )
, ( "params[image_url]", T.pack itemImageURL )
]
instance FromFormUrlEncoded (FeedItemParams k) where
fromFormUrlEncoded dict = do
title <- lookupEither "params[title]" dict
imageURL <- lookupEither "params[image_url]" dict
return BasicFeedItem {
itemTitle = T.unpack title,
itemImageURL = T.unpack imageURL,
itemBody = Nothing,
itemBackgroundColour = Nothing,
itemTitleColour = Nothing,
itemBodyColour = Nothing
}
data FeedItem (k :: FeedItemType) = FeedItem {
itemAccountID :: AccountID,
itemType :: FeedItemType,
itemParams :: FeedItemParams k,
itemURL :: Maybe String
}
instance ToFormUrlEncoded (FeedItem k) where
toFormUrlEncoded item =
[ ( "account_id", T.pack $ itemAccountID item )
, ( "type" , T.pack $ show $ itemType item )
] ++ toFormUrlEncoded (itemParams item)
instance FromFormUrlEncoded (FeedItem k) where
fromFormUrlEncoded dict = do
accountID <- lookupEither "account_id" dict
typ <- lookupEither "type" dict
params <- fromFormUrlEncoded dict
return FeedItem {
itemAccountID = T.unpack accountID,
itemType = read $ T.unpack typ,
itemParams = params,
itemURL = Nothing
}
--------------------------------------------------------------------------------
data Webhooks = Webhooks {
webhooks :: [Webhook]
} deriving (Show, Generic)
instance FromJSON Webhooks
instance ToJSON Webhooks
data Webhook = Webhook {
webhookAccountID :: AccountID,
webhookURL :: String,
webhookID :: Maybe WebhookID
} deriving Show
instance FromJSON Webhook where
parseJSON (Object v) =
Webhook <$> v .: "account_id"
<*> v .: "url"
<*> v .: "id"
parseJSON _ = mzero
instance ToJSON Webhook where
toJSON Webhook{..} =
object [ "account_id" .= webhookAccountID
, "url" .= webhookURL
, "id" .= webhookID
]
instance ToFormUrlEncoded Webhook where
toFormUrlEncoded hook =
[ ( "account_id", T.pack $ webhookAccountID hook )
, ( "url" , T.pack $ webhookURL hook )
]
instance FromFormUrlEncoded Webhook where
fromFormUrlEncoded dict = do
acc <- lookupEither "account_id" dict
url <- lookupEither "url" dict
return Webhook {
webhookAccountID = T.unpack acc,
webhookURL = T.unpack url,
webhookID = Nothing
}
--------------------------------------------------------------------------------
data FileUploadReq = FileUploadReq {
uploadFileName :: String,
uploadFileType :: String
}
instance ToFormUrlEncoded FileUploadReq where
toFormUrlEncoded req =
[ ( "file_name", T.pack $ uploadFileName req )
, ( "file_type", T.pack $ uploadFileType req )
]
instance FromFormUrlEncoded FileUploadReq where
fromFormUrlEncoded dict = do
name <- lookupEither "file_name" dict
typ <- lookupEither "file_type" dict
return $ FileUploadReq (T.unpack name) (T.unpack typ)
data FileUploadRes = FileUploadRes {
uploadURL :: String,
uploadPostURL :: String
}
instance FromJSON FileUploadRes where
parseJSON (Object v) =
FileUploadRes <$> v .: "file_url"
<*> v .: "upload_url"
parseJSON _ = mzero
instance ToJSON FileUploadRes where
toJSON FileUploadRes{..} =
object [ "file_url" .= uploadURL
, "upload_url" .= uploadPostURL
]
--------------------------------------------------------------------------------
-- | Transaction attachments.
data Attachment = Attachment {
attachmentTransaction :: TransactionID,
attachmentFileType :: String,
attachmentURL :: String,
attachmentID :: Maybe AttachmentID,
attachmentUserID :: Maybe String,
attachmentCreated :: Maybe String
} deriving Show
instance FromJSON Attachment where
parseJSON (Object v) =
Attachment <$> v .: "external_id"
<*> v .: "file_type"
<*> v .: "file_url"
<*> v .: "id"
<*> v .: "user_id"
<*> v .: "created"
parseJSON _ = mzero
instance ToJSON Attachment where
toJSON Attachment{..} =
object [ "external_id" .= attachmentTransaction
, "file_type" .= attachmentFileType
, "file_url" .= attachmentURL
, "id" .= attachmentID
, "user_id" .= attachmentUserID
, "created" .= attachmentCreated
]
instance ToFormUrlEncoded Attachment where
toFormUrlEncoded att =
[ ( "external_id", T.pack $ attachmentTransaction att )
, ( "file_url", T.pack $ attachmentURL att )
, ( "file_type", T.pack $ attachmentFileType att )
]
instance FromFormUrlEncoded Attachment where
fromFormUrlEncoded dict = do
id <- lookupEither "external_id" dict
url <- lookupEither "file_url" dict
typ <- lookupEither "file_type" dict
return Attachment {
attachmentTransaction = T.unpack id,
attachmentURL = T.unpack url,
attachmentFileType = T.unpack typ,
attachmentID = Nothing,
attachmentUserID = Nothing,
attachmentCreated = Nothing
}
--------------------------------------------------------------------------------