Project.purs
module Aftok.Api.Project where
import Prelude
import Control.Monad.Except.Trans (ExceptT, runExceptT)
-- import Control.Monad.Except.Trans (ExceptT, runExceptT, except, withExceptT)
-- import Control.Monad.Error.Class (throwError)
import Data.Argonaut.Core (Json, fromString, jsonEmptyObject)
import Data.Argonaut.Decode (class DecodeJson, JsonDecodeError(..), decodeJson, (.:))
import Data.Argonaut.Encode (encodeJson, (:=), (~>))
import Data.DateTime (DateTime)
import Data.DateTime.Instant (Instant, toDateTime)
import Data.Either (Either(..))
import Data.Foldable (class Foldable, foldr, foldl, foldMapDefaultR)
import Data.Functor.Compose (Compose(..))
import Data.Map as M
import Data.Maybe (Maybe(..))
import Data.Newtype (class Newtype, unwrap)
import Data.Ratio (Ratio, (%))
import Data.Time.Duration (Hours(..), Days(..))
import Data.Tuple (Tuple(..))
import Data.Traversable (class Traversable, traverse)
import Effect (Effect)
import Effect.Aff (Aff)
import Effect.Class as EC
import Foreign.Object (Object)
import Affjax (get, post)
import Affjax.ResponseFormat as RF
import Affjax.RequestBody as RB
import Aftok.Types
( UserId
, ProjectId
, pidStr
)
import Aftok.Api.Types
(APIError, CommsAddress(..), Zip321Request(..))
import Aftok.Api.Json
( Decode
, decompose
, parseResponse
, parseDatedResponse
, parseDatedResponseMay
)
data DepreciationFn
= LinearDepreciation { undep :: Days, dep :: Days }
instance decodeDepreciationFn :: DecodeJson DepreciationFn where
decodeJson json = do
x <- decodeJson json
dtype <- x .: "type"
args <- x .: "arguments"
case dtype of
"LinearDepreciation" -> do
undep <- Days <$> args .: "undep"
dep <- Days <$> args .: "dep"
pure $ LinearDepreciation { undep, dep }
other -> Left $ UnexpectedValue (fromString dtype)
newtype Project' date
= Project'
{ projectId :: ProjectId
, projectName :: String
, inceptionDate :: date
, initiator :: UserId
, depf :: DepreciationFn
}
derive instance projectNewtype :: Newtype (Project' a) _
derive instance projectFunctor :: Functor Project'
instance projectFoldable :: Foldable Project' where
foldr f b (Project' p) = f (p.inceptionDate) b
foldl f b (Project' p) = f b (p.inceptionDate)
foldMap = foldMapDefaultR
instance projectTraversable :: Traversable Project' where
traverse f (Project' p) = Project' <<< (\b -> p { inceptionDate = b }) <$> f (p.inceptionDate)
sequence = traverse identity
type Project
= Project' DateTime
parseProject :: ProjectId -> Object Json -> Either JsonDecodeError (Project' String)
parseProject projectId pjson = do
projectName <- pjson .: "projectName"
inceptionDate <- pjson .: "inceptionDate"
initiator <- pjson .: "initiator"
depf <- pjson .: "depf"
pure $ Project' { projectId, projectName, inceptionDate, initiator, depf }
instance decodeJsonProject :: DecodeJson (Project' String) where
decodeJson json = do
x <- decodeJson json
pjson <- x .: "project"
projectId <- x .: "projectId"
parseProject projectId pjson
newtype Contributor' date
= Contributor'
{ userId :: UserId
, handle :: String
, joinedOn :: date
, loggedHours :: Hours
, depreciatedHours :: Hours
, revShare :: Ratio Number
}
derive instance contributorNewtype :: Newtype (Contributor' a) _
derive instance contributorFunctor :: Functor Contributor'
instance contributorFoldable :: Foldable Contributor' where
foldr f b (Contributor' p) = f (p.joinedOn) b
foldl f b (Contributor' p) = f b (p.joinedOn)
foldMap = foldMapDefaultR
instance contributorTraversable :: Traversable Contributor' where
traverse f (Contributor' p) = Contributor' <<< (\b -> p { joinedOn = b }) <$> f (p.joinedOn)
sequence = traverse identity
instance decodeJsonContributor :: DecodeJson (Contributor' String) where
decodeJson json = do
x <- decodeJson json
userId <- x .: "userId"
handle <- x .: "username"
joinedOn <- x .: "joinedOn"
loggedHours <- Hours <$> x .: "loggedHours"
depreciatedHours <- Hours <$> x .: "depreciatedHours"
revShareObj <- x .: "revenureShare"
num <- revShareObj .: "numerator"
den <- revShareObj .: "denominator"
let
revShare = num % den
pure $ Contributor' { userId, handle, joinedOn, loggedHours, depreciatedHours, revShare }
newtype ProjectDetail' date
= ProjectDetail'
{ project :: Project' date
, contributors :: M.Map UserId (Contributor' date)
}
projectDetail ::
forall date.
Project' date ->
M.Map UserId (Contributor' date) ->
ProjectDetail' date
projectDetail project contributors = ProjectDetail' { project, contributors }
type ProjectDetail
= ProjectDetail' DateTime
derive instance projectDetailNewtype :: Newtype (ProjectDetail' a) _
derive instance projectDetailFunctor :: Functor ProjectDetail'
instance projectDetailFoldable :: Foldable ProjectDetail' where
foldr f b (ProjectDetail' p) = foldr f (foldr f b (p.project)) (Compose p.contributors)
foldl f b (ProjectDetail' p) = foldl f (foldl f b (p.project)) (Compose p.contributors)
foldMap = foldMapDefaultR
instance projectDetailTraversable :: Traversable ProjectDetail' where
traverse f (ProjectDetail' p) =
projectDetail <$> traverse f p.project
<*> (map unwrap $ traverse f (Compose p.contributors))
sequence = traverse identity
parseProjectDetail :: ProjectId -> Decode (ProjectDetail' String)
parseProjectDetail pid json = do
x <- decodeJson json
project <- parseProject pid =<< x .: "project"
(contribList :: Array (Contributor' String)) <- x .: "contributors"
let
contributors = M.fromFoldable $ map (\c@(Contributor' xs) -> Tuple xs.userId c) contribList
pure $ ProjectDetail' { project, contributors }
listProjects :: Aff (Either APIError (Array Project))
listProjects = do
response <- get RF.json "/api/projects"
EC.liftEffect
<<< runExceptT
<<< map decompose
<<< map (map toDateTime)
$ parseDatedResponse decodeJson response
getProjectDetail :: ProjectId -> Aff (Either APIError (Maybe ProjectDetail))
getProjectDetail pid = do
response <- get RF.json ("/api/projects/" <> pidStr pid <> "/detail")
let
parsed :: ExceptT APIError Effect (Maybe (ProjectDetail' Instant))
parsed = parseDatedResponseMay (parseProjectDetail pid) response
EC.liftEffect
<<< runExceptT
<<< map (map (map toDateTime))
$ parsed
encodeInviteBy :: CommsAddress -> Json
encodeInviteBy = case _ of
EmailCommsAddr email -> encodeJson ({ email: email })
ZcashCommsAddr zaddr -> encodeJson ({ zaddr: zaddr })
type Invitation' by =
{ greetName :: String
, message :: Maybe String
, inviteBy :: by
}
type Invitation = Invitation' CommsAddress
encodeInvitation :: Invitation' Json -> Json
encodeInvitation = encodeJson
type InvResult =
{ zip321_request :: Maybe String
}
decodeInvResult :: Json -> Either JsonDecodeError InvResult
decodeInvResult = decodeJson
invite :: ProjectId -> Invitation -> Aff (Either APIError (Maybe Zip321Request))
invite pid inv = do
let inv' = inv { inviteBy = encodeInviteBy inv.inviteBy }
let body = RB.json $ encodeInvitation inv'
response <- post RF.json ("/api/projects/" <> pidStr pid <> "/invite") (Just body)
map (\r -> Zip321Request <$> r.zip321_request) <$> parseResponse decodeInvResult response
type ProjectCreateRequest =
{ projectName :: String
, depf :: DepreciationFn
}
encodeProjectCreateRequest :: ProjectCreateRequest -> Json
encodeProjectCreateRequest pc =
"projectName" := pc.projectName
~> "depf" := (
case pc.depf of
LinearDepreciation vs ->
encodeJson {
"type": "LinearDepreciation",
"arguments": { "undep": unwrap vs.undep, dep: unwrap vs.dep }
}
)
~> jsonEmptyObject
decodeProjectId :: Json -> Either JsonDecodeError ProjectId
decodeProjectId json = (_ .: "projectId") =<< decodeJson json
createProject :: ProjectCreateRequest -> Aff (Either APIError ProjectId)
createProject pc = do
let body = RB.json $ encodeProjectCreateRequest pc
response <- post RF.json "/api/projects/" (Just body)
parseResponse decodeProjectId response