@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/src/helpers.ts ADDED
@@ -0,0 +1,110 @@
1
+ import type Stripe from 'stripe'
2
+ import StripeManager from './stripe_manager.ts'
3
+ import Customer from './customer.ts'
4
+ import Subscription from './subscription.ts'
5
+ import SubscriptionBuilder from './subscription_builder.ts'
6
+ import CheckoutBuilder from './checkout_builder.ts'
7
+ import Invoice from './invoice.ts'
8
+ import PaymentMethod from './payment_method.ts'
9
+ import Receipt from './receipt.ts'
10
+ import type { CustomerData, SubscriptionData, ReceiptData } from './types.ts'
11
+
12
+ /**
13
+ * Stripe helper object — the primary convenience API.
14
+ *
15
+ * @example
16
+ * import { stripe } from '@stravigor/stripe'
17
+ *
18
+ * // Direct Stripe instance access
19
+ * stripe.stripe.customers.list()
20
+ *
21
+ * // Subscription builder
22
+ * await stripe.newSubscription('pro', 'price_xxx').trialDays(14).create(user)
23
+ *
24
+ * // Checkout builder
25
+ * const session = await stripe.newCheckout().item('price_xxx').mode('payment').create(user)
26
+ */
27
+ export const stripe = {
28
+ /** Direct access to the configured Stripe SDK instance. */
29
+ get stripe(): Stripe {
30
+ return StripeManager.stripe
31
+ },
32
+
33
+ /** The Stripe publishable key (for frontend use). */
34
+ get key(): string {
35
+ return StripeManager.config.key
36
+ },
37
+
38
+ /** The configured default currency. */
39
+ get currency(): string {
40
+ return StripeManager.config.currency
41
+ },
42
+
43
+ // ----- Customer -----
44
+
45
+ /** Get or create a Stripe customer for a user. */
46
+ createOrGetCustomer(user: unknown, params?: Stripe.CustomerCreateParams): Promise<CustomerData> {
47
+ return Customer.createOrGet(user, params)
48
+ },
49
+
50
+ /** Find the local customer record for a user. */
51
+ findCustomer(user: unknown): Promise<CustomerData | null> {
52
+ return Customer.findByUser(user)
53
+ },
54
+
55
+ // ----- Subscriptions -----
56
+
57
+ /** Start building a new subscription. */
58
+ newSubscription(name: string, ...prices: string[]): SubscriptionBuilder {
59
+ return new SubscriptionBuilder(name, ...prices)
60
+ },
61
+
62
+ /** Find a user's subscription by name. */
63
+ subscription(user: unknown, name: string = 'default'): Promise<SubscriptionData | null> {
64
+ return Subscription.findByName(user, name)
65
+ },
66
+
67
+ /** Check if a user has a valid subscription. */
68
+ async subscribed(user: unknown, name: string = 'default'): Promise<boolean> {
69
+ const sub = await Subscription.findByName(user, name)
70
+ return sub !== null && Subscription.valid(sub)
71
+ },
72
+
73
+ // ----- Checkout -----
74
+
75
+ /** Start building a Stripe Checkout session. */
76
+ newCheckout(): CheckoutBuilder {
77
+ return new CheckoutBuilder()
78
+ },
79
+
80
+ // ----- Invoices -----
81
+
82
+ /** List invoices for a user. */
83
+ invoices(user: unknown, params?: Stripe.InvoiceListParams): Promise<Stripe.Invoice[]> {
84
+ return Invoice.list(user, params)
85
+ },
86
+
87
+ /** Preview the upcoming invoice. */
88
+ upcomingInvoice(user: unknown): Promise<Stripe.UpcomingInvoice | null> {
89
+ return Invoice.upcoming(user)
90
+ },
91
+
92
+ // ----- Payment Methods -----
93
+
94
+ /** List payment methods for a user. */
95
+ paymentMethods(user: unknown): Promise<Stripe.PaymentMethod[]> {
96
+ return PaymentMethod.list(user)
97
+ },
98
+
99
+ /** Set a payment method as default. */
100
+ setDefaultPaymentMethod(user: unknown, paymentMethodId: string): Promise<void> {
101
+ return PaymentMethod.setDefault(user, paymentMethodId)
102
+ },
103
+
104
+ // ----- Receipts -----
105
+
106
+ /** List receipts for a user. */
107
+ receipts(user: unknown): Promise<ReceiptData[]> {
108
+ return Receipt.findByUser(user)
109
+ },
110
+ }
package/src/index.ts ADDED
@@ -0,0 +1,49 @@
1
+ // Manager
2
+ export { default, default as StripeManager } from './stripe_manager.ts'
3
+
4
+ // Provider
5
+ export { default as StripeProvider } from './stripe_provider.ts'
6
+
7
+ // Static helpers
8
+ export { default as Customer } from './customer.ts'
9
+ export { default as Subscription } from './subscription.ts'
10
+ export { default as SubscriptionItem } from './subscription_item.ts'
11
+ export { default as Invoice } from './invoice.ts'
12
+ export { default as PaymentMethod } from './payment_method.ts'
13
+ export { default as Receipt } from './receipt.ts'
14
+
15
+ // Builders
16
+ export { default as SubscriptionBuilder } from './subscription_builder.ts'
17
+ export { default as CheckoutBuilder } from './checkout_builder.ts'
18
+
19
+ // Mixin
20
+ export { billable } from './billable.ts'
21
+ export type { BillableInstance } from './billable.ts'
22
+
23
+ // Helper
24
+ export { stripe } from './helpers.ts'
25
+
26
+ // Webhook
27
+ export { stripeWebhook, onWebhookEvent } from './webhook.ts'
28
+
29
+ // Errors
30
+ export {
31
+ StripeError,
32
+ WebhookSignatureError,
33
+ CustomerNotFoundError,
34
+ SubscriptionNotFoundError,
35
+ PaymentMethodError,
36
+ SubscriptionCreationError,
37
+ } from './errors.ts'
38
+
39
+ // Types
40
+ export type {
41
+ StripeConfig,
42
+ CustomerData,
43
+ SubscriptionData,
44
+ SubscriptionItemData,
45
+ ReceiptData,
46
+ SubscriptionStatusValue,
47
+ WebhookEventHandler,
48
+ } from './types.ts'
49
+ export { SubscriptionStatus } from './types.ts'
package/src/invoice.ts ADDED
@@ -0,0 +1,66 @@
1
+ import type Stripe from 'stripe'
2
+ import StripeManager from './stripe_manager.ts'
3
+ import Customer from './customer.ts'
4
+
5
+ /**
6
+ * Static helper for Stripe invoice operations.
7
+ *
8
+ * @example
9
+ * const invoices = await Invoice.list(user)
10
+ * const upcoming = await Invoice.upcoming(user)
11
+ * const pdf = await Invoice.pdfUrl('in_xxx')
12
+ */
13
+ export default class Invoice {
14
+ /** List invoices for a user. */
15
+ static async list(user: unknown, params?: Stripe.InvoiceListParams): Promise<Stripe.Invoice[]> {
16
+ const customer = await Customer.findByUser(user)
17
+ if (!customer) return []
18
+
19
+ const result = await StripeManager.stripe.invoices.list({
20
+ customer: customer.stripeId,
21
+ limit: 24,
22
+ ...params,
23
+ })
24
+ return result.data
25
+ }
26
+
27
+ /** Get the upcoming invoice preview (prorations, next charge). */
28
+ static async upcoming(
29
+ user: unknown,
30
+ params?: Stripe.InvoiceRetrieveUpcomingParams
31
+ ): Promise<Stripe.UpcomingInvoice | null> {
32
+ const customer = await Customer.findByUser(user)
33
+ if (!customer) return null
34
+
35
+ try {
36
+ return await StripeManager.stripe.invoices.retrieveUpcoming({
37
+ customer: customer.stripeId,
38
+ ...params,
39
+ })
40
+ } catch {
41
+ return null
42
+ }
43
+ }
44
+
45
+ /** Retrieve a specific invoice by Stripe ID. */
46
+ static async find(invoiceId: string): Promise<Stripe.Invoice> {
47
+ return StripeManager.stripe.invoices.retrieve(invoiceId)
48
+ }
49
+
50
+ /** Get the hosted invoice URL for payment. */
51
+ static async hostedUrl(invoiceId: string): Promise<string | null> {
52
+ const invoice = await StripeManager.stripe.invoices.retrieve(invoiceId)
53
+ return invoice.hosted_invoice_url ?? null
54
+ }
55
+
56
+ /** Get the invoice PDF download URL. */
57
+ static async pdfUrl(invoiceId: string): Promise<string | null> {
58
+ const invoice = await StripeManager.stripe.invoices.retrieve(invoiceId)
59
+ return invoice.invoice_pdf ?? null
60
+ }
61
+
62
+ /** Void an invoice. */
63
+ static async void_(invoiceId: string): Promise<Stripe.Invoice> {
64
+ return StripeManager.stripe.invoices.voidInvoice(invoiceId)
65
+ }
66
+ }
@@ -0,0 +1,59 @@
1
+ import type Stripe from 'stripe'
2
+ import StripeManager from './stripe_manager.ts'
3
+ import Customer from './customer.ts'
4
+ import { PaymentMethodError } from './errors.ts'
5
+
6
+ /**
7
+ * Static helper for managing Stripe payment methods.
8
+ *
9
+ * @example
10
+ * const methods = await PaymentMethod.list(user)
11
+ * await PaymentMethod.setDefault(user, 'pm_xxx')
12
+ */
13
+ export default class PaymentMethod {
14
+ /** List all payment methods for a user. */
15
+ static async list(
16
+ user: unknown,
17
+ type: Stripe.PaymentMethodListParams.Type = 'card'
18
+ ): Promise<Stripe.PaymentMethod[]> {
19
+ return Customer.paymentMethods(user, type)
20
+ }
21
+
22
+ /** Retrieve a single payment method from Stripe. */
23
+ static async find(paymentMethodId: string): Promise<Stripe.PaymentMethod> {
24
+ return StripeManager.stripe.paymentMethods.retrieve(paymentMethodId)
25
+ }
26
+
27
+ /** Set a payment method as the customer's default. */
28
+ static async setDefault(user: unknown, paymentMethodId: string): Promise<void> {
29
+ const customer = await Customer.createOrGet(user)
30
+
31
+ // Attach if not already attached
32
+ try {
33
+ await StripeManager.stripe.paymentMethods.attach(paymentMethodId, {
34
+ customer: customer.stripeId,
35
+ })
36
+ } catch (err: any) {
37
+ // Only ignore "already attached" errors — rethrow everything else
38
+ if (err?.code !== 'resource_already_exists') {
39
+ throw new PaymentMethodError(
40
+ `Failed to attach payment method "${paymentMethodId}": ${err?.message ?? err}`
41
+ )
42
+ }
43
+ }
44
+
45
+ // Set as default on Stripe
46
+ await StripeManager.stripe.customers.update(customer.stripeId, {
47
+ invoice_settings: { default_payment_method: paymentMethodId },
48
+ })
49
+
50
+ // Update local record
51
+ const pm = await StripeManager.stripe.paymentMethods.retrieve(paymentMethodId)
52
+ await Customer.updateDefaultPaymentMethod(customer.stripeId, pm)
53
+ }
54
+
55
+ /** Detach a payment method from the customer. */
56
+ static async delete(paymentMethodId: string): Promise<void> {
57
+ return Customer.deletePaymentMethod(paymentMethodId)
58
+ }
59
+ }
package/src/receipt.ts ADDED
@@ -0,0 +1,84 @@
1
+ import { extractUserId } from '@stravigor/database'
2
+ import StripeManager from './stripe_manager.ts'
3
+ import type { ReceiptData } from './types.ts'
4
+
5
+ /**
6
+ * Static helper for managing one-time payment receipts.
7
+ *
8
+ * @example
9
+ * const receipt = await Receipt.create({ user, stripeId: pi.id, amount: 2500, currency: 'usd' })
10
+ * const receipts = await Receipt.findByUser(user)
11
+ */
12
+ export default class Receipt {
13
+ private static get sql() {
14
+ return StripeManager.db.sql
15
+ }
16
+
17
+ private static get fk() {
18
+ return StripeManager.userFkColumn
19
+ }
20
+
21
+ /** Record a one-time payment receipt. */
22
+ static async create(data: {
23
+ user: unknown
24
+ stripeId: string
25
+ amount: number
26
+ currency: string
27
+ description?: string | null
28
+ receiptUrl?: string | null
29
+ }): Promise<ReceiptData> {
30
+ const userId = extractUserId(data.user)
31
+ const fk = Receipt.fk
32
+ const rows = await Receipt.sql.unsafe(
33
+ `INSERT INTO "receipt" ("${fk}", "stripe_id", "amount", "currency", "description", "receipt_url")
34
+ VALUES ($1, $2, $3, $4, $5, $6)
35
+ RETURNING *`,
36
+ [
37
+ userId,
38
+ data.stripeId,
39
+ data.amount,
40
+ data.currency,
41
+ data.description ?? null,
42
+ data.receiptUrl ?? null,
43
+ ]
44
+ )
45
+ return Receipt.hydrate(rows[0] as Record<string, unknown>)
46
+ }
47
+
48
+ /** Find all receipts for a user, newest first. */
49
+ static async findByUser(user: unknown): Promise<ReceiptData[]> {
50
+ const userId = extractUserId(user)
51
+ const fk = Receipt.fk
52
+ const rows = await Receipt.sql.unsafe(
53
+ `SELECT * FROM "receipt" WHERE "${fk}" = $1 ORDER BY "created_at" DESC`,
54
+ [userId]
55
+ )
56
+ return rows.map((r: any) => Receipt.hydrate(r))
57
+ }
58
+
59
+ /** Find a receipt by Stripe payment intent ID. */
60
+ static async findByStripeId(stripeId: string): Promise<ReceiptData | null> {
61
+ const rows = await Receipt.sql`
62
+ SELECT * FROM "receipt" WHERE "stripe_id" = ${stripeId} LIMIT 1
63
+ `
64
+ return rows.length > 0 ? Receipt.hydrate(rows[0] as Record<string, unknown>) : null
65
+ }
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Internal
69
+ // ---------------------------------------------------------------------------
70
+
71
+ private static hydrate(row: Record<string, unknown>): ReceiptData {
72
+ const fk = Receipt.fk
73
+ return {
74
+ id: row.id as number,
75
+ userId: row[fk] as string | number,
76
+ stripeId: row.stripe_id as string,
77
+ amount: row.amount as number,
78
+ currency: row.currency as string,
79
+ description: (row.description as string) ?? null,
80
+ receiptUrl: (row.receipt_url as string) ?? null,
81
+ createdAt: row.created_at as Date,
82
+ }
83
+ }
84
+ }
@@ -0,0 +1,75 @@
1
+ import Stripe from 'stripe'
2
+ import { inject, Configuration, ConfigurationError } from '@stravigor/kernel'
3
+ import { Database, toSnakeCase } from '@stravigor/database'
4
+ import type { StripeConfig } from './types.ts'
5
+
6
+ @inject
7
+ export default class StripeManager {
8
+ private static _db: Database
9
+ private static _config: StripeConfig
10
+ private static _stripe: Stripe
11
+ private static _userFkColumn: string
12
+
13
+ constructor(db: Database, config: Configuration) {
14
+ StripeManager._db = db
15
+
16
+ const userKey = config.get('stripe.userKey', 'id') as string
17
+ StripeManager._userFkColumn = `user_${toSnakeCase(userKey)}`
18
+
19
+ const secret = config.get('stripe.secret', '') as string
20
+
21
+ StripeManager._config = {
22
+ secret,
23
+ key: config.get('stripe.key', '') as string,
24
+ webhookSecret: config.get('stripe.webhookSecret', '') as string,
25
+ currency: config.get('stripe.currency', 'usd') as string,
26
+ userKey,
27
+ urls: {
28
+ success: config.get('stripe.urls.success', '/billing/success') as string,
29
+ cancel: config.get('stripe.urls.cancel', '/billing/cancel') as string,
30
+ },
31
+ }
32
+
33
+ if (secret) {
34
+ StripeManager._stripe = new Stripe(secret)
35
+ }
36
+ }
37
+
38
+ static get db(): Database {
39
+ if (!StripeManager._db) {
40
+ throw new ConfigurationError(
41
+ 'StripeManager not configured. Resolve it through the container first.'
42
+ )
43
+ }
44
+ return StripeManager._db
45
+ }
46
+
47
+ static get config(): StripeConfig {
48
+ if (!StripeManager._config) {
49
+ throw new ConfigurationError(
50
+ 'StripeManager not configured. Resolve it through the container first.'
51
+ )
52
+ }
53
+ return StripeManager._config
54
+ }
55
+
56
+ static get stripe(): Stripe {
57
+ if (!StripeManager._stripe) {
58
+ throw new ConfigurationError('StripeManager not configured. Ensure STRIPE_SECRET is set.')
59
+ }
60
+ return StripeManager._stripe
61
+ }
62
+
63
+ /** The FK column name on stripe tables (e.g. `user_id`, `user_uid`). */
64
+ static get userFkColumn(): string {
65
+ return StripeManager._userFkColumn ?? 'user_id'
66
+ }
67
+
68
+ /** Reset internal state (useful for testing). */
69
+ static reset(): void {
70
+ StripeManager._db = undefined as any
71
+ StripeManager._config = undefined as any
72
+ StripeManager._stripe = undefined as any
73
+ StripeManager._userFkColumn = undefined as any
74
+ }
75
+ }
@@ -0,0 +1,16 @@
1
+ import { ServiceProvider } from '@stravigor/kernel'
2
+ import type { Application } from '@stravigor/kernel'
3
+ import StripeManager from './stripe_manager.ts'
4
+
5
+ export default class StripeProvider extends ServiceProvider {
6
+ readonly name = 'stripe'
7
+ override readonly dependencies = ['database']
8
+
9
+ override register(app: Application): void {
10
+ app.singleton(StripeManager)
11
+ }
12
+
13
+ override boot(app: Application): void {
14
+ app.resolve(StripeManager)
15
+ }
16
+ }