Skip to content

Commit 934e989

Browse files
chore: wip
1 parent a3c9e58 commit 934e989

File tree

6 files changed

+149
-92
lines changed

6 files changed

+149
-92
lines changed

config/saas.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* **Payment Configuration**
3+
*
4+
* This configuration defines all of your Payment options. Because Stacks is fully-typed,
5+
* you may hover any of the options below and the definitions will be provided. In case
6+
* you have any questions, feel free to reach out via Discord or GitHub Discussions.
7+
*/
8+
export default {
9+
plans: [
10+
{
11+
productName: 'Stacks Pro',
12+
description: 'Access to all premium features of Stacks.',
13+
pricing: [
14+
{
15+
key: 'stacks_pro_monthly',
16+
price: 2000,
17+
interval: 'monthly',
18+
currency: 'usd',
19+
},
20+
{
21+
key: 'stacks_pro_yearly',
22+
price: 20000,
23+
interval: 'yearly',
24+
currency: 'usd',
25+
},
26+
],
27+
metadata: {
28+
createdBy: 'admin',
29+
version: '1.0.0',
30+
},
31+
},
32+
// Add other plans as needed
33+
],
34+
35+
webhook: {
36+
endpoint: '/webhooks/stripe',
37+
secret: '',
38+
},
39+
40+
currencies: ['usd'],
41+
42+
coupons: [
43+
{
44+
code: 'SUMMER2024',
45+
amountOff: 500,
46+
duration: 'once',
47+
},
48+
],
49+
50+
products: [
51+
{
52+
name: 'Stacks Pro',
53+
description: 'Access to all premium features of Stacks.',
54+
images: ['url_to_image'],
55+
},
56+
],
57+
} satisfies any

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

Lines changed: 15 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,58 +1151,32 @@ export async function generateModelString(
11511151
return await manageSubscription.isValid(this, type)
11521152
}
11531153
1154+
async isIncomplete(type: string): Promise<boolean> {
1155+
return await manageSubscription.isIncomplete(this, type)
1156+
}
1157+
11541158
async newSubscriptionInvoice(
11551159
type: string,
11561160
priceId: string,
1157-
options: Partial<Stripe.SubscriptionCreateParams> = {},
1161+
options: Partial<Stripe.SubscriptionCreateParams> = {},
11581162
): Promise<{ subscription: Stripe.Subscription, paymentIntent?: Stripe.PaymentIntent }> {
11591163
return await this.newSubscription(type, priceId, { ...options, days_until_due: 15, collection_method: 'send_invoice' })
11601164
}
11611165
1162-
async newSubscription(
1163-
type: string,
1164-
priceId: string,
1165-
options: Partial<Stripe.SubscriptionCreateParams> = {},
1166-
): Promise<{ subscription: Stripe.Subscription, paymentIntent?: Stripe.PaymentIntent }> {
1167-
const price = await managePrice.retrieveByLookupKey(priceId)
1168-
1169-
if (!price)
1170-
throw new Error('Price does not exist in stripe')
1171-
1172-
const subscriptionItems = [{
1173-
price: price.id,
1174-
quantity: 1,
1175-
}]
1176-
1177-
const customer = await this.createOrGetStripeUser({}).then((customer) => {
1178-
if (!customer || !customer.id) {
1179-
throw new Error('Customer does not exist in Stripe')
1180-
}
1181-
return customer.id
1182-
})
1166+
async newSubscription(
1167+
type: string,
1168+
priceId: string,
1169+
options: Partial<Stripe.SubscriptionCreateParams> = {},
1170+
): Promise<{ subscription: Stripe.Subscription, paymentIntent?: Stripe.PaymentIntent }> {
1171+
const subscription = await manageSubscription.create(this, type, priceId, options)
11831172
1184-
const defaultOptions: Stripe.SubscriptionCreateParams = {
1185-
customer,
1186-
payment_behavior: 'default_incomplete',
1187-
expand: ['latest_invoice.payment_intent'],
1188-
items: subscriptionItems,
1189-
}
1173+
const latestInvoice = subscription.latest_invoice as Stripe.Invoice | null
1174+
const paymentIntent = latestInvoice?.payment_intent as Stripe.PaymentIntent | undefined
11901175
1191-
const mergedOptions: Stripe.SubscriptionCreateParams = {
1192-
...defaultOptions,
1193-
...options,
1176+
return { subscription, paymentIntent }
11941177
}
11951178
1196-
const subscription = await manageSubscription.create(this, type, mergedOptions)
1197-
1198-
const latestInvoice = subscription.latest_invoice as Stripe.Invoice | null
1199-
const paymentIntent = latestInvoice?.payment_intent as Stripe.PaymentIntent | undefined
1200-
1201-
return { subscription, paymentIntent }
1202-
}
1203-
1204-
1205-
async checkout(
1179+
async checkout(
12061180
priceIds: CheckoutLineItem[],
12071181
options: CheckoutOptions = {},
12081182
): Promise<Stripe.Response<Stripe.Checkout.Session>> {

storage/framework/core/payments/src/billable/price.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { stripe } from '..'
33

44
export interface PriceManager {
55
retrieveByLookupKey: (lookupKey: string) => Promise<Stripe.Price>
6+
createOrGet: (lookupKey: string, params: Stripe.PriceCreateParams) => Promise<Stripe.Price>
67
}
78

89
export const managePrice: PriceManager = (() => {
@@ -12,5 +13,22 @@ export const managePrice: PriceManager = (() => {
1213
return prices.data[0]
1314
}
1415

15-
return { retrieveByLookupKey }
16+
async function createOrGet(
17+
lookupKey: string,
18+
params: Stripe.PriceCreateParams,
19+
): Promise<Stripe.Price> {
20+
const existingPrice = await retrieveByLookupKey(lookupKey)
21+
22+
if (existingPrice)
23+
return existingPrice
24+
25+
const newPrice = await stripe.price.create({
26+
...params,
27+
lookup_key: lookupKey,
28+
})
29+
30+
return newPrice
31+
}
32+
33+
return { retrieveByLookupKey, createOrGet }
1634
})()

storage/framework/core/payments/src/billable/subscription.ts

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,40 @@ import type Stripe from 'stripe'
22
import type { SubscriptionModel } from '../../../../orm/src/models/Subscription'
33
import type { UserModel } from '../../../../orm/src/models/User'
44

5-
import { stripe } from '..'
5+
import { manageCustomer, managePrice, stripe } from '..'
66
import Subscription from '../../../../orm/src/models/Subscription'
77

88
export interface SubscriptionManager {
9-
create: (user: UserModel, type: string, params: Stripe.SubscriptionCreateParams) => Promise<Stripe.Response<Stripe.Subscription>>
9+
create: (user: UserModel, type: string, priceId: string, params: Partial<Stripe.SubscriptionCreateParams>) => Promise<Stripe.Response<Stripe.Subscription>>
1010
isValid: (user: UserModel, type: string) => Promise<boolean>
11+
isIncomplete: (user: UserModel, type: string) => Promise<boolean>
1112
}
1213

1314
export const manageSubscription: SubscriptionManager = (() => {
14-
async function create(user: UserModel, type: string, params: Stripe.SubscriptionCreateParams): Promise<Stripe.Response<Stripe.Subscription>> {
15-
if (!user.hasStripeId()) {
16-
throw new Error('Customer does not exist in Stripe')
17-
}
15+
async function create(user: UserModel, type: string, priceId: string, params: Partial<Stripe.SubscriptionCreateParams>): Promise<Stripe.Response<Stripe.Subscription>> {
16+
const price = await managePrice.retrieveByLookupKey(priceId)
17+
18+
if (!price)
19+
throw new Error('Price does not exist in stripe')
20+
21+
const subscriptionItems = [{
22+
price: price.id,
23+
quantity: 1,
24+
}]
25+
26+
const customerId = await manageCustomer.createOrGetStripeUser(user, {}).then((customer) => {
27+
if (!customer || !customer.id) {
28+
throw new Error('Customer does not exist in Stripe')
29+
}
30+
31+
return customer.id
32+
})
1833

19-
const defaultParams: Partial<Stripe.SubscriptionCreateParams> = {
20-
customer: user.stripeId(),
34+
const defaultParams: Stripe.SubscriptionCreateParams = {
35+
customer: customerId,
2136
payment_behavior: 'default_incomplete',
2237
expand: ['latest_invoice.payment_intent'],
38+
items: subscriptionItems,
2339
}
2440

2541
const mergedParams = { ...defaultParams, ...params }
@@ -35,14 +51,21 @@ export const manageSubscription: SubscriptionManager = (() => {
3551
return subscription.stripe_status === 'active'
3652
}
3753

38-
async function isIncomplete(subscription: SubscriptionModel): Promise<boolean> {
39-
return subscription.stripe_status === 'incomplete'
40-
}
41-
4254
async function isTrial(subscription: SubscriptionModel): Promise<boolean> {
4355
return subscription.stripe_status === 'trialing'
4456
}
4557

58+
async function isIncomplete(user: UserModel, type: string): Promise<boolean> {
59+
const subscription = await Subscription.where('user_id', user.id)
60+
.where('type', type)
61+
.first()
62+
63+
if (!subscription)
64+
return false
65+
66+
return subscription.stripe_status === 'incomplete'
67+
}
68+
4669
async function isValid(user: UserModel, type: string): Promise<boolean> {
4770
const subscription = await Subscription.where('user_id', user.id)
4871
.where('type', type)

storage/framework/core/payments/src/drivers/stripe.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,20 +113,30 @@ export const subscription: Subscription = (() => {
113113
})()
114114

115115
export interface Price {
116-
retrieve: (priceId: string, params: Stripe.PriceRetrieveParams) => Promise<Stripe.Response<Stripe.PriceRetrieveParams>>
117-
list: (params: Stripe.PriceListParams) => Promise<Stripe.Response<Stripe.ApiListPromise<Stripe.Price>>>
116+
retrieve: (priceId: string, params: Stripe.PriceRetrieveParams) => Promise<Stripe.Response<Stripe.Price>>
117+
list: (params: Stripe.PriceListParams) => Promise<Stripe.ApiListPromise<Stripe.Price>>
118+
create: (params: Stripe.PriceCreateParams) => Promise<Stripe.Response<Stripe.Price>>
119+
update: (priceId: string, params: Stripe.PriceUpdateParams) => Promise<Stripe.Response<Stripe.Price>>
118120
}
119121

120122
export const price: Price = (() => {
121-
async function retrieve(priceId: string, params: Stripe.PriceRetrieveParams): Promise<Stripe.Response<Stripe.PriceRetrieveParams>> {
123+
async function retrieve(priceId: string, params: Stripe.PriceRetrieveParams): Promise<Stripe.Response<Stripe.Price>> {
122124
return await client.prices.retrieve(priceId, params)
123125
}
124126

125-
async function list(params: Stripe.PriceListParams): Promise<Stripe.Response<Stripe.ApiListPromise<Stripe.Price>>> {
127+
async function list(params: Stripe.PriceListParams): Promise<Stripe.ApiListPromise<Stripe.Price>> {
126128
return await client.prices.list(params)
127129
}
128130

129-
return { retrieve, list }
131+
async function create(params: Stripe.PriceCreateParams): Promise<Stripe.Response<Stripe.Price>> {
132+
return await client.prices.create(params)
133+
}
134+
135+
async function update(priceId: string, params: Stripe.PriceUpdateParams): Promise<Stripe.Response<Stripe.Price>> {
136+
return await client.prices.update(priceId, params)
137+
}
138+
139+
return { retrieve, list, create, update }
130140
})()
131141

132142
export interface Refund {

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

Lines changed: 8 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ 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, managePrice, manageSubscription, type Stripe } from '@stacksjs/payments'
7+
import { manageCharge, manageCheckout, manageCustomer, managePaymentMethod, manageSubscription, type Stripe } from '@stacksjs/payments'
88

99
import Post from './Post'
1010

@@ -756,49 +756,24 @@ export class UserModel {
756756
return await manageSubscription.isValid(this, type)
757757
}
758758

759+
async isIncomplete(type: string): Promise<boolean> {
760+
return await manageSubscription.isIncomplete(this, type)
761+
}
762+
759763
async newSubscriptionInvoice(
760764
type: string,
761765
priceId: string,
762-
options: Partial<Stripe.SubscriptionCreateParams> = {},
766+
options: Partial<Stripe.SubscriptionCreateParams> = {},
763767
): Promise<{ subscription: Stripe.Subscription, paymentIntent?: Stripe.PaymentIntent }> {
764768
return await this.newSubscription(type, priceId, { ...options, days_until_due: 15, collection_method: 'send_invoice' })
765769
}
766770

767771
async newSubscription(
768772
type: string,
769773
priceId: string,
770-
options: Partial<Stripe.SubscriptionCreateParams> = {},
774+
options: Partial<Stripe.SubscriptionCreateParams> = {},
771775
): Promise<{ subscription: Stripe.Subscription, paymentIntent?: Stripe.PaymentIntent }> {
772-
const price = await managePrice.retrieveByLookupKey(priceId)
773-
774-
if (!price)
775-
throw new Error('Price does not exist in stripe')
776-
777-
const subscriptionItems = [{
778-
price: price.id,
779-
quantity: 1,
780-
}]
781-
782-
const customer = await this.createOrGetStripeUser({}).then((customer) => {
783-
if (!customer || !customer.id) {
784-
throw new Error('Customer does not exist in Stripe')
785-
}
786-
return customer.id
787-
})
788-
789-
const defaultOptions: Stripe.SubscriptionCreateParams = {
790-
customer,
791-
payment_behavior: 'default_incomplete',
792-
expand: ['latest_invoice.payment_intent'],
793-
items: subscriptionItems,
794-
}
795-
796-
const mergedOptions: Stripe.SubscriptionCreateParams = {
797-
...defaultOptions,
798-
...options,
799-
}
800-
801-
const subscription = await manageSubscription.create(this, type, mergedOptions)
776+
const subscription = await manageSubscription.create(this, type, priceId, options)
802777

803778
const latestInvoice = subscription.latest_invoice as Stripe.Invoice | null
804779
const paymentIntent = latestInvoice?.payment_intent as Stripe.PaymentIntent | undefined

0 commit comments

Comments
 (0)