Implement endpoints for Xero orders

This commit is contained in:
Rene Vergara 2022-08-20 08:09:46 -05:00
parent c965951d0e
commit fbc41f9333
Signed by: pitmutt
GPG key ID: 65122AD495A7F5B2
5 changed files with 288 additions and 42 deletions

View file

@ -51,6 +51,8 @@ library:
- hexstring
- configurator
- scientific
- jwt
- containers
executables:
zgo-backend-exe:

View file

@ -219,6 +219,9 @@ updateOrderTotals o =
findOrder :: T.Text -> Action IO (Maybe Document)
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 i = findOne (select ["_id" =: (read i :: B.ObjectId)] "orders")

View file

@ -6,6 +6,7 @@ import Control.Monad.IO.Class
import Data.Aeson
import qualified Data.Bson as B
import qualified Data.ByteString.Lazy as BL
import qualified Data.Map.Strict as M
import Data.Maybe
import qualified Data.Text as T
import Data.Text.Encoding
@ -14,7 +15,10 @@ import Data.Time.Clock
import Database.MongoDB
import GHC.Generics
import Network.HTTP.Simple
import Network.HTTP.Simple (getResponseStatus)
import Network.HTTP.Types.Header
import Owner
import Web.JWT
-- | Type to represent a Xero app configuration
data Xero =
@ -135,6 +139,123 @@ processToken t a = do
(addUTCTime (fromIntegral $ t_expires t) 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
findXero :: Action IO (Maybe Document)
findXero = findOne (select [] "xero")
@ -152,22 +273,17 @@ findToken :: T.Text -> Action IO (Maybe Document)
findToken a = findOne (select ["address" =: a] "xerotokens")
-- | Function to request accesstoken
requestXeroToken ::
(Action IO (Maybe Document) -> IO (Maybe Document))
-> Xero
-> T.Text
-> T.Text
-> IO Bool
requestXeroToken f cred code address = do
token <- f $ findToken address
requestXeroToken :: Pipe -> T.Text -> Xero -> T.Text -> T.Text -> IO Bool
requestXeroToken pipe dbName cred code address = do
token <- access pipe master dbName $ findToken address
let pars =
case token of
Just xT -> do
let xToken = cast' (Doc xT) :: Maybe XeroToken
case xToken of
Nothing -> return False
Just x -> return True
Nothing -> do
let pars =
Nothing -> error "Failed to parse XeroToken BSON"
Just x -> "grant_type=refresh_token&refresh_token=" <> t_refresh x
Nothing ->
"grant_type=authorization_code&code=" <>
code <> "&redirect_uri=http://localhost:4200/test"
let req =
@ -179,17 +295,93 @@ requestXeroToken f cred code address = do
(encodeUtf8 $ x_clientSecret cred) $
setRequestHost "identity.xero.com" $
setRequestPort 443 $
setRequestMethod "POST" $
setRequestPath "/connect/token" defaultRequest
setRequestMethod "POST" $ setRequestPath "/connect/token" defaultRequest
res <- httpJSON req
let rCode = getResponseStatusCode (res :: Response XeroToken)
case rCode of
200 -> do
let newToken = getResponseBody (res :: Response XeroToken)
pToken <- processToken newToken address
print pToken
_ <- f $ upsertToken pToken
--print pToken
_ <- access pipe master dbName $ upsertToken pToken
_ <- getTenantId pipe dbName pToken
return True
_ -> do
print res
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

View file

@ -28,12 +28,14 @@ import Data.Time.Clock
import Data.Time.Clock.POSIX
import Data.Typeable
import qualified Data.Vector as V
import Data.Vector.Internal.Check (doChecks)
import Data.Word
import Database.MongoDB
import Debug.Trace
import GHC.Generics
import Item
import Network.HTTP.Simple
import Network.HTTP.Types (created201)
import Network.HTTP.Types.Status
import Network.Wai.Middleware.Cors
import Network.Wai.Middleware.HttpAuth
@ -509,15 +511,39 @@ routes pipe config = do
Nothing -> status noContent204
Just c -> do
res <-
liftIO $
requestXeroToken
(run :: Action IO (Maybe Document) -> IO (Maybe Document))
c
code
address
liftIO $ requestXeroToken pipe (c_dbName config) c code address
if res
then status ok200
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
o <- jsonData
let q = payload (o :: Payload XeroToken)
@ -743,7 +769,28 @@ routes pipe config = do
[ "message" .= ("Order found!" :: String)
, "order" .= toJSON (pOrder :: ZGoOrder)
])
--Upsert order
--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
post "/api/order" $ do
newOrder <- jsonData
let q = payload (newOrder :: Payload ZGoOrder)

View file

@ -47,9 +47,11 @@ library
, bson
, bytestring
, configurator
, containers
, hexstring
, http-conduit
, http-types
, jwt
, mongoDB
, quickcheck-instances
, random