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 where
import ClassyPrelude
newtype Days = Days Int
makePrisms ''Days
import Control.Lens (makePrisms)
nextRecurrence :: Recurrence -> T.Day -> Maybe T.Day
nextRecurrence r = case r of
Annually -> Just . T.addGregorianYearsClip 1
Monthly m -> Just . T.addGregorianMonthsClip m
Weekly 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) s
subEndDay = preview (endTime . _Just . C._utctDay) s
next :: Maybe T.Day -> Maybe (T.Day, Maybe T.Day)
next d = do
d' <- d
if (all (d' <) subEndDay) then Just (d', nextRecurrence rec d') else Nothing
in unfoldr next (Just $ view (startTime . C._utctDay) s)
uidParser :: RowParser UserId
uidParser = UserId <$> field
pidParser :: RowParser P.ProjectId
pidParser = P.ProjectId <$> field
idParser :: (UUID -> a) -> RowParser a
idParser 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 KeyedUser
qdbUserParser =
(,) <$> uidParser
<*> userParser
qdbProjectParser :: RowParser KeyedProject
qdbProjectParser =
(,) <$> pidParser
<*> projectParser
paymentRequestParser :: RowParser PaymentRequest
paymentRequestParser =
PaymentRequest <$> (B.SubscriptionId <$> field)
<*> (field >>= (either fail pure . runGet decodeMessage))
<*> (toThyme <$> field)
<*> (toThyme <$> field)
paymentParser :: RowParser Payment
paymentParser =
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 PaymentRequestId
CreatePayment :: UserId -> Payment -> DBOp PaymentId
CreatePaymentRequest :: UserId -> PaymentRequest -> DBOp PaymentRequestId
FindPaymentRequest :: PaymentRequestId -> DBOp (Maybe PaymentRequest)
FindPaymentRequests :: SubscriptionId -> DBOp [(PaymentRequestId, PaymentRequest)]
CreatePayment :: UserId -> Payment -> DBOp PaymentId
FindPayments :: PaymentRequestId -> DBOp [(PaymentId, Payment)]
readBillable :: (MonadDB m) => BillableId -> m (Maybe Billable)
readBillable = liftdb . ReadBillable
findBillable :: (MonadDB m) => BillableId -> MaybeT m Billable
findBillable = MaybeT . liftdb . FindBillable
findSubscriptions :: (MonadDB m) => UserId -> ProjectId -> m [(SubscriptionId, Subscription)]
findSubscriptions uid pid = liftdb $ FindSubscriptions uid pid
findSubscriptionBillable :: (MonadDB m) => SubscriptionId -> MaybeT m (Subscription' Billable)
findSubscriptionBillable sid = do
sub <- MaybeT . liftdb $ FindSubscription sid
traverse findBillable sub
findSubscriptions :: (MonadDB m)
=> UserId
-> ProjectId
-> m [(SubscriptionId, Subscription' Billable)]
findSubscriptions uid pid = do
subscriptions <- liftdb $ FindSubscriptions uid pid
let sub'' s = sequenceA <$> traverse readBillable s
sub' (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 -> Bool
isExpired now req =
let check = any ((now >) . T.toThyme . expiryTime)
-- using error here is reasonable since it would indicate
-- a serialization problem
in either error (check . getExpires) $ getPaymentDetails req
makeLenses ''BillingConfig
makeClassy ''BillingConfig
data 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) subscriptions
createSubscriptionPaymentRequests ::
( 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) = do
billableSub <- maybeT (raiseSubjectNotFound . FindBillable $ sub ^. billable) pure $
traverse findBillable sub
paymentRequests <- findPaymentRequests sid
billableDates <- findUnbilledDates now (view billable billableSub) paymentRequests $
takeWhile (< view _utctDay now) $ billingSchedule billableSub
traverse (createPaymentRequest ops now custId sid billableSub) billableDates
createPaymentRequest ::
( 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 PaymentRequestId
createPaymentRequest ops now custId sid sub bday = do
let createPaymentDetails' s = do
memo <- memogen s
uri <- urigen s
payload <- plgen s
createPaymentDetails t memo uri payload (s ^. billable)
memo <- memoGen ops sub
uri <- uriGen ops sub
payload <- payloadGen ops sub
details <- createPaymentDetails bday now memo uri payload (sub ^. billable)
reqErr <- B.createPaymentRequest (cfg ^. signingKey) (cfg ^. pkiData) details
req <- either (throwError . review _SigningError) pure reqErr
liftdb $ 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 exists
findUnbilledDates now b (px @ (p : ps)) (dx @ (d : ds)) =
case compare (view (_2 . billingDate) p) d of
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
GT -> fmap (d :) $ findUnbilledDates now b px ds
LT -> findUnbilledDates now b ps dx
findUnbilledDates _ _ _ ds = pure ds
createPaymentRequest (sid, s) = do
details <- createPaymentDetails' s
req <- B.createPaymentRequest (cfg ^. signingKey) (cfg ^. pkiData) details
liftdb $ 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 PaymentRequestStatus
getRequestStatus now (reqid, req) =
let ifUnpaid = (if isExpired now (view paymentRequest req) then Expired else Unpaid) req
in 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 = do
payouts <- getProjectPayouts t (b ^. project)
outputs <- createPayoutsOutputs t (b ^. amount) payouts
let expiry = (BT.Expiry . fromThyme . (t .+^)) <$> (b ^. requestExpiryPeriod)
createPaymentDetails payoutDate billingTime memo uri payload b = do
payouts <- getProjectPayouts payoutTime (b ^. project)
outputs <- createPayoutsOutputs payoutTime (b ^. amount) payouts
let 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 payload
where
payoutTime = T.mkUTCTime payoutDate (fromInteger 0)