Skip to content

Commit fd25f3e

Browse files
chore: wip
1 parent 3baecd2 commit fd25f3e

File tree

8 files changed

+219
-53
lines changed

8 files changed

+219
-53
lines changed

app/Actions/Payment/CreateCheckoutAction.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
1-
import type { RequestInstance } from '@stacksjs/types'
21
import { Action } from '@stacksjs/actions'
32
import User from '../../../storage/framework/orm/src/models/User.ts'
43

54
export default new Action({
65
name: 'CreateCheckoutAction',
76
description: 'Create Checkout link for stripe',
87
method: 'POST',
9-
async handle(request: RequestInstance) {
8+
async handle() {
109
const user = await User.find(1)
1110

12-
const checkout = await user?.checkout({
13-
price_1QBEfsBv6MhUdo23avVV0kqx: 1,
14-
}, {
15-
automatic_tax: { enabled: true },
16-
allow_promotion_codes: true,
11+
const checkout = await user?.checkout([
12+
{
13+
priceId: 'price_1QBEfsBv6MhUdo23avVV0kqx',
14+
quantity: 1,
15+
},
16+
], {
17+
enableTax: true,
18+
allowPromotions: true,
19+
cancel_url: 'https://google.com',
20+
success_url: 'https://google.com',
1721
})
1822

1923
return checkout
Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,17 @@
11
import { Action } from '@stacksjs/actions'
2-
import { stripe } from '@stacksjs/payments'
2+
import User from '../../../storage/framework/orm/src/models/User.ts'
33

44
export default new Action({
55
name: 'CreateSubscriptionAction',
66
description: 'Create Subscription for stripe',
77
method: 'POST',
88
async handle() {
9-
const subscription = await stripe.subscription.create({
10-
customer: 'cus_R5DJaEyyeKKlAN',
11-
items: [
12-
{
13-
price: 'price_1QCjMXBv6MhUdo23Pvb5dwUd',
14-
},
15-
],
16-
payment_behavior: 'default_incomplete',
17-
trial_period_days: 7,
18-
expand: ['latest_invoice.payment_intent'],
19-
})
9+
const user = await User.find(1)
2010

21-
// Step 3: Get the PaymentIntent for the first invoice from the subscription
22-
const latestInvoice = subscription.latest_invoice
23-
const paymentIntent = typeof latestInvoice === 'object' ? latestInvoice?.payment_intent : undefined
11+
const subscription = await user?.newSubscription('price_1QCjMXBv6MhUdo23Pvb5dwUd', {
12+
allowPromotions: true, // Example option for allowing promotions
13+
})
2414

25-
// Step 4: Pass the client_secret to the front end for Stripe Elements
26-
return paymentIntent
15+
return subscription
2716
},
2817
})

app/Actions/Payment/UpdateCustomerAction.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export default new Action({
1515
state: 'TX',
1616
postal_code: '78701',
1717
country: 'US',
18-
}})
18+
} })
1919

2020
return customer
2121
},

storage/framework/core/orm/src/utils.ts

Lines changed: 79 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1104,25 +1104,87 @@ export async function generateModelString(
11041104
return customer
11051105
}
11061106
1107+
async newSubscription(
1108+
priceId: string,
1109+
options: SubscriptionOptions = {},
1110+
): Promise<{ subscription: Stripe.Subscription, paymentIntent?: Stripe.PaymentIntent }> {
1111+
// Map price IDs to Stripe subscription items
1112+
const subscriptionItems = [{
1113+
price: priceId,
1114+
quantity: 1,
1115+
}]
1116+
1117+
// Ensure the customer is retrieved correctly
1118+
const customer = await this.createOrGetStripeUser({}).then((customer) => {
1119+
if (!customer || !customer.id) {
1120+
throw new Error('Customer does not exist in Stripe')
1121+
}
1122+
return customer.id // Ensure customer.id is always a string
1123+
})
1124+
1125+
// Configure optional subscription parameters based on options
1126+
const newOptions: Partial<Stripe.SubscriptionCreateParams> = {
1127+
items: subscriptionItems,
1128+
automatic_tax: options.enableTax ? { enabled: true } : undefined,
1129+
payment_behavior: options.allowPromotions ? 'default_incomplete' : undefined,
1130+
trial_period_days: options.trialDays,
1131+
}
1132+
1133+
// Define core subscription parameters, including customer association and expansion options
1134+
const defaultOptions: Stripe.SubscriptionCreateParams = {
1135+
customer, // This is guaranteed to be a string
1136+
payment_behavior: 'default_incomplete',
1137+
expand: ['latest_invoice.payment_intent'],
1138+
// Apply trial_days only if specified
1139+
trial_period_days: options.trialDays || undefined,
1140+
items: subscriptionItems, // Add the subscription items here
1141+
}
1142+
1143+
// Merge new options with default options, giving priority to provided options
1144+
const mergedOptions: Stripe.SubscriptionCreateParams = {
1145+
...defaultOptions,
1146+
...newOptions,
1147+
}
1148+
1149+
// Create the subscription
1150+
const subscription = await manageSubscription.create(this, mergedOptions)
11071151
1108-
async checkout(
1109-
priceIds: Record<string, number | undefined>,
1110-
options: Partial<Stripe.Checkout.SessionCreateParams> = {}
1152+
// Retrieve the latest invoice and payment intent for further use
1153+
const latestInvoice = subscription.latest_invoice as Stripe.Invoice | null
1154+
const paymentIntent = latestInvoice?.payment_intent as Stripe.PaymentIntent | undefined
1155+
1156+
return { subscription, paymentIntent }
1157+
}
1158+
1159+
async checkout(
1160+
priceIds: CheckoutLineItem[],
1161+
options: CheckoutOptions = {},
11111162
): Promise<Stripe.Response<Stripe.Checkout.Session>> {
1112-
const defaultOptions: Partial<Stripe.Checkout.SessionCreateParams> = {
1113-
mode: 'payment',
1114-
customer: await this.createOrGetStripeUser({}).then(customer => customer.id),
1115-
line_items: Object.entries(priceIds).map(([priceId, quantity]) => ({
1116-
price: priceId,
1117-
quantity: quantity || 1
1118-
})),
1119-
success_url: 'http://localhost:3008/checkout/success?session_id={CHECKOUT_SESSION_ID}',
1120-
cancel_url: 'http://localhost:3008/checkout/cancel',
1121-
};
1163+
const newOptions: Partial<Stripe.Checkout.SessionCreateParams> = {}
1164+
1165+
if (options.enableTax) {
1166+
newOptions.automatic_tax = { enabled: true }
1167+
delete options.enableTax
1168+
}
1169+
1170+
if (options.allowPromotions) {
1171+
newOptions.allow_promotion_codes = true
1172+
delete options.allowPromotions
1173+
}
1174+
1175+
const defaultOptions: Partial<Stripe.Checkout.SessionCreateParams> = {
1176+
mode: 'payment',
1177+
customer: await this.createOrGetStripeUser({}).then(customer => customer.id),
1178+
line_items: priceIds.map((item: CheckoutLineItem) => ({
1179+
price: item.priceId,
1180+
quantity: item.quantity || 1,
1181+
})),
1182+
1183+
}
11221184
1123-
const mergedOptions = { ...defaultOptions, ...options };
1185+
const mergedOptions = { ...defaultOptions, ...newOptions, ...options }
11241186
1125-
return await manageCheckout.create(this, mergedOptions);
1187+
return await manageCheckout.create(this, mergedOptions)
11261188
}
11271189
`
11281190

@@ -1250,9 +1312,9 @@ export async function generateModelString(
12501312
const fillable = JSON.stringify(getFillableAttributes(model.attributes))
12511313

12521314
return `import type { Generated, Insertable, Selectable, Updateable } from 'kysely'
1253-
import { manageCustomer, manageCheckout, managePaymentMethod, manageCharge, type Stripe } from '@stacksjs/payments'
1315+
import { manageCharge, manageCheckout, manageCustomer, managePaymentMethod, manageSubscription, type Stripe } from '@stacksjs/payments'
12541316
import { db, sql } from '@stacksjs/database'
1255-
import { StripeCustomerOptions } from '@stacksjs/types'
1317+
import type { CheckoutLineItem, CheckoutOptions, StripeCustomerOptions, SubscriptionOptions } from '@stacksjs/types'
12561318
import { HttpError } from '@stacksjs/error-handling'
12571319
import { dispatch } from '@stacksjs/events'
12581320
import { generateTwoFactorSecret } from '@stacksjs/auth'
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type Stripe from 'stripe'
2+
import type { UserModel } from '../../../../orm/src/models/User'
3+
import { stripe } from '..'
4+
5+
export interface SubscriptionManager {
6+
create: (user: UserModel, params: Stripe.SubscriptionCreateParams) => Promise<Stripe.Response<Stripe.Subscription>>
7+
}
8+
9+
export const manageSubscription: SubscriptionManager = (() => {
10+
async function create(user: UserModel, params: Stripe.SubscriptionCreateParams): Promise<Stripe.Response<Stripe.Subscription>> {
11+
// Check if the user has a Stripe customer ID
12+
if (!user.hasStripeId()) {
13+
throw new Error('Customer does not exist in Stripe')
14+
}
15+
16+
// Define default parameters
17+
const defaultParams: Partial<Stripe.SubscriptionCreateParams> = {
18+
customer: user.stripeId(),
19+
payment_behavior: 'default_incomplete',
20+
expand: ['latest_invoice.payment_intent'],
21+
}
22+
23+
// Merge provided params with defaultParams
24+
const mergedParams = { ...defaultParams, ...params }
25+
26+
// Create and return the subscription
27+
return await stripe.subscription.create(mergedParams)
28+
}
29+
30+
return { create }
31+
})()

storage/framework/core/payments/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ export * from './billable/charge'
22
export * from './billable/checkout'
33
export * from './billable/customer'
44
export * from './billable/payment-method'
5+
export * from './billable/subscription'
56
export * as stripe from './drivers/stripe'
67
export * from 'stripe'

storage/framework/core/types/src/payments.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,19 @@ export interface EventOptions {
8080
type?: string
8181
}
8282
}
83+
84+
export interface CheckoutLineItem {
85+
priceId: string
86+
quantity: number
87+
}
88+
89+
export interface CheckoutOptions extends Partial<Stripe.Checkout.SessionCreateParams> {
90+
enableTax?: boolean
91+
allowPromotions?: boolean
92+
}
93+
94+
export interface SubscriptionOptions extends Partial<Stripe.SubscriptionCreateParams> {
95+
enableTax?: boolean
96+
allowPromotions?: boolean
97+
trialDays?: number
98+
}

storage/framework/orm/src/models/User.ts

Lines changed: 74 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import type { StripeCustomerOptions } from '@stacksjs/types'
1+
import type { CheckoutLineItem, CheckoutOptions, StripeCustomerOptions, SubscriptionOptions } from '@stacksjs/types'
22
import type { Generated, Insertable, Selectable, Updateable } from 'kysely'
33
import { cache } from '@stacksjs/cache'
44
import { db, sql } from '@stacksjs/database'
55
import { HttpError } from '@stacksjs/error-handling'
66
import { dispatch } from '@stacksjs/events'
7-
import { manageCharge, manageCheckout, manageCustomer, managePaymentMethod, type Stripe } from '@stacksjs/payments'
7+
import { manageCharge, manageCheckout, manageCustomer, managePaymentMethod, manageSubscription, type Stripe } from '@stacksjs/payments'
88

99
import Post from './Post'
1010

@@ -53,7 +53,7 @@ export type Users = UserType[]
5353
export type UserColumn = Users
5454
export type UserColumns = Array<keyof Users>
5555

56-
type SortDirection = 'asc' | 'desc'
56+
type SortDirection = 'asc' | 'desc'
5757
interface SortOptions { column: UserType, order: SortDirection }
5858
// Define a type for the options parameter
5959
interface QueryOptions {
@@ -736,22 +736,85 @@ export class UserModel {
736736
return customer
737737
}
738738

739+
async newSubscription(
740+
priceId: string,
741+
options: SubscriptionOptions = {},
742+
): Promise<{ subscription: Stripe.Subscription, paymentIntent?: Stripe.PaymentIntent }> {
743+
// Map price IDs to Stripe subscription items
744+
const subscriptionItems = [{
745+
price: priceId,
746+
quantity: 1,
747+
}]
748+
749+
// Ensure the customer is retrieved correctly
750+
const customer = await this.createOrGetStripeUser({}).then((customer) => {
751+
if (!customer || !customer.id) {
752+
throw new Error('Customer does not exist in Stripe')
753+
}
754+
return customer.id // Ensure customer.id is always a string
755+
})
756+
757+
// Configure optional subscription parameters based on options
758+
const newOptions: Partial<Stripe.SubscriptionCreateParams> = {
759+
items: subscriptionItems,
760+
automatic_tax: options.enableTax ? { enabled: true } : undefined,
761+
payment_behavior: options.allowPromotions ? 'default_incomplete' : undefined,
762+
trial_period_days: options.trialDays,
763+
}
764+
765+
// Define core subscription parameters, including customer association and expansion options
766+
const defaultOptions: Stripe.SubscriptionCreateParams = {
767+
customer, // This is guaranteed to be a string
768+
payment_behavior: 'default_incomplete',
769+
expand: ['latest_invoice.payment_intent'],
770+
// Apply trial_days only if specified
771+
trial_period_days: options.trialDays || undefined,
772+
items: subscriptionItems, // Add the subscription items here
773+
}
774+
775+
// Merge new options with default options, giving priority to provided options
776+
const mergedOptions: Stripe.SubscriptionCreateParams = {
777+
...defaultOptions,
778+
...newOptions,
779+
}
780+
781+
// Create the subscription
782+
const subscription = await manageSubscription.create(this, mergedOptions)
783+
784+
// Retrieve the latest invoice and payment intent for further use
785+
const latestInvoice = subscription.latest_invoice as Stripe.Invoice | null
786+
const paymentIntent = latestInvoice?.payment_intent as Stripe.PaymentIntent | undefined
787+
788+
return { subscription, paymentIntent }
789+
}
790+
739791
async checkout(
740-
priceIds: Record<string, number | undefined>,
741-
options: Partial<Stripe.Checkout.SessionCreateParams> = {},
792+
priceIds: CheckoutLineItem[],
793+
options: CheckoutOptions = {},
742794
): Promise<Stripe.Response<Stripe.Checkout.Session>> {
795+
const newOptions: Partial<Stripe.Checkout.SessionCreateParams> = {}
796+
797+
if (options.enableTax) {
798+
newOptions.automatic_tax = { enabled: true }
799+
delete options.enableTax
800+
}
801+
802+
if (options.allowPromotions) {
803+
newOptions.allow_promotion_codes = true
804+
delete options.allowPromotions
805+
}
806+
743807
const defaultOptions: Partial<Stripe.Checkout.SessionCreateParams> = {
744808
mode: 'payment',
745809
customer: await this.createOrGetStripeUser({}).then(customer => customer.id),
746-
line_items: Object.entries(priceIds).map(([priceId, quantity]) => ({
747-
price: priceId,
748-
quantity: quantity || 1,
810+
line_items: priceIds.map((item: CheckoutLineItem) => ({
811+
price: item.priceId,
812+
quantity: item.quantity || 1,
749813
})),
750-
success_url: 'http://localhost:3008/checkout/success?session_id={CHECKOUT_SESSION_ID}',
751-
cancel_url: 'http://localhost:3008/checkout/cancel',
814+
752815
}
753816

754-
const mergedOptions = { ...defaultOptions, ...options }
817+
const mergedOptions = { ...defaultOptions, ...newOptions, ...options }
755818

756819
return await manageCheckout.create(this, mergedOptions)
757820
}

0 commit comments

Comments
 (0)