Implement payments service endpoints.
[?]
Feb 23, 2017, 4:29 AM
AL37SVTCKRSG4HG2PCYK5Z7QSIZZH5JHH4Q2VLMXFAXSAQRFFG4QCDependencies
- [2]
JFOEOFGAstylish-haskell formatting. - [3]
GCVQD44VCreate amends endpoint, switch to UUID primary keys - [4]
5ZSKPQ3KAdd created_at and auction_start timestamps to auction - [5]
PBD7LZYQPostgres & auth are beginning to function. - [6]
2XQD6KKKAdd invitation logic and clean up DBProg error handling. - [7]
NAS4BFL4Trivial stylish-haskell reformat. - [8]
WAIX6AGNAdd event serialization for PaymentRequest & Payment - [9]
Y3LIJ5USAdd handler for CreatePaymentRequest - [10]
BROSTG5KBeginning of modularization of server. - [11]
I2KHGVD4Require project permissions for access to most data. - [12]
DFOBMSAOInitial work on payments API - [13]
HALRDT2FAdded initial auction create route. - [14]
Z3MK2PJ5Add GET handler for retrieving auction data. - [15]
QMRKFEPGRefactor QDB to use a free monad algebra instead. - [16]
73NDXDEZBegin implementation of billing event persistence. - [17]
O227CEAVAdds storage of original event JSON for some DBOp constructors. - [18]
Q5X5RYQLstylish-haskell reformatting - [19]
O5FVTOM6Undo JSON silliness, enable a couple more routes. - [20]
ASF3UPJLAdd auction creation and bid handlers - [21]
MJ6R42RCUtility methods for reading key & cert data. - [22]
7VGYLTMUClean up schema version handling. - [23]
SEWTRB6SImplement payment request creation functions. - [24]
RPAJLHMTChange to use UUIDs instead of ints for primary keys. - [25]
NEDDHXUKReformat via stylish-haskell - [26]
HMDM3B55Implement core of payments/billing infrastructure. - [*]
64C6AWH6Rename Ananke -> Quixotic, project reboot. - [*]
IZEVQF62Work in progress replacing sqlite with postgres. - [*]
W35DDBFYFactor common JSON conversions up into client lib module. - [*]
NLZ3JXLOFix formatting with stylish-haskell. - [*]
ADMKQQGCInitial empty Snap project. - [*]
O722AOKEAdd route to allow crediting of events to users/projects.
Change contents
- edit in aftok.cabal at line 132
, Aftok.Snaplet.Payments - edit in aftok.cabal at line 146
, cereal - edit in aftok.cabal at line 163
, protobuf - edit in lib/Aftok/Billables.hs at line 4
{-# LANGUAGE ExplicitForAll #-} - replacement in lib/Aftok/Billables.hs at line 78
data Subscription' b = Subscription{ _billable :: bdata Subscription' u b = Subscription{ _customer :: u, _billable :: b - replacement in lib/Aftok/Billables.hs at line 86
type Subscription = Subscription' BillableIdtype Subscription = Subscription' UserId BillableId - replacement in lib/Aftok/Billables.hs at line 100
billingSchedule :: Subscription' Billable -> [T.Day]billingSchedule :: forall u. Subscription' u Billable -> [T.Day] - replacement in lib/Aftok/Database/PostgreSQL.hs at line 167
B.Subscription <$> (B.BillableId <$> field)B.Subscription <$> idParser UserId<*> idParser B.BillableId - replacement in lib/Aftok/Database/PostgreSQL.hs at line 210
storeEvent (CreateSubscription uid s) =Just $ storeEventJSON uid "create_subscription" (createSubscriptionJSON uid s)storeEvent (CreateSubscription uid bid) =Just $ storeEventJSON uid "create_subscription" (createSubscriptionJSON uid bid) - replacement in lib/Aftok/Database/PostgreSQL.hs at line 450
pgEval dbop @ (CreateSubscription uid s) = dopgEval dbop @ (CreateSubscription uid bid) = do - replacement in lib/Aftok/Database/PostgreSQL.hs at line 456
(uid ^. _UserId, s ^. (B.billable . B._BillableId), eventId ^. _EventId)( view _UserId uid, view B._BillableId bid, view _EventId eventId) - replacement in lib/Aftok/Database/PostgreSQL.hs at line 470
"SELECT id, billable_id, start_date, end_date \"SELECT id, user_id, billable_id, start_date, end_date \ - edit in lib/Aftok/Database/PostgreSQL.hs at line 505
pgEval (FindUnpaidRequests sid) =let rowp :: RowParser (PaymentRequestId, PaymentRequest, B.Subscription, B.Billable)rowp = (,,,) <$> idParser PaymentRequestId<*> paymentRequestParser<*> subscriptionParser<*> billableParserin pquery rowp"SELECT id, \\ r.subscription_id, r.request_data, r.request_time, r.billing_date, \\ s.user_id, s.billable_id, s.start_date, s.end_date, \\ b.project_id, e.created_by, b.name, b.description, b.recurrence_type, \\ b.recurrence_count, b.billing_amount, b.grace_period_days \\FROM payment_requests r \\JOIN subscriptions s on s.id = r.subscription_id \\JOIN billables b on b.id = s.billable_id \\WHERE subscription_id = ? \\AND r.id NOT IN (SELECT payment_request_id FROM payments)"(Only (sid ^. B._SubscriptionId)) - replacement in lib/Aftok/Database.hs at line 55[3.13383]→[3.6500:6570](∅→∅),[3.6570]→[3.4887:4954](∅→∅),[3.4954]→[3.6570:6654](∅→∅),[3.6570]→[3.6570:6654](∅→∅)
CreateSubscription :: UserId -> Subscription -> DBOp SubscriptionIdFindSubscription :: SubscriptionId -> DBOp (Maybe Subscription)FindSubscriptions :: UserId -> ProjectId -> DBOp [(SubscriptionId, Subscription)]CreateSubscription :: UserId -> BillableId -> DBOp SubscriptionIdFindSubscription :: SubscriptionId -> DBOp (Maybe Subscription)FindSubscriptions :: UserId -> ProjectId -> DBOp [(SubscriptionId, Subscription)] - edit in lib/Aftok/Database.hs at line 62
FindUnpaidRequests :: SubscriptionId -> DBOp [BillDetail] - edit in lib/Aftok/Database.hs at line 71
| UserNotSubscriber SubscriptionId - replacement in lib/Aftok/Database.hs at line 197
findSubscriptionBillable :: (MonadDB m) => SubscriptionId -> MaybeT m (Subscription' Billable)findSubscriptionBillable :: (MonadDB m) => SubscriptionId -> MaybeT m (Subscription' UserId Billable) - edit in lib/Aftok/Database.hs at line 205
-- this could be implemented in terms of other operations, but it's-- much cleaner to just do the joins in the databasefindUnpaidRequests :: (MonadDB m) => SubscriptionId -> m [BillDetail]findUnpaidRequests = liftdb . FindUnpaidRequests - edit in lib/Aftok/Json.hs at line 3
{-# LANGUAGE RankNTypes #-} - edit in lib/Aftok/Json.hs at line 33
import Aftok.Payments.Types (BillDetail) - edit in lib/Aftok/Json.hs at line 103
idJSON :: forall a. Lens' a UUID -> a -> ValueidJSON l a = toJSON . tshow $ view l a - replacement in lib/Aftok/Json.hs at line 180[3.1605]→[3.1605:1627](∅→∅),[3.1627]→[3.552:620](∅→∅),[3.620]→[3.1690:1836](∅→∅),[3.1690]→[3.1690:1836](∅→∅),[3.1836]→[3.10115:10168](∅→∅),[3.10168]→[3.6328:6374](∅→∅),[3.6374]→[3.10224:10305](∅→∅),[3.10224]→[3.10224:10305](∅→∅),[3.10305]→[3.1836:1844](∅→∅),[3.1836]→[3.1836:1844](∅→∅)
billableJSON b = v1 $obj [ "projectId" .= (b ^. (B.project . _ProjectId . to tshow)), "name" .= (b ^. B.name), "description" .= (b ^. B.description), "recurrence" .= recurrenceJSON' (b ^. B.recurrence), "amount" .= (b ^. (B.amount . satoshi)), "gracePeriod" .= (b ^. B.gracePeriod), "requestExpiryPeriod" .= (C.toSeconds' <$> (b ^. B.requestExpiryPeriod))]billableJSON = v1 . obj . billableKVbillableKV :: (KeyValue kv) => B.Billable -> [kv]billableKV b =[ "projectId" .= (b ^. (B.project . _ProjectId . to tshow)), "name" .= (b ^. B.name), "description" .= (b ^. B.description), "recurrence" .= recurrenceJSON' (b ^. B.recurrence), "amount" .= (b ^. (B.amount . satoshi)), "gracePeriod" .= (b ^. B.gracePeriod), "requestExpiryPeriod" .= (C.toSeconds' <$> (b ^. B.requestExpiryPeriod))] - replacement in lib/Aftok/Json.hs at line 200
createSubscriptionJSON :: UserId -> B.Subscription -> ValuecreateSubscriptionJSON uid sub = v1 $obj [ "user_id" .= tshow (uid ^. _UserId), "billable_id" .= tshow (sub ^. (B.billable . B._BillableId))createSubscriptionJSON :: UserId -> B.BillableId -> ValuecreateSubscriptionJSON uid bid = v1 $obj [ "user_id" .= idJSON _UserId uid, "billable_id" .= idJSON B._BillableId bid - edit in lib/Aftok/Json.hs at line 205
subscriptionJSON :: B.Subscription -> ValuesubscriptionJSON = v1 . obj . subscriptionKV - edit in lib/Aftok/Json.hs at line 209
subscriptionKV :: (KeyValue kv) => B.Subscription -> [kv]subscriptionKV sub =[ "user_id" .= idJSON (B.customer . _UserId) sub, "billable_id" .= idJSON (B.billable . B._BillableId) sub, "start_time" .= view B.startTime sub, "end_time" .= view B.endTime sub] - replacement in lib/Aftok/Json.hs at line 218[3.676]→[3.676:907](∅→∅),[3.907]→[3.6443:6569](∅→∅),[3.6569]→[3.967:975](∅→∅),[3.967]→[3.967:975](∅→∅)
paymentRequestJSON r = v1 $obj [ "subscription_id" .= (r ^. (subscription . B._SubscriptionId . to tshow)), "payment_request_protobuf_64" .= (r ^. (paymentRequest . to (decodeUtf8 . B64.encode . runPut . encodeMessage))), "payment_request_time" .= (r ^. paymentRequestTime), "billing_date" .= (r ^. (billingDate . to showGregorian))]paymentRequestJSON = v1 . obj . paymentRequestKVpaymentRequestKV :: (KeyValue kv) => PaymentRequest -> [kv]paymentRequestKV r =[ "subscription_id" .= (r ^. (subscription . B._SubscriptionId . to tshow)), "payment_request_protobuf_64" .= (r ^. (paymentRequest . to (decodeUtf8 . B64.encode . runPut . encodeMessage))), "payment_request_time" .= (r ^. paymentRequestTime), "billing_date" .= (r ^. (billingDate . to showGregorian))]billDetailsJSON :: [BillDetail] -> ValuebillDetailsJSON r = v1 $obj ["payment_requests" .= fmap billDetailJSON r ] - edit in lib/Aftok/Json.hs at line 232
billDetailJSON :: BillDetail -> ObjectbillDetailJSON r =obj $ concat[ ["payment_request_id" .= idJSON _PaymentRequestId (view _1 r)], paymentRequestKV $ view _2 r, subscriptionKV $ view _3 r, billableKV $ view _4 r] - edit in lib/Aftok/Payments/Types.hs at line 4
{-# LANGUAGE ExplicitForAll #-} - replacement in lib/Aftok/Payments/Types.hs at line 11
import Control.Lens (makeLenses, makePrisms)import Control.Lens (makeLenses, makePrisms, view) - replacement in lib/Aftok/Payments/Types.hs at line 20
import Aftok.Billables (SubscriptionId)import Aftok.Billables (Billable, Subscription, SubscriptionId) - edit in lib/Aftok/Payments/Types.hs at line 47
type BillDetail = (PaymentRequestId, PaymentRequest, Subscription, Billable) - replacement in lib/Aftok/Payments/Types.hs at line 52
isExpired :: C.UTCTime -> P.PaymentRequest -> BoolisExpired :: forall s. C.UTCTime -> PaymentRequest' s -> Bool - replacement in lib/Aftok/Payments/Types.hs at line 57
in either error (check . getExpires) $ getPaymentDetails reqin either error (check . getExpires) $ getPaymentDetails (view paymentRequest req) - replacement in lib/Aftok/Payments.hs at line 48
{ memoGen :: Subscription' Billable -> m (Maybe Text) -- ^ generator user memo, uriGen :: Subscription' Billable -> m (Maybe URI) -- ^ generator for payment response URL, payloadGen :: Subscription' Billable -> m (Maybe ByteString) -- ^ generator for merchant payload{ memoGen :: Subscription' UserId Billable -> m (Maybe Text) -- ^ generator user memo, uriGen :: Subscription' UserId Billable -> m (Maybe URI) -- ^ generator for payment response URL, payloadGen :: Subscription' UserId Billable -> m (Maybe ByteString) -- ^ generator for merchant payload - edit in lib/Aftok/Payments.hs at line 63
- replacement in lib/Aftok/Payments.hs at line 106
-> Subscription' Billable-> Subscription' UserId Billable - replacement in lib/Aftok/Payments.hs at line 149
let ifUnpaid = (if isExpired now (view paymentRequest req) then Expired else Unpaid) reqlet ifUnpaid = (if isExpired now req then Expired else Unpaid) req - edit in lib/Aftok/Payments.hs at line 210[3.15982]
findPayableRequests :: (MonadDB m) => UserId -> SubscriptionId -> C.UTCTime -> m [BillDetail]findPayableRequests uid sid now = dorequests <- liftdb findOpjoin <$> (traverse checkAccess $ filter (not . isExpired now . view _2) requests)wherefindOp = FindUnpaidRequests sidcheckAccess d =if view (_3 . customer) d == uidthen pure [d]else raiseOpForbidden uid (UserNotSubscriber sid) findOp - edit in server/Aftok/Snaplet/Auctions.hs at line 15
import Snap.Snaplet as S - replacement in server/Aftok/Snaplet/Auctions.hs at line 24
import Aftok.Snaplet as Simport Aftok.Snaplet - edit in server/Aftok/Snaplet/Auctions.hs at line 27[3.2963]→[2.5345:5388](∅→∅),[3.986]→[3.2993:2994](∅→∅),[2.5388]→[3.2993:2994](∅→∅),[3.17283]→[3.2993:2994](∅→∅),[3.2993]→[3.2993:2994](∅→∅)
import Snap.Snaplet as S - replacement in server/Aftok/Snaplet/Auth.hs at line 6
import Data.Attoparsec.ByteString (parseOnly, takeByteString)import Data.UUID (fromASCIIBytes)import Data.Attoparsec.ByteString (parseOnly) - edit in server/Aftok/Snaplet/Auth.hs at line 9
import Aftok.Auction (AuctionId (..)) - edit in server/Aftok/Snaplet/Auth.hs at line 10
import Aftok.Project - edit in server/Aftok/Snaplet/Auth.hs at line 38[3.2162]→[3.8268:8315](∅→∅),[3.8315]→[3.8351:8512](∅→∅),[3.8512]→[3.8479:8608](∅→∅),[3.8479]→[3.8479:8608](∅→∅),[3.8608]→[3.1294:1632](∅→∅),[3.1632]→[3.2558:2559](∅→∅),[3.2361]→[3.2558:2559](∅→∅),[3.8608]→[3.2558:2559](∅→∅),[3.11997]→[3.2558:2559](∅→∅),[3.2558]→[3.2558:2559](∅→∅)
requireProjectId :: MonadSnap m => m ProjectIdrequireProjectId = domaybePid <- parseParam "projectId" pidParsermaybe (snapError 400 "Value of parameter \"projectId\" cannot be parsed as a valid UUID")puremaybePidwherepidParser = dobs <- takeByteStringpure $ ProjectId <$> fromASCIIBytes bsrequireAuctionId :: MonadSnap m => m AuctionIdrequireAuctionId = domaybeAid <- parseParam "auctionId" aidParsermaybe (snapError 400 "Value of parameter \"auctionId\" cannot be parsed as a valid UUID")puremaybeAidwhereaidParser = dobs <- takeByteStringpure $ AuctionId <$> fromASCIIBytes bs - edit in server/Aftok/Snaplet/Payments.hs at line 4
import Control.Lens (view, _1, _2)import Data.Thyme.Clock as Cimport Network.Bippy.Proto as P - replacement in server/Aftok/Snaplet/Payments.hs at line 9
import Network.Bippyimport Network.Bippy.Typesimport Snap.Snaplet as Simport Aftok.Billablesimport Aftok.Payments - replacement in server/Aftok/Snaplet/Payments.hs at line 14
import Snap.Coreimport Snap.Snapletimport Aftok.Snapletimport Aftok.Snaplet.Auth - replacement in server/Aftok/Snaplet/Payments.hs at line 17
import Aftok.QConfiglistPayableRequestsHandler :: S.Handler App App [BillDetail]listPayableRequestsHandler = douid <- requireUserIdsid <- requireId "subscriptionId" SubscriptionIdnow <- liftIO $ C.getCurrentTimesnapEval $ findPayableRequests uid sid now - replacement in server/Aftok/Snaplet/Payments.hs at line 24
requestPaymentHandler :: QConfig -> Handler App ApprequestPaymentHandler cfg = do-- get payout percentages from payouts handlergetPaymentRequestHandler :: S.Handler App App P.PaymentRequestgetPaymentRequestHandler = do - replacement in server/Aftok/Snaplet/Payments.hs at line 27[3.10100]→[3.10100:10163](∅→∅),[3.10163]→[3.1355:1414](∅→∅),[3.1414]→[3.17779:17951](∅→∅),[3.17779]→[3.17779:17951](∅→∅)
pid <- requireProjectIdptime <- liftIO $ C.getCurrentTimecreatePaymentRequests ptime memogen urigen plgen uid pid-- look up outstanding subscriptions the user has for this project-- determine which subscriptions need to be paid-- create a payment request for each subscriptionsid <- requireId "subscriptionId" SubscriptionIdrid <- requireId "paymentRequestId" PaymentRequestIdnow <- liftIO $ C.getCurrentTimerequests <- snapEval $ findPayableRequests uid sid nowlet prMay = fmap (view (_2 . paymentRequest)) . headMay $ filter ((==) rid . view _1) requestsmaybe (snapError 404 $ "Outstanding payment request not found for id " <> tshow rid) pure prMay - edit in server/Aftok/Snaplet/Payments.hs at line 38[2.5393]
- replacement in server/Aftok/Snaplet.hs at line 13
import Data.Attoparsec.ByteString (Parser, parseOnly)import Data.Attoparsec.ByteString (Parser, parseOnly,takeByteString)import Data.UUID - edit in server/Aftok/Snaplet.hs at line 18
import Aftok.Auction (AuctionId (..)) - edit in server/Aftok/Snaplet.hs at line 21
import Aftok.Project (ProjectId (..)) - replacement in server/Aftok/Snaplet.hs at line 64
parseParam :: MonadSnap m => ByteString -> Parser a -> m aparseParam :: 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 - replacement in server/Aftok/Snaplet.hs at line 69
maybeBytes <- getParam namemaybeBytes <- getParam (encodeUtf8 name) - edit in server/Aftok/Snaplet.hs at line 76
requireId :: MonadSnap m=> Text -- ^ name of the parameter-> (UUID -> a) -- ^ constructor for the identifier-> m arequireId name f = domaybeId <- parseParam name idParsermaybe (snapError 400 $ "Value of parameter \"" <> name <> "\" is not a valid UUID") pure maybeIdwhereidParser = dobs <- takeByteStringpure $ f <$> fromASCIIBytes bs - edit in server/Aftok/Snaplet.hs at line 94
requireProjectId :: MonadSnap m => m ProjectIdrequireProjectId = requireId "projectId" ProjectId - edit in server/Aftok/Snaplet.hs at line 97[3.3829]
requireAuctionId :: MonadSnap m => m AuctionIdrequireAuctionId = requireId "auctionId" AuctionId - edit in server/Main.hs at line 6
import Data.ProtocolBuffers (encodeMessage)import Data.Serialize.Put (runPutLazy) - edit in server/Main.hs at line 17
import Aftok.Snaplet.Payments - edit in server/Main.hs at line 61[3.4231][33.2894]
payableRequestsRoute = serveJSON billDetailsJSON $ method GET listPayableRequestsHandlerpaymentRequestRoute = writeLBS . runPutLazy . encodeMessage =<< method GET getPaymentRequestHandler - edit in server/Main.hs at line 85
, ("subscriptions/:subscriptionId/payment_requests", payableRequestsRoute), ("subscriptions/:subscriptionId/payment_requests/:paymentRequestId", paymentRequestRoute)