Initial commit
This commit is contained in:
18
.claude-plugin/plugin.json
Normal file
18
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "specweave-payments",
|
||||
"description": "Payment processing integration for Stripe, PayPal, and billing automation. Includes checkout flows, subscription lifecycle management, PCI DSS compliance guidance, and recurring billing. Focus on production-ready payment systems.",
|
||||
"version": "0.22.14",
|
||||
"author": {
|
||||
"name": "SpecWeave Team",
|
||||
"url": "https://spec-weave.com"
|
||||
},
|
||||
"skills": [
|
||||
"./skills"
|
||||
],
|
||||
"agents": [
|
||||
"./agents"
|
||||
],
|
||||
"commands": [
|
||||
"./commands"
|
||||
]
|
||||
}
|
||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# specweave-payments
|
||||
|
||||
Payment processing integration for Stripe, PayPal, and billing automation. Includes checkout flows, subscription lifecycle management, PCI DSS compliance guidance, and recurring billing. Focus on production-ready payment systems.
|
||||
61
agents/payment-integration/AGENT.md
Normal file
61
agents/payment-integration/AGENT.md
Normal file
@@ -0,0 +1,61 @@
|
||||
---
|
||||
name: payment-integration
|
||||
description: Integrate Stripe, PayPal, and payment processors. Handles checkout flows, subscriptions, webhooks, and PCI compliance. Use PROACTIVELY when implementing payments, billing, or subscription features.
|
||||
model: claude-haiku-4-5-20251001
|
||||
model_preference: haiku
|
||||
cost_profile: execution
|
||||
fallback_behavior: flexible
|
||||
---
|
||||
|
||||
You are a payment integration specialist focused on secure, reliable payment processing.
|
||||
|
||||
## 🚀 How to Invoke This Agent
|
||||
|
||||
**Subagent Type**: `specweave-payments:payment-integration:payment-integration`
|
||||
|
||||
**Usage Example**:
|
||||
|
||||
```typescript
|
||||
Task({
|
||||
subagent_type: "specweave-payments:payment-integration:payment-integration",
|
||||
prompt: "Implement Stripe payment integration with checkout flow, webhook handling, and subscription billing",
|
||||
model: "haiku" // optional: haiku, sonnet, opus
|
||||
});
|
||||
```
|
||||
|
||||
**Naming Convention**: `{plugin}:{directory}:{yaml-name-or-directory-name}`
|
||||
- **Plugin**: specweave-payments
|
||||
- **Directory**: payment-integration
|
||||
- **Agent Name**: payment-integration
|
||||
|
||||
**When to Use**:
|
||||
- You're implementing payment processing with Stripe or PayPal
|
||||
- You need to build checkout flows and payment forms
|
||||
- You want to set up recurring billing and subscriptions
|
||||
- You need to handle payment webhooks and events
|
||||
- You want to ensure PCI compliance and security best practices
|
||||
|
||||
## Focus Areas
|
||||
- Stripe/PayPal/Square API integration
|
||||
- Checkout flows and payment forms
|
||||
- Subscription billing and recurring payments
|
||||
- Webhook handling for payment events
|
||||
- PCI compliance and security best practices
|
||||
- Payment error handling and retry logic
|
||||
|
||||
## Approach
|
||||
1. Security first - never log sensitive card data
|
||||
2. Implement idempotency for all payment operations
|
||||
3. Handle all edge cases (failed payments, disputes, refunds)
|
||||
4. Test mode first, with clear migration path to production
|
||||
5. Comprehensive webhook handling for async events
|
||||
|
||||
## Output
|
||||
- Payment integration code with error handling
|
||||
- Webhook endpoint implementations
|
||||
- Database schema for payment records
|
||||
- Security checklist (PCI compliance points)
|
||||
- Test payment scenarios and edge cases
|
||||
- Environment variable configuration
|
||||
|
||||
Always use official SDKs. Include both server-side and client-side code where needed.
|
||||
931
commands/stripe-setup.md
Normal file
931
commands/stripe-setup.md
Normal file
@@ -0,0 +1,931 @@
|
||||
# /specweave-payments:stripe-setup
|
||||
|
||||
Complete Stripe integration setup guide with production-ready code templates, security best practices, and testing workflows.
|
||||
|
||||
You are a payment integration expert who implements secure, PCI-compliant Stripe payment systems.
|
||||
|
||||
## Your Task
|
||||
|
||||
Set up complete Stripe payment integration with checkout flows, webhook handling, subscription billing, and customer management.
|
||||
|
||||
### 1. Environment Setup
|
||||
|
||||
**Install Dependencies**:
|
||||
|
||||
```bash
|
||||
# Node.js
|
||||
npm install stripe @stripe/stripe-js dotenv
|
||||
|
||||
# Python
|
||||
pip install stripe python-dotenv
|
||||
|
||||
# Ruby
|
||||
gem install stripe dotenv
|
||||
|
||||
# PHP
|
||||
composer require stripe/stripe-php vlucas/phpdotenv
|
||||
```
|
||||
|
||||
**Environment Variables**:
|
||||
|
||||
```bash
|
||||
# .env (NEVER commit this file!)
|
||||
# Get keys from https://dashboard.stripe.com/apikeys
|
||||
|
||||
# Test mode (development)
|
||||
STRIPE_PUBLISHABLE_KEY=pk_test_51...
|
||||
STRIPE_SECRET_KEY=sk_test_51...
|
||||
STRIPE_WEBHOOK_SECRET=whsec_...
|
||||
|
||||
# Live mode (production)
|
||||
# STRIPE_PUBLISHABLE_KEY=pk_live_51...
|
||||
# STRIPE_SECRET_KEY=sk_live_51...
|
||||
# STRIPE_WEBHOOK_SECRET=whsec_...
|
||||
|
||||
# App configuration
|
||||
STRIPE_SUCCESS_URL=https://yourdomain.com/success
|
||||
STRIPE_CANCEL_URL=https://yourdomain.com/cancel
|
||||
STRIPE_CURRENCY=usd
|
||||
```
|
||||
|
||||
### 2. Backend Setup (Node.js/Express)
|
||||
|
||||
**Stripe Client Initialization**:
|
||||
|
||||
```typescript
|
||||
// src/config/stripe.ts
|
||||
import Stripe from 'stripe';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
if (!process.env.STRIPE_SECRET_KEY) {
|
||||
throw new Error('STRIPE_SECRET_KEY is not set in environment variables');
|
||||
}
|
||||
|
||||
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
|
||||
apiVersion: '2023-10-16',
|
||||
typescript: true,
|
||||
maxNetworkRetries: 2,
|
||||
timeout: 10000, // 10 seconds
|
||||
});
|
||||
|
||||
export const STRIPE_CONFIG = {
|
||||
publishableKey: process.env.STRIPE_PUBLISHABLE_KEY!,
|
||||
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
|
||||
successUrl: process.env.STRIPE_SUCCESS_URL || 'http://localhost:3000/success',
|
||||
cancelUrl: process.env.STRIPE_CANCEL_URL || 'http://localhost:3000/cancel',
|
||||
currency: process.env.STRIPE_CURRENCY || 'usd',
|
||||
};
|
||||
```
|
||||
|
||||
**Payment Service**:
|
||||
|
||||
```typescript
|
||||
// src/services/payment.service.ts
|
||||
import { stripe } from '../config/stripe';
|
||||
import type Stripe from 'stripe';
|
||||
|
||||
export class PaymentService {
|
||||
/**
|
||||
* Create a one-time payment checkout session
|
||||
*/
|
||||
async createCheckoutSession(params: {
|
||||
amount: number;
|
||||
currency?: string;
|
||||
customerId?: string;
|
||||
metadata?: Record<string, string>;
|
||||
}): Promise<Stripe.Checkout.Session> {
|
||||
try {
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
payment_method_types: ['card'],
|
||||
line_items: [
|
||||
{
|
||||
price_data: {
|
||||
currency: params.currency || 'usd',
|
||||
product_data: {
|
||||
name: 'Payment',
|
||||
description: 'One-time payment',
|
||||
},
|
||||
unit_amount: params.amount, // Amount in cents
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
mode: 'payment',
|
||||
success_url: `${process.env.STRIPE_SUCCESS_URL}?session_id={CHECKOUT_SESSION_ID}`,
|
||||
cancel_url: process.env.STRIPE_CANCEL_URL,
|
||||
customer: params.customerId,
|
||||
metadata: params.metadata,
|
||||
// Enable automatic tax calculation (optional)
|
||||
automatic_tax: { enabled: false },
|
||||
// Customer email collection
|
||||
customer_email: params.customerId ? undefined : '',
|
||||
});
|
||||
|
||||
return session;
|
||||
} catch (error) {
|
||||
console.error('Failed to create checkout session:', error);
|
||||
throw new Error('Payment session creation failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a payment intent for custom checkout UI
|
||||
*/
|
||||
async createPaymentIntent(params: {
|
||||
amount: number;
|
||||
currency?: string;
|
||||
customerId?: string;
|
||||
paymentMethodTypes?: string[];
|
||||
metadata?: Record<string, string>;
|
||||
}): Promise<Stripe.PaymentIntent> {
|
||||
try {
|
||||
const paymentIntent = await stripe.paymentIntents.create({
|
||||
amount: params.amount,
|
||||
currency: params.currency || 'usd',
|
||||
customer: params.customerId,
|
||||
payment_method_types: params.paymentMethodTypes || ['card'],
|
||||
metadata: params.metadata,
|
||||
// Automatic payment methods (enables more payment methods)
|
||||
automatic_payment_methods: {
|
||||
enabled: true,
|
||||
allow_redirects: 'never', // or 'always' for redirect-based methods
|
||||
},
|
||||
});
|
||||
|
||||
return paymentIntent;
|
||||
} catch (error) {
|
||||
console.error('Failed to create payment intent:', error);
|
||||
throw new Error('Payment intent creation failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a payment intent
|
||||
*/
|
||||
async getPaymentIntent(paymentIntentId: string): Promise<Stripe.PaymentIntent> {
|
||||
try {
|
||||
return await stripe.paymentIntents.retrieve(paymentIntentId);
|
||||
} catch (error) {
|
||||
console.error('Failed to retrieve payment intent:', error);
|
||||
throw new Error('Payment intent retrieval failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm a payment intent (server-side confirmation)
|
||||
*/
|
||||
async confirmPaymentIntent(
|
||||
paymentIntentId: string,
|
||||
paymentMethodId: string
|
||||
): Promise<Stripe.PaymentIntent> {
|
||||
try {
|
||||
return await stripe.paymentIntents.confirm(paymentIntentId, {
|
||||
payment_method: paymentMethodId,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to confirm payment intent:', error);
|
||||
throw new Error('Payment confirmation failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update a customer
|
||||
*/
|
||||
async createCustomer(params: {
|
||||
email: string;
|
||||
name?: string;
|
||||
phone?: string;
|
||||
metadata?: Record<string, string>;
|
||||
paymentMethodId?: string;
|
||||
}): Promise<Stripe.Customer> {
|
||||
try {
|
||||
const customer = await stripe.customers.create({
|
||||
email: params.email,
|
||||
name: params.name,
|
||||
phone: params.phone,
|
||||
metadata: params.metadata,
|
||||
payment_method: params.paymentMethodId,
|
||||
invoice_settings: params.paymentMethodId
|
||||
? {
|
||||
default_payment_method: params.paymentMethodId,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
||||
return customer;
|
||||
} catch (error) {
|
||||
console.error('Failed to create customer:', error);
|
||||
throw new Error('Customer creation failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach a payment method to a customer
|
||||
*/
|
||||
async attachPaymentMethod(
|
||||
paymentMethodId: string,
|
||||
customerId: string,
|
||||
setAsDefault = true
|
||||
): Promise<Stripe.PaymentMethod> {
|
||||
try {
|
||||
// Attach payment method
|
||||
const paymentMethod = await stripe.paymentMethods.attach(paymentMethodId, {
|
||||
customer: customerId,
|
||||
});
|
||||
|
||||
// Set as default if requested
|
||||
if (setAsDefault) {
|
||||
await stripe.customers.update(customerId, {
|
||||
invoice_settings: {
|
||||
default_payment_method: paymentMethodId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return paymentMethod;
|
||||
} catch (error) {
|
||||
console.error('Failed to attach payment method:', error);
|
||||
throw new Error('Payment method attachment failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List customer payment methods
|
||||
*/
|
||||
async listPaymentMethods(customerId: string): Promise<Stripe.PaymentMethod[]> {
|
||||
try {
|
||||
const paymentMethods = await stripe.paymentMethods.list({
|
||||
customer: customerId,
|
||||
type: 'card',
|
||||
});
|
||||
|
||||
return paymentMethods.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to list payment methods:', error);
|
||||
throw new Error('Payment method listing failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a refund
|
||||
*/
|
||||
async createRefund(params: {
|
||||
paymentIntentId?: string;
|
||||
chargeId?: string;
|
||||
amount?: number; // Partial refund amount in cents
|
||||
reason?: 'duplicate' | 'fraudulent' | 'requested_by_customer';
|
||||
metadata?: Record<string, string>;
|
||||
}): Promise<Stripe.Refund> {
|
||||
try {
|
||||
const refund = await stripe.refunds.create({
|
||||
payment_intent: params.paymentIntentId,
|
||||
charge: params.chargeId,
|
||||
amount: params.amount,
|
||||
reason: params.reason,
|
||||
metadata: params.metadata,
|
||||
});
|
||||
|
||||
return refund;
|
||||
} catch (error) {
|
||||
console.error('Failed to create refund:', error);
|
||||
throw new Error('Refund creation failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const paymentService = new PaymentService();
|
||||
```
|
||||
|
||||
**Express API Routes**:
|
||||
|
||||
```typescript
|
||||
// src/routes/payment.routes.ts
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { paymentService } from '../services/payment.service';
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* POST /api/payments/checkout
|
||||
* Create a checkout session
|
||||
*/
|
||||
router.post('/checkout', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { amount, currency, customerId, metadata } = req.body;
|
||||
|
||||
// Validate amount
|
||||
if (!amount || amount <= 0) {
|
||||
return res.status(400).json({ error: 'Invalid amount' });
|
||||
}
|
||||
|
||||
const session = await paymentService.createCheckoutSession({
|
||||
amount,
|
||||
currency,
|
||||
customerId,
|
||||
metadata,
|
||||
});
|
||||
|
||||
res.json({ sessionId: session.id, url: session.url });
|
||||
} catch (error) {
|
||||
console.error('Checkout error:', error);
|
||||
res.status(500).json({ error: 'Failed to create checkout session' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/payments/intent
|
||||
* Create a payment intent for custom UI
|
||||
*/
|
||||
router.post('/intent', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { amount, currency, customerId, metadata } = req.body;
|
||||
|
||||
if (!amount || amount <= 0) {
|
||||
return res.status(400).json({ error: 'Invalid amount' });
|
||||
}
|
||||
|
||||
const paymentIntent = await paymentService.createPaymentIntent({
|
||||
amount,
|
||||
currency,
|
||||
customerId,
|
||||
metadata,
|
||||
});
|
||||
|
||||
res.json({ clientSecret: paymentIntent.client_secret });
|
||||
} catch (error) {
|
||||
console.error('Payment intent error:', error);
|
||||
res.status(500).json({ error: 'Failed to create payment intent' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/payments/customers
|
||||
* Create a customer
|
||||
*/
|
||||
router.post('/customers', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { email, name, phone, metadata } = req.body;
|
||||
|
||||
if (!email) {
|
||||
return res.status(400).json({ error: 'Email is required' });
|
||||
}
|
||||
|
||||
const customer = await paymentService.createCustomer({
|
||||
email,
|
||||
name,
|
||||
phone,
|
||||
metadata,
|
||||
});
|
||||
|
||||
res.json({ customerId: customer.id });
|
||||
} catch (error) {
|
||||
console.error('Customer creation error:', error);
|
||||
res.status(500).json({ error: 'Failed to create customer' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/payments/refunds
|
||||
* Create a refund
|
||||
*/
|
||||
router.post('/refunds', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { paymentIntentId, amount, reason, metadata } = req.body;
|
||||
|
||||
if (!paymentIntentId) {
|
||||
return res.status(400).json({ error: 'Payment Intent ID is required' });
|
||||
}
|
||||
|
||||
const refund = await paymentService.createRefund({
|
||||
paymentIntentId,
|
||||
amount,
|
||||
reason,
|
||||
metadata,
|
||||
});
|
||||
|
||||
res.json({ refundId: refund.id, status: refund.status });
|
||||
} catch (error) {
|
||||
console.error('Refund error:', error);
|
||||
res.status(500).json({ error: 'Failed to create refund' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/payments/config
|
||||
* Get public Stripe configuration
|
||||
*/
|
||||
router.get('/config', (req: Request, res: Response) => {
|
||||
res.json({
|
||||
publishableKey: process.env.STRIPE_PUBLISHABLE_KEY,
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
```
|
||||
|
||||
### 3. Frontend Setup (React)
|
||||
|
||||
**Stripe Provider**:
|
||||
|
||||
```typescript
|
||||
// src/providers/StripeProvider.tsx
|
||||
import React from 'react';
|
||||
import { Elements } from '@stripe/react-stripe-js';
|
||||
import { loadStripe, Stripe } from '@stripe/stripe-js';
|
||||
|
||||
// Load Stripe.js outside of component to avoid recreating the instance
|
||||
let stripePromise: Promise<Stripe | null>;
|
||||
|
||||
const getStripe = () => {
|
||||
if (!stripePromise) {
|
||||
const publishableKey = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY || '';
|
||||
stripePromise = loadStripe(publishableKey);
|
||||
}
|
||||
return stripePromise;
|
||||
};
|
||||
|
||||
interface StripeProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const StripeProvider: React.FC<StripeProviderProps> = ({ children }) => {
|
||||
return (
|
||||
<Elements stripe={getStripe()}>
|
||||
{children}
|
||||
</Elements>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
**Payment Form Component**:
|
||||
|
||||
```typescript
|
||||
// src/components/PaymentForm.tsx
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
useStripe,
|
||||
useElements,
|
||||
CardElement,
|
||||
PaymentElement,
|
||||
} from '@stripe/react-stripe-js';
|
||||
import type { StripeError } from '@stripe/stripe-js';
|
||||
|
||||
interface PaymentFormProps {
|
||||
amount: number;
|
||||
currency?: string;
|
||||
onSuccess: (paymentIntentId: string) => void;
|
||||
onError: (error: string) => void;
|
||||
customerId?: string;
|
||||
metadata?: Record<string, string>;
|
||||
}
|
||||
|
||||
export const PaymentForm: React.FC<PaymentFormProps> = ({
|
||||
amount,
|
||||
currency = 'usd',
|
||||
onSuccess,
|
||||
onError,
|
||||
customerId,
|
||||
metadata,
|
||||
}) => {
|
||||
const stripe = useStripe();
|
||||
const elements = useElements();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!stripe || !elements) {
|
||||
// Stripe.js hasn't loaded yet
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Create payment intent on backend
|
||||
const response = await fetch('/api/payments/intent', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
amount,
|
||||
currency,
|
||||
customerId,
|
||||
metadata,
|
||||
}),
|
||||
});
|
||||
|
||||
const { clientSecret } = await response.json();
|
||||
|
||||
// Confirm payment with Stripe.js
|
||||
const { error: stripeError, paymentIntent } = await stripe.confirmCardPayment(
|
||||
clientSecret,
|
||||
{
|
||||
payment_method: {
|
||||
card: elements.getElement(CardElement)!,
|
||||
billing_details: {
|
||||
// Add billing details if collected
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (stripeError) {
|
||||
setError(stripeError.message || 'Payment failed');
|
||||
onError(stripeError.message || 'Payment failed');
|
||||
} else if (paymentIntent && paymentIntent.status === 'succeeded') {
|
||||
onSuccess(paymentIntent.id);
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Payment failed';
|
||||
setError(errorMessage);
|
||||
onError(errorMessage);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="max-w-md mx-auto p-6">
|
||||
<div className="mb-6">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Card Details
|
||||
</label>
|
||||
<div className="border border-gray-300 rounded-lg p-3">
|
||||
<CardElement
|
||||
options={{
|
||||
style: {
|
||||
base: {
|
||||
fontSize: '16px',
|
||||
color: '#424770',
|
||||
'::placeholder': {
|
||||
color: '#aab7c4',
|
||||
},
|
||||
},
|
||||
invalid: {
|
||||
color: '#9e2146',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!stripe || loading}
|
||||
className="w-full bg-blue-600 text-white py-3 px-4 rounded-lg font-medium hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{loading ? 'Processing...' : `Pay $${(amount / 100).toFixed(2)}`}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
**Checkout Session Flow**:
|
||||
|
||||
```typescript
|
||||
// src/components/CheckoutButton.tsx
|
||||
import React, { useState } from 'react';
|
||||
import { loadStripe } from '@stripe/stripe-js';
|
||||
|
||||
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);
|
||||
|
||||
interface CheckoutButtonProps {
|
||||
amount: number;
|
||||
currency?: string;
|
||||
buttonText?: string;
|
||||
}
|
||||
|
||||
export const CheckoutButton: React.FC<CheckoutButtonProps> = ({
|
||||
amount,
|
||||
currency = 'usd',
|
||||
buttonText = 'Checkout',
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleCheckout = async () => {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
// Create checkout session
|
||||
const response = await fetch('/api/payments/checkout', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ amount, currency }),
|
||||
});
|
||||
|
||||
const { sessionId } = await response.json();
|
||||
|
||||
// Redirect to Stripe Checkout
|
||||
const stripe = await stripePromise;
|
||||
if (stripe) {
|
||||
const { error } = await stripe.redirectToCheckout({ sessionId });
|
||||
if (error) {
|
||||
console.error('Checkout error:', error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Checkout error:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleCheckout}
|
||||
disabled={loading}
|
||||
className="bg-blue-600 text-white py-2 px-6 rounded-lg font-medium hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{loading ? 'Loading...' : buttonText}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 4. Testing
|
||||
|
||||
**Test Cards**:
|
||||
|
||||
```typescript
|
||||
// Test card numbers for different scenarios
|
||||
export const TEST_CARDS = {
|
||||
// Success
|
||||
VISA_SUCCESS: '4242424242424242',
|
||||
VISA_DEBIT: '4000056655665556',
|
||||
MASTERCARD: '5555555555554444',
|
||||
|
||||
// Authentication required
|
||||
THREE_D_SECURE: '4000002500003155',
|
||||
|
||||
// Failure scenarios
|
||||
CARD_DECLINED: '4000000000000002',
|
||||
INSUFFICIENT_FUNDS: '4000000000009995',
|
||||
LOST_CARD: '4000000000009987',
|
||||
STOLEN_CARD: '4000000000009979',
|
||||
EXPIRED_CARD: '4000000000000069',
|
||||
INCORRECT_CVC: '4000000000000127',
|
||||
PROCESSING_ERROR: '4000000000000119',
|
||||
|
||||
// Special cases
|
||||
DISPUTE: '4000000000000259',
|
||||
FRAUD: '4100000000000019',
|
||||
};
|
||||
|
||||
// Any future expiry date (e.g., 12/34)
|
||||
// Any 3-digit CVC
|
||||
// Any postal code
|
||||
```
|
||||
|
||||
**Integration Test**:
|
||||
|
||||
```typescript
|
||||
// tests/integration/payment.test.ts
|
||||
import { paymentService } from '../../src/services/payment.service';
|
||||
import Stripe from 'stripe';
|
||||
|
||||
describe('Payment Service Integration', () => {
|
||||
describe('Payment Intent', () => {
|
||||
it('should create a payment intent', async () => {
|
||||
const paymentIntent = await paymentService.createPaymentIntent({
|
||||
amount: 1000,
|
||||
currency: 'usd',
|
||||
});
|
||||
|
||||
expect(paymentIntent).toBeDefined();
|
||||
expect(paymentIntent.amount).toBe(1000);
|
||||
expect(paymentIntent.currency).toBe('usd');
|
||||
expect(paymentIntent.status).toBe('requires_payment_method');
|
||||
});
|
||||
|
||||
it('should confirm payment intent with test card', async () => {
|
||||
// Create payment intent
|
||||
const paymentIntent = await paymentService.createPaymentIntent({
|
||||
amount: 1000,
|
||||
currency: 'usd',
|
||||
});
|
||||
|
||||
// Create test payment method
|
||||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
|
||||
const paymentMethod = await stripe.paymentMethods.create({
|
||||
type: 'card',
|
||||
card: {
|
||||
number: '4242424242424242',
|
||||
exp_month: 12,
|
||||
exp_year: 2034,
|
||||
cvc: '123',
|
||||
},
|
||||
});
|
||||
|
||||
// Confirm payment
|
||||
const confirmed = await paymentService.confirmPaymentIntent(
|
||||
paymentIntent.id,
|
||||
paymentMethod.id
|
||||
);
|
||||
|
||||
expect(confirmed.status).toBe('succeeded');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Customer Management', () => {
|
||||
it('should create a customer', async () => {
|
||||
const customer = await paymentService.createCustomer({
|
||||
email: 'test@example.com',
|
||||
name: 'Test User',
|
||||
});
|
||||
|
||||
expect(customer).toBeDefined();
|
||||
expect(customer.email).toBe('test@example.com');
|
||||
});
|
||||
|
||||
it('should attach payment method to customer', async () => {
|
||||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
|
||||
|
||||
// Create customer
|
||||
const customer = await paymentService.createCustomer({
|
||||
email: 'test@example.com',
|
||||
});
|
||||
|
||||
// Create payment method
|
||||
const paymentMethod = await stripe.paymentMethods.create({
|
||||
type: 'card',
|
||||
card: {
|
||||
number: '4242424242424242',
|
||||
exp_month: 12,
|
||||
exp_year: 2034,
|
||||
cvc: '123',
|
||||
},
|
||||
});
|
||||
|
||||
// Attach payment method
|
||||
const attached = await paymentService.attachPaymentMethod(
|
||||
paymentMethod.id,
|
||||
customer.id
|
||||
);
|
||||
|
||||
expect(attached.customer).toBe(customer.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Refunds', () => {
|
||||
it('should create a refund', async () => {
|
||||
// First create and confirm a payment
|
||||
const paymentIntent = await paymentService.createPaymentIntent({
|
||||
amount: 1000,
|
||||
currency: 'usd',
|
||||
});
|
||||
|
||||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
|
||||
const paymentMethod = await stripe.paymentMethods.create({
|
||||
type: 'card',
|
||||
card: {
|
||||
number: '4242424242424242',
|
||||
exp_month: 12,
|
||||
exp_year: 2034,
|
||||
cvc: '123',
|
||||
},
|
||||
});
|
||||
|
||||
await paymentService.confirmPaymentIntent(paymentIntent.id, paymentMethod.id);
|
||||
|
||||
// Create refund
|
||||
const refund = await paymentService.createRefund({
|
||||
paymentIntentId: paymentIntent.id,
|
||||
reason: 'requested_by_customer',
|
||||
});
|
||||
|
||||
expect(refund).toBeDefined();
|
||||
expect(refund.status).toBe('succeeded');
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 5. Security Checklist
|
||||
|
||||
**Backend Security**:
|
||||
- [ ] NEVER log full card numbers or CVV
|
||||
- [ ] Use HTTPS only (enforce TLS 1.2+)
|
||||
- [ ] Validate webhook signatures
|
||||
- [ ] Implement rate limiting on payment endpoints
|
||||
- [ ] Store Stripe IDs, not card details
|
||||
- [ ] Use environment variables for keys
|
||||
- [ ] Implement idempotency keys for retries
|
||||
- [ ] Sanitize user inputs
|
||||
- [ ] Enable CSRF protection
|
||||
- [ ] Use secure session management
|
||||
|
||||
**Frontend Security**:
|
||||
- [ ] Use Stripe.js (never raw card inputs)
|
||||
- [ ] Load Stripe.js from CDN (integrity check)
|
||||
- [ ] Never send card data to your server
|
||||
- [ ] Implement CSP headers
|
||||
- [ ] Use HTTPS only
|
||||
- [ ] Clear sensitive data from memory
|
||||
- [ ] Disable autocomplete on card fields
|
||||
- [ ] Implement proper error handling
|
||||
|
||||
**Monitoring**:
|
||||
- [ ] Log all payment attempts
|
||||
- [ ] Monitor failed payment rates
|
||||
- [ ] Set up alerts for unusual activity
|
||||
- [ ] Track refund rates
|
||||
- [ ] Monitor webhook delivery
|
||||
- [ ] Implement fraud detection
|
||||
|
||||
### 6. Production Deployment
|
||||
|
||||
**Pre-launch Checklist**:
|
||||
|
||||
1. **Update API Keys**:
|
||||
- Switch from test keys (`sk_test_`, `pk_test_`) to live keys
|
||||
- Update webhook endpoint with live webhook secret
|
||||
- Test with live mode in Stripe Dashboard
|
||||
|
||||
2. **Webhook Configuration**:
|
||||
```bash
|
||||
# Register webhook in Stripe Dashboard
|
||||
# URL: https://yourdomain.com/api/webhooks/stripe
|
||||
# Events: payment_intent.succeeded, payment_intent.payment_failed,
|
||||
# customer.subscription.*, charge.refunded
|
||||
```
|
||||
|
||||
3. **Enable Radar** (fraud detection):
|
||||
- Configure Radar rules in Stripe Dashboard
|
||||
- Enable 3D Secure for high-risk payments
|
||||
- Set up risk score thresholds
|
||||
|
||||
4. **Tax Configuration**:
|
||||
- Enable Stripe Tax if needed
|
||||
- Configure tax rates by location
|
||||
- Set up tax reporting
|
||||
|
||||
5. **Business Verification**:
|
||||
- Complete business verification in Stripe
|
||||
- Add business information
|
||||
- Verify bank account for payouts
|
||||
|
||||
6. **Monitoring**:
|
||||
- Set up Sentry or similar for error tracking
|
||||
- Configure log aggregation (Datadog, Splunk)
|
||||
- Set up uptime monitoring for webhook endpoint
|
||||
- Create alerts for failed payments
|
||||
|
||||
## Output Deliverables
|
||||
|
||||
When you complete this setup, provide:
|
||||
|
||||
1. **Configured Files**:
|
||||
- `.env` template with all required variables
|
||||
- Backend service with payment methods
|
||||
- API routes with error handling
|
||||
- Frontend components (PaymentForm, CheckoutButton)
|
||||
|
||||
2. **Documentation**:
|
||||
- API endpoint documentation
|
||||
- Testing guide with test cards
|
||||
- Deployment checklist
|
||||
- Security audit report
|
||||
|
||||
3. **Testing**:
|
||||
- Integration tests for payment flows
|
||||
- Test scenarios for edge cases
|
||||
- Webhook handling tests
|
||||
|
||||
4. **Deployment**:
|
||||
- Environment-specific configurations
|
||||
- Database migration scripts (if storing payment records)
|
||||
- Monitoring setup guide
|
||||
|
||||
## Resources
|
||||
|
||||
- **Stripe Documentation**: https://stripe.com/docs
|
||||
- **Stripe.js Reference**: https://stripe.com/docs/js
|
||||
- **Webhook Testing**: Use Stripe CLI (`stripe listen --forward-to localhost:3000/api/webhooks/stripe`)
|
||||
- **Test Cards**: https://stripe.com/docs/testing
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always use Stripe.js** for card collection (PCI compliance)
|
||||
2. **Verify webhooks** with signature validation
|
||||
3. **Handle errors gracefully** with user-friendly messages
|
||||
4. **Test thoroughly** with all test cards before production
|
||||
5. **Monitor payment success rates** and investigate drops
|
||||
6. **Implement retry logic** for API failures
|
||||
7. **Use metadata** to link payments to your database records
|
||||
8. **Never expose secret keys** in frontend code
|
||||
9. **Implement idempotency** for payment operations
|
||||
10. **Keep Stripe.js updated** to latest version
|
||||
|
||||
Start with test mode, verify all flows work correctly, then switch to live mode with the same code.
|
||||
1193
commands/subscription-flow.md
Normal file
1193
commands/subscription-flow.md
Normal file
File diff suppressed because it is too large
Load Diff
386
commands/subscription-manage.md
Normal file
386
commands/subscription-manage.md
Normal file
@@ -0,0 +1,386 @@
|
||||
# Subscription Management
|
||||
|
||||
Generate complete subscription management system.
|
||||
|
||||
## Task
|
||||
|
||||
You are a subscription billing expert. Generate production-ready subscription management with billing, upgrades, and cancellations.
|
||||
|
||||
### Steps:
|
||||
|
||||
1. **Ask for Requirements**:
|
||||
- Pricing tiers (Basic, Pro, Enterprise)
|
||||
- Billing interval (monthly, annual)
|
||||
- Features per tier
|
||||
- Trial period
|
||||
|
||||
2. **Generate Pricing Configuration**:
|
||||
|
||||
```typescript
|
||||
// config/pricing.ts
|
||||
export const PRICING_PLANS = {
|
||||
basic: {
|
||||
id: 'basic',
|
||||
name: 'Basic',
|
||||
description: 'For individuals and small teams',
|
||||
prices: {
|
||||
monthly: {
|
||||
amount: 9,
|
||||
stripePriceId: 'price_basic_monthly',
|
||||
},
|
||||
annual: {
|
||||
amount: 90,
|
||||
stripePriceId: 'price_basic_annual',
|
||||
savings: 18, // 2 months free
|
||||
},
|
||||
},
|
||||
features: [
|
||||
'10 projects',
|
||||
'5 GB storage',
|
||||
'Basic support',
|
||||
],
|
||||
limits: {
|
||||
projects: 10,
|
||||
storage: 5 * 1024 * 1024 * 1024, // 5 GB in bytes
|
||||
apiCallsPerMonth: 10000,
|
||||
},
|
||||
},
|
||||
pro: {
|
||||
id: 'pro',
|
||||
name: 'Pro',
|
||||
description: 'For growing teams',
|
||||
prices: {
|
||||
monthly: {
|
||||
amount: 29,
|
||||
stripePriceId: 'price_pro_monthly',
|
||||
},
|
||||
annual: {
|
||||
amount: 290,
|
||||
stripePriceId: 'price_pro_annual',
|
||||
savings: 58,
|
||||
},
|
||||
},
|
||||
features: [
|
||||
'Unlimited projects',
|
||||
'50 GB storage',
|
||||
'Priority support',
|
||||
'Advanced analytics',
|
||||
],
|
||||
limits: {
|
||||
projects: Infinity,
|
||||
storage: 50 * 1024 * 1024 * 1024,
|
||||
apiCallsPerMonth: 100000,
|
||||
},
|
||||
},
|
||||
enterprise: {
|
||||
id: 'enterprise',
|
||||
name: 'Enterprise',
|
||||
description: 'For large organizations',
|
||||
prices: {
|
||||
monthly: {
|
||||
amount: 99,
|
||||
stripePriceId: 'price_enterprise_monthly',
|
||||
},
|
||||
annual: {
|
||||
amount: 990,
|
||||
stripePriceId: 'price_enterprise_annual',
|
||||
savings: 198,
|
||||
},
|
||||
},
|
||||
features: [
|
||||
'Unlimited everything',
|
||||
'1 TB storage',
|
||||
'24/7 dedicated support',
|
||||
'Custom integrations',
|
||||
'SLA guarantee',
|
||||
],
|
||||
limits: {
|
||||
projects: Infinity,
|
||||
storage: 1024 * 1024 * 1024 * 1024,
|
||||
apiCallsPerMonth: Infinity,
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
3. **Generate Subscription Service**:
|
||||
|
||||
```typescript
|
||||
// services/subscription.service.ts
|
||||
import Stripe from 'stripe';
|
||||
import { PRICING_PLANS } from '../config/pricing';
|
||||
|
||||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
|
||||
|
||||
export class SubscriptionService {
|
||||
// Create subscription
|
||||
async create(userId: string, planId: string, interval: 'monthly' | 'annual') {
|
||||
const user = await db.users.findUnique({ where: { id: userId } });
|
||||
const plan = PRICING_PLANS[planId];
|
||||
|
||||
if (!plan) throw new Error('Invalid plan');
|
||||
|
||||
// Create Stripe customer if doesn't exist
|
||||
let customerId = user.stripeCustomerId;
|
||||
if (!customerId) {
|
||||
const customer = await stripe.customers.create({
|
||||
email: user.email,
|
||||
metadata: { userId },
|
||||
});
|
||||
customerId = customer.id;
|
||||
await db.users.update({
|
||||
where: { id: userId },
|
||||
data: { stripeCustomerId: customerId },
|
||||
});
|
||||
}
|
||||
|
||||
// Create subscription
|
||||
const subscription = await stripe.subscriptions.create({
|
||||
customer: customerId,
|
||||
items: [{ price: plan.prices[interval].stripePriceId }],
|
||||
trial_period_days: 14, // 14-day trial
|
||||
payment_behavior: 'default_incomplete',
|
||||
payment_settings: {
|
||||
save_default_payment_method: 'on_subscription',
|
||||
},
|
||||
expand: ['latest_invoice.payment_intent'],
|
||||
});
|
||||
|
||||
// Save to database
|
||||
await db.subscriptions.create({
|
||||
data: {
|
||||
userId,
|
||||
stripeSubscriptionId: subscription.id,
|
||||
stripePriceId: plan.prices[interval].stripePriceId,
|
||||
status: subscription.status,
|
||||
planId,
|
||||
interval,
|
||||
currentPeriodStart: new Date(subscription.current_period_start * 1000),
|
||||
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
|
||||
trialEnd: subscription.trial_end
|
||||
? new Date(subscription.trial_end * 1000)
|
||||
: null,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
subscriptionId: subscription.id,
|
||||
clientSecret: subscription.latest_invoice.payment_intent.client_secret,
|
||||
};
|
||||
}
|
||||
|
||||
// Upgrade/downgrade subscription
|
||||
async changePlan(userId: string, newPlanId: string, newInterval: 'monthly' | 'annual') {
|
||||
const subscription = await db.subscriptions.findFirst({
|
||||
where: { userId, status: 'active' },
|
||||
});
|
||||
|
||||
if (!subscription) throw new Error('No active subscription');
|
||||
|
||||
const newPlan = PRICING_PLANS[newPlanId];
|
||||
const newPriceId = newPlan.prices[newInterval].stripePriceId;
|
||||
|
||||
// Update Stripe subscription
|
||||
const stripeSubscription = await stripe.subscriptions.retrieve(
|
||||
subscription.stripeSubscriptionId
|
||||
);
|
||||
|
||||
const updatedSubscription = await stripe.subscriptions.update(
|
||||
subscription.stripeSubscriptionId,
|
||||
{
|
||||
items: [
|
||||
{
|
||||
id: stripeSubscription.items.data[0].id,
|
||||
price: newPriceId,
|
||||
},
|
||||
],
|
||||
proration_behavior: 'always_invoice', // Prorate charges
|
||||
}
|
||||
);
|
||||
|
||||
// Update database
|
||||
await db.subscriptions.update({
|
||||
where: { id: subscription.id },
|
||||
data: {
|
||||
stripePriceId: newPriceId,
|
||||
planId: newPlanId,
|
||||
interval: newInterval,
|
||||
},
|
||||
});
|
||||
|
||||
return updatedSubscription;
|
||||
}
|
||||
|
||||
// Cancel subscription
|
||||
async cancel(userId: string, cancelAtPeriodEnd = true) {
|
||||
const subscription = await db.subscriptions.findFirst({
|
||||
where: { userId, status: 'active' },
|
||||
});
|
||||
|
||||
if (!subscription) throw new Error('No active subscription');
|
||||
|
||||
if (cancelAtPeriodEnd) {
|
||||
// Cancel at end of billing period
|
||||
await stripe.subscriptions.update(subscription.stripeSubscriptionId, {
|
||||
cancel_at_period_end: true,
|
||||
});
|
||||
|
||||
await db.subscriptions.update({
|
||||
where: { id: subscription.id },
|
||||
data: { cancelAtPeriodEnd: true },
|
||||
});
|
||||
} else {
|
||||
// Cancel immediately
|
||||
await stripe.subscriptions.cancel(subscription.stripeSubscriptionId);
|
||||
|
||||
await db.subscriptions.update({
|
||||
where: { id: subscription.id },
|
||||
data: { status: 'canceled', canceledAt: new Date() },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Resume canceled subscription
|
||||
async resume(userId: string) {
|
||||
const subscription = await db.subscriptions.findFirst({
|
||||
where: { userId, cancelAtPeriodEnd: true },
|
||||
});
|
||||
|
||||
if (!subscription) throw new Error('No subscription to resume');
|
||||
|
||||
await stripe.subscriptions.update(subscription.stripeSubscriptionId, {
|
||||
cancel_at_period_end: false,
|
||||
});
|
||||
|
||||
await db.subscriptions.update({
|
||||
where: { id: subscription.id },
|
||||
data: { cancelAtPeriodEnd: false },
|
||||
});
|
||||
}
|
||||
|
||||
// Get subscription status
|
||||
async getStatus(userId: string) {
|
||||
const subscription = await db.subscriptions.findFirst({
|
||||
where: { userId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
if (!subscription) return null;
|
||||
|
||||
const plan = PRICING_PLANS[subscription.planId];
|
||||
return {
|
||||
...subscription,
|
||||
plan,
|
||||
isActive: subscription.status === 'active',
|
||||
isTrialing: subscription.status === 'trialing',
|
||||
isCanceling: subscription.cancelAtPeriodEnd,
|
||||
};
|
||||
}
|
||||
|
||||
// Check feature access
|
||||
async canAccess(userId: string, feature: string, value?: number) {
|
||||
const status = await this.getStatus(userId);
|
||||
|
||||
if (!status || !status.isActive) return false;
|
||||
|
||||
const limits = status.plan.limits;
|
||||
|
||||
// Check specific limits
|
||||
switch (feature) {
|
||||
case 'projects':
|
||||
const projectCount = await db.projects.count({ where: { userId } });
|
||||
return projectCount < limits.projects;
|
||||
|
||||
case 'storage':
|
||||
const storageUsed = await this.getStorageUsage(userId);
|
||||
return storageUsed < limits.storage;
|
||||
|
||||
case 'api_calls':
|
||||
const apiCalls = await this.getApiCallsThisMonth(userId);
|
||||
return apiCalls < limits.apiCallsPerMonth;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. **Generate Usage Tracking**:
|
||||
|
||||
```typescript
|
||||
// Track API usage for metered billing
|
||||
export class UsageTracker {
|
||||
async recordApiCall(userId: string) {
|
||||
const subscription = await db.subscriptions.findFirst({
|
||||
where: { userId, status: 'active' },
|
||||
});
|
||||
|
||||
if (!subscription) return;
|
||||
|
||||
// Increment usage
|
||||
await db.usageRecords.create({
|
||||
data: {
|
||||
subscriptionId: subscription.id,
|
||||
type: 'api_call',
|
||||
quantity: 1,
|
||||
timestamp: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
// Optional: Report to Stripe for metered billing
|
||||
if (subscription.meteringEnabled) {
|
||||
await stripe.subscriptionItems.createUsageRecord(
|
||||
subscription.stripeSubscriptionItemId,
|
||||
{
|
||||
quantity: 1,
|
||||
timestamp: Math.floor(Date.now() / 1000),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getUsage(userId: string, period: 'month' | 'all' = 'month') {
|
||||
const subscription = await db.subscriptions.findFirst({
|
||||
where: { userId },
|
||||
});
|
||||
|
||||
if (!subscription) return null;
|
||||
|
||||
const startDate =
|
||||
period === 'month'
|
||||
? new Date(new Date().setDate(1)) // Start of month
|
||||
: undefined;
|
||||
|
||||
const usage = await db.usageRecords.groupBy({
|
||||
by: ['type'],
|
||||
where: {
|
||||
subscriptionId: subscription.id,
|
||||
timestamp: startDate ? { gte: startDate } : undefined,
|
||||
},
|
||||
_sum: {
|
||||
quantity: true,
|
||||
},
|
||||
});
|
||||
|
||||
return usage;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Best Practices Included:
|
||||
|
||||
- Trial periods
|
||||
- Proration on plan changes
|
||||
- Cancel at period end vs immediate
|
||||
- Usage tracking for metered billing
|
||||
- Feature gating based on plan
|
||||
- Subscription resumption
|
||||
- Clear pricing configuration
|
||||
|
||||
### Example Usage:
|
||||
|
||||
```
|
||||
User: "Set up subscription with Basic, Pro, Enterprise tiers"
|
||||
Result: Complete subscription system with billing, upgrades, trials
|
||||
```
|
||||
295
commands/webhook-setup.md
Normal file
295
commands/webhook-setup.md
Normal file
@@ -0,0 +1,295 @@
|
||||
# Payment Webhook Configuration
|
||||
|
||||
Generate secure webhook handlers for payment providers.
|
||||
|
||||
## Task
|
||||
|
||||
You are a payment webhook security expert. Generate secure, production-ready webhook handlers.
|
||||
|
||||
### Steps:
|
||||
|
||||
1. **Ask for Provider**:
|
||||
- Stripe
|
||||
- PayPal
|
||||
- Square
|
||||
- Custom payment gateway
|
||||
|
||||
2. **Generate Webhook Endpoint** (Stripe):
|
||||
|
||||
```typescript
|
||||
import crypto from 'crypto';
|
||||
import express from 'express';
|
||||
import Stripe from 'stripe';
|
||||
|
||||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
|
||||
const app = express();
|
||||
|
||||
// CRITICAL: Use raw body for webhook signature verification
|
||||
app.post(
|
||||
'/api/webhooks/stripe',
|
||||
express.raw({ type: 'application/json' }),
|
||||
async (req, res) => {
|
||||
const sig = req.headers['stripe-signature'];
|
||||
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
|
||||
|
||||
let event;
|
||||
|
||||
try {
|
||||
// Verify webhook signature
|
||||
event = stripe.webhooks.constructEvent(
|
||||
req.body,
|
||||
sig!,
|
||||
webhookSecret
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(`Webhook signature verification failed: ${err.message}`);
|
||||
return res.status(400).send(`Webhook Error: ${err.message}`);
|
||||
}
|
||||
|
||||
// Handle event idempotently
|
||||
const eventId = event.id;
|
||||
const existingEvent = await db.webhookEvents.findUnique({
|
||||
where: { stripeEventId: eventId },
|
||||
});
|
||||
|
||||
if (existingEvent) {
|
||||
console.log(`Duplicate webhook event: ${eventId}`);
|
||||
return res.status(200).json({ received: true });
|
||||
}
|
||||
|
||||
// Store event (prevents duplicate processing)
|
||||
await db.webhookEvents.create({
|
||||
data: {
|
||||
stripeEventId: eventId,
|
||||
type: event.type,
|
||||
processedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
// Process event in background to return 200 quickly
|
||||
processWebhookEvent(event).catch((error) => {
|
||||
console.error(`Failed to process webhook: ${error.message}`);
|
||||
// Alert ops team
|
||||
});
|
||||
|
||||
res.status(200).json({ received: true });
|
||||
}
|
||||
);
|
||||
|
||||
async function processWebhookEvent(event: Stripe.Event) {
|
||||
switch (event.type) {
|
||||
case 'checkout.session.completed':
|
||||
await handleCheckoutComplete(event.data.object);
|
||||
break;
|
||||
|
||||
case 'customer.subscription.created':
|
||||
case 'customer.subscription.updated':
|
||||
await handleSubscriptionChange(event.data.object);
|
||||
break;
|
||||
|
||||
case 'customer.subscription.deleted':
|
||||
await handleSubscriptionCanceled(event.data.object);
|
||||
break;
|
||||
|
||||
case 'invoice.paid':
|
||||
await handleInvoicePaid(event.data.object);
|
||||
break;
|
||||
|
||||
case 'invoice.payment_failed':
|
||||
await handleInvoicePaymentFailed(event.data.object);
|
||||
break;
|
||||
|
||||
case 'payment_intent.succeeded':
|
||||
await handlePaymentSuccess(event.data.object);
|
||||
break;
|
||||
|
||||
case 'payment_intent.payment_failed':
|
||||
await handlePaymentFailed(event.data.object);
|
||||
break;
|
||||
|
||||
case 'charge.dispute.created':
|
||||
await handleDisputeCreated(event.data.object);
|
||||
break;
|
||||
|
||||
case 'customer.created':
|
||||
await handleCustomerCreated(event.data.object);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log(`Unhandled event type: ${event.type}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Generate PayPal Webhook**:
|
||||
|
||||
```typescript
|
||||
import crypto from 'crypto';
|
||||
|
||||
app.post('/api/webhooks/paypal', express.json(), async (req, res) => {
|
||||
const webhookId = process.env.PAYPAL_WEBHOOK_ID!;
|
||||
const webhookEvent = req.body;
|
||||
|
||||
// Verify PayPal webhook signature
|
||||
const isValid = await verifyPayPalWebhook(req, webhookId);
|
||||
|
||||
if (!isValid) {
|
||||
return res.status(400).send('Invalid webhook signature');
|
||||
}
|
||||
|
||||
const eventType = webhookEvent.event_type;
|
||||
|
||||
switch (eventType) {
|
||||
case 'PAYMENT.CAPTURE.COMPLETED':
|
||||
await handlePayPalPaymentCompleted(webhookEvent.resource);
|
||||
break;
|
||||
|
||||
case 'BILLING.SUBSCRIPTION.CREATED':
|
||||
await handlePayPalSubscriptionCreated(webhookEvent.resource);
|
||||
break;
|
||||
|
||||
case 'BILLING.SUBSCRIPTION.CANCELLED':
|
||||
await handlePayPalSubscriptionCancelled(webhookEvent.resource);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log(`Unhandled PayPal event: ${eventType}`);
|
||||
}
|
||||
|
||||
res.status(200).json({ received: true });
|
||||
});
|
||||
|
||||
async function verifyPayPalWebhook(req, webhookId) {
|
||||
const transmissionId = req.headers['paypal-transmission-id'];
|
||||
const timestamp = req.headers['paypal-transmission-time'];
|
||||
const signature = req.headers['paypal-transmission-sig'];
|
||||
const certUrl = req.headers['paypal-cert-url'];
|
||||
|
||||
const response = await fetch(
|
||||
`https://api.paypal.com/v1/notifications/verify-webhook-signature`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${await getPayPalAccessToken()}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
transmission_id: transmissionId,
|
||||
transmission_time: timestamp,
|
||||
cert_url: certUrl,
|
||||
auth_algo: req.headers['paypal-auth-algo'],
|
||||
transmission_sig: signature,
|
||||
webhook_id: webhookId,
|
||||
webhook_event: req.body,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
return data.verification_status === 'SUCCESS';
|
||||
}
|
||||
```
|
||||
|
||||
4. **Generate Webhook Testing Script**:
|
||||
|
||||
```typescript
|
||||
// test-webhook.ts
|
||||
import { exec } from 'child_process';
|
||||
import util from 'util';
|
||||
|
||||
const execPromise = util.promisify(exec);
|
||||
|
||||
async function testWebhook() {
|
||||
// Install Stripe CLI: brew install stripe/stripe-cli/stripe
|
||||
|
||||
// Listen to webhooks
|
||||
const { stdout } = await execPromise('stripe listen --forward-to localhost:3000/api/webhooks/stripe');
|
||||
console.log(stdout);
|
||||
|
||||
// Trigger test events
|
||||
await execPromise('stripe trigger payment_intent.succeeded');
|
||||
await execPromise('stripe trigger customer.subscription.created');
|
||||
await execPromise('stripe trigger invoice.payment_failed');
|
||||
}
|
||||
|
||||
testWebhook();
|
||||
```
|
||||
|
||||
5. **Generate Monitoring & Alerting**:
|
||||
|
||||
```typescript
|
||||
// Monitor webhook failures
|
||||
async function monitorWebhooks() {
|
||||
const failedEvents = await db.webhookEvents.findMany({
|
||||
where: {
|
||||
processed: false,
|
||||
createdAt: {
|
||||
lt: new Date(Date.now() - 5 * 60 * 1000), // 5 minutes ago
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (failedEvents.length > 0) {
|
||||
await sendAlert({
|
||||
type: 'webhook_failure',
|
||||
count: failedEvents.length,
|
||||
events: failedEvents.map((e) => e.type),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Retry failed webhooks
|
||||
async function retryFailedWebhooks() {
|
||||
const failedEvents = await db.webhookEvents.findMany({
|
||||
where: { processed: false },
|
||||
take: 10,
|
||||
});
|
||||
|
||||
for (const event of failedEvents) {
|
||||
try {
|
||||
await processWebhookEvent(event.data);
|
||||
await db.webhookEvents.update({
|
||||
where: { id: event.id },
|
||||
data: { processed: true, processedAt: new Date() },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Retry failed for event ${event.id}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
6. **Generate Webhook Schema** (Database):
|
||||
|
||||
```sql
|
||||
CREATE TABLE webhook_events (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
stripe_event_id VARCHAR(255) UNIQUE NOT NULL,
|
||||
type VARCHAR(100) NOT NULL,
|
||||
data JSONB NOT NULL,
|
||||
processed BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
processed_at TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_webhook_events_type ON webhook_events(type);
|
||||
CREATE INDEX idx_webhook_events_processed ON webhook_events(processed);
|
||||
```
|
||||
|
||||
### Security Best Practices:
|
||||
|
||||
- ✅ Verify webhook signatures (prevent spoofing)
|
||||
- ✅ Use raw request body for signature validation
|
||||
- ✅ Idempotency (track event IDs, prevent duplicate processing)
|
||||
- ✅ Return 200 immediately (process in background)
|
||||
- ✅ Retry logic for failures
|
||||
- ✅ Monitoring and alerting
|
||||
- ✅ HTTPS only (secure transmission)
|
||||
- ✅ IP whitelisting (optional but recommended)
|
||||
|
||||
### Example Usage:
|
||||
|
||||
```
|
||||
User: "Set up secure Stripe webhook handler"
|
||||
Result: Complete webhook endpoint with signature verification, idempotency, and monitoring
|
||||
```
|
||||
77
plugin.lock.json
Normal file
77
plugin.lock.json
Normal file
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||
"pluginId": "gh:anton-abyzov/specweave:plugins/specweave-payments",
|
||||
"normalized": {
|
||||
"repo": null,
|
||||
"ref": "refs/tags/v20251128.0",
|
||||
"commit": "74ff4ef1842f904cc4ce74a8a7260a4b1d345033",
|
||||
"treeHash": "90ff82e9531aa182ea37ac8bece0e5ccbc71564b9c519ffc0a52f4c39b48ad3f",
|
||||
"generatedAt": "2025-11-28T10:13:53.304035Z",
|
||||
"toolVersion": "publish_plugins.py@0.2.0"
|
||||
},
|
||||
"origin": {
|
||||
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||
"branch": "master",
|
||||
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||
},
|
||||
"manifest": {
|
||||
"name": "specweave-payments",
|
||||
"description": "Payment processing integration for Stripe, PayPal, and billing automation. Includes checkout flows, subscription lifecycle management, PCI DSS compliance guidance, and recurring billing. Focus on production-ready payment systems.",
|
||||
"version": "0.22.14"
|
||||
},
|
||||
"content": {
|
||||
"files": [
|
||||
{
|
||||
"path": "README.md",
|
||||
"sha256": "e8e8f19307f83e5dd609e9656899fd8ab8b466f3303f23a3043d162d1e49013c"
|
||||
},
|
||||
{
|
||||
"path": "agents/payment-integration/AGENT.md",
|
||||
"sha256": "020245ca834a42196f6b0dde85cb688cefe2496766da3920cfc883a124e58bee"
|
||||
},
|
||||
{
|
||||
"path": ".claude-plugin/plugin.json",
|
||||
"sha256": "017c97bf830c1c16c68541d1ae3acb50e00f358a82d741b25dfabdc01ced480c"
|
||||
},
|
||||
{
|
||||
"path": "commands/webhook-setup.md",
|
||||
"sha256": "7a5486eca995fa3c6f24362045c6dda1c515dd0afecb1d23d658705feab4a6bf"
|
||||
},
|
||||
{
|
||||
"path": "commands/subscription-manage.md",
|
||||
"sha256": "96968e02e733befb7839b6119c7180120609fa819370d07cb58b78782b0f0036"
|
||||
},
|
||||
{
|
||||
"path": "commands/stripe-setup.md",
|
||||
"sha256": "93d4fffbd3fb7f53107474c05d9c43891da31f37aacebd012c7fc66a4c1e956a"
|
||||
},
|
||||
{
|
||||
"path": "commands/subscription-flow.md",
|
||||
"sha256": "fdab7bbbc1b8dc5ae334d667043aafdf5582f75be5e07eb946d9ca5fc3867672"
|
||||
},
|
||||
{
|
||||
"path": "skills/stripe-integration/SKILL.md",
|
||||
"sha256": "c424fb2edf330858cc28b6416abdc09eb1233a0cff98231fa917fba401c3fe6b"
|
||||
},
|
||||
{
|
||||
"path": "skills/billing-automation/SKILL.md",
|
||||
"sha256": "d8cfd13532ef4573539de5b3404c5cb794e9988c96f54c158de37da91ff91caf"
|
||||
},
|
||||
{
|
||||
"path": "skills/paypal-integration/SKILL.md",
|
||||
"sha256": "88802f7ef05d07ff15f8e8e0d2a37714599dc7629aac1d21e4ecba06cdbd6ce0"
|
||||
},
|
||||
{
|
||||
"path": "skills/pci-compliance/SKILL.md",
|
||||
"sha256": "9c50102176f3b0108358299aa7de9168aec84ab1c1fb88b7beaf12ef58e78ee4"
|
||||
}
|
||||
],
|
||||
"dirSha256": "90ff82e9531aa182ea37ac8bece0e5ccbc71564b9c519ffc0a52f4c39b48ad3f"
|
||||
},
|
||||
"security": {
|
||||
"scannedAt": null,
|
||||
"scannerVersion": null,
|
||||
"flags": []
|
||||
}
|
||||
}
|
||||
559
skills/billing-automation/SKILL.md
Normal file
559
skills/billing-automation/SKILL.md
Normal file
@@ -0,0 +1,559 @@
|
||||
---
|
||||
name: billing-automation
|
||||
description: Build automated billing systems for recurring payments, invoicing, subscription lifecycle, and dunning management. Use when implementing subscription billing, automating invoicing, or managing recurring payment systems.
|
||||
---
|
||||
|
||||
# Billing Automation
|
||||
|
||||
Master automated billing systems including recurring billing, invoice generation, dunning management, proration, and tax calculation.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Implementing SaaS subscription billing
|
||||
- Automating invoice generation and delivery
|
||||
- Managing failed payment recovery (dunning)
|
||||
- Calculating prorated charges for plan changes
|
||||
- Handling sales tax, VAT, and GST
|
||||
- Processing usage-based billing
|
||||
- Managing billing cycles and renewals
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### 1. Billing Cycles
|
||||
**Common Intervals:**
|
||||
- Monthly (most common for SaaS)
|
||||
- Annual (discounted long-term)
|
||||
- Quarterly
|
||||
- Weekly
|
||||
- Custom (usage-based, per-seat)
|
||||
|
||||
### 2. Subscription States
|
||||
```
|
||||
trial → active → past_due → canceled
|
||||
→ paused → resumed
|
||||
```
|
||||
|
||||
### 3. Dunning Management
|
||||
Automated process to recover failed payments through:
|
||||
- Retry schedules
|
||||
- Customer notifications
|
||||
- Grace periods
|
||||
- Account restrictions
|
||||
|
||||
### 4. Proration
|
||||
Adjusting charges when:
|
||||
- Upgrading/downgrading mid-cycle
|
||||
- Adding/removing seats
|
||||
- Changing billing frequency
|
||||
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
from billing import BillingEngine, Subscription
|
||||
|
||||
# Initialize billing engine
|
||||
billing = BillingEngine()
|
||||
|
||||
# Create subscription
|
||||
subscription = billing.create_subscription(
|
||||
customer_id="cus_123",
|
||||
plan_id="plan_pro_monthly",
|
||||
billing_cycle_anchor=datetime.now(),
|
||||
trial_days=14
|
||||
)
|
||||
|
||||
# Process billing cycle
|
||||
billing.process_billing_cycle(subscription.id)
|
||||
```
|
||||
|
||||
## Subscription Lifecycle Management
|
||||
|
||||
```python
|
||||
from datetime import datetime, timedelta
|
||||
from enum import Enum
|
||||
|
||||
class SubscriptionStatus(Enum):
|
||||
TRIAL = "trial"
|
||||
ACTIVE = "active"
|
||||
PAST_DUE = "past_due"
|
||||
CANCELED = "canceled"
|
||||
PAUSED = "paused"
|
||||
|
||||
class Subscription:
|
||||
def __init__(self, customer_id, plan, billing_cycle_day=None):
|
||||
self.id = generate_id()
|
||||
self.customer_id = customer_id
|
||||
self.plan = plan
|
||||
self.status = SubscriptionStatus.TRIAL
|
||||
self.current_period_start = datetime.now()
|
||||
self.current_period_end = self.current_period_start + timedelta(days=plan.trial_days or 30)
|
||||
self.billing_cycle_day = billing_cycle_day or self.current_period_start.day
|
||||
self.trial_end = datetime.now() + timedelta(days=plan.trial_days) if plan.trial_days else None
|
||||
|
||||
def start_trial(self, trial_days):
|
||||
"""Start trial period."""
|
||||
self.status = SubscriptionStatus.TRIAL
|
||||
self.trial_end = datetime.now() + timedelta(days=trial_days)
|
||||
self.current_period_end = self.trial_end
|
||||
|
||||
def activate(self):
|
||||
"""Activate subscription after trial or immediately."""
|
||||
self.status = SubscriptionStatus.ACTIVE
|
||||
self.current_period_start = datetime.now()
|
||||
self.current_period_end = self.calculate_next_billing_date()
|
||||
|
||||
def mark_past_due(self):
|
||||
"""Mark subscription as past due after failed payment."""
|
||||
self.status = SubscriptionStatus.PAST_DUE
|
||||
# Trigger dunning workflow
|
||||
|
||||
def cancel(self, at_period_end=True):
|
||||
"""Cancel subscription."""
|
||||
if at_period_end:
|
||||
self.cancel_at_period_end = True
|
||||
# Will cancel when current period ends
|
||||
else:
|
||||
self.status = SubscriptionStatus.CANCELED
|
||||
self.canceled_at = datetime.now()
|
||||
|
||||
def calculate_next_billing_date(self):
|
||||
"""Calculate next billing date based on interval."""
|
||||
if self.plan.interval == 'month':
|
||||
return self.current_period_start + timedelta(days=30)
|
||||
elif self.plan.interval == 'year':
|
||||
return self.current_period_start + timedelta(days=365)
|
||||
elif self.plan.interval == 'week':
|
||||
return self.current_period_start + timedelta(days=7)
|
||||
```
|
||||
|
||||
## Billing Cycle Processing
|
||||
|
||||
```python
|
||||
class BillingEngine:
|
||||
def process_billing_cycle(self, subscription_id):
|
||||
"""Process billing for a subscription."""
|
||||
subscription = self.get_subscription(subscription_id)
|
||||
|
||||
# Check if billing is due
|
||||
if datetime.now() < subscription.current_period_end:
|
||||
return
|
||||
|
||||
# Generate invoice
|
||||
invoice = self.generate_invoice(subscription)
|
||||
|
||||
# Attempt payment
|
||||
payment_result = self.charge_customer(
|
||||
subscription.customer_id,
|
||||
invoice.total
|
||||
)
|
||||
|
||||
if payment_result.success:
|
||||
# Payment successful
|
||||
invoice.mark_paid()
|
||||
subscription.advance_billing_period()
|
||||
self.send_invoice(invoice)
|
||||
else:
|
||||
# Payment failed
|
||||
subscription.mark_past_due()
|
||||
self.start_dunning_process(subscription, invoice)
|
||||
|
||||
def generate_invoice(self, subscription):
|
||||
"""Generate invoice for billing period."""
|
||||
invoice = Invoice(
|
||||
customer_id=subscription.customer_id,
|
||||
subscription_id=subscription.id,
|
||||
period_start=subscription.current_period_start,
|
||||
period_end=subscription.current_period_end
|
||||
)
|
||||
|
||||
# Add subscription line item
|
||||
invoice.add_line_item(
|
||||
description=subscription.plan.name,
|
||||
amount=subscription.plan.amount,
|
||||
quantity=subscription.quantity or 1
|
||||
)
|
||||
|
||||
# Add usage-based charges if applicable
|
||||
if subscription.has_usage_billing:
|
||||
usage_charges = self.calculate_usage_charges(subscription)
|
||||
invoice.add_line_item(
|
||||
description="Usage charges",
|
||||
amount=usage_charges
|
||||
)
|
||||
|
||||
# Calculate tax
|
||||
tax = self.calculate_tax(invoice.subtotal, subscription.customer)
|
||||
invoice.tax = tax
|
||||
|
||||
invoice.finalize()
|
||||
return invoice
|
||||
|
||||
def charge_customer(self, customer_id, amount):
|
||||
"""Charge customer using saved payment method."""
|
||||
customer = self.get_customer(customer_id)
|
||||
|
||||
try:
|
||||
# Charge using payment processor
|
||||
charge = stripe.Charge.create(
|
||||
customer=customer.stripe_id,
|
||||
amount=int(amount * 100), # Convert to cents
|
||||
currency='usd'
|
||||
)
|
||||
|
||||
return PaymentResult(success=True, transaction_id=charge.id)
|
||||
except stripe.error.CardError as e:
|
||||
return PaymentResult(success=False, error=str(e))
|
||||
```
|
||||
|
||||
## Dunning Management
|
||||
|
||||
```python
|
||||
class DunningManager:
|
||||
"""Manage failed payment recovery."""
|
||||
|
||||
def __init__(self):
|
||||
self.retry_schedule = [
|
||||
{'days': 3, 'email_template': 'payment_failed_first'},
|
||||
{'days': 7, 'email_template': 'payment_failed_reminder'},
|
||||
{'days': 14, 'email_template': 'payment_failed_final'}
|
||||
]
|
||||
|
||||
def start_dunning_process(self, subscription, invoice):
|
||||
"""Start dunning process for failed payment."""
|
||||
dunning_attempt = DunningAttempt(
|
||||
subscription_id=subscription.id,
|
||||
invoice_id=invoice.id,
|
||||
attempt_number=1,
|
||||
next_retry=datetime.now() + timedelta(days=3)
|
||||
)
|
||||
|
||||
# Send initial failure notification
|
||||
self.send_dunning_email(subscription, 'payment_failed_first')
|
||||
|
||||
# Schedule retries
|
||||
self.schedule_retries(dunning_attempt)
|
||||
|
||||
def retry_payment(self, dunning_attempt):
|
||||
"""Retry failed payment."""
|
||||
subscription = self.get_subscription(dunning_attempt.subscription_id)
|
||||
invoice = self.get_invoice(dunning_attempt.invoice_id)
|
||||
|
||||
# Attempt payment again
|
||||
result = self.charge_customer(subscription.customer_id, invoice.total)
|
||||
|
||||
if result.success:
|
||||
# Payment succeeded
|
||||
invoice.mark_paid()
|
||||
subscription.status = SubscriptionStatus.ACTIVE
|
||||
self.send_dunning_email(subscription, 'payment_recovered')
|
||||
dunning_attempt.mark_resolved()
|
||||
else:
|
||||
# Still failing
|
||||
dunning_attempt.attempt_number += 1
|
||||
|
||||
if dunning_attempt.attempt_number < len(self.retry_schedule):
|
||||
# Schedule next retry
|
||||
next_retry_config = self.retry_schedule[dunning_attempt.attempt_number]
|
||||
dunning_attempt.next_retry = datetime.now() + timedelta(days=next_retry_config['days'])
|
||||
self.send_dunning_email(subscription, next_retry_config['email_template'])
|
||||
else:
|
||||
# Exhausted retries, cancel subscription
|
||||
subscription.cancel(at_period_end=False)
|
||||
self.send_dunning_email(subscription, 'subscription_canceled')
|
||||
|
||||
def send_dunning_email(self, subscription, template):
|
||||
"""Send dunning notification to customer."""
|
||||
customer = self.get_customer(subscription.customer_id)
|
||||
|
||||
email_content = self.render_template(template, {
|
||||
'customer_name': customer.name,
|
||||
'amount_due': subscription.plan.amount,
|
||||
'update_payment_url': f"https://app.example.com/billing"
|
||||
})
|
||||
|
||||
send_email(
|
||||
to=customer.email,
|
||||
subject=email_content['subject'],
|
||||
body=email_content['body']
|
||||
)
|
||||
```
|
||||
|
||||
## Proration
|
||||
|
||||
```python
|
||||
class ProrationCalculator:
|
||||
"""Calculate prorated charges for plan changes."""
|
||||
|
||||
@staticmethod
|
||||
def calculate_proration(old_plan, new_plan, period_start, period_end, change_date):
|
||||
"""Calculate proration for plan change."""
|
||||
# Days in current period
|
||||
total_days = (period_end - period_start).days
|
||||
|
||||
# Days used on old plan
|
||||
days_used = (change_date - period_start).days
|
||||
|
||||
# Days remaining on new plan
|
||||
days_remaining = (period_end - change_date).days
|
||||
|
||||
# Calculate prorated amounts
|
||||
unused_amount = (old_plan.amount / total_days) * days_remaining
|
||||
new_plan_amount = (new_plan.amount / total_days) * days_remaining
|
||||
|
||||
# Net charge/credit
|
||||
proration = new_plan_amount - unused_amount
|
||||
|
||||
return {
|
||||
'old_plan_credit': -unused_amount,
|
||||
'new_plan_charge': new_plan_amount,
|
||||
'net_proration': proration,
|
||||
'days_used': days_used,
|
||||
'days_remaining': days_remaining
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def calculate_seat_proration(current_seats, new_seats, price_per_seat, period_start, period_end, change_date):
|
||||
"""Calculate proration for seat changes."""
|
||||
total_days = (period_end - period_start).days
|
||||
days_remaining = (period_end - change_date).days
|
||||
|
||||
# Additional seats charge
|
||||
additional_seats = new_seats - current_seats
|
||||
prorated_amount = (additional_seats * price_per_seat / total_days) * days_remaining
|
||||
|
||||
return {
|
||||
'additional_seats': additional_seats,
|
||||
'prorated_charge': max(0, prorated_amount), # No refund for removing seats mid-cycle
|
||||
'effective_date': change_date
|
||||
}
|
||||
```
|
||||
|
||||
## Tax Calculation
|
||||
|
||||
```python
|
||||
class TaxCalculator:
|
||||
"""Calculate sales tax, VAT, GST."""
|
||||
|
||||
def __init__(self):
|
||||
# Tax rates by region
|
||||
self.tax_rates = {
|
||||
'US_CA': 0.0725, # California sales tax
|
||||
'US_NY': 0.04, # New York sales tax
|
||||
'GB': 0.20, # UK VAT
|
||||
'DE': 0.19, # Germany VAT
|
||||
'FR': 0.20, # France VAT
|
||||
'AU': 0.10, # Australia GST
|
||||
}
|
||||
|
||||
def calculate_tax(self, amount, customer):
|
||||
"""Calculate applicable tax."""
|
||||
# Determine tax jurisdiction
|
||||
jurisdiction = self.get_tax_jurisdiction(customer)
|
||||
|
||||
if not jurisdiction:
|
||||
return 0
|
||||
|
||||
# Get tax rate
|
||||
tax_rate = self.tax_rates.get(jurisdiction, 0)
|
||||
|
||||
# Calculate tax
|
||||
tax = amount * tax_rate
|
||||
|
||||
return {
|
||||
'tax_amount': tax,
|
||||
'tax_rate': tax_rate,
|
||||
'jurisdiction': jurisdiction,
|
||||
'tax_type': self.get_tax_type(jurisdiction)
|
||||
}
|
||||
|
||||
def get_tax_jurisdiction(self, customer):
|
||||
"""Determine tax jurisdiction based on customer location."""
|
||||
if customer.country == 'US':
|
||||
# US: Tax based on customer state
|
||||
return f"US_{customer.state}"
|
||||
elif customer.country in ['GB', 'DE', 'FR']:
|
||||
# EU: VAT
|
||||
return customer.country
|
||||
elif customer.country == 'AU':
|
||||
# Australia: GST
|
||||
return 'AU'
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_tax_type(self, jurisdiction):
|
||||
"""Get type of tax for jurisdiction."""
|
||||
if jurisdiction.startswith('US_'):
|
||||
return 'Sales Tax'
|
||||
elif jurisdiction in ['GB', 'DE', 'FR']:
|
||||
return 'VAT'
|
||||
elif jurisdiction == 'AU':
|
||||
return 'GST'
|
||||
return 'Tax'
|
||||
|
||||
def validate_vat_number(self, vat_number, country):
|
||||
"""Validate EU VAT number."""
|
||||
# Use VIES API for validation
|
||||
# Returns True if valid, False otherwise
|
||||
pass
|
||||
```
|
||||
|
||||
## Invoice Generation
|
||||
|
||||
```python
|
||||
class Invoice:
|
||||
def __init__(self, customer_id, subscription_id=None):
|
||||
self.id = generate_invoice_number()
|
||||
self.customer_id = customer_id
|
||||
self.subscription_id = subscription_id
|
||||
self.status = 'draft'
|
||||
self.line_items = []
|
||||
self.subtotal = 0
|
||||
self.tax = 0
|
||||
self.total = 0
|
||||
self.created_at = datetime.now()
|
||||
|
||||
def add_line_item(self, description, amount, quantity=1):
|
||||
"""Add line item to invoice."""
|
||||
line_item = {
|
||||
'description': description,
|
||||
'unit_amount': amount,
|
||||
'quantity': quantity,
|
||||
'total': amount * quantity
|
||||
}
|
||||
self.line_items.append(line_item)
|
||||
self.subtotal += line_item['total']
|
||||
|
||||
def finalize(self):
|
||||
"""Finalize invoice and calculate total."""
|
||||
self.total = self.subtotal + self.tax
|
||||
self.status = 'open'
|
||||
self.finalized_at = datetime.now()
|
||||
|
||||
def mark_paid(self):
|
||||
"""Mark invoice as paid."""
|
||||
self.status = 'paid'
|
||||
self.paid_at = datetime.now()
|
||||
|
||||
def to_pdf(self):
|
||||
"""Generate PDF invoice."""
|
||||
from reportlab.pdfgen import canvas
|
||||
|
||||
# Generate PDF
|
||||
# Include: company info, customer info, line items, tax, total
|
||||
pass
|
||||
|
||||
def to_html(self):
|
||||
"""Generate HTML invoice."""
|
||||
template = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Invoice #{invoice_number}</title></head>
|
||||
<body>
|
||||
<h1>Invoice #{invoice_number}</h1>
|
||||
<p>Date: {date}</p>
|
||||
<h2>Bill To:</h2>
|
||||
<p>{customer_name}<br>{customer_address}</p>
|
||||
<table>
|
||||
<tr><th>Description</th><th>Quantity</th><th>Amount</th></tr>
|
||||
{line_items}
|
||||
</table>
|
||||
<p>Subtotal: ${subtotal}</p>
|
||||
<p>Tax: ${tax}</p>
|
||||
<h3>Total: ${total}</h3>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
return template.format(
|
||||
invoice_number=self.id,
|
||||
date=self.created_at.strftime('%Y-%m-%d'),
|
||||
customer_name=self.customer.name,
|
||||
customer_address=self.customer.address,
|
||||
line_items=self.render_line_items(),
|
||||
subtotal=self.subtotal,
|
||||
tax=self.tax,
|
||||
total=self.total
|
||||
)
|
||||
```
|
||||
|
||||
## Usage-Based Billing
|
||||
|
||||
```python
|
||||
class UsageBillingEngine:
|
||||
"""Track and bill for usage."""
|
||||
|
||||
def track_usage(self, customer_id, metric, quantity):
|
||||
"""Track usage event."""
|
||||
UsageRecord.create(
|
||||
customer_id=customer_id,
|
||||
metric=metric,
|
||||
quantity=quantity,
|
||||
timestamp=datetime.now()
|
||||
)
|
||||
|
||||
def calculate_usage_charges(self, subscription, period_start, period_end):
|
||||
"""Calculate charges for usage in billing period."""
|
||||
usage_records = UsageRecord.get_for_period(
|
||||
subscription.customer_id,
|
||||
period_start,
|
||||
period_end
|
||||
)
|
||||
|
||||
total_usage = sum(record.quantity for record in usage_records)
|
||||
|
||||
# Tiered pricing
|
||||
if subscription.plan.pricing_model == 'tiered':
|
||||
charge = self.calculate_tiered_pricing(total_usage, subscription.plan.tiers)
|
||||
# Per-unit pricing
|
||||
elif subscription.plan.pricing_model == 'per_unit':
|
||||
charge = total_usage * subscription.plan.unit_price
|
||||
# Volume pricing
|
||||
elif subscription.plan.pricing_model == 'volume':
|
||||
charge = self.calculate_volume_pricing(total_usage, subscription.plan.tiers)
|
||||
|
||||
return charge
|
||||
|
||||
def calculate_tiered_pricing(self, total_usage, tiers):
|
||||
"""Calculate cost using tiered pricing."""
|
||||
charge = 0
|
||||
remaining = total_usage
|
||||
|
||||
for tier in sorted(tiers, key=lambda x: x['up_to']):
|
||||
tier_usage = min(remaining, tier['up_to'] - tier['from'])
|
||||
charge += tier_usage * tier['unit_price']
|
||||
remaining -= tier_usage
|
||||
|
||||
if remaining <= 0:
|
||||
break
|
||||
|
||||
return charge
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- **references/billing-cycles.md**: Billing cycle management
|
||||
- **references/dunning-management.md**: Failed payment recovery
|
||||
- **references/proration.md**: Prorated charge calculations
|
||||
- **references/tax-calculation.md**: Tax/VAT/GST handling
|
||||
- **references/invoice-lifecycle.md**: Invoice state management
|
||||
- **assets/billing-state-machine.yaml**: Billing workflow
|
||||
- **assets/invoice-template.html**: Invoice templates
|
||||
- **assets/dunning-policy.yaml**: Dunning configuration
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Automate Everything**: Minimize manual intervention
|
||||
2. **Clear Communication**: Notify customers of billing events
|
||||
3. **Flexible Retry Logic**: Balance recovery with customer experience
|
||||
4. **Accurate Proration**: Fair calculation for plan changes
|
||||
5. **Tax Compliance**: Calculate correct tax for jurisdiction
|
||||
6. **Audit Trail**: Log all billing events
|
||||
7. **Graceful Degradation**: Handle edge cases without breaking
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Incorrect Proration**: Not accounting for partial periods
|
||||
- **Missing Tax**: Forgetting to add tax to invoices
|
||||
- **Aggressive Dunning**: Canceling too quickly
|
||||
- **No Notifications**: Not informing customers of failures
|
||||
- **Hardcoded Cycles**: Not supporting custom billing dates
|
||||
467
skills/paypal-integration/SKILL.md
Normal file
467
skills/paypal-integration/SKILL.md
Normal file
@@ -0,0 +1,467 @@
|
||||
---
|
||||
name: paypal-integration
|
||||
description: Integrate PayPal payment processing with support for express checkout, subscriptions, and refund management. Use when implementing PayPal payments, processing online transactions, or building e-commerce checkout flows.
|
||||
---
|
||||
|
||||
# PayPal Integration
|
||||
|
||||
Master PayPal payment integration including Express Checkout, IPN handling, recurring billing, and refund workflows.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Integrating PayPal as a payment option
|
||||
- Implementing express checkout flows
|
||||
- Setting up recurring billing with PayPal
|
||||
- Processing refunds and payment disputes
|
||||
- Handling PayPal webhooks (IPN)
|
||||
- Supporting international payments
|
||||
- Implementing PayPal subscriptions
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### 1. Payment Products
|
||||
**PayPal Checkout**
|
||||
- One-time payments
|
||||
- Express checkout experience
|
||||
- Guest and PayPal account payments
|
||||
|
||||
**PayPal Subscriptions**
|
||||
- Recurring billing
|
||||
- Subscription plans
|
||||
- Automatic renewals
|
||||
|
||||
**PayPal Payouts**
|
||||
- Send money to multiple recipients
|
||||
- Marketplace and platform payments
|
||||
|
||||
### 2. Integration Methods
|
||||
**Client-Side (JavaScript SDK)**
|
||||
- Smart Payment Buttons
|
||||
- Hosted payment flow
|
||||
- Minimal backend code
|
||||
|
||||
**Server-Side (REST API)**
|
||||
- Full control over payment flow
|
||||
- Custom checkout UI
|
||||
- Advanced features
|
||||
|
||||
### 3. IPN (Instant Payment Notification)
|
||||
- Webhook-like payment notifications
|
||||
- Asynchronous payment updates
|
||||
- Verification required
|
||||
|
||||
## Quick Start
|
||||
|
||||
```javascript
|
||||
// Frontend - PayPal Smart Buttons
|
||||
<div id="paypal-button-container"></div>
|
||||
|
||||
<script src="https://www.paypal.com/sdk/js?client-id=YOUR_CLIENT_ID¤cy=USD"></script>
|
||||
<script>
|
||||
paypal.Buttons({
|
||||
createOrder: function(data, actions) {
|
||||
return actions.order.create({
|
||||
purchase_units: [{
|
||||
amount: {
|
||||
value: '25.00'
|
||||
}
|
||||
}]
|
||||
});
|
||||
},
|
||||
onApprove: function(data, actions) {
|
||||
return actions.order.capture().then(function(details) {
|
||||
// Payment successful
|
||||
console.log('Transaction completed by ' + details.payer.name.given_name);
|
||||
|
||||
// Send to backend for verification
|
||||
fetch('/api/paypal/capture', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({orderID: data.orderID})
|
||||
});
|
||||
});
|
||||
}
|
||||
}).render('#paypal-button-container');
|
||||
</script>
|
||||
```
|
||||
|
||||
```python
|
||||
# Backend - Verify and capture order
|
||||
from paypalrestsdk import Payment
|
||||
import paypalrestsdk
|
||||
|
||||
paypalrestsdk.configure({
|
||||
"mode": "sandbox", # or "live"
|
||||
"client_id": "YOUR_CLIENT_ID",
|
||||
"client_secret": "YOUR_CLIENT_SECRET"
|
||||
})
|
||||
|
||||
def capture_paypal_order(order_id):
|
||||
"""Capture a PayPal order."""
|
||||
payment = Payment.find(order_id)
|
||||
|
||||
if payment.execute({"payer_id": payment.payer.payer_info.payer_id}):
|
||||
# Payment successful
|
||||
return {
|
||||
'status': 'success',
|
||||
'transaction_id': payment.id,
|
||||
'amount': payment.transactions[0].amount.total
|
||||
}
|
||||
else:
|
||||
# Payment failed
|
||||
return {
|
||||
'status': 'failed',
|
||||
'error': payment.error
|
||||
}
|
||||
```
|
||||
|
||||
## Express Checkout Implementation
|
||||
|
||||
### Server-Side Order Creation
|
||||
```python
|
||||
import requests
|
||||
import json
|
||||
|
||||
class PayPalClient:
|
||||
def __init__(self, client_id, client_secret, mode='sandbox'):
|
||||
self.client_id = client_id
|
||||
self.client_secret = client_secret
|
||||
self.base_url = 'https://api-m.sandbox.paypal.com' if mode == 'sandbox' else 'https://api-m.paypal.com'
|
||||
self.access_token = self.get_access_token()
|
||||
|
||||
def get_access_token(self):
|
||||
"""Get OAuth access token."""
|
||||
url = f"{self.base_url}/v1/oauth2/token"
|
||||
headers = {"Accept": "application/json", "Accept-Language": "en_US"}
|
||||
|
||||
response = requests.post(
|
||||
url,
|
||||
headers=headers,
|
||||
data={"grant_type": "client_credentials"},
|
||||
auth=(self.client_id, self.client_secret)
|
||||
)
|
||||
|
||||
return response.json()['access_token']
|
||||
|
||||
def create_order(self, amount, currency='USD'):
|
||||
"""Create a PayPal order."""
|
||||
url = f"{self.base_url}/v2/checkout/orders"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {self.access_token}"
|
||||
}
|
||||
|
||||
payload = {
|
||||
"intent": "CAPTURE",
|
||||
"purchase_units": [{
|
||||
"amount": {
|
||||
"currency_code": currency,
|
||||
"value": str(amount)
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
return response.json()
|
||||
|
||||
def capture_order(self, order_id):
|
||||
"""Capture payment for an order."""
|
||||
url = f"{self.base_url}/v2/checkout/orders/{order_id}/capture"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {self.access_token}"
|
||||
}
|
||||
|
||||
response = requests.post(url, headers=headers)
|
||||
return response.json()
|
||||
|
||||
def get_order_details(self, order_id):
|
||||
"""Get order details."""
|
||||
url = f"{self.base_url}/v2/checkout/orders/{order_id}"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.access_token}"
|
||||
}
|
||||
|
||||
response = requests.get(url, headers=headers)
|
||||
return response.json()
|
||||
```
|
||||
|
||||
## IPN (Instant Payment Notification) Handling
|
||||
|
||||
### IPN Verification and Processing
|
||||
```python
|
||||
from flask import Flask, request
|
||||
import requests
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/ipn', methods=['POST'])
|
||||
def handle_ipn():
|
||||
"""Handle PayPal IPN notifications."""
|
||||
# Get IPN message
|
||||
ipn_data = request.form.to_dict()
|
||||
|
||||
# Verify IPN with PayPal
|
||||
if not verify_ipn(ipn_data):
|
||||
return 'IPN verification failed', 400
|
||||
|
||||
# Process IPN based on transaction type
|
||||
payment_status = ipn_data.get('payment_status')
|
||||
txn_type = ipn_data.get('txn_type')
|
||||
|
||||
if payment_status == 'Completed':
|
||||
handle_payment_completed(ipn_data)
|
||||
elif payment_status == 'Refunded':
|
||||
handle_refund(ipn_data)
|
||||
elif payment_status == 'Reversed':
|
||||
handle_chargeback(ipn_data)
|
||||
|
||||
return 'IPN processed', 200
|
||||
|
||||
def verify_ipn(ipn_data):
|
||||
"""Verify IPN message authenticity."""
|
||||
# Add 'cmd' parameter
|
||||
verify_data = ipn_data.copy()
|
||||
verify_data['cmd'] = '_notify-validate'
|
||||
|
||||
# Send back to PayPal for verification
|
||||
paypal_url = 'https://ipnpb.sandbox.paypal.com/cgi-bin/webscr' # or production URL
|
||||
|
||||
response = requests.post(paypal_url, data=verify_data)
|
||||
|
||||
return response.text == 'VERIFIED'
|
||||
|
||||
def handle_payment_completed(ipn_data):
|
||||
"""Process completed payment."""
|
||||
txn_id = ipn_data.get('txn_id')
|
||||
payer_email = ipn_data.get('payer_email')
|
||||
mc_gross = ipn_data.get('mc_gross')
|
||||
item_name = ipn_data.get('item_name')
|
||||
|
||||
# Check if already processed (prevent duplicates)
|
||||
if is_transaction_processed(txn_id):
|
||||
return
|
||||
|
||||
# Update database
|
||||
# Send confirmation email
|
||||
# Fulfill order
|
||||
print(f"Payment completed: {txn_id}, Amount: ${mc_gross}")
|
||||
|
||||
def handle_refund(ipn_data):
|
||||
"""Handle refund."""
|
||||
parent_txn_id = ipn_data.get('parent_txn_id')
|
||||
mc_gross = ipn_data.get('mc_gross')
|
||||
|
||||
# Process refund in your system
|
||||
print(f"Refund processed: {parent_txn_id}, Amount: ${mc_gross}")
|
||||
|
||||
def handle_chargeback(ipn_data):
|
||||
"""Handle payment reversal/chargeback."""
|
||||
txn_id = ipn_data.get('txn_id')
|
||||
reason_code = ipn_data.get('reason_code')
|
||||
|
||||
# Handle chargeback
|
||||
print(f"Chargeback: {txn_id}, Reason: {reason_code}")
|
||||
```
|
||||
|
||||
## Subscription/Recurring Billing
|
||||
|
||||
### Create Subscription Plan
|
||||
```python
|
||||
def create_subscription_plan(name, amount, interval='MONTH'):
|
||||
"""Create a subscription plan."""
|
||||
client = PayPalClient(CLIENT_ID, CLIENT_SECRET)
|
||||
|
||||
url = f"{client.base_url}/v1/billing/plans"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {client.access_token}"
|
||||
}
|
||||
|
||||
payload = {
|
||||
"product_id": "PRODUCT_ID", # Create product first
|
||||
"name": name,
|
||||
"billing_cycles": [{
|
||||
"frequency": {
|
||||
"interval_unit": interval,
|
||||
"interval_count": 1
|
||||
},
|
||||
"tenure_type": "REGULAR",
|
||||
"sequence": 1,
|
||||
"total_cycles": 0, # Infinite
|
||||
"pricing_scheme": {
|
||||
"fixed_price": {
|
||||
"value": str(amount),
|
||||
"currency_code": "USD"
|
||||
}
|
||||
}
|
||||
}],
|
||||
"payment_preferences": {
|
||||
"auto_bill_outstanding": True,
|
||||
"setup_fee": {
|
||||
"value": "0",
|
||||
"currency_code": "USD"
|
||||
},
|
||||
"setup_fee_failure_action": "CONTINUE",
|
||||
"payment_failure_threshold": 3
|
||||
}
|
||||
}
|
||||
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
return response.json()
|
||||
|
||||
def create_subscription(plan_id, subscriber_email):
|
||||
"""Create a subscription for a customer."""
|
||||
client = PayPalClient(CLIENT_ID, CLIENT_SECRET)
|
||||
|
||||
url = f"{client.base_url}/v1/billing/subscriptions"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {client.access_token}"
|
||||
}
|
||||
|
||||
payload = {
|
||||
"plan_id": plan_id,
|
||||
"subscriber": {
|
||||
"email_address": subscriber_email
|
||||
},
|
||||
"application_context": {
|
||||
"return_url": "https://yourdomain.com/subscription/success",
|
||||
"cancel_url": "https://yourdomain.com/subscription/cancel"
|
||||
}
|
||||
}
|
||||
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
subscription = response.json()
|
||||
|
||||
# Get approval URL
|
||||
for link in subscription.get('links', []):
|
||||
if link['rel'] == 'approve':
|
||||
return {
|
||||
'subscription_id': subscription['id'],
|
||||
'approval_url': link['href']
|
||||
}
|
||||
```
|
||||
|
||||
## Refund Workflows
|
||||
|
||||
```python
|
||||
def create_refund(capture_id, amount=None, note=None):
|
||||
"""Create a refund for a captured payment."""
|
||||
client = PayPalClient(CLIENT_ID, CLIENT_SECRET)
|
||||
|
||||
url = f"{client.base_url}/v2/payments/captures/{capture_id}/refund"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {client.access_token}"
|
||||
}
|
||||
|
||||
payload = {}
|
||||
if amount:
|
||||
payload["amount"] = {
|
||||
"value": str(amount),
|
||||
"currency_code": "USD"
|
||||
}
|
||||
|
||||
if note:
|
||||
payload["note_to_payer"] = note
|
||||
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
return response.json()
|
||||
|
||||
def get_refund_details(refund_id):
|
||||
"""Get refund details."""
|
||||
client = PayPalClient(CLIENT_ID, CLIENT_SECRET)
|
||||
|
||||
url = f"{client.base_url}/v2/payments/refunds/{refund_id}"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {client.access_token}"
|
||||
}
|
||||
|
||||
response = requests.get(url, headers=headers)
|
||||
return response.json()
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```python
|
||||
class PayPalError(Exception):
|
||||
"""Custom PayPal error."""
|
||||
pass
|
||||
|
||||
def handle_paypal_api_call(api_function):
|
||||
"""Wrapper for PayPal API calls with error handling."""
|
||||
try:
|
||||
result = api_function()
|
||||
return result
|
||||
except requests.exceptions.RequestException as e:
|
||||
# Network error
|
||||
raise PayPalError(f"Network error: {str(e)}")
|
||||
except Exception as e:
|
||||
# Other errors
|
||||
raise PayPalError(f"PayPal API error: {str(e)}")
|
||||
|
||||
# Usage
|
||||
try:
|
||||
order = handle_paypal_api_call(lambda: client.create_order(25.00))
|
||||
except PayPalError as e:
|
||||
# Handle error appropriately
|
||||
log_error(e)
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```python
|
||||
# Use sandbox credentials
|
||||
SANDBOX_CLIENT_ID = "..."
|
||||
SANDBOX_SECRET = "..."
|
||||
|
||||
# Test accounts
|
||||
# Create test buyer and seller accounts at developer.paypal.com
|
||||
|
||||
def test_payment_flow():
|
||||
"""Test complete payment flow."""
|
||||
client = PayPalClient(SANDBOX_CLIENT_ID, SANDBOX_SECRET, mode='sandbox')
|
||||
|
||||
# Create order
|
||||
order = client.create_order(10.00)
|
||||
assert 'id' in order
|
||||
|
||||
# Get approval URL
|
||||
approval_url = next((link['href'] for link in order['links'] if link['rel'] == 'approve'), None)
|
||||
assert approval_url is not None
|
||||
|
||||
# After approval (manual step with test account)
|
||||
# Capture order
|
||||
# captured = client.capture_order(order['id'])
|
||||
# assert captured['status'] == 'COMPLETED'
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- **references/express-checkout.md**: Express Checkout implementation guide
|
||||
- **references/ipn-handling.md**: IPN verification and processing
|
||||
- **references/refund-workflows.md**: Refund handling patterns
|
||||
- **references/billing-agreements.md**: Recurring billing setup
|
||||
- **assets/paypal-client.py**: Production PayPal client
|
||||
- **assets/ipn-processor.py**: IPN webhook processor
|
||||
- **assets/recurring-billing.py**: Subscription management
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always Verify IPN**: Never trust IPN without verification
|
||||
2. **Idempotent Processing**: Handle duplicate IPN notifications
|
||||
3. **Error Handling**: Implement robust error handling
|
||||
4. **Logging**: Log all transactions and errors
|
||||
5. **Test Thoroughly**: Use sandbox extensively
|
||||
6. **Webhook Backup**: Don't rely solely on client-side callbacks
|
||||
7. **Currency Handling**: Always specify currency explicitly
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Not Verifying IPN**: Accepting IPN without verification
|
||||
- **Duplicate Processing**: Not checking for duplicate transactions
|
||||
- **Wrong Environment**: Mixing sandbox and production URLs/credentials
|
||||
- **Missing Webhooks**: Not handling all payment states
|
||||
- **Hardcoded Values**: Not making configurable for different environments
|
||||
466
skills/pci-compliance/SKILL.md
Normal file
466
skills/pci-compliance/SKILL.md
Normal file
@@ -0,0 +1,466 @@
|
||||
---
|
||||
name: pci-compliance
|
||||
description: Implement PCI DSS compliance requirements for secure handling of payment card data and payment systems. Use when securing payment processing, achieving PCI compliance, or implementing payment card security measures.
|
||||
---
|
||||
|
||||
# PCI Compliance
|
||||
|
||||
Master PCI DSS (Payment Card Industry Data Security Standard) compliance for secure payment processing and handling of cardholder data.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Building payment processing systems
|
||||
- Handling credit card information
|
||||
- Implementing secure payment flows
|
||||
- Conducting PCI compliance audits
|
||||
- Reducing PCI compliance scope
|
||||
- Implementing tokenization and encryption
|
||||
- Preparing for PCI DSS assessments
|
||||
|
||||
## PCI DSS Requirements (12 Core Requirements)
|
||||
|
||||
### Build and Maintain Secure Network
|
||||
1. Install and maintain firewall configuration
|
||||
2. Don't use vendor-supplied defaults for passwords
|
||||
|
||||
### Protect Cardholder Data
|
||||
3. Protect stored cardholder data
|
||||
4. Encrypt transmission of cardholder data across public networks
|
||||
|
||||
### Maintain Vulnerability Management
|
||||
5. Protect systems against malware
|
||||
6. Develop and maintain secure systems and applications
|
||||
|
||||
### Implement Strong Access Control
|
||||
7. Restrict access to cardholder data by business need-to-know
|
||||
8. Identify and authenticate access to system components
|
||||
9. Restrict physical access to cardholder data
|
||||
|
||||
### Monitor and Test Networks
|
||||
10. Track and monitor all access to network resources and cardholder data
|
||||
11. Regularly test security systems and processes
|
||||
|
||||
### Maintain Information Security Policy
|
||||
12. Maintain a policy that addresses information security
|
||||
|
||||
## Compliance Levels
|
||||
|
||||
**Level 1**: > 6 million transactions/year (annual ROC required)
|
||||
**Level 2**: 1-6 million transactions/year (annual SAQ)
|
||||
**Level 3**: 20,000-1 million e-commerce transactions/year
|
||||
**Level 4**: < 20,000 e-commerce or < 1 million total transactions
|
||||
|
||||
## Data Minimization (Never Store)
|
||||
|
||||
```python
|
||||
# NEVER STORE THESE
|
||||
PROHIBITED_DATA = {
|
||||
'full_track_data': 'Magnetic stripe data',
|
||||
'cvv': 'Card verification code/value',
|
||||
'pin': 'PIN or PIN block'
|
||||
}
|
||||
|
||||
# CAN STORE (if encrypted)
|
||||
ALLOWED_DATA = {
|
||||
'pan': 'Primary Account Number (card number)',
|
||||
'cardholder_name': 'Name on card',
|
||||
'expiration_date': 'Card expiration',
|
||||
'service_code': 'Service code'
|
||||
}
|
||||
|
||||
class PaymentData:
|
||||
"""Safe payment data handling."""
|
||||
|
||||
def __init__(self):
|
||||
self.prohibited_fields = ['cvv', 'cvv2', 'cvc', 'pin']
|
||||
|
||||
def sanitize_log(self, data):
|
||||
"""Remove sensitive data from logs."""
|
||||
sanitized = data.copy()
|
||||
|
||||
# Mask PAN
|
||||
if 'card_number' in sanitized:
|
||||
card = sanitized['card_number']
|
||||
sanitized['card_number'] = f"{card[:6]}{'*' * (len(card) - 10)}{card[-4:]}"
|
||||
|
||||
# Remove prohibited data
|
||||
for field in self.prohibited_fields:
|
||||
sanitized.pop(field, None)
|
||||
|
||||
return sanitized
|
||||
|
||||
def validate_no_prohibited_storage(self, data):
|
||||
"""Ensure no prohibited data is being stored."""
|
||||
for field in self.prohibited_fields:
|
||||
if field in data:
|
||||
raise SecurityError(f"Attempting to store prohibited field: {field}")
|
||||
```
|
||||
|
||||
## Tokenization
|
||||
|
||||
### Using Payment Processor Tokens
|
||||
```python
|
||||
import stripe
|
||||
|
||||
class TokenizedPayment:
|
||||
"""Handle payments using tokens (no card data on server)."""
|
||||
|
||||
@staticmethod
|
||||
def create_payment_method_token(card_details):
|
||||
"""Create token from card details (client-side only)."""
|
||||
# THIS SHOULD ONLY BE DONE CLIENT-SIDE WITH STRIPE.JS
|
||||
# NEVER send card details to your server
|
||||
|
||||
"""
|
||||
// Frontend JavaScript
|
||||
const stripe = Stripe('pk_...');
|
||||
|
||||
const {token, error} = await stripe.createToken({
|
||||
card: {
|
||||
number: '4242424242424242',
|
||||
exp_month: 12,
|
||||
exp_year: 2024,
|
||||
cvc: '123'
|
||||
}
|
||||
});
|
||||
|
||||
// Send token.id to server (NOT card details)
|
||||
"""
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def charge_with_token(token_id, amount):
|
||||
"""Charge using token (server-side)."""
|
||||
# Your server only sees the token, never the card number
|
||||
stripe.api_key = "sk_..."
|
||||
|
||||
charge = stripe.Charge.create(
|
||||
amount=amount,
|
||||
currency="usd",
|
||||
source=token_id, # Token instead of card details
|
||||
description="Payment"
|
||||
)
|
||||
|
||||
return charge
|
||||
|
||||
@staticmethod
|
||||
def store_payment_method(customer_id, payment_method_token):
|
||||
"""Store payment method as token for future use."""
|
||||
stripe.Customer.modify(
|
||||
customer_id,
|
||||
source=payment_method_token
|
||||
)
|
||||
|
||||
# Store only customer_id and payment_method_id in your database
|
||||
# NEVER store actual card details
|
||||
return {
|
||||
'customer_id': customer_id,
|
||||
'has_payment_method': True
|
||||
# DO NOT store: card number, CVV, etc.
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Tokenization (Advanced)
|
||||
```python
|
||||
import secrets
|
||||
from cryptography.fernet import Fernet
|
||||
|
||||
class TokenVault:
|
||||
"""Secure token vault for card data (if you must store it)."""
|
||||
|
||||
def __init__(self, encryption_key):
|
||||
self.cipher = Fernet(encryption_key)
|
||||
self.vault = {} # In production: use encrypted database
|
||||
|
||||
def tokenize(self, card_data):
|
||||
"""Convert card data to token."""
|
||||
# Generate secure random token
|
||||
token = secrets.token_urlsafe(32)
|
||||
|
||||
# Encrypt card data
|
||||
encrypted = self.cipher.encrypt(json.dumps(card_data).encode())
|
||||
|
||||
# Store token -> encrypted data mapping
|
||||
self.vault[token] = encrypted
|
||||
|
||||
return token
|
||||
|
||||
def detokenize(self, token):
|
||||
"""Retrieve card data from token."""
|
||||
encrypted = self.vault.get(token)
|
||||
if not encrypted:
|
||||
raise ValueError("Token not found")
|
||||
|
||||
# Decrypt
|
||||
decrypted = self.cipher.decrypt(encrypted)
|
||||
return json.loads(decrypted.decode())
|
||||
|
||||
def delete_token(self, token):
|
||||
"""Remove token from vault."""
|
||||
self.vault.pop(token, None)
|
||||
```
|
||||
|
||||
## Encryption
|
||||
|
||||
### Data at Rest
|
||||
```python
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
import os
|
||||
|
||||
class EncryptedStorage:
|
||||
"""Encrypt data at rest using AES-256-GCM."""
|
||||
|
||||
def __init__(self, encryption_key):
|
||||
"""Initialize with 256-bit key."""
|
||||
self.key = encryption_key # Must be 32 bytes
|
||||
|
||||
def encrypt(self, plaintext):
|
||||
"""Encrypt data."""
|
||||
# Generate random nonce
|
||||
nonce = os.urandom(12)
|
||||
|
||||
# Encrypt
|
||||
aesgcm = AESGCM(self.key)
|
||||
ciphertext = aesgcm.encrypt(nonce, plaintext.encode(), None)
|
||||
|
||||
# Return nonce + ciphertext
|
||||
return nonce + ciphertext
|
||||
|
||||
def decrypt(self, encrypted_data):
|
||||
"""Decrypt data."""
|
||||
# Extract nonce and ciphertext
|
||||
nonce = encrypted_data[:12]
|
||||
ciphertext = encrypted_data[12:]
|
||||
|
||||
# Decrypt
|
||||
aesgcm = AESGCM(self.key)
|
||||
plaintext = aesgcm.decrypt(nonce, ciphertext, None)
|
||||
|
||||
return plaintext.decode()
|
||||
|
||||
# Usage
|
||||
storage = EncryptedStorage(os.urandom(32))
|
||||
encrypted_pan = storage.encrypt("4242424242424242")
|
||||
# Store encrypted_pan in database
|
||||
```
|
||||
|
||||
### Data in Transit
|
||||
```python
|
||||
# Always use TLS 1.2 or higher
|
||||
# Flask/Django example
|
||||
app.config['SESSION_COOKIE_SECURE'] = True # HTTPS only
|
||||
app.config['SESSION_COOKIE_HTTPONLY'] = True
|
||||
app.config['SESSION_COOKIE_SAMESITE'] = 'Strict'
|
||||
|
||||
# Enforce HTTPS
|
||||
from flask_talisman import Talisman
|
||||
Talisman(app, force_https=True)
|
||||
```
|
||||
|
||||
## Access Control
|
||||
|
||||
```python
|
||||
from functools import wraps
|
||||
from flask import session
|
||||
|
||||
def require_pci_access(f):
|
||||
"""Decorator to restrict access to cardholder data."""
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
user = session.get('user')
|
||||
|
||||
# Check if user has PCI access role
|
||||
if not user or 'pci_access' not in user.get('roles', []):
|
||||
return {'error': 'Unauthorized access to cardholder data'}, 403
|
||||
|
||||
# Log access attempt
|
||||
audit_log(
|
||||
user=user['id'],
|
||||
action='access_cardholder_data',
|
||||
resource=f.__name__
|
||||
)
|
||||
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
@app.route('/api/payment-methods')
|
||||
@require_pci_access
|
||||
def get_payment_methods():
|
||||
"""Retrieve payment methods (restricted access)."""
|
||||
# Only accessible to users with pci_access role
|
||||
pass
|
||||
```
|
||||
|
||||
## Audit Logging
|
||||
|
||||
```python
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
class PCIAuditLogger:
|
||||
"""PCI-compliant audit logging."""
|
||||
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger('pci_audit')
|
||||
# Configure to write to secure, append-only log
|
||||
|
||||
def log_access(self, user_id, resource, action, result):
|
||||
"""Log access to cardholder data."""
|
||||
entry = {
|
||||
'timestamp': datetime.utcnow().isoformat(),
|
||||
'user_id': user_id,
|
||||
'resource': resource,
|
||||
'action': action,
|
||||
'result': result,
|
||||
'ip_address': request.remote_addr
|
||||
}
|
||||
|
||||
self.logger.info(json.dumps(entry))
|
||||
|
||||
def log_authentication(self, user_id, success, method):
|
||||
"""Log authentication attempt."""
|
||||
entry = {
|
||||
'timestamp': datetime.utcnow().isoformat(),
|
||||
'user_id': user_id,
|
||||
'event': 'authentication',
|
||||
'success': success,
|
||||
'method': method,
|
||||
'ip_address': request.remote_addr
|
||||
}
|
||||
|
||||
self.logger.info(json.dumps(entry))
|
||||
|
||||
# Usage
|
||||
audit = PCIAuditLogger()
|
||||
audit.log_access(user_id=123, resource='payment_methods', action='read', result='success')
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
### Input Validation
|
||||
```python
|
||||
import re
|
||||
|
||||
def validate_card_number(card_number):
|
||||
"""Validate card number format (Luhn algorithm)."""
|
||||
# Remove spaces and dashes
|
||||
card_number = re.sub(r'[\s-]', '', card_number)
|
||||
|
||||
# Check if all digits
|
||||
if not card_number.isdigit():
|
||||
return False
|
||||
|
||||
# Luhn algorithm
|
||||
def luhn_checksum(card_num):
|
||||
def digits_of(n):
|
||||
return [int(d) for d in str(n)]
|
||||
|
||||
digits = digits_of(card_num)
|
||||
odd_digits = digits[-1::-2]
|
||||
even_digits = digits[-2::-2]
|
||||
checksum = sum(odd_digits)
|
||||
for d in even_digits:
|
||||
checksum += sum(digits_of(d * 2))
|
||||
return checksum % 10
|
||||
|
||||
return luhn_checksum(card_number) == 0
|
||||
|
||||
def sanitize_input(user_input):
|
||||
"""Sanitize user input to prevent injection."""
|
||||
# Remove special characters
|
||||
# Validate against expected format
|
||||
# Escape for database queries
|
||||
pass
|
||||
```
|
||||
|
||||
## PCI DSS SAQ (Self-Assessment Questionnaire)
|
||||
|
||||
### SAQ A (Least Requirements)
|
||||
- E-commerce using hosted payment page
|
||||
- No card data on your systems
|
||||
- ~20 questions
|
||||
|
||||
### SAQ A-EP
|
||||
- E-commerce with embedded payment form
|
||||
- Uses JavaScript to handle card data
|
||||
- ~180 questions
|
||||
|
||||
### SAQ D (Most Requirements)
|
||||
- Store, process, or transmit card data
|
||||
- Full PCI DSS requirements
|
||||
- ~300 questions
|
||||
|
||||
## Compliance Checklist
|
||||
|
||||
```python
|
||||
PCI_COMPLIANCE_CHECKLIST = {
|
||||
'network_security': [
|
||||
'Firewall configured and maintained',
|
||||
'No vendor default passwords',
|
||||
'Network segmentation implemented'
|
||||
],
|
||||
'data_protection': [
|
||||
'No storage of CVV, track data, or PIN',
|
||||
'PAN encrypted when stored',
|
||||
'PAN masked when displayed',
|
||||
'Encryption keys properly managed'
|
||||
],
|
||||
'vulnerability_management': [
|
||||
'Anti-virus installed and updated',
|
||||
'Secure development practices',
|
||||
'Regular security patches',
|
||||
'Vulnerability scanning performed'
|
||||
],
|
||||
'access_control': [
|
||||
'Access restricted by role',
|
||||
'Unique IDs for all users',
|
||||
'Multi-factor authentication',
|
||||
'Physical security measures'
|
||||
],
|
||||
'monitoring': [
|
||||
'Audit logs enabled',
|
||||
'Log review process',
|
||||
'File integrity monitoring',
|
||||
'Regular security testing'
|
||||
],
|
||||
'policy': [
|
||||
'Security policy documented',
|
||||
'Risk assessment performed',
|
||||
'Security awareness training',
|
||||
'Incident response plan'
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- **references/data-minimization.md**: Never store prohibited data
|
||||
- **references/tokenization.md**: Tokenization strategies
|
||||
- **references/encryption.md**: Encryption requirements
|
||||
- **references/access-control.md**: Role-based access
|
||||
- **references/audit-logging.md**: Comprehensive logging
|
||||
- **assets/pci-compliance-checklist.md**: Complete checklist
|
||||
- **assets/encrypted-storage.py**: Encryption utilities
|
||||
- **scripts/audit-payment-system.sh**: Compliance audit script
|
||||
|
||||
## Common Violations
|
||||
|
||||
1. **Storing CVV**: Never store card verification codes
|
||||
2. **Unencrypted PAN**: Card numbers must be encrypted at rest
|
||||
3. **Weak Encryption**: Use AES-256 or equivalent
|
||||
4. **No Access Controls**: Restrict who can access cardholder data
|
||||
5. **Missing Audit Logs**: Must log all access to payment data
|
||||
6. **Insecure Transmission**: Always use TLS 1.2+
|
||||
7. **Default Passwords**: Change all default credentials
|
||||
8. **No Security Testing**: Regular penetration testing required
|
||||
|
||||
## Reducing PCI Scope
|
||||
|
||||
1. **Use Hosted Payments**: Stripe Checkout, PayPal, etc.
|
||||
2. **Tokenization**: Replace card data with tokens
|
||||
3. **Network Segmentation**: Isolate cardholder data environment
|
||||
4. **Outsource**: Use PCI-compliant payment processors
|
||||
5. **No Storage**: Never store full card details
|
||||
|
||||
By minimizing systems that touch card data, you reduce compliance burden significantly.
|
||||
442
skills/stripe-integration/SKILL.md
Normal file
442
skills/stripe-integration/SKILL.md
Normal file
@@ -0,0 +1,442 @@
|
||||
---
|
||||
name: stripe-integration
|
||||
description: Implement Stripe payment processing for robust, PCI-compliant payment flows including checkout, subscriptions, and webhooks. Use when integrating Stripe payments, building subscription systems, or implementing secure checkout flows.
|
||||
---
|
||||
|
||||
# Stripe Integration
|
||||
|
||||
Master Stripe payment processing integration for robust, PCI-compliant payment flows including checkout, subscriptions, webhooks, and refunds.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Implementing payment processing in web/mobile applications
|
||||
- Setting up subscription billing systems
|
||||
- Handling one-time payments and recurring charges
|
||||
- Processing refunds and disputes
|
||||
- Managing customer payment methods
|
||||
- Implementing SCA (Strong Customer Authentication) for European payments
|
||||
- Building marketplace payment flows with Stripe Connect
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### 1. Payment Flows
|
||||
**Checkout Session (Hosted)**
|
||||
- Stripe-hosted payment page
|
||||
- Minimal PCI compliance burden
|
||||
- Fastest implementation
|
||||
- Supports one-time and recurring payments
|
||||
|
||||
**Payment Intents (Custom UI)**
|
||||
- Full control over payment UI
|
||||
- Requires Stripe.js for PCI compliance
|
||||
- More complex implementation
|
||||
- Better customization options
|
||||
|
||||
**Setup Intents (Save Payment Methods)**
|
||||
- Collect payment method without charging
|
||||
- Used for subscriptions and future payments
|
||||
- Requires customer confirmation
|
||||
|
||||
### 2. Webhooks
|
||||
**Critical Events:**
|
||||
- `payment_intent.succeeded`: Payment completed
|
||||
- `payment_intent.payment_failed`: Payment failed
|
||||
- `customer.subscription.updated`: Subscription changed
|
||||
- `customer.subscription.deleted`: Subscription canceled
|
||||
- `charge.refunded`: Refund processed
|
||||
- `invoice.payment_succeeded`: Subscription payment successful
|
||||
|
||||
### 3. Subscriptions
|
||||
**Components:**
|
||||
- **Product**: What you're selling
|
||||
- **Price**: How much and how often
|
||||
- **Subscription**: Customer's recurring payment
|
||||
- **Invoice**: Generated for each billing cycle
|
||||
|
||||
### 4. Customer Management
|
||||
- Create and manage customer records
|
||||
- Store multiple payment methods
|
||||
- Track customer metadata
|
||||
- Manage billing details
|
||||
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
import stripe
|
||||
|
||||
stripe.api_key = "sk_test_..."
|
||||
|
||||
# Create a checkout session
|
||||
session = stripe.checkout.Session.create(
|
||||
payment_method_types=['card'],
|
||||
line_items=[{
|
||||
'price_data': {
|
||||
'currency': 'usd',
|
||||
'product_data': {
|
||||
'name': 'Premium Subscription',
|
||||
},
|
||||
'unit_amount': 2000, # $20.00
|
||||
'recurring': {
|
||||
'interval': 'month',
|
||||
},
|
||||
},
|
||||
'quantity': 1,
|
||||
}],
|
||||
mode='subscription',
|
||||
success_url='https://yourdomain.com/success?session_id={CHECKOUT_SESSION_ID}',
|
||||
cancel_url='https://yourdomain.com/cancel',
|
||||
)
|
||||
|
||||
# Redirect user to session.url
|
||||
print(session.url)
|
||||
```
|
||||
|
||||
## Payment Implementation Patterns
|
||||
|
||||
### Pattern 1: One-Time Payment (Hosted Checkout)
|
||||
```python
|
||||
def create_checkout_session(amount, currency='usd'):
|
||||
"""Create a one-time payment checkout session."""
|
||||
try:
|
||||
session = stripe.checkout.Session.create(
|
||||
payment_method_types=['card'],
|
||||
line_items=[{
|
||||
'price_data': {
|
||||
'currency': currency,
|
||||
'product_data': {
|
||||
'name': 'Purchase',
|
||||
'images': ['https://example.com/product.jpg'],
|
||||
},
|
||||
'unit_amount': amount, # Amount in cents
|
||||
},
|
||||
'quantity': 1,
|
||||
}],
|
||||
mode='payment',
|
||||
success_url='https://yourdomain.com/success?session_id={CHECKOUT_SESSION_ID}',
|
||||
cancel_url='https://yourdomain.com/cancel',
|
||||
metadata={
|
||||
'order_id': 'order_123',
|
||||
'user_id': 'user_456'
|
||||
}
|
||||
)
|
||||
return session
|
||||
except stripe.error.StripeError as e:
|
||||
# Handle error
|
||||
print(f"Stripe error: {e.user_message}")
|
||||
raise
|
||||
```
|
||||
|
||||
### Pattern 2: Custom Payment Intent Flow
|
||||
```python
|
||||
def create_payment_intent(amount, currency='usd', customer_id=None):
|
||||
"""Create a payment intent for custom checkout UI."""
|
||||
intent = stripe.PaymentIntent.create(
|
||||
amount=amount,
|
||||
currency=currency,
|
||||
customer=customer_id,
|
||||
automatic_payment_methods={
|
||||
'enabled': True,
|
||||
},
|
||||
metadata={
|
||||
'integration_check': 'accept_a_payment'
|
||||
}
|
||||
)
|
||||
return intent.client_secret # Send to frontend
|
||||
|
||||
# Frontend (JavaScript)
|
||||
"""
|
||||
const stripe = Stripe('pk_test_...');
|
||||
const elements = stripe.elements();
|
||||
const cardElement = elements.create('card');
|
||||
cardElement.mount('#card-element');
|
||||
|
||||
const {error, paymentIntent} = await stripe.confirmCardPayment(
|
||||
clientSecret,
|
||||
{
|
||||
payment_method: {
|
||||
card: cardElement,
|
||||
billing_details: {
|
||||
name: 'Customer Name'
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
// Handle error
|
||||
} else if (paymentIntent.status === 'succeeded') {
|
||||
// Payment successful
|
||||
}
|
||||
"""
|
||||
```
|
||||
|
||||
### Pattern 3: Subscription Creation
|
||||
```python
|
||||
def create_subscription(customer_id, price_id):
|
||||
"""Create a subscription for a customer."""
|
||||
try:
|
||||
subscription = stripe.Subscription.create(
|
||||
customer=customer_id,
|
||||
items=[{'price': price_id}],
|
||||
payment_behavior='default_incomplete',
|
||||
payment_settings={'save_default_payment_method': 'on_subscription'},
|
||||
expand=['latest_invoice.payment_intent'],
|
||||
)
|
||||
|
||||
return {
|
||||
'subscription_id': subscription.id,
|
||||
'client_secret': subscription.latest_invoice.payment_intent.client_secret
|
||||
}
|
||||
except stripe.error.StripeError as e:
|
||||
print(f"Subscription creation failed: {e}")
|
||||
raise
|
||||
```
|
||||
|
||||
### Pattern 4: Customer Portal
|
||||
```python
|
||||
def create_customer_portal_session(customer_id):
|
||||
"""Create a portal session for customers to manage subscriptions."""
|
||||
session = stripe.billing_portal.Session.create(
|
||||
customer=customer_id,
|
||||
return_url='https://yourdomain.com/account',
|
||||
)
|
||||
return session.url # Redirect customer here
|
||||
```
|
||||
|
||||
## Webhook Handling
|
||||
|
||||
### Secure Webhook Endpoint
|
||||
```python
|
||||
from flask import Flask, request
|
||||
import stripe
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
endpoint_secret = 'whsec_...'
|
||||
|
||||
@app.route('/webhook', methods=['POST'])
|
||||
def webhook():
|
||||
payload = request.data
|
||||
sig_header = request.headers.get('Stripe-Signature')
|
||||
|
||||
try:
|
||||
event = stripe.Webhook.construct_event(
|
||||
payload, sig_header, endpoint_secret
|
||||
)
|
||||
except ValueError:
|
||||
# Invalid payload
|
||||
return 'Invalid payload', 400
|
||||
except stripe.error.SignatureVerificationError:
|
||||
# Invalid signature
|
||||
return 'Invalid signature', 400
|
||||
|
||||
# Handle the event
|
||||
if event['type'] == 'payment_intent.succeeded':
|
||||
payment_intent = event['data']['object']
|
||||
handle_successful_payment(payment_intent)
|
||||
elif event['type'] == 'payment_intent.payment_failed':
|
||||
payment_intent = event['data']['object']
|
||||
handle_failed_payment(payment_intent)
|
||||
elif event['type'] == 'customer.subscription.deleted':
|
||||
subscription = event['data']['object']
|
||||
handle_subscription_canceled(subscription)
|
||||
|
||||
return 'Success', 200
|
||||
|
||||
def handle_successful_payment(payment_intent):
|
||||
"""Process successful payment."""
|
||||
customer_id = payment_intent.get('customer')
|
||||
amount = payment_intent['amount']
|
||||
metadata = payment_intent.get('metadata', {})
|
||||
|
||||
# Update your database
|
||||
# Send confirmation email
|
||||
# Fulfill order
|
||||
print(f"Payment succeeded: {payment_intent['id']}")
|
||||
|
||||
def handle_failed_payment(payment_intent):
|
||||
"""Handle failed payment."""
|
||||
error = payment_intent.get('last_payment_error', {})
|
||||
print(f"Payment failed: {error.get('message')}")
|
||||
# Notify customer
|
||||
# Update order status
|
||||
|
||||
def handle_subscription_canceled(subscription):
|
||||
"""Handle subscription cancellation."""
|
||||
customer_id = subscription['customer']
|
||||
# Update user access
|
||||
# Send cancellation email
|
||||
print(f"Subscription canceled: {subscription['id']}")
|
||||
```
|
||||
|
||||
### Webhook Best Practices
|
||||
```python
|
||||
import hashlib
|
||||
import hmac
|
||||
|
||||
def verify_webhook_signature(payload, signature, secret):
|
||||
"""Manually verify webhook signature."""
|
||||
expected_sig = hmac.new(
|
||||
secret.encode('utf-8'),
|
||||
payload,
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
return hmac.compare_digest(signature, expected_sig)
|
||||
|
||||
def handle_webhook_idempotently(event_id, handler):
|
||||
"""Ensure webhook is processed exactly once."""
|
||||
# Check if event already processed
|
||||
if is_event_processed(event_id):
|
||||
return
|
||||
|
||||
# Process event
|
||||
try:
|
||||
handler()
|
||||
mark_event_processed(event_id)
|
||||
except Exception as e:
|
||||
log_error(e)
|
||||
# Stripe will retry failed webhooks
|
||||
raise
|
||||
```
|
||||
|
||||
## Customer Management
|
||||
|
||||
```python
|
||||
def create_customer(email, name, payment_method_id=None):
|
||||
"""Create a Stripe customer."""
|
||||
customer = stripe.Customer.create(
|
||||
email=email,
|
||||
name=name,
|
||||
payment_method=payment_method_id,
|
||||
invoice_settings={
|
||||
'default_payment_method': payment_method_id
|
||||
} if payment_method_id else None,
|
||||
metadata={
|
||||
'user_id': '12345'
|
||||
}
|
||||
)
|
||||
return customer
|
||||
|
||||
def attach_payment_method(customer_id, payment_method_id):
|
||||
"""Attach a payment method to a customer."""
|
||||
stripe.PaymentMethod.attach(
|
||||
payment_method_id,
|
||||
customer=customer_id
|
||||
)
|
||||
|
||||
# Set as default
|
||||
stripe.Customer.modify(
|
||||
customer_id,
|
||||
invoice_settings={
|
||||
'default_payment_method': payment_method_id
|
||||
}
|
||||
)
|
||||
|
||||
def list_customer_payment_methods(customer_id):
|
||||
"""List all payment methods for a customer."""
|
||||
payment_methods = stripe.PaymentMethod.list(
|
||||
customer=customer_id,
|
||||
type='card'
|
||||
)
|
||||
return payment_methods.data
|
||||
```
|
||||
|
||||
## Refund Handling
|
||||
|
||||
```python
|
||||
def create_refund(payment_intent_id, amount=None, reason=None):
|
||||
"""Create a refund."""
|
||||
refund_params = {
|
||||
'payment_intent': payment_intent_id
|
||||
}
|
||||
|
||||
if amount:
|
||||
refund_params['amount'] = amount # Partial refund
|
||||
|
||||
if reason:
|
||||
refund_params['reason'] = reason # 'duplicate', 'fraudulent', 'requested_by_customer'
|
||||
|
||||
refund = stripe.Refund.create(**refund_params)
|
||||
return refund
|
||||
|
||||
def handle_dispute(charge_id, evidence):
|
||||
"""Update dispute with evidence."""
|
||||
stripe.Dispute.modify(
|
||||
charge_id,
|
||||
evidence={
|
||||
'customer_name': evidence.get('customer_name'),
|
||||
'customer_email_address': evidence.get('customer_email'),
|
||||
'shipping_documentation': evidence.get('shipping_proof'),
|
||||
'customer_communication': evidence.get('communication'),
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```python
|
||||
# Use test mode keys
|
||||
stripe.api_key = "sk_test_..."
|
||||
|
||||
# Test card numbers
|
||||
TEST_CARDS = {
|
||||
'success': '4242424242424242',
|
||||
'declined': '4000000000000002',
|
||||
'3d_secure': '4000002500003155',
|
||||
'insufficient_funds': '4000000000009995'
|
||||
}
|
||||
|
||||
def test_payment_flow():
|
||||
"""Test complete payment flow."""
|
||||
# Create test customer
|
||||
customer = stripe.Customer.create(
|
||||
email="test@example.com"
|
||||
)
|
||||
|
||||
# Create payment intent
|
||||
intent = stripe.PaymentIntent.create(
|
||||
amount=1000,
|
||||
currency='usd',
|
||||
customer=customer.id,
|
||||
payment_method_types=['card']
|
||||
)
|
||||
|
||||
# Confirm with test card
|
||||
confirmed = stripe.PaymentIntent.confirm(
|
||||
intent.id,
|
||||
payment_method='pm_card_visa' # Test payment method
|
||||
)
|
||||
|
||||
assert confirmed.status == 'succeeded'
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- **references/checkout-flows.md**: Detailed checkout implementation
|
||||
- **references/webhook-handling.md**: Webhook security and processing
|
||||
- **references/subscription-management.md**: Subscription lifecycle
|
||||
- **references/customer-management.md**: Customer and payment method handling
|
||||
- **references/invoice-generation.md**: Invoicing and billing
|
||||
- **assets/stripe-client.py**: Production-ready Stripe client wrapper
|
||||
- **assets/webhook-handler.py**: Complete webhook processor
|
||||
- **assets/checkout-config.json**: Checkout configuration templates
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always Use Webhooks**: Don't rely solely on client-side confirmation
|
||||
2. **Idempotency**: Handle webhook events idempotently
|
||||
3. **Error Handling**: Gracefully handle all Stripe errors
|
||||
4. **Test Mode**: Thoroughly test with test keys before production
|
||||
5. **Metadata**: Use metadata to link Stripe objects to your database
|
||||
6. **Monitoring**: Track payment success rates and errors
|
||||
7. **PCI Compliance**: Never handle raw card data on your server
|
||||
8. **SCA Ready**: Implement 3D Secure for European payments
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Not Verifying Webhooks**: Always verify webhook signatures
|
||||
- **Missing Webhook Events**: Handle all relevant webhook events
|
||||
- **Hardcoded Amounts**: Use cents/smallest currency unit
|
||||
- **No Retry Logic**: Implement retries for API calls
|
||||
- **Ignoring Test Mode**: Test all edge cases with test cards
|
||||
Reference in New Issue
Block a user