X3ES7NUA42D2BF7CQDDKXM5CLMVCYA3H5YU5KXLPTGDBFPE2LNVAC
SQ7UMLN5WCPHIF66RO4UQVX6RSNRRZBOVZP7HEMSKP7VO6YNQPRAC
F4ONFXF4MSA3QM64T7ATRVO3NQR2MC3RVZGVNGSQXCKXXQX2UG7QC
AXKKXBWN4EMUOLV43WN52JSKJPBV7TLSGLNJW5EZXHSJNKCYUWOQC
IPG33FAWXGEQ2PO6OXRT2PWWXHRNMPVUKKADL6UKKN5GD2CNZ25AC
B6HWAPDPXIWH7CHK5VLMWLL6EQN6NOFZEFYO47BPUY2ZO4SL7VDAC
4R7XIYK3BP664CO3YJ2VM64ES2JYN27UTQG5KS34OTEPAIODSZLQC
EFSXYZPOGA5M4DN65IEIDO7JK6U34DMQELAPHOIL2UAT6HRC66BAC
UWMGUJOW5X5HQTS76T2FD7MNAJF7SESPQVU5FDIZO52V75TT2X6AC
LTSVBVA235BQAIU3SQURKSRHIAL33K47G4J6TSEP2K353OCHNJEAC
ENNZIQJG4XJ62QCNRMLNAXN7ICTPCHQFZTURX6QSUYYWNADFJHXQC
NEDDHXUK3GNFMOFO3KLU7NRIHCTYNWBT3D6HTKZAOXFDE6HMDZ6AC
LAROLAYUGJ4Q5AEFV5EJMIA2ZKBNCBWHHHPCJ3CKCNIUIYUKRFVQC
HBULCDN6E75FAPILFVLTQIKABDEWL3HZTBLICLCWOIKDRYM6UIBQC
NAS4BFL43SIUAAC663R6VFBHQ2DKI45K6Y6ZKVQI7S5547HBAN7QC
IZEVQF627FA7VV25KJAWYWGGC35LZUUBBQRPN5ZAINTQLPEDRTEAC
RPAJLHMTUJU4AYNBOHVGHGGB4NY2NLY3BVPYN5FMWB3ZIMAUQHCQC
5ZSKPQ3KY6T6O5S6T6HW4OHJMQXA72WKJSJJMGKGX2WMFTNZ7EGAC
TLQ72DSJD7GGPWN6HGBHAVPBRQFKEQ6KSK43U7JWWID4ZWAF47JAC
75N3UJ4JK56KXF56GASGPAWLFYGJDETVJNYTF4KXFCQM767JUU5AC
EKI57EJR65DA5FPILAHGHHAIU5ITVGHA6V3775OX7GV5XD67OWRQC
2XQD6KKKD6QVHFHAEMVE3XXY7X2T7BLTLL7XIILZAXNJJH2YONUQC
GCVQD44VRPQVKPZEPIC4AOIXLJIG2ZMV3QI2Y7KALUT6NVUBSGSAC
ZP62WC472OTQETO2HTHIQIPO57XZIWVKPA4KL62GYU4OZDMB6NSAC
KEP5WUFJXTMKRRNZLYTGYYWA4VLFCMHTKTJYF5EA5IWBYFMU6WYQC
WO2MINIF4TXOHWSE7JWXRZYN64XRVLYIRFMF4SMPSOXKA2V77KMQC
LHJ2HFXVUQ4VG25I7DADWU73G5K5WNZBDQ3SVNKFYLZ5BEYM4XCQC
7HPY3QPFPN35PSPUBVNW2GTFB3CBQZBST4J2BAVJ7QMXLIUN52JAC
DFOBMSAODB3NKW37B272ZXA2ML5HMIH3N3C4GT2DPEQS7ZFK4SNAC
JFOEOFGA4CQR2LW43IVQGDZSPVJAD4KDN2DZMZXGM2QDIUD7AVCAC
HMDM3B557TO5RYP2IGFFC2C2VN6HYZTDQ47CJY2O37BW55DSMFZAC
AL37SVTCKRSG4HG2PCYK5Z7QSIZZH5JHH4Q2VLMXFAXSAQRFFG4QC
SEWTRB6S5PO5MQBLCPVBD7XT2BDYNZUE2RO6Z2XENZRIOCN6OZJAC
Q5X5RYQLP5K7REYD6VLHOKC4W36ZELJYA45V6YFKTD5S6MPN3NDQC
73NDXDEZRMK672GHSTC3CI6YHXFZ2GGJI5IKQGHKFDZKTNSQXLLQC
Y3LIJ5USPMYBG7HKCIQBE7MWVGLQJPJSQD3KPZCTKXP22GOB535QC
5IDB3IWSB6LFW4U772Y7BH5Y3FQOQ7IFWLVXDZE5XS6SKJITFV4QC
AWWC6P5ZVFDQHX3EAYDG4DKTUZ6A5LHQAV3NIUO3VP6FM7JKPK5AC
HALRDT2F22DAQ54M334YSKD2XJCAXDXEZPDKGMOWKH7XELBAS6MAC
U256ZALIPTVWLNACYPIMWLNEYDQWP7CHF4Y4CGMILQTONJHMGQVQC
A6HKMINBNGQLLX4QJMYWKQ4JAEHVJ4HIRVDKPPDI3FJUO2AAB7OQC
4U7F3CPIDTK6JSEDMNMHVKSR7HOQDLZQD2PPVMDLHO5SFSIMUXZAC
J6S23MDGHVSCVVIRB6XRNSY3EGTDNWFJHV7RYLIEHBUK5KU63CFQC
RN7EI6INGUUHGMNY5RU3NH56WPLRZY5ZMYDNFMNE3TGV4ESFLQIAC
BSIUHCGFDFDFGWYMHZB7OVU3Z3IHPEUXRISIOPGZI2RUXZFDS2EQC
O227CEAV7BTKSE3SSC7XHC5IWEBXZL2AOOKJMBMOOFNTLINBLQMAC
EW2XN7KUMCAQNVFJJ5YTAVDZCPHNWDOEDMRFBUGLY6IE2HKNNX5AC
WAIX6AGNDVJOKTWZ7OP7QOYSJHAJSX5EOWXZHOAO2IG6ALWUCJ6QC
ZTPDQKLAB6JJGUFYNBE2OYDW7LV64FNI6BXBO3TBWOM4YF5UWI5QC
3QVT6MA6I2CILQH3LUZABS4YQ7MN6CNRYTDRVS376OOHTPLYTFJAC
QMRKFEPGFBCEWAIXPEIG5ILKAJ2JH5L3TOITHR4HNJXK5FN3KXBQC
2J37EVJMX255K3XEJHTZGRPEIRMAQ62JQWOA7JU3YTZUB6PUPWVQC
RSF6UAJKG7CEKILSVXI6C4YZXY7PIYZM2EMA2IXKQ7SADKNVSH7QC
7KZP4RHZ3QSYTPPQ257A65Z5UPX44TF2LAI2U5EMULQCLDCEUK2AC
EKY7U7SKPF45OOUAHJBEQKXSUXWOHFBQFFVJWPBN5ARFJUFM2BPAC
TNR3TEHKVADAEZSTOD2XLSUTSW5AWST2YUW4CWK5KE7DSC6XHZNAC
O5FVTOM6YFBLEPF3S576K6IMT6ZZ5VQCSB3YVXNS4CKBITKCRZ7AC
2G3GNDDUOVPF45PELJ65ZB2IXEHJJXJILFRVHZXGPXUL4BVNZJFQC
LD4GLVSF6YTA7OZWIGJ45H6TUXGM4WKUIYXKWQFNUP36WDMYSMXAC
ZIG57EE6RB22JGB3CT33EN2HVYCHCXBT5GROBTBMBLEMDTGQOOBQC
4FDQGIXN3Z4J55DILCSI5EOLIIA7R5CADTGFMW5X7N7MH6JIMBWAC
QADKFHAR3KWQCNYU25Z7PJUGMD5WL26IU3DOAHBTRN2A7NKPUPKAC
WZFQDWW4XK6M4A4PQ7WQJUTZUPRGQR7V7ZVZY5ZTL5AMGIFMHB2QC
NLZ3JXLOOIL37O3RRQWXHNPNSNEOOLPD6MCB754BEBECQB3KGR2AC
64C6AWH66FDKU6UE6Z6JPX2J2GBM2JOPTH2GL6LHKAIUBGNGDZ5AC
EMVTF2IWNQGRL44FC4JNG5FYYQTZSFPNM6SOM7IAEH6T7PPK2NVAC
FXJQACESPGTLPG5ELXBU3M3OQXUZQQIR7HPIEHQ3FNUTMWVH4WBAC
5OI44E4EEVYOMHDWNK2WA7K4L4JWRWCUJUNN2UAUGE5VY4W7GTNAC
ASF3UPJLCX7KIUCNJD5KAXSPDXCUEJHLL4HBRTRUBPCW73IXOCWQC
EQXRXRZDYCM7BDAVBOXQYPG6C7IJT3OFGNIXCDAHJJBRKAXNGL7AC
SCXG6TJWYIPRUMT27KGKIIF6FYKTUTY74UNZ2FQTT63XZ6HIF3AAC
Z7KS5XHHC6PAMTVBHXY7KUSS3BWAOU6FSYIITUCFOOJZU4OUJHBAC
W35DDBFYF6Z4ZPCFEO5RPAONZLYCSNTXUSTS6FIUUVZHCI6Q7GHAC
XTBSG4C7SCZUFOU2BTNFR6B6TCGYI35BWUV4PVTS3N7KNH5VEARQC
POX3UAMTGCNS3SU5I6IKDNCCSHEAUSFBZ3WCMQ3EXKPNXETOI6BAC
Z3MK2PJ5U222DXRS22WCDHVPZ7HVAR3HOCUNXIGX6VMEPBQDF6PQC
7VGYLTMURLVSVUYFW7TCRZTDQ6RE2EPSPPA43XKHDOBFWYVVSJHQC
UOG5H2TW5R3FSHQPJCEMNFDQZS5APZUP7OM54FIBQG7ZP4HASQ7QC
GKLIPHR5YOBKEMC4744J3WYYFLPFXMZEOLC6Z26QXAG4IM2HQVEQC
UILI6PILCRDPZ3XYA54LGIGPSU7ERWNHCE7R3CE64ZEC7ONOEMOQC
BWN72T44GRRZ6K2OPN56FTLNEB7J7AGC7T2U5HSMLEKUPGJP2NUAC
O722AOKEWXWJPRHGJREU6QPW7HEFPPRETZIAADZ2RMAXHARCNEKAC
NVOCQVASZWTKQJG7GPH7KHKZZR7NUG4WLV5YY4KAIRPCJRWCZPIAC
FD7SV5I6VCW27HZ3T3K4MMGB2OYGJTPKFFA263TNTAMRJGQJWVNAC
P6NR2CGXCWAW6GXXSIXCGOBIRAS2BM4LEM6D5ADPN4IL7TMW6UVAC
Y35QCWYW2OTZ27ZVTH2BA3XJTUCJ2WMLKU32ZCOCDY3AW7TIZXRAC
2OIPAQCBDIUJBXB4K2QVP3IEBIUOCQHSWSWFVMVSVZC7GHX2VK7AC
KNSI575VAW6HRCZYXOEPQ4DTSML4EORML5MV4DJBRKE7TXCPS4EAC
5DRIWGLUKMQZU2ZPBXSTLAWJKAMOD5YXAHM5LEDQHDFGYYLHWCDQC
GKGVYBZGPJXO7N7GLHLRNYQPXFHBQSNQN53OKRFCXLQEYDTC5I4QC
4QX5E5ACVN57KJLCWOM4JEI6JSV4XZNCWVYPOTKSOMUW3SOMCNJAC
7XN3I3QJHYMKU2DCUXX34WQMSJ4ZJOWW7FME34EANO3E5W4Q632AC
7DBNV3GV773FH5ZLQWFX4RBOS4Q3CIK2RYZNNABY3ZOETYZCXRNQC
A2J7B4SCCJYKQV3G2LDHEFNE2GUICO3N3Y5FKF4EUZW5AG7PTDWAC
N4NDAZYTLSI2W22KT3SYXL257DBMSH3UT2BXOYM7LH7FSZAY4RLAC
SLL7262CJUE7TZDDZZXFROHCVVDA527WA4PHXCKEGZUJF2EN5MQAC
PBD7LZYQHXAA3KLH2ZUX5GW4UFML6BQ32KXZF4KZ6OYFASUYFJ5QC
V2VDN77HCSRYYWXDJJ2XOVHV4P6PVWNJZLXZ7JUYPQEZQIH5BZ3QC
GMYPBCWEB6NKURRILAHR3TJUKDOGR2ZMK5I6MS6P5G2LAGH36P3QC
MJ6R42RCK2ASXAJ6QXDPMAW56RBOJ4F4HI2LFIV3KXFIKWYMQK3QC
6L5BK5EHPAOQX3JCKUJ273UDNAC23LPQL4HIJGM4AV3P3QK5OKIQC
O2BZOX7MS4JCDS3C6EJQXAWUEQV6HVDCIF2FIN2BCJNRLIU6ZVKAC
I2KHGVD44KT4MQJXGCTVSQKMBO6TVCY72F26TLXGWRL6PHGF6RNQC
NJNMO72S7VIUV22JXB4IFPZMHWTJAOTP6EC6Z4QSKYIROSXT52MQC
PT4276XCOP5NJ3GRFJLIBZKVNVAOATAY5PLWV7FWK6RZW5FTEP5AC
2MNO5FUYXF6GHHWTIDLW2JGMFC3UY54BHJKUYVF7SZCUJQWKZ4DQC
BROSTG5KP3NUNLSYPVQID254TE47E5RKQAKLPIY7PGWETE6JNMTAC
MB5SHULBN3WP7TGUWZDP6BRGP423FTYKF67T5IF5YHHLNXKQ5REAC
BXGLKYRXO2O4NRM3BLNWQ7AWVPQXAMFS57MFYHJNOZZEZZW5BH6AC
Z24SZOGZJLDTDTGWH7M25RYQ7MYSU52ZLFWJ2PSQFTMK4J35PIWAC
EZQG2APB36DDMIAYDPPDGOIXOD7K2RZZSGC2NKGZIHB2HZBTW7EQC
QMEYU4MWLTSWPWEEOFRLK2IKE64BY3V5X73323WPLCGPP3TPDYGAC
ADMKQQGCGVSHHIMVQ4XFRDCG544SBJCYALSKZV45CQQBZ4ACUH2AC
4ZLEDBK7VGLKFUPENAFLUJYNFLKFYJ3TREPQ7P6PKMYGJUXB55HQC
TCOAKCGGHOIRJCTZYEZQ3K6KCGL2LGAYGYFRGSPCHBTJJY2V6AXAC
HO2PFRABW6BBTE4MUKUTEGXCMJS46WGVBCNWOHO4OL52DVAB4YDAC
Z3M53KTLZMPOISMHE25SZJSWX5TA37IV33IRE7KNRAD3PKEAEJXQC
MGOF7IUFGXYQKZOKMM2GGULFFVAULEHLZDSHMUW6B5DBKVXXR74AC
DXIGERDTERUIG7QHHRPKTSJHSQEPJPDJVLUW7YVC7URXBQ4ZJVOAC
DJATFGIC75CQDWMFHIWOKFXF26GKPINREMP6FNNTLF75JZZ3EQEQC
RFYEVKZQLOOQP536GRZOROSQW2O7TEHJ2HZDRVVUSBKLY5FBEO3QC
UUR6SMCAJMA7O3ZFUCQMPZFDDIPUVQ5IHUAC5F252YVD6H3JIKPQC
YWNTVA7PN7MC3HNTER3OCFHQAVKNJUK7KRQDZYFK24S5JLWHNU4AC
3GBSDS5PDSTTJTJOLEKZRRTAONS3T3JFZ3FQGFGS3AOXDBZ6SPLAC
GLFF5ZDKWI7WKPZSAEE3IUM27LL6DFOPIL4VPODXYXV3BCSCJ6GQC
NMWWP4ZNOKHZKSJ6F5KYEREWXXR5F4UD35WOKI3EH42AZWVCTCJAC
OV5AKJHA773ETIJPTMQ7K64U7BRQE34OXJ6FJNH6TZG22WS5QTIAC
JV3UEPNCNIPNEL3EM4MOJPWTD3ZNL5FUEBEMGNUDKFUXSAWTNKNQC
2KZPOGRBY6KBMO76F55ZKIVOLSG3O63VP3RHRZVANXYT3OLZ3OWQC
RSEB2NFGUBTFESE5BJKDUVQL5Y5ZVGY5O4CJX2LNP63MS3NRHHZQC
OBFPJS2GHO2PEHBHGEHKIUOUAFIQHPIZXEVD2YIE3ZIE2PVMH5VAC
4SCFOJGNDAN4XZEAPWQQCBJ3CGZCJP3HUADRQLYZ2ITAKA7EJJTQC
WJO37T74RYR5DRMSVNCXAQBOV42FQB63EG43XDZUU5TA354AIJRAC
nnoremap <leader>h :!~/.nix-profile/bin/hasktags -o tags -c $(find lib server daemon test -name \*.hs) && ~/.nix-profile/bin/ctags --options-maybe=.ctags --options=$HOME/.ctags --append=yes .<CR><CR>
nnoremap <leader>o :!~/.nix-profile/bin/ormolu --mode inplace $(find lib server daemon test -name '*.hs')<CR><CR>
import Control.Error.Util ( maybeT )
import Control.Lens ( (^.)
, makeLenses
, makeClassyPrisms
, traverseOf
, to
)
import Control.Monad.Except ( MonadError
, throwError
)
import Control.Monad.Trans.Except ( withExceptT )
import Control.Monad.Trans.Reader ( mapReaderT )
import Crypto.Random.Types ( MonadRandom(..) )
import Database.PostgreSQL.Simple ( Connection
, connect
)
import Data.Thyme.Clock as C
import Data.Thyme.Time as C
import qualified Data.Text as T
import qualified Network.Mail.Mime as Mime
import qualified Network.Mail.SMTP as SMTP
import Network.URI ( URI
, parseURI
)
import Text.StringTemplate ( directoryGroup
, newSTMP
, getStringTemplate
, setManyAttrib
, render
)
import Filesystem.Path.CurrentOS ( encodeString )
import Bippy.Types ( Satoshi )
import Aftok.Billing
( Billable,
Billable',
ContactChannel (..),
Subscription',
billable,
contactChannel,
customer,
name,
paymentRequestEmailTemplate,
paymentRequestMemoTemplate,
project,
)
import qualified Aftok.Config as AC
import Aftok.Currency.Bitcoin (satoshi)
import qualified Aftok.Database as DB
import Aftok.Database.PostgreSQL (QDBM (..))
import qualified Aftok.Payments as P
import Aftok.Payments.Types
( PaymentKey (..),
paymentKey,
paymentRequestTotal,
subscription,
)
import Aftok.Project
( Project,
projectName,
)
import Aftok.Types
( ProjectId (..),
User,
UserId,
_Email,
)
import qualified AftokD as D
import Bippy.Types (Satoshi)
import Control.Error.Util (maybeT)
import Control.Lens
( (^.),
makeClassyPrisms,
makeLenses,
to,
traverseOf,
)
import Control.Monad.Except
( MonadError,
throwError,
)
import Control.Monad.Trans.Except (withExceptT)
import Control.Monad.Trans.Reader (mapReaderT)
import Crypto.Random.Types (MonadRandom (..))
import qualified Data.Text as T
import Data.Thyme.Clock as C
import Data.Thyme.Time as C
import Database.PostgreSQL.Simple
( Connection,
connect,
)
import Filesystem.Path.CurrentOS (encodeString)
import qualified Network.Mail.Mime as Mime
import qualified Network.Mail.SMTP as SMTP
import Network.URI
( URI,
parseURI,
)
import Text.StringTemplate
( directoryGroup,
getStringTemplate,
newSTMP,
render,
setManyAttrib,
)
import Aftok.Types ( User
, UserId
, ProjectId(..)
, _Email
)
import Aftok.Currency.Bitcoin ( satoshi )
import qualified Aftok.Config as AC
import Aftok.Billing ( Billable
, Billable'
, Subscription'
, ContactChannel(..)
, contactChannel
, 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
, projectName
)
import qualified AftokD as D
preqCfg = cfg ^. (dcfg . D.paymentRequestConfig)
reqMay = do
preq <- DB.findPaymentRequestId reqId
preq' <- traverseOf P.subscription DB.findSubscriptionBillable preq
preqCfg = cfg ^. (dcfg . D.paymentRequestConfig)
reqMay = do
preq <- DB.findPaymentRequestId reqId
preq' <- traverseOf P.subscription DB.findSubscriptionBillable preq
mail <- buildPaymentRequestEmail preqCfg req bip70URL
let mailer = maybe (SMTP.sendMailWithLogin _smtpHost)
(SMTP.sendMailWithLogin' _smtpHost)
_smtpPort
mail <- buildPaymentRequestEmail preqCfg req bip70URL
let mailer =
maybe
(SMTP.sendMailWithLogin _smtpHost)
(SMTP.sendMailWithLogin' _smtpHost)
_smtpPort
buildPaymentRequestEmail
:: (MonadIO m, MonadError AftokDErr m)
=> D.PaymentRequestConfig
-> P.PaymentRequest' (Subscription' User (Billable' Project UserId Satoshi))
-> URI
-> m Mime.Mail
buildPaymentRequestEmail ::
(MonadIO m, MonadError AftokDErr m) =>
D.PaymentRequestConfig ->
P.PaymentRequest' (Subscription' User (Billable' Project UserId Satoshi)) ->
URI ->
m Mime.Mail
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" , show $ total ^. satoshi)
, ("payment_url" , show paymentUrl)
]
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", show $ total ^. satoshi),
("payment_url", show paymentUrl)
]
toAddr = Mime.Address Nothing (toEmail ^. _Email)
subject = "Payment is due for your " <> pname <> " subscription!"
body = Mime.plainPart . render $ setAttrs template
toAddr = Mime.Address Nothing (toEmail ^. _Email)
subject = "Payment is due for your " <> pname <> " subscription!"
body = Mime.plainPart . render $ setAttrs template
setAttrs = setManyAttrib
[ ("project_name", req ^. (billable . project . projectName))
, ("subscription", req ^. (billable . name))
, ("billing_date", show billingDate)
, ("issue_time" , show requestTime)
]
setAttrs =
setManyAttrib
[ ("project_name", req ^. (billable . project . projectName)),
("subscription", req ^. (billable . name)),
("billing_date", show billingDate),
("issue_time", show requestTime)
]
import Control.Lens
import qualified Aftok.Config as AC
import Aftok.Types (Email (..))
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
( encodeString,
fromText,
)
import qualified Filesystem.Path.CurrentOS as P
import qualified Data.Configurator as C
import qualified Data.Configurator.Types as CT
import Database.PostgreSQL.Simple ( ConnectInfo )
import Filesystem.Path.CurrentOS ( fromText
, encodeString
)
import qualified Filesystem.Path.CurrentOS as P
data PaymentRequestConfig
= PaymentRequestConfig
{ _aftokHost :: Text,
_templatePath :: P.FilePath,
_billingFromEmail :: Email
}
data PaymentRequestConfig = PaymentRequestConfig
{ _aftokHost :: Text
, _templatePath :: P.FilePath
, _billingFromEmail :: Email
}
makeLenses ''PaymentRequestConfig
data Config
= Config
{ _smtpConfig :: AC.SmtpConfig,
_billingConfig :: AC.BillingConfig,
_dbConfig :: ConnectInfo,
_paymentRequestConfig :: PaymentRequestConfig
}
import Control.Exception ( try )
import System.Environment ( getEnv )
import System.IO.Error ( IOError )
import Filesystem.Path.CurrentOS ( decodeString )
import qualified AftokD as D
import AftokD.AftokM ( createAllPaymentRequests )
import Control.Lens
import Data.Hourglass ( Seconds(..) )
import Data.Ratio ( (%) )
import Data.Traversable ( for )
import Data.Thyme.Clock as C
import Data.Thyme.Format ( )
import Data.UUID
import Aftok.Currency.Bitcoin
( satoshi,
ssub,
)
import Aftok.Types
( ProjectId,
UserId,
)
import Bippy.Types (Satoshi (..))
import Control.Lens
import Data.Hourglass (Seconds (..))
import Data.Ratio ((%))
import Data.Thyme.Clock as C
import Data.Thyme.Format ()
import Data.Traversable (for)
import Data.UUID
data Auction = Auction
{ _projectId :: ProjectId
, _initiator :: UserId
, _createdAt :: C.UTCTime
, _raiseAmount :: Satoshi
, _auctionStart :: C.UTCTime
, _auctionEnd :: C.UTCTime
}
data Auction
= Auction
{ _projectId :: ProjectId,
_initiator :: UserId,
_createdAt :: C.UTCTime,
_raiseAmount :: Satoshi,
_auctionStart :: C.UTCTime,
_auctionEnd :: C.UTCTime
}
data Bid = Bid
{ _bidUser :: UserId
, _bidSeconds :: Seconds
, _bidAmount :: Satoshi
, _bidTime :: C.UTCTime
} deriving (Eq, Show)
data Bid
= Bid
{ _bidUser :: UserId,
_bidSeconds :: Seconds,
_bidAmount :: Satoshi,
_bidTime :: C.UTCTime
}
deriving (Eq, Show)
data Commitment = Commitment
{ _baseBid :: Bid
, _commitmentSeconds :: Seconds
, _commitmentAmount :: Satoshi
}
data Commitment
= Commitment
{ _baseBid :: Bid,
_commitmentSeconds :: Seconds,
_commitmentAmount :: Satoshi
}
where
secs bid = toRational $ bid ^. bidSeconds
btc bid = toRational $ bid ^. bidAmount . satoshi
costRatio bid = secs bid / btc bid
where
secs bid = toRational $ bid ^. bidSeconds
btc bid = toRational $ bid ^. bidAmount . satoshi
costRatio bid = secs bid / btc bid
let
takeWinningBids :: Satoshi -> [Bid] -> [Bid]
takeWinningBids total (bid : xs)
|
-- if the total is fully within the raise amount
total <> (bid ^. bidAmount) < raiseAmount'
= bid : takeWinningBids (total <> (bid ^. bidAmount)) xs
|
-- if the last bid will exceed the raise amount, reduce it to fit
total < raiseAmount'
= let
winFraction r = r % (bid ^. bidAmount . satoshi)
remainderSeconds (Satoshi r) =
Seconds . round $ winFraction r * fromIntegral (bid ^. bidSeconds)
adjustBid r = bid & bidSeconds .~ remainderSeconds r & bidAmount .~ r
in
toList $ adjustBid <$> raiseAmount' `ssub` total
| otherwise
= []
takeWinningBids _ [] = []
submittedTotal = bidsTotal bids
in
maybe (WinningBids $ takeWinningBids (Satoshi 0) $ sortBy bidOrder bids)
InsufficientBids
(raiseAmount' `ssub` submittedTotal)
let takeWinningBids :: Satoshi -> [Bid] -> [Bid]
takeWinningBids total (bid : xs)
| -- if the total is fully within the raise amount
total <> (bid ^. bidAmount) < raiseAmount' =
bid : takeWinningBids (total <> (bid ^. bidAmount)) xs
| -- if the last bid will exceed the raise amount, reduce it to fit
total < raiseAmount' =
let winFraction r = r % (bid ^. bidAmount . satoshi)
remainderSeconds (Satoshi r) =
Seconds . round $ winFraction r * fromIntegral (bid ^. bidSeconds)
adjustBid r = bid & bidSeconds .~ remainderSeconds r & bidAmount .~ r
in toList $ adjustBid <$> raiseAmount' `ssub` total
| otherwise =
[]
takeWinningBids _ [] = []
submittedTotal = bidsTotal bids
in maybe
(WinningBids $ takeWinningBids (Satoshi 0) $ sortBy bidOrder bids)
InsufficientBids
(raiseAmount' `ssub` submittedTotal)
x | x <> (bid ^. bidAmount) < raiseAmount' ->
put (x <> bid ^. bidAmount)
>> (pure . Just $ Commitment bid (bid ^. bidSeconds) (bid ^. bidAmount))
x
| x <> (bid ^. bidAmount) < raiseAmount' ->
put (x <> bid ^. bidAmount)
>> (pure . Just $ Commitment bid (bid ^. bidSeconds) (bid ^. bidAmount))
x | x < raiseAmount' ->
let winFraction r = r % (bid ^. bidAmount . satoshi)
remainderSeconds (Satoshi r) =
x
| x < raiseAmount' ->
let winFraction r = r % (bid ^. bidAmount . satoshi)
remainderSeconds (Satoshi r) =
in for (raiseAmount' `ssub` x) $ \remainder ->
put (x <> remainder)
*> (pure $ Commitment bid (remainderSeconds remainder) remainder)
in for (raiseAmount' `ssub` x) $ \remainder ->
put (x <> remainder)
*> (pure $ Commitment bid (remainderSeconds remainder) remainder)
import Control.Lens (makeLenses, makePrisms, preview, view, _Just)
import Data.Thyme.Clock as C
import Data.Thyme.Time as T
import Data.UUID
import Aftok.Types (UserId, ProjectId, Email)
import Bippy.Types (Satoshi)
import Aftok.Types (Email, ProjectId, UserId)
import Bippy.Types (Satoshi)
import Control.Lens (_Just, makeLenses, makePrisms, preview, view)
import Data.Thyme.Clock as C
import Data.Thyme.Time as T
import Data.UUID
data Billable' p u c = Billable
{ _project :: p
, _creator :: u
, _name :: Text
, _description :: Text
, _recurrence :: Recurrence
, _amount :: c
, _gracePeriod :: Days
, _requestExpiryPeriod :: Maybe C.NominalDiffTime
, _paymentRequestEmailTemplate :: Maybe Text
, _paymentRequestMemoTemplate :: Maybe Text
}
data Billable' p u c
= Billable
{ _project :: p,
_creator :: u,
_name :: Text,
_description :: Text,
_recurrence :: Recurrence,
_amount :: c,
_gracePeriod :: Days,
_requestExpiryPeriod :: Maybe C.NominalDiffTime,
_paymentRequestEmailTemplate :: Maybe Text,
_paymentRequestMemoTemplate :: Maybe Text
}
data Subscription' u b = Subscription
{ _customer :: u
, _billable :: b
, _contactChannel :: ContactChannel
, _startTime :: C.UTCTime
, _endTime :: Maybe C.UTCTime
}
data Subscription' u b
= Subscription
{ _customer :: u,
_billable :: b,
_contactChannel :: ContactChannel,
_startTime :: C.UTCTime,
_endTime :: Maybe C.UTCTime
}
import Control.Lens ( makeClassy
, (^.)
)
import qualified Data.Configurator as C
import qualified Data.Configurator.Types as C
import Data.X509
import Data.X509.File ( readKeyFile
, readSignedObject
)
import Database.PostgreSQL.Simple ( ConnectInfo(..) )
import Filesystem.Path.CurrentOS ( fromText
, encodeString
)
import qualified Filesystem.Path.CurrentOS as P
import Safe ( headMay )
import Aftok.Currency.Bitcoin (NetworkMode)
import Aftok.Payments (PaymentsConfig (..))
import qualified Bippy.Types as BT
import Control.Lens
( (^.),
makeClassy,
)
import qualified Data.Configurator as C
import qualified Data.Configurator.Types as C
import Data.X509
import Data.X509.File
( readKeyFile,
readSignedObject,
)
import Database.PostgreSQL.Simple (ConnectInfo (..))
import Filesystem.Path.CurrentOS
( encodeString,
fromText,
)
import qualified Filesystem.Path.CurrentOS as P
import qualified Network.Mail.SMTP as SMTP
import qualified Network.Socket as NS
import Safe (headMay)
import qualified Bippy.Types as BT
import qualified Network.Mail.SMTP as SMTP
import qualified Network.Socket as NS
import Aftok.Currency.Bitcoin ( NetworkMode )
import Aftok.Payments ( PaymentsConfig(..) )
data SmtpConfig
= SmtpConfig
{ _smtpHost :: NS.HostName,
_smtpPort :: Maybe NS.PortNumber,
_smtpUser :: SMTP.UserName,
_smtpPass :: SMTP.Password
}
data BillingConfig = BillingConfig
{ _networkMode :: NetworkMode
, _signingKeyFile :: P.FilePath
, _certsFile :: P.FilePath
, _exchangeRateServiceURI :: String
}
data BillingConfig
= BillingConfig
{ _networkMode :: NetworkMode,
_signingKeyFile :: P.FilePath,
_certsFile :: P.FilePath,
_exchangeRateServiceURI :: String
}
fail
$ "Only RSA keys are currently supported for payment request signing."
Nothing -> fail $ "No keys found in private key file " <> encodeString
(c ^. signingKeyFile)
fail $
"Only RSA keys are currently supported for payment request signing."
Nothing ->
fail $
"No keys found in private key file "
<> encodeString
(c ^. signingKeyFile)
import qualified Data.Configurator.Types as C
import Control.Lens
import Bippy.Types ( Satoshi(..) )
import Haskoin.Constants
import Bippy.Types (Satoshi (..))
import Control.Lens
import qualified Data.Configurator.Types as C
import Haskoin.Constants
( ZAddr(..)
, _ZAddr
, RPCError(..)
, ZValidateAddressErr(..)
, ZcashdConfig(..)
, Zatoshi
, ToZatoshi(..)
, rpcAddViewingKey
, rpcValidateZAddr
) where
( ZAddr (..),
_ZAddr,
RPCError (..),
ZValidateAddressErr (..),
ZcashdConfig (..),
Zatoshi,
ToZatoshi (..),
rpcAddViewingKey,
rpcValidateZAddr,
)
where
import Control.Exception ( catch )
import Control.Lens ( makePrisms )
import Control.Monad.Trans.Except ( except )
import qualified Data.Aeson as A
import Data.Aeson ( Value, (.=), (.:), (.:?), object, encode )
import Data.Aeson.Types ( Parser )
import qualified Data.Text.Encoding as T
import Control.Exception (catch)
import Control.Lens (makePrisms)
import Control.Monad.Trans.Except (except)
import Data.Aeson ((.:), (.:?), (.=), Value, encode, object)
import qualified Data.Aeson as A
import Data.Aeson.Types (Parser)
import qualified Data.Text.Encoding as T
import Network.HTTP.Client
( HttpException,
Manager,
RequestBody (..),
applyBasicAuth,
defaultRequest,
host,
httpLbs,
method,
port,
requestBody,
responseBody,
responseStatus,
)
import Network.HTTP.Types (Status, statusCode)
import Network.HTTP.Client ( Manager
, RequestBody(..)
, HttpException
, defaultRequest
, responseBody
, responseStatus
, httpLbs
, host, port, method, requestBody
, applyBasicAuth
)
import Network.HTTP.Types ( Status, statusCode )
data ZcashdConfig = ZcashdConfig
{ zcashdHost :: Text
, zcashdPort :: Int
, rpcUser :: Text
, rpcPassword :: Text
}
data ZcashdConfig
= ZcashdConfig
{ zcashdHost :: Text,
zcashdPort :: Int,
rpcUser :: Text,
rpcPassword :: Text
}
let req = applyBasicAuth (T.encodeUtf8 $ rpcUser cfg) (T.encodeUtf8 $ rpcPassword cfg) $
defaultRequest { host = T.encodeUtf8 $ zcashdHost cfg
, port = zcashdPort cfg
, method = "POST"
, requestBody = RequestBodyLBS . encode $ toRequestBody call
}
response <- ExceptT $ catch
(Right <$> httpLbs req mgr)
(pure . Left . HttpError)
let req =
applyBasicAuth (T.encodeUtf8 $ rpcUser cfg) (T.encodeUtf8 $ rpcPassword cfg) $
defaultRequest
{ host = T.encodeUtf8 $ zcashdHost cfg,
port = zcashdPort cfg,
method = "POST",
requestBody = RequestBodyLBS . encode $ toRequestBody call
}
response <-
ExceptT $
catch
(Right <$> httpLbs req mgr)
(pure . Left . HttpError)
data ZValidateAddressResp = ZValidateAddressResp
{ vzrIsValid :: Bool
--, vzrAddress :: Maybe Text
, vzrAddrType :: Maybe ZAddrType
}
data ZValidateAddressResp
= ZValidateAddressResp
{ vzrIsValid :: Bool,
--, vzrAddress :: Maybe Text
vzrAddrType :: Maybe ZAddrType
}
validateZAddrRequest addr = object
[ "jsonrpc" .= ("1.0" :: Text)
, "id" .= ("aftok-z_validateaddress" :: Text)
, "method" .= ("z_validateaddress" :: Text)
, "params" .= [addr]
]
validateZAddrRequest addr =
object
[ "jsonrpc" .= ("1.0" :: Text),
"id" .= ("aftok-z_validateaddress" :: Text),
"method" .= ("z_validateaddress" :: Text),
"params" .= [addr]
]
except $ if vzrIsValid resp
then
case vzrAddrType resp of
Nothing -> Left (RPCError DataMissing)
Just Sprout -> Left (RPCError SproutAddress)
except $
if vzrIsValid resp
then case vzrAddrType resp of
Nothing -> Left (RPCError DataMissing)
Just Sprout -> Left (RPCError SproutAddress)
importViewingKeyRequest vk = object
[ "jsonrpc" .= ("1.0" :: Text)
, "id" .= ("aftok-z_importviewingkey" :: Text)
, "method" .= ("z_importviewingkey" :: Text)
, "params" .= [vk, "no"] -- no need to rescan, for our purposes
]
importViewingKeyRequest vk =
object
[ "jsonrpc" .= ("1.0" :: Text),
"id" .= ("aftok-z_importviewingkey" :: Text),
"method" .= ("z_importviewingkey" :: Text),
"params" .= [vk, "no"] -- no need to rescan, for our purposes
]
import Data.Aeson ( FromJSON(..)
, ToJSON(..)
)
import Aftok.TimeLog.Serialization ( depfFromJSON
, depfToJSON
)
import Aftok.Types ( DepreciationFunction )
import Aftok.TimeLog.Serialization
( depfFromJSON,
depfToJSON,
)
import Aftok.Types (DepreciationFunction)
import Data.Aeson
( FromJSON (..),
ToJSON (..),
)
import Prelude hiding ( null )
import Control.Lens
import Control.Monad.Trans.Except ( throwE )
import Crypto.Random.Types ( MonadRandom
, getRandomBytes
)
import Data.Aeson ( Value
, toJSON
)
import Data.Hourglass
import qualified Data.List as L
import Data.ProtocolBuffers ( decodeMessage
, encodeMessage
)
import Data.Serialize.Get ( runGet )
import Data.Serialize.Put ( runPut )
import qualified Data.Text as T
import Data.Thyme.Clock as C
import Data.Thyme.Time
import Data.UUID ( UUID )
import Database.PostgreSQL.Simple
import Database.PostgreSQL.Simple.FromField
import Database.PostgreSQL.Simple.FromRow
import Database.PostgreSQL.Simple.SqlQQ
( sql )
import Database.PostgreSQL.Simple.Types
( Null )
import Safe ( headMay )
import qualified Aftok.Auction as A
import qualified Aftok.Billing as B
import Aftok.Currency.Bitcoin
import Aftok.Currency.Zcash (ZAddr (..), _ZAddr)
import Aftok.Database
import Aftok.Database.PostgreSQL.Types
( SerDepFunction (..),
)
import Aftok.Interval
import Aftok.Json
( billableJSON,
createSubscriptionJSON,
paymentJSON,
paymentRequestJSON,
)
import Aftok.Payments.Types
import qualified Aftok.Project as P
import Aftok.TimeLog
import Aftok.Types
import Bippy.Types (Satoshi (..))
import Control.Lens
import Control.Monad.Trans.Except (throwE)
import Crypto.Random.Types
( MonadRandom,
getRandomBytes,
)
import Data.Aeson
( Value,
toJSON,
)
import Data.Hourglass
import qualified Data.List as L
import Data.ProtocolBuffers
( decodeMessage,
encodeMessage,
)
import Data.Serialize.Get (runGet)
import Data.Serialize.Put (runPut)
import qualified Data.Text as T
import Data.Thyme.Clock as C
import Data.Thyme.Time
import Data.UUID (UUID)
import Database.PostgreSQL.Simple
import Database.PostgreSQL.Simple.FromField
import Database.PostgreSQL.Simple.FromRow
import Database.PostgreSQL.Simple.SqlQQ
( sql,
)
import Database.PostgreSQL.Simple.Types
( Null,
)
import Haskoin.Address
( Address,
addrToText,
textToAddr,
)
import Haskoin.Constants (Network)
import Safe (headMay)
import Prelude hiding (null)
import qualified Aftok.Auction as A
import qualified Aftok.Billing as B
import Aftok.Currency.Bitcoin
import Aftok.Currency.Zcash (ZAddr(..), _ZAddr)
import Aftok.Database
import Aftok.Database.PostgreSQL.Types
( SerDepFunction(..) )
import Aftok.Interval
import Aftok.Json ( billableJSON
, createSubscriptionJSON
, paymentJSON
, paymentRequestJSON
)
import Aftok.Payments.Types
import qualified Aftok.Project as P
import Aftok.TimeLog
import Aftok.Types
import Bippy.Types ( Satoshi(..) )
import Haskoin.Address ( Address
, textToAddr
, addrToText
)
import Haskoin.Constants ( Network )
let err = returnError
ConversionFailed
f
( "could not deserialize value "
<> T.unpack fieldValue
<> " to a valid BTC address for network "
<> show n
)
let err =
returnError
ConversionFailed
f
( "could not deserialize value "
<> T.unpack fieldValue
<> " to a valid BTC address for network "
<> show n
)
else maybe
(returnError UnexpectedNull f "event type may not be null")
( maybe (returnError Incompatible f "unrecognized event type value") pure
. nameEvent
. decodeUtf8
)
v
else
maybe
(returnError UnexpectedNull f "event type may not be null")
( maybe (returnError Incompatible f "unrecognized event type value") pure
. nameEvent
. decodeUtf8
)
v
where
parser :: Text -> RowParser (CreditTo (NetworkId, Address))
parser = \case
"credit_to_address" ->
CreditToCurrency <$> (btcAddressParser mode <* nullField <* nullField)
"credit_to_user" ->
CreditToUser <$> (nullField *> nullField *> idParser UserId <* nullField)
"credit_to_project" ->
CreditToProject
<$> (nullField *> nullField *> nullField *> idParser ProjectId)
_ -> empty
where
parser :: Text -> RowParser (CreditTo (NetworkId, Address))
parser = \case
"credit_to_address" ->
CreditToCurrency <$> (btcAddressParser mode <* nullField <* nullField)
"credit_to_user" ->
CreditToUser <$> (nullField *> nullField *> idParser UserId <* nullField)
"credit_to_project" ->
CreditToProject
<$> (nullField *> nullField *> nullField *> idParser ProjectId)
_ -> empty
<*> (
(maybe empty pure =<< fmap (RecoverByEmail . Email) <$> field)
<|>
(maybe empty pure =<< fmap (RecoverByZAddr . ZAddr) <$> field)
)
<*> ( (maybe empty pure =<< fmap (RecoverByEmail . Email) <$> field)
<|> (maybe empty pure =<< fmap (RecoverByZAddr . ZAddr) <$> field)
)
"weekly" -> B.Weekly <$> field
"onetime" -> nullField *> pure B.OneTime
_ -> empty
in field >>= prec
"weekly" -> B.Weekly <$> field
"onetime" -> nullField *> pure B.OneTime
_ -> empty
in field >>= prec
storeEvent (CreateSubscription uid bid t) = Just $ storeEventJSON
(Just uid)
"create_subscription"
(createSubscriptionJSON uid bid t)
storeEvent (CreatePaymentRequest req) = Just
$ storeEventJSON Nothing "create_payment_request" (paymentRequestJSON req)
storeEvent (CreateSubscription uid bid t) =
Just $
storeEventJSON
(Just uid)
"create_subscription"
(createSubscriptionJSON uid bid t)
storeEvent (CreatePaymentRequest req) =
Just $
storeEventJSON Nothing "create_payment_request" (paymentRequestJSON req)
( pid
, uid
, creditToName c
, renderNetworkId nid
, addrToText network addr
, eventName e
, fromThyme $ e ^. eventTime
, m
( pid,
uid,
creditToName c,
renderNetworkId nid,
addrToText network addr,
eventName e,
fromThyme $ e ^. eventTime,
m
CreditToProject pid' -> pinsert
EventId
[sql| INSERT INTO work_events
CreditToProject pid' ->
pinsert
EventId
[sql| INSERT INTO work_events
( pid
, uid
, creditToName c
, pid' ^. _ProjectId
, eventName e
, fromThyme $ e ^. eventTime
, m
)
CreditToUser uid' -> pinsert
EventId
[sql| INSERT INTO work_events
( pid,
uid,
creditToName c,
pid' ^. _ProjectId,
eventName e,
fromThyme $ e ^. eventTime,
m
)
CreditToUser uid' ->
pinsert
EventId
[sql| INSERT INTO work_events
( pid
, uid
, creditToName c
, uid' ^. _UserId
, eventName e
, fromThyme $ e ^. eventTime
, m
)
( pid,
uid,
creditToName c,
uid' ^. _UserId,
eventName e,
fromThyme $ e ^. eventTime,
m
)
(pid, uid, fromThyme e, limit)
(During s e) -> pquery
(logEntryParser mode)
[sql| SELECT credit_to_type,
(pid, uid, fromThyme e, limit)
(During s e) ->
pquery
(logEntryParser mode)
[sql| SELECT credit_to_type,
(pid, uid, fromThyme s, fromThyme e, limit)
(After s) -> pquery
(logEntryParser mode)
[sql| SELECT credit_to_type,
(pid, uid, fromThyme s, fromThyme e, limit)
(After s) ->
pquery
(logEntryParser mode)
[sql| SELECT credit_to_type,
(pid, uid, limit)
pgEval (AmendEvent (EventId eid) (TimeChange mt t)) = pinsert
AmendmentId
[sql| INSERT INTO event_time_amendments
(pid, uid, limit)
pgEval (AmendEvent (EventId eid) (TimeChange mt t)) =
pinsert
AmendmentId
[sql| INSERT INTO event_time_amendments
(eid, fromThyme $ mt ^. _ModTime, fromThyme t)
(eid, fromThyme $ mt ^. _ModTime, fromThyme t)
( eid
, fromThyme $ mt ^. _ModTime
, creditToName c
, renderNetworkId nid
, addrToText network addr
( eid,
fromThyme $ mt ^. _ModTime,
creditToName c,
renderNetworkId nid,
addrToText network addr
CreditToProject pid -> pinsert
AmendmentId
[sql| INSERT INTO event_credit_to_amendments
CreditToProject pid ->
pinsert
AmendmentId
[sql| INSERT INTO event_credit_to_amendments
(eid, fromThyme $ mt ^. _ModTime, creditToName c, pid ^. _ProjectId)
CreditToUser uid -> pinsert
AmendmentId
[sql| INSERT INTO event_credit_to_amendments
(eid, fromThyme $ mt ^. _ModTime, creditToName c, pid ^. _ProjectId)
CreditToUser uid ->
pinsert
AmendmentId
[sql| INSERT INTO event_credit_to_amendments
(eid, fromThyme $ mt ^. _ModTime, creditToName c, uid ^. _UserId)
pgEval (AmendEvent (EventId eid) (MetadataChange mt v)) = pinsert
AmendmentId
[sql| INSERT INTO event_metadata_amendments
(eid, fromThyme $ mt ^. _ModTime, creditToName c, uid ^. _UserId)
pgEval (AmendEvent (EventId eid) (MetadataChange mt v)) =
pinsert
AmendmentId
[sql| INSERT INTO event_metadata_amendments
pgEval (CreateAuction auc) = pinsert
A.AuctionId
[sql| INSERT INTO auctions (project_id, initiator_id, raise_amount, end_time)
pgEval (CreateAuction auc) =
pinsert
A.AuctionId
[sql| INSERT INTO auctions (project_id, initiator_id, raise_amount, end_time)
( auc ^. (A.projectId . _ProjectId)
, auc ^. (A.initiator . _UserId)
, auc ^. (A.raiseAmount . satoshi)
, auc ^. (A.auctionEnd . to fromThyme)
)
pgEval (FindAuction aucId) = headMay <$> pquery
auctionParser
[sql| SELECT project_id, initiator_id, created_at, raise_amount, start_time, end_time
( auc ^. (A.projectId . _ProjectId),
auc ^. (A.initiator . _UserId),
auc ^. (A.raiseAmount . satoshi),
auc ^. (A.auctionEnd . to fromThyme)
)
pgEval (FindAuction aucId) =
headMay
<$> pquery
auctionParser
[sql| SELECT project_id, initiator_id, created_at, raise_amount, start_time, end_time
(Only (aucId ^. A._AuctionId))
pgEval (CreateBid (A.AuctionId aucId) bid) = pinsert
A.BidId
[sql| INSERT INTO bids (auction_id, bidder_id, bid_seconds, bid_amount, bid_time)
(Only (aucId ^. A._AuctionId))
pgEval (CreateBid (A.AuctionId aucId) bid) =
pinsert
A.BidId
[sql| INSERT INTO bids (auction_id, bidder_id, bid_seconds, bid_amount, bid_time)
( aucId
, bid ^. (A.bidUser . _UserId)
, case bid ^. A.bidSeconds of
(Seconds i) -> i
, bid ^. (A.bidAmount . satoshi)
, bid ^. (A.bidTime . to fromThyme)
)
pgEval (FindBids aucId) = pquery
((,) <$> idParser A.BidId <*> bidParser)
[sql| SELECT id, bidder_id, bid_seconds, bid_amount, bid_time FROM bids WHERE auction_id = ? |]
(Only (aucId ^. A._AuctionId))
( aucId,
bid ^. (A.bidUser . _UserId),
case bid ^. A.bidSeconds of
(Seconds i) -> i,
bid ^. (A.bidAmount . satoshi),
bid ^. (A.bidTime . to fromThyme)
)
pgEval (FindBids aucId) =
pquery
((,) <$> idParser A.BidId <*> bidParser)
[sql| SELECT id, bidder_id, bid_seconds, bid_amount, bid_time FROM bids WHERE auction_id = ? |]
(Only (aucId ^. A._AuctionId))
( user' ^. (username . _UserName)
, user' ^? userAccountRecovery . _RecoverByEmail . _Email
, user' ^? userAccountRecovery . _RecoverByZAddr . _ZAddr
( user' ^. (username . _UserName),
user' ^? userAccountRecovery . _RecoverByEmail . _Email,
user' ^? userAccountRecovery . _RecoverByZAddr . _ZAddr
headMay <$> pquery
userParser
[sql| SELECT handle, recovery_email, recovery_zaddr FROM users WHERE id = ? |]
(Only uid)
headMay
<$> pquery
userParser
[sql| SELECT handle, recovery_email, recovery_zaddr FROM users WHERE id = ? |]
(Only uid)
headMay <$> pquery
((,) <$> idParser UserId <*> userParser)
[sql| SELECT id, handle, recovery_email, recovery_zaddr FROM users WHERE handle = ? |]
(Only h)
headMay
<$> pquery
((,) <$> idParser UserId <*> userParser)
[sql| SELECT id, handle, recovery_email, recovery_zaddr FROM users WHERE handle = ? |]
(Only h)
headMay <$> pquery
(btcAddressParser mode)
[sql| SELECT default_payment_network, default_payment_addr FROM users WHERE id = ? |]
(Only uid)
headMay
<$> pquery
(btcAddressParser mode)
[sql| SELECT default_payment_network, default_payment_addr FROM users WHERE id = ? |]
(Only uid)
pgEval (FindInvitation ic) = headMay <$> pquery
invitationParser
[sql| SELECT project_id, invitor_id, invitee_email, invitation_time, acceptance_time
pgEval (FindInvitation ic) =
headMay
<$> pquery
invitationParser
[sql| SELECT project_id, invitor_id, invitee_email, invitation_time, acceptance_time
void $ pexec
[sql| UPDATE invitations SET acceptance_time = ? WHERE invitation_key = ? |]
(fromThyme t, P.renderInvCode ic)
void $ pexec
[sql| INSERT INTO project_companions (project_id, user_id, invited_by, joined_at)
void $
pexec
[sql| UPDATE invitations SET acceptance_time = ? WHERE invitation_key = ? |]
(fromThyme t, P.renderInvCode ic)
void $
pexec
[sql| INSERT INTO project_companions (project_id, user_id, invited_by, joined_at)
(uid, fromThyme t, P.renderInvCode ic)
pgEval (CreateProject p) = pinsert
ProjectId
[sql| INSERT INTO projects (project_name, inception_date, initiator_id, depreciation_fn)
(uid, fromThyme t, P.renderInvCode ic)
pgEval (CreateProject p) =
pinsert
ProjectId
[sql| INSERT INTO projects (project_name, inception_date, initiator_id, depreciation_fn)
( p ^. P.projectName
, p ^. (P.inceptionDate . to fromThyme)
, p ^. (P.initiator . _UserId)
, toJSON $ p ^. P.depf . to SerDepFunction
)
( p ^. P.projectName,
p ^. (P.inceptionDate . to fromThyme),
p ^. (P.initiator . _UserId),
toJSON $ p ^. P.depf . to SerDepFunction
)
(Only (pid ^. _ProjectId))
pgEval (FindProject (ProjectId pid)) = headMay <$> pquery
projectParser
[sql| SELECT project_name, inception_date, initiator_id, depreciation_fn FROM projects WHERE id = ? |]
(Only pid)
pgEval (FindUserProjects (UserId uid)) = pquery
((,) <$> idParser ProjectId <*> projectParser)
[sql| SELECT DISTINCT ON (p.inception_date, p.id) p.id, p.project_name, p.inception_date, p.initiator_id, p.depreciation_fn
(Only (pid ^. _ProjectId))
pgEval (FindProject (ProjectId pid)) =
headMay
<$> pquery
projectParser
[sql| SELECT project_name, inception_date, initiator_id, depreciation_fn FROM projects WHERE id = ? |]
(Only pid)
pgEval (FindUserProjects (UserId uid)) =
pquery
((,) <$> idParser ProjectId <*> projectParser)
[sql| SELECT DISTINCT ON (p.inception_date, p.id) p.id, p.project_name, p.inception_date, p.initiator_id, p.depreciation_fn
(uid, uid)
pgEval (AddUserToProject pid current new) = void $ pexec
[sql| INSERT INTO project_companions (project_id, user_id, invited_by) VALUES (?, ?, ?) |]
(pid ^. _ProjectId, new ^. _UserId, current ^. _UserId)
(uid, uid)
pgEval (AddUserToProject pid current new) =
void $
pexec
[sql| INSERT INTO project_companions (project_id, user_id, invited_by) VALUES (?, ?, ?) |]
(pid ^. _ProjectId, new ^. _UserId, current ^. _UserId)
( b ^. (B.project . _ProjectId)
, eventId ^. _EventId
, b ^. B.name
, b ^. B.description
, b ^. (B.recurrence . to B.recurrenceName)
, b ^. (B.recurrence . to B.recurrenceCount)
, b ^. (B.amount . satoshi)
, b ^. (B.gracePeriod)
, b ^. (B.paymentRequestEmailTemplate)
, b ^. (B.paymentRequestMemoTemplate)
( b ^. (B.project . _ProjectId),
eventId ^. _EventId,
b ^. B.name,
b ^. B.description,
b ^. (B.recurrence . to B.recurrenceName),
b ^. (B.recurrence . to B.recurrenceCount),
b ^. (B.amount . satoshi),
b ^. (B.gracePeriod),
b ^. (B.paymentRequestEmailTemplate),
b ^. (B.paymentRequestMemoTemplate)
pgEval (FindBillable bid) = headMay <$> pquery
billableParser
[sql| SELECT b.project_id, e.created_by, b.name, b.description,
pgEval (FindBillable bid) =
headMay
<$> pquery
billableParser
[sql| SELECT b.project_id, e.created_by, b.name, b.description,
(Only (bid ^. B._BillableId))
pgEval (FindBillables pid) = pquery
((,) <$> idParser B.BillableId <*> billableParser)
[sql| SELECT b.id, b.project_id, e.created_by, b.name, b.description,
(Only (bid ^. B._BillableId))
pgEval (FindBillables pid) =
pquery
((,) <$> idParser B.BillableId <*> billableParser)
[sql| SELECT b.id, b.project_id, e.created_by, b.name, b.description,
( view _UserId uid
, view B._BillableId bid
, view _EventId eventId
, fromThyme start_date
( view _UserId uid,
view B._BillableId bid,
view _EventId eventId,
fromThyme start_date
pgEval (FindSubscription sid) = headMay <$> pquery
subscriptionParser
[sql| SELECT id, billable_id, contact_email, start_date, end_date
pgEval (FindSubscription sid) =
headMay
<$> pquery
subscriptionParser
[sql| SELECT id, billable_id, contact_email, start_date, end_date
(Only (sid ^. B._SubscriptionId))
pgEval (FindSubscriptions uid pid) = pquery
((,) <$> idParser B.SubscriptionId <*> subscriptionParser)
[sql| SELECT s.id, user_id, billable_id, contact_email, start_date, end_date
(Only (sid ^. B._SubscriptionId))
pgEval (FindSubscriptions uid pid) =
pquery
((,) <$> idParser B.SubscriptionId <*> subscriptionParser)
[sql| SELECT s.id, user_id, billable_id, contact_email, start_date, end_date
( req ^. (subscription . B._SubscriptionId)
, eventId ^. _EventId
, req ^. (paymentRequest . to (runPut . encodeMessage))
, req ^. (paymentKey . _PaymentKey)
, req ^. (paymentRequestTime . to fromThyme)
, req ^. (billingDate . to fromThyme)
( req ^. (subscription . B._SubscriptionId),
eventId ^. _EventId,
req ^. (paymentRequest . to (runPut . encodeMessage)),
req ^. (paymentKey . _PaymentKey),
req ^. (paymentRequestTime . to fromThyme),
req ^. (billingDate . to fromThyme)
pgEval (FindPaymentRequest (PaymentKey k)) = headMay <$> pquery
((,) <$> idParser PaymentRequestId <*> paymentRequestParser)
[sql| SELECT id, subscription_id, request_data, url_key, request_time, billing_date
pgEval (FindPaymentRequest (PaymentKey k)) =
headMay
<$> pquery
((,) <$> idParser PaymentRequestId <*> paymentRequestParser)
[sql| SELECT id, subscription_id, request_data, url_key, request_time, billing_date
(Only k)
pgEval (FindPaymentRequestId (PaymentRequestId prid)) = headMay <$> pquery
paymentRequestParser
[sql| SELECT subscription_id, request_data, url_key, request_time, billing_date
(Only k)
pgEval (FindPaymentRequestId (PaymentRequestId prid)) =
headMay
<$> pquery
paymentRequestParser
[sql| SELECT subscription_id, request_data, url_key, request_time, billing_date
(Only prid)
pgEval (FindPaymentRequests sid) = pquery
((,) <$> idParser PaymentRequestId <*> paymentRequestParser)
[sql| SELECT id, subscription_id, request_data, url_key, request_time, billing_date
(Only prid)
pgEval (FindPaymentRequests sid) =
pquery
((,) <$> idParser PaymentRequestId <*> paymentRequestParser)
[sql| SELECT id, subscription_id, request_data, url_key, request_time, billing_date
( p ^. (request . _PaymentRequestId)
, eventId ^. _EventId
, p ^. (payment . to (runPut . encodeMessage))
, p ^. (paymentDate . to fromThyme)
, p ^. exchangeRates
( p ^. (request . _PaymentRequestId),
eventId ^. _EventId,
p ^. (payment . to (runPut . encodeMessage)),
p ^. (paymentDate . to fromThyme),
p ^. exchangeRates
pgEval (FindPayments rid) = pquery
((,) <$> idParser PaymentId <*> paymentParser)
[sql| SELECT id, payment_request_id, payment_data, payment_date
pgEval (FindPayments rid) =
pquery
((,) <$> idParser PaymentId <*> paymentParser)
[sql| SELECT id, payment_request_id, payment_data, payment_date
(Only (rid ^. _PaymentRequestId))
(Only (rid ^. _PaymentRequestId))
{-# LANGUAGE ExplicitForAll #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE ExplicitForAll #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
import Control.Lens ( view
, (^.)
, makeClassyPrisms
, traverseOf
)
import Data.AffineSpace
import Data.Thyme.Clock as C
import Data.Thyme.Time as T
( Day )
import Safe ( headMay )
import Aftok.Types
import Aftok.Auction as A
import Aftok.Billing as B
import Aftok.Currency.Bitcoin ( NetworkId )
import Aftok.Interval
import Aftok.Payments.Types
import Aftok.Project as P
import Aftok.TimeLog
import Aftok.Util
import Aftok.Auction as A
import Aftok.Billing as B
import Aftok.Currency.Bitcoin (NetworkId)
import Aftok.Interval
import Aftok.Payments.Types
import Aftok.Project as P
import Aftok.TimeLog
import Aftok.Types
import Aftok.Util
import Control.Lens
( (^.),
makeClassyPrisms,
traverseOf,
view,
)
import Data.AffineSpace
import Data.Thyme.Clock as C
import Data.Thyme.Time as T
( Day,
)
import Haskoin.Address (Address)
import Safe (headMay)
CreateUser :: User -> DBOp UserId
FindUser :: UserId -> DBOp (Maybe User)
FindUserByName :: UserName -> DBOp (Maybe (UserId, User))
CreateUser :: User -> DBOp UserId
FindUser :: UserId -> DBOp (Maybe User)
FindUserByName :: UserName -> DBOp (Maybe (UserId, User))
CreateProject :: Project -> DBOp ProjectId
FindProject :: ProjectId -> DBOp (Maybe Project)
ListProjects :: DBOp [ProjectId]
FindSubscribers :: ProjectId -> DBOp [UserId]
CreateProject :: Project -> DBOp ProjectId
FindProject :: ProjectId -> DBOp (Maybe Project)
ListProjects :: DBOp [ProjectId]
FindSubscribers :: ProjectId -> DBOp [UserId]
CreateEvent :: ProjectId -> UserId -> LogEntry BTCNet -> DBOp EventId
AmendEvent :: EventId -> EventAmendment BTCNet -> DBOp AmendmentId
FindEvent :: EventId -> DBOp (Maybe (KeyedLogEntry BTCNet))
FindEvents :: ProjectId -> UserId -> RangeQuery -> Word32 -> DBOp [LogEntry BTCNet]
ReadWorkIndex :: ProjectId -> DBOp (WorkIndex BTCNet)
CreateAuction :: Auction -> DBOp AuctionId
FindAuction :: AuctionId -> DBOp (Maybe Auction)
CreateBid :: AuctionId -> Bid -> DBOp BidId
FindBids :: AuctionId -> DBOp [(BidId, Bid)]
CreateBillable :: UserId -> Billable -> DBOp BillableId
FindBillable :: BillableId -> DBOp (Maybe Billable)
FindBillables :: ProjectId -> DBOp [(BillableId, Billable)]
CreateEvent :: ProjectId -> UserId -> LogEntry BTCNet -> DBOp EventId
AmendEvent :: EventId -> EventAmendment BTCNet -> DBOp AmendmentId
FindEvent :: EventId -> DBOp (Maybe (KeyedLogEntry BTCNet))
FindEvents :: ProjectId -> UserId -> RangeQuery -> Word32 -> DBOp [LogEntry BTCNet]
ReadWorkIndex :: ProjectId -> DBOp (WorkIndex BTCNet)
CreateAuction :: Auction -> DBOp AuctionId
FindAuction :: AuctionId -> DBOp (Maybe Auction)
CreateBid :: AuctionId -> Bid -> DBOp BidId
FindBids :: AuctionId -> DBOp [(BidId, Bid)]
CreateBillable :: UserId -> Billable -> DBOp BillableId
FindBillable :: BillableId -> DBOp (Maybe Billable)
FindBillables :: ProjectId -> DBOp [(BillableId, Billable)]
FindSubscription :: SubscriptionId -> DBOp (Maybe Subscription)
FindSubscriptions :: UserId -> ProjectId -> DBOp [(SubscriptionId, Subscription)]
CreatePaymentRequest :: PaymentRequest -> DBOp PaymentRequestId
FindPaymentRequests :: SubscriptionId -> DBOp [(PaymentRequestId, PaymentRequest)]
FindUnpaidRequests :: SubscriptionId -> DBOp [BillDetail]
FindPaymentRequest :: PaymentKey -> DBOp (Maybe (PaymentRequestId, PaymentRequest))
FindPaymentRequestId :: PaymentRequestId -> DBOp (Maybe PaymentRequest)
CreatePayment :: Payment -> DBOp PaymentId
FindPayments :: PaymentRequestId -> DBOp [(PaymentId, Payment)]
FindSubscription :: SubscriptionId -> DBOp (Maybe Subscription)
FindSubscriptions :: UserId -> ProjectId -> DBOp [(SubscriptionId, Subscription)]
CreatePaymentRequest :: PaymentRequest -> DBOp PaymentRequestId
FindPaymentRequests :: SubscriptionId -> DBOp [(PaymentRequestId, PaymentRequest)]
FindUnpaidRequests :: SubscriptionId -> DBOp [BillDetail]
FindPaymentRequest :: PaymentKey -> DBOp (Maybe (PaymentRequestId, PaymentRequest))
FindPaymentRequestId :: PaymentRequestId -> DBOp (Maybe PaymentRequest)
CreatePayment :: Payment -> DBOp PaymentId
FindPayments :: PaymentRequestId -> DBOp [(PaymentId, Payment)]
RaiseDBError :: forall x y. DBError -> DBOp x -> DBOp y
data OpForbiddenReason = UserNotProjectMember
| UserNotEventLogger
| UserNotSubscriber SubscriptionId
| InvitationExpired
| InvitationAlreadyAccepted
| AuctionEnded
deriving (Eq, Show, Typeable)
data DBError
= OpForbidden UserId OpForbiddenReason
| SubjectNotFound
| EventStorageFailed
deriving (Eq, Show, Typeable)
Just i | t .-. (i ^. invitationTime) > fromSeconds (60 * 60 * 72 :: Int) ->
raiseOpForbidden uid InvitationExpired act
Just i | isJust (i ^. acceptanceTime) ->
raiseOpForbidden uid InvitationAlreadyAccepted act
Just i
| t .-. (i ^. invitationTime) > fromSeconds (60 * 60 * 72 :: Int) ->
raiseOpForbidden uid InvitationExpired act
Just i
| isJust (i ^. acceptanceTime) ->
raiseOpForbidden uid InvitationAlreadyAccepted act
missing = raiseSubjectNotFound act
maybe missing
(\(_, uid', _) -> if uid' == uid then liftdb act else forbidden)
ev
missing = raiseSubjectNotFound act
maybe
missing
(\(_, uid', _) -> if uid' == uid then liftdb act else forbidden)
ev
( Interval(..)
, interval
, start
, end
, ilen
, RangeQuery(..)
, rangeQuery
, start'
, end'
, intervalJSON
, parseIntervalJSON
, containsInclusive
( Interval (..),
interval,
start,
end,
ilen,
RangeQuery (..),
rangeQuery,
start',
end',
intervalJSON,
parseIntervalJSON,
containsInclusive,
import Control.Lens ( makeLenses
, (^.)
)
import Data.Aeson
import Data.Aeson.Types
import Data.AffineSpace
import Data.Thyme.Clock as C
import Data.Thyme.Format.Aeson ( )
import Data.Thyme.LocalTime ( )
data Interval
= Interval
{ _start :: C.UTCTime,
_end :: C.UTCTime
}
deriving (Show, Eq, Ord)
{-# LANGUAGE ExplicitForAll #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE ExplicitForAll #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
import Control.FromSum ( fromMaybeM
, fromEitherM
)
import Control.Lens hiding ( (.=) )
import qualified Control.Lens as L
import Data.Aeson
import Data.Aeson.Types
import qualified Data.Attoparsec.ByteString.Char8
as PC
import qualified Data.ByteString.Base64 as B64
import qualified Data.ByteString.Char8 as C
import Data.Data
import Data.HashMap.Strict as O
import Data.List.NonEmpty as L
import Data.Map.Strict as MS
import Data.ProtocolBuffers ( encodeMessage )
import Data.Serialize.Put ( runPut )
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import Data.Thyme.Calendar ( showGregorian )
import Data.Thyme.Clock as Clock
import Data.Thyme.Time ( Day )
import Data.UUID as U
import Haskoin.Address ( Address
, addrToJSON
, addrFromJSON
, textToAddr
)
import Aftok.Currency.Bitcoin
import Aftok.Auction as A
import qualified Aftok.Billing as B
import Aftok.Interval
import Aftok.Payments
import Aftok.Project as P
import Aftok.TimeLog
import Aftok.Types
import Aftok.Util ( traverseKeys )
import qualified Language.Haskell.TH as TH
import Language.Haskell.TH.Quote
import Aftok.Auction as A
import qualified Aftok.Billing as B
import Aftok.Currency.Bitcoin
import Aftok.Interval
import Aftok.Payments
import Aftok.Project as P
import Aftok.TimeLog
import Aftok.Types
import Aftok.Util (traverseKeys)
import Control.FromSum
( fromEitherM,
fromMaybeM,
)
import Control.Lens hiding ((.=))
import qualified Control.Lens as L
import Data.Aeson
import Data.Aeson.Types
import qualified Data.Attoparsec.ByteString.Char8 as PC
import qualified Data.ByteString.Base64 as B64
import qualified Data.ByteString.Char8 as C
import Data.Data
import Data.HashMap.Strict as O
import Data.List.NonEmpty as L
import Data.Map.Strict as MS
import Data.ProtocolBuffers (encodeMessage)
import Data.Serialize.Put (runPut)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import Data.Thyme.Calendar (showGregorian)
import Data.Thyme.Clock as Clock
import Data.Thyme.Time (Day)
import Data.UUID as U
import Haskoin.Address
( Address,
addrFromJSON,
addrToJSON,
textToAddr,
)
import qualified Language.Haskell.TH as TH
import Language.Haskell.TH.Quote
v = QuasiQuoter
{ quoteExp = quoteVersionExp
, quotePat = error "Pattern quasiquotation of versions not supported."
, quoteType = error "Type quasiquotation of versions not supported."
, quoteDec = error "Dec quasiquotation of versions not supported."
}
v =
QuasiQuoter
{ quoteExp = quoteVersionExp,
quotePat = error "Pattern quasiquotation of versions not supported.",
quoteType = error "Type quasiquotation of versions not supported.",
quoteDec = error "Dec quasiquotation of versions not supported."
}
{-|
- Convenience function to allow dispatch of different serialized
- versions to different parsers.
-}
-- |
-- - Convenience function to allow dispatch of different serialized
-- - versions to different parsers.
projectJSON p = v1 $ obj
[ "projectName" .= (p ^. projectName)
, "inceptionDate" .= (p ^. inceptionDate)
, "initiator" .= (p ^. P.initiator . _UserId)
]
projectJSON p =
v1 $
obj
[ "projectName" .= (p ^. projectName),
"inceptionDate" .= (p ^. inceptionDate),
"initiator" .= (p ^. P.initiator . _UserId)
]
auctionJSON x = v1 $ obj
[ "projectId" .= idValue (A.projectId . _ProjectId) x
, "initiator" .= idValue (A.initiator . _UserId) x
, "raiseAmount" .= (x ^. (raiseAmount . satoshi))
]
auctionJSON x =
v1 $
obj
[ "projectId" .= idValue (A.projectId . _ProjectId) x,
"initiator" .= idValue (A.initiator . _UserId) x,
"raiseAmount" .= (x ^. (raiseAmount . satoshi))
]
creditToJSON nmode (CreditToCurrency (netId, addr)) = v2 $ obj
[ "creditToAddress" .= addrToJSON (toNetwork nmode netId) addr
, "creditToNetwork" .= renderNetworkId netId
]
creditToJSON nmode (CreditToCurrency (netId, addr)) =
v2 $
obj
[ "creditToAddress" .= addrToJSON (toNetwork nmode netId) addr,
"creditToNetwork" .= renderNetworkId netId
]
parseBtcAddr
:: NetworkMode -> NetworkId -> Text -> Parser (CreditTo (NetworkId, Address))
parseBtcAddr nmode net addrText = maybe
( fail
. T.unpack
$ "Address "
<> addrText
<> " cannot be parsed as a BTC network address."
)
(pure . CreditToCurrency . (net, ))
(textToAddr (toNetwork nmode net) addrText)
parseBtcAddr ::
NetworkMode -> NetworkId -> Text -> Parser (CreditTo (NetworkId, Address))
parseBtcAddr nmode net addrText =
maybe
( fail
. T.unpack
$ "Address "
<> addrText
<> " cannot be parsed as a BTC network address."
)
(pure . CreditToCurrency . (net,))
(textToAddr (toNetwork nmode net) addrText)
let
parseCreditToAddr = do
netName <- o .: "creditToNetwork"
net <- fromMaybeM
(fail . T.unpack $ "Currency network " <> netName <> " not recognized.")
(parseNetworkId netName)
addrValue <- o .: "creditToAddress"
CreditToCurrency
. (net, )
<$> addrFromJSON (toNetwork nmode net) addrValue
let parseCreditToAddr = do
netName <- o .: "creditToNetwork"
net <-
fromMaybeM
(fail . T.unpack $ "Currency network " <> netName <> " not recognized.")
(parseNetworkId netName)
addrValue <- o .: "creditToAddress"
CreditToCurrency
. (net,)
<$> addrFromJSON (toNetwork nmode net) addrValue
parseCreditToUser =
fmap CreditToUser . parseId _UserId =<< o .: "creditToUser"
parseCreditToProject =
fmap CreditToProject . parseId _ProjectId =<< o .: "creditToProject"
notFound =
fail $ "Value " <> show o <> " does not represent a CreditTo value."
in parseCreditToAddr
<|> parseCreditToUser
<|> parseCreditToProject
<|> notFound
parseCreditToUser =
fmap CreditToUser . parseId _UserId =<< o .: "creditToUser"
parseCreditToProject =
fmap CreditToProject . parseId _ProjectId =<< o .: "creditToProject"
notFound =
fail $ "Value " <> show o <> " does not represent a CreditTo value."
in
parseCreditToAddr
<|> parseCreditToUser
<|> parseCreditToProject
<|> notFound
v2
$ let payoutsRec :: (CreditTo (NetworkId, Address), Rational) -> Value
payoutsRec (c, r) =
object ["creditTo" .= creditToJSON nmode c, "payoutRatio" .= r]
in obj $ ["payouts" .= fmap payoutsRec (MS.assocs m)]
parsePayoutsJSON
:: NetworkMode -> Value -> Parser (Payouts (NetworkId, Address))
parsePayoutsJSON nmode = unversion "Payouts" $ p where
p :: Version -> Object -> Parser (Payouts (NetworkId, Address))
p (Version 1 _) val = Payouts <$> join
(traverseKeys (parseBtcAddr nmode BTC) <$> parseJSON (Object val))
v2 $
let payoutsRec :: (CreditTo (NetworkId, Address), Rational) -> Value
payoutsRec (c, r) =
object ["creditTo" .= creditToJSON nmode c, "payoutRatio" .= r]
in obj $ ["payouts" .= fmap payoutsRec (MS.assocs m)]
p (Version 2 0) val =
let parsePayoutRecord x =
parsePayoutsJSON ::
NetworkMode -> Value -> Parser (Payouts (NetworkId, Address))
parsePayoutsJSON nmode = unversion "Payouts" $ p
where
p :: Version -> Object -> Parser (Payouts (NetworkId, Address))
p (Version 1 _) val =
Payouts
<$> join
(traverseKeys (parseBtcAddr nmode BTC) <$> parseJSON (Object val))
p (Version 2 0) val =
let parsePayoutRecord x =
workIndexJSON nmode (WorkIndex widx) = v2
$ obj ["workIndex" .= fmap widxRec (MS.assocs widx)]
where
widxRec :: (CreditTo (NetworkId, Address), NonEmpty Interval) -> Value
widxRec (c, l) = object
[ "creditTo" .= creditToJSON nmode c
, "intervals" .= (intervalJSON <$> L.toList l)
]
workIndexJSON nmode (WorkIndex widx) =
v2 $
obj ["workIndex" .= fmap widxRec (MS.assocs widx)]
where
widxRec :: (CreditTo (NetworkId, Address), NonEmpty Interval) -> Value
widxRec (c, l) =
object
[ "creditTo" .= creditToJSON nmode c,
"intervals" .= (intervalJSON <$> L.toList l)
]
[ "projectId" .= idValue (B.project . _ProjectId) b
, "name" .= (b ^. B.name)
, "description" .= (b ^. B.description)
, "recurrence" .= recurrenceJSON' (b ^. B.recurrence)
, "amount" .= (b ^. (B.amount . satoshi))
, "gracePeriod" .= (b ^. B.gracePeriod)
, "requestExpiryPeriod" .= (Clock.toSeconds' <$> (b ^. B.requestExpiryPeriod))
[ "projectId" .= idValue (B.project . _ProjectId) b,
"name" .= (b ^. B.name),
"description" .= (b ^. B.description),
"recurrence" .= recurrenceJSON' (b ^. B.recurrence),
"amount" .= (b ^. (B.amount . satoshi)),
"gracePeriod" .= (b ^. B.gracePeriod),
"requestExpiryPeriod" .= (Clock.toSeconds' <$> (b ^. B.requestExpiryPeriod))
recurrenceJSON' (B.Weekly i) = object ["weekly " .= object ["weeks" .= i]]
recurrenceJSON' B.OneTime = object ["onetime" .= Null]
recurrenceJSON' (B.Weekly i) = object ["weekly " .= object ["weeks" .= i]]
recurrenceJSON' B.OneTime = object ["onetime" .= Null]
createSubscriptionJSON uid bid d = v1 $ obj
[ "user_id" .= idValue _UserId uid
, "billable_id" .= idValue B._BillableId bid
, "start_date" .= showGregorian d
]
createSubscriptionJSON uid bid d =
v1 $
obj
[ "user_id" .= idValue _UserId uid,
"billable_id" .= idValue B._BillableId bid,
"start_date" .= showGregorian d
]
[ "user_id" .= idValue (B.customer . _UserId) sub
, "billable_id" .= idValue (B.billable . B._BillableId) sub
, "start_time" .= view B.startTime sub
, "end_time" .= view B.endTime sub
[ "user_id" .= idValue (B.customer . _UserId) sub,
"billable_id" .= idValue (B.billable . B._BillableId) sub,
"start_time" .= view B.startTime sub,
"end_time" .= view B.endTime sub
[ "subscription_id" .= idValue (subscription . B._SubscriptionId) r
, "payment_request_protobuf_64" .= view prBytes r
, "url_key" .= view (paymentKey . _PaymentKey) r
, "payment_request_time" .= view paymentRequestTime r
, "billing_date" .= view (billingDate . to showGregorian) r
[ "subscription_id" .= idValue (subscription . B._SubscriptionId) r,
"payment_request_protobuf_64" .= view prBytes r,
"url_key" .= view (paymentKey . _PaymentKey) r,
"payment_request_time" .= view paymentRequestTime r,
"billing_date" .= view (billingDate . to showGregorian) r
billDetailJSON r = obj $ concat
[ ["payment_request_id" .= view (_1 . _PaymentKey) r]
, paymentRequestKV $ view _2 r
, subscriptionKV $ view _3 r
, billableKV $ view _4 r
]
billDetailJSON r =
obj $
concat
[ ["payment_request_id" .= view (_1 . _PaymentKey) r],
paymentRequestKV $ view _2 r,
subscriptionKV $ view _3 r,
billableKV $ view _4 r
]
paymentJSON r = v1 $ obj
[ "payment_request_id" .= idValue (request . _PaymentRequestId) r
, "payment_protobuf_64" .= view paymentBytes r
, "payment_date" .= (r ^. paymentDate)
]
where
paymentBytes =
payment . to (T.decodeUtf8 . B64.encode . runPut . encodeMessage)
paymentJSON r =
v1 $
obj
[ "payment_request_id" .= idValue (request . _PaymentRequestId) r,
"payment_protobuf_64" .= view paymentBytes r,
"payment_date" .= (r ^. paymentDate)
]
where
paymentBytes =
payment . to (T.decodeUtf8 . B64.encode . runPut . encodeMessage)
parseEventAmendment
:: NetworkMode
-> ModTime
-> Value
-> Parser (EventAmendment (NetworkId, Address))
parseEventAmendment nmode t = unversion "EventAmendment" $ p where
p (Version 1 _) = parseEventAmendmentV1 nmode t
p (Version 2 0) = parseEventAmendmentV2 nmode t
p ver = badVersion "EventAmendment" ver
parseEventAmendment ::
NetworkMode ->
ModTime ->
Value ->
Parser (EventAmendment (NetworkId, Address))
parseEventAmendment nmode t = unversion "EventAmendment" $ p
where
p (Version 1 _) = parseEventAmendmentV1 nmode t
p (Version 2 0) = parseEventAmendmentV2 nmode t
p ver = badVersion "EventAmendment" ver
let
parseA :: Text -> Parser (EventAmendment (NetworkId, Address))
parseA "timeChange" = TimeChange t <$> o .: "eventTime"
parseA "addrChange" = CreditToChange t <$> parseCreditToV1 nmode o
parseA "metadataChange" = MetadataChange t <$> o .: "eventMeta"
parseA tid =
fail . T.unpack $ "Amendment type " <> tid <> " not recognized."
in
o .: "amendment" >>= parseA
let parseA :: Text -> Parser (EventAmendment (NetworkId, Address))
parseA "timeChange" = TimeChange t <$> o .: "eventTime"
parseA "addrChange" = CreditToChange t <$> parseCreditToV1 nmode o
parseA "metadataChange" = MetadataChange t <$> o .: "eventMeta"
parseA tid =
fail . T.unpack $ "Amendment type " <> tid <> " not recognized."
in o .: "amendment" >>= parseA
let
parseA :: Text -> Parser (EventAmendment (NetworkId, Address))
parseA "timeChange" = TimeChange t <$> o .: "eventTime"
parseA "creditToChange" = CreditToChange t <$> parseCreditToV2 nmode o
parseA "metadataChange" = MetadataChange t <$> o .: "eventMeta"
parseA tid =
fail . T.unpack $ "Amendment type " <> tid <> " not recognized."
in
o .: "amendment" >>= parseA
let parseA :: Text -> Parser (EventAmendment (NetworkId, Address))
parseA "timeChange" = TimeChange t <$> o .: "eventTime"
parseA "creditToChange" = CreditToChange t <$> parseCreditToV2 nmode o
parseA "metadataChange" = MetadataChange t <$> o .: "eventMeta"
parseA tid =
fail . T.unpack $ "Amendment type " <> tid <> " not recognized."
in o .: "amendment" >>= parseA
parseLogEntry
:: NetworkMode
-> UserId
-> (UTCTime -> LogEvent)
-> Value
-> Parser (UTCTime -> (LogEntry (NetworkId, Address)))
parseLogEntry nmode uid f = unversion "LogEntry" p where
p (Version 2 0) o = do
creditTo' <- o .:? "creditTo" >>= maybe (pure $ CreditToUser uid)
(parseCreditToV2 nmode)
eventMeta' <- o .:? "eventMeta"
pure $ \t -> LogEntry creditTo' (f t) eventMeta'
parseLogEntry ::
NetworkMode ->
UserId ->
(UTCTime -> LogEvent) ->
Value ->
Parser (UTCTime -> (LogEntry (NetworkId, Address)))
parseLogEntry nmode uid f = unversion "LogEntry" p
where
p (Version 2 0) o = do
creditTo' <-
o .:? "creditTo"
>>= maybe
(pure $ CreditToUser uid)
(parseCreditToV2 nmode)
eventMeta' <- o .:? "eventMeta"
pure $ \t -> LogEntry creditTo' (f t) eventMeta'
p ver o = badVersion "LogEntry" ver 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'
notFound =
fail $ "Value " <> show o <> " does not represent a Recurrence value."
parseV val =
parseAnnually val
<|> parseMonthly val
<|> parseWeekly val
<|> parseOneTime val
in
fromMaybe notFound $ parseV 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'
notFound =
fail $ "Value " <> show o <> " does not represent a Recurrence value."
parseV val =
parseAnnually val
<|> parseMonthly val
<|> parseWeekly val
<|> parseOneTime val
in fromMaybe notFound $ parseV o
import Aftok.Billing
( Billable,
Subscription,
SubscriptionId,
)
import qualified Bippy.Proto as P
import Bippy.Types
( Satoshi (..),
expiryTime,
getExpires,
getPaymentDetails,
)
import Control.Lens
( makeLenses,
makePrisms,
view,
)
import Data.Aeson (Value)
import qualified Data.Text as T
import Data.Thyme.Clock as C
import Data.Thyme.Time as C
import Data.UUID
import Haskoin.Address.Base58 (decodeBase58Check)
import Control.Lens ( makeLenses
, makePrisms
, view
)
import Data.Aeson ( Value )
import Data.Thyme.Clock as C
import Data.Thyme.Time as C
import qualified Data.Text as T
import Data.UUID
import qualified Bippy.Proto as P
import Bippy.Types ( expiryTime
, getExpires
, getPaymentDetails
, Satoshi(..)
)
import Haskoin.Address.Base58 ( decodeBase58Check )
import Aftok.Billing ( Billable
, Subscription
, SubscriptionId
)
newtype PaymentRequestId = PaymentRequestId UUID deriving (Show, Eq)
data PaymentRequest' s = PaymentRequest
{ _subscription :: s
, _paymentRequest :: P.PaymentRequest
, _paymentKey :: PaymentKey
, _paymentRequestTime :: C.UTCTime
, _billingDate :: C.Day
} deriving (Functor, Foldable, Traversable)
data PaymentRequest' s
= PaymentRequest
{ _subscription :: s,
_paymentRequest :: P.PaymentRequest,
_paymentKey :: PaymentKey,
_paymentRequestTime :: C.UTCTime,
_billingDate :: C.Day
}
deriving (Functor, Foldable, Traversable)
data Payment' r = Payment
{ _request :: r
, _payment :: P.Payment
, _paymentDate :: C.UTCTime
, _exchangeRates :: Maybe Value
} deriving (Functor, Foldable, Traversable)
data Payment' r
= Payment
{ _request :: r,
_payment :: P.Payment,
_paymentDate :: C.UTCTime,
_exchangeRates :: Maybe Value
}
deriving (Functor, Foldable, Traversable)
-- using error here is reasonable since it would indicate
-- a serialization problem
in either (error . T.pack) (check . getExpires)
$ getPaymentDetails (view paymentRequest req)
in -- using error here is reasonable since it would indicate
-- a serialization problem
either (error . T.pack) (check . getExpires) $
getPaymentDetails (view paymentRequest req)
import Aftok.Billing
import Aftok.Currency.Bitcoin
( NetworkId (..),
NetworkMode,
satoshi,
toNetwork,
)
import Aftok.Database
import Aftok.Payments.Types
import Aftok.Project (depf)
import qualified Aftok.TimeLog as TL
import Aftok.Types
( ProjectId,
UserId,
)
import qualified Bippy as B
import qualified Bippy.Proto as P
import qualified Bippy.Types as BT
import Control.Error.Util (maybeT)
import Control.Lens
( (%~),
(^.),
makeClassy,
makeClassyPrisms,
review,
traverseOf,
view,
)
import Control.Lens.Tuple
import Control.Monad.Except
( MonadError,
throwError,
)
import qualified Crypto.PubKey.RSA.Types as RSA
( Error (..),
PrivateKey,
)
import Crypto.Random.Types
( MonadRandom,
getRandomBytes,
)
import Data.AffineSpace ((.+^))
import Data.Map.Strict (assocs)
import Data.Thyme.Clock as C
import Data.Thyme.Time as T
import Haskoin.Address (Address (..))
import Haskoin.Address.Base58 (encodeBase58Check)
import Haskoin.Script (ScriptOutput (..))
import Network.URI
import Control.Error.Util ( maybeT )
import Control.Lens ( makeClassy
, makeClassyPrisms
, review
, view
, (%~)
, (^.)
, traverseOf
)
import Control.Lens.Tuple
import Control.Monad.Except ( MonadError
, throwError
)
import qualified Crypto.PubKey.RSA.Types as RSA
( Error(..)
, PrivateKey
)
import Crypto.Random.Types ( MonadRandom
, getRandomBytes
)
import Data.AffineSpace ( (.+^) )
import Data.Map.Strict ( assocs )
import Data.Thyme.Clock as C
import Data.Thyme.Time as T
import qualified Bippy as B
import qualified Bippy.Proto as P
import qualified Bippy.Types as BT
import Haskoin.Address ( Address(..) )
import Haskoin.Address.Base58 ( encodeBase58Check )
import Haskoin.Script ( ScriptOutput(..) )
import Network.URI
import Aftok.Types ( UserId
, ProjectId
)
import Aftok.Billing
import Aftok.Currency.Bitcoin ( NetworkId(..)
, NetworkMode
, satoshi
, toNetwork
)
import Aftok.Database
import Aftok.Payments.Types
import Aftok.Project ( depf )
import qualified Aftok.TimeLog as TL
data PaymentsConfig
= PaymentsConfig
{ _networkMode :: !NetworkMode,
_signingKey :: !RSA.PrivateKey,
_pkiData :: !BT.PKIData
}
data PaymentsConfig = PaymentsConfig
{ _networkMode :: !NetworkMode
, _signingKey :: !RSA.PrivateKey
, _pkiData :: !BT.PKIData
}
data BillingOps (m :: * -> *) = BillingOps
{ -- | generator for user memo
memoGen :: Subscription' UserId Billable -- ^ subscription being billed
-> T.Day -- ^ billing date
-> C.UTCTime -- ^ payment request generation time
-> m (Maybe Text)
-- | generator for payment response URL
, uriGen :: PaymentKey -- ^ payment key to be included in the URL
-> m (Maybe URI)
-- | generator for merchant payload
, payloadGen :: Subscription' UserId Billable -- ^ subscription being billed
-> T.Day -- ^ billing date
-> C.UTCTime -- ^ payment request generation time
-> m (Maybe ByteString)
}
data BillingOps (m :: * -> *)
= BillingOps
{ -- | generator for user memo
memoGen ::
Subscription' UserId Billable -> -- subscription being billed
T.Day -> -- billing date
C.UTCTime -> -- payment request generation time
m (Maybe Text),
-- | generator for payment response URL
uriGen ::
PaymentKey -> -- payment key to be included in the URL
m (Maybe URI),
-- | generator for merchant payload
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
= -- | the request was paid with the specified payment
Paid !Payment
| -- | the request has not been paid, but has not yet expired
Unpaid !PaymentRequest
| -- | the request was not paid prior to the expiration date
Expired !PaymentRequest
createPaymentRequests
:: ( MonadRandom m
, MonadReader r m
, HasPaymentsConfig r
, MonadError e m
, AsPaymentError e
, MonadDB m
)
=> BillingOps m -- ^ generators for payment request components
-> C.UTCTime -- ^ timestamp for payment request creation
-> UserId -- ^ customer responsible for payment
-> ProjectId -- ^ project whose worklog is to be paid
-> m [PaymentRequestId]
createPaymentRequests ::
( MonadRandom m,
MonadReader r m,
HasPaymentsConfig r,
MonadError e m,
AsPaymentError e,
MonadDB m
) =>
-- | generators for payment request components
BillingOps m ->
-- | timestamp for payment request creation
C.UTCTime ->
-- | customer responsible for payment
UserId ->
-- | project whose worklog is to be paid
ProjectId ->
m [PaymentRequestId]
createSubscriptionPaymentRequests
:: ( MonadRandom m
, MonadReader r m
, HasPaymentsConfig r
, MonadError e m
, AsPaymentError e
, MonadDB m
)
=> BillingOps m
-> C.UTCTime
-> (SubscriptionId, Subscription)
-> m [PaymentRequestId]
createSubscriptionPaymentRequests ::
( MonadRandom m,
MonadReader r m,
HasPaymentsConfig r,
MonadError e m,
AsPaymentError e,
MonadDB m
) =>
BillingOps m ->
C.UTCTime ->
(SubscriptionId, Subscription) ->
m [PaymentRequestId]
createPaymentRequest
:: ( MonadRandom m
, MonadReader r m
, HasPaymentsConfig r
, MonadError e m
, AsPaymentError e
, MonadDB m
)
=> BillingOps m
-> C.UTCTime
-> SubscriptionId
-> Subscription' UserId Billable
-> T.Day
-> m PaymentRequestId
createPaymentRequest ::
( MonadRandom m,
MonadReader r m,
HasPaymentsConfig r,
MonadError e m,
AsPaymentError e,
MonadDB m
) =>
BillingOps m ->
C.UTCTime ->
SubscriptionId ->
Subscription' UserId Billable ->
T.Day ->
m PaymentRequestId
pkey <- PaymentKey . encodeBase58Check <$> getRandomBytes 32
memo <- memoGen ops sub bday now
uri <- uriGen ops pkey
pkey <- PaymentKey . encodeBase58Check <$> getRandomBytes 32
memo <- memoGen ops sub bday now
uri <- uriGen ops pkey
reqErr <- B.createPaymentRequest (cfg ^. signingKey) (cfg ^. pkiData) details
req <- either (throwError . review _SigningError) pure reqErr
reqErr <- B.createPaymentRequest (cfg ^. signingKey) (cfg ^. pkiData) details
req <- either (throwError . review _SigningError) pure reqErr
findUnbilledDates
:: (MonadDB m, MonadError e m, AsPaymentError e)
=> C.UTCTime -- ^ the date against which payment request expiration should be checked
-> Billable
-> [(PaymentRequestId, PaymentRequest)] -- ^ the list of existing payment requests
-> [T.Day] -- ^ the list of expected billing days
-> m [T.Day] -- ^ the list of billing days for which no payment request exists
findUnbilledDates ::
(MonadDB m, MonadError e m, AsPaymentError e) =>
-- | the date against which payment request expiration should be checked
C.UTCTime ->
Billable ->
-- | the list of existing payment requests
[(PaymentRequestId, PaymentRequest)] ->
-- | the list of expected billing days
[T.Day] ->
-- | the list of billing days for which no payment request exists
m [T.Day]
EQ -> getRequestStatus now p >>= \s -> case s of
Expired r ->
if view _utctDay now > addDays (view gracePeriod b) (view billingDate r)
then throwError (review _Overdue (r ^. subscription))
else fmap (d :) $ findUnbilledDates now b px dx -- d will be rebilled
_ -> findUnbilledDates now b ps ds -- if paid or unpaid, nothing to do
EQ ->
getRequestStatus now p >>= \s -> case s of
Expired r ->
if view _utctDay now > addDays (view gracePeriod b) (view billingDate r)
then throwError (review _Overdue (r ^. subscription))
else fmap (d :) $ findUnbilledDates now b px dx -- d will be rebilled
_ -> findUnbilledDates now b ps ds -- if paid or unpaid, nothing to do
getRequestStatus
:: (MonadDB m)
=> C.UTCTime -- ^ the date against which request expiration should be checked
-> (PaymentRequestId, PaymentRequest) -- ^ the request for which to find a payment
-> m PaymentRequestStatus
getRequestStatus ::
(MonadDB m) =>
-- | the date against which request expiration should be checked
C.UTCTime ->
-- | the request for which to find a payment
(PaymentRequestId, PaymentRequest) ->
m PaymentRequestStatus
createPaymentDetails
:: ( MonadRandom m
, MonadReader r m
, HasPaymentsConfig r
, MonadError e m
, AsPaymentError e
, MonadDB m
)
=> T.Day -- ^ payout date (billing date)
-> C.UTCTime -- ^ timestamp of payment request creation
-> Maybe Text -- ^ user memo
-> Maybe URI -- ^ payment response URL
-> Maybe ByteString -- ^ merchant payload
-> Billable -- ^ billing information
-> m P.PaymentDetails
createPaymentDetails ::
( MonadRandom m,
MonadReader r m,
HasPaymentsConfig r,
MonadError e m,
AsPaymentError e,
MonadDB m
) =>
-- | payout date (billing date)
T.Day ->
-- | timestamp of payment request creation
C.UTCTime ->
-- | user memo
Maybe Text ->
-- | payment response URL
Maybe URI ->
-- | merchant payload
Maybe ByteString ->
-- | billing information
Billable ->
m P.PaymentDetails
pure $ B.createPaymentDetails (toNetwork (cfg ^. networkMode) BTC)
outputs
(T.fromThyme billingTime)
expiry
memo
uri
payload
where payoutTime = T.mkUTCTime payoutDate (fromInteger 0)
pure $
B.createPaymentDetails
(toNetwork (cfg ^. networkMode) BTC)
outputs
(T.fromThyme billingTime)
expiry
memo
uri
payload
where
payoutTime = T.mkUTCTime payoutDate (fromInteger 0)
getProjectPayouts
:: (MonadDB m, MonadError e m, AsPaymentError e)
=> C.UTCTime
-> ProjectId
-> m (TL.Payouts (NetworkId, Address))
getProjectPayouts ::
(MonadDB m, MonadError e m, AsPaymentError e) =>
C.UTCTime ->
ProjectId ->
m (TL.Payouts (NetworkId, Address))
createPayoutsOutputs
:: (MonadDB m, MonadError e m, AsPaymentError e)
=> C.UTCTime
-> BT.Satoshi
-> TL.Payouts (NetworkId, Address)
-> m [BT.Output]
createPayoutsOutputs ::
(MonadDB m, MonadError e m, AsPaymentError e) =>
C.UTCTime ->
BT.Satoshi ->
TL.Payouts (NetworkId, Address) ->
m [BT.Output]
createOutputs
:: (MonadDB m, MonadError e m, AsPaymentError e)
=> C.UTCTime
-> TL.CreditTo (NetworkId, Address)
-> BT.Satoshi
-> m [BT.Output]
createOutputs ::
(MonadDB m, MonadError e m, AsPaymentError e) =>
C.UTCTime ->
TL.CreditTo (NetworkId, Address) ->
BT.Satoshi ->
m [BT.Output]
other -> throwError $ review _IllegalAddress other
other -> throwError $ review _IllegalAddress other
where
findOp = FindUnpaidRequests sid
checkAccess d = if view (_3 . customer) d == uid
then pure [d]
else raiseOpForbidden uid (UserNotSubscriber sid) findOp
where
findOp = FindUnpaidRequests sid
checkAccess d =
if view (_3 . customer) d == uid
then pure [d]
else raiseOpForbidden uid (UserNotSubscriber sid) findOp
import Control.Lens ( makeLenses
, makePrisms
)
import Crypto.Random.Types ( MonadRandom
, getRandomBytes
)
import qualified Data.ByteString as BS
import Data.ByteString.Base64.URL as B64
import Data.Thyme.Clock as C
import Aftok.Types
data Project = Project
{ _projectName :: ProjectName
, _inceptionDate :: C.UTCTime
, _initiator :: UserId
, _depf :: DepreciationFunction
}
data Project
= Project
{ _projectName :: ProjectName,
_inceptionDate :: C.UTCTime,
_initiator :: UserId,
_depf :: DepreciationFunction
}
data Invitation = Invitation
{ _projectId :: ProjectId
, _invitingUser :: UserId
, _invitedEmail :: Email
, _invitationTime :: C.UTCTime
, _acceptanceTime :: Maybe C.UTCTime
}
data Invitation
= Invitation
{ _projectId :: ProjectId,
_invitingUser :: UserId,
_invitedEmail :: Email,
_invitationTime :: C.UTCTime,
_acceptanceTime :: Maybe C.UTCTime
}
import Data.Aeson ( Value(..)
, (.=)
, (.:)
, object
)
import Data.Aeson.Types ( Parser )
import Data.Text ( unpack )
import Aftok.Types
LinearDepreciation (Months up) (Months dp) -> object
[ "type" .= ("LinearDepreciation" :: Text)
, "arguments" .= object ["undep" .= up, "dep" .= dp]
]
LinearDepreciation (Months up) (Months dp) ->
object
[ "type" .= ("LinearDepreciation" :: Text),
"arguments" .= object ["undep" .= up, "dep" .= dp]
]
( LogEntry(..)
, creditTo
, event
, eventMeta
, CreditTo(..)
, _CreditToCurrency
, _CreditToUser
, _CreditToProject
, creditToName
, LogEvent(..)
, eventName
, nameEvent
, eventTime
, WorkIndex(WorkIndex)
, _WorkIndex
, workIndex
, DepF
, toDepF
, EventId(EventId)
, _EventId
, ModTime(ModTime)
, _ModTime
, EventAmendment(..)
, AmendmentId(AmendmentId)
, _AmendmentId
, Payouts(..)
, _Payouts
, payouts
, linearDepreciation
( LogEntry (..),
creditTo,
event,
eventMeta,
CreditTo (..),
_CreditToCurrency,
_CreditToUser,
_CreditToProject,
creditToName,
LogEvent (..),
eventName,
nameEvent,
eventTime,
WorkIndex (WorkIndex),
_WorkIndex,
workIndex,
DepF,
toDepF,
EventId (EventId),
_EventId,
ModTime (ModTime),
_ModTime,
EventAmendment (..),
AmendmentId (AmendmentId),
_AmendmentId,
Payouts (..),
_Payouts,
payouts,
linearDepreciation,
import Control.Arrow ( (&&&) )
import Control.Lens
import Data.AdditiveGroup
import Data.Aeson as A
import Data.AffineSpace
import Data.Eq ( Eq
, (==)
)
import Data.Either ( Either(..)
, rights
)
import Data.Foldable as F
import Data.Function ( ($)
, (.)
, id
)
import Data.Functor ( fmap )
import Data.Heap as H
import Data.List.NonEmpty as L
import Data.Maybe ( Maybe(..) )
import Data.Map.Strict as MS
import Data.Ord ( Ord(..)
, Ordering(..)
)
import Data.Ratio ( Rational )
import Data.Text ( Text )
import Data.Thyme.Clock as C
import Data.UUID
import Data.VectorSpace
import Prelude ( (/)
, (*)
)
import Text.Show ( Show )
import Aftok.Interval
import Aftok.Types
import Aftok.Interval
import Aftok.Types
import Control.Arrow ((&&&))
import Control.Lens
import Data.AdditiveGroup
import Data.Aeson as A
import Data.AffineSpace
import Data.Either
( Either (..),
rights,
)
import Data.Eq
( (==),
Eq,
)
import Data.Foldable as F
import Data.Function
( ($),
(.),
id,
)
import Data.Functor (fmap)
import Data.Heap as H
import Data.List.NonEmpty as L
import Data.Map.Strict as MS
import Data.Maybe (Maybe (..))
import Data.Ord
( Ord (..),
Ordering (..),
)
import Data.Ratio (Rational)
import Data.Text (Text)
import Data.Thyme.Clock as C
import Data.UUID
import Data.VectorSpace
import Text.Show (Show)
import Prelude
( (*),
(/),
)
{-|
- The depreciation function should return a value between 0 and 1;
- this result is multiplied by the length of an interval of work to determine
- the depreciated value of the work.
-}
-- |
-- - The depreciation function should return a value between 0 and 1;
-- - this result is multiplied by the length of an interval of work to determine
-- - the depreciated value of the work.
data LogEvent = StartWork { _eventTime :: !C.UTCTime }
| StopWork { _eventTime :: !C.UTCTime }
deriving (Show, Eq)
data LogEvent
= StartWork {_eventTime :: !C.UTCTime}
| StopWork {_eventTime :: !C.UTCTime}
deriving (Show, Eq)
compare (StartWork t0) (StopWork t1) = if t0 == t1 then GT else compare t0 t1
compare (StopWork t0) (StartWork t1) = if t0 == t1 then LT else compare t0 t1
compare (StartWork t0) (StopWork t1) = if t0 == t1 then GT else compare t0 t1
compare (StopWork t0) (StartWork t1) = if t0 == t1 then LT else compare t0 t1
data LogEntry a = LogEntry
{ _creditTo :: !(CreditTo a)
, _event :: !LogEvent
, _eventMeta :: !(Maybe A.Value)
} deriving (Show, Eq)
data LogEntry a
= LogEntry
{ _creditTo :: !(CreditTo a),
_event :: !LogEvent,
_eventMeta :: !(Maybe A.Value)
}
deriving (Show, Eq)
{-|
- Given a depreciation function, the "current" time, and a foldable functor of log intervals,
- produce the total, depreciated length of work to be credited to an address.
-}
-- |
-- - Given a depreciation function, the "current" time, and a foldable functor of log intervals,
-- - produce the total, depreciated length of work to be credited to an address.
{-|
- Payouts are determined by computing a depreciated duration value for
- each work interval. This function computes the percentage of the total
- work allocated to each address.
-}
-- |
-- - Payouts are determined by computing a depreciated duration value for
-- - each work interval. This function computes the percentage of the total
-- - work allocated to each address.
(^+^ total) &&& id $ workCredit dep ptime ivals
(^+^ total) &&& id $ workCredit dep ptime ivals
rawIndex = F.foldl' appendLogEntry MS.empty sortedEntries
accum
:: (CreditTo a)
-> [Either LogEvent Interval]
-> Map (CreditTo a) (NonEmpty Interval)
-> Map (CreditTo a) (NonEmpty Interval)
rawIndex = F.foldl' appendLogEntry MS.empty sortedEntries
accum ::
(CreditTo a) ->
[Either LogEvent Interval] ->
Map (CreditTo a) (NonEmpty Interval) ->
Map (CreditTo a) (NonEmpty Interval)
{-|
- The values of the raw index map are either complete intervals (which may be
- extended if a new start is encountered at the same instant as the end of the
- interval) or start events awaiting completion.
-}
-- |
-- - The values of the raw index map are either complete intervals (which may be
-- - extended if a new start is encountered at the same instant as the end of the
-- - interval) or start events awaiting completion.
combine (e1@(StopWork _)) (e2@(StopWork _)) = Left $ min e1 e2 -- ignore redundant ends
combine _ e2 = Left e2
combine (e1@(StopWork _)) (e2@(StopWork _)) = Left $ min e1 e2 -- ignore redundant ends
combine _ e2 = Left e2
{-|
- A very simple linear function for calculating depreciation.
-}
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.
-- |
-- - A very simple linear function for calculating depreciation.
linearDepreciation ::
-- | The number of initial months during which no depreciation occurs
Months ->
-- | The number of months over which each logged interval will be depreciated
Months ->
-- | The resulting configured depreciation function.
DepF
depPct dt = if dt < monthsLength undepLength
then 1
else toSeconds (max zeroV (maxDepreciable ^-^ dt))
/ toSeconds maxDepreciable
in \ptime ival ->
depPct dt =
if dt < monthsLength undepLength
then 1
else
toSeconds (max zeroV (maxDepreciable ^-^ dt))
/ toSeconds maxDepreciable
in \ptime ival ->
import Control.Lens ( makeLenses
, makePrisms
)
import Data.Eq ( Eq )
import Data.Functor ( Functor )
import Data.Ord ( Ord )
import Data.Text ( Text )
import Data.UUID ( UUID )
import Prelude ( Integer )
import Text.Show ( Show )
import Aftok.Currency.Zcash ( ZAddr )
-- payouts are made directly via a cryptocurrency network
= CreditToCurrency !a
-- payouts are distributed as requested by the specified contributor
| CreditToUser !UserId
-- payouts are distributed to this project's contributors
| CreditToProject !ProjectId
= -- payouts are made directly via a cryptocurrency network
CreditToCurrency !a
| -- payouts are distributed as requested by the specified contributor
CreditToUser !UserId
| -- payouts are distributed to this project's contributors
CreditToProject !ProjectId
import Control.Error.Util ( maybeT )
import Control.Monad.Free.Church
import Data.Functor.Coyoneda
import Data.Map.Strict as M
import Control.Error.Util (maybeT)
import Control.Monad.Free.Church
import Data.Functor.Coyoneda
import Data.Map.Strict as M
import Control.Lens ( makeLenses
, (^.)
)
import qualified Data.ByteString.Char8 as C8
import qualified Data.Configurator as C
import qualified Data.Configurator.Types as CT
import qualified Data.List as L
import System.Environment ( getEnvironment )
import Filesystem.Path.CurrentOS ( fromText
, encodeString
)
import qualified Filesystem.Path.CurrentOS as P
import Snap.Core
import qualified Snap.Http.Server.Config as SC
import Snap.Snaplet.PostgresqlSimple
import Aftok.Config
import Aftok.Currency.Zcash (ZcashdConfig (..))
import Aftok.Snaplet.Users (CaptchaConfig (..))
import Control.Lens
( (^.),
makeLenses,
)
import qualified Data.ByteString.Char8 as C8
import qualified Data.Configurator as C
import qualified Data.Configurator.Types as CT
import qualified Data.List as L
import Filesystem.Path.CurrentOS
( encodeString,
fromText,
)
import qualified Filesystem.Path.CurrentOS as P
import Snap.Core
import qualified Snap.Http.Server.Config as SC
import Snap.Snaplet.PostgresqlSimple
import System.Environment (getEnvironment)
import Aftok.Currency.Zcash (ZcashdConfig(..))
import Aftok.Config
import Aftok.Snaplet.Users (CaptchaConfig(..))
data QConfig
= QConfig
{ _hostname :: C8.ByteString,
_port :: Int,
_authSiteKey :: P.FilePath,
_cookieTimeout :: Maybe Int,
_pgsConfig :: PGSConfig,
_smtpConfig :: SmtpConfig,
_billingConfig :: BillingConfig,
_templatePath :: P.FilePath,
_staticAssetPath :: P.FilePath,
_recaptchaSecret :: CaptchaConfig,
_zcashdConfig :: ZcashdConfig
}
data QConfig = QConfig
{ _hostname :: C8.ByteString
, _port :: Int
, _authSiteKey :: P.FilePath
, _cookieTimeout :: Maybe Int
, _pgsConfig :: PGSConfig
, _smtpConfig :: SmtpConfig
, _billingConfig :: BillingConfig
, _templatePath :: P.FilePath
, _staticAssetPath :: P.FilePath
, _recaptchaSecret :: CaptchaConfig
, _zcashdConfig :: ZcashdConfig
}
import Control.Monad.Trans.Maybe ( mapMaybeT )
import Data.Aeson
import Data.Aeson.Types
import Data.Hourglass.Types ( Seconds(..) )
import Data.Thyme.Clock as C
import Snap.Snaplet as S
import Aftok.Types ( UserId )
import Aftok.Auction ( Auction(..)
, AuctionId
, Bid(..)
, BidId
)
import Aftok.Database ( createAuction
, createBid
, findAuction
)
import Aftok.Json
import Aftok.Util ( fromMaybeT )
import Aftok.Snaplet
import Aftok.Snaplet.Auth
import Bippy.Types ( Satoshi(..) )
import Aftok.Auction
( Auction (..),
AuctionId,
Bid (..),
BidId,
)
import Aftok.Database
( createAuction,
createBid,
findAuction,
)
import Aftok.Json
import Aftok.Snaplet
import Aftok.Snaplet.Auth
import Aftok.Types (UserId)
import Aftok.Util (fromMaybeT)
import Bippy.Types (Satoshi (..))
import Control.Monad.Trans.Maybe (mapMaybeT)
import Data.Aeson
import Data.Aeson.Types
import Data.Hourglass.Types (Seconds (..))
import Data.Thyme.Clock as C
import Snap.Snaplet as S
auctionCreateParser = unv1 "auctions" p where
p o = CA <$> o .: "raiseAmount" <*> o .: "auctionStart" <*> o .: "auctionEnd"
auctionCreateParser = unv1 "auctions" p
where
p o = CA <$> o .: "raiseAmount" <*> o .: "auctionStart" <*> o .: "auctionEnd"
bidCreateParser uid t = unv1 "bids" p where
p o =
Bid uid
<$> (Seconds <$> o .: "bidSeconds")
<*> (Satoshi <$> o .: "bidAmount")
<*> pure t
bidCreateParser uid t = unv1 "bids" p
where
p o =
Bid uid
<$> (Seconds <$> o .: "bidSeconds")
<*> (Satoshi <$> o .: "bidAmount")
<*> pure t
fromMaybeT (snapError 404 $ "Auction not found for id " <> show aid)
(mapMaybeT snapEval $ findAuction aid uid) -- this will verify auction access
fromMaybeT
(snapError 404 $ "Auction not found for id " <> show aid)
(mapMaybeT snapEval $ findAuction aid uid) -- this will verify auction access
import Control.Lens
import Control.Error.Util ( maybeT )
import Control.Monad.Trans.Maybe ( mapMaybeT )
import Data.Aeson ( (.:) )
import qualified Data.Aeson as A
import qualified Data.Aeson.Types as A
import Data.Attoparsec.ByteString ( parseOnly )
import Aftok.Types
import Aftok.Database
import Aftok.Snaplet
import Aftok.Util.Http ( authHeaderParser )
import Aftok.Database
import Aftok.Snaplet
import Aftok.Types
import Aftok.Util.Http (authHeaderParser)
import Control.Error.Util (maybeT)
import Control.Lens
import Control.Monad.Trans.Maybe (mapMaybeT)
import Data.Aeson ((.:))
import qualified Data.Aeson as A
import qualified Data.Aeson.Types as A
import Data.Attoparsec.ByteString (parseOnly)
import Snap.Core
import Snap.Snaplet as S
import qualified Snap.Snaplet.Auth as AU
(uname, pwd) <- either (throwDenied . AU.AuthError) pure
$ parseOnly authHeaderParser rawHeader
(uname, pwd) <-
either (throwDenied . AU.AuthError) pure $
parseOnly authHeaderParser rawHeader
authResult <- with auth $ AU.loginByUsername
(loginUser credentials)
(AU.ClearText (encodeUtf8 $ loginPass credentials))
False
authResult <-
with auth $
AU.loginByUsername
(loginUser credentials)
(AU.ClearText (encodeUtf8 $ loginPass credentials))
False
import Aftok.Billing
import Aftok.Database
( DBOp (..),
createBillable,
liftdb,
withProjectAuth,
)
import Aftok.Json
import Aftok.Snaplet
import Aftok.Snaplet.Auth
import Aftok.Types
import Bippy.Types (Satoshi (..))
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 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.Billing
import Bippy.Types ( Satoshi(..) )
import Aftok.Json
import Aftok.Types
import Aftok.Database ( createBillable
, withProjectAuth
, liftdb
, DBOp(..)
)
import Aftok.Snaplet
import Aftok.Snaplet.Auth
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 ver o = badVersion "Billable" ver o
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 ver o = badVersion "Billable" ver o
import Control.Lens ( view
, _1
, _2
, _Right
, _Left
, preview
, (.~)
, (^.)
)
import Control.Monad.Trans.Maybe ( mapMaybeT )
import Control.Exception ( try )
import Data.ProtocolBuffers ( decodeMessage )
import Data.Serialize.Get ( runGetLazy )
import Data.Thyme.Clock as C
import qualified Data.Text.Encoding as T
import qualified Bippy.Proto as P
import Network.HTTP.Client.OpenSSL
import Network.HTTP.Client ( defaultManagerSettings
, managerResponseTimeout
, responseTimeoutMicro
, HttpException
)
import Network.Wreq ( asValue
, responseBody
, defaults
, manager
, getWith
)
import OpenSSL.Session ( context )
import Snap.Core ( readRequestBody
, logError
)
import Snap.Snaplet as S
import Aftok.Config as AC
import Aftok.Billing
import Aftok.Database
import Aftok.Payments
import Aftok.Util ( fromMaybeT )
import Aftok.Snaplet
import Aftok.Snaplet.Auth
import Aftok.Billing
import Aftok.Config as AC
import Aftok.Database
import Aftok.Payments
import Aftok.Snaplet
import Aftok.Snaplet.Auth
import Aftok.Util (fromMaybeT)
import qualified Bippy.Proto as P
import Control.Exception (try)
import Control.Lens
( (.~),
(^.),
_1,
_2,
_Left,
_Right,
preview,
view,
)
import Control.Monad.Trans.Maybe (mapMaybeT)
import Data.ProtocolBuffers (decodeMessage)
import Data.Serialize.Get (runGetLazy)
import qualified Data.Text.Encoding as T
import Data.Thyme.Clock as C
import Network.HTTP.Client
( HttpException,
defaultManagerSettings,
managerResponseTimeout,
responseTimeoutMicro,
)
import Network.HTTP.Client.OpenSSL
import Network.Wreq
( asValue,
defaults,
getWith,
manager,
responseBody,
)
import OpenSSL.Session (context)
import Snap.Core
( logError,
readRequestBody,
)
import Snap.Snaplet as S
preq <- getPaymentRequestHandler'
pmnt <- either
(\msg -> snapError 400 $ "Could not decode payment response: " <> show msg)
pure
(runGetLazy decodeMessage requestBody)
preq <- getPaymentRequestHandler'
pmnt <-
either
(\msg -> snapError 400 $ "Could not decode payment response: " <> show msg)
pure
(runGetLazy decodeMessage requestBody)
let
opts =
defaults
& manager
.~ Left (opensslManagerSettings context)
& manager
.~ Left
(defaultManagerSettings
{ managerResponseTimeout = responseTimeoutMicro 10000
}
)
let opts =
defaults
& manager
.~ Left (opensslManagerSettings context)
& manager
.~ Left
( defaultManagerSettings
{ managerResponseTimeout = responseTimeoutMicro 10000
}
)
import Aftok.Config
import Aftok.Database
import Aftok.Project
import Aftok.QConfig as QC
import Aftok.Snaplet
import Aftok.Snaplet.Auth
import Aftok.TimeLog.Serialization (depfFromJSON)
import Aftok.Types
import Aftok.Util (fromMaybeT)
import Control.Lens
import Control.Monad.Trans.Maybe (mapMaybeT)
import Data.Aeson as A
import Data.Attoparsec.ByteString (takeByteString)
import Data.Thyme.Clock as C
import Filesystem.Path.CurrentOS (encodeString)
import qualified Filesystem.Path.CurrentOS as F
import Network.Mail.Mime
import Network.Mail.SMTP as SMTP
import Snap.Core
import Snap.Snaplet as S
import Text.StringTemplate
import Control.Lens
import Control.Monad.Trans.Maybe ( mapMaybeT )
import Data.Aeson as A
import Data.Attoparsec.ByteString ( takeByteString )
import Data.Thyme.Clock as C
import Filesystem.Path.CurrentOS ( encodeString )
import qualified Filesystem.Path.CurrentOS as F
import Network.Mail.Mime
import Network.Mail.SMTP as SMTP
import Text.StringTemplate
data ProjectCreateRequest = CP {cpn :: Text, cpdepf :: DepreciationFunction}
import Aftok.Types
import Aftok.Config
import Aftok.Database
import Aftok.Project
import Aftok.QConfig as QC
import Aftok.Snaplet
import Aftok.Snaplet.Auth
import Aftok.TimeLog.Serialization ( depfFromJSON )
import Aftok.Util ( fromMaybeT )
import Snap.Core
import Snap.Snaplet as S
data ProjectCreateRequest = CP { cpn :: Text, cpdepf :: DepreciationFunction }
snapEval
$ (,)
<$> (runMaybeT $ findUserProject uid pid)
<*> createInvitation pid uid toEmail t
liftIO $ sendProjectInviteEmail cfg
(p ^. projectName)
(Email "noreply@aftok.com")
toEmail
invCode
snapEval $
(,)
<$> (runMaybeT $ findUserProject uid pid)
<*> createInvitation pid uid toEmail t
liftIO $
sendProjectInviteEmail
cfg
(p ^. projectName)
(Email "noreply@aftok.com")
toEmail
invCode
sendProjectInviteEmail
:: QConfig
-> ProjectName
-> Email -- Inviting user's email address
-> Email -- Invitee's email address
-> InvitationCode
-> IO ()
sendProjectInviteEmail ::
QConfig ->
ProjectName ->
Email -> -- Inviting user's email address
Email -> -- Invitee's email address
InvitationCode ->
IO ()
mailer = maybe (sendMailWithLogin _smtpHost)
(sendMailWithLogin' _smtpHost)
_smtpPort
in buildProjectInviteEmail (cfg ^. templatePath) pn fromEmail toEmail invCode
mailer =
maybe
(sendMailWithLogin _smtpHost)
(sendMailWithLogin' _smtpHost)
_smtpPort
in buildProjectInviteEmail (cfg ^. templatePath) pn fromEmail toEmail invCode
buildProjectInviteEmail
:: F.FilePath
-> ProjectName
-> Email -- Inviting user's email address
-> Email -- Invitee's email address
-> InvitationCode
-> IO Mail
buildProjectInviteEmail ::
F.FilePath ->
ProjectName ->
Email -> -- Inviting user's email address
Email -> -- Invitee's email address
InvitationCode ->
IO Mail
setAttribute "from_email" (fromEmail ^. _Email)
. setAttribute "project_name" pn
. setAttribute "to_email" (toEmail ^. _Email)
. setAttribute "inv_code" (renderInvCode invCode)
setAttribute "from_email" (fromEmail ^. _Email)
. setAttribute "project_name" pn
. setAttribute "to_email" (toEmail ^. _Email)
. setAttribute "inv_code" (renderInvCode invCode)
toAddr = Address Nothing (toEmail ^. _Email)
subject = "Welcome to the " <> pn <> " Aftok!"
body = plainPart . render $ setAttrs template
in pure $ SMTP.simpleMail fromAddr [toAddr] [] [] subject [body]
toAddr = Address Nothing (toEmail ^. _Email)
subject = "Welcome to the " <> pn <> " Aftok!"
body = plainPart . render $ setAttrs template
in pure $ SMTP.simpleMail fromAddr [toAddr] [] [] subject [body]
( acceptInvitationHandler
, checkZAddrHandler
, registerHandler
, CaptchaConfig(..)
, CaptchaError(..)
, checkCaptcha
, RegisterOps(..)
( acceptInvitationHandler,
checkZAddrHandler,
registerHandler,
CaptchaConfig (..),
CaptchaError (..),
checkCaptcha,
RegisterOps (..),
import Control.Lens ( makeLenses, (^.) )
import Control.FromSum ( fromMaybeM )
import qualified Data.Aeson as A
import Data.Aeson ( (.:)
, (.:?)
, (.=)
)
import qualified Data.Map.Strict as M
import Data.Text as T
import Data.Text.Encoding as T
import Data.Thyme.Clock as C
import Network.HTTP.Client ( parseRequest
, responseBody
, responseStatus
, httpLbs
)
import Network.HTTP.Client.TLS ( newTlsManager )
import Network.HTTP.Client.MultipartFormData
( formDataBody
, partBS
)
import Network.HTTP.Types.Status ( statusCode )
import Aftok.Currency.Zcash ( ZAddr, RPCError, ZValidateAddressErr )
import Aftok.Database ( createUser, acceptInvitation )
import Aftok.Project ( InvitationCode, parseInvCode )
import Aftok.Types ( UserId, User(..)
, AccountRecovery(..)
, Email(..)
, UserName(..), _UserName
)
import Aftok.Snaplet
import Aftok.Snaplet.Auth
import Aftok.Currency.Zcash (RPCError, ZAddr, ZValidateAddressErr)
import Aftok.Database (acceptInvitation, createUser)
import Aftok.Project (InvitationCode, parseInvCode)
import Aftok.Snaplet
import Aftok.Snaplet.Auth
import Aftok.Types
( AccountRecovery (..),
Email (..),
User (..),
UserId,
UserName (..),
_UserName,
)
import Control.FromSum (fromMaybeM)
import Control.Lens ((^.), makeLenses)
import Data.Aeson
( (.:),
(.:?),
(.=),
)
import qualified Data.Aeson as A
import qualified Data.Map.Strict as M
import Data.Text as T
import Data.Text.Encoding as T
import Data.Thyme.Clock as C
import Network.HTTP.Client
( httpLbs,
parseRequest,
responseBody,
responseStatus,
)
import Network.HTTP.Client.MultipartFormData
( formDataBody,
partBS,
)
import Network.HTTP.Client.TLS (newTlsManager)
import Network.HTTP.Types.Status (statusCode)
import qualified Snap.Core as S
import qualified Snap.Snaplet as S
import qualified Snap.Snaplet.Auth as AU
import qualified Snap.Core as S
import qualified Snap.Snaplet as S
import qualified Snap.Snaplet.Auth as AU
data RegisterOps m
= RegisterOps
{ validateZAddr :: Text -> m (Either (RPCError ZValidateAddressErr) ZAddr),
sendConfirmationEmail :: Email -> m ()
}
data RegisterRequest = RegisterRequest
{ _regUser :: RegUser
, _password :: ByteString
, _captchaToken :: Text
, _invitationCodes :: [InvitationCode]
}
data RegisterRequest
= RegisterRequest
{ _regUser :: RegUser,
_password :: ByteString,
_captchaToken :: Text,
_invitationCodes :: [InvitationCode]
}
user <- RegUser <$> (UserName <$> v .: "username")
<*> pure recovery
user <-
RegUser <$> (UserName <$> v .: "username")
<*> pure recovery
parseInvitationCodes c = either
(\e -> fail $ "Invitation code was rejected as invalid: " <> e)
pure
(traverse parseInvCode c)
parseInvitationCodes c =
either
(\e -> fail $ "Invitation code was rejected as invalid: " <> e)
pure
(traverse parseInvCode c)
RegParseError msg -> A.object
[ "parseError" .= msg ]
RegCaptchaError e -> A.object
[ "captchaError" .= (show e :: Text) ]
RegZAddrError zerr -> A.object
[ "zaddrError" .= (show zerr :: Text) ]
RegParseError msg ->
A.object
["parseError" .= msg]
RegCaptchaError e ->
A.object
["captchaError" .= (show e :: Text)]
RegZAddrError zerr ->
A.object
["zaddrError" .= (show zerr :: Text)]
let
uname = userData ^. (regUser . username)
createSUser = AU.createUser (uname ^. _UserName) (userData ^. password)
createQUser = snapEval $ do
userId <- createUser $ User uname acctRecovery
void $ traverse (acceptInvitation userId now) (userData ^. invitationCodes)
pure userId
let uname = userData ^. (regUser . username)
createSUser = AU.createUser (uname ^. _UserName) (userData ^. password)
createQUser = snapEval $ do
userId <- createUser $ User uname acctRecovery
void $ traverse (acceptInvitation userId now) (userData ^. invitationCodes)
pure userId
uid <- requireUserId
now <- liftIO C.getCurrentTime
params <- S.getParams
invCodes <- maybe (snapError 400 "invCode parameter is required")
(pure . traverse (parseInvCode . T.decodeUtf8))
(M.lookup "invCode" params)
uid <- requireUserId
now <- liftIO C.getCurrentTime
params <- S.getParams
invCodes <-
maybe
(snapError 400 "invCode parameter is required")
(pure . traverse (parseInvCode . T.decodeUtf8))
(M.lookup "invCode" params)
where
toError = \case
"missing-input-secret" -> MissingInputSecret
"invalid-input-secret" -> InvalidInputSecret
"missing-input-response" -> MissingInputResponse
"invalid-input-response" -> InvalidInputResponse
"bad-request" -> BadRequest
"timeout-or-duplicate" -> TimeoutOrDuplicate
other -> CaptchaError $ "Unexpected error code: " <> other
where
toError = \case
"missing-input-secret" -> MissingInputSecret
"invalid-input-secret" -> InvalidInputSecret
"missing-input-response" -> MissingInputResponse
"invalid-input-response" -> InvalidInputResponse
"bad-request" -> BadRequest
"timeout-or-duplicate" -> TimeoutOrDuplicate
other -> CaptchaError $ "Unexpected error code: " <> other
request <- parseRequest "https://www.google.com/recaptcha/api/siteverify"
reqWithBody <- formDataBody
[ partBS "secret" (T.encodeUtf8 $ secretKey cfg)
, partBS "response" (T.encodeUtf8 token)
]
request
manager <- newTlsManager
request <- parseRequest "https://www.google.com/recaptcha/api/siteverify"
reqWithBody <-
formDataBody
[ partBS "secret" (T.encodeUtf8 $ secretKey cfg),
partBS "response" (T.encodeUtf8 token)
]
request
manager <- newTlsManager
import Data.Attoparsec.ByteString ( parseOnly )
import Data.Attoparsec.ByteString.Char8
( decimal )
import Data.ByteString.Char8 as B
import Data.Thyme.Clock as C
import Data.Thyme.Time
import Data.Time.ISO8601
import Data.Attoparsec.ByteString (parseOnly)
import Data.Attoparsec.ByteString.Char8
( decimal,
)
import Data.ByteString.Char8 as B
import Data.Thyme.Clock as C
import Data.Thyme.Time
import Data.Time.ISO8601
import Snap.Core
{-# LANGUAGE TupleSections #-}
module Aftok.Snaplet.WorkLog where
import Control.Lens ( (^.) )
import Control.Monad.Trans.Maybe ( mapMaybeT )
import qualified Data.Aeson as A
import Data.Aeson ( (.=) )
import qualified Data.Aeson.Types as A
import qualified Data.Text as T
import Data.Thyme.Clock as C
import Data.UUID as U
import Haskoin.Address ( Address
, textToAddr
)
import Aftok.Currency.Bitcoin ( NetworkId(..)
, NetworkMode
, toNetwork
)
import Aftok.Database
import Aftok.Interval
import Aftok.Json
import Aftok.Project
import Aftok.TimeLog
import Aftok.Types ( _ProjectId
, _UserId
)
import Aftok.Util ( fromMaybeT )
{-# LANGUAGE TupleSections #-}
import Snap.Core
import Snap.Snaplet as S
import Aftok.Currency.Bitcoin
( NetworkId (..),
NetworkMode,
toNetwork,
)
import Aftok.Database
import Aftok.Interval
import Aftok.Json
import Aftok.Project
import Aftok.Snaplet
import Aftok.Snaplet.Auth
import Aftok.Snaplet.Util
import Aftok.TimeLog
import Aftok.Types
( _ProjectId,
_UserId,
)
import Aftok.Util (fromMaybeT)
import Control.Lens ((^.))
import Control.Monad.Trans.Maybe (mapMaybeT)
import Data.Aeson ((.=))
import qualified Data.Aeson as A
import qualified Data.Aeson.Types as A
import qualified Data.Text as T
import Data.Thyme.Clock as C
import Data.UUID as U
import Haskoin.Address
( Address,
textToAddr,
)
import Snap.Core
import Snap.Snaplet as S
timestamp <- liftIO C.getCurrentTime
case
A.eitherDecode requestBody
>>= A.parseEither (parseLogEntry nmode uid evCtr)
of
Left err ->
snapError 400
$ "Unable to parse log entry "
timestamp <- liftIO C.getCurrentTime
case A.eitherDecode requestBody
>>= A.parseEither (parseLogEntry nmode uid evCtr) of
Left err ->
snapError 400 $
"Unable to parse log entry "
Right entry -> do
eid <- snapEval $ createEvent pid uid (entry timestamp)
ev <- snapEval $ findEvent eid
maybe
( snapError 500
$ "An error occured retrieving the newly created event record."
)
(pure . (eid, ))
ev
Right entry -> do
eid <- snapEval $ createEvent pid uid (entry timestamp)
ev <- snapEval $ findEvent eid
maybe
( snapError 500 $
"An error occured retrieving the newly created event record."
)
(pure . (eid,))
ev
snapError 400
$ "Unable to parse bitcoin address from "
<> (show addrBytes)
Just addr -> snapEval . createEvent pid uid $ LogEntry
(CreditToCurrency (BTC, addr))
(evCtr timestamp)
(A.decode requestBody)
snapError 400 $
"Unable to parse bitcoin address from "
<> (show addrBytes)
Just addr ->
snapEval . createEvent pid uid $
LogEntry
(CreditToCurrency (BTC, addr))
(evCtr timestamp)
(A.decode requestBody)
uid <- requireUserId
pid <- requireProjectId
project <- fromMaybeT
(snapError 400 $ "Project not found for id " <> show pid)
(mapMaybeT snapEval $ findUserProject uid pid)
widx <- snapEval $ readWorkIndex pid uid
uid <- requireUserId
pid <- requireProjectId
project <-
fromMaybeT
(snapError 400 $ "Project not found for id " <> show pid)
(mapMaybeT snapEval $ findUserProject uid pid)
widx <- snapEval $ readWorkIndex pid uid
eventId <- maybe (snapError 400 "eventId parameter is required")
(pure . EventId)
(eventIdBytes >>= U.fromASCIIBytes)
modTime <- ModTime <$> liftIO C.getCurrentTime
eventId <-
maybe
(snapError 400 "eventId parameter is required")
(pure . EventId)
(eventIdBytes >>= U.fromASCIIBytes)
modTime <- ModTime <$> liftIO C.getCurrentTime
either (snapError 400 . T.pack)
(snapEval . amendEvent uid eventId)
(A.parseEither (parseEventAmendment nmode modTime) requestJSON)
either
(snapError 400 . T.pack)
(snapEval . amendEvent uid eventId)
(A.parseEither (parseEventAmendment nmode modTime) requestJSON)
. obj
$ [ "eventId" .= idValue _EventId eid
, "projectId" .= idValue _ProjectId pid
, "loggedBy" .= idValue _UserId uid
]
<> logEntryFields nmode ev
. obj
$ [ "eventId" .= idValue _EventId eid,
"projectId" .= idValue _ProjectId pid,
"loggedBy" .= idValue _UserId uid
]
<> logEntryFields nmode ev
import Control.Lens
import qualified Data.Aeson as A
import Data.Attoparsec.ByteString ( Parser
, parseOnly
, takeByteString
)
import Data.UUID ( UUID, fromASCIIBytes )
import Aftok.Auction ( AuctionId(..) )
import Aftok.Currency.Bitcoin ( NetworkMode(..) )
import Aftok.Database ( DBError(..)
, DBOp
, liftdb
)
import Aftok.Database.PostgreSQL ( runQDBM )
import Aftok.Types ( UserId(..)
, ProjectId(..)
)
import Aftok.Util
import Snap.Core ( MonadSnap
, getParam
, readRequestBody
, setResponseCode
, modifyResponse
, finishWith
, getResponse
, writeText
, writeLBS
, setResponseStatus
, logError
)
import Snap.Snaplet as S
import qualified Snap.Snaplet.Auth as AU
import Snap.Snaplet.PostgresqlSimple ( Postgres
, HasPostgres(..)
, setLocalPostgresState
, liftPG
)
import Snap.Snaplet.Session ( SessionManager )
import Aftok.Auction (AuctionId (..))
import Aftok.Currency.Bitcoin (NetworkMode (..))
import Aftok.Database
( DBError (..),
DBOp,
liftdb,
)
import Aftok.Database.PostgreSQL (runQDBM)
import Aftok.Types
( ProjectId (..),
UserId (..),
)
import Aftok.Util
import Control.Lens
import qualified Data.Aeson as A
import Data.Attoparsec.ByteString
( Parser,
parseOnly,
takeByteString,
)
import Data.UUID (UUID, fromASCIIBytes)
import Snap.Core
( MonadSnap,
finishWith,
getParam,
getResponse,
logError,
modifyResponse,
readRequestBody,
setResponseCode,
setResponseStatus,
writeLBS,
writeText,
)
import Snap.Snaplet as S
import qualified Snap.Snaplet.Auth as AU
import Snap.Snaplet.PostgresqlSimple
( HasPostgres (..),
Postgres,
liftPG,
setLocalPostgresState,
)
import Snap.Snaplet.Session (SessionManager)
data App = App
{ _networkMode :: NetworkMode
, _sess :: Snaplet SessionManager
, _db :: Snaplet Postgres
, _auth :: Snaplet (AU.AuthManager App)
}
parseParam
:: MonadSnap m
=> Text -- ^ the name of the parameter to be parsed
-> Parser a -- ^ parser for the value of the parameter
-> m a -- ^ the parsed value
parseParam ::
MonadSnap m =>
-- | the name of the parameter to be parsed
Text ->
-- | parser for the value of the parameter
Parser a ->
-- | the parsed value
m a
import Control.Lens ( (^.)
, to
)
import Control.Exception ( try )
import qualified Data.Aeson as A
import Data.ProtocolBuffers ( encodeMessage )
import Data.Serialize.Put ( runPutLazy )
import Filesystem.Path.CurrentOS ( decodeString
, encodeString
)
import Network.HTTP.Client ( Manager, newManager, defaultManagerSettings )
import System.Environment
import System.IO.Error ( IOError )
import Aftok.Currency.Zcash ( rpcValidateZAddr )
import Aftok.Json
import Aftok.TimeLog
import qualified Aftok.Config as C
import Aftok.QConfig as Q
import Aftok.Snaplet
import Aftok.Snaplet.Auctions
import Aftok.Snaplet.Billing
import Aftok.Snaplet.Auth
import Aftok.Snaplet.Payments
import Aftok.Snaplet.Projects
import Aftok.Snaplet.Users
import Aftok.Snaplet.WorkLog
import qualified Aftok.Config as C
import Aftok.Currency.Zcash (rpcValidateZAddr)
import Aftok.Json
import Aftok.QConfig as Q
import Aftok.Snaplet
import Aftok.Snaplet.Auctions
import Aftok.Snaplet.Auth
import Aftok.Snaplet.Billing
import Aftok.Snaplet.Payments
import Aftok.Snaplet.Projects
import Aftok.Snaplet.Users
import Aftok.Snaplet.WorkLog
import Aftok.TimeLog
import Control.Exception (try)
import Control.Lens
( (^.),
to,
)
import qualified Data.Aeson as A
import Data.ProtocolBuffers (encodeMessage)
import Data.Serialize.Put (runPutLazy)
import Filesystem.Path.CurrentOS
( decodeString,
encodeString,
)
import Network.HTTP.Client (Manager, defaultManagerSettings, newManager)
import Snap.Core
import Snap.Snaplet
import qualified Snap.Snaplet.Auth as AU
import Snap.Snaplet.Auth.Backends.PostgresqlSimple
import Snap.Snaplet.PostgresqlSimple
import Snap.Snaplet.Session.Backends.CookieSession
import Snap.Util.FileServe (serveDirectory)
import System.Environment
import System.IO.Error (IOError)
import Snap.Core
import Snap.Snaplet
import qualified Snap.Snaplet.Auth as AU
import Snap.Snaplet.PostgresqlSimple
import Snap.Snaplet.Auth.Backends.PostgresqlSimple
import Snap.Snaplet.Session.Backends.CookieSession
import Snap.Util.FileServe ( serveDirectory )
registerOps mgr cfg = RegisterOps
{ validateZAddr = rpcValidateZAddr mgr (_zcashdConfig cfg)
, sendConfirmationEmail = const $ pure ()
}
registerOps mgr cfg =
RegisterOps
{ validateZAddr = rpcValidateZAddr mgr (_zcashdConfig cfg),
sendConfirmationEmail = const $ pure ()
}
let
nmode = cfg ^. billingConfig . C.networkMode
loginRoute = method GET requireLogin >> redirect "/app"
xhrLoginRoute = void $ method POST requireLoginXHR
checkLoginRoute = void $ method GET requireUser
logoutRoute = method GET (with auth AU.logout)
checkZAddrRoute = void $ method GET (checkZAddrHandler rops)
registerRoute = void $ method POST (registerHandler rops (cfg ^. recaptchaSecret))
inviteRoute = void $ method POST (projectInviteHandler cfg)
acceptInviteRoute = void $ method POST acceptInvitationHandler
projectCreateRoute =
serveJSON projectIdJSON $ method POST projectCreateHandler
projectListRoute =
serveJSON (fmap qdbProjectJSON) $ method GET projectListHandler
projectRoute = serveJSON projectJSON $ method GET projectGetHandler
projectWorkIndexRoute =
serveJSON (workIndexJSON nmode) (method GET projectWorkIndex)
projectPayoutsRoute =
serveJSON (payoutsJSON nmode) $ method GET payoutsHandler
logWorkRoute f =
serveJSON (keyedLogEntryJSON nmode) $ method POST (logWorkHandler f)
-- logWorkBTCRoute f =
-- serveJSON eventIdJSON $ method POST (logWorkBTCHandler f)
amendEventRoute = serveJSON amendmentIdJSON $ method PUT amendEventHandler
userEventsRoute =
serveJSON (fmap $ logEntryJSON nmode) $ method GET userEvents
userWorkIndexRoute =
serveJSON (workIndexJSON nmode) $ method GET userWorkIndex
auctionCreateRoute =
serveJSON auctionIdJSON $ method POST auctionCreateHandler
auctionRoute = serveJSON auctionJSON $ method GET auctionGetHandler
auctionBidRoute = serveJSON bidIdJSON $ method POST auctionBidHandler
billableCreateRoute =
serveJSON billableIdJSON $ method POST billableCreateHandler
billableListRoute =
serveJSON (fmap qdbBillableJSON) $ method GET billableListHandler
subscribeRoute =
serveJSON subscriptionIdJSON $ method POST subscribeHandler
payableRequestsRoute =
serveJSON billDetailsJSON $ method GET listPayableRequestsHandler
getPaymentRequestRoute =
writeLBS
. runPutLazy
. encodeMessage
=<< method GET getPaymentRequestHandler
submitPaymentRoute = serveJSON paymentIdJSON
$ method POST (paymentResponseHandler $ cfg ^. billingConfig)
let nmode = cfg ^. billingConfig . C.networkMode
loginRoute = method GET requireLogin >> redirect "/app"
xhrLoginRoute = void $ method POST requireLoginXHR
checkLoginRoute = void $ method GET requireUser
logoutRoute = method GET (with auth AU.logout)
checkZAddrRoute = void $ method GET (checkZAddrHandler rops)
registerRoute = void $ method POST (registerHandler rops (cfg ^. recaptchaSecret))
inviteRoute = void $ method POST (projectInviteHandler cfg)
acceptInviteRoute = void $ method POST acceptInvitationHandler
projectCreateRoute =
serveJSON projectIdJSON $ method POST projectCreateHandler
projectListRoute =
serveJSON (fmap qdbProjectJSON) $ method GET projectListHandler
projectRoute = serveJSON projectJSON $ method GET projectGetHandler
projectWorkIndexRoute =
serveJSON (workIndexJSON nmode) (method GET projectWorkIndex)
projectPayoutsRoute =
serveJSON (payoutsJSON nmode) $ method GET payoutsHandler
logWorkRoute f =
serveJSON (keyedLogEntryJSON nmode) $ method POST (logWorkHandler f)
-- logWorkBTCRoute f =
-- serveJSON eventIdJSON $ method POST (logWorkBTCHandler f)
amendEventRoute = serveJSON amendmentIdJSON $ method PUT amendEventHandler
userEventsRoute =
serveJSON (fmap $ logEntryJSON nmode) $ method GET userEvents
userWorkIndexRoute =
serveJSON (workIndexJSON nmode) $ method GET userWorkIndex
auctionCreateRoute =
serveJSON auctionIdJSON $ method POST auctionCreateHandler
auctionRoute = serveJSON auctionJSON $ method GET auctionGetHandler
auctionBidRoute = serveJSON bidIdJSON $ method POST auctionBidHandler
billableCreateRoute =
serveJSON billableIdJSON $ method POST billableCreateHandler
billableListRoute =
serveJSON (fmap qdbBillableJSON) $ method GET billableListHandler
subscribeRoute =
serveJSON subscriptionIdJSON $ method POST subscribeHandler
payableRequestsRoute =
serveJSON billDetailsJSON $ method GET listPayableRequestsHandler
getPaymentRequestRoute =
writeLBS
. runPutLazy
. encodeMessage
=<< method GET getPaymentRequestHandler
submitPaymentRoute =
serveJSON paymentIdJSON $
method POST (paymentResponseHandler $ cfg ^. billingConfig)
[ ("static", serveDirectory . encodeString $ cfg ^. staticAssetPath)
, ("login" , loginRoute)
, ("login" , xhrLoginRoute)
, ("logout" , logoutRoute)
, ("login/check", checkLoginRoute)
, ("register" , registerRoute)
, ("validate_zaddr", checkZAddrRoute)
, ( "accept_invitation"
, acceptInviteRoute
)
-- , ("projects/:projectId/logStart/:btcAddr" , logWorkBTCRoute StartWork)
-- , ("projects/:projectId/logEnd/:btcAddr" , logWorkBTCRoute StopWork)
, ("user/projects/:projectId/logStart" , logWorkRoute StartWork)
, ("user/projects/:projectId/logEnd" , logWorkRoute StopWork)
, ("user/projects/:projectId/events" , userEventsRoute)
, ("user/projects/:projectId/workIndex", userWorkIndexRoute)
, ("projects/:projectId/workIndex" , projectWorkIndexRoute)
, ( "projects/:projectId/auctions"
, auctionCreateRoute
) -- <|> auctionListRoute)
, ( "projects/:projectId/billables"
, billableCreateRoute <|> billableListRoute
)
, ("projects/:projectId/payouts", projectPayoutsRoute)
, ("projects/:projectId/invite" , inviteRoute)
, ("projects/:projectId" , projectRoute)
, ("projects" , projectCreateRoute <|> projectListRoute)
, ("auctions/:auctionId" , auctionRoute)
, ("auctions/:auctionId/bid" , auctionBidRoute)
, ("subscribe/:billableId" , subscribeRoute)
, ("subscriptions/:subscriptionId/payment_requests", payableRequestsRoute)
, ("pay/:paymentRequestKey", getPaymentRequestRoute <|> submitPaymentRoute)
, ("events/:eventId/amend" , amendEventRoute)
[ ("static", serveDirectory . encodeString $ cfg ^. staticAssetPath),
("login", loginRoute),
("login", xhrLoginRoute),
("logout", logoutRoute),
("login/check", checkLoginRoute),
("register", registerRoute),
("validate_zaddr", checkZAddrRoute),
( "accept_invitation",
acceptInviteRoute
),
-- , ("projects/:projectId/logStart/:btcAddr" , logWorkBTCRoute StartWork)
-- , ("projects/:projectId/logEnd/:btcAddr" , logWorkBTCRoute StopWork)
("user/projects/:projectId/logStart", logWorkRoute StartWork),
("user/projects/:projectId/logEnd", logWorkRoute StopWork),
("user/projects/:projectId/events", userEventsRoute),
("user/projects/:projectId/workIndex", userWorkIndexRoute),
("projects/:projectId/workIndex", projectWorkIndexRoute),
( "projects/:projectId/auctions",
auctionCreateRoute
), -- <|> auctionListRoute)
( "projects/:projectId/billables",
billableCreateRoute <|> billableListRoute
),
("projects/:projectId/payouts", projectPayoutsRoute),
("projects/:projectId/invite", inviteRoute),
("projects/:projectId", projectRoute),
("projects", projectCreateRoute <|> projectListRoute),
("auctions/:auctionId", auctionRoute),
("auctions/:auctionId/bid", auctionBidRoute),
("subscribe/:billableId", subscribeRoute),
("subscriptions/:subscriptionId/payment_requests", payableRequestsRoute),
("pay/:paymentRequestKey", getPaymentRequestRoute <|> submitPaymentRoute),
("events/:eventId/amend", amendEventRoute)
import Control.Lens
import Data.Hourglass
import Data.List ( (!!) )
import Data.Thyme.Clock ( )
import qualified Data.UUID.V4 as U
import Text.Read ( read )
import Aftok.Auction
import Aftok.Generators
import Aftok.Types
import Bippy.Test.Types (arbitrarySatoshi)
import Bippy.Types (Satoshi (..))
import Control.Lens
import Data.Hourglass
import Data.List ((!!))
import Data.Thyme.Clock ()
import qualified Data.UUID.V4 as U
import Haskoin.Constants (btc)
import Test.HUnit.Base (assertFailure)
import Test.Hspec
import Test.QuickCheck
import Text.Read (read)
import Haskoin.Constants ( btc )
import Bippy.Types ( Satoshi(..) )
import Bippy.Test.Types ( arbitrarySatoshi )
import Aftok.Auction
import Aftok.Generators
import Aftok.Types
import Test.Hspec
import Test.HUnit.Base ( assertFailure )
import Test.QuickCheck
let testB0 = Bid (users !! 0)
(Seconds 3)
(Satoshi 100)
(read "2016-03-05 15:59:20.000000 UTC")
testB1 = Bid (users !! 1)
(Seconds 60)
(Satoshi 1000)
(read "2016-03-05 15:59:21.000000 UTC")
testB2 = Bid (users !! 2)
(Seconds 60)
(Satoshi 100)
(read "2016-03-05 15:59:22.000000 UTC")
testB3 = Bid (users !! 3)
(Seconds 90)
(Satoshi 100)
(read "2016-03-05 15:59:23.000000 UTC")
testB4 = Bid (users !! 4)
(Seconds 60)
(Satoshi 100)
(read "2016-03-05 15:59:24.000000 UTC")
let testB0 =
Bid
(users !! 0)
(Seconds 3)
(Satoshi 100)
(read "2016-03-05 15:59:20.000000 UTC")
testB1 =
Bid
(users !! 1)
(Seconds 60)
(Satoshi 1000)
(read "2016-03-05 15:59:21.000000 UTC")
testB2 =
Bid
(users !! 2)
(Seconds 60)
(Satoshi 100)
(read "2016-03-05 15:59:22.000000 UTC")
testB3 =
Bid
(users !! 3)
(Seconds 90)
(Satoshi 100)
(read "2016-03-05 15:59:23.000000 UTC")
testB4 =
Bid
(users !! 4)
(Seconds 60)
(Satoshi 100)
(read "2016-03-05 15:59:24.000000 UTC")
it "determines a sufficient number of winners to fulfill the raise amount"
$ let
result =
it "determines a sufficient number of winners to fulfill the raise amount" $
let result =
--it "returns the billing date in the presence of an expired payment request" $
-- forAll ((,) <$> genSatoshi <*> listOf genBid) $
-- \(raiseAmount', bids) ->
-- case runAuction' raiseAmount' bids of
-- WinningBids xs -> bidsTotal xs == raiseAmount'
-- InsufficientBids t -> t == (raiseAmount' - bidsTotal bids)
--it "returns the billing date in the presence of an expired payment request" $
-- forAll ((,) <$> genSatoshi <*> listOf genBid) $
-- \(raiseAmount', bids) ->
-- case runAuction' raiseAmount' bids of
-- WinningBids xs -> bidsTotal xs == raiseAmount'
-- InsufficientBids t -> t == (raiseAmount' - bidsTotal bids)
import qualified Aftok.Interval as I
import Aftok.TimeLog
import Control.Lens ((^.))
import Data.AffineSpace
import qualified Data.List.NonEmpty as L
import qualified Data.Map.Strict as M
import Data.Thyme.Time as T
import Data.Time.ISO8601
import Haskoin.Address (Address)
import Haskoin.Util.Arbitrary.Address (arbitraryAddress)
import Test.Hspec
import Test.QuickCheck
import Control.Lens ( (^.) )
import Data.AffineSpace
import qualified Data.List.NonEmpty as L
import qualified Data.Map.Strict as M
import Data.Thyme.Time as T
import Data.Time.ISO8601
import Haskoin.Address ( Address )
import qualified Aftok.Interval as I
import Aftok.TimeLog
import Test.Hspec
import Test.QuickCheck
import Haskoin.Util.Arbitrary.Address ( arbitraryAddress )
let starts = toThyme <$> catMaybes
[ parseISO8601 "2014-01-01T00:08:00Z"
, parseISO8601 "2014-01-01T00:12:00Z"
]
ends = toThyme <$> catMaybes
[ parseISO8601 "2014-01-01T00:11:59Z"
, parseISO8601 "2014-01-01T00:18:00Z"
]
let starts =
toThyme
<$> catMaybes
[ parseISO8601 "2014-01-01T00:08:00Z",
parseISO8601 "2014-01-01T00:12:00Z"
]
ends =
toThyme
<$> catMaybes
[ parseISO8601 "2014-01-01T00:11:59Z",
parseISO8601 "2014-01-01T00:18:00Z"
]