Implement endpoints for Xero orders
This commit is contained in:
parent
c965951d0e
commit
fbc41f9333
5 changed files with 288 additions and 42 deletions
|
@ -51,6 +51,8 @@ library:
|
||||||
- hexstring
|
- hexstring
|
||||||
- configurator
|
- configurator
|
||||||
- scientific
|
- scientific
|
||||||
|
- jwt
|
||||||
|
- containers
|
||||||
|
|
||||||
executables:
|
executables:
|
||||||
zgo-backend-exe:
|
zgo-backend-exe:
|
||||||
|
|
|
@ -219,6 +219,9 @@ updateOrderTotals o =
|
||||||
findOrder :: T.Text -> Action IO (Maybe Document)
|
findOrder :: T.Text -> Action IO (Maybe Document)
|
||||||
findOrder s = findOne (select ["session" =: s, "closed" =: False] "orders")
|
findOrder s = findOne (select ["session" =: s, "closed" =: False] "orders")
|
||||||
|
|
||||||
|
findXeroOrder :: T.Text -> T.Text -> T.Text -> Action IO (Maybe Document)
|
||||||
|
findXeroOrder a i s = findOne (select ["address" =: a, "externalInvoice" =: i, "shortCode" =: s] "orders")
|
||||||
|
|
||||||
findOrderById :: String -> Action IO (Maybe Document)
|
findOrderById :: String -> Action IO (Maybe Document)
|
||||||
findOrderById i = findOne (select ["_id" =: (read i :: B.ObjectId)] "orders")
|
findOrderById i = findOne (select ["_id" =: (read i :: B.ObjectId)] "orders")
|
||||||
|
|
||||||
|
|
224
src/Xero.hs
224
src/Xero.hs
|
@ -6,6 +6,7 @@ import Control.Monad.IO.Class
|
||||||
import Data.Aeson
|
import Data.Aeson
|
||||||
import qualified Data.Bson as B
|
import qualified Data.Bson as B
|
||||||
import qualified Data.ByteString.Lazy as BL
|
import qualified Data.ByteString.Lazy as BL
|
||||||
|
import qualified Data.Map.Strict as M
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import Data.Text.Encoding
|
import Data.Text.Encoding
|
||||||
|
@ -14,7 +15,10 @@ import Data.Time.Clock
|
||||||
import Database.MongoDB
|
import Database.MongoDB
|
||||||
import GHC.Generics
|
import GHC.Generics
|
||||||
import Network.HTTP.Simple
|
import Network.HTTP.Simple
|
||||||
|
import Network.HTTP.Simple (getResponseStatus)
|
||||||
import Network.HTTP.Types.Header
|
import Network.HTTP.Types.Header
|
||||||
|
import Owner
|
||||||
|
import Web.JWT
|
||||||
|
|
||||||
-- | Type to represent a Xero app configuration
|
-- | Type to represent a Xero app configuration
|
||||||
data Xero =
|
data Xero =
|
||||||
|
@ -135,6 +139,123 @@ processToken t a = do
|
||||||
(addUTCTime (fromIntegral $ t_expires t) now)
|
(addUTCTime (fromIntegral $ t_expires t) now)
|
||||||
(addUTCTime 5184000 now)
|
(addUTCTime 5184000 now)
|
||||||
|
|
||||||
|
-- |Type to represent a Xero tenant
|
||||||
|
data XeroTenant =
|
||||||
|
XeroTenant
|
||||||
|
{ xt_id :: T.Text
|
||||||
|
, xt_aei :: T.Text
|
||||||
|
, xt_tid :: T.Text
|
||||||
|
, xt_type :: T.Text
|
||||||
|
, xt_name :: T.Text
|
||||||
|
--, xt_created :: !UTCTime
|
||||||
|
--, xt_updated :: !UTCTime
|
||||||
|
}
|
||||||
|
deriving (Show, Eq)
|
||||||
|
|
||||||
|
instance FromJSON XeroTenant where
|
||||||
|
parseJSON =
|
||||||
|
withObject "XeroTenant" $ \obj -> do
|
||||||
|
i <- obj .: "id"
|
||||||
|
aei <- obj .: "authEventId"
|
||||||
|
tI <- obj .: "tenantId"
|
||||||
|
tT <- obj .: "tenantType"
|
||||||
|
tN <- obj .: "tenantName"
|
||||||
|
--c <- obj .: "createdDateUtc"
|
||||||
|
--u <- obj .: "updatedDateUtc"
|
||||||
|
pure $ XeroTenant i aei tI tT tN
|
||||||
|
|
||||||
|
data XeroInvoice =
|
||||||
|
XeroInvoice
|
||||||
|
{ xi_id :: Maybe ObjectId
|
||||||
|
, xi_eid :: T.Text
|
||||||
|
, xi_type :: T.Text
|
||||||
|
, xi_number :: T.Text
|
||||||
|
, xi_contact :: T.Text
|
||||||
|
, xi_currency :: T.Text
|
||||||
|
, xi_currRate :: Double
|
||||||
|
, xi_total :: Double
|
||||||
|
, xi_status :: T.Text
|
||||||
|
, xi_date :: T.Text
|
||||||
|
, xi_shortcode :: T.Text
|
||||||
|
, xi_loadDate :: UTCTime
|
||||||
|
}
|
||||||
|
deriving (Show, Eq)
|
||||||
|
|
||||||
|
instance FromJSON XeroInvoice where
|
||||||
|
parseJSON =
|
||||||
|
withObject "XeroInvoice" $ \obj -> do
|
||||||
|
i <- obj .:? "_id"
|
||||||
|
eId <- obj .: "InvoiceID"
|
||||||
|
t <- obj .: "Type"
|
||||||
|
n <- obj .: "InvoiceNumber"
|
||||||
|
c <- obj .: "Contact"
|
||||||
|
cN <- c .: "Name"
|
||||||
|
cu <- obj .: "CurrencyCode"
|
||||||
|
cR <- obj .: "CurrencyRate"
|
||||||
|
total <- obj .: "Total"
|
||||||
|
status <- obj .: "Status"
|
||||||
|
d <- obj .: "Date"
|
||||||
|
pure $
|
||||||
|
XeroInvoice
|
||||||
|
(read =<< i)
|
||||||
|
eId
|
||||||
|
t
|
||||||
|
n
|
||||||
|
cN
|
||||||
|
cu
|
||||||
|
cR
|
||||||
|
total
|
||||||
|
status
|
||||||
|
d
|
||||||
|
""
|
||||||
|
(UTCTime (fromGregorian 2000 1 1) (secondsToDiffTime 0))
|
||||||
|
|
||||||
|
instance ToJSON XeroInvoice where
|
||||||
|
toJSON (XeroInvoice i eId t n cN cu cR total status d sC pD) =
|
||||||
|
case i of
|
||||||
|
Just oid ->
|
||||||
|
object
|
||||||
|
[ "_id" .= show oid
|
||||||
|
, "inv_Type" .= t
|
||||||
|
, "inv_Id" .= eId
|
||||||
|
, "inv_No" .= n
|
||||||
|
, "inv_Contact" .= cN
|
||||||
|
, "inv_Currency" .= cu
|
||||||
|
, "inv_CurrencyRate" .= cR
|
||||||
|
, "inv_Total" .= total
|
||||||
|
, "inv_Status" .= status
|
||||||
|
, "inv_Date" .= d
|
||||||
|
, "inv_shortCode" .= sC
|
||||||
|
, "inv_ProcDate" .= pD
|
||||||
|
]
|
||||||
|
Nothing ->
|
||||||
|
object
|
||||||
|
[ "_id" .= ("" :: String)
|
||||||
|
, "inv_Type" .= t
|
||||||
|
, "inv_Id" .= eId
|
||||||
|
, "inv_No" .= n
|
||||||
|
, "inv_Contact" .= cN
|
||||||
|
, "inv_Currency" .= cu
|
||||||
|
, "inv_CurrencyRate" .= cR
|
||||||
|
, "inv_Total" .= total
|
||||||
|
, "inv_Status" .= status
|
||||||
|
, "inv_Date" .= d
|
||||||
|
, "inv_shortCode" .= sC
|
||||||
|
, "inv_ProcDate" .= pD
|
||||||
|
]
|
||||||
|
|
||||||
|
newtype XeroInvResponse =
|
||||||
|
XeroInvResponse
|
||||||
|
{ xir_invs :: [XeroInvoice]
|
||||||
|
}
|
||||||
|
deriving (Show, Eq)
|
||||||
|
|
||||||
|
instance FromJSON XeroInvResponse where
|
||||||
|
parseJSON =
|
||||||
|
withObject "XeroInvResponse" $ \obj -> do
|
||||||
|
invs <- obj .: "Invoices"
|
||||||
|
pure $ XeroInvResponse invs
|
||||||
|
|
||||||
-- Database actions
|
-- Database actions
|
||||||
findXero :: Action IO (Maybe Document)
|
findXero :: Action IO (Maybe Document)
|
||||||
findXero = findOne (select [] "xero")
|
findXero = findOne (select [] "xero")
|
||||||
|
@ -152,22 +273,17 @@ findToken :: T.Text -> Action IO (Maybe Document)
|
||||||
findToken a = findOne (select ["address" =: a] "xerotokens")
|
findToken a = findOne (select ["address" =: a] "xerotokens")
|
||||||
|
|
||||||
-- | Function to request accesstoken
|
-- | Function to request accesstoken
|
||||||
requestXeroToken ::
|
requestXeroToken :: Pipe -> T.Text -> Xero -> T.Text -> T.Text -> IO Bool
|
||||||
(Action IO (Maybe Document) -> IO (Maybe Document))
|
requestXeroToken pipe dbName cred code address = do
|
||||||
-> Xero
|
token <- access pipe master dbName $ findToken address
|
||||||
-> T.Text
|
let pars =
|
||||||
-> T.Text
|
|
||||||
-> IO Bool
|
|
||||||
requestXeroToken f cred code address = do
|
|
||||||
token <- f $ findToken address
|
|
||||||
case token of
|
case token of
|
||||||
Just xT -> do
|
Just xT -> do
|
||||||
let xToken = cast' (Doc xT) :: Maybe XeroToken
|
let xToken = cast' (Doc xT) :: Maybe XeroToken
|
||||||
case xToken of
|
case xToken of
|
||||||
Nothing -> return False
|
Nothing -> error "Failed to parse XeroToken BSON"
|
||||||
Just x -> return True
|
Just x -> "grant_type=refresh_token&refresh_token=" <> t_refresh x
|
||||||
Nothing -> do
|
Nothing ->
|
||||||
let pars =
|
|
||||||
"grant_type=authorization_code&code=" <>
|
"grant_type=authorization_code&code=" <>
|
||||||
code <> "&redirect_uri=http://localhost:4200/test"
|
code <> "&redirect_uri=http://localhost:4200/test"
|
||||||
let req =
|
let req =
|
||||||
|
@ -179,17 +295,93 @@ requestXeroToken f cred code address = do
|
||||||
(encodeUtf8 $ x_clientSecret cred) $
|
(encodeUtf8 $ x_clientSecret cred) $
|
||||||
setRequestHost "identity.xero.com" $
|
setRequestHost "identity.xero.com" $
|
||||||
setRequestPort 443 $
|
setRequestPort 443 $
|
||||||
setRequestMethod "POST" $
|
setRequestMethod "POST" $ setRequestPath "/connect/token" defaultRequest
|
||||||
setRequestPath "/connect/token" defaultRequest
|
|
||||||
res <- httpJSON req
|
res <- httpJSON req
|
||||||
let rCode = getResponseStatusCode (res :: Response XeroToken)
|
let rCode = getResponseStatusCode (res :: Response XeroToken)
|
||||||
case rCode of
|
case rCode of
|
||||||
200 -> do
|
200 -> do
|
||||||
let newToken = getResponseBody (res :: Response XeroToken)
|
let newToken = getResponseBody (res :: Response XeroToken)
|
||||||
pToken <- processToken newToken address
|
pToken <- processToken newToken address
|
||||||
print pToken
|
--print pToken
|
||||||
_ <- f $ upsertToken pToken
|
_ <- access pipe master dbName $ upsertToken pToken
|
||||||
|
_ <- getTenantId pipe dbName pToken
|
||||||
return True
|
return True
|
||||||
_ -> do
|
_ -> do
|
||||||
print res
|
print res
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
-- |Helper function to obtain the authentication event ID
|
||||||
|
extractEventId :: T.Text -> Maybe Data.Aeson.Value
|
||||||
|
extractEventId t = do
|
||||||
|
j <- Web.JWT.decode t
|
||||||
|
(M.lookup "authentication_event_id" .
|
||||||
|
unClaimsMap . unregisteredClaims . claims)
|
||||||
|
j
|
||||||
|
|
||||||
|
getTenantId :: Pipe -> T.Text -> XeroToken -> IO ()
|
||||||
|
getTenantId pipe dbName t = do
|
||||||
|
let eid = extractEventId $ t_access t
|
||||||
|
--print eid
|
||||||
|
case eid of
|
||||||
|
Nothing -> error "Failed to decode JWT"
|
||||||
|
Just (Data.Aeson.String e) -> do
|
||||||
|
print e
|
||||||
|
let req =
|
||||||
|
addRequestHeader hContentType "application/json" $
|
||||||
|
setRequestQueryString [("authEventId", Just (encodeUtf8 e))] $
|
||||||
|
setRequestSecure True $
|
||||||
|
setRequestBearerAuth (encodeUtf8 $ t_access t) $
|
||||||
|
setRequestPort 443 $
|
||||||
|
setRequestPath "/connections" $
|
||||||
|
setRequestHost "api.xero.com" $
|
||||||
|
setRequestMethod "GET" defaultRequest
|
||||||
|
res <- httpJSON req
|
||||||
|
let tenants = getResponseBody (res :: Response [XeroTenant])
|
||||||
|
--print tenants
|
||||||
|
_ <- access pipe master dbName $ setTenant (t_address t) (head tenants)
|
||||||
|
return ()
|
||||||
|
_ -> error "Incorrect type for authorization_event_id"
|
||||||
|
|
||||||
|
-- | Update an Owner with XeroTenant id
|
||||||
|
setTenant :: T.Text -> XeroTenant -> Action IO ()
|
||||||
|
setTenant a t =
|
||||||
|
modify (select ["address" =: a] "owners") ["$set" =: ["crmToken" =: xt_tid t]]
|
||||||
|
|
||||||
|
getXeroInvoice :: Pipe -> T.Text -> T.Text -> T.Text -> IO (Maybe XeroInvoice)
|
||||||
|
getXeroInvoice pipe dbName inv address = do
|
||||||
|
token <- access pipe master dbName $ findToken address
|
||||||
|
case token of
|
||||||
|
Nothing -> return Nothing
|
||||||
|
Just t -> do
|
||||||
|
let xToken = cast' (Doc t)
|
||||||
|
case xToken of
|
||||||
|
Nothing -> return Nothing
|
||||||
|
Just xT -> do
|
||||||
|
let aToken = t_access xT
|
||||||
|
o <- access pipe master dbName $ findOwner address
|
||||||
|
case o of
|
||||||
|
Nothing -> return Nothing
|
||||||
|
Just ow -> do
|
||||||
|
let ownerData = cast' (Doc ow)
|
||||||
|
case ownerData of
|
||||||
|
Nothing -> return Nothing
|
||||||
|
Just oD -> do
|
||||||
|
let tenant = ocrmToken oD
|
||||||
|
let req =
|
||||||
|
addRequestHeader "Accept" "application/json" $
|
||||||
|
addRequestHeader "Xero-tenant-id" (encodeUtf8 tenant) $
|
||||||
|
setRequestSecure True $
|
||||||
|
setRequestBearerAuth (encodeUtf8 aToken) $
|
||||||
|
setRequestPort 443 $
|
||||||
|
setRequestPath
|
||||||
|
("/api.xro/2.0/Invoices/" <> encodeUtf8 inv) $
|
||||||
|
setRequestHost "api.xero.com" $
|
||||||
|
setRequestMethod "GET" defaultRequest
|
||||||
|
res <- httpJSON req
|
||||||
|
let sCode = getResponseStatusCode res
|
||||||
|
case sCode of
|
||||||
|
200 -> do
|
||||||
|
let invData =
|
||||||
|
getResponseBody (res :: Response XeroInvResponse)
|
||||||
|
return $ Just (head $ xir_invs invData)
|
||||||
|
_ -> return Nothing
|
||||||
|
|
|
@ -28,12 +28,14 @@ import Data.Time.Clock
|
||||||
import Data.Time.Clock.POSIX
|
import Data.Time.Clock.POSIX
|
||||||
import Data.Typeable
|
import Data.Typeable
|
||||||
import qualified Data.Vector as V
|
import qualified Data.Vector as V
|
||||||
|
import Data.Vector.Internal.Check (doChecks)
|
||||||
import Data.Word
|
import Data.Word
|
||||||
import Database.MongoDB
|
import Database.MongoDB
|
||||||
import Debug.Trace
|
import Debug.Trace
|
||||||
import GHC.Generics
|
import GHC.Generics
|
||||||
import Item
|
import Item
|
||||||
import Network.HTTP.Simple
|
import Network.HTTP.Simple
|
||||||
|
import Network.HTTP.Types (created201)
|
||||||
import Network.HTTP.Types.Status
|
import Network.HTTP.Types.Status
|
||||||
import Network.Wai.Middleware.Cors
|
import Network.Wai.Middleware.Cors
|
||||||
import Network.Wai.Middleware.HttpAuth
|
import Network.Wai.Middleware.HttpAuth
|
||||||
|
@ -509,15 +511,39 @@ routes pipe config = do
|
||||||
Nothing -> status noContent204
|
Nothing -> status noContent204
|
||||||
Just c -> do
|
Just c -> do
|
||||||
res <-
|
res <-
|
||||||
liftIO $
|
liftIO $ requestXeroToken pipe (c_dbName config) c code address
|
||||||
requestXeroToken
|
|
||||||
(run :: Action IO (Maybe Document) -> IO (Maybe Document))
|
|
||||||
c
|
|
||||||
code
|
|
||||||
address
|
|
||||||
if res
|
if res
|
||||||
then status ok200
|
then status ok200
|
||||||
else status noContent204
|
else status noContent204
|
||||||
|
get "/api/invdata" $ do
|
||||||
|
inv <- param "inv"
|
||||||
|
oAddress <- param "address"
|
||||||
|
xeroConfig <- liftIO $ run findXero
|
||||||
|
case xeroConfig of
|
||||||
|
Nothing -> do
|
||||||
|
status noContent204
|
||||||
|
text "Xero App credentials not found"
|
||||||
|
Just x -> do
|
||||||
|
let xConfig = cast' (Doc x)
|
||||||
|
case xConfig of
|
||||||
|
Nothing -> do
|
||||||
|
status noContent204
|
||||||
|
text "Xero App credentials corrupted"
|
||||||
|
Just c -> do
|
||||||
|
res <-
|
||||||
|
liftIO $ requestXeroToken pipe (c_dbName config) c "none" oAddress
|
||||||
|
if res
|
||||||
|
then do
|
||||||
|
resInv <-
|
||||||
|
liftIO $ getXeroInvoice pipe (c_dbName config) inv oAddress
|
||||||
|
case resInv of
|
||||||
|
Nothing -> do
|
||||||
|
status noContent204
|
||||||
|
text "Xero invoice not found"
|
||||||
|
Just xI -> do
|
||||||
|
status ok200
|
||||||
|
Web.Scotty.json (object ["invdata" .= toJSON xI])
|
||||||
|
else status noContent204
|
||||||
post "/api/xerotoken" $ do
|
post "/api/xerotoken" $ do
|
||||||
o <- jsonData
|
o <- jsonData
|
||||||
let q = payload (o :: Payload XeroToken)
|
let q = payload (o :: Payload XeroToken)
|
||||||
|
@ -743,6 +769,27 @@ routes pipe config = do
|
||||||
[ "message" .= ("Order found!" :: String)
|
[ "message" .= ("Order found!" :: String)
|
||||||
, "order" .= toJSON (pOrder :: ZGoOrder)
|
, "order" .= toJSON (pOrder :: ZGoOrder)
|
||||||
])
|
])
|
||||||
|
--Upsert xero order
|
||||||
|
post "/api/orderx" $ do
|
||||||
|
newOrder <- jsonData
|
||||||
|
let q = payload (newOrder :: Payload ZGoOrder)
|
||||||
|
_ <- liftIO $ run (upsertOrder q)
|
||||||
|
myOrder <-
|
||||||
|
liftIO $
|
||||||
|
run (findXeroOrder (qaddress q) (qexternalInvoice q) (qshortCode q))
|
||||||
|
case myOrder of
|
||||||
|
Nothing -> status noContent204
|
||||||
|
Just o -> do
|
||||||
|
let o' = cast' (Doc o)
|
||||||
|
case o' of
|
||||||
|
Nothing -> status internalServerError500
|
||||||
|
Just pOrder -> do
|
||||||
|
status created201
|
||||||
|
Web.Scotty.json
|
||||||
|
(object
|
||||||
|
[ "message" .= ("Order found!" :: String)
|
||||||
|
, "order" .= toJSON (pOrder :: ZGoOrder)
|
||||||
|
])
|
||||||
-- Upsert order
|
-- Upsert order
|
||||||
post "/api/order" $ do
|
post "/api/order" $ do
|
||||||
newOrder <- jsonData
|
newOrder <- jsonData
|
||||||
|
|
|
@ -47,9 +47,11 @@ library
|
||||||
, bson
|
, bson
|
||||||
, bytestring
|
, bytestring
|
||||||
, configurator
|
, configurator
|
||||||
|
, containers
|
||||||
, hexstring
|
, hexstring
|
||||||
, http-conduit
|
, http-conduit
|
||||||
, http-types
|
, http-types
|
||||||
|
, jwt
|
||||||
, mongoDB
|
, mongoDB
|
||||||
, quickcheck-instances
|
, quickcheck-instances
|
||||||
, random
|
, random
|
||||||
|
|
Loading…
Reference in a new issue