@stravigor/cashier 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.
@@ -0,0 +1,176 @@
1
+ import type Stripe from 'stripe'
2
+ import { extractUserId } from '@stravigor/core/helpers'
3
+ import CashierManager from './cashier_manager.ts'
4
+ import Customer from './customer.ts'
5
+ import Subscription from './subscription.ts'
6
+ import SubscriptionItem from './subscription_item.ts'
7
+ import type { SubscriptionData } from './types.ts'
8
+
9
+ interface PendingItem {
10
+ price: string
11
+ quantity?: number
12
+ }
13
+
14
+ /**
15
+ * Fluent builder for creating Stripe subscriptions.
16
+ *
17
+ * @example
18
+ * const sub = await new SubscriptionBuilder('pro', 'price_xxx')
19
+ * .trialDays(14)
20
+ * .coupon('LAUNCH20')
21
+ * .create(user)
22
+ */
23
+ export default class SubscriptionBuilder {
24
+ private _name: string
25
+ private _items: PendingItem[] = []
26
+ private _trialDays?: number
27
+ private _trialUntil?: Date
28
+ private _skipTrial = false
29
+ private _coupon?: string
30
+ private _promotionCode?: string
31
+ private _metadata: Record<string, string> = {}
32
+ private _paymentBehavior: Stripe.SubscriptionCreateParams.PaymentBehavior = 'default_incomplete'
33
+ private _quantity?: number
34
+ private _anchorBillingCycleOn?: number
35
+
36
+ constructor(name: string, ...prices: string[]) {
37
+ this._name = name
38
+ for (const price of prices) {
39
+ this._items.push({ price })
40
+ }
41
+ }
42
+
43
+ /** Set the default quantity for all items (unless overridden per-item). */
44
+ quantity(qty: number): this {
45
+ this._quantity = qty
46
+ return this
47
+ }
48
+
49
+ /** Add a price with an explicit quantity. */
50
+ plan(price: string, quantity?: number): this {
51
+ this._items.push({ price, quantity })
52
+ return this
53
+ }
54
+
55
+ /** Set a trial period in days. */
56
+ trialDays(days: number): this {
57
+ this._trialDays = days
58
+ return this
59
+ }
60
+
61
+ /** Set a specific trial end date. */
62
+ trialUntil(date: Date): this {
63
+ this._trialUntil = date
64
+ return this
65
+ }
66
+
67
+ /** Skip any trial period. */
68
+ skipTrial(): this {
69
+ this._skipTrial = true
70
+ return this
71
+ }
72
+
73
+ /** Apply a coupon to the subscription. */
74
+ coupon(couponId: string): this {
75
+ this._coupon = couponId
76
+ return this
77
+ }
78
+
79
+ /** Apply a promotion code. */
80
+ promotionCode(code: string): this {
81
+ this._promotionCode = code
82
+ return this
83
+ }
84
+
85
+ /** Add custom metadata to the Stripe subscription. */
86
+ metadata(data: Record<string, string>): this {
87
+ this._metadata = { ...this._metadata, ...data }
88
+ return this
89
+ }
90
+
91
+ /** Anchor the billing cycle to a specific timestamp. */
92
+ anchorBillingCycleOn(timestamp: number): this {
93
+ this._anchorBillingCycleOn = timestamp
94
+ return this
95
+ }
96
+
97
+ /** Set the payment behavior (default: 'default_incomplete'). */
98
+ paymentBehavior(behavior: Stripe.SubscriptionCreateParams.PaymentBehavior): this {
99
+ this._paymentBehavior = behavior
100
+ return this
101
+ }
102
+
103
+ /**
104
+ * Create the subscription on Stripe and record it locally.
105
+ * Returns the local SubscriptionData.
106
+ */
107
+ async create(user: unknown): Promise<SubscriptionData> {
108
+ // 1. Ensure Stripe customer exists
109
+ const customer = await Customer.createOrGet(user)
110
+
111
+ // 2. Build Stripe params
112
+ const items: Stripe.SubscriptionCreateParams.Item[] = this._items.map((item) => ({
113
+ price: item.price,
114
+ quantity: item.quantity ?? this._quantity,
115
+ }))
116
+
117
+ const params: Stripe.SubscriptionCreateParams = {
118
+ customer: customer.stripeId,
119
+ items,
120
+ payment_behavior: this._paymentBehavior,
121
+ expand: ['latest_invoice.payment_intent', 'pending_setup_intent'],
122
+ metadata: {
123
+ strav_user_id: String(extractUserId(user)),
124
+ strav_name: this._name,
125
+ ...this._metadata,
126
+ },
127
+ }
128
+
129
+ // Trial logic
130
+ if (!this._skipTrial) {
131
+ if (this._trialUntil) {
132
+ params.trial_end = Math.floor(this._trialUntil.getTime() / 1000)
133
+ } else if (this._trialDays) {
134
+ params.trial_end = Math.floor(Date.now() / 1000) + this._trialDays * 86400
135
+ }
136
+ }
137
+
138
+ if (this._coupon) params.coupon = this._coupon
139
+ if (this._promotionCode) params.promotion_code = this._promotionCode
140
+ if (this._anchorBillingCycleOn) {
141
+ params.billing_cycle_anchor = this._anchorBillingCycleOn
142
+ }
143
+
144
+ // 3. Create on Stripe
145
+ const stripeSub = await CashierManager.stripe.subscriptions.create(params)
146
+
147
+ // 4. Record locally
148
+ const trialEndsAt = stripeSub.trial_end ? new Date(stripeSub.trial_end * 1000) : null
149
+
150
+ const localSub = await Subscription.create({
151
+ user,
152
+ name: this._name,
153
+ stripeId: stripeSub.id,
154
+ stripeStatus: stripeSub.status,
155
+ stripePriceId: this._items[0]?.price ?? null,
156
+ quantity: this._items[0]?.quantity ?? this._quantity ?? null,
157
+ trialEndsAt,
158
+ })
159
+
160
+ // 5. Record subscription items
161
+ for (const item of stripeSub.items.data) {
162
+ await SubscriptionItem.create({
163
+ subscriptionId: localSub.id,
164
+ stripeId: item.id,
165
+ stripeProductId:
166
+ typeof item.price.product === 'string'
167
+ ? item.price.product
168
+ : item.price.product.id,
169
+ stripePriceId: item.price.id,
170
+ quantity: item.quantity ?? null,
171
+ })
172
+ }
173
+
174
+ return localSub
175
+ }
176
+ }
@@ -0,0 +1,172 @@
1
+ import CashierManager from './cashier_manager.ts'
2
+ import type { SubscriptionItemData, SubscriptionData } from './types.ts'
3
+
4
+ /**
5
+ * Static helper for managing subscription item records (multi-plan subscriptions).
6
+ *
7
+ * @example
8
+ * const items = await SubscriptionItem.findBySubscription(sub.id)
9
+ * await SubscriptionItem.add(sub, sub.id, 'price_addon', 2)
10
+ */
11
+ export default class SubscriptionItem {
12
+ private static get sql() {
13
+ return CashierManager.db.sql
14
+ }
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Queries
18
+ // ---------------------------------------------------------------------------
19
+
20
+ /** Find all items belonging to a subscription. */
21
+ static async findBySubscription(subscriptionId: number): Promise<SubscriptionItemData[]> {
22
+ const rows = await SubscriptionItem.sql`
23
+ SELECT * FROM "subscription_item"
24
+ WHERE "subscription_id" = ${subscriptionId}
25
+ ORDER BY "created_at" ASC
26
+ `
27
+ return rows.map((r: any) => SubscriptionItem.hydrate(r))
28
+ }
29
+
30
+ /** Find by Stripe subscription item ID. */
31
+ static async findByStripeId(stripeId: string): Promise<SubscriptionItemData | null> {
32
+ const rows = await SubscriptionItem.sql`
33
+ SELECT * FROM "subscription_item" WHERE "stripe_id" = ${stripeId} LIMIT 1
34
+ `
35
+ return rows.length > 0 ? SubscriptionItem.hydrate(rows[0] as Record<string, unknown>) : null
36
+ }
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Create / Update
40
+ // ---------------------------------------------------------------------------
41
+
42
+ /** Create a local subscription item record. */
43
+ static async create(data: {
44
+ subscriptionId: number
45
+ stripeId: string
46
+ stripeProductId: string
47
+ stripePriceId: string
48
+ quantity?: number | null
49
+ }): Promise<SubscriptionItemData> {
50
+ const rows = await SubscriptionItem.sql`
51
+ INSERT INTO "subscription_item"
52
+ ("subscription_id", "stripe_id", "stripe_product_id", "stripe_price_id", "quantity")
53
+ VALUES (${data.subscriptionId}, ${data.stripeId}, ${data.stripeProductId},
54
+ ${data.stripePriceId}, ${data.quantity ?? null})
55
+ RETURNING *
56
+ `
57
+ return SubscriptionItem.hydrate(rows[0] as Record<string, unknown>)
58
+ }
59
+
60
+ /** Add a new price/item to an existing Stripe subscription and record it locally. */
61
+ static async add(
62
+ sub: SubscriptionData,
63
+ localSubId: number,
64
+ priceId: string,
65
+ quantity?: number
66
+ ): Promise<SubscriptionItemData> {
67
+ const stripeItem = await CashierManager.stripe.subscriptionItems.create({
68
+ subscription: sub.stripeId,
69
+ price: priceId,
70
+ quantity: quantity ?? 1,
71
+ })
72
+
73
+ return SubscriptionItem.create({
74
+ subscriptionId: localSubId,
75
+ stripeId: stripeItem.id,
76
+ stripeProductId:
77
+ typeof stripeItem.price.product === 'string'
78
+ ? stripeItem.price.product
79
+ : stripeItem.price.product.id,
80
+ stripePriceId: stripeItem.price.id,
81
+ quantity: stripeItem.quantity ?? null,
82
+ })
83
+ }
84
+
85
+ /** Swap an existing item to a different price. */
86
+ static async swap(item: SubscriptionItemData, newPriceId: string): Promise<void> {
87
+ await CashierManager.stripe.subscriptionItems.update(item.stripeId, {
88
+ price: newPriceId,
89
+ proration_behavior: 'create_prorations',
90
+ })
91
+
92
+ await SubscriptionItem.sql`
93
+ UPDATE "subscription_item"
94
+ SET "stripe_price_id" = ${newPriceId}, "updated_at" = NOW()
95
+ WHERE "stripe_id" = ${item.stripeId}
96
+ `
97
+ }
98
+
99
+ /** Update quantity for an item on Stripe and locally. */
100
+ static async updateQuantity(item: SubscriptionItemData, quantity: number): Promise<void> {
101
+ await CashierManager.stripe.subscriptionItems.update(item.stripeId, { quantity })
102
+
103
+ await SubscriptionItem.sql`
104
+ UPDATE "subscription_item"
105
+ SET "quantity" = ${quantity}, "updated_at" = NOW()
106
+ WHERE "stripe_id" = ${item.stripeId}
107
+ `
108
+ }
109
+
110
+ /** Remove an item from the Stripe subscription and delete locally. */
111
+ static async remove(item: SubscriptionItemData): Promise<void> {
112
+ await CashierManager.stripe.subscriptionItems.del(item.stripeId)
113
+ await SubscriptionItem.sql`
114
+ DELETE FROM "subscription_item" WHERE "stripe_id" = ${item.stripeId}
115
+ `
116
+ }
117
+
118
+ /** Report metered usage for a subscription item. */
119
+ static async reportUsage(
120
+ item: SubscriptionItemData,
121
+ quantity: number,
122
+ options?: { timestamp?: number }
123
+ ): Promise<void> {
124
+ await CashierManager.stripe.subscriptionItems.createUsageRecord(item.stripeId, {
125
+ quantity,
126
+ ...(options?.timestamp ? { timestamp: options.timestamp } : {}),
127
+ })
128
+ }
129
+
130
+ /** Sync all items from Stripe into the local table for a subscription. */
131
+ static async syncFromStripe(sub: SubscriptionData, localSubId: number): Promise<void> {
132
+ const stripeSub = await CashierManager.stripe.subscriptions.retrieve(sub.stripeId, {
133
+ expand: ['items.data.price.product'],
134
+ })
135
+
136
+ // Delete existing local items
137
+ await SubscriptionItem.sql`
138
+ DELETE FROM "subscription_item" WHERE "subscription_id" = ${localSubId}
139
+ `
140
+
141
+ // Re-create from Stripe data
142
+ for (const item of stripeSub.items.data) {
143
+ await SubscriptionItem.create({
144
+ subscriptionId: localSubId,
145
+ stripeId: item.id,
146
+ stripeProductId:
147
+ typeof item.price.product === 'string'
148
+ ? item.price.product
149
+ : item.price.product.id,
150
+ stripePriceId: item.price.id,
151
+ quantity: item.quantity ?? null,
152
+ })
153
+ }
154
+ }
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // Internal
158
+ // ---------------------------------------------------------------------------
159
+
160
+ private static hydrate(row: Record<string, unknown>): SubscriptionItemData {
161
+ return {
162
+ id: row.id as number,
163
+ subscriptionId: row.subscription_id as number,
164
+ stripeId: row.stripe_id as string,
165
+ stripeProductId: row.stripe_product_id as string,
166
+ stripePriceId: row.stripe_price_id as string,
167
+ quantity: (row.quantity as number) ?? null,
168
+ createdAt: row.created_at as Date,
169
+ updatedAt: row.updated_at as Date,
170
+ }
171
+ }
172
+ }
package/src/types.ts ADDED
@@ -0,0 +1,97 @@
1
+ import type Stripe from 'stripe'
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Config
5
+ // ---------------------------------------------------------------------------
6
+
7
+ export interface CashierConfig {
8
+ /** Stripe secret key. */
9
+ secret: string
10
+ /** Stripe publishable key (passed to frontend). */
11
+ key: string
12
+ /** Stripe webhook signing secret. */
13
+ webhookSecret: string
14
+ /** Default currency code (lowercase). */
15
+ currency: string
16
+ /** The user model's primary key property name (e.g. 'id', 'uid'). */
17
+ userKey: string
18
+ /** URL prefix for Checkout success/cancel. */
19
+ urls: {
20
+ success: string
21
+ cancel: string
22
+ }
23
+ }
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Data Records
27
+ // ---------------------------------------------------------------------------
28
+
29
+ export interface CustomerData {
30
+ id: number
31
+ userId: string | number
32
+ stripeId: string
33
+ pmType: string | null
34
+ pmLastFour: string | null
35
+ trialEndsAt: Date | null
36
+ createdAt: Date
37
+ updatedAt: Date
38
+ }
39
+
40
+ export interface SubscriptionData {
41
+ id: number
42
+ userId: string | number
43
+ name: string
44
+ stripeId: string
45
+ stripeStatus: string
46
+ stripePriceId: string | null
47
+ quantity: number | null
48
+ trialEndsAt: Date | null
49
+ endsAt: Date | null
50
+ createdAt: Date
51
+ updatedAt: Date
52
+ }
53
+
54
+ export interface SubscriptionItemData {
55
+ id: number
56
+ subscriptionId: number
57
+ stripeId: string
58
+ stripeProductId: string
59
+ stripePriceId: string
60
+ quantity: number | null
61
+ createdAt: Date
62
+ updatedAt: Date
63
+ }
64
+
65
+ export interface ReceiptData {
66
+ id: number
67
+ userId: string | number
68
+ stripeId: string
69
+ amount: number
70
+ currency: string
71
+ description: string | null
72
+ receiptUrl: string | null
73
+ createdAt: Date
74
+ }
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // Subscription Status
78
+ // ---------------------------------------------------------------------------
79
+
80
+ export const SubscriptionStatus = {
81
+ Active: 'active',
82
+ Canceled: 'canceled',
83
+ Incomplete: 'incomplete',
84
+ IncompleteExpired: 'incomplete_expired',
85
+ PastDue: 'past_due',
86
+ Paused: 'paused',
87
+ Trialing: 'trialing',
88
+ Unpaid: 'unpaid',
89
+ } as const
90
+
91
+ export type SubscriptionStatusValue = (typeof SubscriptionStatus)[keyof typeof SubscriptionStatus]
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // Webhook
95
+ // ---------------------------------------------------------------------------
96
+
97
+ export type WebhookEventHandler = (event: Stripe.Event) => void | Promise<void>
package/src/webhook.ts ADDED
@@ -0,0 +1,168 @@
1
+ import type Stripe from 'stripe'
2
+ import type Context from '@stravigor/core/http/context'
3
+ import type { Handler } from '@stravigor/core/http'
4
+ import CashierManager from './cashier_manager.ts'
5
+ import Customer from './customer.ts'
6
+ import Subscription from './subscription.ts'
7
+ import SubscriptionItem from './subscription_item.ts'
8
+ import { WebhookSignatureError } from './errors.ts'
9
+ import type { WebhookEventHandler } from './types.ts'
10
+
11
+ /** Registry of custom webhook event handlers. */
12
+ const customHandlers = new Map<string, WebhookEventHandler[]>()
13
+
14
+ /**
15
+ * Register a custom handler for a Stripe webhook event type.
16
+ *
17
+ * @example
18
+ * import { onWebhookEvent } from '@stravigor/cashier/webhook'
19
+ *
20
+ * onWebhookEvent('invoice.payment_failed', async (event) => {
21
+ * const invoice = event.data.object as Stripe.Invoice
22
+ * // Send notification to user...
23
+ * })
24
+ */
25
+ export function onWebhookEvent(eventType: string, handler: WebhookEventHandler): void {
26
+ const handlers = customHandlers.get(eventType) ?? []
27
+ handlers.push(handler)
28
+ customHandlers.set(eventType, handlers)
29
+ }
30
+
31
+ /**
32
+ * Create a route handler for Stripe webhooks.
33
+ *
34
+ * Verifies the Stripe signature, dispatches built-in handlers to keep
35
+ * local DB in sync, then calls any custom handlers registered via
36
+ * `onWebhookEvent()`.
37
+ *
38
+ * @example
39
+ * import { stripeWebhook } from '@stravigor/cashier/webhook'
40
+ * router.post('/stripe/webhook', stripeWebhook())
41
+ */
42
+ export function stripeWebhook(): Handler {
43
+ return async (ctx: Context): Promise<Response> => {
44
+ const signature = ctx.header('stripe-signature')
45
+ if (!signature) {
46
+ return ctx.json({ error: 'Missing stripe-signature header' }, 400)
47
+ }
48
+
49
+ const rawBody = await ctx.request.text()
50
+ const webhookSecret = CashierManager.config.webhookSecret
51
+
52
+ let event: Stripe.Event
53
+ try {
54
+ event = CashierManager.stripe.webhooks.constructEvent(rawBody, signature, webhookSecret)
55
+ } catch {
56
+ throw new WebhookSignatureError()
57
+ }
58
+
59
+ // Dispatch built-in handlers
60
+ await handleBuiltinEvent(event)
61
+
62
+ // Dispatch custom handlers
63
+ const handlers = customHandlers.get(event.type) ?? []
64
+ for (const handler of handlers) {
65
+ await handler(event)
66
+ }
67
+
68
+ return ctx.json({ received: true }, 200)
69
+ }
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Built-in event handling: keeps local DB in sync with Stripe
74
+ // ---------------------------------------------------------------------------
75
+
76
+ async function handleBuiltinEvent(event: Stripe.Event): Promise<void> {
77
+ switch (event.type) {
78
+ // ---- Customer Events ----
79
+
80
+ case 'customer.updated': {
81
+ const stripeCustomer = event.data.object as Stripe.Customer
82
+ const defaultPm = stripeCustomer.invoice_settings?.default_payment_method
83
+ if (defaultPm && typeof defaultPm !== 'string') {
84
+ await Customer.updateDefaultPaymentMethod(stripeCustomer.id, defaultPm)
85
+ }
86
+ break
87
+ }
88
+
89
+ case 'customer.deleted': {
90
+ const stripeCustomer = event.data.object as Stripe.Customer
91
+ const customer = await Customer.findByStripeId(stripeCustomer.id)
92
+ if (customer) {
93
+ // Clean up all local subscription records
94
+ const subs = await Subscription.findByUser(customer.userId)
95
+ for (const sub of subs) {
96
+ await Subscription.delete(sub.id)
97
+ }
98
+ await Customer.deleteByStripeId(stripeCustomer.id)
99
+ }
100
+ break
101
+ }
102
+
103
+ // ---- Subscription Events ----
104
+
105
+ case 'customer.subscription.created': {
106
+ const stripeSub = event.data.object as Stripe.Subscription
107
+ const customerId =
108
+ typeof stripeSub.customer === 'string' ? stripeSub.customer : stripeSub.customer.id
109
+ const customer = await Customer.findByStripeId(customerId)
110
+
111
+ if (customer) {
112
+ const existing = await Subscription.findByStripeId(stripeSub.id)
113
+ if (!existing) {
114
+ const name = stripeSub.metadata?.strav_name ?? 'default'
115
+ const localSub = await Subscription.create({
116
+ user: customer.userId,
117
+ name,
118
+ stripeId: stripeSub.id,
119
+ stripeStatus: stripeSub.status,
120
+ stripePriceId: stripeSub.items.data[0]?.price.id ?? null,
121
+ quantity: stripeSub.items.data[0]?.quantity ?? null,
122
+ trialEndsAt: stripeSub.trial_end ? new Date(stripeSub.trial_end * 1000) : null,
123
+ })
124
+ await SubscriptionItem.syncFromStripe(localSub, localSub.id)
125
+ }
126
+ }
127
+ break
128
+ }
129
+
130
+ case 'customer.subscription.updated': {
131
+ const stripeSub = event.data.object as Stripe.Subscription
132
+
133
+ const endsAt = stripeSub.cancel_at
134
+ ? new Date(stripeSub.cancel_at * 1000)
135
+ : stripeSub.canceled_at
136
+ ? new Date(stripeSub.current_period_end * 1000)
137
+ : null
138
+
139
+ await Subscription.syncStripeStatus(stripeSub.id, stripeSub.status, endsAt)
140
+
141
+ // Sync items and metadata
142
+ const localSub = await Subscription.findByStripeId(stripeSub.id)
143
+ if (localSub) {
144
+ await SubscriptionItem.syncFromStripe(localSub, localSub.id)
145
+
146
+ // Update price_id, quantity, and trial from first item
147
+ const firstItem = stripeSub.items.data[0]
148
+ if (firstItem) {
149
+ await CashierManager.db.sql`
150
+ UPDATE "subscription"
151
+ SET "stripe_price_id" = ${firstItem.price.id},
152
+ "quantity" = ${firstItem.quantity ?? null},
153
+ "trial_ends_at" = ${stripeSub.trial_end ? new Date(stripeSub.trial_end * 1000) : null},
154
+ "updated_at" = NOW()
155
+ WHERE "stripe_id" = ${stripeSub.id}
156
+ `
157
+ }
158
+ }
159
+ break
160
+ }
161
+
162
+ case 'customer.subscription.deleted': {
163
+ const stripeSub = event.data.object as Stripe.Subscription
164
+ await Subscription.syncStripeStatus(stripeSub.id, 'canceled', new Date())
165
+ break
166
+ }
167
+ }
168
+ }
@@ -0,0 +1,27 @@
1
+ import { env } from '@stravigor/core/helpers'
2
+
3
+ export default {
4
+ /** Stripe secret key. */
5
+ secret: env('STRIPE_SECRET', ''),
6
+
7
+ /** Stripe publishable key. */
8
+ key: env('STRIPE_KEY', ''),
9
+
10
+ /** Stripe webhook signing secret. */
11
+ webhookSecret: env('STRIPE_WEBHOOK_SECRET', ''),
12
+
13
+ /** Default currency for charges. */
14
+ currency: 'usd',
15
+
16
+ /**
17
+ * The user model's primary key property (determines FK column name).
18
+ * 'id' → user_id, 'uid' → user_uid, etc.
19
+ */
20
+ userKey: 'id',
21
+
22
+ /** URLs for Stripe Checkout success/cancel redirects. */
23
+ urls: {
24
+ success: env('APP_URL', 'http://localhost:3000') + '/billing/success',
25
+ cancel: env('APP_URL', 'http://localhost:3000') + '/billing/cancel',
26
+ },
27
+ }
@@ -0,0 +1,12 @@
1
+ import { defineSchema, t, Archetype } from '@stravigor/core/schema'
2
+
3
+ export default defineSchema('customer', {
4
+ archetype: Archetype.Component,
5
+ parent: 'user',
6
+ fields: {
7
+ stripeId: t.varchar(255).required().unique().index(),
8
+ pmType: t.varchar(50).nullable(),
9
+ pmLastFour: t.varchar(4).nullable(),
10
+ trialEndsAt: t.timestamptz().nullable(),
11
+ },
12
+ })
@@ -0,0 +1,13 @@
1
+ import { defineSchema, t, Archetype } from '@stravigor/core/schema'
2
+
3
+ export default defineSchema('receipt', {
4
+ archetype: Archetype.Event,
5
+ parent: 'user',
6
+ fields: {
7
+ stripeId: t.varchar(255).required().unique().index(),
8
+ amount: t.integer().required(),
9
+ currency: t.varchar(3).required(),
10
+ description: t.text().nullable(),
11
+ receiptUrl: t.text().nullable(),
12
+ },
13
+ })
@@ -0,0 +1,15 @@
1
+ import { defineSchema, t, Archetype } from '@stravigor/core/schema'
2
+
3
+ export default defineSchema('subscription', {
4
+ archetype: Archetype.Component,
5
+ parent: 'user',
6
+ fields: {
7
+ name: t.varchar(255).required().index(),
8
+ stripeId: t.varchar(255).required().unique().index(),
9
+ stripeStatus: t.varchar(50).required(),
10
+ stripePriceId: t.varchar(255).nullable(),
11
+ quantity: t.integer().nullable(),
12
+ trialEndsAt: t.timestamptz().nullable(),
13
+ endsAt: t.timestamptz().nullable(),
14
+ },
15
+ })
@@ -0,0 +1,12 @@
1
+ import { defineSchema, t, Archetype } from '@stravigor/core/schema'
2
+
3
+ export default defineSchema('subscription_item', {
4
+ archetype: Archetype.Component,
5
+ parent: 'subscription',
6
+ fields: {
7
+ stripeId: t.varchar(255).required().unique().index(),
8
+ stripeProductId: t.varchar(255).required().index(),
9
+ stripePriceId: t.varchar(255).required().index(),
10
+ quantity: t.integer().nullable(),
11
+ },
12
+ })