From afa8602b53876dd151969fefc42bcaad79758aba Mon Sep 17 00:00:00 2001 From: Erik Post Date: Thu, 7 May 2020 22:09:10 +0200 Subject: [PATCH] console: Cover more Stripe API Surface (Plan, Product, etc). #376 --- console/package.json | 7 +- console/src/Statebox/Console.purs | 148 ++++++++++++++++++++++++- console/src/Statebox/Console/DAO.purs | 24 ++++ console/src/Statebox/Console/Main.purs | 2 + console/src/Stripe.purs | 43 ++++++- 5 files changed, 211 insertions(+), 13 deletions(-) diff --git a/console/package.json b/console/package.json index 784de5a0..557fd8b0 100644 --- a/console/package.json +++ b/console/package.json @@ -25,13 +25,14 @@ "devDependencies": { "concurrently": "^5.0.2", "parcel-bundler": "^1.12.4", - "purescript": "^0.13.5", + "purescript": "^0.13.6", "purescript-psa": "^0.7.3", - "spago": "^0.13" + "spago": "^0.14" }, "dependencies": { "@statebox/stbx-js": "0.0.31", "@statebox/style": "0.0.6", - "dagre": "^0.8.4" + "dagre": "^0.8.4", + "firebaseui": "^4.5.0" } } diff --git a/console/src/Statebox/Console.purs b/console/src/Statebox/Console.purs index 2ce46c2a..c961519d 100644 --- a/console/src/Statebox/Console.purs +++ b/console/src/Statebox/Console.purs @@ -11,7 +11,7 @@ import Effect.Aff.Class (class MonadAff) import Effect.Console (log) import Halogen as H import Halogen (ComponentHTML) -import Halogen.HTML (HTML, p, text, br, div, ul, li, h2, h3, table, tr, th, td) +import Halogen.HTML (HTML, p, text, br, span, div, ul, li, h2, h3, table, tr, th, td) import Halogen.Query.HalogenM (HalogenM) import Statebox.Console.DAO as DAO @@ -25,6 +25,8 @@ import Debug.Trace (spy) type State = { customer :: Maybe Stripe.Customer , paymentMethods :: Array Stripe.PaymentMethod + , subscriptions :: Array Stripe.Subscription + , plans :: Array Stripe.PlanWithExpandedProduct , accounts :: Array { invoices :: Array Stripe.Invoice } , status :: AppStatus @@ -87,6 +89,20 @@ handleAction = case _ of (\x -> H.modify_ $ _ { accounts = [ { invoices: x.data } ] })) spyM "invoicesEE" $ invoicesEE + -- fetch subscriptions for this customer + subscriptionsEE <- H.liftAff $ DAO.listSubscriptions + subscriptionsEE # either (\e -> H.modify_ $ _ { status = ErrorStatus "Failed to fetch subscriptions." }) + (either (\e -> H.modify_ $ _ { status = ErrorStatus "Decoding subscriptions failed."}) + (\x -> H.modify_ $ _ { subscriptions = x.data })) + spyM "subscriptionsEE" $ subscriptionsEE + + -- fetch plans for this customer + plansEE <- H.liftAff $ DAO.listPlans + plansEE # either (\e -> H.modify_ $ _ { status = ErrorStatus "Failed to fetch plans." }) + (either (\e -> H.modify_ $ _ { status = ErrorStatus "Decoding plans failed."}) + (\x -> H.modify_ $ _ { plans = x.data })) + spyM "plansEE" $ plansEE + -- fetch the payment methods for this customer paymentMethodsEE <- H.liftAff $ DAO.listPaymentMethods paymentMethodsEE # either (\e -> H.modify_ $ _ { status = ErrorStatus "Failed to fetch payment methods." }) @@ -106,6 +122,10 @@ render state = , div [] (maybe [] (pure <<< customerHtml) state.customer) , h3 [] [ text "Customer's payment methods" ] , div [] (state.paymentMethods <#> paymentMethodHtml) + , h2 [] [ text "Subscriptions" ] + , div [] (state.subscriptions <#> subscriptionHtml) + , h2 [] [ text "Plans" ] + , div [] (state.plans <#> planWithExpandedProductHtml) , h2 [] [ text "Invoices" ] , div [] (state.accounts <#> \account -> invoiceSummaries account.invoices) ] @@ -118,8 +138,7 @@ invoiceSummaries invoices = invoiceSummaryLineHtml i = tr [] [ td [] [ text $ i.customer_email ] , td [] [ text $ i.account_name ] - , td [] [ text $ i.currency ] - , td [] [ text $ show i.amount_due ] + , td [] [ text $ formatCurrency i.currency i.amount_due ] ] customerHtml :: ∀ m. MonadAff m => Stripe.Customer -> ComponentHTML Action ChildSlots m @@ -140,7 +159,7 @@ customerHtml c = ] <> foldMap addressRowsHtml c.address <> [ tr [] [ th [] [ text "balance" ] - , td [] [ text $ c.currency <> " " <> show c.balance <> " cents" ] + , td [] [ text $ formatCurrency c.currency c.balance ] ] , tr [] [ th [] [ text "tax ids" ] , td [] [ taxIdsHtml c.tax_ids ] @@ -171,7 +190,7 @@ paymentMethodHtml pm = billingDetailsHtml :: ∀ m. MonadAff m => Stripe.BillingDetails -> ComponentHTML Action ChildSlots m billingDetailsHtml bd = nameAddressPhoneHtml bd -nameAddressPhoneHtml :: ∀ r m. MonadAff m => { | Stripe.NameAddressPhoneRow () } -> ComponentHTML Action ChildSlots m +nameAddressPhoneHtml :: ∀ m. MonadAff m => { | Stripe.NameAddressPhoneRow () } -> ComponentHTML Action ChildSlots m nameAddressPhoneHtml x = table [] $ [ tr [] [ th [] [ text "name" ] @@ -222,6 +241,125 @@ cardHtml c = formatExpiryDate :: Stripe.Card -> String formatExpiryDate card = show c.exp_month <> "/" <> show c.exp_year +formatCurrency :: Stripe.Currency -> Stripe.Amount -> String +formatCurrency currency amount = + show amount <> " " <> currency <> " cents" + +timestampHtml :: ∀ m. MonadAff m => Stripe.Timestamp -> ComponentHTML Action ChildSlots m +timestampHtml ts = text $ show ts + +timestampRangeHtml :: ∀ m. MonadAff m => Stripe.Timestamp -> Stripe.Timestamp -> ComponentHTML Action ChildSlots m +timestampRangeHtml start end = + span [] [ timestampHtml start, text " thru ", timestampHtml end ] + +subscriptionHtml :: ∀ m. MonadAff m => Stripe.Subscription -> ComponentHTML Action ChildSlots m +subscriptionHtml s = + table [] + [ tr [] [ td [] [ text "id" ] + , td [] [ text s.id ] + ] + , tr [] [ td [] [ text "status" ] + , td [] [ text s.status ] + ] + , tr [] [ td [] [ text "quantity" ] + , td [] [ text $ show s.quantity ] + ] + , tr [] [ td [] [ text "start date" ] + , td [] [ timestampHtml s.start_date ] + ] + , tr [] [ td [] [ text "current period" ] + , td [] [ timestampRangeHtml s.current_period_start s.current_period_end ] + ] + , tr [] [ td [] [ text "trial period" ] + , td [] [ timestampRangeHtml s.trial_start s.trial_end ] + ] + , tr [] [ td [] [ text "collection method" ] + , td [] [ text s.collection_method ] + ] + , tr [] [ td [] [ text "live mode" ] + , td [] [ text $ show s.livemode ] + ] + , tr [] [ td [] [ text "items" ] + , td [] (s.items.data <#> subscriptionItemHtml) + ] + ] + +subscriptionItemHtml :: ∀ m. MonadAff m => Stripe.SubscriptionItem -> ComponentHTML Action ChildSlots m +subscriptionItemHtml item = + table [] + [ tr [] [ td [] [ text "plan" ] + , td [] [ planHtml item.plan ] + ] + , tr [] [ td [] [ text "created" ] + , td [] [ text $ show item.created ] + ] + ] + +planHtml :: ∀ m. MonadAff m => Stripe.Plan -> ComponentHTML Action ChildSlots m +planHtml plan = + table [] + [ tr [] [ td [] [ text "nickname" ] + , td [] [ text $ fromMaybe "-" plan.nickname ] + ] + , tr [] [ td [] [ text "product id" ] + , td [] [ text plan.product ] + ] + , tr [] [ td [] [ text "created on" ] + , td [] [ timestampHtml plan.created ] + ] + , tr [] [ td [] [ text "amount" ] + , td [] [ text $ formatCurrency plan.currency plan.amount ] + ] + , tr [] [ td [] [ text "billing scheme" ] + , td [] [ text plan.billing_scheme ] + ] + , tr [] [ td [] [ text "interval" ] + , td [] [ text $ plan.interval <> " (" <> show plan.interval_count <> "x)" ] + ] + ] + +-------------------------------------------------------------------------------- + +planWithExpandedProductHtml :: ∀ m. MonadAff m => Stripe.PlanWithExpandedProduct -> ComponentHTML Action ChildSlots m +planWithExpandedProductHtml plan = + table [] + [ tr [] [ td [] [ text "nickname" ] + , td [] [ text $ fromMaybe "-" plan.nickname ] + ] + , tr [] [ td [] [ text "product" ] + , td [] [ productHtml plan.product ] + ] + , tr [] [ td [] [ text "created on" ] + , td [] [ timestampHtml plan.created ] + ] + , tr [] [ td [] [ text "amount" ] + , td [] [ text $ formatCurrency plan.currency plan.amount ] + ] + , tr [] [ td [] [ text "billing scheme" ] + , td [] [ text plan.billing_scheme ] + ] + , tr [] [ td [] [ text "interval" ] + , td [] [ text $ plan.interval <> " (" <> show plan.interval_count <> "x)" ] + ] + ] + +productHtml :: ∀ m. MonadAff m => Stripe.Product -> ComponentHTML Action ChildSlots m +productHtml product = + table [] + [ tr [] [ td [] [ text "product id" ] + , td [] [ text product.id ] + ] + , tr [] [ td [] [ text "name" ] + , td [] [ text product.name ] + ] + , tr [] [ td [] [ text "description" ] + , td [] [ text $ fromMaybe "-" product.description ] + ] + , tr [] [ td [] [ text "unit" ] + , td [] [ text $ fromMaybe "-" product.unit_label ] + ] + ] + -------------------------------------------------------------------------------- spyM :: ∀ m a. Applicative m => String -> a -> m Unit diff --git a/console/src/Statebox/Console/DAO.purs b/console/src/Statebox/Console/DAO.purs index 8a265095..6d01d79a 100644 --- a/console/src/Statebox/Console/DAO.purs +++ b/console/src/Statebox/Console/DAO.purs @@ -52,3 +52,27 @@ listPaymentMethods' = , method = Left GET , responseFormat = ResponseFormat.json } + +-------------------------------------------------------------------------------- + +listSubscriptions :: Aff (Affjax.Error \/ String \/ Stripe.ArrayWrapper Stripe.Subscription) +listSubscriptions = listSubscriptions' # map (map (_.body >>> spy "subscriptions dump" >>> decodeJson)) + +listSubscriptions' :: Aff (Affjax.Error \/ Response Json) +listSubscriptions' = + Affjax.request $ Affjax.defaultRequest { url = mkUrl "/subscriptions" + , method = Left GET + , responseFormat = ResponseFormat.json + } + +-------------------------------------------------------------------------------- + +listPlans :: Aff (Affjax.Error \/ String \/ Stripe.ArrayWrapper Stripe.PlanWithExpandedProduct) +listPlans = listPlans' # map (map (_.body >>> spy "plans dump" >>> decodeJson)) + +listPlans' :: Aff (Affjax.Error \/ Response Json) +listPlans' = + Affjax.request $ Affjax.defaultRequest { url = mkUrl "/plans" + , method = Left GET + , responseFormat = ResponseFormat.json + } diff --git a/console/src/Statebox/Console/Main.purs b/console/src/Statebox/Console/Main.purs index b4cfddfd..b0d9fd21 100644 --- a/console/src/Statebox/Console/Main.purs +++ b/console/src/Statebox/Console/Main.purs @@ -19,6 +19,8 @@ main = runHalogenAff do initialState :: Console.State initialState = { customer: Nothing , paymentMethods: mempty + , subscriptions: mempty + , plans: mempty , accounts: [ { invoices: mempty } ] , status: Console.Ok } diff --git a/console/src/Stripe.purs b/console/src/Stripe.purs index 430a6bf8..94432c8e 100644 --- a/console/src/Stripe.purs +++ b/console/src/Stripe.purs @@ -95,10 +95,17 @@ type Subscription = , customer :: CustomerId , object :: ObjectTag , created :: Timestamp + , status :: SubscriptionStatusString + , start_date :: Timestamp + , trial_start :: Timestamp + , trial_end :: Timestamp , current_period_start :: Timestamp , current_period_end :: Timestamp + , collection_method :: CollectionMethodString , latest_invoice :: Maybe InvoiceId + , quantity :: Int , items :: ArrayWrapper SubscriptionItem + , livemode :: Boolean } type SubscriptionId = String @@ -106,7 +113,6 @@ type SubscriptionId = String type SubscriptionItem = { id :: SubscriptionItemId , object :: ObjectTag - , quantity :: Int , subscription :: SubscriptionId , plan :: Plan , created :: Timestamp @@ -114,14 +120,18 @@ type SubscriptionItem = type SubscriptionItemId = String --- | E.g. `"charge_automatically"` -type CollectionMethod = String +-- | See https://stripe.com/docs/billing/subscriptions/overview#subscription-states. +-- | One of `"trialing"` | `"active"` | `"incomplete"` | `"incomplete_expired"` | `"past_due"` | `"canceled"` | `"unpaid. +type SubscriptionStatusString = String -type Plan = +-- | Either `"charge_automatically"` | `"send_invoice"`. +type CollectionMethodString = String + +type Plan' product = { id :: PlanId , object :: ObjectTag , nickname :: Maybe String - , product :: ProductId + , product :: product , amount :: Amount , amount_decimal :: AmountDecimal , currency :: Currency @@ -131,6 +141,10 @@ type Plan = , interval_count :: Int } +type Plan = Plan' ProductId + +type PlanWithExpandedProduct = Plan' Product + type PlanId = String -- | E.g. `"per_unit"` @@ -139,8 +153,27 @@ type BillingScheme = String -- | E.g. `"month"` type Interval = String +-------------------------------------------------------------------------------- + +-- | https://stripe.com/docs/api/products/object +type Product = + { id :: ProductId + , name :: String + , description :: Maybe String + , unit_label :: Maybe String + , statement_descriptor :: Maybe String -- ^ will appear on a customer's credit card statement + , created :: Timestamp + , updated :: Timestamp + , images :: Array URL + , active :: Boolean + , livemode :: Boolean + } + type ProductId = String +-- | One of `"good"` | `"service"`. +type ProductTypeString = String + -------------------------------------------------------------------------------- type TaxIdData =