38 KiB
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
- Existing Infrastructure
- Phase 3.2: Payment Integration
- Phase 3.3: Plans & Packages
- Phase 4: Email Services
- Required Credentials Checklist
- 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:
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 |
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:
| 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 subscriptioninvoice.paid- Recurring payment successinvoice.payment_failed- Payment failurecustomer.subscription.updated- Plan changescustomer.subscription.deleted- Cancellation
2.1.2 IntegrationProvider Configuration
{
"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:
"""
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:
"""
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:
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:
// 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:
| 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.APPROVEDPAYMENT.CAPTURE.COMPLETEDBILLING.SUBSCRIPTION.ACTIVATEDBILLING.SUBSCRIPTION.CANCELLEDPAYMENT.CAPTURE.DENIED
2.2.2 IntegrationProvider Configuration
{
"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:
"""
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:
- User on Plans page sees current plan and upgrade options
- Clicks "Upgrade" on desired plan
- Redirected to Stripe Checkout (or PayPal)
- Completes payment
- Webhook triggers:
- Updates Subscription to new plan
- Prorates credits (optional)
- User redirected back with success message
Stripe Proration:
Stripe handles proration automatically when upgrading. The subscription_data can include:
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:
- User on Usage page clicks "Buy Credits"
- Selects package
- Redirected to Stripe/PayPal Checkout
- Completes payment
- Webhook adds credits to account
- 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:
| Credential | Where to Find | Store In |
|---|---|---|
| API Key | API Keys → Create API Key | IntegrationProvider.api_key |
Domain Verification:
- Add domain in Resend dashboard
- Add DNS records (DKIM, SPF, DMARC)
- Verify domain
4.1.2 IntegrationProvider Configuration
{
"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):
"""
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):
{
"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:
- Starter Plan - $99/mo -
price_starter_monthly - Growth Plan - $199/mo -
price_growth_monthly - 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)
-
Add dependencies:
pip install resend>=0.7.0 # PayPal uses requests (already installed) # Stripe already installed -
Create service files:
backend/igny8_core/business/billing/services/stripe_service.pybackend/igny8_core/business/billing/services/paypal_service.py- Update
backend/igny8_core/business/billing/services/email_service.py
-
Create view files:
backend/igny8_core/business/billing/views/stripe_views.pybackend/igny8_core/business/billing/views/paypal_views.py
-
Add URL routes:
# 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), ] -
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)
- Create products and prices in Stripe Dashboard
- Configure webhook endpoint
- Get API keys and add to IntegrationProvider via admin
- Test in sandbox mode
Phase 3: Frontend Integration (1-2 days)
- Add billing API methods
- Update PlansAndBillingPage with payment buttons
- Update UsageAnalyticsPage with credit purchase
- Add success/cancel handling
Phase 4: Email Configuration (1 day)
- Add Resend API key to IntegrationProvider
- Verify domain
- Test all email triggers
Phase 5: Testing (1-2 days)
- Test Stripe checkout flow (sandbox)
- Test PayPal flow (sandbox)
- Test webhook handling
- Test email delivery
- 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.pystripe_views.pypaypal_service.pypaypal_views.py- Updated
email_service.py - 5 email templates
Existing Infrastructure Used:
IntegrationProvidermodel for all credentialsPayment,Invoice,CreditPackagemodelsPaymentServicefor payment processingCreditServicefor credit management