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.
@@ -0,0 +1,552 @@
1
+ /**
2
+ * Stripe Service
3
+ * Handles all Stripe API interactions
4
+ */
5
+
6
+ import Stripe from 'stripe';
7
+ import type {
8
+ StripeConfig,
9
+ StripeProduct,
10
+ StripePrice,
11
+ StripeCustomer,
12
+ StripeSubscription,
13
+ StripeInvoice,
14
+ CheckoutSessionParams,
15
+ PortalSessionParams,
16
+ WebhookEvent,
17
+ } from './types';
18
+
19
+ export class StripeService {
20
+ private stripe: Stripe;
21
+ private config: StripeConfig;
22
+
23
+ constructor(config: StripeConfig) {
24
+ this.config = config;
25
+ this.stripe = new Stripe(config.secretKey, {
26
+ apiVersion: '2025-02-24.acacia',
27
+ });
28
+ }
29
+
30
+ // ============================================================================
31
+ // Products
32
+ // ============================================================================
33
+
34
+ async createProduct(params: {
35
+ name: string;
36
+ description?: string;
37
+ metadata?: Record<string, string>;
38
+ }): Promise<StripeProduct> {
39
+ const product = await this.stripe.products.create({
40
+ name: params.name,
41
+ description: params.description,
42
+ metadata: params.metadata,
43
+ });
44
+
45
+ return this.mapProduct(product);
46
+ }
47
+
48
+ async getProduct(id: string): Promise<StripeProduct | null> {
49
+ try {
50
+ const product = await this.stripe.products.retrieve(id);
51
+ return this.mapProduct(product);
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+
57
+ async listProducts(params?: {
58
+ active?: boolean;
59
+ limit?: number;
60
+ }): Promise<StripeProduct[]> {
61
+ const products = await this.stripe.products.list({
62
+ active: params?.active,
63
+ limit: params?.limit || 100,
64
+ });
65
+
66
+ return products.data.map((p) => this.mapProduct(p));
67
+ }
68
+
69
+ async updateProduct(
70
+ id: string,
71
+ params: {
72
+ name?: string;
73
+ description?: string;
74
+ active?: boolean;
75
+ metadata?: Record<string, string>;
76
+ }
77
+ ): Promise<StripeProduct> {
78
+ const product = await this.stripe.products.update(id, params);
79
+ return this.mapProduct(product);
80
+ }
81
+
82
+ async archiveProduct(id: string): Promise<StripeProduct> {
83
+ const product = await this.stripe.products.update(id, { active: false });
84
+ return this.mapProduct(product);
85
+ }
86
+
87
+ private mapProduct(product: Stripe.Product): StripeProduct {
88
+ return {
89
+ id: product.id,
90
+ name: product.name,
91
+ description: product.description || undefined,
92
+ active: product.active,
93
+ metadata: product.metadata,
94
+ };
95
+ }
96
+
97
+ // ============================================================================
98
+ // Prices
99
+ // ============================================================================
100
+
101
+ async createPrice(params: {
102
+ productId: string;
103
+ unitAmount: number;
104
+ currency?: string;
105
+ recurring?: {
106
+ interval: 'day' | 'week' | 'month' | 'year';
107
+ intervalCount?: number;
108
+ };
109
+ trialPeriodDays?: number;
110
+ metadata?: Record<string, string>;
111
+ }): Promise<StripePrice> {
112
+ const priceParams: Stripe.PriceCreateParams = {
113
+ product: params.productId,
114
+ unit_amount: params.unitAmount,
115
+ currency: params.currency || this.config.currency,
116
+ };
117
+
118
+ if (params.recurring) {
119
+ priceParams.recurring = {
120
+ interval: params.recurring.interval,
121
+ interval_count: params.recurring.intervalCount,
122
+ trial_period_days: params.trialPeriodDays,
123
+ };
124
+ }
125
+
126
+ if (params.metadata) {
127
+ priceParams.metadata = params.metadata;
128
+ }
129
+
130
+ const price = await this.stripe.prices.create(priceParams);
131
+ return this.mapPrice(price);
132
+ }
133
+
134
+ async getPrice(id: string): Promise<StripePrice | null> {
135
+ try {
136
+ const price = await this.stripe.prices.retrieve(id);
137
+ return this.mapPrice(price);
138
+ } catch {
139
+ return null;
140
+ }
141
+ }
142
+
143
+ async listPrices(params?: {
144
+ productId?: string;
145
+ active?: boolean;
146
+ type?: 'one_time' | 'recurring';
147
+ limit?: number;
148
+ }): Promise<StripePrice[]> {
149
+ const prices = await this.stripe.prices.list({
150
+ product: params?.productId,
151
+ active: params?.active,
152
+ type: params?.type,
153
+ limit: params?.limit || 100,
154
+ });
155
+
156
+ return prices.data.map((p) => this.mapPrice(p));
157
+ }
158
+
159
+ async updatePrice(
160
+ id: string,
161
+ params: {
162
+ active?: boolean;
163
+ metadata?: Record<string, string>;
164
+ }
165
+ ): Promise<StripePrice> {
166
+ const price = await this.stripe.prices.update(id, params);
167
+ return this.mapPrice(price);
168
+ }
169
+
170
+ private mapPrice(price: Stripe.Price): StripePrice {
171
+ return {
172
+ id: price.id,
173
+ productId: typeof price.product === 'string' ? price.product : price.product.id,
174
+ active: price.active,
175
+ currency: price.currency,
176
+ unitAmount: price.unit_amount || 0,
177
+ type: price.type,
178
+ interval: price.recurring?.interval,
179
+ intervalCount: price.recurring?.interval_count,
180
+ trialPeriodDays: price.recurring?.trial_period_days || undefined,
181
+ };
182
+ }
183
+
184
+ // ============================================================================
185
+ // Customers
186
+ // ============================================================================
187
+
188
+ async createCustomer(params: {
189
+ email: string;
190
+ name?: string;
191
+ metadata?: Record<string, string>;
192
+ }): Promise<StripeCustomer> {
193
+ const customer = await this.stripe.customers.create(params);
194
+ return this.mapCustomer(customer);
195
+ }
196
+
197
+ async getCustomer(id: string): Promise<StripeCustomer | null> {
198
+ try {
199
+ const customer = await this.stripe.customers.retrieve(id);
200
+ if (customer.deleted) return null;
201
+ return this.mapCustomer(customer as Stripe.Customer);
202
+ } catch {
203
+ return null;
204
+ }
205
+ }
206
+
207
+ async getCustomerByEmail(email: string): Promise<StripeCustomer | null> {
208
+ const customers = await this.stripe.customers.list({ email, limit: 1 });
209
+ if (customers.data.length === 0) return null;
210
+ return this.mapCustomer(customers.data[0]);
211
+ }
212
+
213
+ async updateCustomer(
214
+ id: string,
215
+ params: {
216
+ email?: string;
217
+ name?: string;
218
+ metadata?: Record<string, string>;
219
+ }
220
+ ): Promise<StripeCustomer> {
221
+ const customer = await this.stripe.customers.update(id, params);
222
+ return this.mapCustomer(customer);
223
+ }
224
+
225
+ async deleteCustomer(id: string): Promise<void> {
226
+ await this.stripe.customers.del(id);
227
+ }
228
+
229
+ private mapCustomer(customer: Stripe.Customer): StripeCustomer {
230
+ return {
231
+ id: customer.id,
232
+ email: customer.email || '',
233
+ name: customer.name || undefined,
234
+ metadata: customer.metadata,
235
+ };
236
+ }
237
+
238
+ // ============================================================================
239
+ // Subscriptions
240
+ // ============================================================================
241
+
242
+ async createSubscription(params: {
243
+ customerId: string;
244
+ priceId: string;
245
+ trialPeriodDays?: number;
246
+ metadata?: Record<string, string>;
247
+ }): Promise<StripeSubscription> {
248
+ const subscriptionParams: Stripe.SubscriptionCreateParams = {
249
+ customer: params.customerId,
250
+ items: [{ price: params.priceId }],
251
+ };
252
+
253
+ if (params.trialPeriodDays) {
254
+ subscriptionParams.trial_period_days = params.trialPeriodDays;
255
+ }
256
+
257
+ if (params.metadata) {
258
+ subscriptionParams.metadata = params.metadata;
259
+ }
260
+
261
+ const subscription = await this.stripe.subscriptions.create(subscriptionParams);
262
+ return this.mapSubscription(subscription);
263
+ }
264
+
265
+ async getSubscription(id: string): Promise<StripeSubscription | null> {
266
+ try {
267
+ const subscription = await this.stripe.subscriptions.retrieve(id);
268
+ return this.mapSubscription(subscription);
269
+ } catch {
270
+ return null;
271
+ }
272
+ }
273
+
274
+ async listSubscriptions(params?: {
275
+ customerId?: string;
276
+ priceId?: string;
277
+ status?: string;
278
+ limit?: number;
279
+ }): Promise<StripeSubscription[]> {
280
+ const subscriptions = await this.stripe.subscriptions.list({
281
+ customer: params?.customerId,
282
+ price: params?.priceId,
283
+ status: params?.status as Stripe.SubscriptionListParams.Status,
284
+ limit: params?.limit || 100,
285
+ });
286
+
287
+ return subscriptions.data.map((s) => this.mapSubscription(s));
288
+ }
289
+
290
+ async cancelSubscription(
291
+ id: string,
292
+ params?: { atPeriodEnd?: boolean }
293
+ ): Promise<StripeSubscription> {
294
+ let subscription: Stripe.Subscription;
295
+
296
+ if (params?.atPeriodEnd) {
297
+ subscription = await this.stripe.subscriptions.update(id, {
298
+ cancel_at_period_end: true,
299
+ });
300
+ } else {
301
+ subscription = await this.stripe.subscriptions.cancel(id);
302
+ }
303
+
304
+ return this.mapSubscription(subscription);
305
+ }
306
+
307
+ async resumeSubscription(id: string): Promise<StripeSubscription> {
308
+ const subscription = await this.stripe.subscriptions.update(id, {
309
+ cancel_at_period_end: false,
310
+ });
311
+ return this.mapSubscription(subscription);
312
+ }
313
+
314
+ async updateSubscription(
315
+ id: string,
316
+ params: {
317
+ priceId?: string;
318
+ metadata?: Record<string, string>;
319
+ }
320
+ ): Promise<StripeSubscription> {
321
+ const updateParams: Stripe.SubscriptionUpdateParams = {};
322
+
323
+ if (params.priceId) {
324
+ const subscription = await this.stripe.subscriptions.retrieve(id);
325
+ updateParams.items = [
326
+ {
327
+ id: subscription.items.data[0].id,
328
+ price: params.priceId,
329
+ },
330
+ ];
331
+ }
332
+
333
+ if (params.metadata) {
334
+ updateParams.metadata = params.metadata;
335
+ }
336
+
337
+ const subscription = await this.stripe.subscriptions.update(id, updateParams);
338
+ return this.mapSubscription(subscription);
339
+ }
340
+
341
+ private mapSubscription(subscription: Stripe.Subscription): StripeSubscription {
342
+ const item = subscription.items.data[0];
343
+ return {
344
+ id: subscription.id,
345
+ customerId:
346
+ typeof subscription.customer === 'string'
347
+ ? subscription.customer
348
+ : subscription.customer.id,
349
+ priceId: item.price.id,
350
+ status: subscription.status,
351
+ currentPeriodStart: subscription.current_period_start,
352
+ currentPeriodEnd: subscription.current_period_end,
353
+ cancelAtPeriodEnd: subscription.cancel_at_period_end,
354
+ canceledAt: subscription.canceled_at || undefined,
355
+ trialStart: subscription.trial_start || undefined,
356
+ trialEnd: subscription.trial_end || undefined,
357
+ };
358
+ }
359
+
360
+ // ============================================================================
361
+ // Invoices
362
+ // ============================================================================
363
+
364
+ async getInvoice(id: string): Promise<StripeInvoice | null> {
365
+ try {
366
+ const invoice = await this.stripe.invoices.retrieve(id);
367
+ return this.mapInvoice(invoice);
368
+ } catch {
369
+ return null;
370
+ }
371
+ }
372
+
373
+ async listInvoices(params?: {
374
+ customerId?: string;
375
+ subscriptionId?: string;
376
+ status?: string;
377
+ limit?: number;
378
+ }): Promise<StripeInvoice[]> {
379
+ const invoices = await this.stripe.invoices.list({
380
+ customer: params?.customerId,
381
+ subscription: params?.subscriptionId,
382
+ status: params?.status as Stripe.InvoiceListParams.Status,
383
+ limit: params?.limit || 100,
384
+ });
385
+
386
+ return invoices.data.map((i) => this.mapInvoice(i));
387
+ }
388
+
389
+ async payInvoice(id: string): Promise<StripeInvoice> {
390
+ const invoice = await this.stripe.invoices.pay(id);
391
+ return this.mapInvoice(invoice);
392
+ }
393
+
394
+ async voidInvoice(id: string): Promise<StripeInvoice> {
395
+ const invoice = await this.stripe.invoices.voidInvoice(id);
396
+ return this.mapInvoice(invoice);
397
+ }
398
+
399
+ private mapInvoice(invoice: Stripe.Invoice): StripeInvoice {
400
+ return {
401
+ id: invoice.id,
402
+ customerId:
403
+ typeof invoice.customer === 'string'
404
+ ? invoice.customer
405
+ : invoice.customer?.id || '',
406
+ subscriptionId:
407
+ typeof invoice.subscription === 'string'
408
+ ? invoice.subscription
409
+ : invoice.subscription?.id,
410
+ status: invoice.status || 'draft',
411
+ amountDue: invoice.amount_due,
412
+ amountPaid: invoice.amount_paid,
413
+ currency: invoice.currency,
414
+ hostedInvoiceUrl: invoice.hosted_invoice_url || undefined,
415
+ invoicePdf: invoice.invoice_pdf || undefined,
416
+ paidAt: invoice.status_transitions?.paid_at || undefined,
417
+ };
418
+ }
419
+
420
+ // ============================================================================
421
+ // Checkout Sessions
422
+ // ============================================================================
423
+
424
+ async createCheckoutSession(params: CheckoutSessionParams): Promise<{
425
+ id: string;
426
+ url: string;
427
+ }> {
428
+ const sessionParams: Stripe.Checkout.SessionCreateParams = {
429
+ mode: params.mode,
430
+ line_items: [
431
+ {
432
+ price: params.priceId,
433
+ quantity: params.quantity || 1,
434
+ },
435
+ ],
436
+ success_url: this.config.successUrl,
437
+ cancel_url: this.config.cancelUrl,
438
+ };
439
+
440
+ if (params.customerId) {
441
+ sessionParams.customer = params.customerId;
442
+ } else if (params.customerEmail) {
443
+ sessionParams.customer_email = params.customerEmail;
444
+ }
445
+
446
+ if (params.mode === 'subscription' && params.trialPeriodDays) {
447
+ sessionParams.subscription_data = {
448
+ trial_period_days: params.trialPeriodDays,
449
+ };
450
+ }
451
+
452
+ if (params.metadata) {
453
+ sessionParams.metadata = params.metadata;
454
+ }
455
+
456
+ if (params.userId) {
457
+ sessionParams.client_reference_id = params.userId;
458
+ }
459
+
460
+ const session = await this.stripe.checkout.sessions.create(sessionParams);
461
+
462
+ return {
463
+ id: session.id,
464
+ url: session.url || '',
465
+ };
466
+ }
467
+
468
+ async getCheckoutSession(id: string): Promise<Stripe.Checkout.Session | null> {
469
+ try {
470
+ return await this.stripe.checkout.sessions.retrieve(id, {
471
+ expand: ['customer', 'subscription', 'payment_intent'],
472
+ });
473
+ } catch {
474
+ return null;
475
+ }
476
+ }
477
+
478
+ // ============================================================================
479
+ // Customer Portal
480
+ // ============================================================================
481
+
482
+ async createPortalSession(params: PortalSessionParams): Promise<{
483
+ id: string;
484
+ url: string;
485
+ }> {
486
+ const session = await this.stripe.billingPortal.sessions.create({
487
+ customer: params.customerId,
488
+ return_url: params.returnUrl || this.config.portalReturnUrl,
489
+ });
490
+
491
+ return {
492
+ id: session.id,
493
+ url: session.url,
494
+ };
495
+ }
496
+
497
+ // ============================================================================
498
+ // Webhooks
499
+ // ============================================================================
500
+
501
+ verifyWebhookSignature(
502
+ payload: string | Buffer,
503
+ signature: string
504
+ ): WebhookEvent {
505
+ const event = this.stripe.webhooks.constructEvent(
506
+ payload,
507
+ signature,
508
+ this.config.webhookSecret
509
+ );
510
+
511
+ return {
512
+ id: event.id,
513
+ type: event.type,
514
+ data: event.data,
515
+ };
516
+ }
517
+
518
+ // ============================================================================
519
+ // Payment Methods
520
+ // ============================================================================
521
+
522
+ async listPaymentMethods(customerId: string): Promise<Stripe.PaymentMethod[]> {
523
+ const methods = await this.stripe.paymentMethods.list({
524
+ customer: customerId,
525
+ type: 'card',
526
+ });
527
+ return methods.data;
528
+ }
529
+
530
+ async detachPaymentMethod(paymentMethodId: string): Promise<void> {
531
+ await this.stripe.paymentMethods.detach(paymentMethodId);
532
+ }
533
+
534
+ // ============================================================================
535
+ // Usage Records (for metered billing)
536
+ // ============================================================================
537
+
538
+ async createUsageRecord(
539
+ subscriptionItemId: string,
540
+ quantity: number,
541
+ timestamp?: number
542
+ ): Promise<Stripe.UsageRecord> {
543
+ return await this.stripe.subscriptionItems.createUsageRecord(
544
+ subscriptionItemId,
545
+ {
546
+ quantity,
547
+ timestamp: timestamp || Math.floor(Date.now() / 1000),
548
+ action: 'increment',
549
+ }
550
+ );
551
+ }
552
+ }