# Skill: PayPal Payment Integration

## Identity
- **Skill ID**: `payment-integration`
- **Domain**: Payment Processing, Subscription Management
- **Technologies**: PayPal Subscriptions API, Webhooks
- **Source Agent**: `backend-app-expert.md`

## When to Load This Skill

Load this skill when working on:
- PayPal subscription creation
- Payment webhook processing
- Subscription plan management
- Billing cycle handling
- Payment history tracking
- Refund processing
- Dispute handling

**File patterns:**
- `api/src/services/paypal*.ts`
- `api/src/routes/subscriptions*.ts`
- `api/src/routes/payments*.ts`
- `api/src/routes/webhooks/paypal*.ts`

## Core Patterns

### 1. PayPal SDK Setup

```typescript
import axios from 'axios';

// PayPal configuration
const PAYPAL_BASE_URL = process.env.PAYPAL_MODE === 'live'
  ? 'https://api.paypal.com'
  : 'https://api.sandbox.paypal.com';

const PAYPAL_CLIENT_ID = process.env.PAYPAL_CLIENT_ID!;
const PAYPAL_CLIENT_SECRET = process.env.PAYPAL_CLIENT_SECRET!;

// Get access token
async function getPayPalAccessToken(): Promise<string> {
  const auth = Buffer.from(`${PAYPAL_CLIENT_ID}:${PAYPAL_CLIENT_SECRET}`).toString('base64');
  
  const response = await axios.post(
    `${PAYPAL_BASE_URL}/v1/oauth2/token`,
    'grant_type=client_credentials',
    {
      headers: {
        'Authorization': `Basic ${auth}`,
        'Content-Type': 'application/x-www-form-urlencoded'
      }
    }
  );
  
  return response.data.access_token;
}

// Cached token with refresh
let cachedToken: { token: string; expiresAt: number } | null = null;

async function getAccessToken(): Promise<string> {
  const now = Date.now();
  
  if (cachedToken && cachedToken.expiresAt > now) {
    return cachedToken.token;
  }
  
  const token = await getPayPalAccessToken();
  cachedToken = {
    token,
    expiresAt: now + 3600000 // 1 hour
  };
  
  return token;
}
```

### 2. Create Subscription Plans (One-Time Setup)

```typescript
// Subscription plan configuration
interface PlanConfig {
  id: string;
  name: string;
  price: string;
  credits: number;
  billingCycle: 'monthly' | 'annual';
}

const PLANS: PlanConfig[] = [
  {
    id: 'starter-monthly',
    name: 'Starter Plan',
    price: '9.00',
    credits: 100000,
    billingCycle: 'monthly'
  },
  {
    id: 'professional-monthly',
    name: 'Professional Plan',
    price: '29.00',
    credits: 500000,
    billingCycle: 'monthly'
  },
  {
    id: 'enterprise-monthly',
    name: 'Enterprise Plan',
    price: '99.00',
    credits: 2000000,
    billingCycle: 'monthly'
  }
];

// Create plan in PayPal (run once during setup)
async function createPayPalPlan(config: PlanConfig): Promise<string> {
  const token = await getAccessToken();
  
  const productResponse = await axios.post(
    `${PAYPAL_BASE_URL}/v1/catalogs/products`,
    {
      name: config.name,
      description: `${config.name} - ${config.credits.toLocaleString()} tokens/month`,
      type: 'SERVICE',
      category: 'SOFTWARE'
    },
    {
      headers: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json'
      }
    }
  );
  
  const productId = productResponse.data.id;
  
  const planResponse = await axios.post(
    `${PAYPAL_BASE_URL}/v1/billing/plans`,
    {
      product_id: productId,
      name: config.name,
      description: `${config.name} subscription`,
      billing_cycles: [
        {
          frequency: {
            interval_unit: config.billingCycle === 'monthly' ? 'MONTH' : 'YEAR',
            interval_count: 1
          },
          tenure_type: 'REGULAR',
          sequence: 1,
          total_cycles: 0, // Infinite
          pricing_scheme: {
            fixed_price: {
              value: config.price,
              currency_code: 'USD'
            }
          }
        }
      ],
      payment_preferences: {
        auto_bill_outstanding: true,
        setup_fee_failure_action: 'CANCEL',
        payment_failure_threshold: 3
      }
    },
    {
      headers: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json'
      }
    }
  );
  
  return planResponse.data.id;
}

// Store plan IDs in database
interface StoredPlan {
  tier: 'starter' | 'professional' | 'enterprise';
  billing_cycle: 'monthly' | 'annual';
  paypal_plan_id: string;
  price: number;
  credits: number;
}

// Get plan ID from database
async function getPayPalPlanId(
  tier: 'starter' | 'professional' | 'enterprise',
  billingCycle: 'monthly' | 'annual'
): Promise<string> {
  // In production, fetch from database or config
  const plans = {
    'starter-monthly': process.env.PAYPAL_PLAN_STARTER_MONTHLY!,
    'professional-monthly': process.env.PAYPAL_PLAN_PROFESSIONAL_MONTHLY!,
    'enterprise-monthly': process.env.PAYPAL_PLAN_ENTERPRISE_MONTHLY!
  };
  
  return plans[`${tier}-${billingCycle}`];
}
```

### 3. Create Subscription (User Checkout)

```typescript
import { z } from 'zod';

const CreateSubscriptionSchema = z.object({
  plan_tier: z.enum(['starter', 'professional', 'enterprise']),
  billing_cycle: z.enum(['monthly', 'annual']),
  return_url: z.string().url(),
  cancel_url: z.string().url()
});

// Create subscription endpoint
router.post('/subscriptions', requireAuth, async (req: AuthRequest, res) => {
  try {
    const data = CreateSubscriptionSchema.parse(req.body);
    const userId = req.user!.userId;
    
    // Check if user already has active subscription
    const existing = await prisma.subscription.findFirst({
      where: {
        user_id: userId,
        status: 'active'
      }
    });
    
    if (existing) {
      return res.status(409).json({ error: 'Active subscription already exists' });
    }
    
    const token = await getAccessToken();
    const planId = await getPayPalPlanId(data.plan_tier, data.billing_cycle);
    
    // Create PayPal subscription
    const response = await axios.post(
      `${PAYPAL_BASE_URL}/v1/billing/subscriptions`,
      {
        plan_id: planId,
        custom_id: userId, // Store user ID for webhook handling
        application_context: {
          brand_name: 'TranslatePressZone',
          locale: 'en-US',
          shipping_preference: 'NO_SHIPPING',
          user_action: 'SUBSCRIBE_NOW',
          return_url: data.return_url,
          cancel_url: data.cancel_url
        }
      },
      {
        headers: {
          'Authorization': `Bearer ${token}`,
          'Content-Type': 'application/json'
        }
      }
    );
    
    const subscriptionId = response.data.id;
    const approvalUrl = response.data.links.find((link: any) => link.rel === 'approve')?.href;
    
    // Create pending subscription in database
    await prisma.subscription.create({
      data: {
        user_id: userId,
        plan_tier: data.plan_tier,
        billing_cycle: data.billing_cycle,
        status: 'pending',
        paypal_subscription_id: subscriptionId,
        current_period_start: new Date(),
        current_period_end: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) // Placeholder
      }
    });
    
    res.json({
      subscription_id: subscriptionId,
      approval_url: approvalUrl
    });
    
  } catch (error) {
    res.status(500).json({ error: 'Failed to create subscription' });
  }
});
```

### 4. Handle Subscription Webhooks

```typescript
// See webhook-implementation skill for signature verification

// Subscription activated
async function handleSubscriptionActivated(event: any) {
  const subscriptionId = event.resource.id;
  const billingInfo = event.resource.billing_info;
  
  await prisma.subscription.update({
    where: { paypal_subscription_id: subscriptionId },
    data: {
      status: 'active',
      current_period_start: new Date(billingInfo.last_payment.time),
      current_period_end: new Date(billingInfo.next_billing_time)
    }
  });
  
  // Allocate initial credits
  const subscription = await prisma.subscription.findUnique({
    where: { paypal_subscription_id: subscriptionId },
    include: { user: true }
  });
  
  if (subscription) {
    const planCredits = {
      starter: 100000,
      professional: 500000,
      enterprise: 2000000
    };
    
    const credits = planCredits[subscription.plan_tier];
    
    await prisma.creditTransaction.create({
      data: {
        user_id: subscription.user_id,
        type: 'allocation',
        amount: credits,
        balance_after: credits,
        description: `Initial allocation for ${subscription.plan_tier} plan`
      }
    });
  }
}

// Payment completed
async function handlePaymentCompleted(event: any) {
  const paymentId = event.resource.id;
  const amount = parseFloat(event.resource.amount.total);
  const subscriptionId = event.resource.billing_agreement_id;
  
  const subscription = await prisma.subscription.findUnique({
    where: { paypal_subscription_id: subscriptionId }
  });
  
  if (!subscription) {
    console.error(`Subscription not found: ${subscriptionId}`);
    return;
  }
  
  // Record payment
  await prisma.payment.create({
    data: {
      user_id: subscription.user_id,
      paypal_payment_id: paymentId,
      amount,
      currency: 'USD',
      status: 'completed',
      type: 'subscription_payment',
      subscription_id: subscription.id
    }
  });
  
  // Extend subscription period
  const nextPeriodEnd = new Date(subscription.current_period_end);
  nextPeriodEnd.setMonth(nextPeriodEnd.getMonth() + 1);
  
  await prisma.subscription.update({
    where: { id: subscription.id },
    data: {
      current_period_start: subscription.current_period_end,
      current_period_end: nextPeriodEnd
    }
  });
}

// Subscription cancelled
async function handleSubscriptionCancelled(event: any) {
  const subscriptionId = event.resource.id;
  
  await prisma.subscription.update({
    where: { paypal_subscription_id: subscriptionId },
    data: {
      status: 'cancelled',
      cancel_at_period_end: true
    }
  });
  
  // User retains credits until period end
}

// Subscription suspended (payment failed)
async function handleSubscriptionSuspended(event: any) {
  const subscriptionId = event.resource.id;
  
  await prisma.subscription.update({
    where: { paypal_subscription_id: subscriptionId },
    data: { status: 'suspended' }
  });
  
  // Suspend user account
  const subscription = await prisma.subscription.findUnique({
    where: { paypal_subscription_id: subscriptionId }
  });
  
  if (subscription) {
    await prisma.user.update({
      where: { id: subscription.user_id },
      data: { status: 'suspended' }
    });
  }
}
```

### 5. Cancel Subscription

```typescript
// User-initiated cancellation
router.delete('/subscriptions/:id', requireAuth, async (req: AuthRequest, res) => {
  const { id } = req.params;
  const userId = req.user!.userId;
  
  const subscription = await prisma.subscription.findFirst({
    where: {
      id,
      user_id: userId,
      status: 'active'
    }
  });
  
  if (!subscription) {
    return res.status(404).json({ error: 'Active subscription not found' });
  }
  
  try {
    const token = await getAccessToken();
    
    // Cancel via PayPal API
    await axios.post(
      `${PAYPAL_BASE_URL}/v1/billing/subscriptions/${subscription.paypal_subscription_id}/cancel`,
      {
        reason: 'User requested cancellation'
      },
      {
        headers: {
          'Authorization': `Bearer ${token}`,
          'Content-Type': 'application/json'
        }
      }
    );
    
    // Update local database
    await prisma.subscription.update({
      where: { id },
      data: {
        status: 'cancelled',
        cancel_at_period_end: true
      }
    });
    
    res.json({
      success: true,
      message: 'Subscription will be cancelled at period end',
      period_end: subscription.current_period_end
    });
    
  } catch (error) {
    res.status(500).json({ error: 'Failed to cancel subscription' });
  }
});
```

### 6. Payment History

```typescript
// Get payment history
router.get('/payments', requireAuth, async (req: AuthRequest, res) => {
  const userId = req.user!.userId;
  const page = parseInt(req.query.page as string) || 1;
  const limit = 20;
  const skip = (page - 1) * limit;
  
  const [payments, total] = await Promise.all([
    prisma.payment.findMany({
      where: { user_id: userId },
      orderBy: { created_at: 'desc' },
      skip,
      take: limit
    }),
    prisma.payment.count({ where: { user_id: userId } })
  ]);
  
  res.json({
    payments: payments.map(p => ({
      id: p.id,
      amount: p.amount,
      currency: p.currency,
      status: p.status,
      type: p.type,
      date: p.created_at
    })),
    pagination: {
      page,
      limit,
      total,
      pages: Math.ceil(total / limit)
    }
  });
});
```

## Anti-Patterns (Forbidden)

| ❌ Mistake | ✅ Fix |
|-----------|--------|
| Storing PayPal credentials in code | Use environment variables |
| Not caching access tokens | Cache token for 1 hour |
| Trusting webhook data without verification | Always verify PayPal signatures |
| Double-charging users | Check for duplicate payment IDs |
| Not handling suspended subscriptions | Suspend user account access |
| Allowing negative credit balances | Check balance before deducting |
| Not logging payment events | Log all transactions to audit log |
| Hardcoding plan IDs in code | Store in database or env vars |
| Not handling refunds | Implement refund webhook handler |
| Immediate account termination on cancel | Allow access until period end |

## Integration with Other Skills

**Often combined with:**
- `webhook-implementation` - Processing PayPal webhooks
- `authentication-security` - Protecting payment endpoints
- `queue-management` - Scheduled credit allocation
- `error-handling-logging` - Logging payment events

**Depends on:**
- `database-schema-design` - Subscription, Payment, CreditTransaction tables

## Environment Variables Required

```bash
# PayPal Configuration
PAYPAL_MODE="sandbox"  # or "live"
PAYPAL_CLIENT_ID="your-client-id"
PAYPAL_CLIENT_SECRET="your-client-secret"

# PayPal Webhook
PAYPAL_WEBHOOK_ID="webhook-id-from-dashboard"
PAYPAL_WEBHOOK_SECRET="webhook-secret"

# Plan IDs (from PayPal Dashboard)
PAYPAL_PLAN_STARTER_MONTHLY="P-xxx"
PAYPAL_PLAN_PROFESSIONAL_MONTHLY="P-xxx"
PAYPAL_PLAN_ENTERPRISE_MONTHLY="P-xxx"
```

## Quick Reference

### Subscription Tiers

| Tier | Monthly Price | Credits | Annual Discount |
|------|---------------|---------|-----------------|
| Starter | $9 | 100K tokens | 15% |
| Professional | $29 | 500K tokens | 15% |
| Enterprise | $99 | 2M tokens | 15% |

### PayPal Webhook Events

| Event | Action |
|-------|--------|
| `BILLING.SUBSCRIPTION.CREATED` | Store subscription ID |
| `BILLING.SUBSCRIPTION.ACTIVATED` | Activate account, allocate credits |
| `PAYMENT.SALE.COMPLETED` | Record payment, extend period |
| `BILLING.SUBSCRIPTION.CANCELLED` | Mark for cancellation at period end |
| `BILLING.SUBSCRIPTION.SUSPENDED` | Suspend account |
| `BILLING.SUBSCRIPTION.UPDATED` | Sync plan changes |
| `PAYMENT.SALE.REFUNDED` | Process refund, deduct credits |

### Subscription Statuses

- `pending` - Created, awaiting user approval
- `active` - Active subscription with valid payment
- `suspended` - Payment failed, awaiting resolution
- `cancelled` - User cancelled, access until period end
- `past_due` - Payment overdue

## Validation Checklist

Before completing payment integration:

- [ ] PayPal credentials stored securely in env vars
- [ ] Access token caching implemented (1 hour)
- [ ] All webhook events handled properly
- [ ] Webhook signature verification working
- [ ] Duplicate payment prevention implemented
- [ ] Subscription status synced correctly
- [ ] Credits allocated on activation
- [ ] Credits retained until period end on cancellation
- [ ] Payment history endpoint implemented
- [ ] Refund handling implemented
- [ ] Suspended accounts blocked from API access
- [ ] Audit logs for all payment events
- [ ] Proper error handling for PayPal API failures
- [ ] Test mode with sandbox credentials
- [ ] Production plan IDs configured

## Testing

### Sandbox Testing
```bash
# PayPal Sandbox Credentials
# https://developer.paypal.com/dashboard/

# Test Credit Cards:
# Visa: 4111111111111111
# Mastercard: 5555555555554444
# Amex: 378282246310005

# Test accounts created in sandbox
```

### Manual Testing Flow
1. Create subscription via API
2. Complete payment in PayPal sandbox
3. Verify webhook received and processed
4. Check database for subscription activation
5. Verify credits allocated
6. Test cancellation flow
7. Verify access retained until period end

## Monitoring

### Key Metrics
- Subscription creation rate
- Payment success rate
- Churn rate (cancellations)
- Failed payment rate
- Average revenue per user (ARPU)

### Alerts
- Failed webhook deliveries from PayPal
- Payment failures exceeding threshold
- Unusual subscription cancellation spike
- Duplicate payment detections
