@jonsoc/console-app 1.1.34
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/.opencode/agent/css.md +149 -0
- package/README.md +32 -0
- package/package.json +49 -0
- package/public/apple-touch-icon-v3.png +1 -0
- package/public/apple-touch-icon.png +1 -0
- package/public/email +1 -0
- package/public/favicon-96x96-v3.png +1 -0
- package/public/favicon-96x96.png +1 -0
- package/public/favicon-v3.ico +1 -0
- package/public/favicon-v3.svg +1 -0
- package/public/favicon.ico +1 -0
- package/public/favicon.svg +1 -0
- package/public/opencode-brand-assets.zip +0 -0
- package/public/robots.txt +6 -0
- package/public/site.webmanifest +1 -0
- package/public/social-share-black.png +1 -0
- package/public/social-share-zen.png +1 -0
- package/public/social-share.png +1 -0
- package/public/theme.json +182 -0
- package/public/web-app-manifest-192x192.png +1 -0
- package/public/web-app-manifest-512x512.png +1 -0
- package/script/generate-sitemap.ts +103 -0
- package/src/app.css +1 -0
- package/src/app.tsx +27 -0
- package/src/asset/black/hero.png +0 -0
- package/src/asset/brand/opencode-brand-assets.zip +0 -0
- package/src/asset/brand/opencode-logo-dark.png +0 -0
- package/src/asset/brand/opencode-logo-dark.svg +16 -0
- package/src/asset/brand/opencode-logo-light.png +0 -0
- package/src/asset/brand/opencode-logo-light.svg +16 -0
- package/src/asset/brand/opencode-wordmark-dark.png +0 -0
- package/src/asset/brand/opencode-wordmark-dark.svg +30 -0
- package/src/asset/brand/opencode-wordmark-light.png +0 -0
- package/src/asset/brand/opencode-wordmark-light.svg +30 -0
- package/src/asset/brand/opencode-wordmark-simple-dark.png +0 -0
- package/src/asset/brand/opencode-wordmark-simple-dark.svg +22 -0
- package/src/asset/brand/opencode-wordmark-simple-light.png +0 -0
- package/src/asset/brand/opencode-wordmark-simple-light.svg +22 -0
- package/src/asset/brand/preview-opencode-dark.png +0 -0
- package/src/asset/brand/preview-opencode-logo-dark.png +0 -0
- package/src/asset/brand/preview-opencode-logo-light.png +0 -0
- package/src/asset/brand/preview-opencode-wordmark-dark.png +0 -0
- package/src/asset/brand/preview-opencode-wordmark-light.png +0 -0
- package/src/asset/brand/preview-opencode-wordmark-simple-dark.png +0 -0
- package/src/asset/brand/preview-opencode-wordmark-simple-light.png +0 -0
- package/src/asset/lander/avatar-adam.png +0 -0
- package/src/asset/lander/avatar-david.png +0 -0
- package/src/asset/lander/avatar-dax.png +0 -0
- package/src/asset/lander/avatar-frank.png +0 -0
- package/src/asset/lander/avatar-jay.png +0 -0
- package/src/asset/lander/brand-assets-dark.svg +10 -0
- package/src/asset/lander/brand-assets-light.svg +10 -0
- package/src/asset/lander/brand.png +0 -0
- package/src/asset/lander/check.svg +3 -0
- package/src/asset/lander/copy.svg +3 -0
- package/src/asset/lander/desktop-app-icon.png +0 -0
- package/src/asset/lander/dock.png +0 -0
- package/src/asset/lander/logo-dark.svg +11 -0
- package/src/asset/lander/logo-light.svg +11 -0
- package/src/asset/lander/opencode-comparison-min.mp4 +0 -0
- package/src/asset/lander/opencode-comparison-poster.png +0 -0
- package/src/asset/lander/opencode-desktop-icon.png +0 -0
- package/src/asset/lander/opencode-logo-dark.svg +11 -0
- package/src/asset/lander/opencode-logo-light.svg +11 -0
- package/src/asset/lander/opencode-min.mp4 +0 -0
- package/src/asset/lander/opencode-poster.png +0 -0
- package/src/asset/lander/opencode-wordmark-dark.svg +25 -0
- package/src/asset/lander/opencode-wordmark-light.svg +25 -0
- package/src/asset/lander/screenshot-github.png +0 -0
- package/src/asset/lander/screenshot-splash.png +0 -0
- package/src/asset/lander/screenshot-vscode.png +0 -0
- package/src/asset/lander/screenshot.png +0 -0
- package/src/asset/lander/wordmark-dark.svg +3 -0
- package/src/asset/lander/wordmark-light.svg +3 -0
- package/src/asset/logo-ornate-dark.svg +18 -0
- package/src/asset/logo-ornate-light.svg +18 -0
- package/src/asset/logo.svg +18 -0
- package/src/asset/zen-ornate-dark.svg +8 -0
- package/src/asset/zen-ornate-light.svg +8 -0
- package/src/component/dropdown.css +80 -0
- package/src/component/dropdown.tsx +79 -0
- package/src/component/email-signup.tsx +48 -0
- package/src/component/faq.tsx +33 -0
- package/src/component/footer.tsx +38 -0
- package/src/component/header-context-menu.css +63 -0
- package/src/component/header.tsx +279 -0
- package/src/component/icon.tsx +257 -0
- package/src/component/legal.tsx +20 -0
- package/src/component/modal.css +66 -0
- package/src/component/modal.tsx +24 -0
- package/src/component/spotlight.css +15 -0
- package/src/component/spotlight.tsx +820 -0
- package/src/config.ts +29 -0
- package/src/context/auth.session.ts +0 -0
- package/src/context/auth.ts +116 -0
- package/src/context/auth.withActor.ts +7 -0
- package/src/entry-client.tsx +4 -0
- package/src/entry-server.tsx +30 -0
- package/src/global.d.ts +5 -0
- package/src/lib/github.ts +38 -0
- package/src/middleware.ts +5 -0
- package/src/routes/[...404].css +130 -0
- package/src/routes/[...404].tsx +38 -0
- package/src/routes/api/enterprise.ts +47 -0
- package/src/routes/auth/[...callback].ts +41 -0
- package/src/routes/auth/authorize.ts +10 -0
- package/src/routes/auth/index.ts +12 -0
- package/src/routes/auth/logout.ts +17 -0
- package/src/routes/auth/status.ts +7 -0
- package/src/routes/bench/[id].tsx +365 -0
- package/src/routes/bench/index.tsx +86 -0
- package/src/routes/bench/submission.ts +29 -0
- package/src/routes/black/common.tsx +62 -0
- package/src/routes/black/index.tsx +108 -0
- package/src/routes/black/subscribe/[plan].tsx +449 -0
- package/src/routes/black/workspace.css +214 -0
- package/src/routes/black/workspace.tsx +229 -0
- package/src/routes/black.css +828 -0
- package/src/routes/black.tsx +285 -0
- package/src/routes/brand/index.css +555 -0
- package/src/routes/brand/index.tsx +252 -0
- package/src/routes/changelog/index.css +477 -0
- package/src/routes/changelog/index.tsx +147 -0
- package/src/routes/debug/index.ts +13 -0
- package/src/routes/desktop-feedback.ts +5 -0
- package/src/routes/discord.ts +5 -0
- package/src/routes/docs/[...path].ts +20 -0
- package/src/routes/docs/index.ts +20 -0
- package/src/routes/download/[platform].ts +38 -0
- package/src/routes/download/index.css +750 -0
- package/src/routes/download/index.tsx +482 -0
- package/src/routes/download/types.ts +4 -0
- package/src/routes/enterprise/index.css +578 -0
- package/src/routes/enterprise/index.tsx +251 -0
- package/src/routes/index.css +1251 -0
- package/src/routes/index.tsx +840 -0
- package/src/routes/legal/privacy-policy/index.css +343 -0
- package/src/routes/legal/privacy-policy/index.tsx +1512 -0
- package/src/routes/legal/terms-of-service/index.css +254 -0
- package/src/routes/legal/terms-of-service/index.tsx +512 -0
- package/src/routes/openapi.json.ts +7 -0
- package/src/routes/s/[id].ts +20 -0
- package/src/routes/stripe/webhook.ts +532 -0
- package/src/routes/t/[...path].tsx +20 -0
- package/src/routes/temp.tsx +172 -0
- package/src/routes/user-menu.css +18 -0
- package/src/routes/user-menu.tsx +32 -0
- package/src/routes/workspace/[id]/billing/billing-section.module.css +185 -0
- package/src/routes/workspace/[id]/billing/billing-section.tsx +240 -0
- package/src/routes/workspace/[id]/billing/black-section.module.css +142 -0
- package/src/routes/workspace/[id]/billing/black-section.tsx +269 -0
- package/src/routes/workspace/[id]/billing/black-waitlist-section.module.css +23 -0
- package/src/routes/workspace/[id]/billing/index.tsx +32 -0
- package/src/routes/workspace/[id]/billing/monthly-limit-section.module.css +96 -0
- package/src/routes/workspace/[id]/billing/monthly-limit-section.tsx +133 -0
- package/src/routes/workspace/[id]/billing/payment-section.module.css +93 -0
- package/src/routes/workspace/[id]/billing/payment-section.tsx +122 -0
- package/src/routes/workspace/[id]/billing/reload-section.module.css +261 -0
- package/src/routes/workspace/[id]/billing/reload-section.tsx +213 -0
- package/src/routes/workspace/[id]/graph-section.module.css +145 -0
- package/src/routes/workspace/[id]/graph-section.tsx +475 -0
- package/src/routes/workspace/[id]/index.tsx +81 -0
- package/src/routes/workspace/[id]/keys/index.tsx +11 -0
- package/src/routes/workspace/[id]/keys/key-section.module.css +197 -0
- package/src/routes/workspace/[id]/keys/key-section.tsx +176 -0
- package/src/routes/workspace/[id]/members/index.tsx +11 -0
- package/src/routes/workspace/[id]/members/member-section.module.css +249 -0
- package/src/routes/workspace/[id]/members/member-section.tsx +343 -0
- package/src/routes/workspace/[id]/members/role-dropdown.css +72 -0
- package/src/routes/workspace/[id]/members/role-dropdown.tsx +43 -0
- package/src/routes/workspace/[id]/model-section.module.css +173 -0
- package/src/routes/workspace/[id]/model-section.tsx +174 -0
- package/src/routes/workspace/[id]/new-user-section.module.css +143 -0
- package/src/routes/workspace/[id]/new-user-section.tsx +104 -0
- package/src/routes/workspace/[id]/provider-section.module.css +138 -0
- package/src/routes/workspace/[id]/provider-section.tsx +188 -0
- package/src/routes/workspace/[id]/settings/index.tsx +11 -0
- package/src/routes/workspace/[id]/settings/settings-section.module.css +94 -0
- package/src/routes/workspace/[id]/settings/settings-section.tsx +122 -0
- package/src/routes/workspace/[id]/usage-section.module.css +185 -0
- package/src/routes/workspace/[id]/usage-section.tsx +200 -0
- package/src/routes/workspace/[id].css +308 -0
- package/src/routes/workspace/[id].tsx +62 -0
- package/src/routes/workspace/common.tsx +120 -0
- package/src/routes/workspace-picker.css +74 -0
- package/src/routes/workspace-picker.tsx +122 -0
- package/src/routes/workspace.css +107 -0
- package/src/routes/workspace.tsx +38 -0
- package/src/routes/zen/index.css +866 -0
- package/src/routes/zen/index.tsx +343 -0
- package/src/routes/zen/util/dataDumper.ts +44 -0
- package/src/routes/zen/util/error.ts +13 -0
- package/src/routes/zen/util/handler.ts +784 -0
- package/src/routes/zen/util/logger.ts +12 -0
- package/src/routes/zen/util/provider/anthropic.ts +752 -0
- package/src/routes/zen/util/provider/google.ts +75 -0
- package/src/routes/zen/util/provider/openai-compatible.ts +546 -0
- package/src/routes/zen/util/provider/openai.ts +630 -0
- package/src/routes/zen/util/provider/provider.ts +210 -0
- package/src/routes/zen/util/rateLimiter.ts +41 -0
- package/src/routes/zen/util/stickyProviderTracker.ts +16 -0
- package/src/routes/zen/util/trialLimiter.ts +49 -0
- package/src/routes/zen/v1/chat/completions.ts +11 -0
- package/src/routes/zen/v1/messages.ts +11 -0
- package/src/routes/zen/v1/models/[model].ts +13 -0
- package/src/routes/zen/v1/models.ts +60 -0
- package/src/routes/zen/v1/responses.ts +11 -0
- package/src/style/base.css +21 -0
- package/src/style/component/button.css +102 -0
- package/src/style/index.css +8 -0
- package/src/style/reset.css +76 -0
- package/src/style/token/color.css +91 -0
- package/src/style/token/font.css +21 -0
- package/src/style/token/space.css +46 -0
- package/sst-env.d.ts +9 -0
- package/tsconfig.json +21 -0
- package/vite.config.ts +25 -0
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
import { Billing } from "@jonsoc/console-core/billing.js"
|
|
2
|
+
import type { APIEvent } from "@solidjs/start/server"
|
|
3
|
+
import { and, Database, eq, isNull, sql } from "@jonsoc/console-core/drizzle/index.js"
|
|
4
|
+
import { BillingTable, PaymentTable, SubscriptionTable } from "@jonsoc/console-core/schema/billing.sql.js"
|
|
5
|
+
import { Identifier } from "@jonsoc/console-core/identifier.js"
|
|
6
|
+
import { centsToMicroCents } from "@jonsoc/console-core/util/price.js"
|
|
7
|
+
import { Actor } from "@jonsoc/console-core/actor.js"
|
|
8
|
+
import { Resource } from "@jonsoc/console-resource"
|
|
9
|
+
import { UserTable } from "@jonsoc/console-core/schema/user.sql.js"
|
|
10
|
+
import { AuthTable } from "@jonsoc/console-core/schema/auth.sql.js"
|
|
11
|
+
|
|
12
|
+
export async function POST(input: APIEvent) {
|
|
13
|
+
const body = await Billing.stripe().webhooks.constructEventAsync(
|
|
14
|
+
await input.request.text(),
|
|
15
|
+
input.request.headers.get("stripe-signature")!,
|
|
16
|
+
Resource.STRIPE_WEBHOOK_SECRET.value,
|
|
17
|
+
)
|
|
18
|
+
console.log(body.type, JSON.stringify(body, null, 2))
|
|
19
|
+
|
|
20
|
+
return (async () => {
|
|
21
|
+
if (body.type === "customer.updated") {
|
|
22
|
+
// check default payment method changed
|
|
23
|
+
const prevInvoiceSettings = body.data.previous_attributes?.invoice_settings ?? {}
|
|
24
|
+
if (!("default_payment_method" in prevInvoiceSettings)) return "ignored"
|
|
25
|
+
|
|
26
|
+
const customerID = body.data.object.id
|
|
27
|
+
const paymentMethodID = body.data.object.invoice_settings.default_payment_method as string
|
|
28
|
+
|
|
29
|
+
if (!customerID) throw new Error("Customer ID not found")
|
|
30
|
+
if (!paymentMethodID) throw new Error("Payment method ID not found")
|
|
31
|
+
|
|
32
|
+
const paymentMethod = await Billing.stripe().paymentMethods.retrieve(paymentMethodID)
|
|
33
|
+
await Database.use(async (tx) => {
|
|
34
|
+
await tx
|
|
35
|
+
.update(BillingTable)
|
|
36
|
+
.set({
|
|
37
|
+
paymentMethodID,
|
|
38
|
+
paymentMethodLast4: paymentMethod.card?.last4 ?? null,
|
|
39
|
+
paymentMethodType: paymentMethod.type,
|
|
40
|
+
})
|
|
41
|
+
.where(eq(BillingTable.customerID, customerID))
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
if (body.type === "checkout.session.completed" && body.data.object.mode === "payment") {
|
|
45
|
+
const workspaceID = body.data.object.metadata?.workspaceID
|
|
46
|
+
const amountInCents = body.data.object.metadata?.amount && parseInt(body.data.object.metadata?.amount)
|
|
47
|
+
const customerID = body.data.object.customer as string
|
|
48
|
+
const paymentID = body.data.object.payment_intent as string
|
|
49
|
+
const invoiceID = body.data.object.invoice as string
|
|
50
|
+
|
|
51
|
+
if (!workspaceID) throw new Error("Workspace ID not found")
|
|
52
|
+
if (!customerID) throw new Error("Customer ID not found")
|
|
53
|
+
if (!amountInCents) throw new Error("Amount not found")
|
|
54
|
+
if (!paymentID) throw new Error("Payment ID not found")
|
|
55
|
+
if (!invoiceID) throw new Error("Invoice ID not found")
|
|
56
|
+
|
|
57
|
+
await Actor.provide("system", { workspaceID }, async () => {
|
|
58
|
+
const customer = await Billing.get()
|
|
59
|
+
if (customer?.customerID && customer.customerID !== customerID) throw new Error("Customer ID mismatch")
|
|
60
|
+
|
|
61
|
+
// set customer metadata
|
|
62
|
+
if (!customer?.customerID) {
|
|
63
|
+
await Billing.stripe().customers.update(customerID, {
|
|
64
|
+
metadata: {
|
|
65
|
+
workspaceID,
|
|
66
|
+
},
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// get payment method for the payment intent
|
|
71
|
+
const paymentIntent = await Billing.stripe().paymentIntents.retrieve(paymentID, {
|
|
72
|
+
expand: ["payment_method"],
|
|
73
|
+
})
|
|
74
|
+
const paymentMethod = paymentIntent.payment_method
|
|
75
|
+
if (!paymentMethod || typeof paymentMethod === "string") throw new Error("Payment method not expanded")
|
|
76
|
+
|
|
77
|
+
await Database.transaction(async (tx) => {
|
|
78
|
+
await tx
|
|
79
|
+
.update(BillingTable)
|
|
80
|
+
.set({
|
|
81
|
+
balance: sql`${BillingTable.balance} + ${centsToMicroCents(amountInCents)}`,
|
|
82
|
+
customerID,
|
|
83
|
+
paymentMethodID: paymentMethod.id,
|
|
84
|
+
paymentMethodLast4: paymentMethod.card?.last4 ?? null,
|
|
85
|
+
paymentMethodType: paymentMethod.type,
|
|
86
|
+
// enable reload if first time enabling billing
|
|
87
|
+
...(customer?.customerID
|
|
88
|
+
? {}
|
|
89
|
+
: {
|
|
90
|
+
reloadError: null,
|
|
91
|
+
timeReloadError: null,
|
|
92
|
+
}),
|
|
93
|
+
})
|
|
94
|
+
.where(eq(BillingTable.workspaceID, workspaceID))
|
|
95
|
+
await tx.insert(PaymentTable).values({
|
|
96
|
+
workspaceID,
|
|
97
|
+
id: Identifier.create("payment"),
|
|
98
|
+
amount: centsToMicroCents(amountInCents),
|
|
99
|
+
paymentID,
|
|
100
|
+
invoiceID,
|
|
101
|
+
customerID,
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
if (body.type === "checkout.session.completed" && body.data.object.mode === "subscription") {
|
|
107
|
+
const workspaceID = body.data.object.custom_fields.find((f) => f.key === "workspaceid")?.text?.value
|
|
108
|
+
const amountInCents = body.data.object.amount_total as number
|
|
109
|
+
const customerID = body.data.object.customer as string
|
|
110
|
+
const customerEmail = body.data.object.customer_details?.email as string
|
|
111
|
+
const invoiceID = body.data.object.invoice as string
|
|
112
|
+
const subscriptionID = body.data.object.subscription as string
|
|
113
|
+
const promoCode = body.data.object.discounts?.[0]?.promotion_code as string
|
|
114
|
+
|
|
115
|
+
if (!workspaceID) throw new Error("Workspace ID not found")
|
|
116
|
+
if (!customerID) throw new Error("Customer ID not found")
|
|
117
|
+
if (!amountInCents) throw new Error("Amount not found")
|
|
118
|
+
if (!invoiceID) throw new Error("Invoice ID not found")
|
|
119
|
+
if (!subscriptionID) throw new Error("Subscription ID not found")
|
|
120
|
+
|
|
121
|
+
// get payment id from invoice
|
|
122
|
+
const invoice = await Billing.stripe().invoices.retrieve(invoiceID, {
|
|
123
|
+
expand: ["payments"],
|
|
124
|
+
})
|
|
125
|
+
const paymentID = invoice.payments?.data[0].payment.payment_intent as string
|
|
126
|
+
if (!paymentID) throw new Error("Payment ID not found")
|
|
127
|
+
|
|
128
|
+
// get payment method for the payment intent
|
|
129
|
+
const paymentIntent = await Billing.stripe().paymentIntents.retrieve(paymentID, {
|
|
130
|
+
expand: ["payment_method"],
|
|
131
|
+
})
|
|
132
|
+
const paymentMethod = paymentIntent.payment_method
|
|
133
|
+
if (!paymentMethod || typeof paymentMethod === "string") throw new Error("Payment method not expanded")
|
|
134
|
+
|
|
135
|
+
// get coupon id from promotion code
|
|
136
|
+
const couponID = await (async () => {
|
|
137
|
+
if (!promoCode) return
|
|
138
|
+
const coupon = await Billing.stripe().promotionCodes.retrieve(promoCode)
|
|
139
|
+
const couponID = coupon.coupon.id
|
|
140
|
+
if (!couponID) throw new Error("Coupon not found for promotion code")
|
|
141
|
+
return couponID
|
|
142
|
+
})()
|
|
143
|
+
|
|
144
|
+
// get user
|
|
145
|
+
|
|
146
|
+
await Actor.provide("system", { workspaceID }, async () => {
|
|
147
|
+
// look up current billing
|
|
148
|
+
const billing = await Billing.get()
|
|
149
|
+
if (!billing) throw new Error(`Workspace with ID ${workspaceID} not found`)
|
|
150
|
+
|
|
151
|
+
// Temporarily skip this check because during Black drop, user can checkout
|
|
152
|
+
// as a new customer
|
|
153
|
+
//if (billing.customerID !== customerID) throw new Error("Customer ID mismatch")
|
|
154
|
+
|
|
155
|
+
// Temporarily check the user to apply to. After Black drop, we will allow
|
|
156
|
+
// look up the user to apply to
|
|
157
|
+
const users = await Database.use((tx) =>
|
|
158
|
+
tx
|
|
159
|
+
.select({ id: UserTable.id, email: AuthTable.subject })
|
|
160
|
+
.from(UserTable)
|
|
161
|
+
.innerJoin(AuthTable, and(eq(AuthTable.accountID, UserTable.accountID), eq(AuthTable.provider, "email")))
|
|
162
|
+
.where(and(eq(UserTable.workspaceID, workspaceID), isNull(UserTable.timeDeleted))),
|
|
163
|
+
)
|
|
164
|
+
const user = users.find((u) => u.email === customerEmail) ?? users[0]
|
|
165
|
+
if (!user) {
|
|
166
|
+
console.error(`Error: User with email ${customerEmail} not found in workspace ${workspaceID}`)
|
|
167
|
+
process.exit(1)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// set customer metadata
|
|
171
|
+
if (!billing?.customerID) {
|
|
172
|
+
await Billing.stripe().customers.update(customerID, {
|
|
173
|
+
metadata: {
|
|
174
|
+
workspaceID,
|
|
175
|
+
},
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
await Database.transaction(async (tx) => {
|
|
180
|
+
await tx
|
|
181
|
+
.update(BillingTable)
|
|
182
|
+
.set({
|
|
183
|
+
customerID,
|
|
184
|
+
subscriptionID,
|
|
185
|
+
subscription: {
|
|
186
|
+
status: "subscribed",
|
|
187
|
+
coupon: couponID,
|
|
188
|
+
seats: 1,
|
|
189
|
+
plan: "200",
|
|
190
|
+
},
|
|
191
|
+
paymentMethodID: paymentMethod.id,
|
|
192
|
+
paymentMethodLast4: paymentMethod.card?.last4 ?? null,
|
|
193
|
+
paymentMethodType: paymentMethod.type,
|
|
194
|
+
})
|
|
195
|
+
.where(eq(BillingTable.workspaceID, workspaceID))
|
|
196
|
+
|
|
197
|
+
await tx.insert(SubscriptionTable).values({
|
|
198
|
+
workspaceID,
|
|
199
|
+
id: Identifier.create("subscription"),
|
|
200
|
+
userID: user.id,
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
await tx.insert(PaymentTable).values({
|
|
204
|
+
workspaceID,
|
|
205
|
+
id: Identifier.create("payment"),
|
|
206
|
+
amount: centsToMicroCents(amountInCents),
|
|
207
|
+
paymentID,
|
|
208
|
+
invoiceID,
|
|
209
|
+
customerID,
|
|
210
|
+
enrichment: {
|
|
211
|
+
type: "subscription",
|
|
212
|
+
couponID,
|
|
213
|
+
},
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
if (body.type === "customer.subscription.created") {
|
|
219
|
+
/*
|
|
220
|
+
{
|
|
221
|
+
id: "evt_1Smq802SrMQ2Fneksse5FMNV",
|
|
222
|
+
object: "event",
|
|
223
|
+
api_version: "2025-07-30.basil",
|
|
224
|
+
created: 1767766916,
|
|
225
|
+
data: {
|
|
226
|
+
object: {
|
|
227
|
+
id: "sub_1Smq7x2SrMQ2Fnek8F1yf3ZD",
|
|
228
|
+
object: "subscription",
|
|
229
|
+
application: null,
|
|
230
|
+
application_fee_percent: null,
|
|
231
|
+
automatic_tax: {
|
|
232
|
+
disabled_reason: null,
|
|
233
|
+
enabled: false,
|
|
234
|
+
liability: null,
|
|
235
|
+
},
|
|
236
|
+
billing_cycle_anchor: 1770445200,
|
|
237
|
+
billing_cycle_anchor_config: null,
|
|
238
|
+
billing_mode: {
|
|
239
|
+
flexible: {
|
|
240
|
+
proration_discounts: "included",
|
|
241
|
+
},
|
|
242
|
+
type: "flexible",
|
|
243
|
+
updated_at: 1770445200,
|
|
244
|
+
},
|
|
245
|
+
billing_thresholds: null,
|
|
246
|
+
cancel_at: null,
|
|
247
|
+
cancel_at_period_end: false,
|
|
248
|
+
canceled_at: null,
|
|
249
|
+
cancellation_details: {
|
|
250
|
+
comment: null,
|
|
251
|
+
feedback: null,
|
|
252
|
+
reason: null,
|
|
253
|
+
},
|
|
254
|
+
collection_method: "charge_automatically",
|
|
255
|
+
created: 1770445200,
|
|
256
|
+
currency: "usd",
|
|
257
|
+
customer: "cus_TkKmZZvysJ2wej",
|
|
258
|
+
customer_account: null,
|
|
259
|
+
days_until_due: null,
|
|
260
|
+
default_payment_method: null,
|
|
261
|
+
default_source: "card_1Smq7u2SrMQ2FneknjyOa7sq",
|
|
262
|
+
default_tax_rates: [],
|
|
263
|
+
description: null,
|
|
264
|
+
discounts: [],
|
|
265
|
+
ended_at: null,
|
|
266
|
+
invoice_settings: {
|
|
267
|
+
account_tax_ids: null,
|
|
268
|
+
issuer: {
|
|
269
|
+
type: "self",
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
items: {
|
|
273
|
+
object: "list",
|
|
274
|
+
data: [
|
|
275
|
+
{
|
|
276
|
+
id: "si_TkKnBKXFX76t0O",
|
|
277
|
+
object: "subscription_item",
|
|
278
|
+
billing_thresholds: null,
|
|
279
|
+
created: 1770445200,
|
|
280
|
+
current_period_end: 1772864400,
|
|
281
|
+
current_period_start: 1770445200,
|
|
282
|
+
discounts: [],
|
|
283
|
+
metadata: {},
|
|
284
|
+
plan: {
|
|
285
|
+
id: "price_1SmfFG2SrMQ2FnekJuzwHMea",
|
|
286
|
+
object: "plan",
|
|
287
|
+
active: true,
|
|
288
|
+
amount: 20000,
|
|
289
|
+
amount_decimal: "20000",
|
|
290
|
+
billing_scheme: "per_unit",
|
|
291
|
+
created: 1767725082,
|
|
292
|
+
currency: "usd",
|
|
293
|
+
interval: "month",
|
|
294
|
+
interval_count: 1,
|
|
295
|
+
livemode: false,
|
|
296
|
+
metadata: {},
|
|
297
|
+
meter: null,
|
|
298
|
+
nickname: null,
|
|
299
|
+
product: "prod_Tk9LjWT1n0DgYm",
|
|
300
|
+
tiers_mode: null,
|
|
301
|
+
transform_usage: null,
|
|
302
|
+
trial_period_days: null,
|
|
303
|
+
usage_type: "licensed",
|
|
304
|
+
},
|
|
305
|
+
price: {
|
|
306
|
+
id: "price_1SmfFG2SrMQ2FnekJuzwHMea",
|
|
307
|
+
object: "price",
|
|
308
|
+
active: true,
|
|
309
|
+
billing_scheme: "per_unit",
|
|
310
|
+
created: 1767725082,
|
|
311
|
+
currency: "usd",
|
|
312
|
+
custom_unit_amount: null,
|
|
313
|
+
livemode: false,
|
|
314
|
+
lookup_key: null,
|
|
315
|
+
metadata: {},
|
|
316
|
+
nickname: null,
|
|
317
|
+
product: "prod_Tk9LjWT1n0DgYm",
|
|
318
|
+
recurring: {
|
|
319
|
+
interval: "month",
|
|
320
|
+
interval_count: 1,
|
|
321
|
+
meter: null,
|
|
322
|
+
trial_period_days: null,
|
|
323
|
+
usage_type: "licensed",
|
|
324
|
+
},
|
|
325
|
+
tax_behavior: "unspecified",
|
|
326
|
+
tiers_mode: null,
|
|
327
|
+
transform_quantity: null,
|
|
328
|
+
type: "recurring",
|
|
329
|
+
unit_amount: 20000,
|
|
330
|
+
unit_amount_decimal: "20000",
|
|
331
|
+
},
|
|
332
|
+
quantity: 1,
|
|
333
|
+
subscription: "sub_1Smq7x2SrMQ2Fnek8F1yf3ZD",
|
|
334
|
+
tax_rates: [],
|
|
335
|
+
},
|
|
336
|
+
],
|
|
337
|
+
has_more: false,
|
|
338
|
+
total_count: 1,
|
|
339
|
+
url: "/v1/subscription_items?subscription=sub_1Smq7x2SrMQ2Fnek8F1yf3ZD",
|
|
340
|
+
},
|
|
341
|
+
latest_invoice: "in_1Smq7x2SrMQ2FnekSJesfPwE",
|
|
342
|
+
livemode: false,
|
|
343
|
+
metadata: {},
|
|
344
|
+
next_pending_invoice_item_invoice: null,
|
|
345
|
+
on_behalf_of: null,
|
|
346
|
+
pause_collection: null,
|
|
347
|
+
payment_settings: {
|
|
348
|
+
payment_method_options: null,
|
|
349
|
+
payment_method_types: null,
|
|
350
|
+
save_default_payment_method: "off",
|
|
351
|
+
},
|
|
352
|
+
pending_invoice_item_interval: null,
|
|
353
|
+
pending_setup_intent: null,
|
|
354
|
+
pending_update: null,
|
|
355
|
+
plan: {
|
|
356
|
+
id: "price_1SmfFG2SrMQ2FnekJuzwHMea",
|
|
357
|
+
object: "plan",
|
|
358
|
+
active: true,
|
|
359
|
+
amount: 20000,
|
|
360
|
+
amount_decimal: "20000",
|
|
361
|
+
billing_scheme: "per_unit",
|
|
362
|
+
created: 1767725082,
|
|
363
|
+
currency: "usd",
|
|
364
|
+
interval: "month",
|
|
365
|
+
interval_count: 1,
|
|
366
|
+
livemode: false,
|
|
367
|
+
metadata: {},
|
|
368
|
+
meter: null,
|
|
369
|
+
nickname: null,
|
|
370
|
+
product: "prod_Tk9LjWT1n0DgYm",
|
|
371
|
+
tiers_mode: null,
|
|
372
|
+
transform_usage: null,
|
|
373
|
+
trial_period_days: null,
|
|
374
|
+
usage_type: "licensed",
|
|
375
|
+
},
|
|
376
|
+
quantity: 1,
|
|
377
|
+
schedule: null,
|
|
378
|
+
start_date: 1770445200,
|
|
379
|
+
status: "active",
|
|
380
|
+
test_clock: "clock_1Smq6n2SrMQ2FnekQw4yt2PZ",
|
|
381
|
+
transfer_data: null,
|
|
382
|
+
trial_end: null,
|
|
383
|
+
trial_settings: {
|
|
384
|
+
end_behavior: {
|
|
385
|
+
missing_payment_method: "create_invoice",
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
trial_start: null,
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
livemode: false,
|
|
392
|
+
pending_webhooks: 0,
|
|
393
|
+
request: {
|
|
394
|
+
id: "req_6YO9stvB155WJD",
|
|
395
|
+
idempotency_key: "581ba059-6f86-49b2-9c49-0d8450255322",
|
|
396
|
+
},
|
|
397
|
+
type: "customer.subscription.created",
|
|
398
|
+
}
|
|
399
|
+
*/
|
|
400
|
+
}
|
|
401
|
+
if (body.type === "customer.subscription.deleted") {
|
|
402
|
+
const subscriptionID = body.data.object.id
|
|
403
|
+
if (!subscriptionID) throw new Error("Subscription ID not found")
|
|
404
|
+
|
|
405
|
+
const workspaceID = await Database.use((tx) =>
|
|
406
|
+
tx
|
|
407
|
+
.select({ workspaceID: BillingTable.workspaceID })
|
|
408
|
+
.from(BillingTable)
|
|
409
|
+
.where(eq(BillingTable.subscriptionID, subscriptionID))
|
|
410
|
+
.then((rows) => rows[0]?.workspaceID),
|
|
411
|
+
)
|
|
412
|
+
if (!workspaceID) throw new Error("Workspace ID not found for subscription")
|
|
413
|
+
|
|
414
|
+
await Database.transaction(async (tx) => {
|
|
415
|
+
await tx
|
|
416
|
+
.update(BillingTable)
|
|
417
|
+
.set({ subscriptionID: null, subscription: null })
|
|
418
|
+
.where(eq(BillingTable.workspaceID, workspaceID))
|
|
419
|
+
|
|
420
|
+
await tx.delete(SubscriptionTable).where(eq(SubscriptionTable.workspaceID, workspaceID))
|
|
421
|
+
})
|
|
422
|
+
}
|
|
423
|
+
if (body.type === "invoice.payment_succeeded") {
|
|
424
|
+
if (
|
|
425
|
+
body.data.object.billing_reason === "subscription_cycle" ||
|
|
426
|
+
body.data.object.billing_reason === "subscription_create"
|
|
427
|
+
) {
|
|
428
|
+
const invoiceID = body.data.object.id as string
|
|
429
|
+
const amountInCents = body.data.object.amount_paid
|
|
430
|
+
const customerID = body.data.object.customer as string
|
|
431
|
+
const subscriptionID = body.data.object.parent?.subscription_details?.subscription as string
|
|
432
|
+
|
|
433
|
+
if (!customerID) throw new Error("Customer ID not found")
|
|
434
|
+
if (!invoiceID) throw new Error("Invoice ID not found")
|
|
435
|
+
if (!subscriptionID) throw new Error("Subscription ID not found")
|
|
436
|
+
|
|
437
|
+
// get coupon id from subscription
|
|
438
|
+
const subscriptionData = await Billing.stripe().subscriptions.retrieve(subscriptionID, {
|
|
439
|
+
expand: ["discounts"],
|
|
440
|
+
})
|
|
441
|
+
const couponID =
|
|
442
|
+
typeof subscriptionData.discounts[0] === "string"
|
|
443
|
+
? subscriptionData.discounts[0]
|
|
444
|
+
: subscriptionData.discounts[0]?.coupon?.id
|
|
445
|
+
|
|
446
|
+
// get payment id from invoice
|
|
447
|
+
const invoice = await Billing.stripe().invoices.retrieve(invoiceID, {
|
|
448
|
+
expand: ["payments"],
|
|
449
|
+
})
|
|
450
|
+
const paymentID = invoice.payments?.data[0].payment.payment_intent as string
|
|
451
|
+
if (!paymentID) {
|
|
452
|
+
// payment id can be undefined when using coupon
|
|
453
|
+
if (!couponID) throw new Error("Payment ID not found")
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const workspaceID = await Database.use((tx) =>
|
|
457
|
+
tx
|
|
458
|
+
.select({ workspaceID: BillingTable.workspaceID })
|
|
459
|
+
.from(BillingTable)
|
|
460
|
+
.where(eq(BillingTable.customerID, customerID))
|
|
461
|
+
.then((rows) => rows[0]?.workspaceID),
|
|
462
|
+
)
|
|
463
|
+
if (!workspaceID) throw new Error("Workspace ID not found for customer")
|
|
464
|
+
|
|
465
|
+
await Database.use((tx) =>
|
|
466
|
+
tx.insert(PaymentTable).values({
|
|
467
|
+
workspaceID,
|
|
468
|
+
id: Identifier.create("payment"),
|
|
469
|
+
amount: centsToMicroCents(amountInCents),
|
|
470
|
+
paymentID,
|
|
471
|
+
invoiceID,
|
|
472
|
+
customerID,
|
|
473
|
+
enrichment: {
|
|
474
|
+
type: "subscription",
|
|
475
|
+
couponID,
|
|
476
|
+
},
|
|
477
|
+
}),
|
|
478
|
+
)
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (body.type === "charge.refunded") {
|
|
482
|
+
const customerID = body.data.object.customer as string
|
|
483
|
+
const paymentIntentID = body.data.object.payment_intent as string
|
|
484
|
+
if (!customerID) throw new Error("Customer ID not found")
|
|
485
|
+
if (!paymentIntentID) throw new Error("Payment ID not found")
|
|
486
|
+
|
|
487
|
+
const workspaceID = await Database.use((tx) =>
|
|
488
|
+
tx
|
|
489
|
+
.select({
|
|
490
|
+
workspaceID: BillingTable.workspaceID,
|
|
491
|
+
})
|
|
492
|
+
.from(BillingTable)
|
|
493
|
+
.where(eq(BillingTable.customerID, customerID))
|
|
494
|
+
.then((rows) => rows[0]?.workspaceID),
|
|
495
|
+
)
|
|
496
|
+
if (!workspaceID) throw new Error("Workspace ID not found")
|
|
497
|
+
|
|
498
|
+
const amount = await Database.use((tx) =>
|
|
499
|
+
tx
|
|
500
|
+
.select({
|
|
501
|
+
amount: PaymentTable.amount,
|
|
502
|
+
})
|
|
503
|
+
.from(PaymentTable)
|
|
504
|
+
.where(and(eq(PaymentTable.paymentID, paymentIntentID), eq(PaymentTable.workspaceID, workspaceID)))
|
|
505
|
+
.then((rows) => rows[0]?.amount),
|
|
506
|
+
)
|
|
507
|
+
if (!amount) throw new Error("Payment not found")
|
|
508
|
+
|
|
509
|
+
await Database.transaction(async (tx) => {
|
|
510
|
+
await tx
|
|
511
|
+
.update(PaymentTable)
|
|
512
|
+
.set({
|
|
513
|
+
timeRefunded: new Date(body.created * 1000),
|
|
514
|
+
})
|
|
515
|
+
.where(and(eq(PaymentTable.paymentID, paymentIntentID), eq(PaymentTable.workspaceID, workspaceID)))
|
|
516
|
+
|
|
517
|
+
await tx
|
|
518
|
+
.update(BillingTable)
|
|
519
|
+
.set({
|
|
520
|
+
balance: sql`${BillingTable.balance} - ${amount}`,
|
|
521
|
+
})
|
|
522
|
+
.where(eq(BillingTable.workspaceID, workspaceID))
|
|
523
|
+
})
|
|
524
|
+
}
|
|
525
|
+
})()
|
|
526
|
+
.then((message) => {
|
|
527
|
+
return Response.json({ message: message ?? "done" }, { status: 200 })
|
|
528
|
+
})
|
|
529
|
+
.catch((error: any) => {
|
|
530
|
+
return Response.json({ message: error.message }, { status: 500 })
|
|
531
|
+
})
|
|
532
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { APIEvent } from "@solidjs/start/server"
|
|
2
|
+
|
|
3
|
+
async function handler(evt: APIEvent) {
|
|
4
|
+
const req = evt.request.clone()
|
|
5
|
+
const url = new URL(req.url)
|
|
6
|
+
const targetUrl = `https://enterprise.jonsoc.com/${url.pathname}${url.search}`
|
|
7
|
+
const response = await fetch(targetUrl, {
|
|
8
|
+
method: req.method,
|
|
9
|
+
headers: req.headers,
|
|
10
|
+
body: req.body,
|
|
11
|
+
})
|
|
12
|
+
return response
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const GET = handler
|
|
16
|
+
export const POST = handler
|
|
17
|
+
export const PUT = handler
|
|
18
|
+
export const DELETE = handler
|
|
19
|
+
export const OPTIONS = handler
|
|
20
|
+
export const PATCH = handler
|