QMRKFEPGFBCEWAIXPEIG5ILKAJ2JH5L3TOITHR4HNJXK5FN3KXBQC 4U7F3CPIDTK6JSEDMNMHVKSR7HOQDLZQD2PPVMDLHO5SFSIMUXZAC 64C6AWH66FDKU6UE6Z6JPX2J2GBM2JOPTH2GL6LHKAIUBGNGDZ5AC RPAJLHMTUJU4AYNBOHVGHGGB4NY2NLY3BVPYN5FMWB3ZIMAUQHCQC IZEVQF627FA7VV25KJAWYWGGC35LZUUBBQRPN5ZAINTQLPEDRTEAC GCVQD44VRPQVKPZEPIC4AOIXLJIG2ZMV3QI2Y7KALUT6NVUBSGSAC A6HKMINBNGQLLX4QJMYWKQ4JAEHVJ4HIRVDKPPDI3FJUO2AAB7OQC KNSI575VAW6HRCZYXOEPQ4DTSML4EORML5MV4DJBRKE7TXCPS4EAC FD7SV5I6VCW27HZ3T3K4MMGB2OYGJTPKFFA263TNTAMRJGQJWVNAC TNR3TEHKVADAEZSTOD2XLSUTSW5AWST2YUW4CWK5KE7DSC6XHZNAC O5FVTOM6YFBLEPF3S576K6IMT6ZZ5VQCSB3YVXNS4CKBITKCRZ7AC Z7KS5XHHC6PAMTVBHXY7KUSS3BWAOU6FSYIITUCFOOJZU4OUJHBAC EZQG2APB36DDMIAYDPPDGOIXOD7K2RZZSGC2NKGZIHB2HZBTW7EQC 7KZP4RHZ3QSYTPPQ257A65Z5UPX44TF2LAI2U5EMULQCLDCEUK2AC PBD7LZYQHXAA3KLH2ZUX5GW4UFML6BQ32KXZF4KZ6OYFASUYFJ5QC LD4GLVSF6YTA7OZWIGJ45H6TUXGM4WKUIYXKWQFNUP36WDMYSMXAC 2G3GNDDUOVPF45PELJ65ZB2IXEHJJXJILFRVHZXGPXUL4BVNZJFQC BXGLKYRXO2O4NRM3BLNWQ7AWVPQXAMFS57MFYHJNOZZEZZW5BH6AC EMVTF2IWNQGRL44FC4JNG5FYYQTZSFPNM6SOM7IAEH6T7PPK2NVAC TZQJVHBAMDNWDBYCDE3SDVGBG2T5FOE3J5JAD6NENRW36XBHUUFQC 5XFJNUAZUCQ3WCGW4QRIAWR764QYDOPHOIVO2TRMGSSG7UDX2M2AC 64VI73NPSFNWTL6UXM6YHRFLNJZ3NUJ2R3CL53MO2HSZWFGBIRTQC NTPC7KJEAPA34SBIA74FVQSJXYNW32RIUQTHUSUTKMEUCPLUIBJAC LAROLAYUGJ4Q5AEFV5EJMIA2ZKBNCBWHHHPCJ3CKCNIUIYUKRFVQC W35DDBFYF6Z4ZPCFEO5RPAONZLYCSNTXUSTS6FIUUVZHCI6Q7GHAC SLL7262CJUE7TZDDZZXFROHCVVDA527WA4PHXCKEGZUJF2EN5MQAC NVOCQVASZWTKQJG7GPH7KHKZZR7NUG4WLV5YY4KAIRPCJRWCZPIAC TLQ72DSJD7GGPWN6HGBHAVPBRQFKEQ6KSK43U7JWWID4ZWAF47JAC P6NR2CGXCWAW6GXXSIXCGOBIRAS2BM4LEM6D5ADPN4IL7TMW6UVAC 4QX5E5ACVN57KJLCWOM4JEI6JSV4XZNCWVYPOTKSOMUW3SOMCNJAC A2J7B4SCCJYKQV3G2LDHEFNE2GUICO3N3Y5FKF4EUZW5AG7PTDWAC 2OIPAQCBDIUJBXB4K2QVP3IEBIUOCQHSWSWFVMVSVZC7GHX2VK7AC N4NDAZYTLSI2W22KT3SYXL257DBMSH3UT2BXOYM7LH7FSZAY4RLAC 75N3UJ4JK56KXF56GASGPAWLFYGJDETVJNYTF4KXFCQM767JUU5AC I2KHGVD44KT4MQJXGCTVSQKMBO6TVCY72F26TLXGWRL6PHGF6RNQC XTBSG4C7SCZUFOU2BTNFR6B6TCGYI35BWUV4PVTS3N7KNH5VEARQC BROSTG5KP3NUNLSYPVQID254TE47E5RKQAKLPIY7PGWETE6JNMTAC ADMKQQGCGVSHHIMVQ4XFRDCG544SBJCYALSKZV45CQQBZ4ACUH2AC VJPT6HDRMJAJD5PT3VOYJYW43ISKLICEHLSDWSROX2XZWO2OFZPQC 2Y2QZFVFSKXEFEGYJB5A7GI735ONWPCF7DVTIY5T73AUEVTZTBBQC createEvent' :: ProjectId -> UserId -> LogEntry -> QDBM EventIdcreateEvent' (ProjectId pid) (UserId uid) (LogEntry a e m) =pinsert EventId"INSERT INTO work_events (project_id, user_id, btc_addr, event_type, event_time, event_metadata) \\VALUES (?, ?, ?, ?, ?, ?) \\RETURNING id"( pid, uid, a ^. _BtcAddr, eventName e, fromThyme $ e ^. eventTime, m)
instance DBEval QDBM wheredbEval (CreateEvent (ProjectId pid) (UserId uid) (LogEntry a e m)) =pinsert EventId"INSERT INTO work_events (project_id, user_id, btc_addr, event_type, event_time, event_metadata) \\VALUES (?, ?, ?, ?, ?, ?) \\RETURNING id"( pid, uid, a ^. _BtcAddr, eventName e, fromThyme $ e ^. eventTime, m)
findEvent' :: EventId -> QDBM (Maybe QDBLogEntry)findEvent' (EventId eid) = dologEntries <- pquery qdbLogEntryParser"SELECT id, project_id, user_id, btc_addr, event_type, event_time, event_metadata FROM work_events \\WHERE id = ?"(Only eid)pure $ headMay logEntries
dbEval (FindEvent (EventId eid)) = dologEntries <- pquery qdbLogEntryParser"SELECT project_id, user_id, btc_addr, event_type, event_time, event_metadata FROM work_events \\WHERE id = ?"(Only eid)pure $ headMay logEntries
findEvents' :: ProjectId -> UserId -> Interval' -> QDBM [LogEntry]findEvents' (ProjectId pid) (UserId uid) ival =let q p (Before e) = pquery p"SELECT btc_addr, event_type, event_time, event_metadata FROM work_events \\WHERE project_id = ? AND user_id = ? AND event_time <= ?"(pid, uid, PUTCTime e)q p (During s e) = pquery p"SELECT btc_addr, event_type, event_time, event_metadata FROM work_events \\WHERE project_id = ? AND user_id = ? \\AND event_time >= ? AND event_time <= ?"(pid, uid, PUTCTime s, PUTCTime e)q p (After s) = pquery p"SELECT btc_addr, event_type, event_time, event_metadata FROM work_events \\WHERE project_id = ? AND user_id = ? AND event_time >= ?"(pid, uid, PUTCTime s)in q logEntryParser ival
dbEval (FindEvents (ProjectId pid) (UserId uid) ival) =let q p (Before e) = pquery p"SELECT btc_addr, event_type, event_time, event_metadata FROM work_events \\WHERE project_id = ? AND user_id = ? AND event_time <= ?"(pid, uid, PUTCTime e)q p (During s e) = pquery p"SELECT btc_addr, event_type, event_time, event_metadata FROM work_events \\WHERE project_id = ? AND user_id = ? \\AND event_time >= ? AND event_time <= ?"(pid, uid, PUTCTime s, PUTCTime e)q p (After s) = pquery p"SELECT btc_addr, event_type, event_time, event_metadata FROM work_events \\WHERE project_id = ? AND user_id = ? AND event_time >= ?"(pid, uid, PUTCTime s)in q logEntryParser ival
amendEvent' :: EventId -> EventAmendment -> QDBM AmendmentIdamendEvent' (EventId eid) (TimeChange mt t) =pinsert AmendmentId"INSERT INTO event_time_amendments (event_id, mod_time, event_time) VALUES (?, ?, ?) RETURNING id"( eid, fromThyme $ mt ^. _ModTime, fromThyme t )
dbEval (AmendEvent (EventId eid) (TimeChange mt t)) =pinsert AmendmentId"INSERT INTO event_time_amendments (event_id, mod_time, event_time) VALUES (?, ?, ?) RETURNING id"( eid, fromThyme $ mt ^. _ModTime, fromThyme t )
amendEvent' (EventId eid) (AddressChange mt addr) =pinsert AmendmentId"INSERT INTO event_addr_amendments (event_id, mod_time, btc_addr) VALUES (?, ?, ?) RETURNING id"( eid, fromThyme $ mt ^. _ModTime, addr ^. _BtcAddr )
dbEval (AmendEvent (EventId eid) (AddressChange mt addr)) =pinsert AmendmentId"INSERT INTO event_addr_amendments (event_id, mod_time, btc_addr) VALUES (?, ?, ?) RETURNING id"( eid, fromThyme $ mt ^. _ModTime, addr ^. _BtcAddr )
amendEvent' (EventId eid) (MetadataChange mt v) =pinsert AmendmentId"INSERT INTO event_metadata_amendments (event_id, mod_time, btc_addr) VALUES (?, ?, ?) RETURNING id"( eid, fromThyme $ mt ^. _ModTime, v)readWorkIndex' :: ProjectId -> QDBM WorkIndexreadWorkIndex' pid = dologEntries <- pquery logEntryParser"SELECT btc_addr, event_type, event_time, event_metadata FROM work_events WHERE project_id = ?"(Only $ PPid pid)pure $ workIndex logEntriescreateAuction' :: ProjectId -> Auction -> QDBM AuctionIdcreateAuction' pid auc =pinsert AuctionId"INSERT INTO auctions (project_id, raise_amount, end_time) \\VALUES (?, ?, ?) RETURNING id"(pid ^. (_ProjectId), auc ^. (raiseAmount.to PBTC), auc ^. auctionEnd)findAuction' :: AuctionId -> QDBM (Maybe Auction)findAuction' aucId = doauctions <- pquery auctionParser"SELECT raise_amount, end_time FROM auctions WHERE id = ?"(Only (aucId ^. _AuctionId))pure $ headMay auctionscreateBid' :: AuctionId -> Bid -> QDBM BidIdcreateBid' (AuctionId aucId) bid = dopinsert BidId"INSERT INTO bids (auction_id, bidder_id, bid_seconds, bid_amount, bid_time) \\VALUES (?, ?, ?, ?, ?) RETURNING id"( aucId, bid ^. (bidUser._UserId), case bid ^. bidSeconds of (Seconds i) -> i, bid ^. (bidAmount.to PBTC), bid ^. bidTime)
dbEval (AmendEvent (EventId eid) (MetadataChange mt v)) =pinsert AmendmentId"INSERT INTO event_metadata_amendments (event_id, mod_time, btc_addr) VALUES (?, ?, ?) RETURNING id"( eid, fromThyme $ mt ^. _ModTime, v)
readBids' :: AuctionId -> QDBM [Bid]readBids' aucId =pquery bidParser"SELECT user_id, bid_seconds, bid_amount, bid_time FROM bids WHERE auction_id = ?"(Only $ (aucId ^. _AuctionId))
dbEval (ReadWorkIndex pid) = dologEntries <- pquery logEntryParser"SELECT btc_addr, event_type, event_time, event_metadata FROM work_events WHERE project_id = ?"(Only $ PPid pid)pure $ workIndex logEntries
createUser' :: User -> QDBM UserIdcreateUser' user' =pinsert UserId"INSERT INTO users (handle, btc_addr, email) VALUES (?, ?, ?) RETURNING id"(user' ^. (username._UserName), user' ^. (userAddress._BtcAddr), user' ^. userEmail)
dbEval (CreateAuction pid auc) =pinsert AuctionId"INSERT INTO auctions (project_id, raise_amount, end_time) \\VALUES (?, ?, ?) RETURNING id"(pid ^. (_ProjectId), auc ^. (raiseAmount.to PBTC), auc ^. auctionEnd)
findUser' :: UserId -> QDBM (Maybe User)findUser' (UserId uid) = dousers <- pquery userParser"SELECT handle, btc_addr, email FROM users WHERE id = ?"(Only uid)pure $ headMay users
dbEval (FindAuction aucId) = doauctions <- pquery auctionParser"SELECT raise_amount, end_time FROM auctions WHERE id = ?"(Only (aucId ^. _AuctionId))pure $ headMay auctions
findUserByUserName' :: UserName -> QDBM (Maybe QDBUser)findUserByUserName' (UserName h) = dousers <- pquery qdbUserParser"SELECT id, handle, btc_addr, email FROM users WHERE handle = ?"(Only h)pure $ headMay users
dbEval (CreateBid (AuctionId aucId) bid) =pinsert BidId"INSERT INTO bids (auction_id, bidder_id, bid_seconds, bid_amount, bid_time) \\VALUES (?, ?, ?, ?, ?) RETURNING id"( aucId, bid ^. (bidUser._UserId), case bid ^. bidSeconds of (Seconds i) -> i, bid ^. (bidAmount.to PBTC), bid ^. bidTime)
createProject' :: Project -> QDBM ProjectIdcreateProject' p = dolet uid = p ^. (initiator._UserId)pid <- pinsert ProjectId"INSERT INTO projects (project_name, inception_date, initiator_id) VALUES (?, ?, ?) RETURNING id"(p ^. projectName, p ^. inceptionDate, uid)void $ pexec"INSERT INTO project_companions (project_id, user_id) VALUES (?, ?)"(pid ^. _ProjectId, uid)pure pid
dbEval (ReadBids aucId) =pquery bidParser"SELECT user_id, bid_seconds, bid_amount, bid_time FROM bids WHERE auction_id = ?"(Only $ (aucId ^. _AuctionId))
findProject' :: ProjectId -> QDBM (Maybe Project)findProject' (ProjectId pid) = doprojects <- pquery projectParser"SELECT project_name, inception_date, initiator_id FROM projects WHERE id = ?"(Only pid)pure $ headMay projects
dbEval (CreateUser user') =pinsert UserId"INSERT INTO users (handle, btc_addr, email) VALUES (?, ?, ?) RETURNING id"(user' ^. (username._UserName), user' ^. (userAddress._BtcAddr), user' ^. userEmail)
findUserProjects' :: UserId -> QDBM [QDBProject]findUserProjects' (UserId uid) =pquery qdbProjectParser"SELECT p.id, p.project_name, p.inception_date, p.initiator_id \\FROM projects p JOIN project_companions pc ON pc.project_id = p.id \\WHERE pc.user_id = ?"(Only uid)
dbEval (FindUser (UserId uid)) = dousers <- pquery userParser"SELECT handle, btc_addr, email FROM users WHERE id = ?"(Only uid)pure $ headMay users
postgresQDB :: QDB QDBMpostgresQDB = QDB{ createEvent = createEvent', amendEvent = amendEvent', findEvent = findEvent', findEvents = findEvents', readWorkIndex = readWorkIndex'
dbEval (CreateProject p) =pinsert ProjectId"INSERT INTO projects (project_name, inception_date, initiator_id, depreciation_fn) \\VALUES (?, ?, ?, ?) RETURNING id"(p ^. projectName, p ^. inceptionDate, p ^. (initiator._UserId), toJSON $ p ^. depf)
, createBid = createBid', readBids = readBids'
dbEval (FindUserProjects (UserId uid)) =pquery qdbProjectParser"SELECT p.id, p.project_name, p.inception_date, p.initiator_id \\FROM projects p JOIN project_companions pc ON pc.project_id = p.id \\WHERE pc.user_id = ? \\UNION \\SELECT p.id, p.project_name, p.inception_date, p.initiator_id \\FROM projects p \\WHERE p.initiator_id = ?"(uid, uid)
, createUser = createUser', findUser = findUser', findUserByUserName = findUserByUserName'
dbEval (AddUserToProject pid current new) = dovoid $ pexec"INSERT INTO project_companions (project_id, user_id, invited_by) VALUES (?, ?, ?)"(pid ^. _ProjectId, new ^. _UserId, current ^. _UserId)
, createProject = createProject', findProject = findProject', findUserProjects = findUserProjects'}
-- FIXME, these are just placeholdersdbEval (OpForbidden _ reason _) = fail $ show reasondbEval (SubjectNotFound _) = fail "Subject of operation was not found."
import Aftok.Utiltype KeyedUser = (UserId, User)type KeyedLogEntry = (ProjectId, UserId, LogEntry)type KeyedProject = (ProjectId, Project)type InvitingUID = UserIdtype InvitedUID = UserIdtype DBProg a = Program DBOp adata DBOp a whereCreateUser :: User -> DBOp UserIdFindUser :: UserId -> DBOp (Maybe User)FindUserByName :: UserName -> DBOp (Maybe KeyedUser)CreateProject :: Project -> DBOp ProjectIdFindProject :: ProjectId -> DBOp (Maybe Project)FindUserProjects :: UserId -> DBOp [KeyedProject]AddUserToProject :: ProjectId -> InvitingUID -> InvitedUID -> DBOp ()CreateEvent :: ProjectId -> UserId -> LogEntry -> DBOp EventIdAmendEvent :: EventId -> EventAmendment -> DBOp AmendmentIdFindEvent :: EventId -> DBOp (Maybe KeyedLogEntry)FindEvents :: ProjectId -> UserId -> Interval' -> DBOp [LogEntry]ReadWorkIndex :: ProjectId -> DBOp WorkIndexCreateAuction :: ProjectId -> Auction -> DBOp AuctionIdFindAuction :: AuctionId -> DBOp (Maybe Auction)CreateBid :: AuctionId -> Bid -> DBOp BidIdReadBids :: AuctionId -> DBOp [Bid]OpForbidden :: forall x. UserId -> OpForbiddenReason -> DBOp x -> DBOp xSubjectNotFound :: forall x. DBOp x -> DBOp xdata OpForbiddenReason = UserNotProjectMember| UserNotEventLoggerderiving (Eq, Show)class DBEval m wheredbEval :: DBOp a -> m a-- User opscreateUser :: User -> DBProg UserIdcreateUser = fc . CreateUserfindUser :: UserId -> DBProg (Maybe User)findUser = fc . FindUser
type QDBUser = (UserId, User)type QDBLogEntry = (EventId, ProjectId, UserId, LogEntry)type QDBProject = (ProjectId, Project)
findUserByName :: UserName -> DBProg (Maybe KeyedUser)findUserByName = fc . FindUserByName-- Project opscreateProject :: Project -> DBProg ProjectIdcreateProject p = dopid <- fc $ CreateProject paddUserToProject pid (p ^. initiator) (p ^. initiator)return pidfindProject :: ProjectId -> UserId -> DBProg (Maybe Project)findProject pid uid = dokps <- findUserProjects uidpure $ fmap snd (find (\(pid', _) -> pid' == pid) kps)findUserProjects :: UserId -> DBProg [KeyedProject]findUserProjects = fc . FindUserProjectsaddUserToProject :: ProjectId -> InvitingUID -> InvitedUID -> DBProg ()addUserToProject pid current new =withProjectAuth pid current $ AddUserToProject pid current newwithProjectAuth :: ProjectId -> UserId -> DBOp a -> DBProg awithProjectAuth pid uid act = dopx <- findUserProjects uidfc $ if any (\(pid', _) -> pid' == pid) pxthen actelse OpForbidden uid UserNotProjectMember act
data QDB m = QDB{ createEvent :: ProjectId -> UserId -> LogEntry -> m EventId, amendEvent :: EventId -> EventAmendment -> m AmendmentId, findEvent :: EventId -> m (Maybe QDBLogEntry), findEvents :: ProjectId -> UserId -> Interval' -> m [LogEntry], readWorkIndex :: ProjectId -> m WorkIndex
-- Log ops-- TODO: ignore "duplicate" events within some small time limit?createEvent :: ProjectId -> UserId -> LogEntry -> DBProg EventIdcreateEvent p u l = withProjectAuth p u $ CreateEvent p u lamendEvent :: UserId -> EventId -> EventAmendment -> DBProg AmendmentIdamendEvent uid eid a = doev <- findEvent eidlet act = AmendEvent eid aforbidden = OpForbidden uid UserNotEventLogger actmissing = SubjectNotFound actfc $ maybe missing (\(_, uid', _) -> if uid' == uid then act else forbidden) ev
, createBid :: AuctionId -> Bid -> m BidId, readBids :: AuctionId -> m [Bid]
findEvents :: ProjectId -> UserId -> Interval' -> DBProg [LogEntry]findEvents p u i = fc $ FindEvents p u i
, createUser :: User -> m UserId, findUser :: UserId -> m (Maybe User), findUserByUserName :: UserName -> m (Maybe QDBUser)
readWorkIndex :: ProjectId -> UserId -> DBProg WorkIndexreadWorkIndex pid uid = withProjectAuth pid uid $ ReadWorkIndex pid
, createProject :: Project -> m ProjectId, findProject :: ProjectId -> m (Maybe Project), findUserProjects :: UserId -> m [QDBProject]}
linearDepreciation :: Months -> -- ^ The number of initial months during which no depreciation occursMonths -> -- ^ The number of months over which each logged interval will be depreciatedDepFlinearDepreciation undepPeriod depPeriod =
linearDepreciation :: Months -- ^ The number of initial months during which no depreciation occurs-> Months -- ^ The number of months over which each logged interval will be depreciated-> DepF -- ^ The resulting configured depreciation function.linearDepreciation undepLength depLength =
{-# LANGUAGE RankNTypes #-}module Aftok.Util whereimport ClassyPreludeimport Control.Monad.Free.Churchimport Data.Functor.Coyonedatype Program f a = F (Coyoneda f) a-- Shouldn't this exist already in a library somewhere?interpret :: Monad m => (forall x. f x -> m x) -> Program f a -> m ainterpret nt p =let eval (Coyoneda cf cm) = nt cm >>= cfin iterM eval pfc :: f a -> Program f afc = liftF . liftCoyoneda
-- | others tbdinstance ToJSON DepreciationFunction wheretoJSON (LinearDepreciation (Months up) (Months dp)) =object [ "type" .= ("LinearDepreciation" :: Text), "arguments" .= (object [ "undep" .= up, "dep" .= dp])]instance FromJSON DepreciationFunction whereparseJSON (Object v) = dot <- v .: "text" :: Parser Textargs <- v .: "arguments"case unpack t of"LinearDepreciation" ->let undep = Months <$> (args .: "undep")dep = Months <$> (args .: "dep")in LinearDepreciation <$> undep <*> depx -> fail $ "No depreciation function recognized for type " <> xparseJSON _ = mzero
Nothing -> snapError 403 "Value of parameter projectId could not be parsed to a valid value."Just pid -> douid <- requireUserIdprojects <- liftPG . runReaderT $ findUserProjects uidif any (\p -> p ^. _1 == pid) projectsthen pure (uid, pid)else snapError 403 $ "User " ++ (tshow uid) ++ " does not have access to project " ++ (tshow pid)
Nothing -> snapError 400 "Value of parameter projectId could not be parsed to a valid value."Just pid -> pure pid
QDB{..} <- view qdb <$> with qm getpid <- fmap snd requireProjectAccessmp <- liftPG . runReaderT $ findProject pid
uid <- requireUserIdpid <- requireProjectIdmp <- snapEval $ findProject pid uid
QDB{..} <- view qdb <$> with qm getpid <- fmap snd requireProjectAccessliftPG . runReaderT $ readWorkIndex pid
uid <- requireUserIdpid <- requireProjectIdsnapEval $ readWorkIndex pid uid
(QModules QDB{..} df) <- with qm getpid <- fmap snd requireProjectAccesswidx <- liftPG . runReaderT $ readWorkIndex pid
uid <- requireUserIdpid <- requireProjectIdprojectMay <- snapEval $ findProject pid uidproject <- maybe (snapError 400 $ "Project not found for id " <> tshow pid) pure projectMaywidx <- snapEval $ readWorkIndex pid uid
if uid' == uidthen either(snapError 400 . pack)(liftPG . runReaderT . amendEvent eventId)(parseEither (parseEventAmendment modTime) requestJSON)else(snapError 403 "You do not have permission to view this event.")
either(snapError 400 . pack)(snapEval . amendEvent uid eventId)(parseEither (parseEventAmendment modTime) requestJSON)
-- | FIXME, make configurableqdbpgSnapletInit :: SnapletInit a QModulesqdbpgSnapletInit = makeSnaplet "qdbpg" "QDB on Postgresql" Nothing $ dopure $ QModules postgresQDB $ linearDepreciation (Months 6) (Months 60)
snapEval :: DBProg a -> Handler App App asnapEval p = liftPG . runReaderT . runQDBM $ interpret dbEval p