@strav/stripe 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # @stravigor/stripe
2
+
3
+ Stripe billing for the [Strav](https://www.npmjs.com/package/@stravigor/core) framework. Subscriptions, one-time charges, checkout sessions, invoices, payment methods, and webhooks.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ bun add @stravigor/stripe
9
+ bun strav install stripe
10
+ ```
11
+
12
+ Requires `@stravigor/core` as a peer dependency.
13
+
14
+ ## Setup
15
+
16
+ ```ts
17
+ import { StripeProvider } from '@stravigor/stripe'
18
+
19
+ app.use(new StripeProvider())
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ```ts
25
+ import { stripe } from '@stravigor/stripe'
26
+
27
+ // Create a customer
28
+ const customer = await stripe.createOrGetCustomer(user)
29
+
30
+ // Start a subscription
31
+ const subscription = await stripe
32
+ .newSubscription('default', 'price_xxx')
33
+ .create(user)
34
+
35
+ // Checkout session
36
+ const checkout = await stripe
37
+ .newCheckout()
38
+ .item('price_xxx')
39
+ .successUrl('/success')
40
+ .cancelUrl('/cancel')
41
+ .create(user)
42
+ ```
43
+
44
+ ## Billable Mixin
45
+
46
+ ```ts
47
+ import { billable } from '@stravigor/stripe'
48
+
49
+ class User extends billable(BaseModel) {
50
+ // adds subscription, invoice, and payment helpers
51
+ }
52
+ ```
53
+
54
+ ## Webhooks
55
+
56
+ ```ts
57
+ import { stripeWebhook, onWebhookEvent } from '@stravigor/stripe'
58
+
59
+ onWebhookEvent('customer.subscription.updated', async (event) => {
60
+ // handle subscription changes
61
+ })
62
+
63
+ router.post('/stripe/webhook', stripeWebhook())
64
+ ```
65
+
66
+ ## Documentation
67
+
68
+ See the full [Stripe billing guide](../../guides/stripe.md).
69
+
70
+ ## License
71
+
72
+ MIT
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@strav/stripe",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Stripe billing for the Strav framework",
6
+ "license": "MIT",
7
+ "exports": {
8
+ ".": "./src/index.ts",
9
+ "./*": "./src/*.ts"
10
+ },
11
+ "files": [
12
+ "src/",
13
+ "stubs/",
14
+ "package.json",
15
+ "tsconfig.json"
16
+ ],
17
+ "peerDependencies": {
18
+ "@strav/kernel": "0.1.0",
19
+ "@strav/database": "0.1.0",
20
+ "@strav/http": "0.1.0"
21
+ },
22
+ "dependencies": {
23
+ "stripe": "^17.4.0"
24
+ },
25
+ "scripts": {
26
+ "test": "bun test tests/",
27
+ "typecheck": "tsc --noEmit"
28
+ }
29
+ }
@@ -0,0 +1,287 @@
1
+ import type Stripe from 'stripe'
2
+ import type { BaseModel } from '@stravigor/database'
3
+ import type { NormalizeConstructor } from '@stravigor/kernel'
4
+ import { extractUserId } from '@stravigor/database'
5
+ import Customer from './customer.ts'
6
+ import Subscription from './subscription.ts'
7
+ import SubscriptionBuilder from './subscription_builder.ts'
8
+ import CheckoutBuilder from './checkout_builder.ts'
9
+ import Invoice from './invoice.ts'
10
+ import PaymentMethod from './payment_method.ts'
11
+ import StripeManager from './stripe_manager.ts'
12
+ import type { CustomerData, SubscriptionData } from './types.ts'
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Bound builders (auto-pass user to .create())
16
+ // ---------------------------------------------------------------------------
17
+
18
+ class BoundSubscriptionBuilder extends SubscriptionBuilder {
19
+ private _user: unknown
20
+
21
+ constructor(user: unknown, name: string, ...prices: string[]) {
22
+ super(name, ...prices)
23
+ this._user = user
24
+ }
25
+
26
+ override async create(user?: unknown): Promise<SubscriptionData> {
27
+ return super.create(user ?? this._user)
28
+ }
29
+ }
30
+
31
+ class BoundCheckoutBuilder extends CheckoutBuilder {
32
+ private _user: unknown
33
+
34
+ constructor(user: unknown) {
35
+ super()
36
+ this._user = user
37
+ }
38
+
39
+ override async create(user?: unknown): Promise<Stripe.Checkout.Session> {
40
+ return super.create(user ?? this._user)
41
+ }
42
+ }
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Mixin
46
+ // ---------------------------------------------------------------------------
47
+
48
+ /**
49
+ * Mixin that adds billing methods to a BaseModel subclass.
50
+ *
51
+ * @example
52
+ * import { BaseModel } from '@stravigor/database'
53
+ * import { billable } from '@stravigor/stripe'
54
+ *
55
+ * class User extends billable(BaseModel) {
56
+ * declare id: number
57
+ * declare email: string
58
+ * }
59
+ *
60
+ * // Composable with other mixins:
61
+ * import { compose } from '@stravigor/kernel'
62
+ * class User extends compose(BaseModel, softDeletes, billable) { }
63
+ *
64
+ * const user = await User.find(1)
65
+ * await user.subscribe('pro', 'price_xxx')
66
+ * await user.subscribed('pro') // true
67
+ */
68
+ export function billable<T extends NormalizeConstructor<typeof BaseModel>>(Base: T) {
69
+ return class Billable extends Base {
70
+ // ----- Customer -----
71
+
72
+ /** Get or create the Stripe customer record for this user. */
73
+ async createOrGetStripeCustomer(params?: Stripe.CustomerCreateParams): Promise<CustomerData> {
74
+ return Customer.createOrGet(this, params)
75
+ }
76
+
77
+ /** Get the local customer record. */
78
+ async customer(): Promise<CustomerData | null> {
79
+ return Customer.findByUser(this)
80
+ }
81
+
82
+ /** Get the Stripe customer ID. */
83
+ async stripeId(): Promise<string | null> {
84
+ const customer = await Customer.findByUser(this)
85
+ return customer?.stripeId ?? null
86
+ }
87
+
88
+ /** Check if the user has a Stripe customer record. */
89
+ async hasStripeId(): Promise<boolean> {
90
+ return (await Customer.findByUser(this)) !== null
91
+ }
92
+
93
+ // ----- Subscriptions -----
94
+
95
+ /**
96
+ * Start building a new subscription.
97
+ *
98
+ * @example
99
+ * await user.newSubscription('pro', 'price_xxx').trialDays(14).create()
100
+ * await user.newSubscription('enterprise', 'price_a', 'price_b').create()
101
+ */
102
+ newSubscription(name: string, ...prices: string[]): BoundSubscriptionBuilder {
103
+ return new BoundSubscriptionBuilder(this, name, ...prices)
104
+ }
105
+
106
+ /**
107
+ * Create a subscription immediately (shorthand).
108
+ *
109
+ * @example
110
+ * await user.subscribe('pro', 'price_xxx')
111
+ */
112
+ async subscribe(name: string, priceId: string): Promise<SubscriptionData> {
113
+ return new BoundSubscriptionBuilder(this, name, priceId).create()
114
+ }
115
+
116
+ /** Get a specific subscription by name. */
117
+ async subscription(name: string = 'default'): Promise<SubscriptionData | null> {
118
+ return Subscription.findByName(this, name)
119
+ }
120
+
121
+ /** Get all subscriptions. */
122
+ async subscriptions(): Promise<SubscriptionData[]> {
123
+ return Subscription.findByUser(this)
124
+ }
125
+
126
+ /** Check if the user has a valid subscription with the given name. */
127
+ async subscribed(name: string = 'default'): Promise<boolean> {
128
+ const sub = await Subscription.findByName(this, name)
129
+ return sub !== null && Subscription.valid(sub)
130
+ }
131
+
132
+ /** Check if the user is on a trial for the given subscription. */
133
+ async onTrial(name: string = 'default'): Promise<boolean> {
134
+ const sub = await Subscription.findByName(this, name)
135
+ if (sub) return Subscription.onTrial(sub)
136
+
137
+ // Also check customer-level trial (generic trial)
138
+ const customer = await Customer.findByUser(this)
139
+ return (
140
+ customer?.trialEndsAt !== null &&
141
+ customer?.trialEndsAt !== undefined &&
142
+ customer.trialEndsAt.getTime() > Date.now()
143
+ )
144
+ }
145
+
146
+ /** Check if the user has an active subscription to a specific price ID. */
147
+ async subscribedToPrice(priceId: string): Promise<boolean> {
148
+ const subs = await Subscription.findByUser(this)
149
+ return subs.some(sub => Subscription.valid(sub) && sub.stripePriceId === priceId)
150
+ }
151
+
152
+ /** Check if the subscription is on a grace period. */
153
+ async onGracePeriod(name: string = 'default'): Promise<boolean> {
154
+ const sub = await Subscription.findByName(this, name)
155
+ return sub !== null && Subscription.onGracePeriod(sub)
156
+ }
157
+
158
+ // ----- One-time Charges -----
159
+
160
+ /**
161
+ * Create a one-time charge.
162
+ *
163
+ * @example
164
+ * await user.charge(2500, 'pm_xxx', { description: 'Pro-rated upgrade' })
165
+ */
166
+ async charge(
167
+ amount: number,
168
+ paymentMethodId: string,
169
+ options?: {
170
+ currency?: string
171
+ description?: string
172
+ metadata?: Record<string, string>
173
+ }
174
+ ): Promise<Stripe.PaymentIntent> {
175
+ const customer = await Customer.createOrGet(this)
176
+ return StripeManager.stripe.paymentIntents.create({
177
+ amount,
178
+ currency: options?.currency ?? StripeManager.config.currency,
179
+ customer: customer.stripeId,
180
+ payment_method: paymentMethodId,
181
+ confirm: true,
182
+ automatic_payment_methods: {
183
+ enabled: true,
184
+ allow_redirects: 'never',
185
+ },
186
+ description: options?.description,
187
+ metadata: {
188
+ strav_user_id: String(extractUserId(this)),
189
+ ...options?.metadata,
190
+ },
191
+ })
192
+ }
193
+
194
+ /** Refund a payment intent (fully or partially). */
195
+ async refund(paymentIntentId: string, amount?: number): Promise<Stripe.Refund> {
196
+ return StripeManager.stripe.refunds.create({
197
+ payment_intent: paymentIntentId,
198
+ ...(amount ? { amount } : {}),
199
+ })
200
+ }
201
+
202
+ // ----- Payment Methods -----
203
+
204
+ /** List all payment methods for this user. */
205
+ async paymentMethods(
206
+ type?: Stripe.PaymentMethodListParams.Type
207
+ ): Promise<Stripe.PaymentMethod[]> {
208
+ return PaymentMethod.list(this, type)
209
+ }
210
+
211
+ /** Get the default payment method. */
212
+ async defaultPaymentMethod(): Promise<Stripe.PaymentMethod | null> {
213
+ const customer = await Customer.findByUser(this)
214
+ if (!customer) return null
215
+ const methods = await PaymentMethod.list(this)
216
+ return methods[0] ?? null
217
+ }
218
+
219
+ /** Set a payment method as default. */
220
+ async setDefaultPaymentMethod(paymentMethodId: string): Promise<void> {
221
+ return PaymentMethod.setDefault(this, paymentMethodId)
222
+ }
223
+
224
+ /** Create a Stripe SetupIntent for collecting payment info without charging. */
225
+ async createSetupIntent(params?: Stripe.SetupIntentCreateParams): Promise<Stripe.SetupIntent> {
226
+ return Customer.createSetupIntent(this, params)
227
+ }
228
+
229
+ // ----- Checkout -----
230
+
231
+ /**
232
+ * Create a Stripe Checkout session.
233
+ *
234
+ * @example
235
+ * const session = await user.checkout([{ price: 'price_xxx', quantity: 1 }])
236
+ */
237
+ async checkout(
238
+ items: Array<{ price: string; quantity?: number }>
239
+ ): Promise<Stripe.Checkout.Session> {
240
+ const builder = new CheckoutBuilder(items)
241
+ return builder.create(this)
242
+ }
243
+
244
+ /**
245
+ * Start building a checkout session fluently.
246
+ *
247
+ * @example
248
+ * const session = await user.newCheckout()
249
+ * .item('price_xxx', 2)
250
+ * .mode('subscription')
251
+ * .subscriptionName('pro')
252
+ * .create()
253
+ */
254
+ newCheckout(): BoundCheckoutBuilder {
255
+ return new BoundCheckoutBuilder(this)
256
+ }
257
+
258
+ // ----- Invoices -----
259
+
260
+ /** List Stripe invoices for this user. */
261
+ async invoices(params?: Stripe.InvoiceListParams): Promise<Stripe.Invoice[]> {
262
+ return Invoice.list(this, params)
263
+ }
264
+
265
+ /** Preview the upcoming invoice. */
266
+ async upcomingInvoice(
267
+ params?: Stripe.InvoiceRetrieveUpcomingParams
268
+ ): Promise<Stripe.UpcomingInvoice | null> {
269
+ return Invoice.upcoming(this, params)
270
+ }
271
+
272
+ // ----- Billing Portal -----
273
+
274
+ /** Create a Stripe Customer Portal session URL. */
275
+ async billingPortalUrl(returnUrl?: string): Promise<string> {
276
+ const customer = await Customer.createOrGet(this)
277
+ const session = await StripeManager.stripe.billingPortal.sessions.create({
278
+ customer: customer.stripeId,
279
+ return_url: returnUrl ?? StripeManager.config.urls.success,
280
+ })
281
+ return session.url
282
+ }
283
+ }
284
+ }
285
+
286
+ /** The instance type of any billable model. */
287
+ export type BillableInstance = InstanceType<ReturnType<typeof billable>>
@@ -0,0 +1,130 @@
1
+ import type Stripe from 'stripe'
2
+ import { extractUserId } from '@stravigor/database'
3
+ import StripeManager from './stripe_manager.ts'
4
+ import Customer from './customer.ts'
5
+
6
+ interface CheckoutLineItem {
7
+ price: string
8
+ quantity?: number
9
+ }
10
+
11
+ /**
12
+ * Fluent builder for creating Stripe Checkout sessions.
13
+ *
14
+ * @example
15
+ * const session = await new CheckoutBuilder()
16
+ * .item('price_xxx', 2)
17
+ * .mode('subscription')
18
+ * .subscriptionName('pro')
19
+ * .create(user)
20
+ */
21
+ export default class CheckoutBuilder {
22
+ private _items: CheckoutLineItem[] = []
23
+ private _mode: Stripe.Checkout.SessionCreateParams.Mode = 'payment'
24
+ private _successUrl?: string
25
+ private _cancelUrl?: string
26
+ private _allowPromotionCodes = false
27
+ private _metadata: Record<string, string> = {}
28
+ private _subscriptionName?: string
29
+ private _trialDays?: number
30
+ private _customerEmail?: string
31
+
32
+ constructor(items?: CheckoutLineItem[]) {
33
+ if (items) this._items = items
34
+ }
35
+
36
+ /** Add a line item. */
37
+ item(price: string, quantity?: number): this {
38
+ this._items.push({ price, quantity: quantity ?? 1 })
39
+ return this
40
+ }
41
+
42
+ /** Set checkout mode: 'payment' | 'subscription' | 'setup'. */
43
+ mode(mode: Stripe.Checkout.SessionCreateParams.Mode): this {
44
+ this._mode = mode
45
+ return this
46
+ }
47
+
48
+ /** Set success URL. Overrides config default. */
49
+ successUrl(url: string): this {
50
+ this._successUrl = url
51
+ return this
52
+ }
53
+
54
+ /** Set cancel URL. Overrides config default. */
55
+ cancelUrl(url: string): this {
56
+ this._cancelUrl = url
57
+ return this
58
+ }
59
+
60
+ /** Allow promotion codes in the checkout page. */
61
+ allowPromotionCodes(allow = true): this {
62
+ this._allowPromotionCodes = allow
63
+ return this
64
+ }
65
+
66
+ /** Set custom metadata. */
67
+ metadata(data: Record<string, string>): this {
68
+ this._metadata = { ...this._metadata, ...data }
69
+ return this
70
+ }
71
+
72
+ /** Name the subscription (stored in metadata, used by webhook handler). */
73
+ subscriptionName(name: string): this {
74
+ this._subscriptionName = name
75
+ this._mode = 'subscription'
76
+ return this
77
+ }
78
+
79
+ /** Add trial days (subscription mode only). */
80
+ trialDays(days: number): this {
81
+ this._trialDays = days
82
+ return this
83
+ }
84
+
85
+ /** Pre-fill customer email (for guest users without a Stripe customer). */
86
+ email(email: string): this {
87
+ this._customerEmail = email
88
+ return this
89
+ }
90
+
91
+ /**
92
+ * Create the Stripe Checkout Session.
93
+ * If user is provided, attaches to their Stripe customer.
94
+ */
95
+ async create(user?: unknown): Promise<Stripe.Checkout.Session> {
96
+ const config = StripeManager.config
97
+
98
+ const params: Stripe.Checkout.SessionCreateParams = {
99
+ mode: this._mode,
100
+ line_items: this._items.map(i => ({
101
+ price: i.price,
102
+ quantity: i.quantity,
103
+ })),
104
+ success_url: this._successUrl ?? config.urls.success,
105
+ cancel_url: this._cancelUrl ?? config.urls.cancel,
106
+ allow_promotion_codes: this._allowPromotionCodes || undefined,
107
+ metadata: {
108
+ ...(this._subscriptionName ? { strav_name: this._subscriptionName } : {}),
109
+ ...this._metadata,
110
+ },
111
+ }
112
+
113
+ if (user) {
114
+ const customer = await Customer.createOrGet(user)
115
+ params.customer = customer.stripeId
116
+ params.metadata!.strav_user_id = String(extractUserId(user))
117
+ } else if (this._customerEmail) {
118
+ params.customer_email = this._customerEmail
119
+ }
120
+
121
+ if (this._trialDays && this._mode === 'subscription') {
122
+ params.subscription_data = {
123
+ trial_period_days: this._trialDays,
124
+ metadata: params.metadata,
125
+ }
126
+ }
127
+
128
+ return StripeManager.stripe.checkout.sessions.create(params)
129
+ }
130
+ }
@@ -0,0 +1,156 @@
1
+ import type Stripe from 'stripe'
2
+ import { extractUserId } from '@stravigor/database'
3
+ import StripeManager from './stripe_manager.ts'
4
+ import type { CustomerData } from './types.ts'
5
+
6
+ /**
7
+ * Static helper for managing Stripe customer records.
8
+ *
9
+ * All methods are static. Database access goes through StripeManager.db.
10
+ *
11
+ * @example
12
+ * const customer = await Customer.createOrGet(user)
13
+ * const methods = await Customer.paymentMethods(user)
14
+ */
15
+ export default class Customer {
16
+ private static get sql() {
17
+ return StripeManager.db.sql
18
+ }
19
+
20
+ private static get fk() {
21
+ return StripeManager.userFkColumn
22
+ }
23
+
24
+ /** Find a customer record by user (model instance or ID). */
25
+ static async findByUser(user: unknown): Promise<CustomerData | null> {
26
+ const userId = extractUserId(user)
27
+ const fk = Customer.fk
28
+ const rows = await Customer.sql.unsafe(`SELECT * FROM "customer" WHERE "${fk}" = $1 LIMIT 1`, [
29
+ userId,
30
+ ])
31
+ return rows.length > 0 ? Customer.hydrate(rows[0] as Record<string, unknown>) : null
32
+ }
33
+
34
+ /** Find a customer record by Stripe customer ID. */
35
+ static async findByStripeId(stripeId: string): Promise<CustomerData | null> {
36
+ const rows = await Customer.sql`
37
+ SELECT * FROM "customer" WHERE "stripe_id" = ${stripeId} LIMIT 1
38
+ `
39
+ return rows.length > 0 ? Customer.hydrate(rows[0] as Record<string, unknown>) : null
40
+ }
41
+
42
+ /**
43
+ * Get or create the Stripe customer + local record for a user.
44
+ * Optionally pass params forwarded to `stripe.customers.create()`.
45
+ */
46
+ static async createOrGet(
47
+ user: unknown,
48
+ params?: Stripe.CustomerCreateParams
49
+ ): Promise<CustomerData> {
50
+ const existing = await Customer.findByUser(user)
51
+ if (existing) return existing
52
+
53
+ const userId = extractUserId(user)
54
+ const stripeCustomer = await StripeManager.stripe.customers.create({
55
+ metadata: { strav_user_id: String(userId) },
56
+ ...params,
57
+ })
58
+
59
+ const fk = Customer.fk
60
+ const rows = await Customer.sql.unsafe(
61
+ `INSERT INTO "customer" ("${fk}", "stripe_id")
62
+ VALUES ($1, $2)
63
+ RETURNING *`,
64
+ [userId, stripeCustomer.id]
65
+ )
66
+ return Customer.hydrate(rows[0] as Record<string, unknown>)
67
+ }
68
+
69
+ /** Update the default payment method on the local record. */
70
+ static async updateDefaultPaymentMethod(
71
+ stripeCustomerId: string,
72
+ paymentMethod: Stripe.PaymentMethod
73
+ ): Promise<void> {
74
+ await Customer.sql`
75
+ UPDATE "customer"
76
+ SET "pm_type" = ${paymentMethod.type},
77
+ "pm_last_four" = ${paymentMethod.card?.last4 ?? null},
78
+ "updated_at" = NOW()
79
+ WHERE "stripe_id" = ${stripeCustomerId}
80
+ `
81
+ }
82
+
83
+ /** Create a Stripe SetupIntent for the customer. */
84
+ static async createSetupIntent(
85
+ user: unknown,
86
+ params?: Stripe.SetupIntentCreateParams
87
+ ): Promise<Stripe.SetupIntent> {
88
+ const customer = await Customer.createOrGet(user)
89
+ return StripeManager.stripe.setupIntents.create({
90
+ customer: customer.stripeId,
91
+ ...params,
92
+ })
93
+ }
94
+
95
+ /** List all payment methods for a user's Stripe customer. */
96
+ static async paymentMethods(
97
+ user: unknown,
98
+ type: Stripe.PaymentMethodListParams.Type = 'card'
99
+ ): Promise<Stripe.PaymentMethod[]> {
100
+ const customer = await Customer.findByUser(user)
101
+ if (!customer) return []
102
+ const result = await StripeManager.stripe.paymentMethods.list({
103
+ customer: customer.stripeId,
104
+ type,
105
+ })
106
+ return result.data
107
+ }
108
+
109
+ /** Detach a payment method from Stripe. */
110
+ static async deletePaymentMethod(paymentMethodId: string): Promise<void> {
111
+ await StripeManager.stripe.paymentMethods.detach(paymentMethodId)
112
+ }
113
+
114
+ /** Sync trial_ends_at on the local record. */
115
+ static async updateTrialEndsAt(
116
+ stripeCustomerId: string,
117
+ trialEndsAt: Date | null
118
+ ): Promise<void> {
119
+ await Customer.sql`
120
+ UPDATE "customer"
121
+ SET "trial_ends_at" = ${trialEndsAt},
122
+ "updated_at" = NOW()
123
+ WHERE "stripe_id" = ${stripeCustomerId}
124
+ `
125
+ }
126
+
127
+ /** Delete the local customer record. Does NOT delete from Stripe. */
128
+ static async deleteByUser(user: unknown): Promise<void> {
129
+ const userId = extractUserId(user)
130
+ const fk = Customer.fk
131
+ await Customer.sql.unsafe(`DELETE FROM "customer" WHERE "${fk}" = $1`, [userId])
132
+ }
133
+
134
+ /** Delete the local customer record by Stripe ID. */
135
+ static async deleteByStripeId(stripeId: string): Promise<void> {
136
+ await Customer.sql`DELETE FROM "customer" WHERE "stripe_id" = ${stripeId}`
137
+ }
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // Internal
141
+ // ---------------------------------------------------------------------------
142
+
143
+ private static hydrate(row: Record<string, unknown>): CustomerData {
144
+ const fk = Customer.fk
145
+ return {
146
+ id: row.id as number,
147
+ userId: row[fk] as string | number,
148
+ stripeId: row.stripe_id as string,
149
+ pmType: (row.pm_type as string) ?? null,
150
+ pmLastFour: (row.pm_last_four as string) ?? null,
151
+ trialEndsAt: (row.trial_ends_at as Date) ?? null,
152
+ createdAt: row.created_at as Date,
153
+ updatedAt: row.updated_at as Date,
154
+ }
155
+ }
156
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { StravError } from '@stravigor/kernel'
2
+
3
+ /** Base error class for all Stripe billing errors. */
4
+ export class StripeError extends StravError {}
5
+
6
+ /** Thrown when webhook signature verification fails. */
7
+ export class WebhookSignatureError extends StripeError {
8
+ constructor() {
9
+ super('Stripe webhook signature verification failed.')
10
+ }
11
+ }
12
+
13
+ /** Thrown when a user has no Stripe customer record. */
14
+ export class CustomerNotFoundError extends StripeError {
15
+ constructor() {
16
+ super('No Stripe customer found for this user.')
17
+ }
18
+ }
19
+
20
+ /** Thrown when a subscription is not found. */
21
+ export class SubscriptionNotFoundError extends StripeError {
22
+ constructor(name: string) {
23
+ super(`No subscription named "${name}" found for this user.`)
24
+ }
25
+ }
26
+
27
+ /** Thrown when a payment method operation fails on Stripe. */
28
+ export class PaymentMethodError extends StripeError {}
29
+
30
+ /** Thrown when a subscription creation fails on Stripe. */
31
+ export class SubscriptionCreationError extends StripeError {}