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-storeExecutable aftok-daemondefault-language: Haskell2010ghc-options: -Wall -Werrorhs-source-dirs: daemondefault-extensions: NoImplicitPrelude, OverloadedStrings, RecordWildCards, ScopedTypeVariables, KindSignaturesmain-is: Main.hsother-modules: AftokD, AftokD.AftokMbuild-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 whereimport ClassyPreludeimport 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 Cimport Data.Thyme.Time as Timport Network.Mail.Mimeimport Network.Mail.SMTP as SMTPimport Network.URI (URI, parseURI)import Text.StringTemplateimport Filesystem.Path.CurrentOS (encodeString)import Network.Bippy.Types (Satoshi)import Aftok (User, UserId, userEmail, _Email)import Aftok.Types (satoshi)import qualified Aftok.Config as ACimport Aftok.Billables (Billable, Billable', Subscription', customer, name, billable, project, paymentRequestEmailTemplate, paymentRequestMemoTemplate)import qualified Aftok.Database as DBimport Aftok.Database.PostgreSQL (QDBM(..))import qualified Aftok.Payments as Pimport Aftok.Payments.Types (PaymentKey(..), subscription, paymentRequestTotal, paymentKey)import Aftok.Project (Project, ProjectId(..), projectName)import qualified AftokD as Ddata AftokDErr= ConfigError Text| DBErr DB.DBError| PaymentErr P.PaymentErrormakeClassyPrisms ''AftokDErrinstance P.AsPaymentError AftokDErr where_PaymentError = _PaymentErr . P._PaymentError_Overdue = _PaymentErr . P._Overdue_SigningError = _PaymentErr . P._SigningErrordata AftokMEnv = AftokMEnv{ _dcfg :: !D.Config, _conn :: !Connection, _pcfg :: !P.PaymentsConfig}makeLenses ''AftokMEnvinstance P.HasPaymentsConfig AftokMEnv wherenetwork = pcfg . P.networksigningKey = pcfg . P.signingKeypkiData = pcfg . P.pkiDatapaymentsConfig = pcfgnewtype AftokM a = AftokM { runAftokM :: ReaderT AftokMEnv (ExceptT AftokDErr IO) a }deriving (Functor, Applicative, Monad, MonadIO, MonadError AftokDErr, MonadReader AftokMEnv)instance MonadRandom AftokM wheregetRandomBytes = liftIO . getRandomBytesinstance DB.MonadDB AftokM whereliftdb = liftQDBM . DB.liftdbliftQDBM :: QDBM a -> AftokM aliftQDBM (QDBM r) =AftokM . mapReaderT (withExceptT DBErr) . withReaderT _conn $ rcreateAllPaymentRequests :: D.Config -> IO ()createAllPaymentRequests cfg = doconn' <- connect $ cfg ^. D.dbConfigpcfg' <- AC.toPaymentsConfig $ cfg ^. D.billingConfiglet env = AftokMEnv cfg conn' pcfg'void . runExceptT $ (runReaderT . runAftokM) createProjectsPaymentRequests $ envcreateProjectsPaymentRequests :: AftokM ()createProjectsPaymentRequests = doprojects <- liftQDBM $ DB.listProjectstraverse_ createProjectPaymentRequests projectscreateProjectPaymentRequests :: ProjectId -> AftokM ()createProjectPaymentRequests pid = donow <- liftIO C.getCurrentTimelet ops = P.BillingOps memoGen (fmap Just . paymentURL) payloadGensubscribers <- liftQDBM $ DB.findSubscribers pidrequests <- traverse (\uid -> P.createPaymentRequests ops now uid pid) $ subscriberstraverse_ sendPaymentRequestEmail (join requests)sendPaymentRequestEmail :: P.PaymentRequestId -> AftokM ()sendPaymentRequestEmail reqId = docfg <- asklet AC.SmtpConfig{..} = cfg ^. (dcfg . D.smtpConfig)preqCfg = cfg ^. (dcfg . D.paymentRequestConfig)reqMay = dopreq <- DB.findPaymentRequestId reqIdpreq' <- traverseOf P.subscription DB.findSubscriptionBillable preqpreq'' <- traverseOf (P.subscription . customer) DB.findUser preq'traverseOf (P.subscription . billable . project) DB.findProject preq''req <- maybeT (throwError $ DBErr DB.SubjectNotFound) pure reqMaybip70URL <- paymentURL (req ^. paymentKey)mail <- buildPaymentRequestEmail preqCfg req bip70URLlet mailer = maybe (sendMailWithLogin _smtpHost) (sendMailWithLogin' _smtpHost) _smtpPortliftIO $ mailer _smtpUser _smtpPass mailbuildPaymentRequestEmail :: (MonadIO m, MonadError AftokDErr m)=> D.PaymentRequestConfig-> P.PaymentRequest' (Subscription' User (Billable' Project UserId Satoshi))-> URI-> m MailbuildPaymentRequestEmail cfg req paymentUrl = dotemplates <- liftIO . directoryGroup $ encodeString (cfg ^. D.templatePath)let billTemplate = (newSTMP . unpack) <$> req ^. (subscription . billable . paymentRequestEmailTemplate)defaultTemplate = getStringTemplate "payment_request" templatescase billTemplate <|> defaultTemplate ofNothing -> throwError $ ConfigError "Could not find template for invitation email"Just template ->let fromEmail = cfg ^. D.billingFromEmailtoEmail = 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 templatein pure $ SMTP.simpleMail fromAddr [toAddr] [] [] subject [body]memoGen :: Subscription' UserId Billable-> T.Day-> C.UTCTime-> AftokM (Maybe Text)memoGen sub billingDate requestTime = doreq <- traverseOf (billable . project) DB.findProjectOrError sublet 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 URIpaymentURL (PaymentKey k) = doenv <- asklet hostname = env ^. (dcfg . D.paymentRequestConfig . D.aftokHost)paymentRequestPath = "https://" <> hostname <> "/pay/" <> kmaybe(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 whereimport ClassyPrelude hiding (FilePath)import Control.Lensimport qualified Data.Configurator as Cimport qualified Data.Configurator.Types as CTimport Database.PostgreSQL.Simple (ConnectInfo)import Filesystem.Path.CurrentOS (FilePath, fromText, encodeString)import Aftok (Email(..))import qualified Aftok.Config as ACdata PaymentRequestConfig = PaymentRequestConfig{ _aftokHost :: Text, _templatePath :: FilePath, _billingFromEmail :: Email}makeLenses ''PaymentRequestConfigdata Config = Config{ _smtpConfig :: AC.SmtpConfig, _billingConfig :: AC.BillingConfig, _dbConfig :: ConnectInfo, _paymentRequestConfig :: PaymentRequestConfig}makeLenses ''ConfigloadConfig :: FilePath -> IO ConfigloadConfig cfgFile =readConfig =<< C.load [C.Required $ encodeString cfgFile]readConfig :: CT.Config -> IO ConfigreadConfig 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 PaymentRequestConfigreadPaymentRequestConfig 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) whereimport ClassyPreludeimport System.Environment (getEnv)import Filesystem.Path.CurrentOS (decodeString)import qualified AftokD as Dimport AftokD.AftokM (createAllPaymentRequests)main :: IO ()main = docfgPath <- try $ getEnv "AFTOK_CFG" :: IO (Either IOError String)cfg <- D.loadConfig . decodeString $ either (const "conf/aftok.cfg") id cfgPathcreateAllPaymentRequests cfg
{-# LANGUAGE TemplateHaskell #-}module Aftok.Config whereimport ClassyPrelude hiding (FilePath)import Control.Lens (makeClassy, (^.))import qualified Data.Configurator as Cimport qualified Data.Configurator.Types as CTimport Data.X509import Data.X509.File (readKeyFile, readSignedObject)import Database.PostgreSQL.Simple (ConnectInfo(..))import Filesystem.Path.CurrentOS (FilePath, fromText, encodeString)import qualified Network.Bippy.Types as BTimport qualified Network.Mail.SMTP as SMTPimport qualified Network.Socket as NSimport Aftok.Payments (PaymentsConfig(..))data SmtpConfig = SmtpConfig{ _smtpHost :: NS.HostName, _smtpPort :: Maybe NS.PortNumber, _smtpUser :: SMTP.UserName, _smtpPass :: SMTP.Password}makeClassy ''SmtpConfigdata BillingConfig = BillingConfig{ _network :: BT.Network, _signingKeyFile :: FilePath, _certsFile :: FilePath, _exchangeRateServiceURI :: String}makeClassy ''BillingConfigreadSmtpConfig :: CT.Config -> IO SmtpConfigreadSmtpConfig 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 BillingConfigreadBillingConfig 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.NetworkparseNetwork "main" = BT.MainNetparseNetwork _ = BT.TestNetreadConnectInfo :: CT.Config -> IO ConnectInforeadConnectInfo 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 PaymentsConfigtoPaymentsConfig c = doprivKeys <- readKeyFile . encodeString $ c ^. signingKeyFilepkiEntries <- readSignedObject . encodeString $ c ^. certsFileprivKey <- case headMay privKeys ofJust (PrivKeyRSA k) -> pure kJust (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 $ pkiEntriespure $ 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_eventsWHERE 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_metadataFROM work_eventsWHERE 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_metadataFROM work_eventsWHERE 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_metadataFROM work_eventsWHERE 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_metadataFROM work_eventsWHERE 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_timeFROM auctionsWHERE 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_timeFROM 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 iWHERE 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_idFROM subscripions sJOIN billables b ON s.billable_id = b.idWHERE 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_fnFROM projects p LEFT OUTER JOIN project_companions pc ON pc.project_id = p.idWHERE 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_templateFROM billables b JOIN aftok_events e ON e.id = b.event_idWHERE 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_daysb.payment_request_email_template, b.payment_request_memo_templateFROM billables b JOIN aftok_events e ON e.id = b.event_idWHERE 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_dateFROM subscriptions sWHERE 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_dateFROM subscriptions sJOIN billables b ON b.id = s.billable_idWHERE 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_dateFROM payment_requestsWHERE 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_dateFROM payment_requestsWHERE 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_templateFROM payment_requests rJOIN subscriptions s on s.id = r.subscription_idJOIN billables b on b.id = s.billable_idJOIN aftok_events e on e.id = b.event_idWHERE 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_dateFROM paymentsWHERE 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 = dokps <- findUserProjects uidpure $ fmap snd (find (\(pid', _) -> pid' == pid) kps)
findProject :: (MonadDB m) => ProjectId -> MaybeT m ProjectfindProject = MaybeT . liftdb . FindProjectfindProjectOrError :: (MonadDB m) => ProjectId -> m ProjectfindProjectOrError pid = fromMaybeT(raiseSubjectNotFound $ FindProject pid)(findProject pid)findUserProject :: (MonadDB m) => UserId -> ProjectId -> MaybeT m ProjectfindUserProject uid pid = dokps <- lift $ findUserProjects uidMaybeT . 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 . FindPaymentRequestfindPaymentRequestId :: (MonadDB m) => PaymentRequestId -> MaybeT m PaymentRequestfindPaymentRequestId = MaybeT . liftdb . FindPaymentRequestId
findPayment :: (MonadDB m) => PaymentRequestId -> m (Maybe Payment)findPayment prid = (fmap snd . headMay) <$> liftdb (FindPayments prid)
findPayment :: (MonadDB m) => PaymentRequestId -> MaybeT m PaymentfindPayment prid = MaybeT $ (fmap snd . headMay) <$> liftdb (FindPayments prid)
maybeAuc <- liftdb findOp_ <- traverse (\auc -> checkProjectAuth (auc ^. A.projectId) uid findOp) maybeAucpure maybeAuc
auc <- MaybeT $ liftdb findOp_ <- lift $ checkProjectAuth (auc ^. A.projectId) uid findOppure auc
qdbProjectJSON :: (ProjectId, Project) -> ValueqdbProjectJSON (pid, project) = v1 $obj [ "projectId" .= idValue _ProjectId pid, "project" .= projectJSON project
qdbJSON :: Text -> (Lens' a UUID) -> (b -> Value) -> (a, b) -> ValueqdbJSON 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.RecurrenceparseRecurrence 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.RecurrenceparseRecurrence' (Object o) = parseRecurrence oparseRecurrence' v = fail $ "Value " <> show v <> " is not a JSON object."
paymentRequestTotal :: P.PaymentRequest -> SatoshipaymentRequestTotal _ = 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 = doaddrMay <- (>>= view userAddress) <$> findUser uidlet createOutput addr = BT.Output amt (PayPKHash (addr ^. _BtcAddr))pure . maybeToList $ createOutput <$> addrMay
createOutputs _ (TL.CreditToUser uid) amt = (fmap maybeToList) . runMaybeT $ douser <- findUser uidaddr <- MaybeT . pure $ user ^. userAddresspure $ 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 afromMaybeT a m = maybeT a pure m
Description: Add missing identifiers to event metadata tablesCreated: 2017-06-08 04:38:05.341636 UTCDepends: 2016-10-14_02-49-36_event-amendmentsApply: |alter table event_metadata_amendmentsadd column id uuid primary key default uuid_generate_v4();alter table event_credit_to_amendmentsadd column id uuid primary key default uuid_generate_v4();alter table event_time_amendmentsadd column id uuid primary key default uuid_generate_v4();
Description: (Describe migration here.)Created: 2017-09-24 22:06:53.509947 UTCDepends: 2016-12-31_03-45-17_create-paymentsApply: |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.X509import Data.X509.File (readKeyFile, readSignedObject)import qualified Network.Bippy.Types as BTimport qualified Network.Mail.SMTP as SMTPimport qualified Network.Socket as NSimport System.Environmentimport 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 SmtpConfigreadSmtpConfig 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 BillingConfigreadBillingConfig cfg =BillingConfig <$> (parseNetwork <$> C.require cfg "network")<*> C.require cfg "signingKeyFile"<*> C.require cfg "certsFile"<*> C.require cfg "exchangeRateServiceURI"where parseNetwork :: String -> BT.NetworkparseNetwork "main" = BT.MainNetparseNetwork _ = BT.TestNet
toBillingConfig :: BillingConfig -> IO AP.BillingConfigtoBillingConfig c = doprivKeys <- readKeyFile (signingKeyFile c)pkiEntries <- readSignedObject (certsFile c)privKey <- case headMay privKeys ofJust (PrivKeyRSA k) -> pure kJust (PrivKeyDSA _) -> fail "DSA keys not supported for payment request signing."Nothing -> fail $ "No keys found in private key file " <> signingKeyFile clet pkiData = BT.X509SHA256 . CertificateChain $ pkiEntriespure $ AP.BillingConfig (network c) privKey pkiData
maybeAuc <- snapEval $ findAuction aid uid -- this will verify auction accessmaybe (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 <$> requireLoginqdbUser <- snapEval $ findUserByName currentUsercase qdbUser ofNothing -> snapError 403 "Unable to retrieve user record for authenticated user"Just u -> pure (u ^. _1)
currentUser <- UserName . AU.userLogin <$> requireUsermaybeT(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) whereimport ClassyPreludeimport Control.Lens ((^.))import Data.Aesonimport Data.Aeson.Typesimport Data.Thyme.Clock as Cimport Data.Thyme.Time.Core (toThyme)import Snap.Snaplet as Simport Aftok (UserId)import Aftok.Billablesimport Aftok.Jsonimport Aftok.Typesimport Aftok.Projectimport Aftok.Database (createBillable, withProjectAuth, liftdb, DBOp(..))import Aftok.Snapletimport Aftok.Snaplet.AuthparseCreateBillable :: UserId -> ProjectId -> Value -> Parser BillableparseCreateBillable uid pid = unversion "Billable" p wherep (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 obillableCreateHandler :: S.Handler App App BillableIdbillableCreateHandler = douid <- requireUserIdpid <- requireProjectIdrequestBody <- readRequestJSON 4096b <- either (snapError 400 . tshow) pure $ parseEither (parseCreateBillable uid pid) requestBodysnapEval $ createBillable uid bbillableListHandler :: S.Handler App App [(BillableId, Billable)]billableListHandler = douid <- requireUserIdpid <- requireProjectIdsnapEval $ withProjectAuth pid uid (FindBillables pid)subscribeHandler :: S.Handler App App SubscriptionIdsubscribeHandler = douid <- requireUserIdbid <- requireId "billableId" BillableIdt <- liftIO C.getCurrentTimesnapEval . liftdb $ CreateSubscription uid bid (t ^. C._utctDay)
prMay <- snapEval $ findPaymentRequest pkeymaybe (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 uidmaybe (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 cfgmailer = maybe (sendMailWithLogin smtpHost) (sendMailWithLogin' smtpHost) smtpPort
let SmtpConfig{..} = QC.smtpConfig cfgmailer = maybe (sendMailWithLogin _smtpHost) (sendMailWithLogin' _smtpHost) _smtpPort
projectMay <- snapEval $ findProject pid uidproject <- 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 requireLoginregisterRoute = void $ method POST registerHandleracceptInviteRoute = void $ method POST acceptInvitationHandler
let loginRoute = method GET requireLogin >> redirect "/home"xhrLoginRoute = void $ method POST requireLoginregisterRoute = void $ method POST registerHandlerinviteRoute = 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 auctionGetHandlerauctionBidRoute = serveJSON bidIdJSON $ method POST auctionBidHandler
auctionRoute = serveJSON auctionJSON $ method GET auctionGetHandlerauctionBidRoute = serveJSON bidIdJSON $ method POST auctionBidHandler
payableRequestsRoute = serveJSON billDetailsJSON $ method GET listPayableRequestsHandlerpaymentRoute = (writeLBS . runPutLazy . encodeMessage =<< method GET getPaymentRequestHandler)<|> (void . method POST . paymentResponseHandler $ billingConfig cfg)
billableCreateRoute = serveJSON billableIdJSON $ method POST billableCreateHandlerbillableListRoute = serveJSON (fmap qdbBillableJSON) $ method GET billableListHandlersubscribeRoute = serveJSON subscriptionIdJSON $ method POST subscribeHandler
amendEventRoute = serveJSON amendmentIdJSON $ method PUT amendEventHandler
payableRequestsRoute = serveJSON billDetailsJSON $ method GET listPayableRequestsHandlergetPaymentRequestRoute = writeLBS . runPutLazy . encodeMessage =<< method GET getPaymentRequestHandlersubmitPaymentRoute = serveJSON paymentIdJSON $ method POST (paymentResponseHandler $ billingConfig cfg)