Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 17:57:01 +08:00
commit 46c826cfb9
12 changed files with 4898 additions and 0 deletions

View 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
View 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.

View 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
View 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.

File diff suppressed because it is too large Load Diff

View 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
View 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
View 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": []
}
}

View 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

View 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&currency=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

View 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.

View 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