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 +3/−0
- Data/Api/Accounts.hs +27/−0
- Data/Api/Auth.hs +17/−0
- Data/Api/Helper.hs +26/−0
- Data/Api/Identity.hs +17/−0
- Data/Api/Income.hs +17/−0
- Data/Api/InternalPure.hs +131/−0
- Data/Api/Link.hs +48/−0
- Data/Api/Plaid.hs +26/−0
- Data/Api/TestByteStrings.hs +880/−0
- Data/Api/Transactions.hs +18/−0
- Data/Api/Types.hs +769/−0
- Data/Common.hs +25/−0
- Data/Plaid.hs +21/−0
- Data/Proof/AuthGetBody.hs +19/−0
- Data/Proof/BalanceGetBody.hs +20/−0
- Data/Proof/IdentityGetBody.hs +20/−0
- Data/Proof/IncomeGetBody.hs +20/−0
- Data/Proof/Named.hs +15/−0
- Data/Proof/Proof.hs +25/−0
- Data/Proof/PublicTokenCreate.hs +25/−0
- Data/Proof/TransactionsGetBody.hs +24/−0
- LICENSE +30/−0
- README.md +29/−0
- Setup.hs +2/−0
- examples/Main.hs +231/−0
- plaid.cabal +165/−0
- test/Plaid/PlaidSpec.hs +162/−0
- test/Spec.hs +1/−0
+ 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://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 #-}