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

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

  1. Existing Infrastructure
  2. Phase 3.2: Payment Integration
  3. Phase 3.3: Plans & Packages
  4. Phase 4: Email Services
  5. Required Credentials Checklist
  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:

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:

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

{
  "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.APPROVED
  • PAYMENT.CAPTURE.COMPLETED
  • BILLING.SUBSCRIPTION.ACTIVATED
  • BILLING.SUBSCRIPTION.CANCELLED
  • PAYMENT.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:

  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:

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:

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

{
  "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:

  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:

    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:

    # 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