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 +31/−0
- Setup.hs +2/−0
- quickbooks.cabal +102/−0
- src/QuickBooks.hs +419/−0
- src/QuickBooks/Authentication.hs +130/−0
- src/QuickBooks/Customer.hs +64/−0
- src/QuickBooks/Invoice.hs +141/−0
- src/QuickBooks/Item.hs +64/−0
- src/QuickBooks/Logging.hs +55/−0
- src/QuickBooks/Types.hs +721/−0
- test/Data.hs +110/−0
- test/doctests.hs +4/−0
+ 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"]