packages feed

plaid (empty) → 0.1.0.0

raw patch · 29 files changed

+2813/−0 lines, 29 filesdep +QuickCheckdep +aesondep +basesetup-changed

Dependencies added: QuickCheck, aeson, base, bytestring, casing, conduit, conduit-extra, containers, either, errors, hspec, hspec-wai, hspec-wai-json, http-client, http-client-tls, http-conduit, http-types, microlens, microlens-th, mtl, network, plaid, pretty-simple, raw-strings-qq, safe-exceptions, text, time, transformers, wai

Files

+ ChangeLog.md view
@@ -0,0 +1,3 @@+# Changelog for plaid++## Unreleased changes
+ Data/Api/Accounts.hs view
@@ -0,0 +1,27 @@+-- |+--+-- Module      :  Data.Api.Product.Accounts+-- Copyright   :  2019 Sasha Bogicevic+-- License     :  BSD 3 clause+--+-- Maintainer  :  Sasha Bogicevic <sasa.bogicevic@pm.me>+-- Stability   :  experimental+-- Portability :  portable+--+module Data.Api.Accounts+  ( plaidGetBalance+  ) where++import           Data.Api.Types (executePost)+import           Data.Common++-- | Get the account balance+-- http://plaid.com/docs/#retrieve-balance-request+plaidGetBalance+  :: ( MonadReader PlaidEnv m+     , PlaidHttp m)+  => PlaidBody GetBalance+  -> m ByteString+plaidGetBalance body = do+  env <- ask+  executePost (envUrl (env ^. plaidEnvEnvironment) <> "/accounts/balance/get") body
+ Data/Api/Auth.hs view
@@ -0,0 +1,17 @@+module Data.Api.Auth+  ( plaidGetAuth+  ) where++import           Data.Api.Types (executePost)+import           Data.Common++plaidGetAuth+  :: ( MonadReader PlaidEnv m+     , MonadThrow m+     , PlaidHttp m+     )+  => PlaidBody AuthGet+  -> m ByteString+plaidGetAuth body = do+  env <- ask+  executePost (envUrl (env ^. plaidEnvEnvironment) <> "/auth/get") body
+ Data/Api/Helper.hs view
@@ -0,0 +1,26 @@+{-# LANGUAGE ScopedTypeVariables #-}++module Data.Api.Helper+  ( parseOrFail+  ) where+++import           Control.Exception.Safe (MonadThrow, throwM)+import           Data.Aeson (FromJSON, eitherDecode)+import           Data.Api.Types+import qualified Data.ByteString.Lazy as BSL+import qualified Data.Text as T++-- | Try to parse any plaid response to appropriate type or fail+parseOrFail+  :: forall b m.+     ( MonadThrow m+     , PlaidHttp m+     , FromJSON b)+  => BSL.ByteString+  -> m b+parseOrFail bs = do+  let edecoded = eitherDecode bs :: Either String b+  case edecoded of+    Left e        -> throwM (PlaidError $ T.pack e)+    Right decoded -> return decoded
+ Data/Api/Identity.hs view
@@ -0,0 +1,17 @@+module Data.Api.Identity+  ( plaidGetIdentity+  ) where++import           Data.Common++plaidGetIdentity+  :: ( MonadReader PlaidEnv m+     , MonadThrow m+     , PlaidHttp m+     )+  => PlaidBody PlaidIdentityGet+  -> m ByteString+plaidGetIdentity body = do+  env <- ask+  let url = envUrl (env ^. plaidEnvEnvironment)+  executePost (url <> "/identity/get") body
+ Data/Api/Income.hs view
@@ -0,0 +1,17 @@+module Data.Api.Income+  ( plaidGetIncome+  ) where++import           Data.Common++plaidGetIncome+  :: ( MonadReader PlaidEnv m+     , MonadThrow m+     , PlaidHttp m+     )+  => PlaidBody PlaidIncomeGet+  -> m ByteString+plaidGetIncome body = do+  env <- ask+  let url = envUrl (env ^. plaidEnvEnvironment)+  executePost (url <> "/income/get") body
+ Data/Api/InternalPure.hs view
@@ -0,0 +1,131 @@+module Data.Api.InternalPure+  ( mkExchangePublicTokenEnv+  , mkCreatePublicTokenEnv+  , mkGetBalanceEnv+  , mkGetAuthEnv+  , mkCreateTransactionsGetEnv+  , mkCreateIdentityGetEnv+  , mkCreateIncomeGetEnv+  ) where++import           Data.Common+import           Data.Proof.Proof+import           Data.Time        (Day)++mkGetAuthEnv+  :: PlaidEnv+  -> AccessToken+  -> Either PlaidError (PlaidBody AuthGet)+mkGetAuthEnv env atoken =+  name env (\namedEnv ->+    name atoken (\namedAccessToken ->+      let proof = proveAuthGetBody namedEnv namedAccessToken+          body = mempty & plaidBodyEnv .~ env+                        & plaidBodyAccessToken ?~ atoken+      in either (Left . PlaidError) (const (Right body)) proof+  ))++mkGetBalanceEnv+  :: PlaidEnv+  -> AccessToken+  -> Either PlaidError (PlaidBody GetBalance)+mkGetBalanceEnv env atoken =+  name env (\namedEnv ->+    name atoken (\namedAccessToken ->+      let proof = proveGetBalanceBody namedEnv namedAccessToken+          body = mempty & plaidBodyEnv .~ env+                        & plaidBodyAccessToken ?~ atoken+       in either (Left . PlaidError) (const (return body)) proof+  ))++-- | Creates a Public Token+-- https://plaid.com/docs/#exchange-token-flow+mkCreatePublicTokenEnv+  :: PlaidEnv+  -> InstitutionId+  -> [PlaidProduct]+  -> Maybe PlaidOptions+  -> Either PlaidError (PlaidBody PublicTokenCreate)+mkCreatePublicTokenEnv env instId products options =+  name env (\namedEnv ->+    name instId (\namedInstId ->+      name products (\namedProducts ->+        name options (\namedOptions ->+          let proof = proveCreatePublicTokenBody namedEnv namedInstId namedProducts namedOptions+              body = mempty & plaidBodyEnv .~ env+                            & plaidBodyOptions .~ options+                            & plaidBodyInstitutionId ?~ instId+                            & plaidBodyInitialProducts ?~ products+           in either (Left . PlaidError) (const (Right body)) proof+  ))))++mkExchangePublicTokenEnv+  :: PlaidEnv+  -> PublicToken+  -> Either PlaidError (PlaidBody PlaidTokenExchange)+mkExchangePublicTokenEnv _ (PublicToken "") = Left $ PlaidError "PublicToken cannot be empty"+mkExchangePublicTokenEnv env publicToken =+  Right $ mempty & plaidBodyEnv .~ env+                 & plaidBodyPublicToken ?~ publicToken+++-- | Creates request body for /transactions/get call+-- https://plaid.com/docs/#retrieve-transactions-request+mkCreateTransactionsGetEnv+  :: PlaidEnv+  -> AccessToken+  -> Day+  -> Day+  -> Maybe PlaidOptions+  -> Maybe PlaidPaginationOptions+  -> Either PlaidError (PlaidBody PlaidTransactionsGet)+mkCreateTransactionsGetEnv env accessToken startDate endDate options paginationOptions =+  name env (\namedEnv ->+    name accessToken (\namedAccessToken ->+      name startDate (\namedStartDate ->+        name endDate (\namedEndDate ->+          name options (\namedOptions ->+            name paginationOptions (\namedPaginationOptions ->+              let proof =+                    proveTransactionsGetBody namedEnv namedAccessToken namedStartDate namedEndDate namedOptions namedPaginationOptions+                  body = mempty & plaidBodyEnv .~ env+                                & plaidBodyAccessToken ?~ accessToken+                                & plaidBodyOptions .~ options+                                & plaidBodyStartDate ?~ startDate+                                & plaidBodyEndDate ?~ endDate+                                & plaidBodyPaginationOptions ?~ defPaginationOptions++              in either (Left . PlaidError) (const (Right body)) proof+  ))))))++-- | Creates request body for /identity/get call+-- https://plaid.com/docs/#identity+mkCreateIdentityGetEnv+  :: PlaidEnv+  -> AccessToken+  -> Either PlaidError (PlaidBody PlaidIdentityGet)+mkCreateIdentityGetEnv env accessToken =+  name env (\namedEnv ->+    name accessToken (\namedAccessToken ->+      let proof = proveIdentityGetBody namedEnv namedAccessToken+          body = mempty & plaidBodyEnv .~ env+                        & plaidBodyAccessToken ?~ accessToken++      in either (Left . PlaidError) (const (Right body)) proof+  ))++-- | Creates request body for /income/get call+-- https://plaid.com/docs/#income+mkCreateIncomeGetEnv+  :: PlaidEnv+  -> AccessToken+  -> Either PlaidError (PlaidBody PlaidIncomeGet)+mkCreateIncomeGetEnv env accessToken =+  name env (\namedEnv ->+    name accessToken (\namedAccessToken ->+      let proof = proveIncomeGetBody namedEnv namedAccessToken+          body = mempty & plaidBodyEnv .~ env+                        & plaidBodyAccessToken ?~ accessToken++      in either (Left . PlaidError) (const (Right body)) proof+  ))
+ Data/Api/Link.hs view
@@ -0,0 +1,48 @@+-- |+--+-- Module      :  Data.Api.Link.Link+-- Copyright   :  2019 Sasha Bogicevic+-- License     :  BSD 3 clause+--+-- Maintainer  :  Sasha Bogicevic <sasa.bogicevic@pm.me>+-- Stability   :  experimental+-- Portability :  portable+--+module Data.Api.Link+  ( plaidCreatePublicToken+  , plaidExchangeToken+  ) where++import           Data.Api.Types (executePost)+import           Data.Common++-- | Creates a public token+-- that can be used in further interaction with plaid.com+-- https://plaid.com/docs/#creating-public-tokens+-- returns PlaidPublicTokenResponse or fails with PlaidError+plaidCreatePublicToken+  :: ( MonadReader PlaidEnv m+     , MonadThrow m+     , PlaidHttp m+     )+  => PlaidBody PublicTokenCreate+  -> m PlaidPublicTokenResponse+plaidCreatePublicToken body = do+  env <- ask+  response <- executePost (envUrl (env ^. plaidEnvEnvironment) <> "/sandbox/public_token/create") body+  parseOrFail response++-- | Exchange a public token to access token.+-- Public token is invalidated after the exchange.+-- https://plaid.com/docs/#exchange-token-flow+-- returns PlaidAccessTokenResponse or fails with PlaidError+plaidExchangeToken+  :: ( MonadReader PlaidEnv m+     , MonadThrow m+     , PlaidHttp m)+  => PlaidBody PlaidTokenExchange+  -> m PlaidAccessTokenResponse+plaidExchangeToken body = do+  env <- ask+  response <- executePost (envUrl (env ^. plaidEnvEnvironment) <> "/item/public_token/exchange") body+  parseOrFail response
+ Data/Api/Plaid.hs view
@@ -0,0 +1,26 @@+module Data.Api.Plaid+  ( mkExchangePublicTokenEnv+  , mkCreatePublicTokenEnv+  , mkGetBalanceEnv+  , mkGetAuthEnv+  , mkCreateTransactionsGetEnv+  , mkCreateIdentityGetEnv+  , mkCreateIncomeGetEnv+  , plaidCreatePublicToken+  , plaidExchangeToken+  , plaidGetAuth+  , plaidGetBalance+  , plaidGetTransactions+  , plaidGetIdentity+  , plaidGetIncome+  ) where++import           Data.Api.Accounts     (plaidGetBalance)+import           Data.Api.Auth         (plaidGetAuth)+import           Data.Api.Identity     (plaidGetIdentity)+import           Data.Api.Income       (plaidGetIncome)+import           Data.Api.InternalPure (mkCreateIdentityGetEnv, mkCreateIncomeGetEnv,+                                        mkCreatePublicTokenEnv, mkCreateTransactionsGetEnv,+                                        mkExchangePublicTokenEnv, mkGetAuthEnv, mkGetBalanceEnv)+import           Data.Api.Link         (plaidCreatePublicToken, plaidExchangeToken)+import           Data.Api.Transactions (plaidGetTransactions)
+ Data/Api/TestByteStrings.hs view
@@ -0,0 +1,880 @@+{-# LANGUAGE QuasiQuotes #-}++module Data.Api.TestByteStrings where++import           Data.ByteString.Lazy (ByteString)+import           Text.RawString.QQ    (r)++responseAuthGet :: ByteString+responseAuthGet = [r|{"accounts":[{"account_id":"vzeNDwK7KQIm4yEog683uElbp9GRLEFXGK98D","balances":{"available":100,"current":110,"limit":null,"iso_currency_code":"USD","unofficial_currency_code":null},"mask":"9606","name":"Plaid Checking","official_name":"Plaid Gold Checking","subtype":"checking","type":"depository"}],"numbers":{"ach":[{"account":"9900009606","account_id":"vzeNDwK7KQIm4yEog683uElbp9GRLEFXGK98D","routing":"011401533","wire_routing":"021000021"}],"eft":[{"account":"111122223333","account_id":"vzeNDwK7KQIm4yEog683uElbp9GRLEFXGK98D","institution":"021","branch":"01140"}],"international":[{"account_id":"vzeNDwK7KQIm4yEog683uElbp9GRLEFXGK98D","bic":"NWBKGB21","iban":"GB29NWBK60161331926819"}],"bacs":[{"account":"31926819","account_id":"vzeNDwK7KQIm4yEog683uElbp9GRLEFXGK98D","sort_code":"601613"}]},"item":{"available_products":["assets","balance","credit_details","identity","income","investments","liabilities"],"billed_products":["auth","transactions"],"error": null,"institution_id":"ins_3","item_id":"MylwooVBojuBwgzNVdZ6FDLRnElJN3C9WPd49","webhook":"http://bogicevicsasa.com"},"request_id":"m8MDnv9okwxFNBV"} |]++responseTransactionsGet :: ByteString+responseTransactionsGet = [r|{"accounts":[{"account_id":"vokyE5Rn6vHKqDLRXEn5fne7LwbKPLIXGK98d","balances":{"available":100,"current":110,"limit":null,"iso_currency_code":"USD","unofficial_currency_code":null},"mask":"9606","name":"Plaid Checking","official_name":"Plaid Gold Checking","subtype":"checking","type":"depository"}], "transactions": [{"account_id": "vokyE5Rn6vHKqDLRXEn5fne7LwbKPLIXGK98d", "amount": 2307.21, "iso_currency_code": "USD", "unofficial_currency_code": null, "category": ["Shops", "Computers and Electronics"], "category_id": "19013000", "date": "2017-01-29", "location": {"address": "300 Post St", "city": "San Francisco", "region": "CA", "postal_code": "94108", "country": "US", "lat": null, "lon": null}, "name": "Apple Store", "payment_meta": { "reference_number":null, "ppd_id":null, "payee":null}, "pending": false, "pending_transaction_id": null, "account_owner": null, "transaction_id": "lPNjeW1nR6CDn5okmGQ6hEpMo4lLNoSrzqDje", "transaction_type": "place"}, {"account_id": "XA96y1wW3xS7wKyEdbRzFkpZov6x1ohxMXwep", "amount": 78.5, "iso_currency_code": "USD", "unofficial_currency_code": null, "category": ["Food and Drink", "Restaurants"], "category_id": "13005000", "date": "2017-01-29", "location": {"address": "262 W 15th St", "city": "New York", "region": "NY", "postal_code": "10011", "country": "US", "lat": 40.740352, "lon": -74.001761}, "name": "Golden Crepes", "payment_meta": { "reference_number":null, "ppd_id":null, "payee":null}, "pending": false, "pending_transaction_id": null, "account_owner": null, "transaction_id": "4WPD9vV5A1cogJwyQ5kVFB3vPEmpXPS3qvjXQ", "transaction_type": "place"}], "item": {"available_products":["assets","balance","credit_details","identity","income","investments","liabilities"],"billed_products":["auth","transactions"],"error": null,"institution_id":"ins_3","item_id":"MylwooVBojuBwgzNVdZ6jFDLRnElJN3C9WPd49","webhook":"http://bogicevicsasa.com"}, "total_transactions": 100, "request_id": "45QSn"}|]++responsePublicTokenCreate :: ByteString+responsePublicTokenCreate = [r|+   {"public_token":"public-sandbox-b0e2c4ee-a763-4df5-bfe9-46a46bce993d","request_id":"Aim3b"}+|]++responsePublicTokenExchange :: ByteString+responsePublicTokenExchange = [r|+  {"access_token":"access-sandbox-de3ce8ef-33f8-452c-a685-8671031fc0f6","item_id":"M5eVJqLnv3tbzdngLDp9FL5OlDNxlNhlE55op","request_id":"Aim3b"}+|]++balanceJson :: ByteString+balanceJson = [r|{"available":100,"current":110,"limit":null,"iso_currency_code":"USD","unofficial_currency_code":null}|]++accountJson :: ByteString+accountJson = [r|{"account_id":"vzeNDwK7KQIm4yEog683uElbp9GRLEFXGK98D","balances":{"available":100,"current":110,"limit":null,"iso_currency_code":"USD","unofficial_currency_code":null},"mask":"9606","name":"Plaid Checking","official_name":"Plaid Gold Checking","subtype":"checking","type":"depository"}|]++accountListJson :: ByteString+accountListJson = [r|[{"account_id":"vokyE5Rn6vHKqDLRXEn5fne7LwbKPLIXGK98d","balances":{"available":100,"current":110,"limit":null,"iso_currency_code":"USD","unofficial_currency_code":null},"mask":"9606","name":"Plaid Checking","official_name":"Plaid Gold Checking","subtype":"checking","type":"depository"}]|]++paymentMetaJson :: ByteString+paymentMetaJson = [r|{"reference_number":null, "ppd_id":null, "payee":null}|]++itemJson :: ByteString+itemJson = [r|{"available_products": ["assets","balance","credit_details","identity","income","investments","liabilities"], "billed_products":["auth","transactions"], "error": null, "institution_id":"ins_3", "item_id":"MylwooVBojuBwgzNVdZ6jFDLRnElJN3C9WPd49", "webhook":"http://bogicevicsasa.com"}|]++transactionJson :: ByteString+transactionJson = [r|{"account_id": "vokyE5Rn6vHKqDLRXEn5fne7LwbKPLIXGK98d", "amount": 2307.21, "iso_currency_code": "USD", "unofficial_currency_code": null, "category": ["Shops", "Computers and Electronics"], "category_id": "19013000", "date": "2017-01-29", "location": {"address": "300 Post St", "city": "San Francisco", "region": "CA", "postal_code": "94108", "country": "US", "lat": null, "lon": null}, "name": "Apple Store", "payment_meta": { "reference_number":null, "ppd_id":null, "payee":null}, "pending": false, "pending_transaction_id": null, "account_owner": null, "transaction_id": "lPNjeW1nR6CDn5okmGQ6hEpMo4lLNoSrzqDje", "transaction_type": "place"}|]++transactionLocationJson :: ByteString+transactionLocationJson = [r|{"address": "300 Post St", "city": "San Francisco", "region": "CA", "postal_code": "94108", "country": "US", "lat": null, "lon": null}|]++emailJson :: ByteString+emailJson = [r|+  {+    "data": "accountholder0@example.com",+    "primary": true,+    "type": "primary"+  }+|]++phoneNumberJson :: ByteString+phoneNumberJson = [r|+  {+    "data": "1112223333",+    "primary": false,+    "type": "home"+  }+|]++addressesJson :: ByteString+addressesJson = [r|+  {+    "data": {+      "city": "Malakoff",+      "country": null,+      "postal_code": "14236",+      "region": "NY",+      "street": "2992 Cameron Road"+    },+    "primary": true+  }+|]++ownersJson :: ByteString+ownersJson = [r|+  {+    "addresses": [+      {+        "data": {+          "city": "Malakoff",+          "country": null,+          "postal_code": "14236",+          "region": "NY",+          "street": "2992 Cameron Road"+        },+        "primary": true+      },+      {+        "data": {+          "city": "San Matias",+          "country": null,+          "postal_code": "93405-2255",+          "region": "CA",+          "street": "2493 Leisure Lane"+        },+        "primary": false+      }+    ],+    "emails": [+      {+        "data": "accountholder0@example.com",+        "primary": true,+        "type": "primary"+      },+      {+        "data": "accountholder1@example.com",+        "primary": false,+        "type": "secondary"+      },+      {+        "data": "extraordinarily.long.email.username.123",+        "primary": false,+        "type": "other"+      }+    ],+    "names": [+      "Alberta Bobbeth Charleson"+    ],+    "phone_numbers": [+      {+        "data": "1112223333",+        "primary": false,+        "type": "home"+      },+      {+        "data": "1112224444",+        "primary": false,+        "type": "work"+      },+      {+        "data": "1112225555",+        "primary": false,+        "type": "mobile1"+      }+    ]+  }+|]++accountsJson :: ByteString+accountsJson = [r|+{+     "account_id": "9xPwwnJGpDI8Qby6ElmEuk1jGDmjlqcRWwLAz",+     "balances": {+       "available": 100,+       "current": 110,+       "iso_currency_code": "USD",+       "limit": null,+       "unofficial_currency_code": null+     },+     "mask": "0000",+     "name": "Plaid Checking",+     "official_name": "Plaid Gold Standard 0% Interest Checki",+     "owners": [+       {+         "addresses": [+           {+             "data": {+               "city": "Malakoff",+               "country": null,+               "postal_code": "14236",+               "region": "NY",+               "street": "2992 Cameron Road"+             },+             "primary": true+           },+           {+             "data": {+               "city": "San Matias",+               "country": null,+               "postal_code": "93405-2255",+               "region": "CA",+               "street": "2493 Leisure Lane"+             },+             "primary": false+           }+         ],+         "emails": [+           {+             "data": "accountholder0@example.com",+             "primary": true,+             "type": "primary"+           },+           {+             "data": "accountholder1@example.com",+             "primary": false,+             "type": "secondary"+           },+           {+             "data": "extraordinarily.long.email.username.123",+             "primary": false,+             "type": "other"+           }+         ],+         "names": [+           "Alberta Bobbeth Charleson"+         ],+         "phone_numbers": [+           {+             "data": "1112223333",+             "primary": false,+             "type": "home"+           },+           {+             "data": "1112224444",+             "primary": false,+             "type": "work"+           },+           {+             "data": "1112225555",+             "primary": false,+             "type": "mobile1"+           }+         ]+       }+     ],+     "subtype": "checking",+     "type": "depository"+   }+|]++identityJson :: ByteString+identityJson = [r|+{+ "accounts": [+   {+     "account_id": "9xPwwnJGpDI8Qby6ElmEuk1jGDmjlqcRWwLAz",+     "balances": {+       "available": 100,+       "current": 110,+       "iso_currency_code": "USD",+       "limit": null,+       "unofficial_currency_code": null+     },+     "mask": "0000",+     "name": "Plaid Checking",+     "official_name": "Plaid Gold Standard 0% Interest Checki",+     "owners": [+       {+         "addresses": [+           {+             "data": {+               "city": "Malakoff",+               "country": null,+               "postal_code": "14236",+               "region": "NY",+               "street": "2992 Cameron Road"+             },+             "primary": true+           },+           {+             "data": {+               "city": "San Matias",+               "country": null,+               "postal_code": "93405-2255",+               "region": "CA",+               "street": "2493 Leisure Lane"+             },+             "primary": false+           }+         ],+         "emails": [+           {+             "data": "accountholder0@example.com",+             "primary": true,+             "type": "primary"+           },+           {+             "data": "accountholder1@example.com",+             "primary": false,+             "type": "secondary"+           },+           {+             "data": "extraordinarily.long.email.username.123",+             "primary": false,+             "type": "other"+           }+         ],+         "names": [+           "Alberta Bobbeth Charleson"+         ],+         "phone_numbers": [+           {+             "data": "1112223333",+             "primary": false,+             "type": "home"+           },+           {+             "data": "1112224444",+             "primary": false,+             "type": "work"+           },+           {+             "data": "1112225555",+             "primary": false,+             "type": "mobile1"+           }+         ]+       }+     ],+     "subtype": "checking",+     "type": "depository"+   },+   {+     "account_id": "v4xRRVvpzyTlBMpaDjQDFWAGVXNGadSW3BjNo",+     "balances": {+       "available": 200,+       "current": 210,+       "iso_currency_code": "USD",+       "limit": null,+       "unofficial_currency_code": null+     },+     "mask": "1111",+     "name": "Plaid Saving",+     "official_name": "Plaid Silver Standard 0.1% Interest Sa",+     "owners": [+       {+         "addresses": [+           {+             "data": {+               "city": "Malakoff",+               "country": null,+               "postal_code": "14236",+               "region": "NY",+               "street": "2992 Cameron Road"+             },+             "primary": true+           },+           {+             "data": {+               "city": "San Matias",+               "country": null,+               "postal_code": "93405-2255",+               "region": "CA",+               "street": "2493 Leisure Lane"+             },+             "primary": false+           }+         ],+         "emails": [+           {+             "data": "accountholder0@example.com",+             "primary": true,+             "type": "primary"+           },+           {+             "data": "accountholder1@example.com",+             "primary": false,+             "type": "secondary"+           },+           {+             "data": "extraordinarily.long.email.username.123",+             "primary": false,+             "type": "other"+           }+         ],+         "names": [+           "Alberta Bobbeth Charleson"+         ],+         "phone_numbers": [+           {+             "data": "1112223333",+             "primary": false,+             "type": "home"+           },+           {+             "data": "1112224444",+             "primary": false,+             "type": "work"+           },+           {+             "data": "1112225555",+             "primary": false,+             "type": "mobile1"+           }+         ]+       }+     ],+     "subtype": "savings",+     "type": "depository"+   },+   {+     "account_id": "Rrz11gqRL6SVljzWGJkGI7apezqpJbhRvWKDR",+     "balances": {+       "available": null,+       "current": 1000,+       "iso_currency_code": "USD",+       "limit": null,+       "unofficial_currency_code": null+     },+     "mask": "2222",+     "name": "Plaid CD",+     "official_name": "Plaid Bronze Standard 0.2% Interest CD",+     "owners": [+       {+         "addresses": [+           {+             "data": {+               "city": "Malakoff",+               "country": null,+               "postal_code": "14236",+               "region": "NY",+               "street": "2992 Cameron Road"+             },+             "primary": true+           },+           {+             "data": {+               "city": "San Matias",+               "country": null,+               "postal_code": "93405-2255",+               "region": "CA",+               "street": "2493 Leisure Lane"+             },+             "primary": false+           }+         ],+         "emails": [+           {+             "data": "accountholder0@example.com",+             "primary": true,+             "type": "primary"+           },+           {+             "data": "accountholder1@example.com",+             "primary": false,+             "type": "secondary"+           },+           {+             "data": "extraordinarily.long.email.username.123",+             "primary": false,+             "type": "other"+           }+         ],+         "names": [+           "Alberta Bobbeth Charleson"+         ],+         "phone_numbers": [+           {+             "data": "1112223333",+             "primary": false,+             "type": "home"+           },+           {+             "data": "1112224444",+             "primary": false,+             "type": "work"+           },+           {+             "data": "1112225555",+             "primary": false,+             "type": "mobile1"+           }+         ]+       }+     ],+     "subtype": "cd",+     "type": "depository"+   },+   {+     "account_id": "6zaBBWpoM5HJqEkVy5zyUeML9xRL8kigopWN8",+     "balances": {+       "available": null,+       "current": 410,+       "iso_currency_code": "USD",+       "limit": 2000,+       "unofficial_currency_code": null+     },+     "mask": "3333",+     "name": "Plaid Credit Card",+     "official_name": "Plaid Diamond 12.5% APR Interest Credi",+     "owners": [+       {+         "addresses": [+           {+             "data": {+               "city": "Malakoff",+               "country": null,+               "postal_code": "14236",+               "region": "NY",+               "street": "2992 Cameron Road"+             },+             "primary": true+           },+           {+             "data": {+               "city": "San Matias",+               "country": null,+               "postal_code": "93405-2255",+               "region": "CA",+               "street": "2493 Leisure Lane"+             },+             "primary": false+           }+         ],+         "emails": [+           {+             "data": "accountholder0@example.com",+             "primary": true,+             "type": "primary"+           },+           {+             "data": "accountholder1@example.com",+             "primary": false,+             "type": "secondary"+           },+           {+             "data": "extraordinarily.long.email.username.123",+             "primary": false,+             "type": "other"+           }+         ],+         "names": [+           "Alberta Bobbeth Charleson"+         ],+         "phone_numbers": [+           {+             "data": "1112223333",+             "primary": false,+             "type": "home"+           },+           {+             "data": "1112224444",+             "primary": false,+             "type": "work"+           },+           {+             "data": "1112225555",+             "primary": false,+             "type": "mobile1"+           }+         ]+       }+     ],+     "subtype": "credit card",+     "type": "credit"+   },+   {+     "account_id": "Xa4wwnxzl6IQWD4eKBvKFwjeG4XeBLHdWN4V1",+     "balances": {+       "available": 43200,+       "current": 43200,+       "iso_currency_code": "USD",+       "limit": null,+       "unofficial_currency_code": null+     },+     "mask": "4444",+     "name": "Plaid Money Market",+     "official_name": "Plaid Platinum Standard 1.85% Interest",+     "owners": [+       {+         "addresses": [+           {+             "data": {+               "city": "Malakoff",+               "country": null,+               "postal_code": "14236",+               "region": "NY",+               "street": "2992 Cameron Road"+             },+             "primary": true+           },+           {+             "data": {+               "city": "San Matias",+               "country": null,+               "postal_code": "93405-2255",+               "region": "CA",+               "street": "2493 Leisure Lane"+             },+             "primary": false+           }+         ],+         "emails": [+           {+             "data": "accountholder0@example.com",+             "primary": true,+             "type": "primary"+           },+           {+             "data": "accountholder1@example.com",+             "primary": false,+             "type": "secondary"+           },+           {+             "data": "extraordinarily.long.email.username.123",+             "primary": false,+             "type": "other"+           }+         ],+         "names": [+           "Alberta Bobbeth Charleson"+         ],+         "phone_numbers": [+           {+             "data": "1112223333",+             "primary": false,+             "type": "home"+           },+           {+             "data": "1112224444",+             "primary": false,+             "type": "work"+           },+           {+             "data": "1112225555",+             "primary": false,+             "type": "mobile1"+           }+         ]+       }+     ],+     "subtype": "money market",+     "type": "depository"+   },+   {+     "account_id": "DMgwwaJx36SdwzmkaNMaSlxb56PbNXtvPBgVE",+     "balances": {+       "available": null,+       "current": 320.76,+       "iso_currency_code": "USD",+       "limit": null,+       "unofficial_currency_code": null+     },+     "mask": "5555",+     "name": "Plaid IRA",+     "official_name": null,+     "owners": [+       {+         "addresses": [+           {+             "data": {+               "city": "Malakoff",+               "country": null,+               "postal_code": "14236",+               "region": "NY",+               "street": "2992 Cameron Road"+             },+             "primary": true+           },+           {+             "data": {+               "city": "San Matias",+               "country": null,+               "postal_code": "93405-2255",+               "region": "CA",+               "street": "2493 Leisure Lane"+             },+             "primary": false+           }+         ],+         "emails": [+           {+             "data": "accountholder0@example.com",+             "primary": true,+             "type": "primary"+           },+           {+             "data": "accountholder1@example.com",+             "primary": false,+             "type": "secondary"+           },+           {+             "data": "extraordinarily.long.email.username.123",+             "primary": false,+             "type": "other"+           }+         ],+         "names": [+           "Alberta Bobbeth Charleson"+         ],+         "phone_numbers": [+           {+             "data": "1112223333",+             "primary": false,+             "type": "home"+           },+           {+             "data": "1112224444",+             "primary": false,+             "type": "work"+           },+           {+             "data": "1112225555",+             "primary": false,+             "type": "mobile1"+           }+         ]+       }+     ],+     "subtype": "ira",+     "type": "investment"+   },+   {+     "account_id": "VxdggABl86IamVdJNXkNuMdEDnVEBWHW8X4xn",+     "balances": {+       "available": null,+       "current": 23631.9805,+       "iso_currency_code": "USD",+       "limit": null,+       "unofficial_currency_code": null+     },+     "mask": "6666",+     "name": "Plaid 401k",+     "official_name": null,+     "owners": [+       {+         "addresses": [+           {+             "data": {+               "city": "Malakoff",+               "country": null,+               "postal_code": "14236",+               "region": "NY",+               "street": "2992 Cameron Road"+             },+             "primary": true+           },+           {+             "data": {+               "city": "San Matias",+               "country": null,+               "postal_code": "93405-2255",+               "region": "CA",+               "street": "2493 Leisure Lane"+             },+             "primary": false+           }+         ],+         "emails": [+           {+             "data": "accountholder0@example.com",+             "primary": true,+             "type": "primary"+           },+           {+             "data": "accountholder1@example.com",+             "primary": false,+             "type": "secondary"+           },+           {+             "data": "extraordinarily.long.email.username.123",+             "primary": false,+             "type": "other"+           }+         ],+         "names": [+           "Alberta Bobbeth Charleson"+         ],+         "phone_numbers": [+           {+             "data": "1112223333",+             "primary": false,+             "type": "home"+           },+           {+             "data": "1112224444",+             "primary": false,+             "type": "work"+           },+           {+             "data": "1112225555",+             "primary": false,+             "type": "mobile1"+           }+         ]+       }+     ],+     "subtype": "401k",+     "type": "investment"+   },+   {+     "account_id": "wNd55VvgBwSRXJEP67k6Unl9wxJ9jvFr7xvkL",+     "balances": {+       "available": null,+       "current": 65262,+       "iso_currency_code": "USD",+       "limit": null,+       "unofficial_currency_code": null+     },+     "mask": "7777",+     "name": "Plaid Student Loan",+     "official_name": null,+     "owners": [+       {+         "addresses": [+           {+             "data": {+               "city": "Malakoff",+               "country": null,+               "postal_code": "14236",+               "region": "NY",+               "street": "2992 Cameron Road"+             },+             "primary": true+           },+           {+             "data": {+               "city": "San Matias",+               "country": null,+               "postal_code": "93405-2255",+               "region": "CA",+               "street": "2493 Leisure Lane"+             },+             "primary": false+           }+         ],+         "emails": [+           {+             "data": "accountholder0@example.com",+             "primary": true,+             "type": "primary"+           },+           {+             "data": "accountholder1@example.com",+             "primary": false,+             "type": "secondary"+           },+           {+             "data": "extraordinarily.long.email.username.123",+             "primary": false,+             "type": "other"+           }+         ],+         "names": [+           "Alberta Bobbeth Charleson"+         ],+         "phone_numbers": [+           {+             "data": "1112223333",+             "primary": false,+             "type": "home"+           },+           {+             "data": "1112224444",+             "primary": false,+             "type": "work"+           },+           {+             "data": "1112225555",+             "primary": false,+             "type": "mobile1"+           }+         ]+       }+     ],+     "subtype": "student",+     "type": "loan"+   }+ ],+ "item": {+   "available_products": [+     "assets",+     "auth",+     "balance",+     "credit_details",+     "income",+     "investments",+     "liabilities"+   ],+   "billed_products": [+     "identity",+     "transactions"+   ],+   "consent_expiration_time": null,+   "error": null,+   "institution_id": "ins_3",+   "item_id": "Wda44G3KEVsz9ValeLmeh14nVAKLJwhlG8XN3",+   "webhook": ""+ },+ "request_id": "Mjv0DF1S06RqIuT"+}+|]++incomeJson :: ByteString+incomeJson = [r|++|]
+ Data/Api/Transactions.hs view
@@ -0,0 +1,18 @@+module Data.Api.Transactions+  ( plaidGetTransactions+  ) where++import           Data.Common++plaidGetTransactions+  :: ( MonadReader PlaidEnv m+     , MonadThrow m+     , PlaidHttp m+     )+  => PlaidBody PlaidTransactionsGet+  -> m ByteString+plaidGetTransactions body = do+  env <- ask+  let url = envUrl (env ^. plaidEnvEnvironment)+  executePost (url <> "/transactions/get") body+
+ Data/Api/Types.hs view
@@ -0,0 +1,769 @@+{-# LANGUAGE DeriveAnyClass     #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE InstanceSigs       #-}++module Data.Api.Types where++import           Control.Exception.Safe+import           Control.Monad.IO.Class   (MonadIO, liftIO)+import           Control.Monad.Reader     (MonadReader (..), ReaderT (..))+import           Data.Aeson+import           Data.Aeson.TH+import           Data.Api.TestByteStrings+import           Data.ByteString.Lazy     (ByteString)+import           Data.Fixed+import           Data.Functor.Identity+import qualified Data.Map                 as M+import           Data.Maybe               (isJust, isNothing)+import           Data.Monoid              ((<>))+import           Data.Text                (Text)+import qualified Data.Text                as T+import           Data.Time                (Day)+import           Lens.Micro+import           Lens.Micro.TH+import           Network.HTTP.Conduit+import           Text.Casing              (camel, quietSnake)++data Environment+  = Sandbox+  | Development+  | Production deriving (Ord, Eq)++$(deriveFromJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 11 }) ''Environment)++instance Show Environment where+  show Sandbox     = "sandbox"+  show Development = "development"+  show Production  = "production"++class Monad m => PlaidHttp m where+  executePost :: ToJSON a => Text -> a -> m ByteString+  executeGet :: Text -> m ByteString++newtype PlaidError =+  PlaidError Text+  deriving Show+  deriving anyclass Exception++$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 10 }) ''PlaidError)++newtype AccessToken =+  AccessToken+    { unAccessToken :: Text+    } deriving (Eq, Show)++instance ToJSON AccessToken where+  toJSON AccessToken {..} = String unAccessToken++instance FromJSON AccessToken where+  parseJSON = withText "AccessToken" $ \t ->+    return $ AccessToken t++newtype PublicToken =+  PublicToken+    { unPublicToken :: Text+    } deriving (Eq, Show)++instance ToJSON PublicToken where+  toJSON PublicToken {..} = String unPublicToken++instance FromJSON PublicToken where+  parseJSON = withText "PublicToken" $ \t ->+    return $ PublicToken t++newtype ClientId =+  ClientId+    { unClientId :: Text+    } deriving (Eq, Show)++instance ToJSON ClientId where+  toJSON ClientId {..} = String unClientId++instance FromJSON ClientId where+  parseJSON = withText "ClientId" $ \t ->+    return $ ClientId t++newtype Secret =+  Secret+    { unSecret :: Text+    } deriving (Eq, Show)++instance ToJSON Secret where+  toJSON Secret {..} = String unSecret++instance FromJSON Secret where+  parseJSON = withText "Secret" $ \t ->+    return $ Secret t++newtype InstitutionId =+  InstitutionId+    { unInstitutionId :: Text+    } deriving (Eq, Show)++instance ToJSON InstitutionId where+  toJSON InstitutionId {..} = String unInstitutionId++instance FromJSON InstitutionId where+  parseJSON = withText "InstitutionId" $ \t ->+    return $ InstitutionId t++newtype Url a = Url { unUrl :: Text }++instance ToJSON (Url a) where+  toJSON Url {..} = String unUrl++instance FromJSON (Url a) where+  parseJSON = withText "Url" $ \t ->+    return $ Url t++type PublicKey     = Text+type AccessKey     = Text+type PlaidProduct  = Text+type Institution = Text+type AccountNumber = Text+type SortCode = Text+type AccountId = Text+type Iban = Text+type Bic = Text+type RequestId = Text++data AuthGet+data GetBalance+data PublicTokenCreate+data PlaidTokenExchange+data PlaidTransactionsGet+data PlaidIdentityGet+data PlaidIncomeGet++data PlaidOptions =+  PlaidOptions+    { _plaidOptionsWebHook          :: Text+    , _plaidOptionsOverrideUsername :: Text+    , _plaidOptionsOverridePassword :: Text+    } deriving (Eq, Show)++makeLenses ''PlaidOptions+$(deriveFromJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 13 }) ''PlaidOptions)++instance ToJSON PlaidOptions where+  toJSON b =+    object [ "webhook"           .= (b ^. plaidOptionsWebHook)+           , "override_username" .= (b ^. plaidOptionsOverrideUsername)+           , "override_password" .= (b ^. plaidOptionsOverridePassword)+           ]++data PlaidPaginationOptions =+  PlaidPaginationOptions+    { _plaidPaginationOptionsCount  :: Int+    , _plaidPaginationOptionsOffset :: Int+    } deriving (Eq, Show)++makeLenses ''PlaidPaginationOptions+$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 23 }) ''PlaidPaginationOptions)++defPaginationOptions :: PlaidPaginationOptions+defPaginationOptions =+  PlaidPaginationOptions+    { _plaidPaginationOptionsCount  = 100+    , _plaidPaginationOptionsOffset = 0+    }++data PlaidEnv =+  PlaidEnv+    { _plaidEnvPublicKey   :: PublicKey+    , _plaidEnvClientId    :: ClientId+    , _plaidEnvSecret      :: Secret+    , _plaidEnvEnvironment :: Environment+    } deriving (Eq, Show)++makeLenses ''PlaidEnv+$(deriveFromJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 8 }) ''PlaidEnv)++data PlaidBody a+  = PlaidBody+      { _plaidBodyEnv               :: PlaidEnv+      , _plaidBodyPublicToken       :: Maybe PublicToken+      , _plaidBodyAccessToken       :: Maybe AccessToken+      , _plaidBodyOptions           :: Maybe PlaidOptions+      , _plaidBodyPaginationOptions :: Maybe PlaidPaginationOptions+      , _plaidBodyInstitutionId     :: Maybe InstitutionId+      , _plaidBodyInitialProducts   :: Maybe [PlaidProduct]+      , _plaidBodyStartDate         :: Maybe Day+      , _plaidBodyEndDate           :: Maybe Day+      } deriving (Eq, Show)++makeLenses ''PlaidBody++instance ToJSON (PlaidBody GetBalance) where+  toJSON b =+    if isNothing (b ^. plaidBodyOptions)+      then+        object [ "client_id"    .= (b ^. plaidBodyEnv . plaidEnvClientId)+               , "secret"       .= (b ^. plaidBodyEnv . plaidEnvSecret)+               , "access_token" .= toJSON (b ^. plaidBodyAccessToken)+               ]+      else+        object [ "client_id"    .= (b ^. plaidBodyEnv . plaidEnvClientId)+               , "secret"       .= (b ^. plaidBodyEnv . plaidEnvSecret)+               , "access_token" .= (b ^. plaidBodyAccessToken)+               , "options"      .= (b ^. plaidBodyOptions)+               ]++instance ToJSON (PlaidBody PublicTokenCreate) where+  toJSON b =+    if isNothing (b ^. plaidBodyOptions)+      then+        object [ "institution_id"   .= (b ^. plaidBodyInstitutionId)+               , "public_key"       .= (b ^. plaidBodyEnv . plaidEnvPublicKey)+               , "initial_products" .= (b ^. plaidBodyInitialProducts)+               ]+      else+        object [ "institution_id"   .= (b ^. plaidBodyInstitutionId)+               , "public_key"       .= (b ^. plaidBodyEnv . plaidEnvPublicKey)+               , "initial_products" .= (b ^. plaidBodyInitialProducts)+               , "options"          .= toJSON (b ^. plaidBodyOptions)+               ]++instance ToJSON (PlaidBody AuthGet) where+  toJSON b =+    object [ "client_id"    .= (b ^. plaidBodyEnv . plaidEnvClientId)+           , "secret"       .= (b ^. plaidBodyEnv . plaidEnvSecret)+           , "access_token" .= (b ^. plaidBodyAccessToken)+           ]++instance ToJSON (PlaidBody PlaidTokenExchange) where+  toJSON b =+    object [ "client_id"    .= (b ^. plaidBodyEnv . plaidEnvClientId)+           , "secret"       .= (b ^. plaidBodyEnv . plaidEnvSecret)+           , "public_token" .= (b ^. plaidBodyPublicToken)+           ]++instance ToJSON (PlaidBody PlaidTransactionsGet) where+  toJSON b =+    object $ [ "client_id"    .= (b ^. plaidBodyEnv . plaidEnvClientId)+             , "secret"       .= (b ^. plaidBodyEnv . plaidEnvSecret)+             , "access_token" .= (b ^. plaidBodyAccessToken)+             , "start_date"   .= (b ^. plaidBodyStartDate)+             , "end_date"     .= (b ^. plaidBodyEndDate)+             ] <> perhapsSendObject+    where+      perhapsSendObject =+        ["options" .= toJSON (b ^. plaidBodyPaginationOptions) | isJust (b ^. plaidBodyPaginationOptions)]++instance ToJSON (PlaidBody PlaidIdentityGet) where+  toJSON b =+    object [ "client_id"    .= (b ^. plaidBodyEnv . plaidEnvClientId)+           , "secret"       .= (b ^. plaidBodyEnv . plaidEnvSecret)+           , "access_token" .= (b ^. plaidBodyAccessToken)+           ]++instance ToJSON (PlaidBody PlaidIncomeGet) where+  toJSON b =+    object [ "client_id"    .= (b ^. plaidBodyEnv . plaidEnvClientId)+           , "secret"       .= (b ^. plaidBodyEnv . plaidEnvSecret)+           , "access_token" .= (b ^. plaidBodyAccessToken)+           ]++-- | Main type used as the carrier for 'PlaidHttp' instance.+-- We use mtl style constraints in library functions and at+-- the end call 'runPlaid' to run all operations inside of IO.+-- You never need to construct value of this type it is just used+-- as a monad stack to carry Plaid operations.+newtype Plaid a =+  Plaid+    { unPlaid :: ReaderT PlaidEnv IO a+    } deriving newtype ( Functor+                       , Applicative+                       , Monad+                       , MonadReader PlaidEnv+                       , MonadIO+                       , MonadThrow+                       )++-- | Carrier type used for testing+newtype PlaidTest a =+  PlaidTest+    { unPlaidTest :: Identity a+    } deriving newtype ( Functor+                       , Applicative+                       , Monad+                       )++runPlaid :: PlaidEnv -> Plaid a -> IO a+runPlaid env = flip runReaderT env . unPlaid++runTestPlaid :: PlaidTest a -> a+runTestPlaid = runIdentity . unPlaidTest++data CurrencyCode+  = USD+  | EUR+  deriving (Eq, Show)++$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 12 }) ''CurrencyCode)++data Balance =+  Balance+    { balanceAvailable              :: Double+    , balanceCurrent                :: Double+    , balanceLimit                  :: Maybe Double+    , balanceIsoCurrencyCode        :: CurrencyCode+    , balanceUnofficialCurrencyCode :: Maybe CurrencyCode+    } deriving (Eq, Show)++$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 7 }) ''Balance)++data Account =+  Account+    { accountAccountId    :: Text+    , accountBalances     :: Balance+    , accountMask         :: Text+    , accountName         :: Text+    , accountOfficialName :: Text+    , accountSubtype      :: Text+    , accountType         :: Text+    } deriving (Eq, Show)++$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 7 }) ''Account)++data Ach =+  Ach+    { achAccount     :: AccountNumber+    , achAccountId   :: AccountId+    , achRouting     :: Text+    , achWireRouting :: Text+    } deriving (Eq, Show)++$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 3 }) ''Ach)++data Error =+  Error+    { errorType      :: Text+    , errorCode      :: Text+    , errorMessage   :: Text+    , displayMessage :: Maybe Text+    } deriving (Eq, Show)++$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 5 }) ''Error)++data Item =+  Item+    { itemAvailableProducts :: [Text]+    , itemBilledProducts    :: [Text]+    , itemError             :: Maybe Error+    , itemInstitutionId     :: InstitutionId+    , itemItemId            :: Text+    , itemWebhook           :: Maybe Text+    } deriving (Eq, Show)++$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 4 }) ''Item)++data Eft =+  Eft+    { eftAccount     :: AccountNumber+    , eftAccountId   :: AccountId+    , eftInstitution :: Institution+    , eftBranch      :: Text+    } deriving (Eq, Show)++$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 3 }) ''Eft)++data International =+  International+    { internationalAccountId :: AccountId+    , internationalBic       :: Bic+    , internationalIban      :: Iban+    } deriving (Eq, Show)++$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 13 }) ''International)++data Bacs =+  Bacs+    { bacsAccount   :: AccountNumber+    , bacsAccountId :: AccountId+    , bacsSortCode  :: SortCode+    } deriving (Eq, Show)++$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 4 }) ''Bacs)++data Numbers =+  Numbers+    { numbersAch           :: [Ach]+    , numbersEft           :: [Eft]+    , numbersInternational :: [International]+    , numbersBacs          :: [Bacs]+    } deriving (Eq, Show)++$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 7 }) ''Numbers)++data TransactionType+  = Digital+  | Place+  | Special+  | Unresolved+  deriving (Eq, Show)++instance ToJSON TransactionType where+  toJSON Digital    = String "digital"+  toJSON Place      = String "place"+  toJSON Special    = String "special"+  toJSON Unresolved = String "unresolved"++instance FromJSON TransactionType where+  parseJSON "digital"    = pure Digital+  parseJSON "place"      = pure Place+  parseJSON "special"    = pure Special+  parseJSON "unresolved" = pure Unresolved+  parseJSON _            = mempty++data TransactionLocation =+  TransactionLocation+    { transactionLocationAddress       :: Maybe Text+    , transactionLocationCity          :: Maybe Text+    , transactionLocationRegion        :: Maybe Text+    , transactionLocationPostalCode    :: Maybe Text+    , transactionLocationPostalCountry :: Maybe Text+    , transactionLocationPostalLat     :: Maybe Double+    , transactionLocationPostalLon     :: Maybe Double+    } deriving (Eq, Show)++$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 19 }) ''TransactionLocation)++data PaymentMeta =+  PaymentMeta+    { paymentMetaReferenceNumber :: Maybe Text+    , paymentMetaPpdId           :: Maybe Text+    , paymentMetaPayee           :: Maybe Text+    } deriving (Eq, Show)++$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 11 }) ''PaymentMeta)++data Transaction =+  Transaction+    { transactionAccountId              :: AccountId+    , transactionAmount                 :: Double+    , transactionIsoCurrencyCode        :: CurrencyCode+    , transactionUnofficialCurrencyCode :: Maybe CurrencyCode+    , transactionCategory               :: Maybe [Text]+    , transactionCategoryId             :: Maybe Text+    , transactionTransactionType        :: Maybe TransactionType+    , transactionName                   :: Text+    , transactionDate                   :: Day+    , transactionLocation               :: TransactionLocation+    , transactionPending                :: Bool+    , transactionPaymentMeta            :: PaymentMeta+    , transactionPendingTransactionId   :: Maybe Text+    , transactionAccountOwner           :: Maybe Text+    } deriving (Eq, Show)++$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 11 }) ''Transaction)++data PlaidTransactionsGetResponse =+  PlaidTransactionsGetResponse+    { _plaidTransactionsGetResponseAccounts     :: [Account]+    , _plaidTransactionsGetResponseTransactions :: [Transaction]+    , _plaidTransactionsItem                    :: Item+    , _plaidTransactionsTotalTransactions       :: Int+    , _plaidTransactionsRequestId               :: Text+    } deriving (Eq, Show)++makeLenses ''PlaidTransactionsGetResponse++$(deriveToJSON (defaultOptions { fieldLabelModifier = camel . drop 29 }) ''PlaidTransactionsGetResponse)++instance FromJSON PlaidTransactionsGetResponse where+  parseJSON = withObject "PlaidTransactionsGetResponse" $ \o ->+    PlaidTransactionsGetResponse+      <$> o .: "accounts"+      <*> o .: "transactions"+      <*> o .: "item"+      <*> o .: "total_transactions"+      <*> o .: "request_id"++data PlaidAuthGetResponse =+  PlaidAuthGetResponse+    { _plaidAuthGetResponseAccounts  :: [Account]+    , _plaidAuthGetResponseNumbers   :: Numbers+    , _plaidAuthGetResponseItem      :: Item+    , _plaidAuthGetResponseRequestId :: RequestId+    } deriving (Eq, Show)++makeLenses ''PlaidAuthGetResponse++$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 21 }) ''PlaidAuthGetResponse)++data PlaidPublicTokenResponse =+  PlaidPublicTokenResponse+    { _plaidPublicTokenResponsePublicToken :: PublicToken+    , _plaidPublicTokenResponseRequestId   :: Text+    } deriving (Eq, Show)++makeLenses ''PlaidPublicTokenResponse++instance ToJSON PlaidPublicTokenResponse where+  toJSON PlaidPublicTokenResponse {..} =+    object [ "public_token" .= _plaidPublicTokenResponsePublicToken+           , "request_id"   .= _plaidPublicTokenResponseRequestId+           ]++instance FromJSON PlaidPublicTokenResponse where+  parseJSON = withObject "PlaidPublicTokenResponse" $ \o ->+    PlaidPublicTokenResponse+      <$> o .: "public_token"+      <*> o .: "request_id"++data PlaidAccessTokenResponse =+  PlaidAccessTokenResponse+    { _plaidAccessTokenResponseAccessToken :: AccessToken+    , _plaidAccessTokenResponseRequestId   :: Text+    , _plaidAccessTokenResponseItemId      :: Text+    } deriving (Eq, Show)++makeLenses ''PlaidAccessTokenResponse++instance ToJSON PlaidAccessTokenResponse where+  toJSON PlaidAccessTokenResponse {..} =+    object [ "access_token" .= _plaidAccessTokenResponseAccessToken+           , "request_id"   .= _plaidAccessTokenResponseRequestId+           , "item_id"      .= _plaidAccessTokenResponseItemId+           ]++instance FromJSON PlaidAccessTokenResponse where+  parseJSON = withObject "PlaidAccessTokenResponse" $ \o ->+    PlaidAccessTokenResponse+      <$> o .: "access_token"+      <*> o .: "request_id"+      <*> o .: "item_id"++data PhoneNumber =+  PhoneNumber+    { _phoneNumberData    :: Text+    , _phoneNumberPrimary :: Bool+    , _phoneNumberType    :: Text+    } deriving (Eq, Show)++makeLenses ''PhoneNumber++$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 12 }) ''PhoneNumber)++data Email =+  Email+    { _emailData    :: Text+    , _emailPrimary :: Bool+    , _emailType    :: Text+    } deriving (Eq, Show)++makeLenses ''Email++$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 6 }) ''Email)++data Address =+  Address+    { _addressCity       :: Text+    , _addressRegion     :: Text+    , _addressStreet     :: Text+    , _addressPostalCode :: Text+    , _addressCountry    :: Maybe Text+    } deriving (Eq, Show)++makeLenses ''Address++$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 8 }) ''Address)++data Addresses =+  Addresses+    { _addressesData    :: Address+    , _addressesPrimary :: Bool+    } deriving (Eq, Show)++makeLenses ''Addresses++$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 10 }) ''Addresses)++data Owners =+  Owners+    { _ownersAddresses    :: [Addresses]+    , _ownersEmails       :: [Email]+    , _ownersNames        :: [Text]+    , _ownersPhoneNumbers :: [PhoneNumber]+    } deriving (Eq, Show)++makeLenses ''Owners++$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 7 }) ''Owners)++data Accounts =+  Accounts+    { _accountsAccountId    :: Text+    , _accountsBalances     :: Balance+    , _accountsMask         :: Text+    , _accountsName         :: Text+    , _accountsOfficialName :: Maybe Text+    , _accountsOwners       :: [Owners]+    , _accountsSubtype      :: Text+    , _accountsType         :: Text+    } deriving (Eq, Show)++$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 9 }) ''Accounts)++data PlaidIdentityGetResponse =+  PlaidIdentityGetResponse+    { _plaidIdentityGetResponseAccounts :: [Accounts]+    , _plaidIdentityGetItem             :: Item+    , _plaidIdentityGetRequestId        :: Text+    } deriving (Eq, Show)++makeLenses ''PlaidIdentityGetResponse++$(deriveToJSON (defaultOptions { fieldLabelModifier = camel . drop 25 }) ''PlaidIdentityGetResponse)++instance FromJSON PlaidIdentityGetResponse where+  parseJSON = withObject "PlaidIdentityGetResponse" $ \o ->+    PlaidIdentityGetResponse+      <$> o .: "accounts"+      <*> o .: "item"+      <*> o .: "request_id"++data IncomeStream =+  IncomeStream+    { _incomeStreamMonthlyIncome :: Fixed E2+    , _incomeStreamConfidence    :: Fixed E2+    , _incomeStreamDays          :: Fixed E2+    , _incomeStreamName          :: Text+    } deriving (Eq, Show)++makeLenses ''IncomeStream++$(deriveJSON (defaultOptions { fieldLabelModifier = camel . drop 13 }) ''IncomeStream)++data Income =+  Income+    { _incomeLastYearIncome                      :: Fixed E2+    , _incomeLastYearIncomeBeforeTax             :: Fixed E2+    , _incomeProjectedYearlyIncome               :: Fixed E2+    , _incomeProjectedYearlyIncomeBeforeTax      :: Fixed E2+    , _incomeIncomeStreams                       :: [IncomeStream]+    , _incomeMaxNumberOfOverlappingIncomeStreams :: Fixed E2+    , _incomeNumberOfIncomeStreams               :: Fixed E2+    } deriving (Eq, Show)++makeLenses ''Income++$(deriveJSON (defaultOptions { fieldLabelModifier = camel . drop 7 }) ''Income)++baseUrl :: Environment -> Url Environment+baseUrl e = Url $ T.pack $ "https://" <> show e <> ".plaid.com"++envUrl :: Environment -> Text+envUrl = unUrl . baseUrl++validInstitutionIds :: [InstitutionId]+validInstitutionIds =+  InstitutionId <$>+    [ "ins_1"+    , "ins_2"+    , "ins_3"+    , "ins_4"+    , "ins_5"+    , "ins_6"+    , "ins_7"+    , "ins_9"+    , "ins_10"+    , "ins_11"+    , "ins_13"+    , "ins_14"+    , "ins_15"+    , "ins_16"+    , "ins_19"+    , "ins_20"+    , "ins_21"+    , "ins_23"+    , "ins_24"+    , "ins_27"+    , "ins_29"+    ]++-- | Map used for testing different plaid endpoints+requestMap :: M.Map Text ByteString+requestMap =+  M.fromList+    [ ("/auth/get", responseAuthGet)+    , ("/transactions/get", responseTransactionsGet)+    , ("/public_token/create", responsePublicTokenCreate)+    , ("/item/public_token/exchange", responsePublicTokenExchange)+    , ("/identity/get", identityJson)+    ]++-- | Instances+instance PlaidHttp Plaid where+  executeGet :: Text -> Plaid ByteString+  executeGet url = do+    request <- parseUrlThrow (T.unpack url)+    m <- liftIO $ newManager tlsManagerSettings+    response <- httpLbs request m+    return $ responseBody response++  executePost :: ToJSON a => Text -> a -> Plaid ByteString+  executePost url postBody = do+    initialRequest <- parseUrlThrow (T.unpack url)+    m <- liftIO (newManager tlsManagerSettings)+    let request =+          initialRequest+          { method = "POST"+          , requestBody = RequestBodyLBS (encode postBody)+          , requestHeaders = [ ("Content-Type", "application/json") ]+          }+    response <- httpLbs request m+    return $ responseBody response++instance PlaidHttp PlaidTest where+  executeGet :: Text -> PlaidTest ByteString+  executeGet url =+    case M.lookup url requestMap of+      Nothing -> return ""+      Just bs -> return bs++  executePost :: ToJSON a => Text -> a -> PlaidTest ByteString+  executePost url _postBody =+    case M.lookup url requestMap of+      Nothing -> return ""+      Just bs -> return bs++instance Semigroup PlaidOptions where+  (<>) _ b = b++instance Monoid PlaidOptions where+  mempty =+    PlaidOptions+      { _plaidOptionsWebHook          = ""+      , _plaidOptionsOverrideUsername = ""+      , _plaidOptionsOverridePassword = ""+      }+  mappend _ b = b++instance Semigroup (PlaidBody a) where+  (<>) _ b = b++instance Monoid (PlaidBody a) where+  mempty =+    let env =+          PlaidEnv+            { _plaidEnvPublicKey   = ""+            , _plaidEnvClientId    = ClientId ""+            , _plaidEnvSecret      = Secret ""+            , _plaidEnvEnvironment = Sandbox+            }+    in+      PlaidBody+        { _plaidBodyEnv               = env+        , _plaidBodyPublicToken       = Nothing+        , _plaidBodyAccessToken       = Nothing+        , _plaidBodyOptions           = Nothing+        , _plaidBodyPaginationOptions = Nothing+        , _plaidBodyInstitutionId     = Nothing+        , _plaidBodyInitialProducts   = Nothing+        , _plaidBodyStartDate         = Nothing+        , _plaidBodyEndDate           = Nothing+        }+  mappend _ b = b+
+ Data/Common.hs view
@@ -0,0 +1,25 @@+{-# OPTIONS_HADDOCK hide, prune, ignore-exports #-}+module Data.Common+  ( module Data.Api.Types+  , module Data.Api.Helper+  , module Data.Aeson+  , module Lens.Micro+  , MonadReader (..)+  , MonadThrow+  , MonadIO (..)+  , ReaderT (..)+  , ByteString+  , fromStrict+  , toStrict+  , Text+  ) where++import           Control.Monad.Reader (MonadReader (..), ReaderT (..))+import           Data.Api.Helper+import           Data.Api.Types+import           Data.Aeson hiding (Error)+import           Data.ByteString.Lazy (ByteString, fromStrict, toStrict)+import           Data.Text            (Text)+import           Control.Exception.Safe+import           Control.Monad.IO.Class+import           Lens.Micro
+ Data/Plaid.hs view
@@ -0,0 +1,21 @@+-- |+--+-- Module      :  Data.Plaid+-- Copyright   :  2019 Sasha Bogicevic+-- License     :  BSD 3 clause+--+-- Maintainer  :  Sasha Bogicevic <sasa.bogicevic@pm.me>+-- Stability   :  experimental+-- Portability :  portable+--+-- === Welcome to Plaid+-- As you could guess this library aims to provide easy+-- integration with Plaid.com++module Data.Plaid+  ( module Data.Api.Plaid+  , module Data.Api.Types+  ) where++import           Data.Api.Plaid+import           Data.Api.Types hiding (Plaid, PlaidTest, envUrl, requestMap, runTestPlaid)
+ Data/Proof/AuthGetBody.hs view
@@ -0,0 +1,19 @@+module Data.Proof.AuthGetBody+  ( HasAuthGetBody+  , proveAuthGetBody+  ) where++import           Data.Api.Types+import           Data.Proof.Named+import           Data.Text (Text)++data HasAuthGetBody plaidEnv accessToken = Proof++proveAuthGetBody+  :: Named plaidEnv PlaidEnv+  -> Named accessToken AccessToken+  -> Either Text (HasAuthGetBody plaidEnv accessToken)+proveAuthGetBody _ atoken =+  case unWrapNamed atoken of+    (AccessToken "") -> Left "AccessToken cannot be empty"+    (AccessToken _ ) -> Right Proof
+ Data/Proof/BalanceGetBody.hs view
@@ -0,0 +1,20 @@+module Data.Proof.BalanceGetBody+  ( HasGetBalanceBody+  , proveGetBalanceBody+  ) where++import           Data.Api.Types+import           Data.Proof.Named+import           Data.Text (Text)++data HasGetBalanceBody plaidEnv accessToken = Proof++-- | Check for creation of get balance request body+proveGetBalanceBody+  :: Named plaidEnv PlaidEnv+  -> Named accessToken AccessToken+  -> Either Text (HasGetBalanceBody plaidEnv accessToken)+proveGetBalanceBody _ atoken =+  case unWrapNamed atoken of+    (AccessToken "") -> Left "AccessToken cannot be empty"+    (AccessToken _ ) -> Right Proof
+ Data/Proof/IdentityGetBody.hs view
@@ -0,0 +1,20 @@+module Data.Proof.IdentityGetBody+  ( HasIdentityGetBody+  , proveIdentityGetBody+  ) where++import           Data.Api.Types+import           Data.Proof.Named+import           Data.Text (Text)++data HasIdentityGetBody plaidEnv accessToken = Proof++-- | Check for creation of get identity request body+proveIdentityGetBody+  :: Named plaidEnv PlaidEnv+  -> Named accessToken AccessToken+  -> Either Text (HasIdentityGetBody plaidEnv accessToken)+proveIdentityGetBody _ atoken =+  case unWrapNamed atoken of+    (AccessToken "") -> Left "AccessToken cannot be empty"+    (AccessToken _ ) -> Right Proof
+ Data/Proof/IncomeGetBody.hs view
@@ -0,0 +1,20 @@+module Data.Proof.IncomeGetBody+  ( HasIncomeGetBody+  , proveIncomeGetBody+  ) where++import           Data.Api.Types+import           Data.Proof.Named+import           Data.Text (Text)++data HasIncomeGetBody plaidEnv accessToken = Proof++-- | Check for creation of get income request body+proveIncomeGetBody+  :: Named plaidEnv PlaidEnv+  -> Named accessToken AccessToken+  -> Either Text (HasIncomeGetBody plaidEnv accessToken)+proveIncomeGetBody _ atoken =+  case unWrapNamed atoken of+    (AccessToken "") -> Left "AccessToken cannot be empty"+    (AccessToken _ ) -> Right Proof
+ Data/Proof/Named.hs view
@@ -0,0 +1,15 @@+{-# LANGUAGE Rank2Types #-}++module Data.Proof.Named+  ( Named+  , name+  , unWrapNamed+  ) where++newtype Named n a = Named { forgetName :: a } deriving Show++name :: a -> ( forall name. Named name a -> r ) -> r+name x f = f ( Named x )++unWrapNamed :: Named n a -> a+unWrapNamed = forgetName
+ Data/Proof/Proof.hs view
@@ -0,0 +1,25 @@+module Data.Proof.Proof+  ( HasAuthGetBody+  , HasGetBalanceBody+  , HasCreatePublicTokenBody+  , HasTransactionsGetBody+  , HasIdentityGetBody+  , HasIncomeGetBody+  , name+  , unWrapNamed+  , proveAuthGetBody+  , proveTransactionsGetBody+  , proveIdentityGetBody+  , proveGetBalanceBody+  , proveCreatePublicTokenBody+  , proveIncomeGetBody+  ) where++import           Data.Proof.AuthGetBody         (HasAuthGetBody, proveAuthGetBody)+import           Data.Proof.BalanceGetBody      (HasGetBalanceBody, proveGetBalanceBody)+import           Data.Proof.IdentityGetBody     (HasIdentityGetBody, proveIdentityGetBody)+import           Data.Proof.IncomeGetBody       (HasIncomeGetBody, proveIncomeGetBody)+import           Data.Proof.Named               (name, unWrapNamed)+import           Data.Proof.PublicTokenCreate   (HasCreatePublicTokenBody,+                                                 proveCreatePublicTokenBody)+import           Data.Proof.TransactionsGetBody (HasTransactionsGetBody, proveTransactionsGetBody)
+ Data/Proof/PublicTokenCreate.hs view
@@ -0,0 +1,25 @@+module Data.Proof.PublicTokenCreate+  ( HasCreatePublicTokenBody+  , proveCreatePublicTokenBody+  ) where++import           Data.Api.Types+import           Data.Proof.Named+import           Data.Text        (Text)++data HasCreatePublicTokenBody plaidEnv institutionId productList plaidOptions = Proof++-- | Check if all elements for creating a public token are present+proveCreatePublicTokenBody+  :: Named plaidEnv PlaidEnv+  -> Named institutionId InstitutionId+  -> Named productList [PlaidProduct]+  -> Named options (Maybe PlaidOptions)+  -> Either Text (HasCreatePublicTokenBody plaidEnv institutionId productList options)+proveCreatePublicTokenBody _ institutionId productList _+  | null (unWrapNamed productList) =+      Left "Initial products list cannot be empty"+  | unWrapNamed institutionId `elem` validInstitutionIds+      = Right Proof+  | otherwise = Left (unInstitutionId (unWrapNamed institutionId)+                  <> " is not a valid institution id. Check the plaid documentation.")
+ Data/Proof/TransactionsGetBody.hs view
@@ -0,0 +1,24 @@+module Data.Proof.TransactionsGetBody+  ( HasTransactionsGetBody+  , proveTransactionsGetBody+  ) where++import           Data.Api.Types+import           Data.Proof.Named+import           Data.Text        (Text)+import           Data.Time        (Day)++data HasTransactionsGetBody plaidEnv accessToken startDate endDate options paginationOptions = Proof++proveTransactionsGetBody+  :: Named plaidEnv PlaidEnv+  -> Named accessToken AccessToken+  -> Named startDate Day+  -> Named endDate Day+  -> Named options (Maybe PlaidOptions)+  -> Named paginationOptions (Maybe PlaidPaginationOptions)+  -> Either Text (HasTransactionsGetBody plaidEnv accessToken startDate endDate options paginationOptions)+proveTransactionsGetBody _ atoken _ _ _ _ =+  case unWrapNamed atoken of+    (AccessToken "") -> Left "AccessToken cannot be empty"+    (AccessToken _ ) -> Right Proof
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Sasa Bogicevic (c) 2019++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 Sasa Bogicevic nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,29 @@+![](https://plaid.com/assets/img/navbar/logo.svg) +++[![Build Status](https://travis-ci.org/v0d1ch/plaid.png)](https://travis-ci.org/v0d1ch/plaid)+++This is a Haskell library for interacting with [plaid.com](https://plaid.com) api.++It utilizes some of the nice ideas presented in the [GDP](https://kataskeue.com/gdp.pdf) paper and in general tries to stay out of your way and let you issue plaid.com api calls easily.++You should check out the [/examples/Main.hs](https://github.com/v0d1ch/plaid/blob/master/examples/Main.hs) to see some of the examples of using+the lib.++#### Contributing+++I encourage you to play with the library, open up issues, get yourself involved.++Plan is to build nice documentation with helpful examples.++I don't have specific code style guidelines, just follow the code style that you+already see. ++There is a stylish-haskell.yaml file in the root and it would be nice if you can configure your editor to use it.++Please squash your commits into one when issuing a PR so we keep a nice git timeline.+++
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ examples/Main.hs view
@@ -0,0 +1,231 @@+module Main where++{-+ Module containing useful real world examples of using plaid library.+-}++import           Control.Exception.Safe (MonadThrow, throwM)+import           Control.Monad.Except   (liftEither, runExceptT)+import           Control.Monad.Reader+import           Data.ByteString.Lazy   (ByteString)+import           Data.Plaid+import           Data.Text              (Text)+import           Data.Time+import           Text.Pretty.Simple++-- | Client id+clientId :: ClientId+clientId = ClientId "5d11dd0fdeb8a80013cc99e9"++-- | Public key+publicKey :: PublicKey+publicKey = "f19293a734d8e9cc89faacb09719e5"++-- | Sandbox secret key+sandboxSecret :: Secret+sandboxSecret = Secret "0d5e0ec25d432388f7fb34d3528394"++-- | Public token+publicToken :: PublicToken+publicToken = PublicToken "public-sandbox-e5c3a0d9-822b-4d8c-bd13-4253485c99b2"++-- | Access token+accessToken :: AccessToken+accessToken =  AccessToken "access-sandbox-9e96926d-4da6-4620-a13b-c72980976436"++-- | Example of item id+itemId :: Text+itemId = "Qlr98nQgkdTQyM8agmjGt3a4RV6lKxup88BLV"++-- | Main function from which we call different api endpoints for demonstration+main :: IO ()+main = do+  let env = PlaidEnv publicKey clientId sandboxSecret Sandbox+  exchangeTokenResponse <- runPlaid env exampleExchangeToken+  pPrint exchangeTokenResponse+  authResponse <- runPlaid env exampleRetrieveAuthRequest+  pPrint authResponse+  transactionsResponse <- runPlaid env exampleRetrieveTransactionsRequest+  pPrint transactionsResponse+  identityResponse <- runPlaid env exampleIdentityRequest+  pPrint identityResponse+  incomeResponse <- runPlaid env exampleIncomeRequest+  pPrint incomeResponse++-- | POST /item/public_token/exchange+--  https://plaid.com/docs/#exchange-token-flow+exampleExchangeToken+  :: ( MonadReader PlaidEnv m+     , MonadIO m+     , MonadThrow m+     , PlaidHttp m+     )+  => m (Either PlaidError PlaidAccessTokenResponse)+exampleExchangeToken = do+  -- get the plaid environment+  env <- ask+  runExceptT $ do+    -- create the public token request body+    -- this action can fail if we provide wrong parameters+    publicTokenBody <- liftEither $+        mkCreatePublicTokenEnv env+          -- Pay attention to provide valid institution id! There is a list of valid ones+          -- in the docs https:://plaid.com/docs/#investments-top-institutions-we-cover+          (InstitutionId "ins_3")+          -- Provide the list of plaid products or empty list+          ["transactions"]+          -- This is optional. If you want you can setup a web-hook where plaid will send its responses.+          defaultPlaidOptions+    -- fire the create public token action+    PlaidPublicTokenResponse {..} <- lift $ plaidCreatePublicToken publicTokenBody+    -- use the response to create an exchange public token request body+    -- it looks something like this:+    -- @+    -- PlaidBody {plaidBodyEnv = PlaidEnv {plaidEnvPublicKey = "f19293a734d8e9cc89faacb09719e5", plaidEnvClientId = ClientId {unClientId = "5d11dd0fdeb8a80013cc99e9"}, plaidEnvSecret = Secret {unSecret = "0d5e0ec25d432388f7fb34d3528394"}, plaidEnvEnvironment = sandbox}, plaidBodyPublicToken = Just (PublicToken {unPublicToken = "public-sandbox-6a6d0da0-4d53-4702-a1eb-9b430625122b"}), plaidBodyAccessToken = Nothing, plaidBodyOptions = Nothing, plaidBodyInstitutionId = Nothing, plaidBodyInitialProducts = Nothing}+    -- @++    exchangeBody <- liftEither (mkExchangePublicTokenEnv env _plaidPublicTokenResponsePublicToken)+    -- fire exchange token action+    lift $ plaidExchangeToken exchangeBody+    -- if the response is successfull it looks something like this:+    -- @+    -- Right (PlaidAccessTokenResponse {plaidAccessTokenResponseAccessToken = "access-sandbox-0ca6b500-c682-437e-bdc9-051665b71941", plaidAccessTokenResponseRequestId = "2vBC1rv95r7Se60", plaidAccessTokenResponseItemId = "mdwNyLvv9zF655lGQ1bxikExVGb4doULb5VPp"})+    -- @+    +getAuthBody+  :: ( MonadReader PlaidEnv m+     , MonadIO m+     , MonadThrow m+     , PlaidHttp m+     )+  => m (Either PlaidError (PlaidBody AuthGet))+getAuthBody = do+  env <- ask+  runExceptT $ do+    publicTokenBody <- liftEither $ createPublicTokenBody env+    PlaidAccessTokenResponse {..} <- lift $ exchangeToken publicTokenBody+    liftEither (mkGetAuthEnv env _plaidAccessTokenResponseAccessToken)++-- | POST /auth/get+-- https://plaid.com/docs/#retrieve-auth-request+exampleRetrieveAuthRequest+  :: ( MonadReader PlaidEnv m+     , MonadIO m+     , MonadThrow m+     , PlaidHttp m+     )+  => m (Either PlaidError ByteString)+exampleRetrieveAuthRequest =+  -- get the plaid environment+  runExceptT $ do+    -- create the public token request body+    -- this action can fail if we provide wrong parameters+    eauthBody <- lift getAuthBody+    authBody <- liftEither eauthBody+    lift $ plaidGetAuth authBody++-- | POST /transactions/get+-- https://plaid.com/docs/#retrieve-transactions-request+exampleRetrieveTransactionsRequest+  :: ( MonadReader PlaidEnv m+     , MonadIO m+     , MonadThrow m+     , PlaidHttp m+     )+  => m (Either PlaidError ByteString)+exampleRetrieveTransactionsRequest = do+  -- get the plaid environment+  env <- ask+  runExceptT $ do+    -- create the public token request body+    -- this action can fail if we provide wrong parameters+    publicTokenBody <- liftEither $ createPublicTokenBody env+    PlaidAccessTokenResponse {..} <- lift $ exchangeToken publicTokenBody+    authBody <- liftEither (mkGetAuthEnv env _plaidAccessTokenResponseAccessToken)+    _ <- lift $ plaidGetAuth authBody+    let startTime = fromGregorian 2018 1 1+        endTime   = fromGregorian 2019 1 1+    transactionBody <-+      liftEither+        (mkCreateTransactionsGetEnv+           env+           _plaidAccessTokenResponseAccessToken+           startTime+           endTime+           Nothing+           (Just defPaginationOptions)+        )+    lift $ plaidGetTransactions transactionBody+-- | POST /income/get+-- http://plaid.com/docs/#income+exampleIncomeRequest+  :: ( MonadReader PlaidEnv m+     , MonadIO m+     , MonadThrow m+     , PlaidHttp m+     )+  => m (Either PlaidError ByteString)+exampleIncomeRequest = do+  env <- ask+  runExceptT $ do+    -- create the public token request body+    -- this action can fail if we provide wrong parameters+    publicTokenBody <- liftEither $ createPublicTokenBody env+    PlaidAccessTokenResponse {..} <- lift $ exchangeToken publicTokenBody+    incomeBody <- liftEither (mkCreateIncomeGetEnv env _plaidAccessTokenResponseAccessToken)+    lift $ plaidGetIncome incomeBody++-- | POST /identity/get+-- http://plaid.com/docs/#identity+exampleIdentityRequest+  :: ( MonadReader PlaidEnv m+     , MonadIO m+     , MonadThrow m+     , PlaidHttp m+     )+  => m (Either PlaidError ByteString)+exampleIdentityRequest = do+  env <- ask+  runExceptT $ do+    -- create the public token request body+    -- this action can fail if we provide wrong parameters+    publicTokenBody <- liftEither $ createPublicTokenBody env+    PlaidAccessTokenResponse {..} <- lift $ exchangeToken publicTokenBody+    identityBody <- liftEither (mkCreateIdentityGetEnv env _plaidAccessTokenResponseAccessToken)+    lift $ plaidGetIdentity identityBody++exchangeToken+  :: ( MonadReader PlaidEnv m+     , MonadIO m+     , MonadThrow m+     , PlaidHttp m+     )+  => PlaidBody PublicTokenCreate+  -> m PlaidAccessTokenResponse+exchangeToken publicTokenBody = do+  env <- ask+  -- fire the create public token action+  PlaidPublicTokenResponse {..} <- plaidCreatePublicToken publicTokenBody+  case mkExchangePublicTokenEnv env _plaidPublicTokenResponsePublicToken of+    Left err           -> throwM err+    Right exchangeBody -> plaidExchangeToken exchangeBody++createPublicTokenBody :: PlaidEnv -> Either PlaidError (PlaidBody PublicTokenCreate)+createPublicTokenBody env =+  mkCreatePublicTokenEnv env+    -- pay attention to provide valid institution id! There is a list of valid ones+    -- in the docs https:://plaid.com/docs/#investments-top-institutions-we-cover+    (InstitutionId "ins_3")+    -- provide the list of plaid products or empty list+    ["transactions"]+    -- this is optional. If you want you can setup a web-hook where plaid will send its responses.+    mempty++defaultPlaidOptions :: Maybe PlaidOptions+defaultPlaidOptions =+  Just $+     PlaidOptions+       { _plaidOptionsWebHook = "http://bogicevicsasa.com"+       , _plaidOptionsOverrideUsername = "user_good"+       , _plaidOptionsOverridePassword = "pass_good"+       }
+ plaid.cabal view
@@ -0,0 +1,165 @@+cabal-version: 1.12+name:           plaid+version:        0.1.0.0+synopsis:       Plaid.com api integration library+description: @Plaid@ is a library for interacting with https://plaid.com+  .+  Plaid.com connects the users to their bank accounts via the app and this library provides programmatic+  access to all these features.+  .+  Please take a look at examples folder to easily get started.+  .+  Haskell examples in the plaid [documentation] (https://plaid.com/docs) come from this library.+  .+category:       Data, Web, Network, Plaid+homepage:       https://github.com/v0d1ch/plaid#readme+bug-reports:    https://github.com/v0d1ch/plaid/issues+author:         Sasha Bogicevic+maintainer:     Sasha Bogicevic <sasa.bogicevic@pm.me>+copyright:      2019 Sasa Bogicevic+license:        BSD3+license-file:   LICENSE+build-type:     Simple+extra-source-files:+    README.md+    ChangeLog.md++source-repository head+  type: git+  location: https://github.com/v0d1ch/plaid++library+  exposed-modules:+      Data.Plaid+      Data.Common+      Data.Api.TestByteStrings+  other-modules:+      Data.Api.Helper+      Data.Api.InternalPure+      Data.Api.Link+      Data.Api.Transactions+      Data.Api.Identity+      Data.Api.Income+      Data.Api.Plaid+      Data.Api.Accounts+      Data.Api.Auth+      Data.Api.Types+      Data.Proof.TransactionsGetBody+      Data.Proof.IdentityGetBody+      Data.Proof.IncomeGetBody+      Data.Proof.AuthGetBody+      Data.Proof.BalanceGetBody+      Data.Proof.Named+      Data.Proof.Proof+      Data.Proof.PublicTokenCreate++  ghc-options: -Wall+  build-depends:+      aeson+    , base >= 4.6 && < 4.13+    , bytestring+    , mtl+    , text+    , network+    , http-conduit+    , http-client-tls+    , safe-exceptions+    , containers+    , casing+    , raw-strings-qq+    , time+    , microlens+    , microlens-th+    , pretty-simple+  default-language: Haskell2010+  default-extensions: OverloadedStrings+                      RecordWildCards+                      FlexibleContexts+                      ScopedTypeVariables+                      DataKinds+                      DeriveAnyClass+                      DeriveGeneric+                      DerivingStrategies+                      FlexibleContexts+                      FlexibleInstances+                      GADTs+                      GeneralizedNewtypeDeriving+                      InstanceSigs+                      MultiParamTypeClasses+                      TemplateHaskell+                      TypeFamilies+                      UndecidableInstances+                      StrictData+++test-suite spec+  type: exitcode-stdio-1.0+  main-is: Spec.hs+  other-modules:+      Paths_plaid+      Plaid.PlaidSpec+  hs-source-dirs:+      test+  ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N+  build-depends:+      aeson+    , base >= 4.6 && < 4.13+    , containers+    , bytestring+    , text+    , hspec+    , hspec-wai+    , hspec-wai-json+    , http-types+    , QuickCheck+    , wai+    , plaid+    , errors+    , time+    , pretty-simple+    , microlens+    , microlens-th+  default-language: Haskell2010+  default-extensions: OverloadedStrings+                      RecordWildCards+                      FlexibleContexts+                      TemplateHaskell+                      TypeFamilies+                      ScopedTypeVariables+                      StrictData+                      GADTs++executable plaid+  main-is: Main.hs+  hs-source-dirs:+      examples++  default-language: Haskell2010+  ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N+  build-depends:+      aeson+    , base >= 4.6 && < 4.13+    , bytestring+    , mtl+    , text+    , transformers+    , conduit+    , conduit-extra+    , network+    , http-client+    , http-client-tls+    , either+    , safe-exceptions+    , plaid+    , time+    , pretty-simple+    , microlens+    , microlens-th+  default-extensions: OverloadedStrings+                      RecordWildCards+                      FlexibleContexts+                      ScopedTypeVariables+                      TemplateHaskell+                      TypeFamilies+                      GADTs+                      StrictData
+ test/Plaid/PlaidSpec.hs view
@@ -0,0 +1,162 @@+module Plaid.PlaidSpec (spec) where++import qualified Control.Error            as ER+import           Data.Api.TestByteStrings+import           Data.ByteString.Char8    (unpack)+import           Data.Common+import           Data.Either              (isRight)+import           Data.Plaid+import qualified Data.Text                as T+import           Data.Time                (fromGregorian)+import           Debug.Trace+import           Test.Hspec++_runTestGetRequest :: PlaidHttp m => Text -> m ByteString+_runTestGetRequest url = executeGet url++runTestPostRequest :: (PlaidHttp m, ToJSON a) => Text -> a -> m ByteString+runTestPostRequest url postBody = executePost url postBody++checkResult :: FromJSON a => ByteString -> Either PlaidError a+checkResult result =+  case result of+    "" -> Left (PlaidError $ T.pack $ "Failed to decode bytestring " ++ (unpack $ toStrict result))+    _  ->+      case eitherDecode' result of+        Left e   -> Left $ PlaidError (T.pack e)+        Right ok -> Right ok++env :: PlaidEnv+env = PlaidEnv "" (ClientId "") (Secret "") Sandbox++publicTokenBody :: Either PlaidError (PlaidBody PublicTokenCreate)+publicTokenBody =+  mkCreatePublicTokenEnv env+    (InstitutionId "ins_3")+    ["transactions"]+    Nothing+    -- (Just $+    --   PlaidOptions+    --   "http://bogicevicsasa.com"+    --   "user_good"+    --   "pass_good"+    -- )+    +spec :: Spec+spec = do+  describe "Create public token" $ do+    it "/public_token/create" $ do+      let result = runTestPlaid $ runTestPostRequest "/public_token/create" publicTokenBody+          testResult = checkResult result :: Either PlaidError PlaidPublicTokenResponse+      isRight testResult `shouldBe` True++  describe "Exchange public token" $ do+    it "/item/public_token/exchange" $ do+      let result = runTestPlaid $ runTestPostRequest "/public_token/create" publicTokenBody+      testResult <- ER.runExceptT $ do+        PlaidPublicTokenResponse {..} <- ER.hoistEither (checkResult result :: Either PlaidError PlaidPublicTokenResponse)+        exchangeBody <- ER.hoistEither (mkExchangePublicTokenEnv env _plaidPublicTokenResponsePublicToken)+        let result' = runTestPlaid $ runTestPostRequest "/item/public_token/exchange" exchangeBody+        ER.hoistEither (checkResult result' :: Either PlaidError PlaidAccessTokenResponse)+      isRight testResult `shouldBe` True++  describe "Auth get" $ do+    it "/auth/get" $ do+      let result = runTestPlaid $ runTestPostRequest "/public_token/create" publicTokenBody+      testResult <- ER.runExceptT $ do+        PlaidPublicTokenResponse {..} <- ER.hoistEither (checkResult result :: Either PlaidError PlaidPublicTokenResponse)+        exchangeBody <- ER.hoistEither (mkExchangePublicTokenEnv env _plaidPublicTokenResponsePublicToken)+        let result' = runTestPlaid $ runTestPostRequest "/item/public_token/exchange" exchangeBody+        PlaidAccessTokenResponse {..} <- ER.hoistEither (checkResult result' :: Either PlaidError PlaidAccessTokenResponse)+        authBody <- ER.hoistEither (mkGetAuthEnv env _plaidAccessTokenResponseAccessToken)+        let authGetResult = runTestPlaid $ runTestPostRequest "/auth/get" authBody+        ER.hoistEither (checkResult authGetResult :: Either PlaidError PlaidAuthGetResponse)+      isRight testResult `shouldBe` True++  describe "Identity get" $ do+    it "/identity/get" $ do+      let result = runTestPlaid $ runTestPostRequest "/public_token/create" publicTokenBody+      testResult <- ER.runExceptT $ do+        PlaidPublicTokenResponse {..} <- ER.hoistEither (checkResult result :: Either PlaidError PlaidPublicTokenResponse)+        exchangeBody <- ER.hoistEither (mkExchangePublicTokenEnv env _plaidPublicTokenResponsePublicToken)+        let result' = runTestPlaid $ runTestPostRequest "/item/public_token/exchange" exchangeBody+        PlaidAccessTokenResponse {..} <- ER.hoistEither (checkResult result' :: Either PlaidError PlaidAccessTokenResponse)+        identityBody <- ER.hoistEither (mkCreateIdentityGetEnv env _plaidAccessTokenResponseAccessToken)+        let identityGetResult = runTestPlaid $ runTestPostRequest "/identity/get" identityBody+        ER.hoistEither (checkResult identityGetResult :: Either PlaidError PlaidIdentityGetResponse)+      isRight testResult `shouldBe` True+  describe "Transactions get" $ do+    it "/transactions/get" $ do+      let result = runTestPlaid $ runTestPostRequest "/public_token/create" publicTokenBody+      testResult <- ER.runExceptT $ do+        PlaidPublicTokenResponse {..} <- ER.hoistEither (checkResult result :: Either PlaidError PlaidPublicTokenResponse)+        exchangeBody <- ER.hoistEither (mkExchangePublicTokenEnv env _plaidPublicTokenResponsePublicToken)+        let result' = runTestPlaid $ runTestPostRequest "/item/public_token/exchange" exchangeBody+        PlaidAccessTokenResponse {..} <- ER.hoistEither (checkResult result' :: Either PlaidError PlaidAccessTokenResponse)+        authBody <- ER.hoistEither (mkGetAuthEnv env _plaidAccessTokenResponseAccessToken)+        let authGetResult = runTestPlaid $ runTestPostRequest "/auth/get" authBody+        PlaidAuthGetResponse {..} <- ER.hoistEither (checkResult authGetResult :: Either PlaidError PlaidAuthGetResponse)+        let startTime = fromGregorian 2018 1 1+        let endTime   = fromGregorian 2019 1 1+        transactionBody <-+          ER.hoistEither (mkCreateTransactionsGetEnv+                           env+                           _plaidAccessTokenResponseAccessToken+                           startTime+                           endTime+                           Nothing+                           Nothing+                         )++        let transactionsGetResult =+              runTestPlaid $ runTestPostRequest "/transactions/get" transactionBody+        ER.hoistEither (checkResult transactionsGetResult :: Either PlaidError PlaidTransactionsGetResponse)+      isRight testResult `shouldBe` True+  describe "Balance json" $ do+    it "should be able to decode Balance" $ do+      let result = eitherDecode' balanceJson :: Either String Balance+      isRight result `shouldBe` True+  describe "Account json" $ do+    it "should be able to decode Account" $ do+      let result = eitherDecode' accountJson :: Either String Account+      isRight result `shouldBe` True+  describe "Account list json" $ do+    it "should be able to decode Account list" $ do+      let result = eitherDecode' accountListJson :: Either String [Account]+      isRight result `shouldBe` True+  describe "PaymentMeta json" $ do+    it "should be able to decode PaymentMeta" $ do+      let result = eitherDecode' accountJson :: Either String PaymentMeta+      isRight result `shouldBe` True+  describe "Item json" $ do+    it "should be able to decode Item" $ do+      let result = eitherDecode' itemJson :: Either String Item+      isRight result `shouldBe` True+  describe "Transaction json" $ do+    it "should be able to decode Transaction" $ do+      let result = eitherDecode' transactionJson :: Either String Transaction+      isRight result `shouldBe` True+  describe "TransactionLocation json" $ do+    it "should be able to decode TransactionLocation" $ do+      let result = eitherDecode' transactionLocationJson :: Either String TransactionLocation+      isRight result `shouldBe` True+  describe "email json" $ do+    it "should be able to decode Email" $ do+      let result = eitherDecode' emailJson :: Either String Email+      isRight result `shouldBe` True+  describe "phone json" $ do+    it "should be able to decode Phone" $ do+      let result = eitherDecode' phoneNumberJson :: Either String PhoneNumber+      isRight result `shouldBe` True+  describe "address json" $ do+    it "should be able to decode Addresses" $ do+      let result = eitherDecode' addressesJson :: Either String Addresses+      isRight result `shouldBe` True+  describe "Accounts json" $ do+    it "should be able to decode Accounts" $ do+      let result = eitherDecode' accountsJson :: Either String Accounts+      isRight result `shouldBe` True+  describe "Owners json" $ do+    it "should be able to decode Owners" $ do+      let result = eitherDecode' ownersJson :: Either String Owners+      isRight result `shouldBe` True
+ test/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}