-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathbilling.js
121 lines (109 loc) · 4.13 KB
/
billing.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
import { Failure } from '@ucanto/core'
import { toEmail } from '@web3-storage/did-mailto'
export class InvalidSubscriptionState extends Failure {
/**
* @param {string} [message] Context for the message.
* @param {ErrorOptions} [options]
*/
constructor(message, options) {
super(undefined, options)
this.name = /** @type {const} */ ('InvalidSubscriptionState')
this.detail = message
}
describe() {
return `subscription cannot be updated because it is not in a valid state: ${this.detail}`
}
}
export class BillingProviderUpdateError extends Failure {
/**
* @param {string} [message] Context for the message.
* @param {ErrorOptions} [options]
*/
constructor(message, options) {
super(undefined, options)
this.name = /** @type {const} */ ('BillingProviderUpdateError')
this.detail = message
}
describe() {
return `encountered an error updating subscription: ${this.detail}`
}
}
/**
*
* @param {import('stripe').Stripe} stripe
* @param {import("@web3-storage/w3infra-billing/lib/api").CustomerStore} customerStore
* @returns {import("./types").BillingProvider}
*/
export function createStripeBillingProvider(stripe, customerStore) {
return {
async hasCustomer(customer) {
const customersResponse = await stripe.customers.list({ email: toEmail(/** @type {import('@web3-storage/did-mailto').DidMailto} */(customer)) })
return { ok: (customersResponse.data.length > 0) }
},
async setPlan(customerDID, plan) {
/** @type {import('stripe').Stripe.SubscriptionItem[] | undefined} */
let subscriptionItems
/** @type {string | undefined} */
let priceID
try {
const prices = await stripe.prices.list({ lookup_keys: [plan] })
priceID = prices.data.find(price => price.lookup_key === plan)?.id
if (!priceID) return (
{ error: new InvalidSubscriptionState(`could not find Stripe price with lookup_key ${plan} - cannot set plan`) }
)
const email = toEmail(/** @type {import('@web3-storage/did-mailto').DidMailto} */(customerDID))
const customers = await stripe.customers.list({ email, expand: ['data.subscriptions'] })
if (customers.data.length !== 1) return (
{ error: new InvalidSubscriptionState(`found ${customers.data.length} Stripe customer(s) with email ${email} - cannot set plan`) }
)
const customer = customers.data[0]
const subscriptions = customer.subscriptions?.data
if (subscriptions?.length !== 1) return (
{ error: new InvalidSubscriptionState(`found ${subscriptions?.length} Stripe subscriptions(s) for customer with email ${email} - cannot set plan`) }
)
subscriptionItems = customer.subscriptions?.data[0].items.data
if (subscriptionItems?.length !== 1) return (
{ error: new InvalidSubscriptionState(`found ${subscriptionItems?.length} Stripe subscriptions item(s) for customer with email ${email} - cannot set plan`) }
)
} catch (/** @type {any} */ err) {
return { error: new InvalidSubscriptionState(err.message, { cause: err }) }
}
try {
await stripe.subscriptionItems.update(subscriptionItems[0].id, { price: priceID })
return { ok: {} }
} catch (/** @type {any} */ err) {
return { error: new BillingProviderUpdateError(err.message, { cause: err }) }
}
},
async createAdminSession(account, returnURL) {
const response = await customerStore.get({ customer: account })
if (response.error) {
return {
error: {
name: 'CustomerNotFound',
message: 'Error getting customer',
cause: response.error
}
}
}
if (!response.ok.account) {
return {
error: {
name: 'CustomerNotFound',
message: 'Customer account not set'
}
}
}
const customer = response.ok.account.slice('stripe:'.length)
const session = await stripe.billingPortal.sessions.create({
customer,
return_url: returnURL
})
return {
ok: {
url: session.url
}
}
}
}
}