@kernhq/module-billing 0.2.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/LICENSE +662 -0
- package/dist/contract.d.ts +965 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +265 -0
- package/dist/contract.js.map +1 -0
- package/dist/server/index.d.ts +11 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +165 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/router.d.ts +697 -0
- package/dist/server/router.d.ts.map +1 -0
- package/dist/server/router.js +126 -0
- package/dist/server/router.js.map +1 -0
- package/dist/server/schema.d.ts +1030 -0
- package/dist/server/schema.d.ts.map +1 -0
- package/dist/server/schema.js +137 -0
- package/dist/server/schema.js.map +1 -0
- package/dist/server/services/entitlements.d.ts +3 -0
- package/dist/server/services/entitlements.d.ts.map +1 -0
- package/dist/server/services/entitlements.js +68 -0
- package/dist/server/services/entitlements.js.map +1 -0
- package/dist/server/services/plans.d.ts +20 -0
- package/dist/server/services/plans.d.ts.map +1 -0
- package/dist/server/services/plans.js +122 -0
- package/dist/server/services/plans.js.map +1 -0
- package/dist/server/services/stripe.d.ts +42 -0
- package/dist/server/services/stripe.d.ts.map +1 -0
- package/dist/server/services/stripe.js +267 -0
- package/dist/server/services/stripe.js.map +1 -0
- package/dist/server/services/subscriptions.d.ts +35 -0
- package/dist/server/services/subscriptions.d.ts.map +1 -0
- package/dist/server/services/subscriptions.js +193 -0
- package/dist/server/services/subscriptions.js.map +1 -0
- package/dist/server/services/usage.d.ts +40 -0
- package/dist/server/services/usage.d.ts.map +1 -0
- package/dist/server/services/usage.js +98 -0
- package/dist/server/services/usage.js.map +1 -0
- package/migrations/0000_init.sql +82 -0
- package/migrations/0001_rls.sql +18 -0
- package/migrations/meta/0000_snapshot.json +580 -0
- package/migrations/meta/_journal.json +20 -0
- package/package.json +73 -0
- package/src/client/api.ts +15 -0
- package/src/client/format.test.ts +64 -0
- package/src/client/index.ts +81 -0
- package/src/contract.ts +311 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { KernError } from '@kernhq/kernel';
|
|
2
|
+
import { eq } from 'drizzle-orm';
|
|
3
|
+
import Stripe from 'stripe';
|
|
4
|
+
import { invoices, plans, subscriptions, webhookEvents } from '../schema.js';
|
|
5
|
+
import * as subs from './subscriptions.js';
|
|
6
|
+
/**
|
|
7
|
+
* Stripe, or nothing at all.
|
|
8
|
+
*
|
|
9
|
+
* The module ships in every Kern image, including every self-hosted one, so the absence of a key is
|
|
10
|
+
* the normal case rather than a misconfiguration. `client()` returning null is how the rest of the
|
|
11
|
+
* module finds out, and every caller has to handle it — there is no "assume it is configured" path.
|
|
12
|
+
*/
|
|
13
|
+
export function client() {
|
|
14
|
+
const key = process.env.STRIPE_SECRET_KEY;
|
|
15
|
+
if (!key)
|
|
16
|
+
return null;
|
|
17
|
+
return new Stripe(key, {
|
|
18
|
+
// Pinned deliberately: an account-level API version change must not reach a running instance
|
|
19
|
+
// before its image has been built against it.
|
|
20
|
+
apiVersion: '2026-07-29.dahlia',
|
|
21
|
+
appInfo: { name: 'Kern', url: 'https://kernaio.com' },
|
|
22
|
+
maxNetworkRetries: 2,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export const paymentsEnabled = () => Boolean(process.env.STRIPE_SECRET_KEY);
|
|
26
|
+
function required() {
|
|
27
|
+
const s = client();
|
|
28
|
+
if (!s)
|
|
29
|
+
throw KernError.conflict('This instance is not configured to take payments', 'billing.stripe.not_configured');
|
|
30
|
+
return s;
|
|
31
|
+
}
|
|
32
|
+
/** Stripe's subscription statuses, mapped onto the five this module recognises. */
|
|
33
|
+
function mapStatus(s) {
|
|
34
|
+
switch (s) {
|
|
35
|
+
case 'trialing':
|
|
36
|
+
return 'trialing';
|
|
37
|
+
case 'active':
|
|
38
|
+
return 'active';
|
|
39
|
+
case 'past_due':
|
|
40
|
+
case 'unpaid':
|
|
41
|
+
return 'past_due';
|
|
42
|
+
case 'canceled':
|
|
43
|
+
case 'incomplete_expired':
|
|
44
|
+
return 'canceled';
|
|
45
|
+
// `incomplete` means the first payment has not succeeded yet: not entitled, not yet a customer
|
|
46
|
+
case 'incomplete':
|
|
47
|
+
case 'paused':
|
|
48
|
+
return 'suspended';
|
|
49
|
+
default:
|
|
50
|
+
return 'suspended';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** The Stripe customer for a workspace, created on first use and remembered. */
|
|
54
|
+
async function customerFor(kernel, workspaceId, email) {
|
|
55
|
+
const existing = await subs.get(kernel, workspaceId);
|
|
56
|
+
if (existing?.stripeCustomerId)
|
|
57
|
+
return existing.stripeCustomerId;
|
|
58
|
+
const stripe = required();
|
|
59
|
+
const customer = await stripe.customers.create({
|
|
60
|
+
email,
|
|
61
|
+
// the workspace id travels with the customer so a webhook can find its way home even if our
|
|
62
|
+
// own row is missing — which is exactly the case during a first checkout
|
|
63
|
+
metadata: { kern_workspace_id: workspaceId },
|
|
64
|
+
});
|
|
65
|
+
await subs.upsert(kernel, workspaceId, { stripeCustomerId: customer.id });
|
|
66
|
+
return customer.id;
|
|
67
|
+
}
|
|
68
|
+
export async function checkout(kernel, input) {
|
|
69
|
+
const stripe = required();
|
|
70
|
+
const [plan] = await kernel.database.db.select().from(plans).where(eq(plans.slug, input.planSlug)).limit(1);
|
|
71
|
+
if (!plan)
|
|
72
|
+
throw KernError.notFound('Plan');
|
|
73
|
+
if (!plan.stripePriceId)
|
|
74
|
+
throw KernError.conflict('That plan has no Stripe price attached', 'billing.plan.no_price');
|
|
75
|
+
const customer = await customerFor(kernel, input.workspaceId, input.email);
|
|
76
|
+
const session = await stripe.checkout.sessions.create({
|
|
77
|
+
mode: 'subscription',
|
|
78
|
+
customer,
|
|
79
|
+
line_items: [{ price: plan.stripePriceId, quantity: plan.perSeat ? (input.seats ?? 1) : 1 }],
|
|
80
|
+
subscription_data: {
|
|
81
|
+
...(plan.trialDays > 0 ? { trial_period_days: plan.trialDays } : {}),
|
|
82
|
+
metadata: { kern_workspace_id: input.workspaceId, kern_plan_id: plan.id },
|
|
83
|
+
},
|
|
84
|
+
// The card is taken up front, trial or not, so the subscription converts without asking again.
|
|
85
|
+
payment_method_collection: 'always',
|
|
86
|
+
success_url: `${input.baseUrl}/settings/billing?checkout=done`,
|
|
87
|
+
cancel_url: `${input.baseUrl}/settings/billing?checkout=cancelled`,
|
|
88
|
+
});
|
|
89
|
+
if (!session.url)
|
|
90
|
+
throw KernError.conflict('Stripe did not return a checkout URL', 'billing.stripe.no_url');
|
|
91
|
+
return { url: session.url };
|
|
92
|
+
}
|
|
93
|
+
export async function portal(kernel, input) {
|
|
94
|
+
const stripe = required();
|
|
95
|
+
const sub = await subs.get(kernel, input.workspaceId);
|
|
96
|
+
if (!sub?.stripeCustomerId)
|
|
97
|
+
throw KernError.conflict('This workspace has no billing account yet', 'billing.stripe.no_customer');
|
|
98
|
+
const session = await stripe.billingPortal.sessions.create({
|
|
99
|
+
customer: sub.stripeCustomerId,
|
|
100
|
+
return_url: input.returnUrl,
|
|
101
|
+
});
|
|
102
|
+
return { url: session.url };
|
|
103
|
+
}
|
|
104
|
+
/** Keep the seat quantity on Stripe in step with the workspace's actual membership. */
|
|
105
|
+
export async function syncSeats(kernel, workspaceId, seats) {
|
|
106
|
+
const stripe = client();
|
|
107
|
+
if (!stripe)
|
|
108
|
+
return;
|
|
109
|
+
const sub = await subs.get(kernel, workspaceId);
|
|
110
|
+
if (!sub?.stripeSubscriptionId)
|
|
111
|
+
return;
|
|
112
|
+
const remote = await stripe.subscriptions.retrieve(sub.stripeSubscriptionId);
|
|
113
|
+
const item = remote.items.data[0];
|
|
114
|
+
if (!item || item.quantity === seats)
|
|
115
|
+
return;
|
|
116
|
+
await stripe.subscriptions.update(sub.stripeSubscriptionId, {
|
|
117
|
+
items: [{ id: item.id, quantity: seats }],
|
|
118
|
+
// the customer is billed for the seat from the moment it is used, not from the next period
|
|
119
|
+
proration_behavior: 'create_prorations',
|
|
120
|
+
});
|
|
121
|
+
await subs.upsert(kernel, workspaceId, { seatsPurchased: seats });
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Record that an event id has been handled, and say whether it is new.
|
|
125
|
+
*
|
|
126
|
+
* The insert *is* the check. Reading first and then inserting is a race that two concurrent
|
|
127
|
+
* deliveries of the same event will lose, and losing it means charging or crediting twice.
|
|
128
|
+
*/
|
|
129
|
+
async function claim(kernel, id, type) {
|
|
130
|
+
const rows = await kernel.database.db
|
|
131
|
+
.insert(webhookEvents)
|
|
132
|
+
.values({ id, type })
|
|
133
|
+
.onConflictDoNothing()
|
|
134
|
+
.returning({ id: webhookEvents.id });
|
|
135
|
+
return rows.length > 0;
|
|
136
|
+
}
|
|
137
|
+
/** The workspace a Stripe object belongs to, from the metadata we set when creating it. */
|
|
138
|
+
function workspaceOf(o) {
|
|
139
|
+
return o.metadata?.kern_workspace_id ?? null;
|
|
140
|
+
}
|
|
141
|
+
async function applySubscription(kernel, s) {
|
|
142
|
+
const workspaceId = workspaceOf(s);
|
|
143
|
+
if (!workspaceId) {
|
|
144
|
+
kernel.log.warn({ subscription: s.id }, 'billing: Stripe subscription without a workspace id');
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const planId = s.metadata?.kern_plan_id ?? null;
|
|
148
|
+
const item = s.items.data[0];
|
|
149
|
+
const periodEnd = item?.current_period_end ?? null;
|
|
150
|
+
await subs.upsert(kernel, workspaceId, {
|
|
151
|
+
...(planId ? { planId } : {}),
|
|
152
|
+
status: mapStatus(s.status),
|
|
153
|
+
seatsPurchased: item?.quantity ?? 0,
|
|
154
|
+
stripeSubscriptionId: s.id,
|
|
155
|
+
stripeCustomerId: typeof s.customer === 'string' ? s.customer : s.customer.id,
|
|
156
|
+
cancelAtPeriodEnd: s.cancel_at_period_end,
|
|
157
|
+
trialEndsAt: s.trial_end ? new Date(s.trial_end * 1000) : null,
|
|
158
|
+
currentPeriodEnd: periodEnd ? new Date(periodEnd * 1000) : null,
|
|
159
|
+
graceEndsAt: null,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
async function applyInvoice(kernel, inv) {
|
|
163
|
+
const customer = typeof inv.customer === 'string' ? inv.customer : inv.customer?.id;
|
|
164
|
+
if (!customer)
|
|
165
|
+
return;
|
|
166
|
+
const [row] = await kernel.database.db
|
|
167
|
+
.select({ workspaceId: subscriptions.workspaceId })
|
|
168
|
+
.from(subscriptions)
|
|
169
|
+
.where(eq(subscriptions.stripeCustomerId, customer))
|
|
170
|
+
.limit(1);
|
|
171
|
+
if (!row)
|
|
172
|
+
return;
|
|
173
|
+
const workspaceId = row.workspaceId;
|
|
174
|
+
const line = inv.lines?.data?.[0];
|
|
175
|
+
await kernel.database.withWorkspace(workspaceId, async (tx) => {
|
|
176
|
+
await tx
|
|
177
|
+
.insert(invoices)
|
|
178
|
+
.values({
|
|
179
|
+
workspaceId,
|
|
180
|
+
stripeInvoiceId: inv.id ?? null,
|
|
181
|
+
number: inv.number ?? null,
|
|
182
|
+
status: inv.status ?? 'draft',
|
|
183
|
+
totalMinor: inv.total ?? 0,
|
|
184
|
+
currency: inv.currency ?? 'usd',
|
|
185
|
+
periodStart: line?.period?.start ? new Date(line.period.start * 1000) : null,
|
|
186
|
+
periodEnd: line?.period?.end ? new Date(line.period.end * 1000) : null,
|
|
187
|
+
hostedUrl: inv.hosted_invoice_url ?? null,
|
|
188
|
+
pdfUrl: inv.invoice_pdf ?? null,
|
|
189
|
+
})
|
|
190
|
+
.onConflictDoUpdate({
|
|
191
|
+
target: invoices.stripeInvoiceId,
|
|
192
|
+
set: {
|
|
193
|
+
status: inv.status ?? 'draft',
|
|
194
|
+
totalMinor: inv.total ?? 0,
|
|
195
|
+
hostedUrl: inv.hosted_invoice_url ?? null,
|
|
196
|
+
pdfUrl: inv.invoice_pdf ?? null,
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Apply one webhook.
|
|
203
|
+
*
|
|
204
|
+
* Verification happens against the **raw body**, before anything parses it — a signature over
|
|
205
|
+
* re-encoded JSON proves nothing, because re-encoding is not guaranteed to reproduce the bytes that
|
|
206
|
+
* were signed.
|
|
207
|
+
*/
|
|
208
|
+
export async function handleWebhook(kernel, raw, signature) {
|
|
209
|
+
const stripe = required();
|
|
210
|
+
const secret = process.env.STRIPE_WEBHOOK_SECRET;
|
|
211
|
+
if (!secret)
|
|
212
|
+
throw KernError.conflict('No Stripe webhook secret is configured', 'billing.stripe.no_webhook_secret');
|
|
213
|
+
let event;
|
|
214
|
+
try {
|
|
215
|
+
event = stripe.webhooks.constructEvent(raw, signature, secret);
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
throw KernError.badRequest('Stripe signature did not verify', { err: String(err) });
|
|
219
|
+
}
|
|
220
|
+
if (!(await claim(kernel, event.id, event.type))) {
|
|
221
|
+
kernel.log.info({ event: event.id, type: event.type }, 'billing: webhook already applied');
|
|
222
|
+
return { handled: false, type: event.type };
|
|
223
|
+
}
|
|
224
|
+
switch (event.type) {
|
|
225
|
+
case 'customer.subscription.created':
|
|
226
|
+
case 'customer.subscription.updated':
|
|
227
|
+
case 'customer.subscription.deleted':
|
|
228
|
+
await applySubscription(kernel, event.data.object);
|
|
229
|
+
break;
|
|
230
|
+
case 'checkout.session.completed': {
|
|
231
|
+
const session = event.data.object;
|
|
232
|
+
const subId = typeof session.subscription === 'string' ? session.subscription : session.subscription?.id;
|
|
233
|
+
if (subId)
|
|
234
|
+
await applySubscription(kernel, await stripe.subscriptions.retrieve(subId));
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
case 'invoice.paid':
|
|
238
|
+
await applyInvoice(kernel, event.data.object);
|
|
239
|
+
break;
|
|
240
|
+
case 'invoice.payment_failed': {
|
|
241
|
+
const inv = event.data.object;
|
|
242
|
+
await applyInvoice(kernel, inv);
|
|
243
|
+
const customer = typeof inv.customer === 'string' ? inv.customer : inv.customer?.id;
|
|
244
|
+
if (customer) {
|
|
245
|
+
const [row] = await kernel.database.db
|
|
246
|
+
.select({ workspaceId: subscriptions.workspaceId })
|
|
247
|
+
.from(subscriptions)
|
|
248
|
+
.where(eq(subscriptions.stripeCustomerId, customer))
|
|
249
|
+
.limit(1);
|
|
250
|
+
if (row) {
|
|
251
|
+
// A failed payment starts a clock, it does not close the workspace. Stripe keeps retrying
|
|
252
|
+
// for its own dunning window; the grace period is what decides when we stop waiting.
|
|
253
|
+
const grace = new Date();
|
|
254
|
+
grace.setUTCDate(grace.getUTCDate() + GRACE_DAYS);
|
|
255
|
+
await subs.upsert(kernel, row.workspaceId, { status: 'past_due', graceEndsAt: grace });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
default:
|
|
261
|
+
kernel.log.debug({ type: event.type }, 'billing: unhandled Stripe event');
|
|
262
|
+
}
|
|
263
|
+
return { handled: true, type: event.type };
|
|
264
|
+
}
|
|
265
|
+
/** How long a workspace keeps working after a payment fails. */
|
|
266
|
+
export const GRACE_DAYS = 14;
|
|
267
|
+
//# sourceMappingURL=stripe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stripe.js","sourceRoot":"","sources":["../../../src/server/services/stripe.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type Kernel } from '@kernhq/kernel';
|
|
2
|
+
import { type AdminWorkspaceRow, type Invoice, PlanLimitsPatch, type Subscription, type SubscriptionStatus } from '../../contract.js';
|
|
3
|
+
import { subscriptions } from '../schema.js';
|
|
4
|
+
declare const ser: (r: typeof subscriptions.$inferSelect, plan: {
|
|
5
|
+
name: string;
|
|
6
|
+
slug: string;
|
|
7
|
+
} | null) => Subscription;
|
|
8
|
+
export declare function get(kernel: Kernel, workspaceId: string): Promise<Subscription | null>;
|
|
9
|
+
/**
|
|
10
|
+
* Write a subscription and tell the rest of the instance.
|
|
11
|
+
*
|
|
12
|
+
* The event matters as much as the row: the kernel drops its entitlement cache on
|
|
13
|
+
* `billing.subscription.*`, so a customer who has just paid gets the seat now rather than when a TTL
|
|
14
|
+
* happens to expire.
|
|
15
|
+
*/
|
|
16
|
+
export declare function upsert(kernel: Kernel, workspaceId: string, patch: Partial<typeof subscriptions.$inferInsert>): Promise<Subscription>;
|
|
17
|
+
export declare function setPlan(kernel: Kernel, workspaceId: string, planId: string | null, seatsPurchased?: number): Promise<Subscription>;
|
|
18
|
+
export declare function extendTrial(kernel: Kernel, workspaceId: string, days: number): Promise<Subscription>;
|
|
19
|
+
export declare function setStatus(kernel: Kernel, workspaceId: string, status: 'active' | 'suspended'): Promise<Subscription>;
|
|
20
|
+
export declare function setOverride(kernel: Kernel, workspaceId: string, limits: PlanLimitsPatch | null, actorId: string | null): Promise<Subscription>;
|
|
21
|
+
/**
|
|
22
|
+
* Every workspace on the instance, with what it pays.
|
|
23
|
+
*
|
|
24
|
+
* Workspace names live in core, so this reads billing's own rows first and then asks core to name
|
|
25
|
+
* them — a module never reaches into another module's tables, and this is the one screen where that
|
|
26
|
+
* rule costs an extra round trip.
|
|
27
|
+
*/
|
|
28
|
+
export declare function adminList(kernel: Kernel, input: {
|
|
29
|
+
q?: string;
|
|
30
|
+
status?: SubscriptionStatus;
|
|
31
|
+
limit: number;
|
|
32
|
+
}): Promise<AdminWorkspaceRow[]>;
|
|
33
|
+
export declare function listInvoices(kernel: Kernel, workspaceId: string, limit: number): Promise<Invoice[]>;
|
|
34
|
+
export { ser as serialiseSubscription };
|
|
35
|
+
//# sourceMappingURL=subscriptions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subscriptions.d.ts","sourceRoot":"","sources":["../../../src/server/services/subscriptions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAEvD,OAAO,EACL,KAAK,iBAAiB,EAEtB,KAAK,OAAO,EACZ,eAAe,EACf,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACxB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAA8B,aAAa,EAAE,MAAM,cAAc,CAAA;AAUxE,QAAA,MAAM,GAAG,GACP,GAAG,OAAO,aAAa,CAAC,YAAY,EACpC,MAAM;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,KAC1C,YAYD,CAAA;AAEF,wBAAsB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAS3F;AAED;;;;;;GAMG;AACH,wBAAsB,MAAM,CAC1B,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,OAAO,CAAC,OAAO,aAAa,CAAC,YAAY,CAAC,GAChD,OAAO,CAAC,YAAY,CAAC,CAiBvB;AAED,wBAAsB,OAAO,CAC3B,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC,YAAY,CAAC,CAgBvB;AAED,wBAAsB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAQ1G;AAED,wBAAsB,SAAS,CAC7B,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,QAAQ,GAAG,WAAW,GAC7B,OAAO,CAAC,YAAY,CAAC,CAYvB;AAED,wBAAsB,WAAW,CAC/B,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,eAAe,GAAG,IAAI,EAC9B,OAAO,EAAE,MAAM,GAAG,IAAI,GACrB,OAAO,CAAC,YAAY,CAAC,CAyBvB;AAQD;;;;;;GAMG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE;IAAE,CAAC,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAChE,OAAO,CAAC,iBAAiB,EAAE,CAAC,CA6C9B;AAED,wBAAsB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAuBzG;AAED,OAAO,EAAE,GAAG,IAAI,qBAAqB,EAAE,CAAA"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { KernError } from '@kernhq/kernel';
|
|
2
|
+
import { desc, eq, inArray } from 'drizzle-orm';
|
|
3
|
+
import { billingEvents, PlanLimitsPatch, } from '../../contract.js';
|
|
4
|
+
import { invoices, overrides, plans, subscriptions } from '../schema.js';
|
|
5
|
+
import * as usage from './usage.js';
|
|
6
|
+
const ser = (r, plan) => ({
|
|
7
|
+
workspaceId: r.workspaceId,
|
|
8
|
+
planId: r.planId,
|
|
9
|
+
planName: plan?.name ?? null,
|
|
10
|
+
planSlug: plan?.slug ?? null,
|
|
11
|
+
status: r.status,
|
|
12
|
+
seatsPurchased: r.seatsPurchased,
|
|
13
|
+
trialEndsAt: r.trialEndsAt?.toISOString() ?? null,
|
|
14
|
+
currentPeriodEnd: r.currentPeriodEnd?.toISOString() ?? null,
|
|
15
|
+
cancelAtPeriodEnd: r.cancelAtPeriodEnd,
|
|
16
|
+
stripeCustomerId: r.stripeCustomerId,
|
|
17
|
+
stripeSubscriptionId: r.stripeSubscriptionId,
|
|
18
|
+
});
|
|
19
|
+
export async function get(kernel, workspaceId) {
|
|
20
|
+
const [row] = await kernel.database.db
|
|
21
|
+
.select({ sub: subscriptions, planName: plans.name, planSlug: plans.slug })
|
|
22
|
+
.from(subscriptions)
|
|
23
|
+
.leftJoin(plans, eq(plans.id, subscriptions.planId))
|
|
24
|
+
.where(eq(subscriptions.workspaceId, workspaceId))
|
|
25
|
+
.limit(1);
|
|
26
|
+
if (!row)
|
|
27
|
+
return null;
|
|
28
|
+
return ser(row.sub, row.planName && row.planSlug ? { name: row.planName, slug: row.planSlug } : null);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Write a subscription and tell the rest of the instance.
|
|
32
|
+
*
|
|
33
|
+
* The event matters as much as the row: the kernel drops its entitlement cache on
|
|
34
|
+
* `billing.subscription.*`, so a customer who has just paid gets the seat now rather than when a TTL
|
|
35
|
+
* happens to expire.
|
|
36
|
+
*/
|
|
37
|
+
export async function upsert(kernel, workspaceId, patch) {
|
|
38
|
+
const db = kernel.database.db;
|
|
39
|
+
await db
|
|
40
|
+
.insert(subscriptions)
|
|
41
|
+
.values({ workspaceId, status: 'trialing', ...patch })
|
|
42
|
+
.onConflictDoUpdate({
|
|
43
|
+
target: subscriptions.workspaceId,
|
|
44
|
+
set: { ...patch, updatedAt: new Date() },
|
|
45
|
+
});
|
|
46
|
+
const next = await get(kernel, workspaceId);
|
|
47
|
+
if (!next)
|
|
48
|
+
throw KernError.notFound('Subscription');
|
|
49
|
+
await kernel.emit(billingEvents.subscriptionChanged, { workspaceId: workspaceId, status: next.status, planSlug: next.planSlug }, { workspaceId });
|
|
50
|
+
return next;
|
|
51
|
+
}
|
|
52
|
+
export async function setPlan(kernel, workspaceId, planId, seatsPurchased) {
|
|
53
|
+
if (planId) {
|
|
54
|
+
const [plan] = await kernel.database.db
|
|
55
|
+
.select({ id: plans.id, trialDays: plans.trialDays })
|
|
56
|
+
.from(plans)
|
|
57
|
+
.where(eq(plans.id, planId))
|
|
58
|
+
.limit(1);
|
|
59
|
+
if (!plan)
|
|
60
|
+
throw KernError.notFound('Plan');
|
|
61
|
+
}
|
|
62
|
+
return upsert(kernel, workspaceId, {
|
|
63
|
+
planId,
|
|
64
|
+
...(seatsPurchased !== undefined ? { seatsPurchased } : {}),
|
|
65
|
+
// An admin assigning a plan by hand is a decision, not a trial: it takes effect now.
|
|
66
|
+
status: 'active',
|
|
67
|
+
graceEndsAt: null,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
export async function extendTrial(kernel, workspaceId, days) {
|
|
71
|
+
const current = await get(kernel, workspaceId);
|
|
72
|
+
const from = current?.trialEndsAt && new Date(current.trialEndsAt) > new Date()
|
|
73
|
+
? new Date(current.trialEndsAt)
|
|
74
|
+
: new Date();
|
|
75
|
+
from.setUTCDate(from.getUTCDate() + days);
|
|
76
|
+
return upsert(kernel, workspaceId, { status: 'trialing', trialEndsAt: from, graceEndsAt: null });
|
|
77
|
+
}
|
|
78
|
+
export async function setStatus(kernel, workspaceId, status) {
|
|
79
|
+
const next = await upsert(kernel, workspaceId, { status, graceEndsAt: null });
|
|
80
|
+
if (status === 'suspended')
|
|
81
|
+
await kernel.emit(billingEvents.subscriptionSuspended, {
|
|
82
|
+
workspaceId: workspaceId,
|
|
83
|
+
reason: 'an instance admin suspended this workspace',
|
|
84
|
+
}, { workspaceId });
|
|
85
|
+
return next;
|
|
86
|
+
}
|
|
87
|
+
export async function setOverride(kernel, workspaceId, limits, actorId) {
|
|
88
|
+
const db = kernel.database.db;
|
|
89
|
+
if (limits === null)
|
|
90
|
+
await db.delete(overrides).where(eq(overrides.workspaceId, workspaceId));
|
|
91
|
+
else
|
|
92
|
+
await db
|
|
93
|
+
.insert(overrides)
|
|
94
|
+
.values({ workspaceId, limits: PlanLimitsPatch.parse(limits), createdBy: actorId })
|
|
95
|
+
.onConflictDoUpdate({
|
|
96
|
+
target: overrides.workspaceId,
|
|
97
|
+
set: { limits: PlanLimitsPatch.parse(limits), updatedAt: new Date() },
|
|
98
|
+
});
|
|
99
|
+
const next = await get(kernel, workspaceId);
|
|
100
|
+
// an override changes what the workspace may do, so the cache has to be told even though the
|
|
101
|
+
// subscription row itself did not move
|
|
102
|
+
await kernel.emit(billingEvents.subscriptionChanged, {
|
|
103
|
+
workspaceId: workspaceId,
|
|
104
|
+
status: next?.status ?? 'trialing',
|
|
105
|
+
planSlug: next?.planSlug ?? null,
|
|
106
|
+
}, { workspaceId });
|
|
107
|
+
if (!next)
|
|
108
|
+
throw KernError.notFound('Subscription');
|
|
109
|
+
return next;
|
|
110
|
+
}
|
|
111
|
+
/** What a plan bills per month, so annual and monthly plans can be added up in one column. */
|
|
112
|
+
function monthlyMinor(priceMinor, interval, perSeat, seats) {
|
|
113
|
+
const perMonth = interval === 'year' ? Math.round(priceMinor / 12) : priceMinor;
|
|
114
|
+
return perSeat ? perMonth * seats : perMonth;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Every workspace on the instance, with what it pays.
|
|
118
|
+
*
|
|
119
|
+
* Workspace names live in core, so this reads billing's own rows first and then asks core to name
|
|
120
|
+
* them — a module never reaches into another module's tables, and this is the one screen where that
|
|
121
|
+
* rule costs an extra round trip.
|
|
122
|
+
*/
|
|
123
|
+
export async function adminList(kernel, input) {
|
|
124
|
+
const refs = await kernel.call('core.workspaces.list', { q: input.q, limit: input.limit });
|
|
125
|
+
if (!refs.length)
|
|
126
|
+
return [];
|
|
127
|
+
const ids = refs.map((w) => w.id);
|
|
128
|
+
const subs = await kernel.database.db
|
|
129
|
+
.select({ sub: subscriptions, plan: plans })
|
|
130
|
+
.from(subscriptions)
|
|
131
|
+
.leftJoin(plans, eq(plans.id, subscriptions.planId))
|
|
132
|
+
.where(inArray(subscriptions.workspaceId, ids));
|
|
133
|
+
const byWs = new Map(subs.map((s) => [s.sub.workspaceId, s]));
|
|
134
|
+
const ovr = await kernel.database.db
|
|
135
|
+
.select({ workspaceId: overrides.workspaceId })
|
|
136
|
+
.from(overrides)
|
|
137
|
+
.where(inArray(overrides.workspaceId, ids));
|
|
138
|
+
const overridden = new Set(ovr.map((o) => o.workspaceId));
|
|
139
|
+
const rows = [];
|
|
140
|
+
for (const w of refs) {
|
|
141
|
+
const hit = byWs.get(w.id);
|
|
142
|
+
if (input.status && hit?.sub.status !== input.status)
|
|
143
|
+
continue;
|
|
144
|
+
const used = await usage.read(kernel, w.id);
|
|
145
|
+
const plan = hit?.plan ?? null;
|
|
146
|
+
rows.push({
|
|
147
|
+
workspaceId: w.id,
|
|
148
|
+
workspaceName: w.name,
|
|
149
|
+
workspaceSlug: w.slug,
|
|
150
|
+
planName: plan?.name ?? null,
|
|
151
|
+
planSlug: plan?.slug ?? null,
|
|
152
|
+
status: hit?.sub.status ?? null,
|
|
153
|
+
seatsUsed: used.seats,
|
|
154
|
+
seatsPurchased: hit?.sub.seatsPurchased ?? 0,
|
|
155
|
+
storageBytes: used.storageBytes,
|
|
156
|
+
trialEndsAt: hit?.sub.trialEndsAt?.toISOString() ?? null,
|
|
157
|
+
currentPeriodEnd: hit?.sub.currentPeriodEnd?.toISOString() ?? null,
|
|
158
|
+
monthlyMinor: plan
|
|
159
|
+
? monthlyMinor(plan.priceMinor, plan.interval, plan.perSeat, hit?.sub.seatsPurchased ?? used.seats)
|
|
160
|
+
: 0,
|
|
161
|
+
currency: plan?.currency ?? 'usd',
|
|
162
|
+
overridden: overridden.has(w.id),
|
|
163
|
+
stripeCustomerId: hit?.sub.stripeCustomerId ?? null,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return rows;
|
|
167
|
+
}
|
|
168
|
+
export async function listInvoices(kernel, workspaceId, limit) {
|
|
169
|
+
// a tenant table, so it is read inside the workspace's own RLS context
|
|
170
|
+
return kernel.database.withWorkspace(workspaceId, async (tx) => {
|
|
171
|
+
const rows = await tx
|
|
172
|
+
.select()
|
|
173
|
+
.from(invoices)
|
|
174
|
+
.where(eq(invoices.workspaceId, workspaceId))
|
|
175
|
+
.orderBy(desc(invoices.createdAt))
|
|
176
|
+
.limit(limit);
|
|
177
|
+
return rows.map((r) => ({
|
|
178
|
+
id: r.id,
|
|
179
|
+
workspaceId: r.workspaceId,
|
|
180
|
+
number: r.number,
|
|
181
|
+
status: r.status,
|
|
182
|
+
totalMinor: r.totalMinor,
|
|
183
|
+
currency: r.currency,
|
|
184
|
+
periodStart: r.periodStart?.toISOString() ?? null,
|
|
185
|
+
periodEnd: r.periodEnd?.toISOString() ?? null,
|
|
186
|
+
hostedUrl: r.hostedUrl,
|
|
187
|
+
pdfUrl: r.pdfUrl,
|
|
188
|
+
createdAt: r.createdAt.toISOString(),
|
|
189
|
+
}));
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
export { ser as serialiseSubscription };
|
|
193
|
+
//# sourceMappingURL=subscriptions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subscriptions.js","sourceRoot":"","sources":["../../../src/server/services/subscriptions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Kernel } from '@kernhq/kernel';
|
|
2
|
+
/** What core reports when asked to recount a workspace from its own tables. */
|
|
3
|
+
export interface CountedUsage {
|
|
4
|
+
seats: number;
|
|
5
|
+
storageBytes: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Move a counter by a delta.
|
|
9
|
+
*
|
|
10
|
+
* Arithmetic in SQL rather than read-modify-write in JavaScript: two members joining at once is the
|
|
11
|
+
* normal case, not a rare one, and the read-modify-write version loses one of them.
|
|
12
|
+
* `greatest(0, …)` because a counter that has drifted negative is a bug that must not also start
|
|
13
|
+
* refusing uploads — the nightly reconcile is what corrects it.
|
|
14
|
+
*/
|
|
15
|
+
export declare function bump(kernel: Kernel, workspaceId: string, delta: Partial<CountedUsage>): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Recount seats for one workspace and write the answer down.
|
|
18
|
+
*
|
|
19
|
+
* Seats are recounted rather than moved by a delta, because the events do not carry enough to do the
|
|
20
|
+
* arithmetic safely: `core.member.removed` does not say what role the person had, and
|
|
21
|
+
* `core.member.updated` does not say what role they had *before* — so a guest being promoted, or a
|
|
22
|
+
* member leaving, would each be counted wrongly. A count over one workspace's memberships is cheap;
|
|
23
|
+
* being wrong about what a customer is charged is not.
|
|
24
|
+
*/
|
|
25
|
+
export declare function recountSeats(kernel: Kernel, workspaceId: string): Promise<number>;
|
|
26
|
+
export declare function read(kernel: Kernel, workspaceId: string): Promise<CountedUsage & {
|
|
27
|
+
updatedAt: Date;
|
|
28
|
+
}>;
|
|
29
|
+
/**
|
|
30
|
+
* Recount one workspace from core's own tables and write the answer down.
|
|
31
|
+
*
|
|
32
|
+
* Returns the drift it found. The caller logs it rather than swallowing it: a counter that keeps
|
|
33
|
+
* needing correction means an event is being missed somewhere, and silently fixing the number every
|
|
34
|
+
* night is how that goes unnoticed for a year.
|
|
35
|
+
*/
|
|
36
|
+
export declare function reconcile(kernel: Kernel, workspaceId: string): Promise<{
|
|
37
|
+
drift: CountedUsage;
|
|
38
|
+
counted: CountedUsage;
|
|
39
|
+
}>;
|
|
40
|
+
//# sourceMappingURL=usage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../../../src/server/services/usage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAI5C,+EAA+E;AAC/E,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,YAAY,EAAE,MAAM,CAAA;CACrB;AAUD;;;;;;;GAOG;AACH,wBAAsB,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAc3G;AAED;;;;;;;;GAQG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAQvF;AAED,wBAAsB,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG;IAAE,SAAS,EAAE,IAAI,CAAA;CAAE,CAAC,CAW3G;AAED;;;;;;GAMG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC;IAAE,KAAK,EAAE,YAAY,CAAC;IAAC,OAAO,EAAE,YAAY,CAAA;CAAE,CAAC,CA2BzD"}
|