1290 lines
41 KiB
Markdown
1290 lines
41 KiB
Markdown
# Third-Party Integrations Implementation Plan
|
|
|
|
**Version:** 1.1
|
|
**Created:** January 6, 2026
|
|
**Updated:** January 7, 2026
|
|
**Status:** ✅ **IMPLEMENTATION COMPLETE**
|
|
**Covers:** FINAL-PRELAUNCH.md Phase 3.2, 3.3, and Phase 4
|
|
|
|
---
|
|
|
|
## Implementation Status
|
|
|
|
| Phase | Component | Status | Notes |
|
|
|-------|-----------|--------|-------|
|
|
| 3.2 | Stripe Integration | ✅ Complete | Full checkout, billing portal, webhooks |
|
|
| 3.2 | PayPal Integration | ✅ Complete | REST API v2, orders, subscriptions |
|
|
| 3.3 | Plan Upgrade Flow | ✅ Complete | Stripe/PayPal/Manual payment selection |
|
|
| 3.3 | Credit Purchase Flow | ✅ Complete | One-time credit package checkout |
|
|
| 4 | Resend Email Service | ✅ Complete | Transactional emails with templates |
|
|
| 4 | Brevo Marketing | ⏸️ Deferred | Future implementation |
|
|
|
|
---
|
|
|
|
## Executive Summary
|
|
|
|
This document provides complete implementation details for:
|
|
- **Phase 3.2:** Stripe & PayPal payment integration
|
|
- **Phase 3.3:** Plans & packages (upgrade flows, service packages)
|
|
- **Phase 4:** Email services (Resend for transactional, Brevo for marketing)
|
|
|
|
All integrations use the existing `IntegrationProvider` model in `/backend/igny8_core/modules/system/models.py` for centralized API key management.
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
1. [Existing Infrastructure](#1-existing-infrastructure)
|
|
2. [Phase 3.2: Payment Integration](#2-phase-32-payment-integration)
|
|
3. [Phase 3.3: Plans & Packages](#3-phase-33-plans--packages)
|
|
4. [Phase 4: Email Services](#4-phase-4-email-services)
|
|
5. [Required Credentials Checklist](#5-required-credentials-checklist)
|
|
6. [Implementation Order](#6-implementation-order)
|
|
|
|
---
|
|
|
|
## 1. Existing Infrastructure
|
|
|
|
### 1.1 IntegrationProvider Model
|
|
|
|
**Location:** `backend/igny8_core/modules/system/models.py`
|
|
|
|
The system already has a centralized model for all third-party credentials:
|
|
|
|
```python
|
|
class IntegrationProvider(models.Model):
|
|
provider_id = CharField(primary_key=True) # 'stripe', 'paypal', 'resend'
|
|
display_name = CharField(max_length=100)
|
|
provider_type = CharField(choices=['ai', 'payment', 'email', 'storage'])
|
|
api_key = CharField(max_length=500) # Primary API key
|
|
api_secret = CharField(max_length=500) # Secondary secret
|
|
webhook_secret = CharField(max_length=500) # Webhook signing secret
|
|
api_endpoint = URLField() # Custom endpoint (PayPal sandbox/live)
|
|
config = JSONField() # Provider-specific config
|
|
is_active = BooleanField()
|
|
is_sandbox = BooleanField()
|
|
```
|
|
|
|
**Pre-seeded Providers (from migration 0016):**
|
|
|
|
| Provider ID | Type | Purpose | Status |
|
|
|------------|------|---------|--------|
|
|
| `stripe` | payment | Card payments, subscriptions | Active (sandbox) |
|
|
| `paypal` | payment | PayPal payments | Active (sandbox) |
|
|
| `resend` | email | Transactional emails | Active |
|
|
|
|
### 1.2 Existing Billing Models
|
|
|
|
**Location:** `backend/igny8_core/business/billing/models.py`
|
|
|
|
| Model | Purpose |
|
|
|-------|---------|
|
|
| `Payment` | Payment records with Stripe/PayPal/Manual support |
|
|
| `Invoice` | Invoice records with line items |
|
|
| `CreditPackage` | One-time credit purchase packages |
|
|
| `CreditTransaction` | Credit ledger |
|
|
| `Subscription` (in auth) | Account subscription tracking |
|
|
| `Plan` (in auth) | Plan definitions with Stripe IDs |
|
|
|
|
### 1.3 Existing Services
|
|
|
|
| Service | Location | Status |
|
|
|---------|----------|--------|
|
|
| `PaymentService` | `business/billing/services/payment_service.py` | ✅ Complete |
|
|
| `InvoiceService` | `business/billing/services/invoice_service.py` | ✅ Complete |
|
|
| `StripeService` | `business/billing/services/stripe_service.py` | ✅ **NEW - Complete** |
|
|
| `PayPalService` | `business/billing/services/paypal_service.py` | ✅ **NEW - Complete** |
|
|
| `EmailService` | `business/billing/services/email_service.py` | ✅ **Updated - Resend Integration** |
|
|
|
|
---
|
|
|
|
## 2. Phase 3.2: Payment Integration
|
|
|
|
### 2.1 Stripe Integration
|
|
|
|
#### 2.1.1 Required Credentials from Stripe Dashboard
|
|
|
|
**From [dashboard.stripe.com](https://dashboard.stripe.com):**
|
|
|
|
| Credential | Where to Find | Store In |
|
|
|------------|---------------|----------|
|
|
| **Publishable Key** | Developers → API Keys → Publishable key | `IntegrationProvider.api_key` |
|
|
| **Secret Key** | Developers → API Keys → Secret key | `IntegrationProvider.api_secret` |
|
|
| **Webhook Signing Secret** | Developers → Webhooks → Endpoint → Signing secret | `IntegrationProvider.webhook_secret` |
|
|
|
|
**Webhook Endpoint to Configure:**
|
|
```
|
|
Production: https://api.igny8.com/api/v1/billing/webhooks/stripe/
|
|
Sandbox: https://api-staging.igny8.com/api/v1/billing/webhooks/stripe/
|
|
```
|
|
|
|
**Events to Subscribe:**
|
|
- `checkout.session.completed` - New subscription
|
|
- `invoice.paid` - Recurring payment success
|
|
- `invoice.payment_failed` - Payment failure
|
|
- `customer.subscription.updated` - Plan changes
|
|
- `customer.subscription.deleted` - Cancellation
|
|
|
|
#### 2.1.2 IntegrationProvider Configuration
|
|
|
|
```json
|
|
{
|
|
"provider_id": "stripe",
|
|
"display_name": "Stripe",
|
|
"provider_type": "payment",
|
|
"api_key": "pk_live_xxx...", // Publishable key
|
|
"api_secret": "sk_live_xxx...", // Secret key
|
|
"webhook_secret": "whsec_xxx...", // Webhook secret
|
|
"config": {
|
|
"currency": "usd",
|
|
"payment_methods": ["card"],
|
|
"billing_portal_enabled": true
|
|
},
|
|
"is_active": true,
|
|
"is_sandbox": false // Set to true for test mode
|
|
}
|
|
```
|
|
|
|
#### 2.1.3 Backend Implementation
|
|
|
|
**New Files to Create:**
|
|
|
|
```
|
|
backend/igny8_core/business/billing/
|
|
├── services/
|
|
│ └── stripe_service.py # Stripe API wrapper
|
|
├── views/
|
|
│ └── stripe_views.py # Checkout, portal, webhooks
|
|
└── tasks/
|
|
└── stripe_sync.py # Sync jobs (optional)
|
|
```
|
|
|
|
**stripe_service.py:**
|
|
|
|
```python
|
|
"""
|
|
Stripe Service - Wrapper for Stripe API operations
|
|
"""
|
|
import stripe
|
|
from django.conf import settings
|
|
from igny8_core.modules.system.models import IntegrationProvider
|
|
|
|
|
|
class StripeService:
|
|
"""Service for Stripe payment operations"""
|
|
|
|
def __init__(self):
|
|
provider = IntegrationProvider.get_provider('stripe')
|
|
if not provider:
|
|
raise ValueError("Stripe provider not configured")
|
|
|
|
self.is_sandbox = provider.is_sandbox
|
|
stripe.api_key = provider.api_secret
|
|
self.publishable_key = provider.api_key
|
|
self.webhook_secret = provider.webhook_secret
|
|
self.config = provider.config or {}
|
|
|
|
def create_checkout_session(self, account, plan, success_url, cancel_url):
|
|
"""
|
|
Create Stripe Checkout session for new subscription
|
|
"""
|
|
session = stripe.checkout.Session.create(
|
|
customer_email=account.billing_email or account.owner.email,
|
|
payment_method_types=['card'],
|
|
mode='subscription',
|
|
line_items=[{
|
|
'price': plan.stripe_price_id,
|
|
'quantity': 1,
|
|
}],
|
|
success_url=success_url,
|
|
cancel_url=cancel_url,
|
|
metadata={
|
|
'account_id': str(account.id),
|
|
'plan_id': str(plan.id),
|
|
},
|
|
subscription_data={
|
|
'metadata': {
|
|
'account_id': str(account.id),
|
|
}
|
|
}
|
|
)
|
|
return session
|
|
|
|
def create_credit_checkout_session(self, account, credit_package, success_url, cancel_url):
|
|
"""
|
|
Create Stripe Checkout for one-time credit purchase
|
|
"""
|
|
session = stripe.checkout.Session.create(
|
|
customer_email=account.billing_email or account.owner.email,
|
|
payment_method_types=['card'],
|
|
mode='payment',
|
|
line_items=[{
|
|
'price_data': {
|
|
'currency': self.config.get('currency', 'usd'),
|
|
'product_data': {
|
|
'name': credit_package.name,
|
|
'description': f'{credit_package.credits} credits',
|
|
},
|
|
'unit_amount': int(credit_package.price * 100), # Cents
|
|
},
|
|
'quantity': 1,
|
|
}],
|
|
success_url=success_url,
|
|
cancel_url=cancel_url,
|
|
metadata={
|
|
'account_id': str(account.id),
|
|
'credit_package_id': str(credit_package.id),
|
|
'credit_amount': credit_package.credits,
|
|
}
|
|
)
|
|
return session
|
|
|
|
def create_billing_portal_session(self, account, return_url):
|
|
"""
|
|
Create Stripe Billing Portal session for subscription management
|
|
"""
|
|
# Get or create Stripe customer
|
|
customer_id = self._get_or_create_customer(account)
|
|
|
|
session = stripe.billing_portal.Session.create(
|
|
customer=customer_id,
|
|
return_url=return_url,
|
|
)
|
|
return session
|
|
|
|
def construct_webhook_event(self, payload, sig_header):
|
|
"""
|
|
Verify and construct webhook event
|
|
"""
|
|
return stripe.Webhook.construct_event(
|
|
payload, sig_header, self.webhook_secret
|
|
)
|
|
|
|
def _get_or_create_customer(self, account):
|
|
"""Get existing Stripe customer or create new one"""
|
|
if account.stripe_customer_id:
|
|
return account.stripe_customer_id
|
|
|
|
customer = stripe.Customer.create(
|
|
email=account.billing_email or account.owner.email,
|
|
name=account.name,
|
|
metadata={'account_id': str(account.id)},
|
|
)
|
|
|
|
account.stripe_customer_id = customer.id
|
|
account.save(update_fields=['stripe_customer_id'])
|
|
|
|
return customer.id
|
|
```
|
|
|
|
**stripe_views.py:**
|
|
|
|
```python
|
|
"""
|
|
Stripe Views - Checkout, Portal, and Webhook endpoints
|
|
"""
|
|
from rest_framework.views import APIView
|
|
from rest_framework.decorators import api_view, permission_classes
|
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
|
from rest_framework.response import Response
|
|
from rest_framework import status
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
import json
|
|
import logging
|
|
|
|
from .stripe_service import StripeService
|
|
from ..services.payment_service import PaymentService
|
|
from igny8_core.auth.models import Plan, Account
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class StripeCheckoutView(APIView):
|
|
"""Create Stripe Checkout session"""
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def post(self, request):
|
|
account = request.user.account
|
|
plan_id = request.data.get('plan_id')
|
|
|
|
try:
|
|
plan = Plan.objects.get(id=plan_id, is_active=True)
|
|
except Plan.DoesNotExist:
|
|
return Response({'error': 'Plan not found'}, status=404)
|
|
|
|
if not plan.stripe_price_id:
|
|
return Response({'error': 'Plan not configured for Stripe'}, status=400)
|
|
|
|
service = StripeService()
|
|
frontend_url = settings.FRONTEND_URL
|
|
|
|
session = service.create_checkout_session(
|
|
account=account,
|
|
plan=plan,
|
|
success_url=f'{frontend_url}/account/plans?success=true',
|
|
cancel_url=f'{frontend_url}/account/plans?canceled=true',
|
|
)
|
|
|
|
return Response({
|
|
'checkout_url': session.url,
|
|
'session_id': session.id,
|
|
})
|
|
|
|
|
|
class StripeCreditCheckoutView(APIView):
|
|
"""Create Stripe Checkout for credit package"""
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def post(self, request):
|
|
from ..models import CreditPackage
|
|
|
|
account = request.user.account
|
|
package_id = request.data.get('package_id')
|
|
|
|
try:
|
|
package = CreditPackage.objects.get(id=package_id, is_active=True)
|
|
except CreditPackage.DoesNotExist:
|
|
return Response({'error': 'Credit package not found'}, status=404)
|
|
|
|
service = StripeService()
|
|
frontend_url = settings.FRONTEND_URL
|
|
|
|
session = service.create_credit_checkout_session(
|
|
account=account,
|
|
credit_package=package,
|
|
success_url=f'{frontend_url}/account/usage?purchase=success',
|
|
cancel_url=f'{frontend_url}/account/usage?purchase=canceled',
|
|
)
|
|
|
|
return Response({
|
|
'checkout_url': session.url,
|
|
'session_id': session.id,
|
|
})
|
|
|
|
|
|
class StripeBillingPortalView(APIView):
|
|
"""Create Stripe Billing Portal session"""
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def post(self, request):
|
|
account = request.user.account
|
|
|
|
service = StripeService()
|
|
frontend_url = settings.FRONTEND_URL
|
|
|
|
session = service.create_billing_portal_session(
|
|
account=account,
|
|
return_url=f'{frontend_url}/account/plans',
|
|
)
|
|
|
|
return Response({
|
|
'portal_url': session.url,
|
|
})
|
|
|
|
|
|
@csrf_exempt
|
|
@api_view(['POST'])
|
|
@permission_classes([AllowAny])
|
|
def stripe_webhook(request):
|
|
"""
|
|
Handle Stripe webhook events
|
|
|
|
Events handled:
|
|
- checkout.session.completed: New subscription or credit purchase
|
|
- invoice.paid: Recurring payment success
|
|
- invoice.payment_failed: Payment failure
|
|
- customer.subscription.updated: Plan changes
|
|
- customer.subscription.deleted: Cancellation
|
|
"""
|
|
payload = request.body
|
|
sig_header = request.META.get('HTTP_STRIPE_SIGNATURE')
|
|
|
|
try:
|
|
service = StripeService()
|
|
event = service.construct_webhook_event(payload, sig_header)
|
|
except Exception as e:
|
|
logger.error(f'Stripe webhook error: {str(e)}')
|
|
return Response({'error': str(e)}, status=400)
|
|
|
|
event_type = event['type']
|
|
data = event['data']['object']
|
|
|
|
logger.info(f'Stripe webhook received: {event_type}')
|
|
|
|
if event_type == 'checkout.session.completed':
|
|
_handle_checkout_completed(data)
|
|
elif event_type == 'invoice.paid':
|
|
_handle_invoice_paid(data)
|
|
elif event_type == 'invoice.payment_failed':
|
|
_handle_payment_failed(data)
|
|
elif event_type == 'customer.subscription.updated':
|
|
_handle_subscription_updated(data)
|
|
elif event_type == 'customer.subscription.deleted':
|
|
_handle_subscription_deleted(data)
|
|
|
|
return Response({'status': 'success'})
|
|
|
|
|
|
def _handle_checkout_completed(session):
|
|
"""Handle successful checkout"""
|
|
metadata = session.get('metadata', {})
|
|
account_id = metadata.get('account_id')
|
|
|
|
if not account_id:
|
|
logger.error('No account_id in checkout session metadata')
|
|
return
|
|
|
|
try:
|
|
account = Account.objects.get(id=account_id)
|
|
except Account.DoesNotExist:
|
|
logger.error(f'Account {account_id} not found')
|
|
return
|
|
|
|
if session.get('mode') == 'subscription':
|
|
# Handle new subscription
|
|
subscription_id = session.get('subscription')
|
|
_activate_subscription(account, subscription_id, metadata)
|
|
elif session.get('mode') == 'payment':
|
|
# Handle credit purchase
|
|
credit_package_id = metadata.get('credit_package_id')
|
|
credit_amount = metadata.get('credit_amount')
|
|
_add_purchased_credits(account, credit_package_id, credit_amount)
|
|
|
|
|
|
def _activate_subscription(account, stripe_subscription_id, metadata):
|
|
"""Activate subscription after successful payment"""
|
|
import stripe
|
|
from django.utils import timezone
|
|
from igny8_core.auth.models import Subscription, Plan
|
|
|
|
# Get subscription details from Stripe
|
|
stripe_sub = stripe.Subscription.retrieve(stripe_subscription_id)
|
|
|
|
plan_id = metadata.get('plan_id')
|
|
plan = Plan.objects.get(id=plan_id)
|
|
|
|
# Update or create subscription
|
|
subscription, created = Subscription.objects.update_or_create(
|
|
account=account,
|
|
defaults={
|
|
'plan': plan,
|
|
'stripe_subscription_id': stripe_subscription_id,
|
|
'status': 'active',
|
|
'current_period_start': timezone.datetime.fromtimestamp(
|
|
stripe_sub.current_period_start, tz=timezone.utc
|
|
),
|
|
'current_period_end': timezone.datetime.fromtimestamp(
|
|
stripe_sub.current_period_end, tz=timezone.utc
|
|
),
|
|
}
|
|
)
|
|
|
|
# Add initial credits
|
|
from ..services.credit_service import CreditService
|
|
CreditService.add_credits(
|
|
account=account,
|
|
amount=plan.included_credits,
|
|
transaction_type='subscription',
|
|
description=f'Subscription: {plan.name}'
|
|
)
|
|
|
|
logger.info(f'Subscription activated for account {account.id}')
|
|
|
|
|
|
def _add_purchased_credits(account, credit_package_id, credit_amount):
|
|
"""Add purchased credits to account"""
|
|
from ..services.credit_service import CreditService
|
|
|
|
credit_amount = int(credit_amount)
|
|
|
|
CreditService.add_credits(
|
|
account=account,
|
|
amount=credit_amount,
|
|
transaction_type='purchase',
|
|
description=f'Credit package purchase ({credit_amount} credits)'
|
|
)
|
|
|
|
logger.info(f'Added {credit_amount} credits to account {account.id}')
|
|
|
|
|
|
def _handle_invoice_paid(invoice):
|
|
"""Handle successful recurring payment"""
|
|
subscription_id = invoice.get('subscription')
|
|
if not subscription_id:
|
|
return
|
|
|
|
from igny8_core.auth.models import Subscription
|
|
|
|
try:
|
|
subscription = Subscription.objects.get(stripe_subscription_id=subscription_id)
|
|
except Subscription.DoesNotExist:
|
|
return
|
|
|
|
# Add monthly credits
|
|
from ..services.credit_service import CreditService
|
|
CreditService.add_credits(
|
|
account=subscription.account,
|
|
amount=subscription.plan.included_credits,
|
|
transaction_type='subscription',
|
|
description=f'Monthly renewal: {subscription.plan.name}'
|
|
)
|
|
|
|
|
|
def _handle_payment_failed(invoice):
|
|
"""Handle failed payment"""
|
|
subscription_id = invoice.get('subscription')
|
|
if not subscription_id:
|
|
return
|
|
|
|
from igny8_core.auth.models import Subscription
|
|
|
|
try:
|
|
subscription = Subscription.objects.get(stripe_subscription_id=subscription_id)
|
|
subscription.status = 'past_due'
|
|
subscription.save()
|
|
except Subscription.DoesNotExist:
|
|
pass
|
|
|
|
# TODO: Send notification email
|
|
|
|
|
|
def _handle_subscription_updated(subscription_data):
|
|
"""Handle subscription update (plan change)"""
|
|
subscription_id = subscription_data.get('id')
|
|
|
|
from igny8_core.auth.models import Subscription
|
|
|
|
try:
|
|
subscription = Subscription.objects.get(stripe_subscription_id=subscription_id)
|
|
# Update status
|
|
status_map = {
|
|
'active': 'active',
|
|
'past_due': 'past_due',
|
|
'canceled': 'canceled',
|
|
'trialing': 'trialing',
|
|
}
|
|
stripe_status = subscription_data.get('status')
|
|
subscription.status = status_map.get(stripe_status, 'active')
|
|
subscription.save()
|
|
except Subscription.DoesNotExist:
|
|
pass
|
|
|
|
|
|
def _handle_subscription_deleted(subscription_data):
|
|
"""Handle subscription cancellation"""
|
|
subscription_id = subscription_data.get('id')
|
|
|
|
from igny8_core.auth.models import Subscription
|
|
|
|
try:
|
|
subscription = Subscription.objects.get(stripe_subscription_id=subscription_id)
|
|
subscription.status = 'canceled'
|
|
subscription.save()
|
|
except Subscription.DoesNotExist:
|
|
pass
|
|
```
|
|
|
|
#### 2.1.4 Frontend Implementation
|
|
|
|
**New API calls in `frontend/src/services/billing.api.ts`:**
|
|
|
|
```typescript
|
|
export const createStripeCheckout = async (planId: string): Promise<{ checkout_url: string }> => {
|
|
const response = await api.post('/billing/stripe/checkout/', { plan_id: planId });
|
|
return response.data;
|
|
};
|
|
|
|
export const createStripeCreditCheckout = async (packageId: string): Promise<{ checkout_url: string }> => {
|
|
const response = await api.post('/billing/stripe/credit-checkout/', { package_id: packageId });
|
|
return response.data;
|
|
};
|
|
|
|
export const openStripeBillingPortal = async (): Promise<{ portal_url: string }> => {
|
|
const response = await api.post('/billing/stripe/billing-portal/');
|
|
return response.data;
|
|
};
|
|
```
|
|
|
|
**Button handlers:**
|
|
|
|
```typescript
|
|
// Subscribe to plan
|
|
const handleSubscribe = async (planId: string) => {
|
|
const { checkout_url } = await createStripeCheckout(planId);
|
|
window.location.href = checkout_url;
|
|
};
|
|
|
|
// Buy credits
|
|
const handleBuyCredits = async (packageId: string) => {
|
|
const { checkout_url } = await createStripeCreditCheckout(packageId);
|
|
window.location.href = checkout_url;
|
|
};
|
|
|
|
// Manage subscription
|
|
const handleManageSubscription = async () => {
|
|
const { portal_url } = await openStripeBillingPortal();
|
|
window.location.href = portal_url;
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
### 2.2 PayPal Integration
|
|
|
|
#### 2.2.1 Required Credentials from PayPal Developer Dashboard
|
|
|
|
**From [developer.paypal.com](https://developer.paypal.com):**
|
|
|
|
| Credential | Where to Find | Store In |
|
|
|------------|---------------|----------|
|
|
| **Client ID** | My Apps & Credentials → App → Client ID | `IntegrationProvider.api_key` |
|
|
| **Client Secret** | My Apps & Credentials → App → Secret | `IntegrationProvider.api_secret` |
|
|
| **Webhook ID** | My Apps & Credentials → Webhooks → Webhook ID | `config.webhook_id` |
|
|
|
|
**Endpoints:**
|
|
- Sandbox: `https://api-m.sandbox.paypal.com`
|
|
- Production: `https://api-m.paypal.com`
|
|
|
|
**Webhook Events to Subscribe:**
|
|
- `CHECKOUT.ORDER.APPROVED`
|
|
- `PAYMENT.CAPTURE.COMPLETED`
|
|
- `BILLING.SUBSCRIPTION.ACTIVATED`
|
|
- `BILLING.SUBSCRIPTION.CANCELLED`
|
|
- `PAYMENT.CAPTURE.DENIED`
|
|
|
|
#### 2.2.2 IntegrationProvider Configuration
|
|
|
|
```json
|
|
{
|
|
"provider_id": "paypal",
|
|
"display_name": "PayPal",
|
|
"provider_type": "payment",
|
|
"api_key": "AY...", // Client ID
|
|
"api_secret": "EL...", // Client Secret
|
|
"webhook_secret": "", // Not used for PayPal (uses webhook ID)
|
|
"api_endpoint": "https://api-m.paypal.com", // or sandbox
|
|
"config": {
|
|
"currency": "USD",
|
|
"webhook_id": "WH-xxx...",
|
|
"return_url": "https://app.igny8.com/account/plans?paypal=success",
|
|
"cancel_url": "https://app.igny8.com/account/plans?paypal=cancel"
|
|
},
|
|
"is_active": true,
|
|
"is_sandbox": false
|
|
}
|
|
```
|
|
|
|
#### 2.2.3 Backend Implementation
|
|
|
|
**paypal_service.py:**
|
|
|
|
```python
|
|
"""
|
|
PayPal Service - REST API v2 integration
|
|
"""
|
|
import requests
|
|
import base64
|
|
from django.conf import settings
|
|
from igny8_core.modules.system.models import IntegrationProvider
|
|
|
|
|
|
class PayPalService:
|
|
"""Service for PayPal payment operations"""
|
|
|
|
def __init__(self):
|
|
provider = IntegrationProvider.get_provider('paypal')
|
|
if not provider:
|
|
raise ValueError("PayPal provider not configured")
|
|
|
|
self.client_id = provider.api_key
|
|
self.client_secret = provider.api_secret
|
|
self.base_url = provider.api_endpoint or 'https://api-m.paypal.com'
|
|
self.config = provider.config or {}
|
|
self.is_sandbox = provider.is_sandbox
|
|
|
|
self._access_token = None
|
|
|
|
def _get_access_token(self):
|
|
"""Get OAuth access token"""
|
|
if self._access_token:
|
|
return self._access_token
|
|
|
|
auth = base64.b64encode(
|
|
f'{self.client_id}:{self.client_secret}'.encode()
|
|
).decode()
|
|
|
|
response = requests.post(
|
|
f'{self.base_url}/v1/oauth2/token',
|
|
headers={
|
|
'Authorization': f'Basic {auth}',
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
},
|
|
data='grant_type=client_credentials'
|
|
)
|
|
response.raise_for_status()
|
|
|
|
self._access_token = response.json()['access_token']
|
|
return self._access_token
|
|
|
|
def _make_request(self, method, endpoint, **kwargs):
|
|
"""Make authenticated API request"""
|
|
token = self._get_access_token()
|
|
headers = kwargs.pop('headers', {})
|
|
headers['Authorization'] = f'Bearer {token}'
|
|
headers['Content-Type'] = 'application/json'
|
|
|
|
url = f'{self.base_url}{endpoint}'
|
|
response = requests.request(method, url, headers=headers, **kwargs)
|
|
response.raise_for_status()
|
|
return response.json()
|
|
|
|
def create_order(self, account, amount, currency='USD', description=''):
|
|
"""
|
|
Create PayPal order for one-time payment
|
|
"""
|
|
order = self._make_request('POST', '/v2/checkout/orders', json={
|
|
'intent': 'CAPTURE',
|
|
'purchase_units': [{
|
|
'amount': {
|
|
'currency_code': currency,
|
|
'value': str(amount),
|
|
},
|
|
'description': description,
|
|
'custom_id': str(account.id),
|
|
}],
|
|
'application_context': {
|
|
'return_url': self.config.get('return_url'),
|
|
'cancel_url': self.config.get('cancel_url'),
|
|
'brand_name': 'IGNY8',
|
|
'landing_page': 'BILLING',
|
|
'user_action': 'PAY_NOW',
|
|
}
|
|
})
|
|
|
|
return order
|
|
|
|
def capture_order(self, order_id):
|
|
"""
|
|
Capture payment for approved order
|
|
"""
|
|
return self._make_request('POST', f'/v2/checkout/orders/{order_id}/capture')
|
|
|
|
def get_order(self, order_id):
|
|
"""
|
|
Get order details
|
|
"""
|
|
return self._make_request('GET', f'/v2/checkout/orders/{order_id}')
|
|
|
|
def create_subscription(self, account, plan_id):
|
|
"""
|
|
Create PayPal subscription
|
|
Requires plan to be created in PayPal dashboard first
|
|
"""
|
|
subscription = self._make_request('POST', '/v1/billing/subscriptions', json={
|
|
'plan_id': plan_id, # PayPal plan ID
|
|
'custom_id': str(account.id),
|
|
'application_context': {
|
|
'return_url': self.config.get('return_url'),
|
|
'cancel_url': self.config.get('cancel_url'),
|
|
'brand_name': 'IGNY8',
|
|
'user_action': 'SUBSCRIBE_NOW',
|
|
}
|
|
})
|
|
|
|
return subscription
|
|
|
|
def verify_webhook_signature(self, headers, body):
|
|
"""
|
|
Verify webhook signature
|
|
"""
|
|
verification = self._make_request('POST', '/v1/notifications/verify-webhook-signature', json={
|
|
'auth_algo': headers.get('PAYPAL-AUTH-ALGO'),
|
|
'cert_url': headers.get('PAYPAL-CERT-URL'),
|
|
'transmission_id': headers.get('PAYPAL-TRANSMISSION-ID'),
|
|
'transmission_sig': headers.get('PAYPAL-TRANSMISSION-SIG'),
|
|
'transmission_time': headers.get('PAYPAL-TRANSMISSION-TIME'),
|
|
'webhook_id': self.config.get('webhook_id'),
|
|
'webhook_event': body,
|
|
})
|
|
|
|
return verification.get('verification_status') == 'SUCCESS'
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Phase 3.3: Plans & Packages
|
|
|
|
### 3.1 Plan Upgrade Flow
|
|
|
|
**User Journey:**
|
|
1. User on Plans page sees current plan and upgrade options
|
|
2. Clicks "Upgrade" on desired plan
|
|
3. Redirected to Stripe Checkout (or PayPal)
|
|
4. Completes payment
|
|
5. Webhook triggers:
|
|
- Updates Subscription to new plan
|
|
- Prorates credits (optional)
|
|
6. User redirected back with success message
|
|
|
|
**Stripe Proration:**
|
|
Stripe handles proration automatically when upgrading. The `subscription_data` can include:
|
|
|
|
```python
|
|
subscription_data={
|
|
'proration_behavior': 'create_prorations', # or 'always_invoice'
|
|
}
|
|
```
|
|
|
|
### 3.2 Credit Package Purchase Flow
|
|
|
|
**Current Packages (from CreditPackage model):**
|
|
|
|
| Package | Credits | Price | Value |
|
|
|---------|---------|-------|-------|
|
|
| Starter | 500 | $9.99 | ~$0.02/credit |
|
|
| Value | 2,000 | $29.99 | ~$0.015/credit |
|
|
| Pro | 5,000 | $59.99 | ~$0.012/credit |
|
|
| Enterprise | 15,000 | $149.99 | ~$0.01/credit |
|
|
|
|
**Purchase Flow:**
|
|
1. User on Usage page clicks "Buy Credits"
|
|
2. Selects package
|
|
3. Redirected to Stripe/PayPal Checkout
|
|
4. Completes payment
|
|
5. Webhook adds credits to account
|
|
6. User redirected back to Usage page
|
|
|
|
### 3.3 Service Packages (Future)
|
|
|
|
| Package | Type | Price | Includes |
|
|
|---------|------|-------|----------|
|
|
| Setup App | One-time | $299 | Initial config, 1hr onboarding |
|
|
| Campaign Mgmt | Monthly | $199/mo | 10 keywords/mo, content review |
|
|
|
|
**Implementation:** Create `ServicePackage` model similar to `CreditPackage`.
|
|
|
|
---
|
|
|
|
## 4. Phase 4: Email Services
|
|
|
|
### 4.1 Resend (Transactional)
|
|
|
|
#### 4.1.1 Required Credentials
|
|
|
|
**From [resend.com/api-keys](https://resend.com/api-keys):**
|
|
|
|
| Credential | Where to Find | Store In |
|
|
|------------|---------------|----------|
|
|
| **API Key** | API Keys → Create API Key | `IntegrationProvider.api_key` |
|
|
|
|
**Domain Verification:**
|
|
1. Add domain in Resend dashboard
|
|
2. Add DNS records (DKIM, SPF, DMARC)
|
|
3. Verify domain
|
|
|
|
#### 4.1.2 IntegrationProvider Configuration
|
|
|
|
```json
|
|
{
|
|
"provider_id": "resend",
|
|
"display_name": "Resend",
|
|
"provider_type": "email",
|
|
"api_key": "re_xxx...",
|
|
"config": {
|
|
"from_email": "noreply@igny8.com",
|
|
"from_name": "IGNY8",
|
|
"reply_to": "support@igny8.com"
|
|
},
|
|
"is_active": true
|
|
}
|
|
```
|
|
|
|
#### 4.1.3 Requirements.txt Addition
|
|
|
|
```
|
|
resend>=0.7.0
|
|
```
|
|
|
|
#### 4.1.4 Email Service Implementation
|
|
|
|
**email_service.py (Updated):**
|
|
|
|
```python
|
|
"""
|
|
Email Service - Multi-provider email sending
|
|
Uses Resend for transactional, Brevo for marketing
|
|
"""
|
|
import resend
|
|
import logging
|
|
from typing import Optional, List, Dict
|
|
from django.template.loader import render_to_string
|
|
from igny8_core.modules.system.models import IntegrationProvider
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class EmailService:
|
|
"""Unified email service supporting multiple providers"""
|
|
|
|
def __init__(self):
|
|
self._resend_configured = False
|
|
self._brevo_configured = False
|
|
self._setup_providers()
|
|
|
|
def _setup_providers(self):
|
|
"""Initialize email providers"""
|
|
# Setup Resend
|
|
resend_provider = IntegrationProvider.get_provider('resend')
|
|
if resend_provider and resend_provider.api_key:
|
|
resend.api_key = resend_provider.api_key
|
|
self._resend_config = resend_provider.config or {}
|
|
self._resend_configured = True
|
|
|
|
# Setup Brevo (future)
|
|
brevo_provider = IntegrationProvider.get_provider('brevo')
|
|
if brevo_provider and brevo_provider.api_key:
|
|
self._brevo_config = brevo_provider.config or {}
|
|
self._brevo_configured = True
|
|
|
|
def send_transactional(
|
|
self,
|
|
to: str,
|
|
subject: str,
|
|
html: Optional[str] = None,
|
|
text: Optional[str] = None,
|
|
template: Optional[str] = None,
|
|
context: Optional[Dict] = None,
|
|
from_email: Optional[str] = None,
|
|
reply_to: Optional[str] = None,
|
|
attachments: Optional[List] = None,
|
|
):
|
|
"""
|
|
Send transactional email via Resend
|
|
|
|
Args:
|
|
to: Recipient email
|
|
subject: Email subject
|
|
html: HTML content (or use template)
|
|
text: Plain text content
|
|
template: Django template path (e.g., 'emails/welcome.html')
|
|
context: Template context
|
|
from_email: Override sender
|
|
reply_to: Reply-to address
|
|
attachments: List of attachments
|
|
"""
|
|
if not self._resend_configured:
|
|
logger.error("Resend not configured - falling back to Django mail")
|
|
return self._send_django_mail(to, subject, text or html)
|
|
|
|
# Render template if provided
|
|
if template:
|
|
html = render_to_string(template, context or {})
|
|
|
|
try:
|
|
params = {
|
|
'from': from_email or f"{self._resend_config.get('from_name', 'IGNY8')} <{self._resend_config.get('from_email', 'noreply@igny8.com')}>",
|
|
'to': [to] if isinstance(to, str) else to,
|
|
'subject': subject,
|
|
}
|
|
|
|
if html:
|
|
params['html'] = html
|
|
if text:
|
|
params['text'] = text
|
|
if reply_to:
|
|
params['reply_to'] = reply_to
|
|
|
|
response = resend.Emails.send(params)
|
|
|
|
logger.info(f"Email sent via Resend: {subject} to {to}")
|
|
return response
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to send email via Resend: {str(e)}")
|
|
raise
|
|
|
|
def _send_django_mail(self, to, subject, message):
|
|
"""Fallback to Django's send_mail"""
|
|
from django.core.mail import send_mail
|
|
from django.conf import settings
|
|
|
|
send_mail(
|
|
subject=subject,
|
|
message=message,
|
|
from_email=settings.DEFAULT_FROM_EMAIL,
|
|
recipient_list=[to],
|
|
fail_silently=False,
|
|
)
|
|
|
|
|
|
# Singleton instance
|
|
_email_service = None
|
|
|
|
def get_email_service():
|
|
global _email_service
|
|
if _email_service is None:
|
|
_email_service = EmailService()
|
|
return _email_service
|
|
|
|
|
|
# Convenience functions
|
|
def send_welcome_email(user, account):
|
|
"""Send welcome email after signup"""
|
|
service = get_email_service()
|
|
service.send_transactional(
|
|
to=user.email,
|
|
subject='Welcome to IGNY8!',
|
|
template='emails/welcome.html',
|
|
context={
|
|
'user': user,
|
|
'account': account,
|
|
'login_url': f'{settings.FRONTEND_URL}/login',
|
|
}
|
|
)
|
|
|
|
|
|
def send_password_reset_email(user, reset_token):
|
|
"""Send password reset email"""
|
|
service = get_email_service()
|
|
service.send_transactional(
|
|
to=user.email,
|
|
subject='Reset Your IGNY8 Password',
|
|
template='emails/password_reset.html',
|
|
context={
|
|
'user': user,
|
|
'reset_url': f'{settings.FRONTEND_URL}/reset-password?token={reset_token}',
|
|
}
|
|
)
|
|
|
|
|
|
def send_payment_confirmation(account, payment):
|
|
"""Send payment confirmation email"""
|
|
service = get_email_service()
|
|
service.send_transactional(
|
|
to=account.billing_email or account.owner.email,
|
|
subject=f'Payment Confirmed - Invoice #{payment.invoice.invoice_number}',
|
|
template='emails/payment_confirmed.html',
|
|
context={
|
|
'account': account,
|
|
'payment': payment,
|
|
}
|
|
)
|
|
|
|
|
|
def send_subscription_activated(account, subscription):
|
|
"""Send subscription activation email"""
|
|
service = get_email_service()
|
|
service.send_transactional(
|
|
to=account.billing_email or account.owner.email,
|
|
subject=f'Subscription Activated - {subscription.plan.name}',
|
|
template='emails/subscription_activated.html',
|
|
context={
|
|
'account': account,
|
|
'subscription': subscription,
|
|
'plan': subscription.plan,
|
|
}
|
|
)
|
|
|
|
|
|
def send_low_credits_warning(account, current_credits, threshold):
|
|
"""Send low credits warning"""
|
|
service = get_email_service()
|
|
service.send_transactional(
|
|
to=account.billing_email or account.owner.email,
|
|
subject='Low Credits Warning - IGNY8',
|
|
template='emails/low_credits.html',
|
|
context={
|
|
'account': account,
|
|
'current_credits': current_credits,
|
|
'threshold': threshold,
|
|
'topup_url': f'{settings.FRONTEND_URL}/account/usage',
|
|
}
|
|
)
|
|
```
|
|
|
|
### 4.2 Brevo (Marketing) - Future
|
|
|
|
**Configuration (when needed):**
|
|
|
|
```json
|
|
{
|
|
"provider_id": "brevo",
|
|
"display_name": "Brevo",
|
|
"provider_type": "email",
|
|
"api_key": "xkeysib-xxx...",
|
|
"config": {
|
|
"from_email": "hello@igny8.com",
|
|
"from_name": "IGNY8",
|
|
"list_id": 123
|
|
},
|
|
"is_active": false
|
|
}
|
|
```
|
|
|
|
**Requirements:** `sib-api-v3-sdk>=7.6.0`
|
|
|
|
---
|
|
|
|
## 5. Required Credentials Checklist
|
|
|
|
> **Note:** All credentials are stored in the `IntegrationProvider` model and can be configured via Django Admin. The implementation supports both sandbox (testing) and production environments.
|
|
|
|
### 5.1 Stripe Credentials
|
|
|
|
| Item | Value | Status |
|
|
|------|-------|--------|
|
|
| Publishable Key (Live) | `pk_live_...` | ⬜ Add to IntegrationProvider |
|
|
| Secret Key (Live) | `sk_live_...` | ⬜ Add to IntegrationProvider |
|
|
| Webhook Signing Secret | `whsec_...` | ⬜ Add to IntegrationProvider |
|
|
| Products/Prices Created | Plan IDs | ⬜ Create in Stripe Dashboard |
|
|
| **Code Implementation** | — | ✅ **Complete** |
|
|
|
|
**Stripe Products to Create:**
|
|
1. **Starter Plan** - $99/mo - `price_starter_monthly`
|
|
2. **Growth Plan** - $199/mo - `price_growth_monthly`
|
|
3. **Scale Plan** - $299/mo - `price_scale_monthly`
|
|
|
|
### 5.2 PayPal Credentials
|
|
|
|
| Item | Value | Status |
|
|
|------|-------|--------|
|
|
| Client ID (Live) | `AY...` | ⬜ Add to IntegrationProvider |
|
|
| Client Secret (Live) | `EL...` | ⬜ Add to IntegrationProvider |
|
|
| Webhook ID | `WH-...` | ⬜ Add to IntegrationProvider config |
|
|
| Subscription Plans Created | PayPal Plan IDs | ⬜ Create in PayPal Dashboard |
|
|
| **Code Implementation** | — | ✅ **Complete** |
|
|
|
|
### 5.3 Resend Credentials
|
|
|
|
| Item | Value | Status |
|
|
|------|-------|--------|
|
|
| API Key | `re_...` | ⬜ Add to IntegrationProvider |
|
|
| Domain Verified | igny8.com | ⬜ Configure in Resend Dashboard |
|
|
| DNS Records Added | DKIM, SPF | ⬜ Add to DNS provider |
|
|
| **Code Implementation** | — | ✅ **Complete** |
|
|
|
|
### 5.4 Brevo Credentials (Future)
|
|
|
|
| Item | Value | Status |
|
|
|------|-------|--------|
|
|
| API Key | `xkeysib-...` | ⬜ Future |
|
|
| Contact List Created | List ID | ⬜ Future |
|
|
|
|
---
|
|
|
|
## 6. Implementation Order
|
|
|
|
### Phase 1: Backend Setup ✅ COMPLETE
|
|
|
|
1. **✅ Add dependencies:**
|
|
```bash
|
|
pip install resend>=0.7.0
|
|
# PayPal uses requests (already installed)
|
|
# Stripe already installed
|
|
```
|
|
|
|
2. **✅ Create service files:**
|
|
- ✅ `backend/igny8_core/business/billing/services/stripe_service.py`
|
|
- ✅ `backend/igny8_core/business/billing/services/paypal_service.py`
|
|
- ✅ Update `backend/igny8_core/business/billing/services/email_service.py`
|
|
|
|
3. **✅ Create view files:**
|
|
- ✅ `backend/igny8_core/business/billing/views/stripe_views.py`
|
|
- ✅ `backend/igny8_core/business/billing/views/paypal_views.py`
|
|
|
|
4. **✅ Add URL routes:**
|
|
```python
|
|
# backend/igny8_core/business/billing/urls.py
|
|
urlpatterns += [
|
|
path('stripe/checkout/', StripeCheckoutView.as_view()),
|
|
path('stripe/credit-checkout/', StripeCreditCheckoutView.as_view()),
|
|
path('stripe/billing-portal/', StripeBillingPortalView.as_view()),
|
|
path('webhooks/stripe/', stripe_webhook),
|
|
path('paypal/create-order/', PayPalCreateOrderView.as_view()),
|
|
path('paypal/capture-order/', PayPalCaptureOrderView.as_view()),
|
|
path('webhooks/paypal/', paypal_webhook),
|
|
]
|
|
```
|
|
|
|
5. **✅ Add email templates:**
|
|
```
|
|
backend/igny8_core/templates/emails/
|
|
├── base.html
|
|
├── welcome.html
|
|
├── password_reset.html
|
|
├── email_verification.html
|
|
├── payment_confirmation.html
|
|
├── payment_approved.html
|
|
├── payment_rejected.html
|
|
├── payment_failed.html
|
|
├── subscription_activated.html
|
|
├── subscription_renewal.html
|
|
├── low_credits.html
|
|
└── refund_notification.html
|
|
```
|
|
|
|
### Phase 2: Stripe Configuration ⏳ PENDING USER CREDENTIALS
|
|
|
|
1. ⏳ Create products and prices in Stripe Dashboard (User action needed)
|
|
2. ⏳ Configure webhook endpoint (User action needed)
|
|
3. ⏳ Get API keys and add to IntegrationProvider via admin (User action needed)
|
|
4. ⏳ Test in sandbox mode (After credentials added)
|
|
|
|
### Phase 3: Frontend Integration ✅ COMPLETE
|
|
|
|
1. ✅ Add billing API methods (stripe, paypal, purchaseCredits, subscribeToPlan)
|
|
2. ✅ Update PlansAndBillingPage with payment buttons
|
|
3. ✅ Add PaymentGatewaySelector component
|
|
4. ✅ Add success/cancel handling
|
|
|
|
### Phase 4: Email Configuration ⏳ PENDING USER CREDENTIALS
|
|
|
|
1. ⏳ Add Resend API key to IntegrationProvider (User action needed)
|
|
2. ⏳ Verify domain at resend.com (User action needed)
|
|
3. ⏳ Test all email triggers (After credentials added)
|
|
|
|
### Phase 5: Testing ⏳ PENDING USER CREDENTIALS
|
|
|
|
1. ⏳ Test Stripe checkout flow (sandbox) - After credentials added
|
|
2. ⏳ Test PayPal flow (sandbox) - After credentials added
|
|
3. ⏳ Test webhook handling - After webhooks configured
|
|
4. ⏳ Test email delivery - After Resend configured
|
|
5. ⏳ Switch to production credentials - After testing complete
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
**Implementation Status:** ✅ **CODE COMPLETE** - Ready for credentials and testing
|
|
|
|
**Time Spent:** 2-3 days (Backend + Frontend implementation)
|
|
|
|
**Pending Actions (User):**
|
|
- ⏳ Stripe account: Create products/prices, configure webhooks, add API keys
|
|
- ⏳ PayPal developer account: Create app, configure webhooks, add credentials
|
|
- ⏳ Resend account: Verify domain, add API key
|
|
|
|
**Files Created:** ✅
|
|
- ✅ `stripe_service.py` (500+ lines)
|
|
- ✅ `stripe_views.py` (560+ lines)
|
|
- ✅ `paypal_service.py` (500+ lines)
|
|
- ✅ `paypal_views.py` (600+ lines)
|
|
- ✅ Updated `email_service.py` (760+ lines)
|
|
- ✅ 12 email templates
|
|
- ✅ `PaymentGatewaySelector.tsx` (160+ lines)
|
|
- ✅ Updated `PlansAndBillingPage.tsx`
|
|
- ✅ Updated `billing.api.ts` (+285 lines)
|
|
|
|
**Existing Infrastructure Used:**
|
|
- ✅ `IntegrationProvider` model for all credentials
|
|
- ✅ `Payment`, `Invoice`, `CreditPackage` models
|
|
- ✅ `PaymentService` for payment processing
|
|
- ✅ `CreditService` for credit management
|
|
|
|
**Next Steps:**
|
|
1. Add Stripe credentials to Django Admin → Integration Providers
|
|
2. Add PayPal credentials to Django Admin → Integration Providers
|
|
3. Add Resend API key to Django Admin → Integration Providers
|
|
4. Test payment flows in sandbox mode
|
|
5. Switch to production credentials when ready
|