This adds a daemon application for handling the scheduled task of sending payment request emails. It consists of the following parts:
Add subscription route. Add billing list route. Add billable creation route. Move billing config to lib module Add payment request email templating to Billable. Add synthesis of billing URL from payment key Add memo & payment response URI to bill payload.
Fix a bunch of broken SQL using postgresql-simple-query-validator
See https://github.com/joncfoo/postgresql-simple-query-validator for the awesomeness.
IPG33FAWXGEQ2PO6OXRT2PWWXHRNMPVUKKADL6UKKN5GD2CNZ25AC
4B66XH43UYRVNTX57ORJ7U6IJTRFKSUS6IJ3CXVODMEF7NA7UHVQC
SOIAMXLWIB5RIEMKXUFMBSE2SKQQTMHYSW3DKUX6GEV4VNOQVHAQC
7VGYLTMURLVSVUYFW7TCRZTDQ6RE2EPSPPA43XKHDOBFWYVVSJHQC
4ZLEDBK7VGLKFUPENAFLUJYNFLKFYJ3TREPQ7P6PKMYGJUXB55HQC
LEINLS3X55PB6TSCNC5RVMDMV56XHTV4MNDUC42H7DDFMPDYUNTAC
EW2XN7KUMCAQNVFJJ5YTAVDZCPHNWDOEDMRFBUGLY6IE2HKNNX5AC
DFOBMSAODB3NKW37B272ZXA2ML5HMIH3N3C4GT2DPEQS7ZFK4SNAC
4U7F3CPIDTK6JSEDMNMHVKSR7HOQDLZQD2PPVMDLHO5SFSIMUXZAC
64C6AWH66FDKU6UE6Z6JPX2J2GBM2JOPTH2GL6LHKAIUBGNGDZ5AC
EKI57EJR65DA5FPILAHGHHAIU5ITVGHA6V3775OX7GV5XD67OWRQC
HMDM3B557TO5RYP2IGFFC2C2VN6HYZTDQ47CJY2O37BW55DSMFZAC
GCVQD44VRPQVKPZEPIC4AOIXLJIG2ZMV3QI2Y7KALUT6NVUBSGSAC
5DRIWGLUKMQZU2ZPBXSTLAWJKAMOD5YXAHM5LEDQHDFGYYLHWCDQC
7KZP4RHZ3QSYTPPQ257A65Z5UPX44TF2LAI2U5EMULQCLDCEUK2AC
AL37SVTCKRSG4HG2PCYK5Z7QSIZZH5JHH4Q2VLMXFAXSAQRFFG4QC
AXKKXBWN4EMUOLV43WN52JSKJPBV7TLSGLNJW5EZXHSJNKCYUWOQC
SEWTRB6S5PO5MQBLCPVBD7XT2BDYNZUE2RO6Z2XENZRIOCN6OZJAC
JFOEOFGA4CQR2LW43IVQGDZSPVJAD4KDN2DZMZXGM2QDIUD7AVCAC
NEDDHXUK3GNFMOFO3KLU7NRIHCTYNWBT3D6HTKZAOXFDE6HMDZ6AC
IZEVQF627FA7VV25KJAWYWGGC35LZUUBBQRPN5ZAINTQLPEDRTEAC
2XQD6KKKD6QVHFHAEMVE3XXY7X2T7BLTLL7XIILZAXNJJH2YONUQC
BSIUHCGFDFDFGWYMHZB7OVU3Z3IHPEUXRISIOPGZI2RUXZFDS2EQC
RN7EI6INGUUHGMNY5RU3NH56WPLRZY5ZMYDNFMNE3TGV4ESFLQIAC
A6HKMINBNGQLLX4QJMYWKQ4JAEHVJ4HIRVDKPPDI3FJUO2AAB7OQC
QMRKFEPGFBCEWAIXPEIG5ILKAJ2JH5L3TOITHR4HNJXK5FN3KXBQC
4FDQGIXN3Z4J55DILCSI5EOLIIA7R5CADTGFMW5X7N7MH6JIMBWAC
73NDXDEZRMK672GHSTC3CI6YHXFZ2GGJI5IKQGHKFDZKTNSQXLLQC
O227CEAV7BTKSE3SSC7XHC5IWEBXZL2AOOKJMBMOOFNTLINBLQMAC
Y3LIJ5USPMYBG7HKCIQBE7MWVGLQJPJSQD3KPZCTKXP22GOB535QC
QADKFHAR3KWQCNYU25Z7PJUGMD5WL26IU3DOAHBTRN2A7NKPUPKAC
WZFQDWW4XK6M4A4PQ7WQJUTZUPRGQR7V7ZVZY5ZTL5AMGIFMHB2QC
EMVTF2IWNQGRL44FC4JNG5FYYQTZSFPNM6SOM7IAEH6T7PPK2NVAC
NLZ3JXLOOIL37O3RRQWXHNPNSNEOOLPD6MCB754BEBECQB3KGR2AC
Q5X5RYQLP5K7REYD6VLHOKC4W36ZELJYA45V6YFKTD5S6MPN3NDQC
5OI44E4EEVYOMHDWNK2WA7K4L4JWRWCUJUNN2UAUGE5VY4W7GTNAC
NAS4BFL43SIUAAC663R6VFBHQ2DKI45K6Y6ZKVQI7S5547HBAN7QC
FXJQACESPGTLPG5ELXBU3M3OQXUZQQIR7HPIEHQ3FNUTMWVH4WBAC
W35DDBFYF6Z4ZPCFEO5RPAONZLYCSNTXUSTS6FIUUVZHCI6Q7GHAC
Z3MK2PJ5U222DXRS22WCDHVPZ7HVAR3HOCUNXIGX6VMEPBQDF6PQC
WAIX6AGNDVJOKTWZ7OP7QOYSJHAJSX5EOWXZHOAO2IG6ALWUCJ6QC
O5FVTOM6YFBLEPF3S576K6IMT6ZZ5VQCSB3YVXNS4CKBITKCRZ7AC
POX3UAMTGCNS3SU5I6IKDNCCSHEAUSFBZ3WCMQ3EXKPNXETOI6BAC
O722AOKEWXWJPRHGJREU6QPW7HEFPPRETZIAADZ2RMAXHARCNEKAC
NVOCQVASZWTKQJG7GPH7KHKZZR7NUG4WLV5YY4KAIRPCJRWCZPIAC
RPAJLHMTUJU4AYNBOHVGHGGB4NY2NLY3BVPYN5FMWB3ZIMAUQHCQC
KNSI575VAW6HRCZYXOEPQ4DTSML4EORML5MV4DJBRKE7TXCPS4EAC
GKGVYBZGPJXO7N7GLHLRNYQPXFHBQSNQN53OKRFCXLQEYDTC5I4QC
Z7KS5XHHC6PAMTVBHXY7KUSS3BWAOU6FSYIITUCFOOJZU4OUJHBAC
3QVT6MA6I2CILQH3LUZABS4YQ7MN6CNRYTDRVS376OOHTPLYTFJAC
2WOOGXDHVQ6L2MQYUTLJ6H6FVSQNJN6SMJL5DG7HAHFYPJLRT2SAC
V2VDN77HCSRYYWXDJJ2XOVHV4P6PVWNJZLXZ7JUYPQEZQIH5BZ3QC
MJ6R42RCK2ASXAJ6QXDPMAW56RBOJ4F4HI2LFIV3KXFIKWYMQK3QC
HO2PFRABW6BBTE4MUKUTEGXCMJS46WGVBCNWOHO4OL52DVAB4YDAC
6L5BK5EHPAOQX3JCKUJ273UDNAC23LPQL4HIJGM4AV3P3QK5OKIQC
LCBJULKEU4I5FRUGYNRQSHXYLY5X3LRTSNWDCTZLS7YE4BBBDE6AC
HALRDT2F22DAQ54M334YSKD2XJCAXDXEZPDKGMOWKH7XELBAS6MAC
ASF3UPJLCX7KIUCNJD5KAXSPDXCUEJHLL4HBRTRUBPCW73IXOCWQC
I2KHGVD44KT4MQJXGCTVSQKMBO6TVCY72F26TLXGWRL6PHGF6RNQC
2G3GNDDUOVPF45PELJ65ZB2IXEHJJXJILFRVHZXGPXUL4BVNZJFQC
PBD7LZYQHXAA3KLH2ZUX5GW4UFML6BQ32KXZF4KZ6OYFASUYFJ5QC
BROSTG5KP3NUNLSYPVQID254TE47E5RKQAKLPIY7PGWETE6JNMTAC
ADMKQQGCGVSHHIMVQ4XFRDCG544SBJCYALSKZV45CQQBZ4ACUH2AC
MGOF7IUFGXYQKZOKMM2GGULFFVAULEHLZDSHMUW6B5DBKVXXR74AC
UILI6PILCRDPZ3XYA54LGIGPSU7ERWNHCE7R3CE64ZEC7ONOEMOQC
DXIGERDTERUIG7QHHRPKTSJHSQEPJPDJVLUW7YVC7URXBQ4ZJVOAC
XTBSG4C7SCZUFOU2BTNFR6B6TCGYI35BWUV4PVTS3N7KNH5VEARQC
, system-filepath
, text
, thyme
, transformers
, uuid
, wreq
, x509
, x509-store
Executable aftok-daemon
default-language: Haskell2010
ghc-options: -Wall -Werror
hs-source-dirs: daemon
default-extensions: NoImplicitPrelude
, OverloadedStrings
, RecordWildCards
, ScopedTypeVariables
, KindSignatures
main-is: Main.hs
other-modules: AftokD
, AftokD.AftokM
build-depends:
aftok
, base
, aeson
, attoparsec
, base64-bytestring
, bytestring
, bippy
, cereal
, classy-prelude
, containers
, configurator
, cryptonite
, either
, errors
, hourglass
, HStringTemplate
, iso8601-time
, HsOpenSSL
, http-client
, http-client-openssl
, lens
, mime-mail
, mtl
, network
, network-uri
, optparse-applicative
, postgresql-simple
, protobuf
, smtp-mail
, system-filepath
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE FlexibleContexts #-}
module AftokD.AftokM where
import ClassyPrelude
import Control.Error.Util (maybeT)
import Control.Lens ((^.), makeLenses, makeClassyPrisms, traverseOf, to)
import Control.Monad.IO.Class (MonadIO(..))
import Control.Monad.Except (MonadError, throwError)
import Control.Monad.Reader (MonadReader)
import Control.Monad.Trans.Except (ExceptT, withExceptT, runExceptT)
import Control.Monad.Trans.Reader (mapReaderT, withReaderT)
import Crypto.Random.Types (MonadRandom(..))
import Database.PostgreSQL.Simple (Connection, connect)
import Data.Thyme.Clock as C
import Data.Thyme.Time as T
import Network.Mail.Mime
import Network.Mail.SMTP as SMTP
import Network.URI (URI, parseURI)
import Text.StringTemplate
import Filesystem.Path.CurrentOS (encodeString)
import Network.Bippy.Types (Satoshi)
import Aftok (User, UserId, userEmail, _Email)
import Aftok.Types (satoshi)
import qualified Aftok.Config as AC
import Aftok.Billables (Billable, Billable', Subscription', customer, name, billable, project, paymentRequestEmailTemplate, paymentRequestMemoTemplate)
import qualified Aftok.Database as DB
import Aftok.Database.PostgreSQL (QDBM(..))
import qualified Aftok.Payments as P
import Aftok.Payments.Types (PaymentKey(..), subscription, paymentRequestTotal, paymentKey)
import Aftok.Project (Project, ProjectId(..), projectName)
import qualified AftokD as D
data AftokDErr
= ConfigError Text
| DBErr DB.DBError
| PaymentErr P.PaymentError
makeClassyPrisms ''AftokDErr
instance P.AsPaymentError AftokDErr where
_PaymentError = _PaymentErr . P._PaymentError
_Overdue = _PaymentErr . P._Overdue
_SigningError = _PaymentErr . P._SigningError
data AftokMEnv = AftokMEnv
{ _dcfg :: !D.Config
, _conn :: !Connection
, _pcfg :: !P.PaymentsConfig
}
makeLenses ''AftokMEnv
instance P.HasPaymentsConfig AftokMEnv where
network = pcfg . P.network
signingKey = pcfg . P.signingKey
pkiData = pcfg . P.pkiData
paymentsConfig = pcfg
newtype AftokM a = AftokM { runAftokM :: ReaderT AftokMEnv (ExceptT AftokDErr IO) a }
deriving (Functor, Applicative, Monad, MonadIO, MonadError AftokDErr, MonadReader AftokMEnv)
instance MonadRandom AftokM where
getRandomBytes = liftIO . getRandomBytes
instance DB.MonadDB AftokM where
liftdb = liftQDBM . DB.liftdb
liftQDBM :: QDBM a -> AftokM a
liftQDBM (QDBM r) =
AftokM . mapReaderT (withExceptT DBErr) . withReaderT _conn $ r
createAllPaymentRequests :: D.Config -> IO ()
createAllPaymentRequests cfg = do
conn' <- connect $ cfg ^. D.dbConfig
pcfg' <- AC.toPaymentsConfig $ cfg ^. D.billingConfig
let env = AftokMEnv cfg conn' pcfg'
void . runExceptT $ (runReaderT . runAftokM) createProjectsPaymentRequests $ env
createProjectsPaymentRequests :: AftokM ()
createProjectsPaymentRequests = do
projects <- liftQDBM $ DB.listProjects
traverse_ createProjectPaymentRequests projects
createProjectPaymentRequests :: ProjectId -> AftokM ()
createProjectPaymentRequests pid = do
now <- liftIO C.getCurrentTime
let ops = P.BillingOps memoGen (fmap Just . paymentURL) payloadGen
subscribers <- liftQDBM $ DB.findSubscribers pid
requests <- traverse (\uid -> P.createPaymentRequests ops now uid pid) $ subscribers
traverse_ sendPaymentRequestEmail (join requests)
sendPaymentRequestEmail :: P.PaymentRequestId -> AftokM ()
sendPaymentRequestEmail reqId = do
cfg <- ask
let AC.SmtpConfig{..} = cfg ^. (dcfg . D.smtpConfig)
preqCfg = cfg ^. (dcfg . D.paymentRequestConfig)
reqMay = do
preq <- DB.findPaymentRequestId reqId
preq' <- traverseOf P.subscription DB.findSubscriptionBillable preq
preq'' <- traverseOf (P.subscription . customer) DB.findUser preq'
traverseOf (P.subscription . billable . project) DB.findProject preq''
req <- maybeT (throwError $ DBErr DB.SubjectNotFound) pure reqMay
bip70URL <- paymentURL (req ^. paymentKey)
mail <- buildPaymentRequestEmail preqCfg req bip70URL
let mailer = maybe (sendMailWithLogin _smtpHost) (sendMailWithLogin' _smtpHost) _smtpPort
liftIO $ mailer _smtpUser _smtpPass mail
buildPaymentRequestEmail :: (MonadIO m, MonadError AftokDErr m)
=> D.PaymentRequestConfig
-> P.PaymentRequest' (Subscription' User (Billable' Project UserId Satoshi))
-> URI
-> m Mail
buildPaymentRequestEmail cfg req paymentUrl = do
templates <- liftIO . directoryGroup $ encodeString (cfg ^. D.templatePath)
let billTemplate = (newSTMP . unpack) <$> req ^. (subscription . billable . paymentRequestEmailTemplate)
defaultTemplate = getStringTemplate "payment_request" templates
case billTemplate <|> defaultTemplate of
Nothing -> throwError $ ConfigError "Could not find template for invitation email"
Just template ->
let fromEmail = cfg ^. D.billingFromEmail
toEmail = req ^. (subscription . customer . userEmail)
pname = req ^. (subscription . billable . project . projectName)
total = req ^. (P.paymentRequest . to paymentRequestTotal)
setAttrs = setManyAttrib
[ ("from_email", fromEmail ^. _Email)
, ("project_name", pname)
, ("to_email", toEmail ^. _Email)
, ("amount_due", tshow $ total ^. satoshi)
, ("payment_url", tshow paymentUrl)
]
fromAddr = Address Nothing ("billing@aftok.com")
toAddr = Address Nothing (toEmail ^. _Email)
subject = "Payment is due for your "<>pname<>" subscription!"
body = plainTextPart . render $ setAttrs template
in pure $ SMTP.simpleMail fromAddr [toAddr] [] [] subject [body]
memoGen :: Subscription' UserId Billable
-> T.Day
-> C.UTCTime
-> AftokM (Maybe Text)
memoGen sub billingDate requestTime = do
req <- traverseOf (billable . project) DB.findProjectOrError sub
let template = (newSTMP . unpack) <$> (sub ^. (billable . paymentRequestMemoTemplate))
setAttrs = setManyAttrib
[ ("project_name", req ^. (billable . project . projectName))
, ("subscription", req ^. (billable . name))
, ("billing_date", tshow billingDate)
, ("issue_time", tshow requestTime)
]
pure $ fmap (render . setAttrs) template
-- The same URL is used for retrieving a BIP-70 payment request and for submitting
-- the response.
paymentURL :: PaymentKey -> AftokM URI
paymentURL (PaymentKey k) = do
env <- ask
let hostname = env ^. (dcfg . D.paymentRequestConfig . D.aftokHost)
paymentRequestPath = "https://" <> hostname <> "/pay/" <> k
maybe
(throwError . ConfigError $ "Could not parse path " <> paymentRequestPath <> " to a valid URI")
pure
(parseURI $ show paymentRequestPath)
payloadGen :: Monad m => Subscription' UserId Billable -> T.Day -> C.UTCTime -> m (Maybe ByteString)
payloadGen _ _ _ = pure Nothing
{-# LANGUAGE TemplateHaskell #-}
module AftokD where
import ClassyPrelude hiding (FilePath)
import Control.Lens
import qualified Data.Configurator as C
import qualified Data.Configurator.Types as CT
import Database.PostgreSQL.Simple (ConnectInfo)
import Filesystem.Path.CurrentOS (FilePath, fromText, encodeString)
import Aftok (Email(..))
import qualified Aftok.Config as AC
data PaymentRequestConfig = PaymentRequestConfig
{ _aftokHost :: Text
, _templatePath :: FilePath
, _billingFromEmail :: Email
}
makeLenses ''PaymentRequestConfig
data Config = Config
{ _smtpConfig :: AC.SmtpConfig
, _billingConfig :: AC.BillingConfig
, _dbConfig :: ConnectInfo
, _paymentRequestConfig :: PaymentRequestConfig
}
makeLenses ''Config
loadConfig :: FilePath -> IO Config
loadConfig cfgFile =
readConfig =<< C.load [C.Required $ encodeString cfgFile]
readConfig :: CT.Config -> IO Config
readConfig cfg = Config
<$> (AC.readSmtpConfig $ C.subconfig "smtp" cfg)
<*> (AC.readBillingConfig $ C.subconfig "billing" cfg)
<*> (AC.readConnectInfo $ C.subconfig "db" cfg)
<*> (readPaymentRequestConfig $ C.subconfig "payment_requests" cfg)
readPaymentRequestConfig :: CT.Config -> IO PaymentRequestConfig
readPaymentRequestConfig cfg = PaymentRequestConfig
<$> C.require cfg "aftok_host"
<*> (fromText <$> C.require cfg "template_path")
<*> (Email <$> C.require cfg "payment_from_email")
{-# LANGUAGE TemplateHaskell #-}
module Main (main) where
import ClassyPrelude
import System.Environment (getEnv)
import Filesystem.Path.CurrentOS (decodeString)
import qualified AftokD as D
import AftokD.AftokM (createAllPaymentRequests)
main :: IO ()
main = do
cfgPath <- try $ getEnv "AFTOK_CFG" :: IO (Either IOError String)
cfg <- D.loadConfig . decodeString $ either (const "conf/aftok.cfg") id cfgPath
createAllPaymentRequests cfg
{-# LANGUAGE TemplateHaskell #-}
module Aftok.Config where
import ClassyPrelude hiding (FilePath)
import Control.Lens (makeClassy, (^.))
import qualified Data.Configurator as C
import qualified Data.Configurator.Types as CT
import Data.X509
import Data.X509.File (readKeyFile, readSignedObject)
import Database.PostgreSQL.Simple (ConnectInfo(..))
import Filesystem.Path.CurrentOS (FilePath, fromText, encodeString)
import qualified Network.Bippy.Types as BT
import qualified Network.Mail.SMTP as SMTP
import qualified Network.Socket as NS
import Aftok.Payments (PaymentsConfig(..))
data SmtpConfig = SmtpConfig
{ _smtpHost :: NS.HostName
, _smtpPort :: Maybe NS.PortNumber
, _smtpUser :: SMTP.UserName
, _smtpPass :: SMTP.Password
}
makeClassy ''SmtpConfig
data BillingConfig = BillingConfig
{ _network :: BT.Network
, _signingKeyFile :: FilePath
, _certsFile :: FilePath
, _exchangeRateServiceURI :: String
}
makeClassy ''BillingConfig
readSmtpConfig :: CT.Config -> IO SmtpConfig
readSmtpConfig cfg =
SmtpConfig <$> C.require cfg "smtpHost"
<*> ((fmap . fmap) fromInteger $ C.lookup cfg "smtpPort")
<*> C.require cfg "smtpUser"
<*> C.require cfg "smtpKey"
readBillingConfig :: CT.Config -> IO BillingConfig
readBillingConfig cfg =
BillingConfig <$> (parseNetwork <$> C.require cfg "network")
<*> (fromText <$> C.require cfg "signingKeyFile")
<*> (fromText <$> C.require cfg "certsFile")
<*> C.require cfg "exchangeRateServiceURI"
where parseNetwork :: String -> BT.Network
parseNetwork "main" = BT.MainNet
parseNetwork _ = BT.TestNet
readConnectInfo :: CT.Config -> IO ConnectInfo
readConnectInfo cfg =
ConnectInfo <$> C.require cfg "host"
<*> C.require cfg "port"
<*> C.require cfg "user"
<*> C.require cfg "password"
<*> C.require cfg "database"
toPaymentsConfig :: BillingConfig -> IO PaymentsConfig
toPaymentsConfig c = do
privKeys <- readKeyFile . encodeString $ c ^. signingKeyFile
pkiEntries <- readSignedObject . encodeString $ c ^. certsFile
privKey <- case headMay privKeys of
Just (PrivKeyRSA k) -> pure k
Just (PrivKeyDSA _) -> fail "DSA keys not supported for payment request signing."
Nothing -> fail $ "No keys found in private key file " <> encodeString (c ^. signingKeyFile)
let pkiData = BT.X509SHA256 . CertificateChain $ pkiEntries
pure $ PaymentsConfig (c ^. network) privKey pkiData
pinsert EventId
"INSERT INTO aftok_events \
\(event_time, created_by, event_type, event_json) \
\VALUES (?, ?, ?, ?) RETURNING id"
pinsert EventId
[sql| INSERT INTO aftok_events
(event_time, created_by, event_type, event_json)
VALUES (?, ?, ?, ?) RETURNING id |]
pinsert EventId
"INSERT INTO work_events \
\(project_id, user_id, credit_to_type, credit_to_btc_addr, event_type, event_time, event_metadata) \
\VALUES (?, ?, ?, ?, ?, ?, ?) \
\RETURNING id"
pinsert EventId
[sql| INSERT INTO work_events
( project_id, user_id, credit_to_type, credit_to_address
, event_type, event_time, event_metadata )
VALUES (?, ?, ?, ?, ?, ?, ?)
RETURNING id |]
pinsert EventId
"INSERT INTO work_events \
\(project_id, user_id, credit_to_type, credit_to_project_id, event_type, event_time, event_metadata) \
\VALUES (?, ?, ?, ?, ?, ?, ?) \
\RETURNING id"
pinsert EventId
[sql| INSERT INTO work_events
( project_id, user_id, credit_to_type, credit_to_project_id
, event_type, event_time, event_metadata )
VALUES (?, ?, ?, ?, ?, ?, ?)
RETURNING id |]
pinsert EventId
"INSERT INTO work_events \
\(project_id, user_id, credit_to_type, credit_to_user_id, event_type, event_time, event_metadata) \
\VALUES (?, ?, ?, ?, ?, ?, ?) \
\RETURNING id"
pinsert EventId
[sql| INSERT INTO work_events
(project_id, user_id, credit_to_type, credit_to_user_id, event_type, event_time, event_metadata)
VALUES (?, ?, ?, ?, ?, ?, ?)
RETURNING id |]
headMay <$> pquery qdbLogEntryParser
"SELECT project_id, user_id, \
\credit_to_type, credit_to_btc_addr, credit_to_user_id, credit_to_project_id, \
\event_type, event_time, event_metadata FROM work_events \
\WHERE id = ?"
headMay <$> pquery qdbLogEntryParser
[sql| SELECT project_id, user_id,
credit_to_type, credit_to_address, credit_to_user_id, credit_to_project_id,
event_type, event_time, event_metadata FROM work_events
WHERE id = ? |]
let q (Before e) = pquery logEntryParser
"SELECT btc_addr, event_type, event_time, event_metadata FROM work_events \
\WHERE project_id = ? AND user_id = ? AND event_time <= ?"
let q (Before e) = pquery logEntryParser
[sql| SELECT credit_to_type, credit_to_address, credit_to_user_id, credit_to_project_id,
event_type, event_time,
event_metadata
FROM work_events
WHERE project_id = ? AND user_id = ? AND event_time <= ? |]
q (During s e) = pquery logEntryParser
"SELECT btc_addr, event_type, event_time, event_metadata FROM work_events \
\WHERE project_id = ? AND user_id = ? \
\AND event_time >= ? AND event_time <= ?"
q (During s e) = pquery logEntryParser
[sql| SELECT credit_to_type, credit_to_address, credit_to_user_id, credit_to_project_id,
event_type, event_time, event_metadata
FROM work_events
WHERE project_id = ? AND user_id = ?
AND event_time >= ? AND event_time <= ? |]
q (After s) = pquery logEntryParser
"SELECT btc_addr, event_type, event_time, event_metadata FROM work_events \
\WHERE project_id = ? AND user_id = ? AND event_time >= ?"
q (After s) = pquery logEntryParser
[sql| SELECT credit_to_type, credit_to_address, credit_to_user_id, credit_to_project_id,
event_type, event_time, event_metadata
FROM work_events
WHERE project_id = ? AND user_id = ? AND event_time >= ? |]
pinsert AmendmentId
"INSERT INTO event_time_amendments \
\(event_id, amended_at, event_time) \
\VALUES (?, ?, ?) RETURNING id"
pinsert AmendmentId
[sql| INSERT INTO event_time_amendments
(event_id, amended_at, event_time)
VALUES (?, ?, ?) RETURNING id |]
pinsert AmendmentId
"INSERT INTO event_credit_to_amendments \
\(event_id, amended_at, credit_to_type, credit_to_btc_addr) \
\VALUES (?, ?, ?, ?) RETURNING id"
pinsert AmendmentId
[sql| INSERT INTO event_credit_to_amendments
(event_id, amended_at, credit_to_type, credit_to_btc_addr)
VALUES (?, ?, ?, ?) RETURNING id |]
pinsert AmendmentId
"INSERT INTO event_credit_to_amendments \
\(event_id, amended_at, credit_to_type, credit_to_project_id) \
\VALUES (?, ?, ?, ?) RETURNING id"
pinsert AmendmentId
[sql| INSERT INTO event_credit_to_amendments
(event_id, amended_at, credit_to_type, credit_to_project_id)
VALUES (?, ?, ?, ?) RETURNING id |]
pinsert AmendmentId
"INSERT INTO event_credit_to_amendments \
\(event_id, amended_at, credit_to_type, credit_to_user_id) \
\VALUES (?, ?, ?, ?) RETURNING id"
pinsert AmendmentId
[sql| INSERT INTO event_credit_to_amendments
(event_id, amended_at, credit_to_type, credit_to_user_id)
VALUES (?, ?, ?, ?) RETURNING id |]
pinsert AmendmentId
"INSERT INTO event_metadata_amendments \
\(event_id, amended_at, event_metadata) \
\VALUES (?, ?, ?) RETURNING id"
pinsert AmendmentId
[sql| INSERT INTO event_metadata_amendments
(event_id, amended_at, event_metadata)
VALUES (?, ?, ?) RETURNING id |]
logEntries <- pquery logEntryParser
"SELECT btc_addr, event_type, event_time, event_metadata FROM work_events WHERE project_id = ?"
logEntries <- pquery logEntryParser
[sql| SELECT credit_to_type, credit_to_address, credit_to_user_id, credit_to_project_id,
event_type, event_time, event_metadata
FROM work_events
WHERE project_id = ? |]
pinsert A.AuctionId
"INSERT INTO auctions (project_id, user_id, raise_amount, end_time) \
\VALUES (?, ?, ?, ?) RETURNING id"
pinsert A.AuctionId
[sql| INSERT INTO auctions (project_id, initiator_id, raise_amount, end_time)
VALUES (?, ?, ?, ?) RETURNING id |]
headMay <$> pquery auctionParser
"SELECT project_id, initiator_id, created_at, raise_amount, start_time, end_time FROM auctions WHERE id = ?"
headMay <$> pquery auctionParser
[sql| SELECT project_id, initiator_id, created_at, raise_amount, start_time, end_time
FROM auctions
WHERE id = ? |]
pinsert A.BidId
"INSERT INTO bids (auction_id, bidder_id, bid_seconds, bid_amount, bid_time) \
\VALUES (?, ?, ?, ?, ?) RETURNING id"
pinsert A.BidId
[sql| INSERT INTO bids (auction_id, bidder_id, bid_seconds, bid_amount, bid_time)
VALUES (?, ?, ?, ?, ?) RETURNING id |]
pquery ((,) <$> idParser A.BidId <*> bidParser)
"SELECT id, user_id, bid_seconds, bid_amount, bid_time FROM bids WHERE auction_id = ?"
pquery ((,) <$> idParser A.BidId <*> bidParser)
[sql| SELECT id, bidder_id, bid_seconds, bid_amount, bid_time FROM bids WHERE auction_id = ? |]
headMay <$> pquery ((,) <$> idParser UserId <*> userParser)
"SELECT id, handle, btc_addr, email FROM users WHERE handle = ?"
headMay <$> pquery ((,) <$> idParser UserId <*> userParser)
[sql| SELECT id, handle, btc_addr, email FROM users WHERE handle = ? |]
void $ pexec
"INSERT INTO invitations (project_id, invitor_id, invitee_email, invitation_key, invitation_time) \
\VALUES (?, ?, ?, ?, ?)"
void $ pexec
[sql| INSERT INTO invitations (project_id, invitor_id, invitee_email, invitation_key, invitation_time)
VALUES (?, ?, ?, ?, ?) |]
headMay <$> pquery invitationParser
"SELECT project_id, invitor_id, invitee_email, invitation_time, acceptance_time \
\FROM invitations WHERE invitation_key = ?"
headMay <$> pquery invitationParser
[sql| SELECT project_id, invitor_id, invitee_email, invitation_time, acceptance_time
FROM invitations WHERE invitation_key = ? |]
void $ pexec
"INSERT INTO project_companions (project_id, user_id, invited_by, joined_at) \
\SELECT i.project_id, ?, i.invitor_id, ? \
\FROM invitations i \
\WHERE i.invitation_key = ?"
void $ pexec
[sql| INSERT INTO project_companions (project_id, user_id, invited_by, joined_at)
SELECT i.project_id, ?, i.invitor_id, ?
FROM invitations i
WHERE i.invitation_key = ? |]
pinsert P.ProjectId
"INSERT INTO projects (project_name, inception_date, initiator_id, depreciation_fn) \
\VALUES (?, ?, ?, ?) RETURNING id"
pinsert P.ProjectId
[sql| INSERT INTO projects (project_name, inception_date, initiator_id, depreciation_fn)
VALUES (?, ?, ?, ?) RETURNING id |]
pgEval ListProjects =
pquery (idParser P.ProjectId)
[sql| SELECT id FROM projects |]
()
pgEval (FindSubscribers pid) =
pquery (idParser UserId)
[sql| SELECT s.user_id
FROM subscripions s
JOIN billables b ON s.billable_id = b.id
WHERE b.project_id = ? |]
(Only (pid ^. P._ProjectId))
headMay <$> pquery projectParser
"SELECT project_name, inception_date, initiator_id, depreciation_fn FROM projects WHERE id = ?"
headMay <$> pquery projectParser
[sql| SELECT project_name, inception_date, initiator_id, depreciation_fn FROM projects WHERE id = ? |]
pquery ((,) <$> idParser P.ProjectId <*> projectParser)
"SELECT p.id, p.project_name, p.inception_date, p.initiator_id, p.depreciation_fn \
\FROM projects p LEFT OUTER JOIN project_companions pc ON pc.project_id = p.id \
\WHERE pc.user_id = ? \
\OR p.initiator_id = ?"
pquery ((,) <$> idParser P.ProjectId <*> projectParser)
[sql| SELECT p.id, p.project_name, p.inception_date, p.initiator_id, p.depreciation_fn
FROM projects p LEFT OUTER JOIN project_companions pc ON pc.project_id = p.id
WHERE pc.user_id = ?
OR p.initiator_id = ? |]
pinsert B.BillableId
"INSERT INTO billables \
\( project_id, event_id, name, description \
\, recurrence_type, recurrence_count \
\, billing_amount, grace_period_days) \
\VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING id"
pinsert B.BillableId
[sql| INSERT INTO billables
( project_id, event_id, name, description
, recurrence_type, recurrence_count
, billing_amount, grace_period_days
, payment_request_email_template
, payment_request_memo_template)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id |]
headMay <$> pquery billableParser
"SELECT b.project_id, e.created_by, b.name, b.description, b.recurrence_type, b.recurrence_count, \
\ b.billing_amount, b.grace_period_days \
\FROM billables b JOIN aftok_events e ON e.id = b.event_id \
\WHERE b.id = ?"
headMay <$> pquery billableParser
[sql| SELECT b.project_id, e.created_by, b.name, b.description,
b.recurrence_type, b.recurrence_count,
b.billing_amount, b.grace_period_days,
b.payment_request_email_template, b.payment_request_memo_template
FROM billables b JOIN aftok_events e ON e.id = b.event_id
WHERE b.id = ? |]
pgEval (FindBillables pid) =
pquery ((,) <$> idParser B.BillableId <*> billableParser)
[sql| SELECT b.id, b.project_id, e.created_by, b.name, b.description,
b.recurrence_type, b.recurrence_count,
b.billing_amount, b.grace_period_days
b.payment_request_email_template, b.payment_request_memo_template
FROM billables b JOIN aftok_events e ON e.id = b.event_id
WHERE b.project_id = ? |]
(Only (pid ^. P._ProjectId))
pinsert B.SubscriptionId
"INSERT INTO subscriptions \
\(user_id, billable_id, event_id) \
\VALUES (?, ?, ?, ?) RETURNING id"
pinsert B.SubscriptionId
[sql| INSERT INTO subscriptions
(user_id, billable_id, event_id, start_date)
VALUES (?, ?, ?, ?) RETURNING id |]
headMay <$> pquery subscriptionParser
"SELECT id, billable_id, start_date, end_date \
\FROM subscriptions s \
\WHERE s.id = ?"
headMay <$> pquery subscriptionParser
[sql| SELECT id, billable_id, start_date, end_date
FROM subscriptions s
WHERE s.id = ? |]
pquery ((,) <$> idParser B.SubscriptionId <*> subscriptionParser)
"SELECT id, user_id, billable_id, start_date, end_date \
\FROM subscriptions s \
\JOIN billables b ON b.id = s.billable_id \
\WHERE s.user_id = ? \
\AND b.project_id = ?"
pquery ((,) <$> idParser B.SubscriptionId <*> subscriptionParser)
[sql| SELECT s.id, user_id, billable_id, start_date, end_date
FROM subscriptions s
JOIN billables b ON b.id = s.billable_id
WHERE s.user_id = ?
AND b.project_id = ? |]
pinsert PaymentRequestId
"INSERT INTO payment_requests \
\(subscription_id, event_id, request_data, url_key, request_time, billing_date) \
\VALUES (?, ?, ?, ?, ?, ?) RETURNING id"
pinsert PaymentRequestId
[sql| INSERT INTO payment_requests
(subscription_id, event_id, request_data, url_key, request_time, billing_date)
VALUES (?, ?, ?, ?, ?, ?) RETURNING id |]
headMay <$> pquery ((,) <$> idParser PaymentRequestId <*> paymentRequestParser)
"SELECT id, subscription_id, request_data, url_key, request_time, billing_date \
\FROM payment_requests \
\WHERE url_key = ? \
\AND id NOT IN (SELECT payment_request_id FROM payments)"
headMay <$> pquery ((,) <$> idParser PaymentRequestId <*> paymentRequestParser)
[sql| SELECT id, subscription_id, request_data, url_key, request_time, billing_date
FROM payment_requests
WHERE url_key = ?
AND id NOT IN (SELECT payment_request_id FROM payments) |]
pquery ((,) <$> idParser PaymentRequestId <*> paymentRequestParser)
"SELECT id, subscription_id, request_data, url_key, request_time, billing_date \
\FROM payment_requests \
\WHERE subscription_id = ?"
pquery ((,) <$> idParser PaymentRequestId <*> paymentRequestParser)
[sql| SELECT id, subscription_id, request_data, url_key, request_time, billing_date
FROM payment_requests
WHERE subscription_id = ? |]
in pquery rowp
"SELECT r.url_key, \
\ r.subscription_id, r.request_data, r.url_key, r.request_time, r.billing_date, \
\ s.user_id, s.billable_id, s.start_date, s.end_date, \
\ b.project_id, e.created_by, b.name, b.description, b.recurrence_type, \
\ b.recurrence_count, b.billing_amount, b.grace_period_days \
\FROM payment_requests r \
\JOIN subscriptions s on s.id = r.subscription_id \
\JOIN billables b on b.id = s.billable_id \
\WHERE subscription_id = ? \
\AND r.id NOT IN (SELECT payment_request_id FROM payments)"
in pquery rowp
[sql| SELECT r.url_key,
r.subscription_id, r.request_data, r.url_key, r.request_time, r.billing_date,
s.user_id, s.billable_id, s.start_date, s.end_date,
b.project_id, e.created_by, b.name, b.description, b.recurrence_type,
b.recurrence_count, b.billing_amount, b.grace_period_days,
b.payment_request_email_template, b.payment_request_memo_template
FROM payment_requests r
JOIN subscriptions s on s.id = r.subscription_id
JOIN billables b on b.id = s.billable_id
JOIN aftok_events e on e.id = b.event_id
WHERE subscription_id = ?
AND r.id NOT IN (SELECT payment_request_id FROM payments) |]
pinsert PaymentId
"INSERT INTO payments \
\(payment_request_id, event_id, payment_data, payment_date, exchange_rates) \
\VALUES (?, ?, ?, ?) RETURNING id"
pinsert PaymentId
[sql| INSERT INTO payments
(payment_request_id, event_id, payment_data, payment_date, exchange_rates)
VALUES (?, ?, ?, ?, ?) RETURNING id |]
pquery ((,) <$> idParser PaymentId <*> paymentParser)
"SELECT id, payment_request_id, payment_data, payment_date \
\FROM payments \
\WHERE payment_request_id = ?"
pquery ((,) <$> idParser PaymentId <*> paymentParser)
[sql| SELECT id, payment_request_id, payment_data, payment_date
FROM payments
WHERE payment_request_id = ? |]
findUserByName :: (MonadDB m) => UserName -> m (Maybe (UserId, User))
findUserByName = liftdb . FindUserByName
findUserByName :: (MonadDB m) => UserName -> MaybeT m (UserId, User)
findUserByName = MaybeT . liftdb . FindUserByName
findProject :: (MonadDB m) => ProjectId -> UserId -> m (Maybe Project)
findProject pid uid = do
kps <- findUserProjects uid
pure $ fmap snd (find (\(pid', _) -> pid' == pid) kps)
findProject :: (MonadDB m) => ProjectId -> MaybeT m Project
findProject = MaybeT . liftdb . FindProject
findProjectOrError :: (MonadDB m) => ProjectId -> m Project
findProjectOrError pid = fromMaybeT
(raiseSubjectNotFound $ FindProject pid)
(findProject pid)
findUserProject :: (MonadDB m) => UserId -> ProjectId -> MaybeT m Project
findUserProject uid pid = do
kps <- lift $ findUserProjects uid
MaybeT . pure $ fmap snd (find (\(pid', _) -> pid' == pid) kps)
findPaymentRequest :: (MonadDB m) => PaymentKey -> m (Maybe (PaymentRequestId, PaymentRequest))
findPaymentRequest = liftdb . FindPaymentRequest
findPaymentRequest :: (MonadDB m) => PaymentKey -> MaybeT m (PaymentRequestId, PaymentRequest)
findPaymentRequest = MaybeT . liftdb . FindPaymentRequest
findPaymentRequestId :: (MonadDB m) => PaymentRequestId -> MaybeT m PaymentRequest
findPaymentRequestId = MaybeT . liftdb . FindPaymentRequestId
findPayment :: (MonadDB m) => PaymentRequestId -> m (Maybe Payment)
findPayment prid = (fmap snd . headMay) <$> liftdb (FindPayments prid)
findPayment :: (MonadDB m) => PaymentRequestId -> MaybeT m Payment
findPayment prid = MaybeT $ (fmap snd . headMay) <$> liftdb (FindPayments prid)
maybeAuc <- liftdb findOp
_ <- traverse (\auc -> checkProjectAuth (auc ^. A.projectId) uid findOp) maybeAuc
pure maybeAuc
auc <- MaybeT $ liftdb findOp
_ <- lift $ checkProjectAuth (auc ^. A.projectId) uid findOp
pure auc
qdbProjectJSON :: (ProjectId, Project) -> Value
qdbProjectJSON (pid, project) = v1 $
obj [ "projectId" .= idValue _ProjectId pid
, "project" .= projectJSON project
qdbJSON :: Text -> (Lens' a UUID) -> (b -> Value) -> (a, b) -> Value
qdbJSON name l f (xid, x) = v1 $
obj [ (name <> "Id") .= idValue l xid
, name .= f x
p v o = badVersion "Billable" v o
parseRecurrence :: Object -> Parser B.Recurrence
parseRecurrence o =
let parseAnnually o' = const (pure B.Annually) <$> O.lookup "annually" o'
parseMonthly o' = fmap B.Monthly . parseJSON <$> O.lookup "monthly" o'
parseWeekly o' = fmap B.Weekly . parseJSON <$> O.lookup "weekly" o'
parseOneTime o' = const (pure B.OneTime) <$> O.lookup "one-time" o'
parseRecurrence' :: Value -> Parser B.Recurrence
parseRecurrence' (Object o) = parseRecurrence o
parseRecurrence' v = fail $ "Value " <> show v <> " is not a JSON object."
paymentRequestTotal :: P.PaymentRequest -> Satoshi
paymentRequestTotal _ = error "Not yet implemented"
data BillingConfig = BillingConfig
{ _network :: BT.Network
, _signingKey :: RSA.PrivateKey
, _pkiData :: BT.PKIData
data PaymentsConfig = PaymentsConfig
{ _network :: !BT.Network
, _signingKey :: !RSA.PrivateKey
, _pkiData :: !BT.PKIData
, payloadGen :: Subscription' UserId Billable -> T.Day -> C.UTCTime -> m (Maybe ByteString)
, payloadGen :: Subscription' UserId Billable -- ^ subscription being billed
-> T.Day -- ^ billing date
-> C.UTCTime -- ^ payment request generation time
-> m (Maybe ByteString)
= Paid Payment -- ^ the request was paid with the specified payment
| Unpaid PaymentRequest -- ^ the request has not been paid, but has not yet expired
| Expired PaymentRequest -- ^ the request was not paid prior to the expiration date
= Paid !Payment -- ^ the request was paid with the specified payment
| Unpaid !PaymentRequest -- ^ the request has not been paid, but has not yet expired
| Expired !PaymentRequest -- ^ the request was not paid prior to the expiration date
createOutputs _ (TL.CreditToUser uid) amt = do
addrMay <- (>>= view userAddress) <$> findUser uid
let createOutput addr = BT.Output amt (PayPKHash (addr ^. _BtcAddr))
pure . maybeToList $ createOutput <$> addrMay
createOutputs _ (TL.CreditToUser uid) amt = (fmap maybeToList) . runMaybeT $ do
user <- findUser uid
addr <- MaybeT . pure $ user ^. userAddress
pure $ BT.Output amt (PayPKHash (addr ^. _BtcAddr))
{ _creditTo :: CreditTo
, _event :: LogEvent
, _eventMeta :: Maybe A.Value
{ _creditTo :: !CreditTo
, _event :: !LogEvent
, _eventMeta :: !(Maybe A.Value)
data EventAmendment = TimeChange ModTime C.UTCTime
| CreditToChange ModTime CreditTo
| MetadataChange ModTime A.Value
data EventAmendment = TimeChange !ModTime !C.UTCTime
| CreditToChange !ModTime !CreditTo
| MetadataChange !ModTime !A.Value
fromMaybeT :: (Monad m) => m a -> MaybeT m a -> m a
fromMaybeT a m = maybeT a pure m
Description: Add missing identifiers to event metadata tables
Created: 2017-06-08 04:38:05.341636 UTC
Depends: 2016-10-14_02-49-36_event-amendments
Apply: |
alter table event_metadata_amendments
add column id uuid primary key default uuid_generate_v4();
alter table event_credit_to_amendments
add column id uuid primary key default uuid_generate_v4();
alter table event_time_amendments
add column id uuid primary key default uuid_generate_v4();
Description: (Describe migration here.)
Created: 2017-09-24 22:06:53.509947 UTC
Depends: 2016-12-31_03-45-17_create-payments
Apply: |
alter table billables add column payment_request_email_template text null;
alter table billables add column payment_request_memo_template text null;
Revert: |
alter table billables drop column payment_request_email_template;
alter table billables drop column payment_request_memo_template;
import Data.X509
import Data.X509.File (readKeyFile, readSignedObject)
import qualified Network.Bippy.Types as BT
import qualified Network.Mail.SMTP as SMTP
import qualified Network.Socket as NS
import System.Environment
import System.IO (FilePath)
import System.Environment (getEnvironment)
import Filesystem.Path.CurrentOS (FilePath, fromText, encodeString)
, templatePath :: System.IO.FilePath
, staticAssetPath :: System.IO.FilePath
}
data SmtpConfig = SmtpConfig
{ smtpHost :: NS.HostName
, smtpPort :: Maybe NS.PortNumber
, smtpUser :: SMTP.UserName
, smtpPass :: SMTP.Password
, templatePath :: FilePath
, staticAssetPath :: FilePath
data BillingConfig = BillingConfig
{ network :: BT.Network
, signingKeyFile :: System.IO.FilePath
, certsFile :: System.IO.FilePath
, exchangeRateServiceURI :: String
}
loadQConfig :: System.IO.FilePath -> IO QConfig
loadQConfig :: FilePath -> IO QConfig
cfg <- C.load [C.Required cfgFile]
let dbEnvCfg = pgsDefaultConfig . C.pack <$> lookup "DATABASE_URL" env
cfg <- C.load [C.Required $ encodeString cfgFile]
let dbEnvCfg = pgsDefaultConfig . C8.pack <$> lookup "DATABASE_URL" env
<*> C.lookupDefault "/opt/aftok/server/templates/" cfg "templatePath"
<*> C.lookupDefault "/opt/aftok/server/static/" cfg "staticAssetPath"
<*> (fromText <$> C.lookupDefault "/opt/aftok/server/templates/" cfg "templatePath")
<*> (fromText <$> C.lookupDefault "/opt/aftok/server/static/" cfg "staticAssetPath")
readSmtpConfig :: CT.Config -> IO SmtpConfig
readSmtpConfig cfg =
SmtpConfig <$> C.require cfg "smtpHost"
<*> ((fmap . fmap) fromInteger $ C.lookup cfg "smtpPort")
<*> C.require cfg "smtpUser"
<*> C.require cfg "smtpKey"
readBillingConfig :: CT.Config -> IO BillingConfig
readBillingConfig cfg =
BillingConfig <$> (parseNetwork <$> C.require cfg "network")
<*> C.require cfg "signingKeyFile"
<*> C.require cfg "certsFile"
<*> C.require cfg "exchangeRateServiceURI"
where parseNetwork :: String -> BT.Network
parseNetwork "main" = BT.MainNet
parseNetwork _ = BT.TestNet
toBillingConfig :: BillingConfig -> IO AP.BillingConfig
toBillingConfig c = do
privKeys <- readKeyFile (signingKeyFile c)
pkiEntries <- readSignedObject (certsFile c)
privKey <- case headMay privKeys of
Just (PrivKeyRSA k) -> pure k
Just (PrivKeyDSA _) -> fail "DSA keys not supported for payment request signing."
Nothing -> fail $ "No keys found in private key file " <> signingKeyFile c
let pkiData = BT.X509SHA256 . CertificateChain $ pkiEntries
pure $ AP.BillingConfig (network c) privKey pkiData
maybeAuc <- snapEval $ findAuction aid uid -- this will verify auction access
maybe (snapError 404 $ "Auction not found for id " <> tshow aid) pure maybeAuc
fromMaybeT
(snapError 404 $ "Auction not found for id " <> tshow aid)
(mapMaybeT snapEval $ findAuction aid uid) -- this will verify auction access
currentUser <- UserName . AU.userLogin <$> requireLogin
qdbUser <- snapEval $ findUserByName currentUser
case qdbUser of
Nothing -> snapError 403 "Unable to retrieve user record for authenticated user"
Just u -> pure (u ^. _1)
currentUser <- UserName . AU.userLogin <$> requireUser
maybeT
(snapError 403 "Unable to retrieve user record for authenticated user")
(pure . (^. _1))
(mapMaybeT snapEval $ findUserByName currentUser)
{-# LANGUAGE TemplateHaskell #-}
module Aftok.Snaplet.Billing
( billableCreateHandler
, billableListHandler
, subscribeHandler
) where
import ClassyPrelude
import Control.Lens ((^.))
import Data.Aeson
import Data.Aeson.Types
import Data.Thyme.Clock as C
import Data.Thyme.Time.Core (toThyme)
import Snap.Snaplet as S
import Aftok (UserId)
import Aftok.Billables
import Aftok.Json
import Aftok.Types
import Aftok.Project
import Aftok.Database (createBillable, withProjectAuth, liftdb, DBOp(..))
import Aftok.Snaplet
import Aftok.Snaplet.Auth
parseCreateBillable :: UserId -> ProjectId -> Value -> Parser Billable
parseCreateBillable uid pid = unversion "Billable" p where
p (Version 1 0) o =
Billable <$> pure pid
<*> pure uid
<*> o .: "name"
<*> o .: "description"
<*> (parseRecurrence' =<< o .: "recurrence")
<*> (Satoshi <$> o .: "amount")
<*> o .: "gracePeriod"
<*> (fmap toThyme <$> o .: "requestExpiryPeriod")
<*> o .:? "paymentRequestEmailTemplate"
<*> o .:? "paymentRequestMemoTemplate"
p v o = badVersion "Billable" v o
billableCreateHandler :: S.Handler App App BillableId
billableCreateHandler = do
uid <- requireUserId
pid <- requireProjectId
requestBody <- readRequestJSON 4096
b <- either (snapError 400 . tshow) pure $ parseEither (parseCreateBillable uid pid) requestBody
snapEval $ createBillable uid b
billableListHandler :: S.Handler App App [(BillableId, Billable)]
billableListHandler = do
uid <- requireUserId
pid <- requireProjectId
snapEval $ withProjectAuth pid uid (FindBillables pid)
subscribeHandler :: S.Handler App App SubscriptionId
subscribeHandler = do
uid <- requireUserId
bid <- requireId "billableId" BillableId
t <- liftIO C.getCurrentTime
snapEval . liftdb $ CreateSubscription uid bid (t ^. C._utctDay)
prMay <- snapEval $ findPaymentRequest pkey
maybe (snapError 404 $ "Outstanding payment request not found for key " <> (view _PaymentKey pkey))
pure prMay
fromMaybeT
(snapError 404 $ "Outstanding payment request not found for key " <> (view _PaymentKey pkey))
(mapMaybeT snapEval $ findPaymentRequest pkey)
mp <- snapEval $ findProject pid uid
maybe (snapError 404 $ "Project not found for id " <> tshow pid) pure mp
fromMaybeT
(snapError 404 $ "Project not found for id " <> tshow pid)
(mapMaybeT snapEval $ findUserProject uid pid)
let SmtpConfig{..} = smtpConfig cfg
mailer = maybe (sendMailWithLogin smtpHost) (sendMailWithLogin' smtpHost) smtpPort
let SmtpConfig{..} = QC.smtpConfig cfg
mailer = maybe (sendMailWithLogin _smtpHost) (sendMailWithLogin' _smtpHost) _smtpPort
projectMay <- snapEval $ findProject pid uid
project <- maybe (snapError 400 $ "Project not found for id " <> tshow pid) pure projectMay
project <- fromMaybeT
(snapError 400 $ "Project not found for id " <> tshow pid)
(mapMaybeT snapEval $ findUserProject uid pid)
let loginRoute = method GET requireLogin >> redirect "/home"
xhrLoginRoute = void $ method POST requireLogin
registerRoute = void $ method POST registerHandler
acceptInviteRoute = void $ method POST acceptInvitationHandler
let loginRoute = method GET requireLogin >> redirect "/home"
xhrLoginRoute = void $ method POST requireLogin
registerRoute = void $ method POST registerHandler
inviteRoute = void $ method POST (projectInviteHandler cfg)
acceptInviteRoute = void $ method POST acceptInvitationHandler
inviteRoute = void . method POST $ projectInviteHandler cfg
logWorkRoute f = serveJSON eventIdJSON $ method POST (logWorkHandler f)
logWorkBTCRoute f = serveJSON eventIdJSON $ method POST (logWorkBTCHandler f)
amendEventRoute = serveJSON amendmentIdJSON $ method PUT amendEventHandler
auctionRoute = serveJSON auctionJSON $ method GET auctionGetHandler
auctionBidRoute = serveJSON bidIdJSON $ method POST auctionBidHandler
auctionRoute = serveJSON auctionJSON $ method GET auctionGetHandler
auctionBidRoute = serveJSON bidIdJSON $ method POST auctionBidHandler
payableRequestsRoute = serveJSON billDetailsJSON $ method GET listPayableRequestsHandler
paymentRoute = (writeLBS . runPutLazy . encodeMessage =<< method GET getPaymentRequestHandler)
<|> (void . method POST . paymentResponseHandler $ billingConfig cfg)
billableCreateRoute = serveJSON billableIdJSON $ method POST billableCreateHandler
billableListRoute = serveJSON (fmap qdbBillableJSON) $ method GET billableListHandler
subscribeRoute = serveJSON subscriptionIdJSON $ method POST subscribeHandler
amendEventRoute = serveJSON amendmentIdJSON $ method PUT amendEventHandler
payableRequestsRoute = serveJSON billDetailsJSON $ method GET listPayableRequestsHandler
getPaymentRequestRoute = writeLBS . runPutLazy . encodeMessage =<< method GET getPaymentRequestHandler
submitPaymentRoute = serveJSON paymentIdJSON $ method POST (paymentResponseHandler $ billingConfig cfg)