omgkit 2.0.7 → 2.1.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/package.json +2 -2
- package/plugin/skills/backend/api-architecture/SKILL.md +857 -0
- package/plugin/skills/backend/caching-strategies/SKILL.md +755 -0
- package/plugin/skills/backend/event-driven-architecture/SKILL.md +753 -0
- package/plugin/skills/backend/real-time-systems/SKILL.md +635 -0
- package/plugin/skills/databases/database-optimization/SKILL.md +571 -0
- package/plugin/skills/devops/monorepo-management/SKILL.md +595 -0
- package/plugin/skills/devops/observability/SKILL.md +622 -0
- package/plugin/skills/devops/performance-profiling/SKILL.md +905 -0
- package/plugin/skills/frontend/advanced-ui-design/SKILL.md +426 -0
- package/plugin/skills/integrations/ai-integration/SKILL.md +730 -0
- package/plugin/skills/integrations/payment-integration/SKILL.md +735 -0
- package/plugin/skills/methodology/problem-solving/SKILL.md +355 -0
- package/plugin/skills/methodology/research-validation/SKILL.md +668 -0
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +260 -0
- package/plugin/skills/mobile/mobile-development/SKILL.md +756 -0
- package/plugin/skills/security/security-hardening/SKILL.md +633 -0
- package/plugin/skills/tools/document-processing/SKILL.md +916 -0
- package/plugin/skills/tools/image-processing/SKILL.md +748 -0
- package/plugin/skills/tools/mcp-development/SKILL.md +883 -0
- package/plugin/skills/tools/media-processing/SKILL.md +831 -0
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: payment-integration
|
|
3
|
+
description: Enterprise payment processing with Stripe, PayPal, and LemonSqueezy including subscriptions, webhooks, and PCI compliance
|
|
4
|
+
category: integrations
|
|
5
|
+
triggers:
|
|
6
|
+
- payment integration
|
|
7
|
+
- stripe
|
|
8
|
+
- paypal
|
|
9
|
+
- lemonsqueezy
|
|
10
|
+
- checkout
|
|
11
|
+
- subscription billing
|
|
12
|
+
- payment processing
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Payment Integration
|
|
16
|
+
|
|
17
|
+
Enterprise-grade **payment processing** with Stripe, PayPal, and LemonSqueezy. This skill covers checkout flows, subscription management, webhook handling, and PCI compliance patterns.
|
|
18
|
+
|
|
19
|
+
## Purpose
|
|
20
|
+
|
|
21
|
+
Implement secure, reliable payment systems:
|
|
22
|
+
|
|
23
|
+
- Process one-time and recurring payments
|
|
24
|
+
- Handle subscription lifecycle management
|
|
25
|
+
- Implement secure webhook processing
|
|
26
|
+
- Manage refunds and disputes
|
|
27
|
+
- Ensure PCI DSS compliance
|
|
28
|
+
- Support multiple payment methods
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
### 1. Stripe Integration
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import Stripe from 'stripe';
|
|
36
|
+
|
|
37
|
+
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
|
|
38
|
+
apiVersion: '2023-10-16',
|
|
39
|
+
typescript: true,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Create checkout session
|
|
43
|
+
async function createCheckoutSession(
|
|
44
|
+
items: CartItem[],
|
|
45
|
+
customerId?: string,
|
|
46
|
+
metadata?: Record<string, string>
|
|
47
|
+
): Promise<Stripe.Checkout.Session> {
|
|
48
|
+
const lineItems: Stripe.Checkout.SessionCreateParams.LineItem[] = items.map(item => ({
|
|
49
|
+
price_data: {
|
|
50
|
+
currency: 'usd',
|
|
51
|
+
product_data: {
|
|
52
|
+
name: item.name,
|
|
53
|
+
description: item.description,
|
|
54
|
+
images: item.images,
|
|
55
|
+
metadata: { productId: item.id },
|
|
56
|
+
},
|
|
57
|
+
unit_amount: Math.round(item.price * 100), // Convert to cents
|
|
58
|
+
},
|
|
59
|
+
quantity: item.quantity,
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
return stripe.checkout.sessions.create({
|
|
63
|
+
mode: 'payment',
|
|
64
|
+
line_items: lineItems,
|
|
65
|
+
customer: customerId,
|
|
66
|
+
success_url: `${process.env.APP_URL}/checkout/success?session_id={CHECKOUT_SESSION_ID}`,
|
|
67
|
+
cancel_url: `${process.env.APP_URL}/checkout/cancel`,
|
|
68
|
+
metadata,
|
|
69
|
+
payment_intent_data: {
|
|
70
|
+
metadata,
|
|
71
|
+
},
|
|
72
|
+
shipping_address_collection: {
|
|
73
|
+
allowed_countries: ['US', 'CA', 'GB'],
|
|
74
|
+
},
|
|
75
|
+
automatic_tax: { enabled: true },
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Create subscription
|
|
80
|
+
async function createSubscription(
|
|
81
|
+
customerId: string,
|
|
82
|
+
priceId: string,
|
|
83
|
+
options?: {
|
|
84
|
+
trialDays?: number;
|
|
85
|
+
couponId?: string;
|
|
86
|
+
metadata?: Record<string, string>;
|
|
87
|
+
}
|
|
88
|
+
): Promise<Stripe.Subscription> {
|
|
89
|
+
const params: Stripe.SubscriptionCreateParams = {
|
|
90
|
+
customer: customerId,
|
|
91
|
+
items: [{ price: priceId }],
|
|
92
|
+
payment_behavior: 'default_incomplete',
|
|
93
|
+
payment_settings: {
|
|
94
|
+
save_default_payment_method: 'on_subscription',
|
|
95
|
+
},
|
|
96
|
+
expand: ['latest_invoice.payment_intent'],
|
|
97
|
+
metadata: options?.metadata,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
if (options?.trialDays) {
|
|
101
|
+
params.trial_period_days = options.trialDays;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (options?.couponId) {
|
|
105
|
+
params.coupon = options.couponId;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return stripe.subscriptions.create(params);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Handle subscription changes
|
|
112
|
+
async function updateSubscription(
|
|
113
|
+
subscriptionId: string,
|
|
114
|
+
newPriceId: string,
|
|
115
|
+
prorationBehavior: 'create_prorations' | 'none' | 'always_invoice' = 'create_prorations'
|
|
116
|
+
): Promise<Stripe.Subscription> {
|
|
117
|
+
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
|
|
118
|
+
|
|
119
|
+
return stripe.subscriptions.update(subscriptionId, {
|
|
120
|
+
items: [{
|
|
121
|
+
id: subscription.items.data[0].id,
|
|
122
|
+
price: newPriceId,
|
|
123
|
+
}],
|
|
124
|
+
proration_behavior: prorationBehavior,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Cancel subscription
|
|
129
|
+
async function cancelSubscription(
|
|
130
|
+
subscriptionId: string,
|
|
131
|
+
cancelImmediately: boolean = false
|
|
132
|
+
): Promise<Stripe.Subscription> {
|
|
133
|
+
if (cancelImmediately) {
|
|
134
|
+
return stripe.subscriptions.cancel(subscriptionId);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return stripe.subscriptions.update(subscriptionId, {
|
|
138
|
+
cancel_at_period_end: true,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Process refund
|
|
143
|
+
async function processRefund(
|
|
144
|
+
paymentIntentId: string,
|
|
145
|
+
amount?: number, // Partial refund in cents
|
|
146
|
+
reason?: 'duplicate' | 'fraudulent' | 'requested_by_customer'
|
|
147
|
+
): Promise<Stripe.Refund> {
|
|
148
|
+
return stripe.refunds.create({
|
|
149
|
+
payment_intent: paymentIntentId,
|
|
150
|
+
amount, // Omit for full refund
|
|
151
|
+
reason,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### 2. Webhook Handling
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
import { buffer } from 'micro';
|
|
160
|
+
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
161
|
+
|
|
162
|
+
// Webhook handler with signature verification
|
|
163
|
+
async function handleStripeWebhook(
|
|
164
|
+
req: NextApiRequest,
|
|
165
|
+
res: NextApiResponse
|
|
166
|
+
): Promise<void> {
|
|
167
|
+
if (req.method !== 'POST') {
|
|
168
|
+
res.setHeader('Allow', 'POST');
|
|
169
|
+
res.status(405).end('Method Not Allowed');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const buf = await buffer(req);
|
|
174
|
+
const sig = req.headers['stripe-signature'] as string;
|
|
175
|
+
|
|
176
|
+
let event: Stripe.Event;
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
event = stripe.webhooks.constructEvent(
|
|
180
|
+
buf,
|
|
181
|
+
sig,
|
|
182
|
+
process.env.STRIPE_WEBHOOK_SECRET!
|
|
183
|
+
);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
console.error('Webhook signature verification failed:', err);
|
|
186
|
+
res.status(400).send(`Webhook Error: ${err.message}`);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Handle events with idempotency
|
|
191
|
+
const idempotencyKey = event.id;
|
|
192
|
+
const processed = await isEventProcessed(idempotencyKey);
|
|
193
|
+
|
|
194
|
+
if (processed) {
|
|
195
|
+
res.status(200).json({ received: true, duplicate: true });
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
await processWebhookEvent(event);
|
|
201
|
+
await markEventProcessed(idempotencyKey);
|
|
202
|
+
res.status(200).json({ received: true });
|
|
203
|
+
} catch (err) {
|
|
204
|
+
console.error('Webhook processing error:', err);
|
|
205
|
+
// Return 200 to prevent retries for business logic errors
|
|
206
|
+
// Return 500 for transient errors that should be retried
|
|
207
|
+
res.status(err.retryable ? 500 : 200).json({ error: err.message });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Event processor
|
|
212
|
+
async function processWebhookEvent(event: Stripe.Event): Promise<void> {
|
|
213
|
+
switch (event.type) {
|
|
214
|
+
case 'checkout.session.completed': {
|
|
215
|
+
const session = event.data.object as Stripe.Checkout.Session;
|
|
216
|
+
await handleCheckoutComplete(session);
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
case 'customer.subscription.created':
|
|
221
|
+
case 'customer.subscription.updated': {
|
|
222
|
+
const subscription = event.data.object as Stripe.Subscription;
|
|
223
|
+
await syncSubscriptionStatus(subscription);
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
case 'customer.subscription.deleted': {
|
|
228
|
+
const subscription = event.data.object as Stripe.Subscription;
|
|
229
|
+
await handleSubscriptionCanceled(subscription);
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
case 'invoice.payment_succeeded': {
|
|
234
|
+
const invoice = event.data.object as Stripe.Invoice;
|
|
235
|
+
await handlePaymentSuccess(invoice);
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
case 'invoice.payment_failed': {
|
|
240
|
+
const invoice = event.data.object as Stripe.Invoice;
|
|
241
|
+
await handlePaymentFailure(invoice);
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
case 'charge.dispute.created': {
|
|
246
|
+
const dispute = event.data.object as Stripe.Dispute;
|
|
247
|
+
await handleDispute(dispute);
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
default:
|
|
252
|
+
console.log(`Unhandled event type: ${event.type}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Subscription sync
|
|
257
|
+
async function syncSubscriptionStatus(subscription: Stripe.Subscription): Promise<void> {
|
|
258
|
+
await db.subscription.upsert({
|
|
259
|
+
where: { stripeSubscriptionId: subscription.id },
|
|
260
|
+
update: {
|
|
261
|
+
status: subscription.status,
|
|
262
|
+
currentPeriodStart: new Date(subscription.current_period_start * 1000),
|
|
263
|
+
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
|
|
264
|
+
cancelAtPeriodEnd: subscription.cancel_at_period_end,
|
|
265
|
+
priceId: subscription.items.data[0].price.id,
|
|
266
|
+
},
|
|
267
|
+
create: {
|
|
268
|
+
stripeSubscriptionId: subscription.id,
|
|
269
|
+
stripeCustomerId: subscription.customer as string,
|
|
270
|
+
status: subscription.status,
|
|
271
|
+
currentPeriodStart: new Date(subscription.current_period_start * 1000),
|
|
272
|
+
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
|
|
273
|
+
priceId: subscription.items.data[0].price.id,
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### 3. PayPal Integration
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
import { PayPalHttpClient, OrdersCreateRequest, OrdersCaptureRequest } from '@paypal/checkout-server-sdk';
|
|
283
|
+
|
|
284
|
+
// PayPal client setup
|
|
285
|
+
function getPayPalClient(): PayPalHttpClient {
|
|
286
|
+
const environment = process.env.NODE_ENV === 'production'
|
|
287
|
+
? new paypal.core.LiveEnvironment(
|
|
288
|
+
process.env.PAYPAL_CLIENT_ID!,
|
|
289
|
+
process.env.PAYPAL_CLIENT_SECRET!
|
|
290
|
+
)
|
|
291
|
+
: new paypal.core.SandboxEnvironment(
|
|
292
|
+
process.env.PAYPAL_CLIENT_ID!,
|
|
293
|
+
process.env.PAYPAL_CLIENT_SECRET!
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
return new PayPalHttpClient(environment);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Create PayPal order
|
|
300
|
+
async function createPayPalOrder(
|
|
301
|
+
items: CartItem[],
|
|
302
|
+
shippingCost: number = 0
|
|
303
|
+
): Promise<PayPalOrder> {
|
|
304
|
+
const client = getPayPalClient();
|
|
305
|
+
|
|
306
|
+
const itemTotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
|
|
307
|
+
const total = itemTotal + shippingCost;
|
|
308
|
+
|
|
309
|
+
const request = new OrdersCreateRequest();
|
|
310
|
+
request.prefer('return=representation');
|
|
311
|
+
request.requestBody({
|
|
312
|
+
intent: 'CAPTURE',
|
|
313
|
+
purchase_units: [{
|
|
314
|
+
amount: {
|
|
315
|
+
currency_code: 'USD',
|
|
316
|
+
value: total.toFixed(2),
|
|
317
|
+
breakdown: {
|
|
318
|
+
item_total: { currency_code: 'USD', value: itemTotal.toFixed(2) },
|
|
319
|
+
shipping: { currency_code: 'USD', value: shippingCost.toFixed(2) },
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
items: items.map(item => ({
|
|
323
|
+
name: item.name,
|
|
324
|
+
unit_amount: { currency_code: 'USD', value: item.price.toFixed(2) },
|
|
325
|
+
quantity: item.quantity.toString(),
|
|
326
|
+
category: 'PHYSICAL_GOODS',
|
|
327
|
+
})),
|
|
328
|
+
}],
|
|
329
|
+
application_context: {
|
|
330
|
+
brand_name: process.env.APP_NAME,
|
|
331
|
+
landing_page: 'BILLING',
|
|
332
|
+
user_action: 'PAY_NOW',
|
|
333
|
+
return_url: `${process.env.APP_URL}/checkout/paypal/success`,
|
|
334
|
+
cancel_url: `${process.env.APP_URL}/checkout/paypal/cancel`,
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const response = await client.execute(request);
|
|
339
|
+
return response.result;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Capture PayPal payment
|
|
343
|
+
async function capturePayPalOrder(orderId: string): Promise<PayPalCapture> {
|
|
344
|
+
const client = getPayPalClient();
|
|
345
|
+
const request = new OrdersCaptureRequest(orderId);
|
|
346
|
+
request.prefer('return=representation');
|
|
347
|
+
|
|
348
|
+
const response = await client.execute(request);
|
|
349
|
+
|
|
350
|
+
if (response.result.status === 'COMPLETED') {
|
|
351
|
+
await handlePayPalPaymentComplete(response.result);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return response.result;
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### 4. Subscription Management UI
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
// Subscription management component
|
|
362
|
+
interface SubscriptionManagerProps {
|
|
363
|
+
subscription: UserSubscription;
|
|
364
|
+
availablePlans: Plan[];
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function SubscriptionManager({ subscription, availablePlans }: SubscriptionManagerProps) {
|
|
368
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
369
|
+
|
|
370
|
+
async function handleUpgrade(newPriceId: string) {
|
|
371
|
+
setIsLoading(true);
|
|
372
|
+
try {
|
|
373
|
+
await updateSubscription(subscription.id, newPriceId);
|
|
374
|
+
toast.success('Subscription updated successfully');
|
|
375
|
+
} catch (error) {
|
|
376
|
+
toast.error('Failed to update subscription');
|
|
377
|
+
} finally {
|
|
378
|
+
setIsLoading(false);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function handleCancel() {
|
|
383
|
+
if (!confirm('Are you sure you want to cancel your subscription?')) return;
|
|
384
|
+
|
|
385
|
+
setIsLoading(true);
|
|
386
|
+
try {
|
|
387
|
+
await cancelSubscription(subscription.id);
|
|
388
|
+
toast.success('Subscription will be canceled at the end of the billing period');
|
|
389
|
+
} catch (error) {
|
|
390
|
+
toast.error('Failed to cancel subscription');
|
|
391
|
+
} finally {
|
|
392
|
+
setIsLoading(false);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function handleReactivate() {
|
|
397
|
+
setIsLoading(true);
|
|
398
|
+
try {
|
|
399
|
+
await reactivateSubscription(subscription.id);
|
|
400
|
+
toast.success('Subscription reactivated');
|
|
401
|
+
} catch (error) {
|
|
402
|
+
toast.error('Failed to reactivate subscription');
|
|
403
|
+
} finally {
|
|
404
|
+
setIsLoading(false);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return (
|
|
409
|
+
<div className="subscription-manager">
|
|
410
|
+
<div className="current-plan">
|
|
411
|
+
<h3>Current Plan: {subscription.plan.name}</h3>
|
|
412
|
+
<p>Status: {subscription.status}</p>
|
|
413
|
+
<p>Renews: {format(subscription.currentPeriodEnd, 'MMM d, yyyy')}</p>
|
|
414
|
+
</div>
|
|
415
|
+
|
|
416
|
+
{subscription.cancelAtPeriodEnd && (
|
|
417
|
+
<Alert variant="warning">
|
|
418
|
+
Your subscription will end on {format(subscription.currentPeriodEnd, 'MMM d, yyyy')}
|
|
419
|
+
<Button onClick={handleReactivate} disabled={isLoading}>
|
|
420
|
+
Reactivate
|
|
421
|
+
</Button>
|
|
422
|
+
</Alert>
|
|
423
|
+
)}
|
|
424
|
+
|
|
425
|
+
<div className="available-plans">
|
|
426
|
+
<h4>Available Plans</h4>
|
|
427
|
+
{availablePlans.map(plan => (
|
|
428
|
+
<PlanCard
|
|
429
|
+
key={plan.id}
|
|
430
|
+
plan={plan}
|
|
431
|
+
isCurrent={plan.priceId === subscription.priceId}
|
|
432
|
+
onSelect={() => handleUpgrade(plan.priceId)}
|
|
433
|
+
disabled={isLoading}
|
|
434
|
+
/>
|
|
435
|
+
))}
|
|
436
|
+
</div>
|
|
437
|
+
|
|
438
|
+
{!subscription.cancelAtPeriodEnd && (
|
|
439
|
+
<Button variant="destructive" onClick={handleCancel} disabled={isLoading}>
|
|
440
|
+
Cancel Subscription
|
|
441
|
+
</Button>
|
|
442
|
+
)}
|
|
443
|
+
</div>
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### 5. LemonSqueezy Integration
|
|
449
|
+
|
|
450
|
+
```typescript
|
|
451
|
+
import { lemonSqueezySetup, createCheckout, getSubscription } from '@lemonsqueezy/lemonsqueezy.js';
|
|
452
|
+
|
|
453
|
+
// Initialize LemonSqueezy
|
|
454
|
+
lemonSqueezySetup({ apiKey: process.env.LEMONSQUEEZY_API_KEY! });
|
|
455
|
+
|
|
456
|
+
// Create LemonSqueezy checkout
|
|
457
|
+
async function createLemonSqueezyCheckout(
|
|
458
|
+
variantId: string,
|
|
459
|
+
customerEmail: string,
|
|
460
|
+
metadata?: Record<string, string>
|
|
461
|
+
): Promise<string> {
|
|
462
|
+
const checkout = await createCheckout(
|
|
463
|
+
process.env.LEMONSQUEEZY_STORE_ID!,
|
|
464
|
+
variantId,
|
|
465
|
+
{
|
|
466
|
+
checkoutData: {
|
|
467
|
+
email: customerEmail,
|
|
468
|
+
custom: metadata,
|
|
469
|
+
},
|
|
470
|
+
checkoutOptions: {
|
|
471
|
+
embed: false,
|
|
472
|
+
logo: true,
|
|
473
|
+
dark: false,
|
|
474
|
+
},
|
|
475
|
+
productOptions: {
|
|
476
|
+
enabledVariants: [parseInt(variantId)],
|
|
477
|
+
redirectUrl: `${process.env.APP_URL}/checkout/success`,
|
|
478
|
+
},
|
|
479
|
+
}
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
return checkout.data.attributes.url;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// LemonSqueezy webhook handler
|
|
486
|
+
async function handleLemonSqueezyWebhook(req: NextApiRequest): Promise<void> {
|
|
487
|
+
const signature = req.headers['x-signature'] as string;
|
|
488
|
+
const payload = JSON.stringify(req.body);
|
|
489
|
+
|
|
490
|
+
// Verify signature
|
|
491
|
+
const hmac = crypto.createHmac('sha256', process.env.LEMONSQUEEZY_WEBHOOK_SECRET!);
|
|
492
|
+
hmac.update(payload);
|
|
493
|
+
const expectedSignature = hmac.digest('hex');
|
|
494
|
+
|
|
495
|
+
if (signature !== expectedSignature) {
|
|
496
|
+
throw new Error('Invalid webhook signature');
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const { event_name, data } = req.body;
|
|
500
|
+
|
|
501
|
+
switch (event_name) {
|
|
502
|
+
case 'subscription_created':
|
|
503
|
+
await handleLSSubscriptionCreated(data);
|
|
504
|
+
break;
|
|
505
|
+
case 'subscription_updated':
|
|
506
|
+
await handleLSSubscriptionUpdated(data);
|
|
507
|
+
break;
|
|
508
|
+
case 'subscription_cancelled':
|
|
509
|
+
await handleLSSubscriptionCancelled(data);
|
|
510
|
+
break;
|
|
511
|
+
case 'order_created':
|
|
512
|
+
await handleLSOrderCreated(data);
|
|
513
|
+
break;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### 6. Payment Security
|
|
519
|
+
|
|
520
|
+
```typescript
|
|
521
|
+
// PCI-compliant payment handling patterns
|
|
522
|
+
|
|
523
|
+
// Never log sensitive payment data
|
|
524
|
+
const SENSITIVE_FIELDS = ['card_number', 'cvv', 'cvc', 'exp_month', 'exp_year'];
|
|
525
|
+
|
|
526
|
+
function sanitizeForLogging(data: Record<string, any>): Record<string, any> {
|
|
527
|
+
const sanitized = { ...data };
|
|
528
|
+
|
|
529
|
+
for (const field of SENSITIVE_FIELDS) {
|
|
530
|
+
if (field in sanitized) {
|
|
531
|
+
sanitized[field] = '[REDACTED]';
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return sanitized;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Secure API endpoint
|
|
539
|
+
async function createPaymentIntent(req: NextApiRequest, res: NextApiResponse) {
|
|
540
|
+
// Validate request
|
|
541
|
+
const { amount, currency, customerId } = req.body;
|
|
542
|
+
|
|
543
|
+
if (!amount || amount < 50) { // Minimum $0.50
|
|
544
|
+
return res.status(400).json({ error: 'Invalid amount' });
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Use idempotency key
|
|
548
|
+
const idempotencyKey = req.headers['idempotency-key'] as string;
|
|
549
|
+
|
|
550
|
+
if (!idempotencyKey) {
|
|
551
|
+
return res.status(400).json({ error: 'Idempotency key required' });
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
try {
|
|
555
|
+
const paymentIntent = await stripe.paymentIntents.create(
|
|
556
|
+
{
|
|
557
|
+
amount,
|
|
558
|
+
currency: currency || 'usd',
|
|
559
|
+
customer: customerId,
|
|
560
|
+
automatic_payment_methods: { enabled: true },
|
|
561
|
+
metadata: {
|
|
562
|
+
userId: req.user.id,
|
|
563
|
+
},
|
|
564
|
+
},
|
|
565
|
+
{ idempotencyKey }
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
// Only return necessary data
|
|
569
|
+
res.json({
|
|
570
|
+
clientSecret: paymentIntent.client_secret,
|
|
571
|
+
paymentIntentId: paymentIntent.id,
|
|
572
|
+
});
|
|
573
|
+
} catch (error) {
|
|
574
|
+
console.error('Payment error:', sanitizeForLogging(error));
|
|
575
|
+
|
|
576
|
+
if (error.type === 'StripeCardError') {
|
|
577
|
+
return res.status(400).json({ error: error.message });
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
res.status(500).json({ error: 'Payment processing failed' });
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Webhook security middleware
|
|
585
|
+
function verifyWebhookSignature(
|
|
586
|
+
payload: Buffer,
|
|
587
|
+
signature: string,
|
|
588
|
+
secret: string
|
|
589
|
+
): boolean {
|
|
590
|
+
try {
|
|
591
|
+
stripe.webhooks.constructEvent(payload, signature, secret);
|
|
592
|
+
return true;
|
|
593
|
+
} catch {
|
|
594
|
+
return false;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
## Use Cases
|
|
600
|
+
|
|
601
|
+
### 1. E-commerce Checkout
|
|
602
|
+
|
|
603
|
+
```typescript
|
|
604
|
+
// Complete e-commerce checkout flow
|
|
605
|
+
async function processCheckout(cart: Cart, user: User): Promise<CheckoutResult> {
|
|
606
|
+
// Calculate totals
|
|
607
|
+
const subtotal = calculateSubtotal(cart.items);
|
|
608
|
+
const tax = await calculateTax(subtotal, user.address);
|
|
609
|
+
const shipping = await calculateShipping(cart.items, user.address);
|
|
610
|
+
const total = subtotal + tax + shipping;
|
|
611
|
+
|
|
612
|
+
// Create Stripe checkout
|
|
613
|
+
const session = await createCheckoutSession(cart.items, user.stripeCustomerId, {
|
|
614
|
+
orderId: generateOrderId(),
|
|
615
|
+
userId: user.id,
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
// Create pending order
|
|
619
|
+
await db.order.create({
|
|
620
|
+
data: {
|
|
621
|
+
userId: user.id,
|
|
622
|
+
status: 'pending',
|
|
623
|
+
subtotal,
|
|
624
|
+
tax,
|
|
625
|
+
shipping,
|
|
626
|
+
total,
|
|
627
|
+
stripeSessionId: session.id,
|
|
628
|
+
items: {
|
|
629
|
+
create: cart.items.map(item => ({
|
|
630
|
+
productId: item.productId,
|
|
631
|
+
quantity: item.quantity,
|
|
632
|
+
price: item.price,
|
|
633
|
+
})),
|
|
634
|
+
},
|
|
635
|
+
},
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
return {
|
|
639
|
+
checkoutUrl: session.url,
|
|
640
|
+
sessionId: session.id,
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
### 2. SaaS Subscription
|
|
646
|
+
|
|
647
|
+
```typescript
|
|
648
|
+
// SaaS subscription management
|
|
649
|
+
async function handlePlanChange(
|
|
650
|
+
userId: string,
|
|
651
|
+
newPlanId: string
|
|
652
|
+
): Promise<PlanChangeResult> {
|
|
653
|
+
const user = await db.user.findUnique({
|
|
654
|
+
where: { id: userId },
|
|
655
|
+
include: { subscription: true },
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
if (!user.subscription) {
|
|
659
|
+
// New subscription
|
|
660
|
+
const session = await createSubscriptionCheckout(user, newPlanId);
|
|
661
|
+
return { action: 'redirect', url: session.url };
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const currentPlan = await getPlan(user.subscription.priceId);
|
|
665
|
+
const newPlan = await getPlan(newPlanId);
|
|
666
|
+
|
|
667
|
+
if (newPlan.price > currentPlan.price) {
|
|
668
|
+
// Upgrade - charge prorated amount
|
|
669
|
+
await updateSubscription(user.subscription.stripeSubscriptionId, newPlanId, 'create_prorations');
|
|
670
|
+
return { action: 'upgraded', newPlan };
|
|
671
|
+
} else {
|
|
672
|
+
// Downgrade - apply at period end
|
|
673
|
+
await updateSubscription(user.subscription.stripeSubscriptionId, newPlanId, 'none');
|
|
674
|
+
return { action: 'scheduled', effectiveDate: user.subscription.currentPeriodEnd };
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
## Best Practices
|
|
680
|
+
|
|
681
|
+
### Do's
|
|
682
|
+
|
|
683
|
+
- **Use idempotency keys** - Prevent duplicate charges
|
|
684
|
+
- **Verify webhook signatures** - Always validate webhook authenticity
|
|
685
|
+
- **Handle all payment states** - Success, failure, pending, disputed
|
|
686
|
+
- **Store payment references** - Keep Stripe IDs for reconciliation
|
|
687
|
+
- **Test with sandbox** - Use test mode during development
|
|
688
|
+
- **Monitor for fraud** - Implement Stripe Radar or equivalent
|
|
689
|
+
|
|
690
|
+
### Don'ts
|
|
691
|
+
|
|
692
|
+
- Never log full card numbers
|
|
693
|
+
- Never store CVV/CVC codes
|
|
694
|
+
- Never handle raw card data (use Stripe.js/Elements)
|
|
695
|
+
- Never skip signature verification
|
|
696
|
+
- Never trust client-side amounts
|
|
697
|
+
- Never expose secret keys in frontend
|
|
698
|
+
|
|
699
|
+
### Security Checklist
|
|
700
|
+
|
|
701
|
+
```markdown
|
|
702
|
+
## Payment Security Checklist
|
|
703
|
+
|
|
704
|
+
### PCI Compliance
|
|
705
|
+
- [ ] No raw card data on server
|
|
706
|
+
- [ ] Using Stripe.js/Elements for card collection
|
|
707
|
+
- [ ] Webhook signature verification enabled
|
|
708
|
+
- [ ] HTTPS only for all payment endpoints
|
|
709
|
+
|
|
710
|
+
### Data Handling
|
|
711
|
+
- [ ] Sensitive fields excluded from logs
|
|
712
|
+
- [ ] Customer IDs used instead of card details
|
|
713
|
+
- [ ] Idempotency keys for all mutations
|
|
714
|
+
- [ ] Payment references stored securely
|
|
715
|
+
|
|
716
|
+
### Fraud Prevention
|
|
717
|
+
- [ ] Stripe Radar enabled
|
|
718
|
+
- [ ] Address verification (AVS)
|
|
719
|
+
- [ ] 3D Secure enabled
|
|
720
|
+
- [ ] Velocity checks implemented
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
## Related Skills
|
|
724
|
+
|
|
725
|
+
- **backend-development** - Server-side payment handling
|
|
726
|
+
- **security** - PCI compliance and secure handling
|
|
727
|
+
- **oauth** - Customer authentication
|
|
728
|
+
- **api-architecture** - Payment API design
|
|
729
|
+
|
|
730
|
+
## Reference Resources
|
|
731
|
+
|
|
732
|
+
- [Stripe Documentation](https://stripe.com/docs)
|
|
733
|
+
- [PayPal Developer](https://developer.paypal.com/)
|
|
734
|
+
- [LemonSqueezy Docs](https://docs.lemonsqueezy.com/)
|
|
735
|
+
- [PCI DSS Standards](https://www.pcisecuritystandards.org/)
|