These are the necessary functions that will support the future development of the payment request endpoint and the scheduled bill processing system.
SEWTRB6S5PO5MQBLCPVBD7XT2BDYNZUE2RO6Z2XENZRIOCN6OZJAC HMDM3B557TO5RYP2IGFFC2C2VN6HYZTDQ47CJY2O37BW55DSMFZAC HALRDT2F22DAQ54M334YSKD2XJCAXDXEZPDKGMOWKH7XELBAS6MAC DFOBMSAODB3NKW37B272ZXA2ML5HMIH3N3C4GT2DPEQS7ZFK4SNAC 64C6AWH66FDKU6UE6Z6JPX2J2GBM2JOPTH2GL6LHKAIUBGNGDZ5AC 4U7F3CPIDTK6JSEDMNMHVKSR7HOQDLZQD2PPVMDLHO5SFSIMUXZAC 73NDXDEZRMK672GHSTC3CI6YHXFZ2GGJI5IKQGHKFDZKTNSQXLLQC Q5X5RYQLP5K7REYD6VLHOKC4W36ZELJYA45V6YFKTD5S6MPN3NDQC NEDDHXUK3GNFMOFO3KLU7NRIHCTYNWBT3D6HTKZAOXFDE6HMDZ6AC IZEVQF627FA7VV25KJAWYWGGC35LZUUBBQRPN5ZAINTQLPEDRTEAC Y3LIJ5USPMYBG7HKCIQBE7MWVGLQJPJSQD3KPZCTKXP22GOB535QC WAIX6AGNDVJOKTWZ7OP7QOYSJHAJSX5EOWXZHOAO2IG6ALWUCJ6QC O227CEAV7BTKSE3SSC7XHC5IWEBXZL2AOOKJMBMOOFNTLINBLQMAC 2XQD6KKKD6QVHFHAEMVE3XXY7X2T7BLTLL7XIILZAXNJJH2YONUQC EKY7U7SKPF45OOUAHJBEQKXSUXWOHFBQFFVJWPBN5ARFJUFM2BPAC GCVQD44VRPQVKPZEPIC4AOIXLJIG2ZMV3QI2Y7KALUT6NVUBSGSAC QMRKFEPGFBCEWAIXPEIG5ILKAJ2JH5L3TOITHR4HNJXK5FN3KXBQC TNR3TEHKVADAEZSTOD2XLSUTSW5AWST2YUW4CWK5KE7DSC6XHZNAC O5FVTOM6YFBLEPF3S576K6IMT6ZZ5VQCSB3YVXNS4CKBITKCRZ7AC QADKFHAR3KWQCNYU25Z7PJUGMD5WL26IU3DOAHBTRN2A7NKPUPKAC NLZ3JXLOOIL37O3RRQWXHNPNSNEOOLPD6MCB754BEBECQB3KGR2AC BWN72T44GRRZ6K2OPN56FTLNEB7J7AGC7T2U5HSMLEKUPGJP2NUAC RN7EI6INGUUHGMNY5RU3NH56WPLRZY5ZMYDNFMNE3TGV4ESFLQIAC W35DDBFYF6Z4ZPCFEO5RPAONZLYCSNTXUSTS6FIUUVZHCI6Q7GHAC Z3MK2PJ5U222DXRS22WCDHVPZ7HVAR3HOCUNXIGX6VMEPBQDF6PQC PBD7LZYQHXAA3KLH2ZUX5GW4UFML6BQ32KXZF4KZ6OYFASUYFJ5QC XTBSG4C7SCZUFOU2BTNFR6B6TCGYI35BWUV4PVTS3N7KNH5VEARQC KEP5WUFJXTMKRRNZLYTGYYWA4VLFCMHTKTJYF5EA5IWBYFMU6WYQC {-# LANGUAGE TemplateHaskell #-}module Aftok.Time whereimport ClassyPreludenewtype Days = Days IntmakePrisms ''Daysimport Control.Lens (makePrisms)
nextRecurrence :: Recurrence -> T.Day -> Maybe T.DaynextRecurrence r = case r ofAnnually -> Just . T.addGregorianYearsClip 1Monthly m -> Just . T.addGregorianMonthsClip mWeekly w -> Just . T.addDays (w * 7)OneTime -> const Nothing{-- A stream of dates upon which the specified subscription- should be billed, beginning with the first day of the- subscription.-}billingSchedule :: Subscription' Billable -> [T.Day]billingSchedule s =let rec = view (billable . recurrence) ssubEndDay = preview (endTime . _Just . C._utctDay) snext :: Maybe T.Day -> Maybe (T.Day, Maybe T.Day)next d = dod' <- dif (all (d' <) subEndDay) then Just (d', nextRecurrence rec d') else Nothingin unfoldr next (Just $ view (startTime . C._utctDay) s)
uidParser :: RowParser UserIduidParser = UserId <$> fieldpidParser :: RowParser P.ProjectIdpidParser = P.ProjectId <$> field
idParser :: (UUID -> a) -> RowParser aidParser f = f <$> field
parser "credit_to_user" = CreditToUser <$> (nullField *> uidParser <* nullField)parser "credit_to_project" = CreditToProject <$> (nullField *> nullField *> pidParser)
parser "credit_to_user" = CreditToUser <$> (nullField *> idParser UserId <* nullField)parser "credit_to_project" = CreditToProject <$> (nullField *> nullField *> idParser P.ProjectId)
qdbUserParser :: RowParser KeyedUserqdbUserParser =(,) <$> uidParser<*> userParser
qdbProjectParser :: RowParser KeyedProjectqdbProjectParser =(,) <$> pidParser<*> projectParser
paymentRequestParser :: RowParser PaymentRequestpaymentRequestParser =PaymentRequest <$> (B.SubscriptionId <$> field)<*> (field >>= (either fail pure . runGet decodeMessage))<*> (toThyme <$> field)<*> (toThyme <$> field)paymentParser :: RowParser PaymentpaymentParser =Payment <$> (PaymentRequestId <$> field)<*> (field >>= (either fail pure . runGet decodeMessage))<*> (toThyme <$> field)
pgEval (ReadBids aucId) =pquery bidParser"SELECT user_id, bid_seconds, bid_amount, bid_time FROM bids WHERE auction_id = ?"
pgEval (FindBids aucId) =pquery ((,) <$> idParser A.BidId <*> bidParser)"SELECT id, user_id, bid_seconds, bid_amount, bid_time FROM bids WHERE auction_id = ?"
pgEval dbop @ (CreatePayment _ req) = do
pgEval (FindPaymentRequest rid) =headMay <$> pquery paymentRequestParser"SELECT subscription_id, request_data, request_time, billing_date \\FROM payment_requests \\WHERE id = ?"(Only (rid ^. _PaymentRequestId))pgEval (FindPaymentRequests sid) =pquery ((,) <$> idParser PaymentRequestId <*> paymentRequestParser)"SELECT id, subscription_id, request_data, request_time, billing_date \\FROM payment_requests \\WHERE subscription_id = ?"(Only (sid ^. B._SubscriptionId))pgEval dbop @ (CreatePayment _ p) = do
CreatePaymentRequest :: UserId -> PaymentRequest -> DBOp PaymentRequestIdCreatePayment :: UserId -> Payment -> DBOp PaymentId
CreatePaymentRequest :: UserId -> PaymentRequest -> DBOp PaymentRequestIdFindPaymentRequest :: PaymentRequestId -> DBOp (Maybe PaymentRequest)FindPaymentRequests :: SubscriptionId -> DBOp [(PaymentRequestId, PaymentRequest)]CreatePayment :: UserId -> Payment -> DBOp PaymentIdFindPayments :: PaymentRequestId -> DBOp [(PaymentId, Payment)]
readBillable :: (MonadDB m) => BillableId -> m (Maybe Billable)readBillable = liftdb . ReadBillable
findBillable :: (MonadDB m) => BillableId -> MaybeT m BillablefindBillable = MaybeT . liftdb . FindBillablefindSubscriptions :: (MonadDB m) => UserId -> ProjectId -> m [(SubscriptionId, Subscription)]findSubscriptions uid pid = liftdb $ FindSubscriptions uid pidfindSubscriptionBillable :: (MonadDB m) => SubscriptionId -> MaybeT m (Subscription' Billable)findSubscriptionBillable sid = dosub <- MaybeT . liftdb $ FindSubscription sidtraverse findBillable sub
findSubscriptions :: (MonadDB m)=> UserId-> ProjectId-> m [(SubscriptionId, Subscription' Billable)]findSubscriptions uid pid = dosubscriptions <- liftdb $ FindSubscriptions uid pidlet sub'' s = sequenceA <$> traverse readBillable ssub' (sid, s) = fmap (fmap (sid,)) (sub'' s)catMaybes <$> traverse sub' subscriptions
findPaymentRequests :: (MonadDB m) => SubscriptionId -> m [(PaymentRequestId, PaymentRequest)]findPaymentRequests = liftdb . FindPaymentRequests
readPaymentHistory :: (MonadDB m) => UserId -> m [Payment]readPaymentHistory = error "Not yet implemented"
findPayment :: (MonadDB m) => PaymentRequestId -> m (Maybe Payment)findPayment prid = (fmap snd . headMay) <$> liftdb (FindPayments prid)
{- Check whether the specified payment request has expired (whether wallet software- will still consider the payment request valid)-}isExpired :: C.UTCTime -> P.PaymentRequest -> BoolisExpired now req =let check = any ((now >) . T.toThyme . expiryTime)-- using error here is reasonable since it would indicate-- a serialization problemin either error (check . getExpires) $ getPaymentDetails req
makeLenses ''BillingConfig
makeClassy ''BillingConfigdata BillingOps (m :: * -> *) = BillingOps{ 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}data PaymentRequestStatus= 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
createPaymentRequests :: (MonadRandom m, MonadReader BillingConfig m, MonadError Error m, MonadDB m) =>C.UTCTime -- timestamp for payment request creation-> (Subscription' Billable -> m (Maybe Text)) -- generator user memo-> (Subscription' Billable -> m (Maybe URI)) -- generator for payment response URL-> (Subscription' Billable -> m (Maybe ByteString)) -- generator for merchant payload-> UserId -- user responsible for payment-> ProjectId -- project whose worklog is to be paid out to
createPaymentRequests :: ( MonadRandom m, MonadReader r m, HasBillingConfig 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
join <$> traverse (createSubscriptionPaymentRequests ops now custId) subscriptionscreateSubscriptionPaymentRequests ::( MonadRandom m, MonadReader r m, HasBillingConfig r, MonadError e m, AsPaymentError e, MonadDB m)=> BillingOps m-> C.UTCTime-> UserId-> (SubscriptionId, Subscription)-> m [PaymentRequestId]createSubscriptionPaymentRequests ops now custId (sid, sub) = dobillableSub <- maybeT (raiseSubjectNotFound . FindBillable $ sub ^. billable) pure $traverse findBillable subpaymentRequests <- findPaymentRequests sidbillableDates <- findUnbilledDates now (view billable billableSub) paymentRequests $takeWhile (< view _utctDay now) $ billingSchedule billableSubtraverse (createPaymentRequest ops now custId sid billableSub) billableDatescreatePaymentRequest ::( MonadRandom m, MonadReader r m, HasBillingConfig r, MonadError e m, AsPaymentError e, MonadDB m)=> BillingOps m-> C.UTCTime-> UserId-> SubscriptionId-> Subscription' Billable-> T.Day-> m PaymentRequestIdcreatePaymentRequest ops now custId sid sub bday = do
let createPaymentDetails' s = domemo <- memogen suri <- urigen spayload <- plgen screatePaymentDetails t memo uri payload (s ^. billable)
memo <- memoGen ops suburi <- uriGen ops subpayload <- payloadGen ops subdetails <- createPaymentDetails bday now memo uri payload (sub ^. billable)reqErr <- B.createPaymentRequest (cfg ^. signingKey) (cfg ^. pkiData) detailsreq <- either (throwError . review _SigningError) pure reqErrliftdb $ CreatePaymentRequest custId (PaymentRequest sid req now bday){-- FIXME: The current implementation expects the billing day to be a suitable- key for comparison to payment requests. This is almost certainly inadequate.-}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 existsfindUnbilledDates now b (px @ (p : ps)) (dx @ (d : ds)) =case compare (view (_2 . billingDate) p) d ofEQ -> getRequestStatus now p >>= \s -> case s ofExpired 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 doGT -> fmap (d :) $ findUnbilledDates now b px dsLT -> findUnbilledDates now b ps dxfindUnbilledDates _ _ _ ds = pure ds
createPaymentRequest (sid, s) = dodetails <- createPaymentDetails' sreq <- B.createPaymentRequest (cfg ^. signingKey) (cfg ^. pkiData) detailsliftdb $ CreatePaymentRequest custId (PaymentRequest sid req t)traverse createPaymentRequest subscriptions
{- Check whether the specified payment request has a payment associated with- it, and return a PaymentRequestStatus value indicating the result.-}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 PaymentRequestStatusgetRequestStatus now (reqid, req) =let ifUnpaid = (if isExpired now (view paymentRequest req) then Expired else Unpaid) reqin maybe ifUnpaid Paid <$> findPayment reqid
createPaymentDetails :: (MonadRandom m, MonadReader BillingConfig m, MonadDB m) =>C.UTCTime -- timestamp for payment request creation-> Maybe Text -- user memo-> Maybe URI -- payment response URL-> Maybe ByteString -- merchant payload-> Billable
{- Create the PaymentDetails section of the payment request.-}createPaymentDetails :: (MonadRandom m, MonadReader r m, HasBillingConfig r, 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
createPaymentDetails t memo uri payload b = dopayouts <- getProjectPayouts t (b ^. project)outputs <- createPayoutsOutputs t (b ^. amount) payoutslet expiry = (BT.Expiry . fromThyme . (t .+^)) <$> (b ^. requestExpiryPeriod)
createPaymentDetails payoutDate billingTime memo uri payload b = dopayouts <- getProjectPayouts payoutTime (b ^. project)outputs <- createPayoutsOutputs payoutTime (b ^. amount) payoutslet expiry = (BT.Expiry . T.fromThyme . (billingTime .+^)) <$> (b ^. requestExpiryPeriod)
pure $ B.createPaymentDetails (cfg ^. network) outputs (fromThyme t) expiry memo uri payload
pure $ B.createPaymentDetails(cfg ^. network)outputs(T.fromThyme billingTime)expiry memo uri payloadwherepayoutTime = T.mkUTCTime payoutDate (fromInteger 0)