module Web.Stripe.Subscription ( Subscription(..) , SubStatus(..) , SubProrate(..) , SubTrialEnd(..) , SubAtPeriodEnd(..) , updateSubRCard , updateSubToken , cancelSub {- Re-Export -} , UTCTime(..) , SConfig(..) , StripeT(StripeT) , runStripeT ) where import Control.Applicative ( (<$>) ) import Control.Monad ( liftM, ap ) import Control.Monad.Error ( MonadIO ) import Data.Char ( toLower ) import Network.HTTP.Types ( StdMethod(..) ) import Text.JSON ( Result(Error), JSON(..), JSValue(JSObject) ) import Web.Stripe.Card ( RequestCard, rCardKV ) import Web.Stripe.Client ( StripeT(..), SConfig(..), SRequest(..), baseSReq , query, runStripeT ) import Web.Stripe.Coupon ( CpnId(..) ) import Web.Stripe.Customer ( CustomerId(..) ) import Web.Stripe.Token ( TokenId(..) ) import Web.Stripe.Plan ( Plan, PlanId(..) ) import Web.Stripe.Utils ( UTCTime(..), fromSeconds, jGet, optionalArgs ) ------------------ -- Subsriptions -- ------------------ -- | Represents a subscription in the Stripe API. data Subscription = Subscription { subCustomerId :: CustomerId , subPlan :: Plan , subStatus :: SubStatus , subStart :: UTCTime , subTrialStart :: UTCTime , subTrialEnd :: UTCTime , subPeriodStart :: UTCTime -- ^ Current period start , subPeriodEnd :: UTCTime -- ^ Current period end } deriving Show -- | Describes the various stages that a data SubStatus = Trialing | Active | PastDue | Unpaid | Canceled | UnknownStatus String deriving Show -- | A boolean flag that determines whether or not to prorate switching plans -- during a billing cycle. newtype SubProrate = SubProrate { unSubProrate :: Bool } deriving Show -- | UTC integer timestamp representing the end of the trial period that the -- customer receives before being charged for the first time. newtype SubTrialEnd = SubTrialEnd { unSubTrialEnd :: Int } deriving Show -- | A boolean flag that determines whether or not the cancellation of the -- 'Subscription' should be delayed until the end of the current period. newtype SubAtPeriodEnd = SubAtPeriodEnd { unSubAtPeriodEnd :: Bool } deriving Show -- | Update the subscription associated with a 'Customer', identified by -- 'CustomerId', in the Stripe system. -- -- If 'SubTrialEnd' is provided, this will override the default trial period -- of the plan to which the customer is subscribed. updateSubRCard :: MonadIO m => RequestCard -> CustomerId -> PlanId -> Maybe CpnId -> Maybe SubProrate -> Maybe SubTrialEnd -> StripeT m Subscription updateSubRCard = updateSub . rCardKV -- | Behaves precisely like 'updateSubRCard', but uses a 'Token', identified by -- 'TokenId', rather than a 'RequestCard'. updateSubToken :: MonadIO m => TokenId -> CustomerId -> PlanId -> Maybe CpnId -> Maybe SubProrate -> Maybe SubTrialEnd -> StripeT m Subscription updateSubToken (TokenId tid) = updateSub [("token", tid)] -- | Internal convenience function to update a 'Subscription'. updateSub :: MonadIO m => [(String, String)] -> CustomerId -> PlanId -> Maybe CpnId -> Maybe SubProrate -> Maybe SubTrialEnd -> StripeT m Subscription updateSub sdata cid pid mcpnid mspr mste = snd `liftM` query (subRq cid []) { sMethod = POST, sData = fdata } where fdata = ("plan", unPlanId pid) : sdata ++ optionalArgs odata odata = [ ("coupon", unCpnId <$> mcpnid) , ("prorate", show . unSubProrate <$> mspr) , ("trial_end", show . unSubTrialEnd <$> mste) ] -- | Cancels the 'Subscription' associated with a 'Customer', identified by -- 'CustomerId', in the Stripe system. cancelSub :: MonadIO m => CustomerId -> Maybe SubAtPeriodEnd -> StripeT m Subscription cancelSub cid mspe = snd `liftM` query (subRq cid []) { sMethod = DELETE, sData = optionalArgs odata } where odata = [("at_period_end", show . unSubAtPeriodEnd <$> mspe)] -- | Convenience function to create a 'SRequest' specific to -- subscription-related actions. subRq :: CustomerId -> [String] -> SRequest subRq (CustomerId cid) pcs = baseSReq { sDestination = "customers":cid:"subscription":pcs } ------------------ -- JSON Parsing -- ------------------ -- | Convert a string to a 'SubStatus'. If the code is not known, -- 'UnkownStatus' will be returned with the originally provided code. toSubStatus :: String -> SubStatus toSubStatus s = case map toLower s of "trialing" -> Trialing "active" -> Active "past_due" -> PastDue "canceled" -> Canceled "unpaid" -> Unpaid _ -> UnknownStatus s -- | Attempts to parse JSON into a 'Subscription'. instance JSON Subscription where readJSON (JSObject c) = Subscription `liftM` (CustomerId <$> jGet c "customer") `ap` jGet c "plan" `ap` (toSubStatus <$> jGet c "status") `ap` (fromSeconds <$> jGet c "start") `ap` (fromSeconds <$> jGet c "trial_start") `ap` (fromSeconds <$> jGet c "trial_end") `ap` (fromSeconds <$> jGet c "current_period_start") `ap` (fromSeconds <$> jGet c "current_period_end") readJSON _ = Error "Unable to read Stripe subscription." showJSON _ = undefined