Files
igny8/docs/plans/THIRD-PARTY-INTEGRATIONS-PLAN.md
2026-01-06 22:07:19 +00:00

1249 lines
38 KiB
Markdown

# Third-Party Integrations Implementation Plan
**Version:** 1.0
**Created:** January 6, 2026
**Covers:** FINAL-PRELAUNCH.md Phase 3.2, 3.3, and Phase 4
---
## 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` | ✅ Scaffolded |
| `InvoiceService` | `business/billing/services/invoice_service.py` | ✅ Scaffolded |
| `BillingEmailService` | `business/billing/services/email_service.py` | ✅ Uses Django send_mail |
---
## 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
### 5.1 Stripe Credentials
| Item | Value | Status |
|------|-------|--------|
| Publishable Key (Live) | `pk_live_...` | ⬜ Needed |
| Secret Key (Live) | `sk_live_...` | ⬜ Needed |
| Webhook Signing Secret | `whsec_...` | ⬜ Needed |
| Products/Prices Created | Plan IDs | ⬜ Needed |
**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...` | ⬜ Needed |
| Client Secret (Live) | `EL...` | ⬜ Needed |
| Webhook ID | `WH-...` | ⬜ Needed |
| Subscription Plans Created | PayPal Plan IDs | ⬜ Needed |
### 5.3 Resend Credentials
| Item | Value | Status |
|------|-------|--------|
| API Key | `re_...` | ⬜ Needed |
| Domain Verified | igny8.com | ⬜ Needed |
| DNS Records Added | DKIM, SPF | ⬜ Needed |
### 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 (2-3 days)
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/
├── welcome.html
├── password_reset.html
├── payment_confirmed.html
├── subscription_activated.html
└── low_credits.html
```
### Phase 2: Stripe Configuration (1 day)
1. Create products and prices in Stripe Dashboard
2. Configure webhook endpoint
3. Get API keys and add to IntegrationProvider via admin
4. Test in sandbox mode
### Phase 3: Frontend Integration (1-2 days)
1. Add billing API methods
2. Update PlansAndBillingPage with payment buttons
3. Update UsageAnalyticsPage with credit purchase
4. Add success/cancel handling
### Phase 4: Email Configuration (1 day)
1. Add Resend API key to IntegrationProvider
2. Verify domain
3. Test all email triggers
### Phase 5: Testing (1-2 days)
1. Test Stripe checkout flow (sandbox)
2. Test PayPal flow (sandbox)
3. Test webhook handling
4. Test email delivery
5. Switch to production credentials
---
## Summary
**Total Estimated Time:** 6-9 days
**Dependencies:**
- Stripe account with products/prices created
- PayPal developer account with app created
- Resend account with domain verified
**Files to Create:**
- `stripe_service.py`
- `stripe_views.py`
- `paypal_service.py`
- `paypal_views.py`
- Updated `email_service.py`
- 5 email templates
**Existing Infrastructure Used:**
- `IntegrationProvider` model for all credentials
- `Payment`, `Invoice`, `CreditPackage` models
- `PaymentService` for payment processing
- `CreditService` for credit management