packages feed

quickbooks (empty) → 0.5.0.1

raw patch · 12 files changed

+1843/−0 lines, 12 filesdep +aesondep +authenticate-oauthdep +basesetup-changed

Dependencies added: aeson, authenticate-oauth, base, bytestring, doctest, email-validate, fast-logger, http-client, http-client-tls, http-types, interpolate, old-locale, text, thyme, yaml

Files

+ LICENSE view
@@ -0,0 +1,31 @@+Copyright (c) 2016, Plow Technologies++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 the copyright holder nor the names of its+      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.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ quickbooks.cabal view
@@ -0,0 +1,102 @@+name:                quickbooks+version:             0.5.0.1+synopsis:            QuickBooks API binding.+license:             BSD3+license-file:        LICENSE+author:              Stack Builders+maintainer:          hackage@stackbuilders.com+copyright:           Plow Technologies+category:            Web+build-type:          Simple+cabal-version:       >=1.10++description: This package is a binding to the QuickBooks API. The+             documentation can be test using the `cabal test doctests`+             command. However, the documentation tests are run against+             an Intuit sandbox which you must create.++             .++             Visit Intuit's developer site to create an "app":+             <https://developer.intuit.com/>++             .++             After creating an app you can use your "app token" and+             consumer information to aquire credentials here:+             <https://appcenter.intuit.com/Playground/OAuth>++             .++             This package is configure by environment variables. Once+             you have your credentials please export the following+             environment:+             .+             * INTUIT_COMPANY_ID (your app's company ID)+             .+             * INTUIT_CONSUMER_KEY (your app's consumer key)+             .+             * INTUIT_CONSUMER_SECRET (your app's consumer secret)+             .+             * INTUIT_TOKEN (the OAuth1.0 Token you created above *used for tests*)+             .+             * INTUIT_SECRET (the OAuth1.0 Secret you created above *used for tests*)+             .+             * INTUIT_HOSTNAME (which will be+               sandbox-quickbooks.api.intuit.com for development/test+               environments. See the developer website for production)++             .++             You can now run verify the documentation using  `cabal test`.++source-repository head+  type:     git+  location: https://github.com/stackbuilders/quickbooks++library+  exposed-modules:     QuickBooks+                       QuickBooks.Authentication+                       QuickBooks.Customer+                       QuickBooks.Invoice+                       QuickBooks.Item+                       QuickBooks.Logging+                       QuickBooks.Types+++  hs-source-dirs:      src+  default-language:    Haskell2010+  ghc-options:         -Wall+  build-depends:         base >= 4.7   && < 5+                         -- We restrict the version of aeson until+                         -- https://github.com/bos/aeson/issues/293+                         -- is fixed.+                       , aeson <= 0.9.0.1+                       , authenticate-oauth+                       , bytestring+                       , http-client+                       , http-types+                       , http-client-tls+                       , interpolate+                       , text+                       , fast-logger+                       , thyme+                       , old-locale+                       , email-validate+                       , yaml++test-suite doctests+  type:             exitcode-stdio-1.0+  default-language: Haskell2010+  ghc-options:      -Wall -threaded+  hs-source-dirs:   src, test+  main-is:          doctests.hs++  other-modules:   Data++  build-depends:+    base >= 4.7 && < 5,+    doctest++  buildable:+    False
+ src/QuickBooks.hs view
@@ -0,0 +1,419 @@+{-# LANGUAGE ImplicitParams    #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeFamilies      #-}++------------------------------------------------------------------------------+-- |+-- Module: QuickBooks+--+-- For more information, see:+--+--   * QuickBooks API Reference:+--     <https://developer.intuit.com/docs/api/accounting>.+--+------------------------------------------------------------------------------++module QuickBooks+  ( -- * Authentication and authorization+    getAccessTokens+  , getAccessTokens'+  , getTempTokens+  , getTempTokens'+  , authorizationURLForToken+  , cancelOAuthAuthorization+  , cancelOAuthAuthorization'+    -- * Transaction entities+    -- ** Invoices+    -- *** Types+  , module QuickBooks.Types+    -- *** CRUD an invoice+  , createInvoice+  , createInvoice'+  , readInvoice+  , readInvoice'+  , updateInvoice+  , updateInvoice'+  , deleteInvoice+  , deleteInvoice'+    -- *** Send an invoice via email+  , EmailAddress+  , emailAddress+  , sendInvoice+  , sendInvoice'+    -- *** Read in configuration files+  , readAPIConfigFromFile+  , readAppConfigFromFile+    -- * Name list entities+    -- ** Customer+  , queryCustomer+  , queryCustomer'+    -- ** Line+  , queryItem+  , queryItem'+  ) where++import QuickBooks.Authentication+import QuickBooks.Types hiding (EmailAddress,emailAddress)++import Control.Applicative     ((<$>),(<*>), (<|>))+import Control.Arrow           (second)+import Data.ByteString.Char8   (pack)+import Data.Maybe              (fromJust)+import Data.Text               (Text)+import Network.HTTP.Client.TLS (tlsManagerSettings)+import Network.HTTP.Client     (newManager)+import System.Environment      (getEnvironment)+import Text.Email.Validate     (EmailAddress, emailAddress)+++import QuickBooks.Customer+import QuickBooks.Invoice      ( createInvoiceRequest+                               , deleteInvoiceRequest+                               , readInvoiceRequest+                               , updateInvoiceRequest+                               , sendInvoiceRequest+                               )+import QuickBooks.Item+import QuickBooks.Logging      (apiLogger, getLogger)+import Data.Yaml (ParseException, decodeFileEither)++-- $setup+--+-- >>> import Data+--+-- >>> :set -XOverloadedStrings+-- >>> maybeTestOAuthToken <- lookupTestOAuthTokenFromEnv+-- >>> let oAuthToken = maybe (error "") id maybeTestOAuthToken++-- |+--+-- >>> :{+--   do eitherQueryCustomer <-+--        queryCustomer oAuthToken "Rondonuwu Fruit and Vegi"+--      case eitherQueryCustomer of+--        Right (QuickBooksCustomerResponse (customer:_)) ->+--          print (customerId customer)+--        _ ->+--          putStrLn "Nothing"+-- :}+-- Just "21"++queryCustomer+  :: OAuthToken+  -> Text+  -> IO (Either String (QuickBooksResponse [Customer]))+queryCustomer tok =+  queryQuickBooks tok . QueryCustomer++queryCustomer'+  :: APIConfig+  -> AppConfig+  -> OAuthToken+  -> Text+  -> IO (Either String (QuickBooksResponse [Customer]))+queryCustomer' apiConfig appConfig tok =+  queryQuickBooks' apiConfig appConfig tok . QueryCustomer++-- |+--+-- >>> :{+--   do eitherQueryItem <- queryItem oAuthToken "Hours"+--      case eitherQueryItem of+--        Right (QuickBooksItemResponse (item:_)) ->+--          print (itemId item)+--        _ ->+--          putStrLn "Nothing"+-- :}+-- Just "2"++queryItem+  :: OAuthToken+  -> Text+  -> IO (Either String (QuickBooksResponse [Item]))+queryItem tok =+  queryQuickBooks tok . QueryItem++queryItem'+  :: APIConfig+  -> AppConfig+  -> OAuthToken+  -> Text+  -> IO (Either String (QuickBooksResponse [Item]))+queryItem' apiConfig appConfig tok =+  queryQuickBooks' apiConfig appConfig tok . QueryItem++-- | Create an invoice.+--+-- Example:+--+-- >>> import Data.Maybe (fromJust)+-- >>> :{+-- do resp <- createInvoice oAuthToken testInvoice+--    case resp of+--      Left err -> putStrLn $ "My custom error message: " ++ err+--      Right (QuickBooksInvoiceResponse invoice) -> do+--        deleteInvoice oAuthToken (fromJust (invoiceId invoice)) (fromJust (invoiceSyncToken invoice))+--        putStrLn "I created an invoice!"+-- :}+-- I created an invoice!+--+-- Note that we deleted the item we created using 'deleteInvoice'.++createInvoice :: OAuthToken -> Invoice -> IO (Either String (QuickBooksResponse Invoice))+createInvoice tok = queryQuickBooks tok . CreateInvoice++-- | Like createInvoice but accepts an APIConfig rather than reading it from the environment+createInvoice' :: APIConfig -> AppConfig -> OAuthToken -> Invoice -> IO (Either String (QuickBooksResponse Invoice))+createInvoice' apiConfig appConfig tok = queryQuickBooks' apiConfig appConfig tok . CreateInvoice+++-- | Retrieve the details of an invoice that has been previously created.+--+-- Example:+--+-- First, we create an invoice (see 'createInvoice'):+--+-- >>> import Data.Maybe (fromJust)+-- >>> Right (QuickBooksInvoiceResponse cInvoice) <- createInvoice oAuthToken testInvoice+--+-- Then, we read the invoice and test that it is the same invoice we created:+--+-- >>> let cInvoiceId = fromJust (invoiceId cInvoice)+-- >>> :{+-- do eitherReadInvoice <- readInvoice oAuthToken cInvoiceId+--    case eitherReadInvoice of+--      Left _ -> return False+--      Right (QuickBooksInvoiceResponse rInvoice) -> return (cInvoice == rInvoice)+-- :}+-- True+--+-- Finally, we delete the invoice we created:+--+-- >>> deleteInvoice oAuthToken cInvoiceId (fromJust (invoiceSyncToken cInvoice))++readInvoice ::  OAuthToken -> InvoiceId -> IO (Either String (QuickBooksResponse Invoice))+readInvoice tok = queryQuickBooks tok . ReadInvoice++-- | Like readInvoice but accepts an APIConfig rather than reading it from the environment+readInvoice' :: APIConfig -> AppConfig -> OAuthToken -> InvoiceId -> IO (Either String (QuickBooksResponse Invoice))+readInvoice' apiConfig appConfig tok = queryQuickBooks' apiConfig appConfig tok . ReadInvoice++-- | Update an invoice.+--+-- Example:+--+-- First, we create an invoice (see 'createInvoice'):+--+-- >>> import Data.Maybe (fromJust)+-- >>> Right (QuickBooksInvoiceResponse cInvoice) <- createInvoice oAuthToken testInvoice+--+-- Then, we update the customer reference of the invoice:+--+-- >>> let nInvoice = cInvoice { invoiceCustomerRef = Reference Nothing Nothing "1" }+-- >>> :{+-- do eitherUpdateInvoice <- updateInvoice oAuthToken nInvoice+--    case eitherUpdateInvoice of+--      Left _ -> return False+--      Right (QuickBooksInvoiceResponse uInvoice) ->+--        return (invoiceCustomerRef cInvoice == invoiceCustomerRef uInvoice)+-- :}+-- False+--+-- Finally, we delete the invoice we created:+--+-- >>> deleteInvoice oAuthToken (fromJust (invoiceId cInvoice)) (fromJust (invoiceSyncToken cInvoice))++updateInvoice ::  OAuthToken -> Invoice -> IO (Either String (QuickBooksResponse Invoice))+updateInvoice tok = queryQuickBooks tok . UpdateInvoice++-- | Like updateInvoice but accepts an APIConfig rather than reading it from the environment+updateInvoice' :: APIConfig -> AppConfig -> OAuthToken -> Invoice -> IO (Either String (QuickBooksResponse Invoice))+updateInvoice' apiConfig appConfig tok = queryQuickBooks' apiConfig appConfig tok . UpdateInvoice++-- | Delete an invoice.+--+-- Example:+--+-- First, we create an invoice (see 'createInvoice'):+--+-- >>> import Data.Maybe (fromJust)+-- >>> Right (QuickBooksInvoiceResponse cInvoice) <- createInvoice oAuthToken testInvoice+--+-- Then, we delete it:+--+-- >>> let cInvoiceId = fromJust (invoiceId cInvoice)+-- >>> let cInvoiceSyncToken = fromJust (invoiceSyncToken cInvoice)+-- >>> :{+-- do eitherDeleteInvoice <- deleteInvoice oAuthToken cInvoiceId cInvoiceSyncToken+--    case eitherDeleteInvoice of+--      Left e -> putStrLn e+--      Right _ -> putStrLn "I deleted an invoice!"+-- :}+-- I deleted an invoice!++deleteInvoice ::  OAuthToken -> InvoiceId -> SyncToken -> IO (Either String (QuickBooksResponse DeletedInvoice))+deleteInvoice tok iId = queryQuickBooks tok . DeleteInvoice iId++-- | Like deleteInvoice but accepts an APIConfig rather than reading it from the environment+deleteInvoice' :: APIConfig -> AppConfig -> OAuthToken -> InvoiceId -> SyncToken -> IO (Either String (QuickBooksResponse DeletedInvoice))+deleteInvoice' apiConfig appConfig tok iId = queryQuickBooks' apiConfig appConfig tok . DeleteInvoice iId+++-- | Send an invoice via email.+--+-- Example:+--+-- First, we create an invoice (see 'createInvoice'):+--+-- >>> import Data.Maybe (fromJust)+-- >>> Right (QuickBooksInvoiceResponse cInvoice) <- createInvoice oAuthToken testInvoice+--+-- Then, we send the invoice via email:+--+-- >>> let cInvoiceId = fromJust (invoiceId cInvoice)+-- >>> let testEmail = fromJust (emailAddress "test@test.com")+-- >>> :{+-- do eitherSendInvoice <- sendInvoice oAuthToken cInvoiceId testEmail+--    case eitherSendInvoice of+--      Left e  -> putStrLn e+--      Right _ -> putStrLn "I sent an invoice!"+-- :}+-- I sent an invoice!+--+-- Finally, we delete the invoice we created:+--+-- >>> deleteInvoice oAuthToken cInvoiceId (fromJust (invoiceSyncToken cInvoice))++sendInvoice ::  OAuthToken -> InvoiceId -> EmailAddress -> IO (Either String (QuickBooksResponse Invoice))+sendInvoice tok invId = queryQuickBooks tok . SendInvoice invId++sendInvoice' :: APIConfig+             -> AppConfig+             -> OAuthToken+             -> InvoiceId+             -> EmailAddress+             -> IO (Either String (QuickBooksResponse Invoice))+sendInvoice' apiConfig appConfig tok invId  =+  queryQuickBooks' apiConfig appConfig tok . SendInvoice invId++-- | Get temporary tokens to request permission.+--+-- Example:+--+-- >>> :{+-- do eitherTempTokens <- getTempTokens "localhost"+--    case eitherTempTokens of+--      Left e -> putStrLn e+--      Right _ -> putStrLn "I got my request tokens!"+-- :}+-- ...+-- I got my request tokens!++getTempTokens :: CallbackURL -> IO (Either String (QuickBooksResponse OAuthToken))+getTempTokens =+  queryQuickBooksOAuth Nothing . GetTempOAuthCredentials++getTempTokens' :: AppConfig -> CallbackURL -> IO (Either String (QuickBooksResponse OAuthToken))+getTempTokens' appConfig =+  queryQuickBooksOAuth' appConfig Nothing . GetTempOAuthCredentials+ +-- | Exchange oauth_verifier for access tokens+getAccessTokens :: OAuthToken -> OAuthVerifier -> IO (Either String (QuickBooksResponse OAuthToken))+getAccessTokens tempToken =+  queryQuickBooksOAuth (Just tempToken) . GetAccessTokens++getAccessTokens' :: AppConfig          -- Your application's consumer key and consumer secret+                 -> OAuthToken         -- The temporary OAuth tokens obtained from getTempTokens+                 -> OAuthVerifier      -- The OAuthVerifier returned by QuickBooks when it calls your callback+                 -> IO (Either String (QuickBooksResponse OAuthToken))+getAccessTokens' appConfig tempToken = do+  queryQuickBooksOAuth' appConfig (Just tempToken) . GetAccessTokens++-- | Invalidate an OAuth access token and disconnect from QuickBooks.+cancelOAuthAuthorization :: OAuthToken -> IO (Either String (QuickBooksResponse ()))+cancelOAuthAuthorization tok =+  queryQuickBooksOAuth (Just tok)  DisconnectQuickBooks++cancelOAuthAuthorization' :: AppConfig+                          -> OAuthToken+                          -> IO (Either String (QuickBooksResponse ()))+cancelOAuthAuthorization' appConfig tok =+  queryQuickBooksOAuth' appConfig (Just tok) DisconnectQuickBooks++queryQuickBooks :: OAuthToken -> QuickBooksQuery a -> IO (Either String (QuickBooksResponse a))+queryQuickBooks tok query = do+  apiConfig <- readAPIConfig+  appConfig <- readAppConfig+  queryQuickBooks' apiConfig appConfig tok query++queryQuickBooks' :: APIConfig -> AppConfig -> OAuthToken -> QuickBooksQuery a -> IO (Either String (QuickBooksResponse a))+queryQuickBooks' apiConfig appConfig tok query = do+  manager   <- newManager tlsManagerSettings+  logger    <- getLogger apiLogger+  let ?appConfig = appConfig+  let ?apiConfig = apiConfig+  let ?manager   = manager+  let ?logger    = logger+  case query of+    CreateInvoice invoice               -> createInvoiceRequest tok invoice+    ReadInvoice _invoiceId              -> readInvoiceRequest tok _invoiceId+    UpdateInvoice invoice               -> updateInvoiceRequest tok invoice+    DeleteInvoice _invoiceId syncToken  -> deleteInvoiceRequest tok _invoiceId syncToken+    SendInvoice _invoiceId emailAddr    -> sendInvoiceRequest tok _invoiceId emailAddr    +    QueryCustomer queryCustomerName     -> queryCustomerRequest tok queryCustomerName+    QueryItem queryItemName             -> queryItemRequest tok queryItemName++queryQuickBooksOAuth :: Maybe OAuthToken+                     -> QuickBooksOAuthQuery a+                     -> IO (Either String (QuickBooksResponse a))+queryQuickBooksOAuth maybeOAuthToken query = do+  appConfig <- readAppConfig+  queryQuickBooksOAuth' appConfig maybeOAuthToken query ++queryQuickBooksOAuth' :: AppConfig+                      -> Maybe OAuthToken+                      -> QuickBooksOAuthQuery a+                      -> IO (Either String (QuickBooksResponse a))+queryQuickBooksOAuth' appConfig maybeOauthToken query = do+  manager   <- newManager tlsManagerSettings+  logger    <- getLogger apiLogger+  let ?appConfig = appConfig+  let ?manager   = manager+  let ?logger    = logger+  case query of+    (GetTempOAuthCredentials callbackURL) -> getTempOAuthCredentialsRequest callbackURL+    (GetAccessTokens oauthVerifier)       -> getAccessTokensRequest (fromJust maybeOauthToken) oauthVerifier+    DisconnectQuickBooks                  -> disconnectRequest (fromJust maybeOauthToken)++readAPIConfig :: IO APIConfig+readAPIConfig = do+  env <- getEnvironment+  case lookupAPIConfig env of+    Just config -> return config+    Nothing     -> fail "The environment variables INTUIT_COMPANY_ID,INTUIT_TOKEN,INTUIT_SECRET, and INTUIT_HOSTNAME must be set"++readAppConfig :: IO AppConfig+readAppConfig = do+  env <- getEnvironment+  case lookupAppConfig env of+    Just config -> return config+    Nothing     -> fail "The evironment variables INTUIT_CONSUMER_KEY and INTUIT_CONSUMER_SECRET must be set"++lookupAPIConfig :: [(String, String)] -> Maybe APIConfig+lookupAPIConfig environment = APIConfig <$> lookup "INTUIT_COMPANY_ID" env+                                        <*> lookup "INTUIT_TOKEN" env+                                        <*> lookup "INTUIT_SECRET" env+                                        <*> lookup "INTUIT_HOSTNAME" env+                                        <*> (lookup "INTUIT_API_LOGGING_ENABLED" env <|> Just "true")+    where env = map (second pack) environment++readAPIConfigFromFile :: FilePath -> IO (Either ParseException APIConfig)+readAPIConfigFromFile = decodeFileEither++readAppConfigFromFile :: FilePath -> IO (Either ParseException AppConfig)+readAppConfigFromFile = decodeFileEither++lookupAppConfig :: [(String, String)] -> Maybe AppConfig+lookupAppConfig environment = AppConfig <$> lookup "INTUIT_CONSUMER_KEY" env+                                        <*> lookup "INTUIT_CONSUMER_SECRET" env+    where env = map (second pack) environment
+ src/QuickBooks/Authentication.hs view
@@ -0,0 +1,130 @@+{-# LANGUAGE ImplicitParams     #-}+{-# LANGUAGE OverloadedStrings  #-}+{-# LANGUAGE RankNTypes         #-}+{-# LANGUAGE ConstraintKinds    #-}++module QuickBooks.Authentication+  ( getTempOAuthCredentialsRequest+  , getAccessTokensRequest+  , oauthSignRequest+  , authorizationURLForToken+  , disconnectRequest+  ) where++import Control.Monad (void, liftM, ap)+import Data.Monoid ((<>))+import qualified Data.ByteString.Lazy as BSL+import Data.ByteString.Char8 (unpack, ByteString)+import Network.HTTP.Client (Manager+                           ,Request(..)+                           ,RequestBody(RequestBodyLBS)+                           ,responseBody+                           ,parseUrl+                           ,httpLbs)+import Network.HTTP.Types.URI (parseSimpleQuery)+import Web.Authenticate.OAuth (signOAuth+                              ,newCredential+                              ,emptyCredential+                              ,injectVerifier+                              ,newOAuth+                              ,OAuth(..))++import QuickBooks.Logging (logAPICall')+import QuickBooks.Types++getTempOAuthCredentialsRequest :: AppEnv+                               => CallbackURL+                               -> IO (Either String (QuickBooksResponse OAuthToken)) -- ^ Temporary OAuthToken+getTempOAuthCredentialsRequest callbackURL =+  return.handleQuickBooksTokenResponse "Couldn't get temporary tokens" =<< tokensRequest+    where tokensRequest = getTokens temporaryTokenURL "?oauth_callback=" callbackURL oauthSignRequestWithEmptyCredentials++getAccessTokensRequest :: AppEnv+                       => OAuthToken                                         -- ^ Temporary Token+                       -> OAuthVerifier                                      -- ^ OAuthVerifier provided by QuickBooks+                       -> IO (Either String (QuickBooksResponse OAuthToken)) -- ^ OAuthToken                                                                                 +getAccessTokensRequest tempToken verifier =+  return.handleQuickBooksTokenResponse "Couldn't get access tokens" =<< tokensRequest+  where+    tokensRequest = getTokens accessTokenURL "?oauth_token=" (unpack $ token tempToken)+                                                             (oauthSignRequestWithVerifier verifier tempToken)+                    +disconnectRequest :: AppEnv+                  => OAuthToken+                  -> IO (Either String (QuickBooksResponse ()))+disconnectRequest tok = do+  req  <- parseUrl $ disconnectURL+  req' <- oauthSignRequest tok req+  void $ httpLbs req' ?manager+  logAPICall' req'+  return $ Right QuickBooksVoidResponse+    +getTokens :: AppEnv+          => String                  -- ^ Endpoint to request the token+          -> String                  -- ^ URL parameter name+          -> String                  -- ^ URL parameter value+          -> ((?appConfig :: AppConfig) => Request -> IO Request) -- ^ Signing function+          -> IO (Maybe OAuthToken)+getTokens tokenURL parameterName parameterValue signRequest = do+  request  <- parseUrl $ concat [tokenURL, parameterName, parameterValue]+  request' <- signRequest request { method="POST", requestBody = RequestBodyLBS "" }+  response <- httpLbs request' ?manager+  logAPICall' request'+  return $ tokensFromResponse (responseBody response)++oauthSignRequestWithVerifier :: (?appConfig :: AppConfig)+                             => OAuthVerifier+                             -> OAuthToken+                             -> Request+                             -> IO Request+oauthSignRequestWithVerifier verifier tempTokens = signOAuth oauthApp credsWithVerifier+  where+    credentials       = newCredential (token tempTokens)+                                      (tokenSecret tempTokens)+    credsWithVerifier = injectVerifier (unOAuthVerifier verifier) credentials+    oauthApp          = newOAuth { oauthConsumerKey    = consumerToken ?appConfig+                                 , oauthConsumerSecret = consumerSecret ?appConfig }++oauthSignRequest :: (?appConfig :: AppConfig)+                 => OAuthToken+                 -> Request+                 -> IO Request+oauthSignRequest tok req = signOAuth oauthApp credentials req+  where+    credentials = newCredential (token tok)+                                (tokenSecret tok)+    oauthApp    = newOAuth { oauthConsumerKey    = consumerToken ?appConfig+                           , oauthConsumerSecret = consumerSecret ?appConfig }++oauthSignRequestWithEmptyCredentials :: (?appConfig :: AppConfig)+                                     => Request+                                     -> IO Request+oauthSignRequestWithEmptyCredentials = signOAuth oauthApp credentials+  where+    credentials = emptyCredential+    oauthApp    = newOAuth { oauthConsumerKey    = consumerToken ?appConfig+                           , oauthConsumerSecret = consumerSecret ?appConfig }++disconnectURL :: String+disconnectURL = "https://appcenter.intuit.com/api/v1/connection/disconnect"++accessTokenURL :: String+accessTokenURL = "https://oauth.intuit.com/oauth/v1/get_access_token"++temporaryTokenURL :: String+temporaryTokenURL = "https://oauth.intuit.com/oauth/v1/get_request_token"++authorizationURL :: ByteString+authorizationURL = "https://appcenter.intuit.com/Connect/Begin"++authorizationURLForToken :: OAuthToken -> ByteString +authorizationURLForToken oatoken = authorizationURL <> "?oauth_token=" <> (token oatoken)+ +handleQuickBooksTokenResponse :: String -> Maybe OAuthToken -> Either String (QuickBooksResponse OAuthToken)+handleQuickBooksTokenResponse _ (Just tokensInResponse) = Right $ QuickBooksAuthResponse tokensInResponse+handleQuickBooksTokenResponse errorMessage Nothing      = Left errorMessage++tokensFromResponse :: BSL.ByteString -> Maybe OAuthToken+tokensFromResponse response = OAuthToken `liftM` lookup "oauth_token" responseParams+                                         `ap` lookup "oauth_token_secret" responseParams+  where responseParams = parseSimpleQuery (BSL.toStrict response)
+ src/QuickBooks/Customer.hs view
@@ -0,0 +1,64 @@+{-# LANGUAGE ImplicitParams    #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes       #-}+{-# LANGUAGE RecordWildCards   #-}+{-# LANGUAGE ConstraintKinds   #-}++------------------------------------------------------------------------------+-- |+-- Module      : QuickBooks.Customer+-- Description :+-- Copyright   :+-- License     :+-- Maintainer  :+-- Stability   :+-- Portability :+--+--+--+------------------------------------------------------------------------------++module QuickBooks.Customer+  ( queryCustomerRequest+  )+  where++import QuickBooks.Authentication+import QuickBooks.Logging+import QuickBooks.Types++import qualified Data.Aeson as Aeson+import Data.String.Interpolate (i)+import Data.Text (Text)+import Network.HTTP.Client+import Network.HTTP.Types.Header (hAccept)++-- GET /v3/company/<companyID>/query=<selectStatement>++queryCustomerRequest :: APIEnv +                     => OAuthToken+                     -> Text+                     -> IO (Either String (QuickBooksResponse [Customer]))+queryCustomerRequest tok queryCustomerName = do+  let apiConfig = ?apiConfig+  let queryURI = parseUrl [i|#{queryURITemplate apiConfig}#{query}|]+  req <- oauthSignRequest tok =<< queryURI+  let oauthHeaders = requestHeaders req+  let req' = req { method = "GET"+                 , requestHeaders = oauthHeaders ++ [(hAccept, "application/json")]+                 }+  resp <- httpLbs req' ?manager+  logAPICall req'+  let eitherAllCustomers = Aeson.eitherDecode (responseBody resp)+  case eitherAllCustomers of+    Left er -> return (Left er)+    Right (QuickBooksCustomerResponse allCustomers) ->+      return $ Right $ QuickBooksCustomerResponse $+        filter (\Customer{..} -> customerDisplayName == queryCustomerName) allCustomers+  where+    query :: String+    query = "SELECT * FROM Customer"++queryURITemplate :: APIConfig -> String+queryURITemplate APIConfig{..} =+  [i|https://#{hostname}/v3/company/#{companyId}/query?query=|]
+ src/QuickBooks/Invoice.hs view
@@ -0,0 +1,141 @@+{-# LANGUAGE ImplicitParams    #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes       #-}+{-# LANGUAGE RecordWildCards   #-}+{-# LANGUAGE ConstraintKinds   #-}+{-# LANGUAGE TypeOperators     #-}+{-# LANGUAGE RankNTypes        #-}+{-# LANGUAGE PolyKinds         #-}+------------------------------------------------------------------------------+-- |+-- Module      : QuickBooks.Requests+-- Description :+-- Copyright   :+-- License     :+-- Maintainer  :+-- Stability   :+-- Portability :+--+--+--+------------------------------------------------------------------------------++module QuickBooks.Invoice+ ( createInvoiceRequest+ , readInvoiceRequest+ , updateInvoiceRequest+ , deleteInvoiceRequest+ , sendInvoiceRequest+ ) where++import Data.Aeson                (encode, eitherDecode, object, Value(String))+import Data.String.Interpolate   (i)+import Network.HTTP.Client       (httpLbs+                                 ,parseUrl+                                 ,Request(..)+                                 ,RequestBody(..)+                                 ,Response(responseBody))+import Network.HTTP.Types.Header (hAccept,hContentType)++import QuickBooks.Authentication (oauthSignRequest)++import QuickBooks.Types (APIConfig(..)+                        ,Invoice+                        ,InvoiceId(..)+                        ,QuickBooksResponse+                        ,SyncToken(..)+                        ,DeletedInvoice(..)+                        ,OAuthToken+                        ,APIEnv)++import Text.Email.Validate (EmailAddress, toByteString)+import QuickBooks.Logging  (logAPICall)++-- | Create an invoice.+createInvoiceRequest :: APIEnv+                     => OAuthToken+                     -> Invoice                        +                     -> IO (Either String (QuickBooksResponse Invoice))+createInvoiceRequest tok = postInvoice tok+                           +-- | Update an invoice.+updateInvoiceRequest :: APIEnv+                     => OAuthToken+                     -> Invoice+                     -> IO (Either String (QuickBooksResponse Invoice))+updateInvoiceRequest tok = postInvoice tok++-- | Read an invoice.+readInvoiceRequest :: APIEnv+                   => OAuthToken+                   -> InvoiceId+                   -> IO (Either String (QuickBooksResponse Invoice))+readInvoiceRequest tok iId = do+  let apiConfig = ?apiConfig+  req  <- oauthSignRequest tok =<< parseUrl [i|#{invoiceURITemplate apiConfig}#{unInvoiceId iId}|]+  let oauthHeaders = requestHeaders req+  let req' = req{method = "GET", requestHeaders = oauthHeaders ++ [(hAccept, "application/json")]}+  resp <-  httpLbs req' ?manager+  logAPICall req'+  return $ eitherDecode $ responseBody resp++-- | Delete an invoice.+deleteInvoiceRequest :: APIEnv+                     => OAuthToken+                     -> InvoiceId+                     -> SyncToken+                     -> IO (Either String (QuickBooksResponse DeletedInvoice))+deleteInvoiceRequest tok iId syncToken = do+  let apiConfig = ?apiConfig+  req  <- parseUrl [i|#{invoiceURITemplate apiConfig}?operation=delete|]+  req' <- oauthSignRequest tok req{ method = "POST"+                                  , requestBody    = RequestBodyLBS $ encode body+                                  , requestHeaders = [ (hAccept, "application/json")+                                                     , (hContentType, "application/json")+                                                     ]+                                  }+  resp <-  httpLbs req' ?manager+  logAPICall req'+  return $ eitherDecode $ responseBody resp+  where+    body = object [ ("Id", String (unInvoiceId iId))+                  , ("SyncToken", String (unSyncToken syncToken))+                  ]++-- | email and invoice+sendInvoiceRequest :: APIEnv+                   => OAuthToken+                   -> InvoiceId +                   -> EmailAddress +                   -> IO (Either String (QuickBooksResponse Invoice))+sendInvoiceRequest tok iId emailAddr =  do+  let apiConfig = ?apiConfig+  req  <- parseUrl [i|#{invoiceURITemplate apiConfig}#{unInvoiceId iId}/send?sendTo=#{toByteString emailAddr}|]+  req' <- oauthSignRequest tok req{ method = "POST"+                                  , requestHeaders = [ (hAccept, "application/json")+                                                     ]+                                  }+  logAPICall req'+  resp <-  httpLbs req' ?manager+  return $ eitherDecode $ responseBody resp++invoiceURITemplate :: APIConfig -> String+invoiceURITemplate APIConfig{..} = [i|https://#{hostname}/v3/company/#{companyId}/invoice/|]+++postInvoice :: APIEnv+            => OAuthToken+            -> Invoice+            -> IO (Either String (QuickBooksResponse Invoice))+postInvoice tok invoice = do+  let apiConfig = ?apiConfig+  req <- parseUrl [i|#{invoiceURITemplate apiConfig}|]+  req' <- oauthSignRequest tok req{ method         = "POST"+                                  , requestBody    = RequestBodyLBS $ encode invoice+                                  , requestHeaders = [ (hAccept, "application/json")+                                                     , (hContentType, "application/json")+                                                     ]+                                  } +  resp <- httpLbs req' ?manager+  logAPICall req'+  return $ eitherDecode $ responseBody resp
+ src/QuickBooks/Item.hs view
@@ -0,0 +1,64 @@+{-# LANGUAGE ImplicitParams    #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes       #-}+{-# LANGUAGE RecordWildCards   #-}+{-# LANGUAGE ConstraintKinds   #-}++------------------------------------------------------------------------------+-- |+-- Module      : QuickBooks.Item+-- Description :+-- Copyright   :+-- License     :+-- Maintainer  :+-- Stability   :+-- Portability :+--+--+--+------------------------------------------------------------------------------++module QuickBooks.Item+  ( queryItemRequest+  )+  where++import QuickBooks.Authentication+import QuickBooks.Logging+import QuickBooks.Types++import qualified Data.Aeson as Aeson+import Data.String.Interpolate (i)+import Data.Text (Text)+import Network.HTTP.Client+import Network.HTTP.Types.Header (hAccept)++-- GET /v3/company/<companyID>/query=<selectStatement>++queryItemRequest :: APIEnv+                 => OAuthToken+                 -> Text+                 -> IO (Either String (QuickBooksResponse [Item]))+queryItemRequest tok queryItemName = do+  let apiConfig = ?apiConfig+  let queryURI = parseUrl [i|#{queryURITemplate apiConfig}#{query}|]+  req <- oauthSignRequest tok =<< queryURI+  let oauthHeaders = requestHeaders req+  let req' = req { method = "GET"+                 , requestHeaders = oauthHeaders ++ [(hAccept, "application/json")]+                 }+  resp <- httpLbs req' ?manager+  logAPICall req'+  let eitherAllItems = Aeson.eitherDecode (responseBody resp)+  case eitherAllItems of+    Left er -> return (Left er)+    Right (QuickBooksItemResponse allItems) ->+      return $ Right $ QuickBooksItemResponse $+        filter (\Item{..} -> itemName == queryItemName) allItems+  where+    query :: String+    query = "SELECT * FROM Item"++queryURITemplate :: APIConfig -> String+queryURITemplate APIConfig{..} =+  [i|https://#{hostname}/v3/company/#{companyId}/query?query=|]
+ src/QuickBooks/Logging.hs view
@@ -0,0 +1,55 @@+{-# LANGUAGE ImplicitParams    #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes       #-}+{-# LANGUAGE RecordWildCards   #-}++module QuickBooks.Logging ( logAPICall+                          , logAPICall'+                          , Logger+                          , apiLogger+                          , getLogger+                          ) where++import Control.Monad             (when)+import Data.Char                 (toLower)+import Data.IORef+import Data.Monoid               ((<>))+import Data.String               (fromString)+import Data.String.Interpolate   (i)+import Data.Thyme+import Network.HTTP.Client       (Request(..),RequestBody(..),getUri)+import System.IO.Unsafe+import System.Locale +import System.Log.FastLogger     (LogStr, pushLogStr, flushLogStr, newStdoutLoggerSet)+import qualified Data.ByteString.Char8 as BS++import QuickBooks.Types (APIConfig(..), Logger)++apiLogger :: IORef Logger+apiLogger = unsafePerformIO $ newIORef =<< newStdoutLoggerSet 0++getLogger :: IORef Logger -> IO Logger+getLogger = readIORef++logAPICall :: ( ?logger :: Logger+              , ?apiConfig :: APIConfig+              ) => Request -> IO ()+logAPICall req =+  let isLoggingEnabled = BS.map toLower (loggingEnabled ?apiConfig)+      in when (isLoggingEnabled == "true") $ logAPICall' req++logAPICall' :: (?logger :: Logger) => Request -> IO ()+logAPICall' req = do +  now <- getCurrentTime+  let formattedTime = fromString $ formatTime defaultTimeLocale rfc822DateFormat now+  pushLogStr ?logger (requestLogLine req formattedTime)+  flushLogStr ?logger++requestLogLine :: Request -> LogStr -> LogStr+requestLogLine req formattedTime =+  let body = case requestBody req of+              (RequestBodyLBS bs) -> show bs +              (RequestBodyBS bs)  -> show bs+              _                   -> ""+  in formattedTime <> fromString [i| [INFO] [#{method req}] [#{getUri req}] [#{body}]\n|]+
+ src/QuickBooks/Types.hs view
@@ -0,0 +1,721 @@+{-# LANGUAGE GADTs                      #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE ImplicitParams             #-}+{-# LANGUAGE OverloadedStrings          #-}+{-# LANGUAGE QuasiQuotes                #-}+{-# LANGUAGE RecordWildCards            #-}+{-# LANGUAGE TemplateHaskell            #-}+{-# LANGUAGE TypeFamilies               #-}+{-# LANGUAGE ConstraintKinds            #-}+++------------------------------------------------------------------------------+-- |+-- Module      : QuickBooks.Types+-- Description :+-- Copyright   :+-- License     :+-- Maintainer  :+-- Stability   :+-- Portability :+--+--+--+------------------------------------------------------------------------------++module QuickBooks.Types where++import           Control.Applicative ((<$>), (<*>))+import           Control.Monad       (mzero)+import           Data.Aeson          (FromJSON (..), ToJSON(..), Value (Object),+                                      (.:), object, (.=))+import           Data.Aeson.TH       (Options (fieldLabelModifier, omitNothingFields),+                                      defaultOptions, deriveJSON)+import           Data.ByteString     (ByteString)+import           Data.Char           (toLower)+import           Data.Text           (Text)+import           Data.Text.Encoding  (encodeUtf8, decodeUtf8)+import           Prelude             hiding (lines)+import qualified Text.Email.Validate as E (EmailAddress)+import           System.Log.FastLogger (LoggerSet)+import           Network.HTTP.Client   (Manager)++type Logger = LoggerSet++type CallbackURL = String++newtype OAuthVerifier = OAuthVerifier { unOAuthVerifier :: ByteString }+  deriving (Show, Eq)++-- | QuickBooks Application Keys++data AppConfig = AppConfig+  { consumerToken  :: !ByteString+  , consumerSecret :: !ByteString+  } deriving (Show, Eq)++instance FromJSON AppConfig where+  parseJSON (Object o) = AppConfig <$> (parseByteString o "consumerToken")+                                   <*> (parseByteString o "consumerSecret")+    where parseByteString obj name = encodeUtf8 <$> (obj .: name)+  parseJSON _ = mzero+  +data APIConfig = APIConfig+  { companyId      :: !ByteString+  , oauthToken     :: !ByteString+  , oauthSecret    :: !ByteString+  , hostname       :: !ByteString+  , loggingEnabled :: !ByteString+  } deriving (Show, Eq)++instance FromJSON APIConfig where+  parseJSON (Object o) = APIConfig <$> (parseByteString o "companyId")+                                   <*> (parseByteString o "oauthToken")+                                   <*> (parseByteString o "oauthSecret")+                                   <*> (parseByteString o "hostname")+                                   <*> (parseByteString o "loggingEnabled")+    where parseByteString obj name = encodeUtf8 <$> (obj .: name)+  parseJSON _ = mzero++instance ToJSON APIConfig where+  toJSON (APIConfig cId oToken oSecret hName lEnabled) = object [+                                                                  "companyId" .= (decodeUtf8 cId),+                                                                  "oauthToken" .= (decodeUtf8 oToken),+                                                                  "oauthSecret" .= (decodeUtf8 oSecret),+                                                                  "hostname" .= (decodeUtf8 hName),+                                                                  "loggingEnabled" .= (decodeUtf8 lEnabled)+                                                                ]+                                                                +  +type APIEnv = ( ?apiConfig :: APIConfig+              , AppEnv               +              , NetworkEnv              +              )++type AppEnv = ( ?appConfig :: AppConfig+              , NetworkEnv   +              )+              +type NetworkEnv = ( ?manager :: Manager+                  , ?logger  :: Logger+                  )+             +-- | A request or access OAuth token.++data OAuthToken = OAuthToken+  { token       :: ByteString+  , tokenSecret :: ByteString+  } deriving (Show, Eq)++data family QuickBooksResponse a+data instance QuickBooksResponse Invoice = QuickBooksInvoiceResponse { quickBooksResponseInvoice :: Invoice }+data instance QuickBooksResponse DeletedInvoice = QuickBooksDeletedInvoiceResponse DeletedInvoice+data instance QuickBooksResponse OAuthToken = QuickBooksAuthResponse { tokens :: OAuthToken }+data instance QuickBooksResponse () = QuickBooksVoidResponse++data instance QuickBooksResponse [Customer] =+  QuickBooksCustomerResponse { quickBooksResponseCustomer :: [Customer] }++data instance QuickBooksResponse [Item] =+  QuickBooksItemResponse { quickBooksResponseItem :: [Item] }++instance FromJSON (QuickBooksResponse Invoice) where+  parseJSON (Object o) = QuickBooksInvoiceResponse `fmap` (o .: "Invoice")+  parseJSON _          = fail "Could not parse invoice response from QuickBooks"++instance FromJSON (QuickBooksResponse DeletedInvoice) where+  parseJSON (Object o) = QuickBooksDeletedInvoiceResponse `fmap` (o .: "Invoice")+  parseJSON _          = fail "Could not parse deleted invoice response from QuickBooks"++instance FromJSON (QuickBooksResponse [Customer]) where+  parseJSON (Object o) = do+    let customers =+          o .: "QueryResponse" >>= \queryResponse -> queryResponse .: "Customer"+    fmap QuickBooksCustomerResponse customers+  parseJSON _          = fail "Could not parse customer response from QuickBooks"++instance FromJSON (QuickBooksResponse [Item]) where+  parseJSON (Object o) = do+    let items =+          o .: "QueryResponse" >>= \queryResponse -> queryResponse .: "Item"+    fmap QuickBooksItemResponse items+  parseJSON _          = fail "Could not parse item response from QuickBooks"++type QuickBooksQuery a = QuickBooksRequest (QuickBooksResponse a)+type QuickBooksOAuthQuery a = QuickBooksOAuthRequest (QuickBooksResponse a) ++data QuickBooksOAuthRequest a where+  GetTempOAuthCredentials :: CallbackURL   -> QuickBooksOAuthQuery OAuthToken+  GetAccessTokens         :: OAuthVerifier -> QuickBooksOAuthQuery OAuthToken+  DisconnectQuickBooks    :: QuickBooksOAuthQuery ()++data QuickBooksRequest a where+  CreateInvoice           :: Invoice     -> QuickBooksQuery Invoice+  ReadInvoice             :: InvoiceId   -> QuickBooksQuery Invoice+  UpdateInvoice           :: Invoice     -> QuickBooksQuery Invoice+  DeleteInvoice           :: InvoiceId   -> SyncToken -> QuickBooksQuery DeletedInvoice+  SendInvoice             :: InvoiceId   -> E.EmailAddress -> QuickBooksQuery Invoice++  QueryCustomer           :: Text -> QuickBooksQuery [Customer]+  QueryItem               :: Text -> QuickBooksQuery [Item]++newtype InvoiceId = InvoiceId {unInvoiceId :: Text}+  deriving (Show, Eq, FromJSON, ToJSON)++newtype LineId    = LineId {unLineId :: Text}+  deriving (Show, Eq, FromJSON, ToJSON)++newtype SyncToken = SyncToken { unSyncToken :: Text }+  deriving (Show, Eq, FromJSON, ToJSON)++-- | Details of a description line.++data DescriptionLineDetail = DescriptionLineDetail+  { descriptionLineDetailServiceDate :: !(Maybe Text)+  , descriptionLineDetailTaxCodeRef  :: !(Maybe TaxCodeRef)+  }+  deriving (Show, Eq)++-- | Details of a discount line.++data DiscountLineDetail = DiscountLineDetail+  { discountLineDetailDiscountRef        :: !(Maybe DiscountRef)+  , discountLineDetailPercentBased       :: !(Maybe Bool)+  , discountLineDetailDiscountPercent    :: !(Maybe Double)+  , discountLineDetailDiscountAccountRef :: !(Maybe DiscountAccountRef)+  }+  deriving (Show, Eq)++-- | Details of a sales item line.+-- In order to create a sales item line detail, use 'salesItemLineDetail'.++data SalesItemLineDetail = SalesItemLineDetail+  { salesItemLineDetailItemRef         :: !(Maybe ItemRef)+  , salesItemLineDetailClassRef        :: !(Maybe ClassRef)+  , salesItemLineDetailUnitPrice       :: !(Maybe Double)+  , salesItemLineDetailRatePercent     :: !(Maybe Double)+  , salesItemLineDetailPriceLevelRef   :: !(Maybe PriceLevelRef)+  , salesItemLineDetailMarkupInfo      :: !(Maybe Text)+  , salesItemLineDetailQty             :: !(Maybe Double)+  , salesItemLineDetailTaxCodeRef      :: !(Maybe TaxCodeRef)+  , salesItemLineDetailServiceData     :: !(Maybe Text)+  , salesItemLineDetailTaxInclusiveAmt :: !(Maybe Double)+  }+  deriving (Show, Eq)++-- | Create a sales item line detail with a reference to an item.+--+-- Example:+--+-- >>> let aSalesItemLineDetail = salesItemLineDetail ((reference "1") {referenceName = Just "Services"})+-- >>> salesItemLineDetailItemRef aSalesItemLineDetail+-- Just (Reference {referenceName = Just "Services", referenceType = Nothing, referenceValue = "1"})++salesItemLineDetail :: ItemRef -> SalesItemLineDetail+salesItemLineDetail itemRef =+  SalesItemLineDetail (Just itemRef)+                      Nothing+                      Nothing+                      Nothing+                      Nothing+                      Nothing+                      Nothing+                      Nothing+                      Nothing+                      Nothing++-- | Details of a subtotal line.++data SubTotalLineDetail = SubTotalLineDetail+  { subtotalLineDetailItemRef :: !(Maybe ItemRef) }+  deriving (Show, Eq)++-- | An individual line item of a transaction.++data Line = Line+  { lineId                    :: !(Maybe LineId)+  , lineLineNum               :: !(Maybe Double)+  , lineDescription           :: !(Maybe Text)+  , lineAmount                :: !(Maybe Double)+  , lineLinkedTxn             :: !(Maybe [LinkedTxn])+  , lineDetailType            :: !Text+  , lineDescriptionLineDetail :: !(Maybe DescriptionLineDetail)+  , lineDiscountLineDetail    :: !(Maybe DiscountLineDetail)+  , lineSalesItemLineDetail   :: !(Maybe SalesItemLineDetail)+  , lineSubTotalLineDetail    :: !(Maybe SubTotalLineDetail)+  , lineCustomField           :: !(Maybe [CustomField])+  }+  deriving (Show, Eq)++-- | Create a sales item line with amount and details.+--+-- Example:+--+-- >>> let aSalesItemLineDetail = salesItemLineDetail ((reference "1") {referenceName = Just "Services"})+-- >>> let aSalesItemLine = salesItemLine 100.0 aSalesItemLineDetail++salesItemLine :: Double+              -> SalesItemLineDetail+              -> Line+salesItemLine amount detail =+  Line Nothing+       Nothing+       Nothing+       (Just amount)+       Nothing+       "SalesItemLineDetail"+       Nothing+       Nothing+       (Just detail)+       Nothing+       Nothing+-- | if you are using a salesItemLineDetail that has a Qty and a price it generates amount automatically+emptySalesItemLine :: Line+emptySalesItemLine =+  Line Nothing+       Nothing+       Nothing+       Nothing+       Nothing+       "SalesItemLineDetail"+       Nothing+       Nothing+       Nothing+       Nothing+       Nothing+++newtype DeletedInvoiceId = DeletedInvoiceId { unDeletedInvoiceId :: Text }+  deriving (Show, Eq, FromJSON, ToJSON)++data DeletedInvoice = DeletedInvoice+  { deletedInvoiceId     :: !DeletedInvoiceId+  , deletedInvoicedomain :: !Text+  , deletedInvoicestatus :: !Text+  } deriving (Show, Eq)++-- | A reference.+-- In order to create a reference, use 'reference'.++data Reference = Reference+  { referenceName  :: !(Maybe Text)+  , referenceType  :: !(Maybe Text)+  , referenceValue :: !Text+  }+  deriving (Show, Eq)++-- | Create a reference with a value.+--+-- Example:+--+-- >>> reference "21"+-- Reference {referenceName = Nothing, referenceType = Nothing, referenceValue = "21"}++reference :: Text -> Reference+reference = Reference Nothing Nothing++type ClassRef            = Reference+type CurrencyRef         = Reference++-- | A reference to a customer or a job.+--+-- Example:+--+-- >>> (reference "21") {referenceName = Just "John Doe"} :: CustomerRef+-- Reference {referenceName = Just "John Doe", referenceType = Nothing, referenceValue = "21"}++type CustomerRef         = Reference++type DepartmentRef       = Reference+type DepositToAccountRef = Reference+type DiscountAccountRef  = Reference+type DiscountRef         = Reference+type ItemRef             = Reference+type PriceLevelRef       = Reference+type SalesTermRef        = Reference+type ShipMethodRef       = Reference+type TaxCodeRef          = Reference+type TxnTaxCodeRef       = Reference++data ModificationMetaData = ModificationMetaData+  { modificationMetaDataCreateTime      :: !Text+  , modificationMetaDataLastUpdatedTime :: !Text+  }+  deriving (Show, Eq)++data TelephoneNumber = TelephoneNumber+  { telephoneNumberFreeFormNumber :: !Text+  }+  deriving (Eq, Show)++data WebSiteAddress = WebAddress+  { webSiteAddressURI :: !Text+  }+  deriving (Eq, Show)++data PhysicalAddress = PhysicalAddress+  { physicalAddressId                     :: !Text+  , physicalAddressLine1                  :: !Text+  , physicalAddressLine2                  :: !(Maybe Text)+  , physicalAddressLine3                  :: !(Maybe Text)+  , physicalAddressLine4                  :: !(Maybe Text)+  , physicalAddressLine5                  :: !(Maybe Text)+  , physicalAddressCity                   :: !Text+  , physicalAddressCountry                :: !(Maybe Text)+  , physicalAddressCountrySubDivisionCode :: !Text+  , physicalAddressPostalCode             :: !Text+  , physicalAddressNote                   :: !(Maybe Text)+  , physicalAddressLat                    :: !(Maybe Text)+  , physicalAddressLong                   :: !(Maybe Text)+  }+  deriving (Show, Eq)++type BillAddr = PhysicalAddress+type ShipAddr = PhysicalAddress++data EmailAddress = EmailAddress+  { emailAddress :: !Text+  }+  deriving (Show, Eq)++data TxnTaxDetail = TxnTaxDetail+  { txnTaxDetailTxnTaxCodeRef :: !(Maybe TxnTaxCodeRef)+  , txnTaxDetailTotalTax      :: !Double+  , txnTaxDetailTaxLine       :: !(Maybe Line)+  }+  deriving (Show, Eq)++data DeliveryInfo = DeliveryInfo+  { deliveryInfoDeliveryType :: !(Maybe Text)+  , deliveryInfoDeliveryTime :: !(Maybe Text)+  }+  deriving (Show, Eq)++data LinkedTxn = LinkedTxn+  { linkedTxnId     :: !(Maybe Text)+  , linkedTxnType   :: !(Maybe Text)+  , linkedTxnLineId :: !(Maybe Text)+  }+  deriving (Show, Eq)++data CustomField = CustomField+  { customFieldDefinitionId :: !Text+  , customFieldName         :: !Text+  , customFieldType         :: !CustomFieldType+  , customFieldStringValue  :: !(Maybe Text)+  , customFieldBooleanValue :: !(Maybe Bool)+  , customFieldDateValue    :: !(Maybe Text)+  , customFieldNumberValue  :: !(Maybe Double)+  }+  deriving (Show, Eq)++data CustomFieldType+  = BooleanType+  | DateType+  | NumberType+  | StringType+  deriving (Show, Eq)++data GlobalTaxModel+  = NotApplicable+  | TaxExcluded+  | TaxInclusive+  deriving (Show, Eq)++-- | An invoice transaction entity, that is, a sales form where the customer+-- pays for a product or service later.+--+-- Business rules:+--+--   * An invoice must have at least one 'Line' that describes an item.+--   * An invoice must have a 'CustomerRef'.+--+-- In order to create an invoice, use 'defaultInvoice'.++data Invoice = Invoice+  { invoiceId                    :: !(Maybe InvoiceId)+  , invoiceSyncToken             :: !(Maybe SyncToken)+  , invoiceMetaData              :: !(Maybe ModificationMetaData)+  , invoiceCustomField           :: !(Maybe [CustomField])+  , invoiceDocNumber             :: !(Maybe Text)+  , invoiceTxnDate               :: !(Maybe Text)+  , invoiceDepartmentRef         :: !(Maybe DepartmentRef)+  , invoiceCurrencyRef           :: !(Maybe CurrencyRef) -- Non-US+  , invoiceExchangeRate          :: !(Maybe Double) -- Non-US+  , invoicePrivateNote           :: !(Maybe Text)+  , invoiceLinkedTxn             :: !(Maybe [LinkedTxn])+  , invoiceLine                  :: ![Line]+  , invoiceTxnTaxDetail          :: !(Maybe TxnTaxDetail)+  , invoiceCustomerRef           :: !CustomerRef+  , invoiceCustomerMemo          :: !(Maybe Text)+  , invoiceBillAddr              :: !(Maybe BillAddr)+  , invoiceShipAddr              :: !(Maybe ShipAddr)+  , invoiceClassRef              :: !(Maybe ClassRef)+  , invoiceSalesTermRef          :: !(Maybe SalesTermRef)+  , invoiceDueDate               :: !(Maybe Text)+  , invoiceGlobalTaxCalculation  :: !(Maybe GlobalTaxModel) -- Non-US+  , invoiceShipMethodRef         :: !(Maybe ShipMethodRef)+  , invoiceShipDate              :: !(Maybe Text)+  , invoiceTrackingNum           :: !(Maybe Text)+  , invoiceTotalAmt              :: !(Maybe Double)+  , invoiceHomeTotalAmt          :: !(Maybe Double) -- Non-US+  , invoiceApplyTaxAfterDiscount :: !(Maybe Bool)+  , invoicePrintStatus           :: !(Maybe Text)+  , invoiceEmailStatus           :: !(Maybe Text)+  , invoiceBillEmail             :: !(Maybe EmailAddress)+  , invoiceDeliveryInfo          :: !(Maybe DeliveryInfo)+  , invoiceBalance               :: !(Maybe Double)+  , invoiceDepositToAccountRef   :: !(Maybe DepositToAccountRef)+  , invoiceDeposit               :: !(Maybe Double)++  , invoiceAllowIPNPayment       :: !(Maybe Bool)+  , invoiceDomain                :: !(Maybe Text)+  , invoiceSparse                :: !(Maybe Bool)+  }+  deriving (Show, Eq)++-- | Create an 'Invoice' with the minimum elements.+--+-- Example:+--+-- >>> let customer21 = reference "21" :: CustomerRef+-- >>> let aSalesItemLineDetail = salesItemLineDetail ((reference "1") {referenceName = Just "Services"})+-- >>> let aSalesItemLine = salesItemLine 100.0 aSalesItemLineDetail+--+-- >>> let anInvoice = defaultInvoice [aSalesItemLine] customer21++defaultInvoice :: [Line]      -- ^ The line items of a transaction+               -> CustomerRef -- ^ Reference to a customer or a job+               -> Invoice+defaultInvoice [] _ = error "Bad invoice"+defaultInvoice lines customerRef =+  Invoice Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          lines+          Nothing+          customerRef+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing++data InvoiceResponse = InvoiceResponse+  { invoiceResponseInvoice :: Invoice }+  deriving (Show, Eq)++data CustomerResponse = CustomerResponse+  { customerResponseCustomer :: !Customer+  }+  deriving (Eq, Show)++data Customer = Customer+  { customerId                      :: !(Maybe Text)+  , customerSyncToken               :: !(Maybe SyncToken)+  , customerMetaData                :: !(Maybe ModificationMetaData)+  , customerTitle                   :: !(Maybe Text) -- def null+  , customerGivenName               :: !(Maybe Text) -- max 25 def null+  , customerMiddleName              :: !(Maybe Text) -- max 25, def null+  , customerFamilyName              :: !(Maybe Text) -- max 25, def null+  , customerSuffix                  :: !(Maybe Text) -- max 10, def null+  , customerFullyQualifiedName      :: !(Maybe Text)+  , customerCompanyName             :: !(Maybe Text) -- max 50, def null+  , customerDisplayName             :: !Text -- unique+  , customerPrintOnCheckName        :: !(Maybe Text) -- max 100+  , customerActive                  :: !(Maybe Bool) -- def true+  , customerPrimaryPhone            :: !(Maybe TelephoneNumber)+  , customerAlternatePhone          :: !(Maybe TelephoneNumber)+  , customerMobile                  :: !(Maybe TelephoneNumber)+  , customerFax                     :: !(Maybe TelephoneNumber)+  , customerPrimaryEmailAddress     :: !(Maybe EmailAddress)+  , customerWebAddr                 :: !(Maybe WebSiteAddress)+  , customerDefaultTaxCodeRef       :: !(Maybe TaxCodeRef)+  , customerTaxable                 :: !(Maybe Bool)+  , customerBillAddr                :: !(Maybe BillAddr)+  , customerShipAddr                :: !(Maybe ShipAddr)+  , customerNotes                   :: !(Maybe Text) -- max 2000+  , customerJob                     :: !(Maybe Bool) -- def false or null+  , customerBillWithParent          :: !(Maybe Bool) -- def false or null+  , customerParentRef               :: !(Maybe CustomerRef)+  , customerLevel                   :: !(Maybe Int) -- def 0, up to 5+  , customerSalesTermRef            :: !(Maybe SalesTermRef)+  , customerPaymentMethodRef        :: !(Maybe Reference)+  , customerBalance                 :: !(Maybe Double)+  , customerOpenBalanceDate         :: !(Maybe Text)+  , customerBalanceWithJobs         :: !(Maybe Double)+  , customerCurrencyRef             :: !(Maybe CurrencyRef)+  , customerPreferredDeliveryMethod :: !(Maybe Text)+  , customerResaleNum               :: !(Maybe Text) -- max 15+  }+  deriving (Eq, Show)++data ItemResponse = ItemResponse+  { itemResponseItem :: !Item+  }+  deriving (Eq, Show)++data Item = Item+  { itemId                   :: !(Maybe Text)+  , itemSyncToken            :: !(Maybe SyncToken)+  , itemMetaData             :: !(Maybe ModificationMetaData)+  , itemName                 :: !Text -- max 100+  , itemDescription          :: !(Maybe Text) -- max 4000+  , itemActive               :: !(Maybe Bool) -- def true+  , itemSubItem              :: !(Maybe Bool) -- def false or null+  , itemParentRef            :: !(Maybe ItemRef) -- def null+  , itemLevel                :: !(Maybe Int) -- def 0 up to 5+  , itemFullyQualifiedName   :: !(Maybe String) -- def null+  , itemTaxable              :: !(Maybe Bool) -- US only+  , itemSalesTaxInclusive    :: !(Maybe Bool) -- def false+  , itemUnitPrice            :: !(Maybe Double) -- max 99999999999, def 0+  , itemType                 :: !(Maybe Text) -- def Inventory (Inventory/Service)+  , itemIncomeAccountRef     :: !(Maybe Reference) -- required+  , itemPurchaseDesc         :: !(Maybe String) -- max 1000+  , itemPurchaseTaxInclusive :: !(Maybe Bool) -- def false+  , itemPurchaseCost         :: !(Maybe Double) -- max 99999999999+  , itemExpenseAccountRef    :: !(Maybe Reference) -- required+  , itemAssetAccountRef      :: !(Maybe Reference) -- req for inventory items+  , itemTrackQtyOnHand       :: !(Maybe Bool) -- def false+  , itemQtyOnHand            :: !(Maybe Double) -- req for inventory items+  , itemSalesTaxCodeRef      :: !(Maybe Reference)+  , itemPurchaseTaxCodeRef   :: !(Maybe Reference)+  , itemInvStartDate         :: !(Maybe Text) -- required for inventory items+  }+  deriving (Eq, Show)++$(deriveJSON defaultOptions+               { fieldLabelModifier = drop 8+               , omitNothingFields  = True+               }+             ''Customer)++$(deriveJSON defaultOptions+               { fieldLabelModifier = drop 16+               }+             ''CustomerResponse)++$(deriveJSON defaultOptions+               { fieldLabelModifier = drop 4+               , omitNothingFields  = True+               }+             ''Item)++$(deriveJSON defaultOptions+               { fieldLabelModifier = drop 12+               }+             ''ItemResponse)++$(deriveJSON defaultOptions+               { fieldLabelModifier = drop 15+               }+             ''TelephoneNumber)++$(deriveJSON defaultOptions+               { fieldLabelModifier = drop 14+               }+             ''WebSiteAddress)++$(deriveJSON defaultOptions+               { fieldLabelModifier = drop 11+               , omitNothingFields  = True }+             ''CustomField)++$(deriveJSON defaultOptions+             ''CustomFieldType)++$(deriveJSON defaultOptions+               { fieldLabelModifier = drop 12 }+             ''DeliveryInfo)++$(deriveJSON defaultOptions+               { fieldLabelModifier = drop 21+               , omitNothingFields  = True }+             ''DescriptionLineDetail)++$(deriveJSON defaultOptions+               { fieldLabelModifier = drop 18+               , omitNothingFields  = True }+             ''DiscountLineDetail)++$(deriveJSON defaultOptions+               { fieldLabelModifier = drop 5 }+             ''EmailAddress)++$(deriveJSON defaultOptions+             ''GlobalTaxModel)++$(deriveJSON defaultOptions+               { fieldLabelModifier = drop 7+               , omitNothingFields  = True }+             ''Invoice)++$(deriveJSON defaultOptions+               { fieldLabelModifier = drop 15 }+             ''InvoiceResponse)++$(deriveJSON defaultOptions+               { fieldLabelModifier = drop 4 }+             ''Line)++$(deriveJSON defaultOptions+               { fieldLabelModifier = drop 6+               , omitNothingFields  = True }+             ''LinkedTxn)++$(deriveJSON defaultOptions+               { fieldLabelModifier = drop 20 }+             ''ModificationMetaData)++$(deriveJSON defaultOptions+               { fieldLabelModifier = drop 15 }+             ''PhysicalAddress)++$(deriveJSON defaultOptions+               { fieldLabelModifier = map toLower . drop 9+               , omitNothingFields  = True }+             ''Reference)++$(deriveJSON defaultOptions+               { fieldLabelModifier = drop 19+               , omitNothingFields  = True }+             ''SalesItemLineDetail)++$(deriveJSON defaultOptions+               { fieldLabelModifier = drop 18+               , omitNothingFields  = True }+             ''SubTotalLineDetail)++$(deriveJSON defaultOptions+               { fieldLabelModifier = drop 12 }+             ''TxnTaxDetail)++$(deriveJSON defaultOptions+               { fieldLabelModifier = drop (length ("deletedInvoice" :: String)) }+             ''DeletedInvoice)
+ test/Data.hs view
@@ -0,0 +1,110 @@+{-# LANGUAGE OverloadedStrings #-}++module Data where++import           Control.Monad         (ap, liftM)+import           Data.ByteString.Char8 (pack)+import           Data.Maybe            (fromJust)+import           Data.String+import           QuickBooks.Types+import           System.Environment    (getEnvironment)+import qualified Text.Email.Validate   as E (EmailAddress, emailAddress)++lookupTestOAuthTokenFromEnv :: IO (Maybe OAuthToken)+lookupTestOAuthTokenFromEnv = do+  env <- getEnvironment+  return $ OAuthToken `liftM` (pack `fmap` (lookup "INTUIT_TOKEN" env))+                      `ap`    (pack `fmap` (lookup "INTUIT_SECRET" env))++trashEmailAccount :: (IsString a) => a+trashEmailAccount = "xvh221@sharklasers.com"++testEmail :: E.EmailAddress+testEmail = fromJust $ E.emailAddress trashEmailAccount++testLine :: Line+testLine = Line+  { lineId                    = Nothing+  , lineLineNum               = Nothing+  , lineDescription           = Nothing+  , lineAmount                = Just 100.0+  , lineLinkedTxn             = Nothing+  , lineDetailType            = "SalesItemLineDetail"+  , lineDescriptionLineDetail = Nothing+  , lineDiscountLineDetail    = Nothing+  , lineSalesItemLineDetail   = Just testSalesItemLineDetail+  , lineSubTotalLineDetail    = Nothing+  , lineCustomField           = Nothing+  }++testSalesItemLineDetail :: SalesItemLineDetail+testSalesItemLineDetail =+  SalesItemLineDetail+   { salesItemLineDetailItemRef         = Just testItemRef+   , salesItemLineDetailClassRef        = Nothing+   , salesItemLineDetailUnitPrice       = Nothing+   , salesItemLineDetailRatePercent     = Nothing+   , salesItemLineDetailPriceLevelRef   = Nothing+   , salesItemLineDetailMarkupInfo      = Nothing+   , salesItemLineDetailQty             = Nothing+   , salesItemLineDetailTaxCodeRef      = Nothing+   , salesItemLineDetailServiceData     = Nothing+   , salesItemLineDetailTaxInclusiveAmt = Nothing+  }+++testItemRef :: Reference+testItemRef = Reference+  { referenceValue = "1"+  , referenceName  = Nothing+  , referenceType  = Nothing+  }+++testCustomerRef :: CustomerRef+testCustomerRef  = Reference+  { referenceValue = "21"+  , referenceName  = Nothing+  , referenceType  = Nothing+  }++testInvoice :: Invoice+testInvoice =+  Invoice Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          [testLine]+          Nothing+          testCustomerRef+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          (Just $ EmailAddress trashEmailAccount)+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+          Nothing+
+ test/doctests.hs view
@@ -0,0 +1,4 @@+import Test.DocTest++main :: IO ()+main = doctest ["-isrc", "test/Data.hs", "src/QuickBooks.hs"]