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

41 KiB

Third-Party Integrations Implementation Plan

Version: 1.1 Created: January 6, 2026 Updated: January 7, 2026 Status: IMPLEMENTATION COMPLETE Covers: FINAL-PRELAUNCH.md Phase 3.2, 3.3, and Phase 4


Implementation Status

Phase Component Status Notes
3.2 Stripe Integration Complete Full checkout, billing portal, webhooks
3.2 PayPal Integration Complete REST API v2, orders, subscriptions
3.3 Plan Upgrade Flow Complete Stripe/PayPal/Manual payment selection
3.3 Credit Purchase Flow Complete One-time credit package checkout
4 Resend Email Service Complete Transactional emails with templates
4 Brevo Marketing ⏸️ Deferred Future implementation

Executive Summary

This document provides complete implementation details for:

  • Phase 3.2: Stripe & PayPal payment integration
  • Phase 3.3: Plans & packages (upgrade flows, service packages)
  • Phase 4: Email services (Resend for transactional, Brevo for marketing)

All integrations use the existing IntegrationProvider model in /backend/igny8_core/modules/system/models.py for centralized API key management.


Table of Contents

  1. Existing Infrastructure
  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 Complete
InvoiceService business/billing/services/invoice_service.py Complete
StripeService business/billing/services/stripe_service.py NEW - Complete
PayPalService business/billing/services/paypal_service.py NEW - Complete
EmailService business/billing/services/email_service.py Updated - Resend Integration

2. Phase 3.2: Payment Integration

2.1 Stripe Integration

2.1.1 Required Credentials from Stripe Dashboard

From dashboard.stripe.com:

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

Note: All credentials are stored in the IntegrationProvider model and can be configured via Django Admin. The implementation supports both sandbox (testing) and production environments.

5.1 Stripe Credentials

Item Value Status
Publishable Key (Live) pk_live_... Add to IntegrationProvider
Secret Key (Live) sk_live_... Add to IntegrationProvider
Webhook Signing Secret whsec_... Add to IntegrationProvider
Products/Prices Created Plan IDs Create in Stripe Dashboard
Code Implementation Complete

Stripe Products to Create:

  1. Starter Plan - $99/mo - price_starter_monthly
  2. Growth Plan - $199/mo - price_growth_monthly
  3. Scale Plan - $299/mo - price_scale_monthly

5.2 PayPal Credentials

Item Value Status
Client ID (Live) AY... Add to IntegrationProvider
Client Secret (Live) EL... Add to IntegrationProvider
Webhook ID WH-... Add to IntegrationProvider config
Subscription Plans Created PayPal Plan IDs Create in PayPal Dashboard
Code Implementation Complete

5.3 Resend Credentials

Item Value Status
API Key re_... Add to IntegrationProvider
Domain Verified igny8.com Configure in Resend Dashboard
DNS Records Added DKIM, SPF Add to DNS provider
Code Implementation Complete

5.4 Brevo Credentials (Future)

Item Value Status
API Key xkeysib-... Future
Contact List Created List ID Future

6. Implementation Order

Phase 1: Backend Setup COMPLETE

  1. Add dependencies:

    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/
    ├── base.html
    ├── welcome.html
    ├── password_reset.html
    ├── email_verification.html
    ├── payment_confirmation.html
    ├── payment_approved.html
    ├── payment_rejected.html
    ├── payment_failed.html
    ├── subscription_activated.html
    ├── subscription_renewal.html
    ├── low_credits.html
    └── refund_notification.html
    

Phase 2: Stripe Configuration PENDING USER CREDENTIALS

  1. Create products and prices in Stripe Dashboard (User action needed)
  2. Configure webhook endpoint (User action needed)
  3. Get API keys and add to IntegrationProvider via admin (User action needed)
  4. Test in sandbox mode (After credentials added)

Phase 3: Frontend Integration COMPLETE

  1. Add billing API methods (stripe, paypal, purchaseCredits, subscribeToPlan)
  2. Update PlansAndBillingPage with payment buttons
  3. Add PaymentGatewaySelector component
  4. Add success/cancel handling

Phase 4: Email Configuration PENDING USER CREDENTIALS

  1. Add Resend API key to IntegrationProvider (User action needed)
  2. Verify domain at resend.com (User action needed)
  3. Test all email triggers (After credentials added)

Phase 5: Testing PENDING USER CREDENTIALS

  1. Test Stripe checkout flow (sandbox) - After credentials added
  2. Test PayPal flow (sandbox) - After credentials added
  3. Test webhook handling - After webhooks configured
  4. Test email delivery - After Resend configured
  5. Switch to production credentials - After testing complete

Summary

Implementation Status: CODE COMPLETE - Ready for credentials and testing

Time Spent: 2-3 days (Backend + Frontend implementation)

Pending Actions (User):

  • Stripe account: Create products/prices, configure webhooks, add API keys
  • PayPal developer account: Create app, configure webhooks, add credentials
  • Resend account: Verify domain, add API key

Files Created:

  • stripe_service.py (500+ lines)
  • stripe_views.py (560+ lines)
  • paypal_service.py (500+ lines)
  • paypal_views.py (600+ lines)
  • Updated email_service.py (760+ lines)
  • 12 email templates
  • PaymentGatewaySelector.tsx (160+ lines)
  • Updated PlansAndBillingPage.tsx
  • Updated billing.api.ts (+285 lines)

Existing Infrastructure Used:

  • IntegrationProvider model for all credentials
  • Payment, Invoice, CreditPackage models
  • PaymentService for payment processing
  • CreditService for credit management

Next Steps:

  1. Add Stripe credentials to Django Admin → Integration Providers
  2. Add PayPal credentials to Django Admin → Integration Providers
  3. Add Resend API key to Django Admin → Integration Providers
  4. Test payment flows in sandbox mode
  5. Switch to production credentials when ready