Files
igny8/docs/plans/implemented/THIRD-PARTY-INTEGRATIONS-PLAN.md
IGNY8 VPS (Salman) 7da3334c03 Reorg docs
2026-01-08 00:58:28 +00:00

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