Skip to content

Commit e972889

Browse files
committed
console: Cover more Stripe API Surface (Plan, Product, etc). #376
1 parent 022bd9f commit e972889

File tree

5 files changed

+212
-15
lines changed

5 files changed

+212
-15
lines changed

console/package.json

+4-3
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,14 @@
2525
"devDependencies": {
2626
"concurrently": "^5.0.2",
2727
"parcel-bundler": "^1.12.4",
28-
"purescript": "^0.13.5",
28+
"purescript": "^0.13.6",
2929
"purescript-psa": "^0.7.3",
30-
"spago": "^0.13"
30+
"spago": "^0.14"
3131
},
3232
"dependencies": {
3333
"@statebox/stbx-js": "0.0.31",
3434
"@statebox/style": "0.0.6",
35-
"dagre": "^0.8.4"
35+
"dagre": "^0.8.4",
36+
"firebaseui": "^4.5.0"
3637
}
3738
}

console/src/Statebox/Console.purs

+143-5
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import Effect.Aff.Class (class MonadAff)
1111
import Effect.Console (log)
1212
import Halogen as H
1313
import Halogen (ComponentHTML)
14-
import Halogen.HTML (HTML, p, text, br, div, ul, li, h2, h3, table, tr, th, td)
14+
import Halogen.HTML (HTML, p, text, br, span, div, ul, li, h2, h3, table, tr, th, td)
1515
import Halogen.Query.HalogenM (HalogenM)
1616

1717
import Statebox.Console.DAO as DAO
@@ -25,6 +25,8 @@ import Debug.Trace (spy)
2525
type State =
2626
{ customer :: Maybe Stripe.Customer
2727
, paymentMethods :: Array Stripe.PaymentMethod
28+
, subscriptions :: Array Stripe.Subscription
29+
, plans :: Array Stripe.PlanWithExpandedProduct
2830
, accounts :: Array { invoices :: Array Stripe.Invoice
2931
}
3032
, status :: AppStatus
@@ -87,6 +89,20 @@ handleAction = case _ of
8789
(\x -> H.modify_ $ _ { accounts = [ { invoices: x.data } ] }))
8890
spyM "invoicesEE" $ invoicesEE
8991

92+
-- fetch subscriptions for this customer
93+
subscriptionsEE <- H.liftAff $ DAO.listSubscriptions
94+
subscriptionsEE # either (\e -> H.modify_ $ _ { status = ErrorStatus "Failed to fetch subscriptions." })
95+
(either (\e -> H.modify_ $ _ { status = ErrorStatus "Decoding subscriptions failed."})
96+
(\x -> H.modify_ $ _ { subscriptions = x.data }))
97+
spyM "subscriptionsEE" $ subscriptionsEE
98+
99+
-- fetch plans for this customer
100+
plansEE <- H.liftAff $ DAO.listPlans
101+
plansEE # either (\e -> H.modify_ $ _ { status = ErrorStatus "Failed to fetch plans." })
102+
(either (\e -> H.modify_ $ _ { status = ErrorStatus "Decoding plans failed."})
103+
(\x -> H.modify_ $ _ { plans = x.data }))
104+
spyM "plansEE" $ plansEE
105+
90106
-- fetch the payment methods for this customer
91107
paymentMethodsEE <- H.liftAff $ DAO.listPaymentMethods
92108
paymentMethodsEE # either (\e -> H.modify_ $ _ { status = ErrorStatus "Failed to fetch payment methods." })
@@ -106,6 +122,10 @@ render state =
106122
, div [] (maybe [] (pure <<< customerHtml) state.customer)
107123
, h3 [] [ text "Customer's payment methods" ]
108124
, div [] (state.paymentMethods <#> paymentMethodHtml)
125+
, h2 [] [ text "Subscriptions" ]
126+
, div [] (state.subscriptions <#> subscriptionHtml)
127+
, h2 [] [ text "Plans" ]
128+
, div [] (state.plans <#> planWithExpandedProductHtml)
109129
, h2 [] [ text "Invoices" ]
110130
, div [] (state.accounts <#> \account -> invoiceSummaries account.invoices)
111131
]
@@ -118,8 +138,7 @@ invoiceSummaries invoices =
118138
invoiceSummaryLineHtml i =
119139
tr [] [ td [] [ text $ i.customer_email ]
120140
, td [] [ text $ i.account_name ]
121-
, td [] [ text $ i.currency ]
122-
, td [] [ text $ show i.amount_due ]
141+
, td [] [ text $ formatCurrency i.currency i.amount_due ]
123142
]
124143

125144
customerHtml :: m. MonadAff m => Stripe.Customer -> ComponentHTML Action ChildSlots m
@@ -140,7 +159,7 @@ customerHtml c =
140159
] <>
141160
foldMap addressRowsHtml c.address <>
142161
[ tr [] [ th [] [ text "balance" ]
143-
, td [] [ text $ c.currency <> " " <> show c.balance <> " cents" ]
162+
, td [] [ text $ formatCurrency c.currency c.balance ]
144163
]
145164
, tr [] [ th [] [ text "tax ids" ]
146165
, td [] [ taxIdsHtml c.tax_ids ]
@@ -171,7 +190,7 @@ paymentMethodHtml pm =
171190
billingDetailsHtml :: m. MonadAff m => Stripe.BillingDetails -> ComponentHTML Action ChildSlots m
172191
billingDetailsHtml bd = nameAddressPhoneHtml bd
173192

174-
nameAddressPhoneHtml :: r m. MonadAff m => { | Stripe.NameAddressPhoneRow () } -> ComponentHTML Action ChildSlots m
193+
nameAddressPhoneHtml :: m. MonadAff m => { | Stripe.NameAddressPhoneRow () } -> ComponentHTML Action ChildSlots m
175194
nameAddressPhoneHtml x =
176195
table [] $
177196
[ tr [] [ th [] [ text "name" ]
@@ -222,6 +241,125 @@ cardHtml c =
222241
formatExpiryDate :: Stripe.Card -> String
223242
formatExpiryDate card = show c.exp_month <> "/" <> show c.exp_year
224243

244+
formatCurrency :: Stripe.Currency -> Stripe.Amount -> String
245+
formatCurrency currency amount =
246+
show amount <> " " <> currency <> " cents"
247+
248+
timestampHtml :: m. MonadAff m => Stripe.Timestamp -> ComponentHTML Action ChildSlots m
249+
timestampHtml ts = text $ show ts
250+
251+
timestampRangeHtml :: m. MonadAff m => Stripe.Timestamp -> Stripe.Timestamp -> ComponentHTML Action ChildSlots m
252+
timestampRangeHtml start end =
253+
span [] [ timestampHtml start, text " thru ", timestampHtml end ]
254+
255+
subscriptionHtml :: m. MonadAff m => Stripe.Subscription -> ComponentHTML Action ChildSlots m
256+
subscriptionHtml s =
257+
table []
258+
[ tr [] [ td [] [ text "id" ]
259+
, td [] [ text s.id ]
260+
]
261+
, tr [] [ td [] [ text "status" ]
262+
, td [] [ text s.status ]
263+
]
264+
, tr [] [ td [] [ text "quantity" ]
265+
, td [] [ text $ show s.quantity ]
266+
]
267+
, tr [] [ td [] [ text "start date" ]
268+
, td [] [ timestampHtml s.start_date ]
269+
]
270+
, tr [] [ td [] [ text "current period" ]
271+
, td [] [ timestampRangeHtml s.current_period_start s.current_period_end ]
272+
]
273+
, tr [] [ td [] [ text "trial period" ]
274+
, td [] [ timestampRangeHtml s.trial_start s.trial_end ]
275+
]
276+
, tr [] [ td [] [ text "collection method" ]
277+
, td [] [ text s.collection_method ]
278+
]
279+
, tr [] [ td [] [ text "live mode" ]
280+
, td [] [ text $ show s.livemode ]
281+
]
282+
, tr [] [ td [] [ text "items" ]
283+
, td [] (s.items.data <#> subscriptionItemHtml)
284+
]
285+
]
286+
287+
subscriptionItemHtml :: m. MonadAff m => Stripe.SubscriptionItem -> ComponentHTML Action ChildSlots m
288+
subscriptionItemHtml item =
289+
table []
290+
[ tr [] [ td [] [ text "plan" ]
291+
, td [] [ planHtml item.plan ]
292+
]
293+
, tr [] [ td [] [ text "created" ]
294+
, td [] [ text $ show item.created ]
295+
]
296+
]
297+
298+
planHtml :: m. MonadAff m => Stripe.Plan -> ComponentHTML Action ChildSlots m
299+
planHtml plan =
300+
table []
301+
[ tr [] [ td [] [ text "nickname" ]
302+
, td [] [ text $ fromMaybe "-" plan.nickname ]
303+
]
304+
, tr [] [ td [] [ text "product id" ]
305+
, td [] [ text plan.product ]
306+
]
307+
, tr [] [ td [] [ text "created on" ]
308+
, td [] [ timestampHtml plan.created ]
309+
]
310+
, tr [] [ td [] [ text "amount" ]
311+
, td [] [ text $ formatCurrency plan.currency plan.amount ]
312+
]
313+
, tr [] [ td [] [ text "billing scheme" ]
314+
, td [] [ text plan.billing_scheme ]
315+
]
316+
, tr [] [ td [] [ text "interval" ]
317+
, td [] [ text $ plan.interval <> " (" <> show plan.interval_count <> "x)" ]
318+
]
319+
]
320+
321+
--------------------------------------------------------------------------------
322+
323+
planWithExpandedProductHtml :: m. MonadAff m => Stripe.PlanWithExpandedProduct -> ComponentHTML Action ChildSlots m
324+
planWithExpandedProductHtml plan =
325+
table []
326+
[ tr [] [ td [] [ text "nickname" ]
327+
, td [] [ text $ fromMaybe "-" plan.nickname ]
328+
]
329+
, tr [] [ td [] [ text "product" ]
330+
, td [] [ productHtml plan.product ]
331+
]
332+
, tr [] [ td [] [ text "created on" ]
333+
, td [] [ timestampHtml plan.created ]
334+
]
335+
, tr [] [ td [] [ text "amount" ]
336+
, td [] [ text $ formatCurrency plan.currency plan.amount ]
337+
]
338+
, tr [] [ td [] [ text "billing scheme" ]
339+
, td [] [ text plan.billing_scheme ]
340+
]
341+
, tr [] [ td [] [ text "interval" ]
342+
, td [] [ text $ plan.interval <> " (" <> show plan.interval_count <> "x)" ]
343+
]
344+
]
345+
346+
productHtml :: m. MonadAff m => Stripe.Product -> ComponentHTML Action ChildSlots m
347+
productHtml product =
348+
table []
349+
[ tr [] [ td [] [ text "product id" ]
350+
, td [] [ text product.id ]
351+
]
352+
, tr [] [ td [] [ text "name" ]
353+
, td [] [ text product.name ]
354+
]
355+
, tr [] [ td [] [ text "description" ]
356+
, td [] [ text $ fromMaybe "-" product.description ]
357+
]
358+
, tr [] [ td [] [ text "unit" ]
359+
, td [] [ text $ fromMaybe "-" product.unit_label ]
360+
]
361+
]
362+
225363
--------------------------------------------------------------------------------
226364

227365
spyM :: m a. Applicative m => String -> a -> m Unit

console/src/Statebox/Console/DAO.purs

+24
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,27 @@ listPaymentMethods' =
5252
, method = Left GET
5353
, responseFormat = ResponseFormat.json
5454
}
55+
56+
--------------------------------------------------------------------------------
57+
58+
listSubscriptions :: Aff (Affjax.Error \/ String \/ Stripe.ArrayWrapper Stripe.Subscription)
59+
listSubscriptions = listSubscriptions' # map (map (_.body >>> spy "subscriptions dump" >>> decodeJson))
60+
61+
listSubscriptions' :: Aff (Affjax.Error \/ Response Json)
62+
listSubscriptions' =
63+
Affjax.request $ Affjax.defaultRequest { url = mkUrl "/subscriptions"
64+
, method = Left GET
65+
, responseFormat = ResponseFormat.json
66+
}
67+
68+
--------------------------------------------------------------------------------
69+
70+
listPlans :: Aff (Affjax.Error \/ String \/ Stripe.ArrayWrapper Stripe.PlanWithExpandedProduct)
71+
listPlans = listPlans' # map (map (_.body >>> spy "plans dump" >>> decodeJson))
72+
73+
listPlans' :: Aff (Affjax.Error \/ Response Json)
74+
listPlans' =
75+
Affjax.request $ Affjax.defaultRequest { url = mkUrl "/plans"
76+
, method = Left GET
77+
, responseFormat = ResponseFormat.json
78+
}

console/src/Statebox/Console/Main.purs

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ main = runHalogenAff do
1919
initialState :: Console.State
2020
initialState = { customer: Nothing
2121
, paymentMethods: mempty
22+
, subscriptions: mempty
23+
, plans: mempty
2224
, accounts: [ { invoices: mempty } ]
2325
, status: Console.Ok
2426
}

console/src/Stripe.purs

+39-7
Original file line numberDiff line numberDiff line change
@@ -95,33 +95,43 @@ type Subscription =
9595
, customer :: CustomerId
9696
, object :: ObjectTag
9797
, created :: Timestamp
98+
, status :: SubscriptionStatusString
99+
, start_date :: Timestamp
100+
, trial_start :: Timestamp
101+
, trial_end :: Timestamp
98102
, current_period_start :: Timestamp
99103
, current_period_end :: Timestamp
104+
, collection_method :: CollectionMethodString
100105
, latest_invoice :: Maybe InvoiceId
106+
, quantity :: Int
101107
, items :: ArrayWrapper SubscriptionItem
108+
, livemode :: Boolean
102109
}
103110

104111
type SubscriptionId = String
105112

106113
type SubscriptionItem =
107114
{ id :: SubscriptionItemId
108115
, object :: ObjectTag
109-
, quantity :: Int
110116
, subscription :: SubscriptionId
111117
, plan :: Plan
112118
, created :: Timestamp
113119
}
114120

115121
type SubscriptionItemId = String
116122

117-
-- | E.g. `"charge_automatically"`
118-
type CollectionMethod = String
123+
-- | See https://stripe.com/docs/billing/subscriptions/overview#subscription-states.
124+
-- | One of `"trialing"` | `"active"` | `"incomplete"` | `"incomplete_expired"` | `"past_due"` | `"canceled"` | `"unpaid.
125+
type SubscriptionStatusString = String
119126

120-
type Plan =
127+
-- | Either `"charge_automatically"` | `"send_invoice"`.
128+
type CollectionMethodString = String
129+
130+
type Plan' product =
121131
{ id :: PlanId
122132
, object :: ObjectTag
123133
, nickname :: Maybe String
124-
, product :: ProductId
134+
, product :: product
125135
, amount :: Amount
126136
, amount_decimal :: AmountDecimal
127137
, currency :: Currency
@@ -131,6 +141,10 @@ type Plan =
131141
, interval_count :: Int
132142
}
133143

144+
type Plan = Plan' ProductId
145+
146+
type PlanWithExpandedProduct = Plan' Product
147+
134148
type PlanId = String
135149

136150
-- | E.g. `"per_unit"`
@@ -139,8 +153,27 @@ type BillingScheme = String
139153
-- | E.g. `"month"`
140154
type Interval = String
141155

156+
--------------------------------------------------------------------------------
157+
158+
-- | https://stripe.com/docs/api/products/object
159+
type Product =
160+
{ id :: ProductId
161+
, name :: String
162+
, description :: Maybe String
163+
, unit_label :: Maybe String
164+
, statement_descriptor :: Maybe String -- ^ will appear on a customer's credit card statement
165+
, created :: Timestamp
166+
, updated :: Timestamp
167+
, images :: Array URL
168+
, active :: Boolean
169+
, livemode :: Boolean
170+
}
171+
142172
type ProductId = String
143173

174+
-- | One of `"good"` | `"service"`.
175+
type ProductTypeString = String
176+
144177
--------------------------------------------------------------------------------
145178

146179
type TaxIdData =
@@ -216,8 +249,7 @@ type Year = Int
216249

217250
--------------------------------------------------------------------------------
218251

219-
-- | Stripe populates this with things like `"customer"`, `"object"`,
220-
-- | `"list"` and so on.
252+
-- | Stripe populates this with things like `"customer"`, `"object"`, `"list"` and so on.
221253
type ObjectTag = String
222254

223255
type ArrayWrapperRow a r =

0 commit comments

Comments
 (0)