lokicms-plugin-stripe 1.0.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 +21 -0
- package/README.md +209 -0
- package/package.json +44 -0
- package/src/index.ts +1092 -0
- package/src/stripe-service.ts +552 -0
- package/src/types.ts +214 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,1092 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stripe Plugin for LokiCMS
|
|
3
|
+
* Provides payment processing and subscription management via Stripe
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Hono } from 'hono';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { StripeService } from './stripe-service';
|
|
9
|
+
import type {
|
|
10
|
+
PluginDefinition,
|
|
11
|
+
PluginAPI,
|
|
12
|
+
StripeConfig,
|
|
13
|
+
ContentTypeRegistration,
|
|
14
|
+
} from './types';
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Content Types
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
// Content type slugs get prefixed with plugin name by LokiCMS
|
|
21
|
+
const CT_PRODUCT = 'stripe-stripe-product';
|
|
22
|
+
const CT_PRICE = 'stripe-stripe-price';
|
|
23
|
+
const CT_SUBSCRIPTION = 'stripe-stripe-subscription';
|
|
24
|
+
const CT_INVOICE = 'stripe-stripe-invoice';
|
|
25
|
+
const CT_CUSTOMER = 'stripe-stripe-customer';
|
|
26
|
+
|
|
27
|
+
// Convert Stripe IDs to valid slugs (prod_xxx -> prod-xxx)
|
|
28
|
+
function toSlug(stripeId: string): string {
|
|
29
|
+
return stripeId.toLowerCase().replace(/_/g, '-');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const CONTENT_TYPES: ContentTypeRegistration[] = [
|
|
33
|
+
{
|
|
34
|
+
name: 'Stripe Product',
|
|
35
|
+
slug: CT_PRODUCT,
|
|
36
|
+
description: 'Products synced from Stripe',
|
|
37
|
+
fields: [
|
|
38
|
+
{ name: 'stripeId', label: 'Stripe ID', type: 'text', required: true },
|
|
39
|
+
{ name: 'name', label: 'Name', type: 'text', required: true },
|
|
40
|
+
{ name: 'description', label: 'Description', type: 'textarea' },
|
|
41
|
+
{ name: 'active', label: 'Active', type: 'boolean' },
|
|
42
|
+
{ name: 'metadata', label: 'Metadata', type: 'json' },
|
|
43
|
+
],
|
|
44
|
+
titleField: 'name',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'Stripe Price',
|
|
48
|
+
slug: CT_PRICE,
|
|
49
|
+
description: 'Prices synced from Stripe',
|
|
50
|
+
fields: [
|
|
51
|
+
{ name: 'stripeId', label: 'Stripe ID', type: 'text', required: true },
|
|
52
|
+
{ name: 'productId', label: 'Product ID', type: 'text', required: true },
|
|
53
|
+
{ name: 'active', label: 'Active', type: 'boolean' },
|
|
54
|
+
{ name: 'currency', label: 'Currency', type: 'text', required: true },
|
|
55
|
+
{ name: 'unitAmount', label: 'Unit Amount (cents)', type: 'number', required: true },
|
|
56
|
+
{ name: 'type', label: 'Type', type: 'select', validation: { options: ['one_time', 'recurring'] } },
|
|
57
|
+
{ name: 'interval', label: 'Interval', type: 'select', validation: { options: ['day', 'week', 'month', 'year'] } },
|
|
58
|
+
{ name: 'intervalCount', label: 'Interval Count', type: 'number' },
|
|
59
|
+
{ name: 'trialPeriodDays', label: 'Trial Period (days)', type: 'number' },
|
|
60
|
+
],
|
|
61
|
+
titleField: 'stripeId',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'Stripe Subscription',
|
|
65
|
+
slug: CT_SUBSCRIPTION,
|
|
66
|
+
description: 'Customer subscriptions from Stripe',
|
|
67
|
+
fields: [
|
|
68
|
+
{ name: 'stripeId', label: 'Stripe ID', type: 'text', required: true },
|
|
69
|
+
{ name: 'customerId', label: 'Customer ID', type: 'text', required: true },
|
|
70
|
+
{ name: 'customerEmail', label: 'Customer Email', type: 'email' },
|
|
71
|
+
{ name: 'priceId', label: 'Price ID', type: 'text', required: true },
|
|
72
|
+
{ name: 'status', label: 'Status', type: 'select', validation: {
|
|
73
|
+
options: ['active', 'canceled', 'incomplete', 'incomplete_expired', 'past_due', 'trialing', 'unpaid', 'paused']
|
|
74
|
+
}},
|
|
75
|
+
{ name: 'currentPeriodStart', label: 'Current Period Start', type: 'datetime' },
|
|
76
|
+
{ name: 'currentPeriodEnd', label: 'Current Period End', type: 'datetime' },
|
|
77
|
+
{ name: 'cancelAtPeriodEnd', label: 'Cancel at Period End', type: 'boolean' },
|
|
78
|
+
{ name: 'canceledAt', label: 'Canceled At', type: 'datetime' },
|
|
79
|
+
{ name: 'trialStart', label: 'Trial Start', type: 'datetime' },
|
|
80
|
+
{ name: 'trialEnd', label: 'Trial End', type: 'datetime' },
|
|
81
|
+
{ name: 'userId', label: 'LokiCMS User ID', type: 'text' },
|
|
82
|
+
],
|
|
83
|
+
titleField: 'stripeId',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'Stripe Invoice',
|
|
87
|
+
slug: CT_INVOICE,
|
|
88
|
+
description: 'Invoices from Stripe',
|
|
89
|
+
fields: [
|
|
90
|
+
{ name: 'stripeId', label: 'Stripe ID', type: 'text', required: true },
|
|
91
|
+
{ name: 'customerId', label: 'Customer ID', type: 'text', required: true },
|
|
92
|
+
{ name: 'subscriptionId', label: 'Subscription ID', type: 'text' },
|
|
93
|
+
{ name: 'status', label: 'Status', type: 'select', validation: {
|
|
94
|
+
options: ['draft', 'open', 'paid', 'void', 'uncollectible']
|
|
95
|
+
}},
|
|
96
|
+
{ name: 'amountDue', label: 'Amount Due (cents)', type: 'number' },
|
|
97
|
+
{ name: 'amountPaid', label: 'Amount Paid (cents)', type: 'number' },
|
|
98
|
+
{ name: 'currency', label: 'Currency', type: 'text' },
|
|
99
|
+
{ name: 'hostedInvoiceUrl', label: 'Invoice URL', type: 'url' },
|
|
100
|
+
{ name: 'invoicePdf', label: 'PDF URL', type: 'url' },
|
|
101
|
+
{ name: 'paidAt', label: 'Paid At', type: 'datetime' },
|
|
102
|
+
],
|
|
103
|
+
titleField: 'stripeId',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'Stripe Customer',
|
|
107
|
+
slug: CT_CUSTOMER,
|
|
108
|
+
description: 'Customers synced from Stripe',
|
|
109
|
+
fields: [
|
|
110
|
+
{ name: 'stripeId', label: 'Stripe ID', type: 'text', required: true },
|
|
111
|
+
{ name: 'email', label: 'Email', type: 'email', required: true },
|
|
112
|
+
{ name: 'name', label: 'Name', type: 'text' },
|
|
113
|
+
{ name: 'userId', label: 'LokiCMS User ID', type: 'text' },
|
|
114
|
+
{ name: 'metadata', label: 'Metadata', type: 'json' },
|
|
115
|
+
],
|
|
116
|
+
titleField: 'email',
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
// ============================================================================
|
|
121
|
+
// MCP Tool Schemas
|
|
122
|
+
// ============================================================================
|
|
123
|
+
|
|
124
|
+
const CreateCheckoutSchema = z.object({
|
|
125
|
+
priceId: z.string().describe('The Stripe Price ID'),
|
|
126
|
+
customerEmail: z.string().email().optional().describe('Customer email'),
|
|
127
|
+
customerId: z.string().optional().describe('Existing Stripe Customer ID'),
|
|
128
|
+
userId: z.string().optional().describe('LokiCMS User ID to associate'),
|
|
129
|
+
mode: z.enum(['payment', 'subscription']).default('subscription'),
|
|
130
|
+
quantity: z.number().positive().optional().default(1),
|
|
131
|
+
trialDays: z.number().positive().optional().describe('Trial period in days'),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const CreatePortalSchema = z.object({
|
|
135
|
+
customerId: z.string().describe('Stripe Customer ID'),
|
|
136
|
+
returnUrl: z.string().url().optional().describe('Return URL after portal'),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const GetSubscriptionSchema = z.object({
|
|
140
|
+
subscriptionId: z.string().describe('Stripe Subscription ID'),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const CancelSubscriptionSchema = z.object({
|
|
144
|
+
subscriptionId: z.string().describe('Stripe Subscription ID'),
|
|
145
|
+
atPeriodEnd: z.boolean().default(true).describe('Cancel at end of billing period'),
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const ListProductsSchema = z.object({
|
|
149
|
+
active: z.boolean().optional().describe('Filter by active status'),
|
|
150
|
+
limit: z.number().positive().max(100).optional().default(20),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const ListPricesSchema = z.object({
|
|
154
|
+
productId: z.string().optional().describe('Filter by product'),
|
|
155
|
+
active: z.boolean().optional().describe('Filter by active status'),
|
|
156
|
+
type: z.enum(['one_time', 'recurring']).optional().describe('Filter by price type'),
|
|
157
|
+
limit: z.number().positive().max(100).optional().default(20),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const ListSubscriptionsSchema = z.object({
|
|
161
|
+
customerId: z.string().optional().describe('Filter by customer'),
|
|
162
|
+
status: z.string().optional().describe('Filter by status'),
|
|
163
|
+
limit: z.number().positive().max(100).optional().default(20),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const ListInvoicesSchema = z.object({
|
|
167
|
+
customerId: z.string().optional().describe('Filter by customer'),
|
|
168
|
+
subscriptionId: z.string().optional().describe('Filter by subscription'),
|
|
169
|
+
status: z.string().optional().describe('Filter by status'),
|
|
170
|
+
limit: z.number().positive().max(100).optional().default(20),
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const SyncProductsSchema = z.object({
|
|
174
|
+
active: z.boolean().optional().default(true).describe('Only sync active products'),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// ============================================================================
|
|
178
|
+
// Plugin Definition
|
|
179
|
+
// ============================================================================
|
|
180
|
+
|
|
181
|
+
let stripeService: StripeService | null = null;
|
|
182
|
+
let pluginApi: PluginAPI | null = null;
|
|
183
|
+
|
|
184
|
+
const stripePlugin: PluginDefinition = {
|
|
185
|
+
name: 'stripe',
|
|
186
|
+
displayName: 'Stripe Payments',
|
|
187
|
+
version: '1.0.0',
|
|
188
|
+
description: 'Accept payments and manage subscriptions with Stripe',
|
|
189
|
+
|
|
190
|
+
lifecycle: {
|
|
191
|
+
onLoad: () => {
|
|
192
|
+
console.log('[Stripe] Plugin loaded');
|
|
193
|
+
},
|
|
194
|
+
onEnable: () => {
|
|
195
|
+
console.log('[Stripe] Plugin enabled');
|
|
196
|
+
},
|
|
197
|
+
onDisable: () => {
|
|
198
|
+
console.log('[Stripe] Plugin disabled');
|
|
199
|
+
stripeService = null;
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
setup: async (api: PluginAPI) => {
|
|
204
|
+
pluginApi = api;
|
|
205
|
+
const config = api.config.getAll() as unknown as StripeConfig;
|
|
206
|
+
|
|
207
|
+
// Validate configuration
|
|
208
|
+
if (!config.secretKey) {
|
|
209
|
+
api.logger.error('Stripe secret key not configured');
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Initialize Stripe service
|
|
214
|
+
stripeService = new StripeService({
|
|
215
|
+
secretKey: config.secretKey,
|
|
216
|
+
webhookSecret: config.webhookSecret || '',
|
|
217
|
+
successUrl: config.successUrl || 'https://localhost:3000/checkout/success',
|
|
218
|
+
cancelUrl: config.cancelUrl || 'https://localhost:3000/checkout/cancel',
|
|
219
|
+
portalReturnUrl: config.portalReturnUrl || 'https://localhost:3000/account',
|
|
220
|
+
currency: config.currency || 'usd',
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
api.logger.info('Stripe service initialized');
|
|
224
|
+
|
|
225
|
+
// Register content types
|
|
226
|
+
for (const contentType of CONTENT_TYPES) {
|
|
227
|
+
try {
|
|
228
|
+
await api.contentTypes.register(contentType);
|
|
229
|
+
api.logger.debug(`Registered content type: ${contentType.slug}`);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
api.logger.debug(`Content type ${contentType.slug} may already exist`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Register routes
|
|
236
|
+
const routes = createRoutes(api);
|
|
237
|
+
api.routes.register(routes);
|
|
238
|
+
api.logger.info(`Routes registered at ${api.routes.getBasePath()}`);
|
|
239
|
+
|
|
240
|
+
// Register MCP tools
|
|
241
|
+
registerMCPTools(api);
|
|
242
|
+
api.logger.info('MCP tools registered');
|
|
243
|
+
|
|
244
|
+
// Register hooks for user-customer sync
|
|
245
|
+
api.hooks.on('user:afterCreate', async (payload) => {
|
|
246
|
+
const user = payload as { id: string; email: string; name?: string };
|
|
247
|
+
if (stripeService && config.secretKey) {
|
|
248
|
+
try {
|
|
249
|
+
const customer = await stripeService.createCustomer({
|
|
250
|
+
email: user.email,
|
|
251
|
+
name: user.name,
|
|
252
|
+
metadata: { lokicms_user_id: user.id },
|
|
253
|
+
});
|
|
254
|
+
api.logger.info(`Created Stripe customer ${customer.id} for user ${user.id}`);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
api.logger.error(`Failed to create Stripe customer for user ${user.id}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
api.logger.info('Stripe plugin setup complete');
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// ============================================================================
|
|
266
|
+
// Routes
|
|
267
|
+
// ============================================================================
|
|
268
|
+
|
|
269
|
+
function createRoutes(api: PluginAPI): Hono {
|
|
270
|
+
const app = new Hono();
|
|
271
|
+
|
|
272
|
+
// Health check
|
|
273
|
+
app.get('/health', (c) => {
|
|
274
|
+
return c.json({
|
|
275
|
+
status: 'ok',
|
|
276
|
+
service: 'stripe',
|
|
277
|
+
configured: stripeService !== null,
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Create checkout session
|
|
282
|
+
app.post('/checkout', async (c) => {
|
|
283
|
+
if (!stripeService) {
|
|
284
|
+
return c.json({ error: 'Stripe not configured' }, 500);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
const body = await c.req.json();
|
|
289
|
+
const parsed = CreateCheckoutSchema.parse(body);
|
|
290
|
+
|
|
291
|
+
const session = await stripeService.createCheckoutSession({
|
|
292
|
+
priceId: parsed.priceId,
|
|
293
|
+
customerEmail: parsed.customerEmail,
|
|
294
|
+
customerId: parsed.customerId,
|
|
295
|
+
userId: parsed.userId,
|
|
296
|
+
mode: parsed.mode,
|
|
297
|
+
quantity: parsed.quantity,
|
|
298
|
+
trialPeriodDays: parsed.trialDays,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
return c.json(session);
|
|
302
|
+
} catch (error) {
|
|
303
|
+
api.logger.error('Checkout error:', error);
|
|
304
|
+
return c.json({ error: 'Failed to create checkout session' }, 400);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Create customer portal session
|
|
309
|
+
app.post('/portal', async (c) => {
|
|
310
|
+
if (!stripeService) {
|
|
311
|
+
return c.json({ error: 'Stripe not configured' }, 500);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
const body = await c.req.json();
|
|
316
|
+
const parsed = CreatePortalSchema.parse(body);
|
|
317
|
+
|
|
318
|
+
const session = await stripeService.createPortalSession({
|
|
319
|
+
customerId: parsed.customerId,
|
|
320
|
+
returnUrl: parsed.returnUrl,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
return c.json(session);
|
|
324
|
+
} catch (error) {
|
|
325
|
+
api.logger.error('Portal error:', error);
|
|
326
|
+
return c.json({ error: 'Failed to create portal session' }, 400);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// List products
|
|
331
|
+
app.get('/products', async (c) => {
|
|
332
|
+
if (!stripeService) {
|
|
333
|
+
return c.json({ error: 'Stripe not configured' }, 500);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
const active = c.req.query('active');
|
|
338
|
+
const limit = parseInt(c.req.query('limit') || '20');
|
|
339
|
+
|
|
340
|
+
const products = await stripeService.listProducts({
|
|
341
|
+
active: active !== undefined ? active === 'true' : undefined,
|
|
342
|
+
limit,
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
return c.json({ products });
|
|
346
|
+
} catch (error) {
|
|
347
|
+
api.logger.error('List products error:', error);
|
|
348
|
+
return c.json({ error: 'Failed to list products' }, 500);
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// List prices
|
|
353
|
+
app.get('/prices', async (c) => {
|
|
354
|
+
if (!stripeService) {
|
|
355
|
+
return c.json({ error: 'Stripe not configured' }, 500);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
const productId = c.req.query('productId');
|
|
360
|
+
const active = c.req.query('active');
|
|
361
|
+
const type = c.req.query('type') as 'one_time' | 'recurring' | undefined;
|
|
362
|
+
const limit = parseInt(c.req.query('limit') || '20');
|
|
363
|
+
|
|
364
|
+
const prices = await stripeService.listPrices({
|
|
365
|
+
productId,
|
|
366
|
+
active: active !== undefined ? active === 'true' : undefined,
|
|
367
|
+
type,
|
|
368
|
+
limit,
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
return c.json({ prices });
|
|
372
|
+
} catch (error) {
|
|
373
|
+
api.logger.error('List prices error:', error);
|
|
374
|
+
return c.json({ error: 'Failed to list prices' }, 500);
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// Get subscription
|
|
379
|
+
app.get('/subscriptions/:id', async (c) => {
|
|
380
|
+
if (!stripeService) {
|
|
381
|
+
return c.json({ error: 'Stripe not configured' }, 500);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
const subscription = await stripeService.getSubscription(c.req.param('id'));
|
|
386
|
+
if (!subscription) {
|
|
387
|
+
return c.json({ error: 'Subscription not found' }, 404);
|
|
388
|
+
}
|
|
389
|
+
return c.json(subscription);
|
|
390
|
+
} catch (error) {
|
|
391
|
+
api.logger.error('Get subscription error:', error);
|
|
392
|
+
return c.json({ error: 'Failed to get subscription' }, 500);
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// List subscriptions
|
|
397
|
+
app.get('/subscriptions', async (c) => {
|
|
398
|
+
if (!stripeService) {
|
|
399
|
+
return c.json({ error: 'Stripe not configured' }, 500);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
try {
|
|
403
|
+
const customerId = c.req.query('customerId');
|
|
404
|
+
const status = c.req.query('status');
|
|
405
|
+
const limit = parseInt(c.req.query('limit') || '20');
|
|
406
|
+
|
|
407
|
+
const subscriptions = await stripeService.listSubscriptions({
|
|
408
|
+
customerId,
|
|
409
|
+
status,
|
|
410
|
+
limit,
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
return c.json({ subscriptions });
|
|
414
|
+
} catch (error) {
|
|
415
|
+
api.logger.error('List subscriptions error:', error);
|
|
416
|
+
return c.json({ error: 'Failed to list subscriptions' }, 500);
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// Cancel subscription
|
|
421
|
+
app.post('/subscriptions/:id/cancel', async (c) => {
|
|
422
|
+
if (!stripeService) {
|
|
423
|
+
return c.json({ error: 'Stripe not configured' }, 500);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
const body = await c.req.json().catch(() => ({}));
|
|
428
|
+
const atPeriodEnd = body.atPeriodEnd !== false;
|
|
429
|
+
|
|
430
|
+
const subscription = await stripeService.cancelSubscription(
|
|
431
|
+
c.req.param('id'),
|
|
432
|
+
{ atPeriodEnd }
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
return c.json(subscription);
|
|
436
|
+
} catch (error) {
|
|
437
|
+
api.logger.error('Cancel subscription error:', error);
|
|
438
|
+
return c.json({ error: 'Failed to cancel subscription' }, 500);
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// Resume subscription
|
|
443
|
+
app.post('/subscriptions/:id/resume', async (c) => {
|
|
444
|
+
if (!stripeService) {
|
|
445
|
+
return c.json({ error: 'Stripe not configured' }, 500);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
try {
|
|
449
|
+
const subscription = await stripeService.resumeSubscription(c.req.param('id'));
|
|
450
|
+
return c.json(subscription);
|
|
451
|
+
} catch (error) {
|
|
452
|
+
api.logger.error('Resume subscription error:', error);
|
|
453
|
+
return c.json({ error: 'Failed to resume subscription' }, 500);
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// List invoices
|
|
458
|
+
app.get('/invoices', async (c) => {
|
|
459
|
+
if (!stripeService) {
|
|
460
|
+
return c.json({ error: 'Stripe not configured' }, 500);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
try {
|
|
464
|
+
const customerId = c.req.query('customerId');
|
|
465
|
+
const subscriptionId = c.req.query('subscriptionId');
|
|
466
|
+
const status = c.req.query('status');
|
|
467
|
+
const limit = parseInt(c.req.query('limit') || '20');
|
|
468
|
+
|
|
469
|
+
const invoices = await stripeService.listInvoices({
|
|
470
|
+
customerId,
|
|
471
|
+
subscriptionId,
|
|
472
|
+
status,
|
|
473
|
+
limit,
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
return c.json({ invoices });
|
|
477
|
+
} catch (error) {
|
|
478
|
+
api.logger.error('List invoices error:', error);
|
|
479
|
+
return c.json({ error: 'Failed to list invoices' }, 500);
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// Webhook handler
|
|
484
|
+
app.post('/webhook', async (c) => {
|
|
485
|
+
if (!stripeService) {
|
|
486
|
+
return c.json({ error: 'Stripe not configured' }, 500);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
try {
|
|
490
|
+
const signature = c.req.header('stripe-signature');
|
|
491
|
+
if (!signature) {
|
|
492
|
+
return c.json({ error: 'Missing stripe-signature header' }, 400);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const rawBody = await c.req.text();
|
|
496
|
+
const event = stripeService.verifyWebhookSignature(rawBody, signature);
|
|
497
|
+
|
|
498
|
+
// Process webhook event
|
|
499
|
+
await handleWebhookEvent(event, api);
|
|
500
|
+
|
|
501
|
+
return c.json({ received: true });
|
|
502
|
+
} catch (error) {
|
|
503
|
+
api.logger.error('Webhook error:', error);
|
|
504
|
+
return c.json({ error: 'Webhook verification failed' }, 400);
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
// Sync products from Stripe to LokiCMS
|
|
509
|
+
app.post('/sync/products', async (c) => {
|
|
510
|
+
if (!stripeService) {
|
|
511
|
+
return c.json({ error: 'Stripe not configured' }, 500);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
try {
|
|
515
|
+
const products = await stripeService.listProducts({ active: true });
|
|
516
|
+
let synced = 0;
|
|
517
|
+
|
|
518
|
+
for (const product of products) {
|
|
519
|
+
try {
|
|
520
|
+
// Check if exists
|
|
521
|
+
const slug = toSlug(product.id);
|
|
522
|
+
const existing = await api.services.entries.findBySlug(
|
|
523
|
+
CT_PRODUCT,
|
|
524
|
+
slug
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
const entryData = {
|
|
528
|
+
contentTypeSlug: CT_PRODUCT,
|
|
529
|
+
title: product.name,
|
|
530
|
+
slug,
|
|
531
|
+
content: {
|
|
532
|
+
stripeId: product.id,
|
|
533
|
+
name: product.name,
|
|
534
|
+
description: product.description,
|
|
535
|
+
active: product.active,
|
|
536
|
+
metadata: product.metadata,
|
|
537
|
+
},
|
|
538
|
+
status: 'published',
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
if (existing) {
|
|
542
|
+
await api.services.entries.update((existing as { id: string }).id, entryData);
|
|
543
|
+
} else {
|
|
544
|
+
await api.services.entries.create(entryData, 'system', 'Stripe Sync');
|
|
545
|
+
}
|
|
546
|
+
synced++;
|
|
547
|
+
} catch (err) {
|
|
548
|
+
api.logger.error(`Failed to sync product ${product.id}:`, err);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return c.json({ synced, total: products.length });
|
|
553
|
+
} catch (error) {
|
|
554
|
+
api.logger.error('Sync products error:', error);
|
|
555
|
+
return c.json({ error: 'Failed to sync products' }, 500);
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
// Sync prices from Stripe to LokiCMS
|
|
560
|
+
app.post('/sync/prices', async (c) => {
|
|
561
|
+
if (!stripeService) {
|
|
562
|
+
return c.json({ error: 'Stripe not configured' }, 500);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
try {
|
|
566
|
+
const prices = await stripeService.listPrices({ active: true });
|
|
567
|
+
let synced = 0;
|
|
568
|
+
|
|
569
|
+
for (const price of prices) {
|
|
570
|
+
try {
|
|
571
|
+
const slug = toSlug(price.id);
|
|
572
|
+
const existing = await api.services.entries.findBySlug(
|
|
573
|
+
CT_PRICE,
|
|
574
|
+
slug
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
const entryData = {
|
|
578
|
+
contentTypeSlug: CT_PRICE,
|
|
579
|
+
title: `${price.id} - ${price.unitAmount / 100} ${price.currency.toUpperCase()}`,
|
|
580
|
+
slug,
|
|
581
|
+
content: {
|
|
582
|
+
stripeId: price.id,
|
|
583
|
+
productId: price.productId,
|
|
584
|
+
active: price.active,
|
|
585
|
+
currency: price.currency,
|
|
586
|
+
unitAmount: price.unitAmount,
|
|
587
|
+
type: price.type,
|
|
588
|
+
interval: price.interval,
|
|
589
|
+
intervalCount: price.intervalCount,
|
|
590
|
+
trialPeriodDays: price.trialPeriodDays,
|
|
591
|
+
},
|
|
592
|
+
status: 'published',
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
if (existing) {
|
|
596
|
+
await api.services.entries.update((existing as { id: string }).id, entryData);
|
|
597
|
+
} else {
|
|
598
|
+
await api.services.entries.create(entryData, 'system', 'Stripe Sync');
|
|
599
|
+
}
|
|
600
|
+
synced++;
|
|
601
|
+
} catch (err) {
|
|
602
|
+
api.logger.error(`Failed to sync price ${price.id}:`, err);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return c.json({ synced, total: prices.length });
|
|
607
|
+
} catch (error) {
|
|
608
|
+
api.logger.error('Sync prices error:', error);
|
|
609
|
+
return c.json({ error: 'Failed to sync prices' }, 500);
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
return app;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// ============================================================================
|
|
617
|
+
// Webhook Event Handler
|
|
618
|
+
// ============================================================================
|
|
619
|
+
|
|
620
|
+
async function handleWebhookEvent(
|
|
621
|
+
event: { id: string; type: string; data: { object: unknown } },
|
|
622
|
+
api: PluginAPI
|
|
623
|
+
): Promise<void> {
|
|
624
|
+
api.logger.info(`Processing webhook: ${event.type}`);
|
|
625
|
+
|
|
626
|
+
switch (event.type) {
|
|
627
|
+
case 'customer.subscription.created':
|
|
628
|
+
case 'customer.subscription.updated':
|
|
629
|
+
await syncSubscription(event.data.object as SubscriptionObject, api);
|
|
630
|
+
break;
|
|
631
|
+
|
|
632
|
+
case 'customer.subscription.deleted':
|
|
633
|
+
await handleSubscriptionDeleted(event.data.object as SubscriptionObject, api);
|
|
634
|
+
break;
|
|
635
|
+
|
|
636
|
+
case 'invoice.paid':
|
|
637
|
+
case 'invoice.payment_failed':
|
|
638
|
+
case 'invoice.created':
|
|
639
|
+
await syncInvoice(event.data.object as InvoiceObject, api);
|
|
640
|
+
break;
|
|
641
|
+
|
|
642
|
+
case 'checkout.session.completed':
|
|
643
|
+
await handleCheckoutCompleted(event.data.object as CheckoutObject, api);
|
|
644
|
+
break;
|
|
645
|
+
|
|
646
|
+
case 'product.created':
|
|
647
|
+
case 'product.updated':
|
|
648
|
+
await syncProduct(event.data.object as ProductObject, api);
|
|
649
|
+
break;
|
|
650
|
+
|
|
651
|
+
case 'price.created':
|
|
652
|
+
case 'price.updated':
|
|
653
|
+
await syncPrice(event.data.object as PriceObject, api);
|
|
654
|
+
break;
|
|
655
|
+
|
|
656
|
+
default:
|
|
657
|
+
api.logger.debug(`Unhandled webhook event: ${event.type}`);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Type helpers for webhook objects
|
|
662
|
+
interface SubscriptionObject {
|
|
663
|
+
id: string;
|
|
664
|
+
customer: string;
|
|
665
|
+
items: { data: Array<{ price: { id: string } }> };
|
|
666
|
+
status: string;
|
|
667
|
+
current_period_start: number;
|
|
668
|
+
current_period_end: number;
|
|
669
|
+
cancel_at_period_end: boolean;
|
|
670
|
+
canceled_at?: number;
|
|
671
|
+
trial_start?: number;
|
|
672
|
+
trial_end?: number;
|
|
673
|
+
metadata?: Record<string, string>;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
interface InvoiceObject {
|
|
677
|
+
id: string;
|
|
678
|
+
customer: string;
|
|
679
|
+
subscription?: string;
|
|
680
|
+
status: string;
|
|
681
|
+
amount_due: number;
|
|
682
|
+
amount_paid: number;
|
|
683
|
+
currency: string;
|
|
684
|
+
hosted_invoice_url?: string;
|
|
685
|
+
invoice_pdf?: string;
|
|
686
|
+
status_transitions?: { paid_at?: number };
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
interface CheckoutObject {
|
|
690
|
+
id: string;
|
|
691
|
+
customer: string;
|
|
692
|
+
subscription?: string;
|
|
693
|
+
client_reference_id?: string;
|
|
694
|
+
metadata?: Record<string, string>;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
interface ProductObject {
|
|
698
|
+
id: string;
|
|
699
|
+
name: string;
|
|
700
|
+
description?: string;
|
|
701
|
+
active: boolean;
|
|
702
|
+
metadata?: Record<string, string>;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
interface PriceObject {
|
|
706
|
+
id: string;
|
|
707
|
+
product: string;
|
|
708
|
+
active: boolean;
|
|
709
|
+
currency: string;
|
|
710
|
+
unit_amount: number;
|
|
711
|
+
type: string;
|
|
712
|
+
recurring?: {
|
|
713
|
+
interval: string;
|
|
714
|
+
interval_count: number;
|
|
715
|
+
trial_period_days?: number;
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
async function syncSubscription(sub: SubscriptionObject, api: PluginAPI): Promise<void> {
|
|
720
|
+
try {
|
|
721
|
+
const slug = toSlug(sub.id);
|
|
722
|
+
const existing = await api.services.entries.findBySlug(CT_SUBSCRIPTION, slug);
|
|
723
|
+
|
|
724
|
+
// Get customer email if possible
|
|
725
|
+
let customerEmail = '';
|
|
726
|
+
if (stripeService) {
|
|
727
|
+
const customer = await stripeService.getCustomer(sub.customer);
|
|
728
|
+
customerEmail = customer?.email || '';
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const entryData = {
|
|
732
|
+
contentTypeSlug: CT_SUBSCRIPTION,
|
|
733
|
+
title: sub.id,
|
|
734
|
+
slug,
|
|
735
|
+
content: {
|
|
736
|
+
stripeId: sub.id,
|
|
737
|
+
customerId: sub.customer,
|
|
738
|
+
customerEmail,
|
|
739
|
+
priceId: sub.items.data[0]?.price.id,
|
|
740
|
+
status: sub.status,
|
|
741
|
+
currentPeriodStart: sub.current_period_start * 1000,
|
|
742
|
+
currentPeriodEnd: sub.current_period_end * 1000,
|
|
743
|
+
cancelAtPeriodEnd: sub.cancel_at_period_end,
|
|
744
|
+
canceledAt: sub.canceled_at ? sub.canceled_at * 1000 : null,
|
|
745
|
+
trialStart: sub.trial_start ? sub.trial_start * 1000 : null,
|
|
746
|
+
trialEnd: sub.trial_end ? sub.trial_end * 1000 : null,
|
|
747
|
+
userId: sub.metadata?.lokicms_user_id,
|
|
748
|
+
},
|
|
749
|
+
status: 'published',
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
if (existing) {
|
|
753
|
+
await api.services.entries.update((existing as { id: string }).id, entryData);
|
|
754
|
+
} else {
|
|
755
|
+
await api.services.entries.create(entryData, 'system', 'Stripe Webhook');
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
api.logger.info(`Synced subscription: ${sub.id}`);
|
|
759
|
+
} catch (error) {
|
|
760
|
+
api.logger.error(`Failed to sync subscription ${sub.id}:`, error);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
async function handleSubscriptionDeleted(sub: SubscriptionObject, api: PluginAPI): Promise<void> {
|
|
765
|
+
try {
|
|
766
|
+
const slug = toSlug(sub.id);
|
|
767
|
+
const existing = await api.services.entries.findBySlug(CT_SUBSCRIPTION, slug);
|
|
768
|
+
if (existing) {
|
|
769
|
+
await api.services.entries.update((existing as { id: string }).id, {
|
|
770
|
+
content: { status: 'canceled' },
|
|
771
|
+
status: 'archived',
|
|
772
|
+
});
|
|
773
|
+
api.logger.info(`Marked subscription as deleted: ${sub.id}`);
|
|
774
|
+
}
|
|
775
|
+
} catch (error) {
|
|
776
|
+
api.logger.error(`Failed to handle subscription deletion ${sub.id}:`, error);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
async function syncInvoice(inv: InvoiceObject, api: PluginAPI): Promise<void> {
|
|
781
|
+
try {
|
|
782
|
+
const slug = toSlug(inv.id);
|
|
783
|
+
const existing = await api.services.entries.findBySlug(CT_INVOICE, slug);
|
|
784
|
+
|
|
785
|
+
const entryData = {
|
|
786
|
+
contentTypeSlug: CT_INVOICE,
|
|
787
|
+
title: inv.id,
|
|
788
|
+
slug,
|
|
789
|
+
content: {
|
|
790
|
+
stripeId: inv.id,
|
|
791
|
+
customerId: inv.customer,
|
|
792
|
+
subscriptionId: inv.subscription,
|
|
793
|
+
status: inv.status,
|
|
794
|
+
amountDue: inv.amount_due,
|
|
795
|
+
amountPaid: inv.amount_paid,
|
|
796
|
+
currency: inv.currency,
|
|
797
|
+
hostedInvoiceUrl: inv.hosted_invoice_url,
|
|
798
|
+
invoicePdf: inv.invoice_pdf,
|
|
799
|
+
paidAt: inv.status_transitions?.paid_at ? inv.status_transitions.paid_at * 1000 : null,
|
|
800
|
+
},
|
|
801
|
+
status: 'published',
|
|
802
|
+
};
|
|
803
|
+
|
|
804
|
+
if (existing) {
|
|
805
|
+
await api.services.entries.update((existing as { id: string }).id, entryData);
|
|
806
|
+
} else {
|
|
807
|
+
await api.services.entries.create(entryData, 'system', 'Stripe Webhook');
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
api.logger.info(`Synced invoice: ${inv.id}`);
|
|
811
|
+
} catch (error) {
|
|
812
|
+
api.logger.error(`Failed to sync invoice ${inv.id}:`, error);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
async function handleCheckoutCompleted(checkout: CheckoutObject, api: PluginAPI): Promise<void> {
|
|
817
|
+
try {
|
|
818
|
+
// If there's a user ID, link the customer
|
|
819
|
+
if (checkout.client_reference_id && checkout.customer) {
|
|
820
|
+
const customerSlug = toSlug(checkout.customer);
|
|
821
|
+
const existing = await api.services.entries.findBySlug(
|
|
822
|
+
CT_CUSTOMER,
|
|
823
|
+
customerSlug
|
|
824
|
+
);
|
|
825
|
+
|
|
826
|
+
if (!existing && stripeService) {
|
|
827
|
+
const customer = await stripeService.getCustomer(checkout.customer);
|
|
828
|
+
if (customer) {
|
|
829
|
+
await api.services.entries.create({
|
|
830
|
+
contentTypeSlug: CT_CUSTOMER,
|
|
831
|
+
title: customer.email,
|
|
832
|
+
slug: toSlug(customer.id),
|
|
833
|
+
content: {
|
|
834
|
+
stripeId: customer.id,
|
|
835
|
+
email: customer.email,
|
|
836
|
+
name: customer.name,
|
|
837
|
+
userId: checkout.client_reference_id,
|
|
838
|
+
metadata: customer.metadata,
|
|
839
|
+
},
|
|
840
|
+
status: 'published',
|
|
841
|
+
}, 'system', 'Stripe Webhook');
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
api.logger.info(`Checkout completed: ${checkout.id}`);
|
|
847
|
+
} catch (error) {
|
|
848
|
+
api.logger.error(`Failed to handle checkout ${checkout.id}:`, error);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
async function syncProduct(product: ProductObject, api: PluginAPI): Promise<void> {
|
|
853
|
+
try {
|
|
854
|
+
const slug = toSlug(product.id);
|
|
855
|
+
const existing = await api.services.entries.findBySlug(CT_PRODUCT, slug);
|
|
856
|
+
|
|
857
|
+
const entryData = {
|
|
858
|
+
contentTypeSlug: CT_PRODUCT,
|
|
859
|
+
title: product.name,
|
|
860
|
+
slug,
|
|
861
|
+
content: {
|
|
862
|
+
stripeId: product.id,
|
|
863
|
+
name: product.name,
|
|
864
|
+
description: product.description,
|
|
865
|
+
active: product.active,
|
|
866
|
+
metadata: product.metadata,
|
|
867
|
+
},
|
|
868
|
+
status: 'published',
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
if (existing) {
|
|
872
|
+
await api.services.entries.update((existing as { id: string }).id, entryData);
|
|
873
|
+
} else {
|
|
874
|
+
await api.services.entries.create(entryData, 'system', 'Stripe Webhook');
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
api.logger.info(`Synced product: ${product.id}`);
|
|
878
|
+
} catch (error) {
|
|
879
|
+
api.logger.error(`Failed to sync product ${product.id}:`, error);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
async function syncPrice(price: PriceObject, api: PluginAPI): Promise<void> {
|
|
884
|
+
try {
|
|
885
|
+
const slug = toSlug(price.id);
|
|
886
|
+
const existing = await api.services.entries.findBySlug(CT_PRICE, slug);
|
|
887
|
+
|
|
888
|
+
const entryData = {
|
|
889
|
+
contentTypeSlug: CT_PRICE,
|
|
890
|
+
title: `${price.id} - ${price.unit_amount / 100} ${price.currency.toUpperCase()}`,
|
|
891
|
+
slug,
|
|
892
|
+
content: {
|
|
893
|
+
stripeId: price.id,
|
|
894
|
+
productId: price.product,
|
|
895
|
+
active: price.active,
|
|
896
|
+
currency: price.currency,
|
|
897
|
+
unitAmount: price.unit_amount,
|
|
898
|
+
type: price.type,
|
|
899
|
+
interval: price.recurring?.interval,
|
|
900
|
+
intervalCount: price.recurring?.interval_count,
|
|
901
|
+
trialPeriodDays: price.recurring?.trial_period_days,
|
|
902
|
+
},
|
|
903
|
+
status: 'published',
|
|
904
|
+
};
|
|
905
|
+
|
|
906
|
+
if (existing) {
|
|
907
|
+
await api.services.entries.update((existing as { id: string }).id, entryData);
|
|
908
|
+
} else {
|
|
909
|
+
await api.services.entries.create(entryData, 'system', 'Stripe Webhook');
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
api.logger.info(`Synced price: ${price.id}`);
|
|
913
|
+
} catch (error) {
|
|
914
|
+
api.logger.error(`Failed to sync price ${price.id}:`, error);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// ============================================================================
|
|
919
|
+
// MCP Tools
|
|
920
|
+
// ============================================================================
|
|
921
|
+
|
|
922
|
+
function registerMCPTools(api: PluginAPI): void {
|
|
923
|
+
// Create checkout session
|
|
924
|
+
api.mcp.registerTool('create_checkout', {
|
|
925
|
+
description: 'Create a Stripe checkout session for payment or subscription',
|
|
926
|
+
inputSchema: CreateCheckoutSchema,
|
|
927
|
+
handler: async (args) => {
|
|
928
|
+
if (!stripeService) throw new Error('Stripe not configured');
|
|
929
|
+
const params = args as z.infer<typeof CreateCheckoutSchema>;
|
|
930
|
+
return await stripeService.createCheckoutSession({
|
|
931
|
+
priceId: params.priceId,
|
|
932
|
+
customerEmail: params.customerEmail,
|
|
933
|
+
customerId: params.customerId,
|
|
934
|
+
userId: params.userId,
|
|
935
|
+
mode: params.mode,
|
|
936
|
+
quantity: params.quantity,
|
|
937
|
+
trialPeriodDays: params.trialDays,
|
|
938
|
+
});
|
|
939
|
+
},
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
// Create customer portal
|
|
943
|
+
api.mcp.registerTool('create_portal', {
|
|
944
|
+
description: 'Create a Stripe customer portal session for subscription management',
|
|
945
|
+
inputSchema: CreatePortalSchema,
|
|
946
|
+
handler: async (args) => {
|
|
947
|
+
if (!stripeService) throw new Error('Stripe not configured');
|
|
948
|
+
const params = args as z.infer<typeof CreatePortalSchema>;
|
|
949
|
+
return await stripeService.createPortalSession({
|
|
950
|
+
customerId: params.customerId,
|
|
951
|
+
returnUrl: params.returnUrl,
|
|
952
|
+
});
|
|
953
|
+
},
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
// Get subscription
|
|
957
|
+
api.mcp.registerTool('get_subscription', {
|
|
958
|
+
description: 'Get details of a Stripe subscription',
|
|
959
|
+
inputSchema: GetSubscriptionSchema,
|
|
960
|
+
handler: async (args) => {
|
|
961
|
+
if (!stripeService) throw new Error('Stripe not configured');
|
|
962
|
+
const params = args as z.infer<typeof GetSubscriptionSchema>;
|
|
963
|
+
return await stripeService.getSubscription(params.subscriptionId);
|
|
964
|
+
},
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
// Cancel subscription
|
|
968
|
+
api.mcp.registerTool('cancel_subscription', {
|
|
969
|
+
description: 'Cancel a Stripe subscription',
|
|
970
|
+
inputSchema: CancelSubscriptionSchema,
|
|
971
|
+
handler: async (args) => {
|
|
972
|
+
if (!stripeService) throw new Error('Stripe not configured');
|
|
973
|
+
const params = args as z.infer<typeof CancelSubscriptionSchema>;
|
|
974
|
+
return await stripeService.cancelSubscription(params.subscriptionId, {
|
|
975
|
+
atPeriodEnd: params.atPeriodEnd,
|
|
976
|
+
});
|
|
977
|
+
},
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
// List products
|
|
981
|
+
api.mcp.registerTool('list_products', {
|
|
982
|
+
description: 'List Stripe products',
|
|
983
|
+
inputSchema: ListProductsSchema,
|
|
984
|
+
handler: async (args) => {
|
|
985
|
+
if (!stripeService) throw new Error('Stripe not configured');
|
|
986
|
+
const params = args as z.infer<typeof ListProductsSchema>;
|
|
987
|
+
return await stripeService.listProducts({
|
|
988
|
+
active: params.active,
|
|
989
|
+
limit: params.limit,
|
|
990
|
+
});
|
|
991
|
+
},
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
// List prices
|
|
995
|
+
api.mcp.registerTool('list_prices', {
|
|
996
|
+
description: 'List Stripe prices',
|
|
997
|
+
inputSchema: ListPricesSchema,
|
|
998
|
+
handler: async (args) => {
|
|
999
|
+
if (!stripeService) throw new Error('Stripe not configured');
|
|
1000
|
+
const params = args as z.infer<typeof ListPricesSchema>;
|
|
1001
|
+
return await stripeService.listPrices({
|
|
1002
|
+
productId: params.productId,
|
|
1003
|
+
active: params.active,
|
|
1004
|
+
type: params.type,
|
|
1005
|
+
limit: params.limit,
|
|
1006
|
+
});
|
|
1007
|
+
},
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
// List subscriptions
|
|
1011
|
+
api.mcp.registerTool('list_subscriptions', {
|
|
1012
|
+
description: 'List Stripe subscriptions',
|
|
1013
|
+
inputSchema: ListSubscriptionsSchema,
|
|
1014
|
+
handler: async (args) => {
|
|
1015
|
+
if (!stripeService) throw new Error('Stripe not configured');
|
|
1016
|
+
const params = args as z.infer<typeof ListSubscriptionsSchema>;
|
|
1017
|
+
return await stripeService.listSubscriptions({
|
|
1018
|
+
customerId: params.customerId,
|
|
1019
|
+
status: params.status,
|
|
1020
|
+
limit: params.limit,
|
|
1021
|
+
});
|
|
1022
|
+
},
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
// List invoices
|
|
1026
|
+
api.mcp.registerTool('list_invoices', {
|
|
1027
|
+
description: 'List Stripe invoices',
|
|
1028
|
+
inputSchema: ListInvoicesSchema,
|
|
1029
|
+
handler: async (args) => {
|
|
1030
|
+
if (!stripeService) throw new Error('Stripe not configured');
|
|
1031
|
+
const params = args as z.infer<typeof ListInvoicesSchema>;
|
|
1032
|
+
return await stripeService.listInvoices({
|
|
1033
|
+
customerId: params.customerId,
|
|
1034
|
+
subscriptionId: params.subscriptionId,
|
|
1035
|
+
status: params.status,
|
|
1036
|
+
limit: params.limit,
|
|
1037
|
+
});
|
|
1038
|
+
},
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
// Sync products
|
|
1042
|
+
api.mcp.registerTool('sync_products', {
|
|
1043
|
+
description: 'Sync products from Stripe to LokiCMS content entries',
|
|
1044
|
+
inputSchema: SyncProductsSchema,
|
|
1045
|
+
handler: async (args) => {
|
|
1046
|
+
if (!stripeService || !pluginApi) throw new Error('Stripe not configured');
|
|
1047
|
+
const params = args as z.infer<typeof SyncProductsSchema>;
|
|
1048
|
+
const products = await stripeService.listProducts({ active: params.active });
|
|
1049
|
+
let synced = 0;
|
|
1050
|
+
|
|
1051
|
+
for (const product of products) {
|
|
1052
|
+
try {
|
|
1053
|
+
const slug = toSlug(product.id);
|
|
1054
|
+
const existing = await pluginApi.services.entries.findBySlug(
|
|
1055
|
+
CT_PRODUCT,
|
|
1056
|
+
slug
|
|
1057
|
+
);
|
|
1058
|
+
|
|
1059
|
+
const entryData = {
|
|
1060
|
+
contentTypeSlug: CT_PRODUCT,
|
|
1061
|
+
title: product.name,
|
|
1062
|
+
slug,
|
|
1063
|
+
content: {
|
|
1064
|
+
stripeId: product.id,
|
|
1065
|
+
name: product.name,
|
|
1066
|
+
description: product.description,
|
|
1067
|
+
active: product.active,
|
|
1068
|
+
metadata: product.metadata,
|
|
1069
|
+
},
|
|
1070
|
+
status: 'published',
|
|
1071
|
+
};
|
|
1072
|
+
|
|
1073
|
+
if (existing) {
|
|
1074
|
+
await pluginApi.services.entries.update(
|
|
1075
|
+
(existing as { id: string }).id,
|
|
1076
|
+
entryData
|
|
1077
|
+
);
|
|
1078
|
+
} else {
|
|
1079
|
+
await pluginApi.services.entries.create(entryData, 'system', 'MCP Sync');
|
|
1080
|
+
}
|
|
1081
|
+
synced++;
|
|
1082
|
+
} catch {
|
|
1083
|
+
// Continue with next product
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
return { synced, total: products.length };
|
|
1088
|
+
},
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
export default stripePlugin;
|