# Third-Party Integrations Implementation Plan **Version:** 1.1 **Created:** January 6, 2026 **Updated:** January 7, 2026 **Status:** ✅ **IMPLEMENTATION COMPLETE** **Covers:** FINAL-PRELAUNCH.md Phase 3.2, 3.3, and Phase 4 --- ## Implementation Status | Phase | Component | Status | Notes | |-------|-----------|--------|-------| | 3.2 | Stripe Integration | ✅ Complete | Full checkout, billing portal, webhooks | | 3.2 | PayPal Integration | ✅ Complete | REST API v2, orders, subscriptions | | 3.3 | Plan Upgrade Flow | ✅ Complete | Stripe/PayPal/Manual payment selection | | 3.3 | Credit Purchase Flow | ✅ Complete | One-time credit package checkout | | 4 | Resend Email Service | ✅ Complete | Transactional emails with templates | | 4 | Brevo Marketing | ⏸️ Deferred | Future implementation | --- ## Executive Summary This document provides complete implementation details for: - **Phase 3.2:** Stripe & PayPal payment integration - **Phase 3.3:** Plans & packages (upgrade flows, service packages) - **Phase 4:** Email services (Resend for transactional, Brevo for marketing) All integrations use the existing `IntegrationProvider` model in `/backend/igny8_core/modules/system/models.py` for centralized API key management. --- ## Table of Contents 1. [Existing Infrastructure](#1-existing-infrastructure) 2. [Phase 3.2: Payment Integration](#2-phase-32-payment-integration) 3. [Phase 3.3: Plans & Packages](#3-phase-33-plans--packages) 4. [Phase 4: Email Services](#4-phase-4-email-services) 5. [Required Credentials Checklist](#5-required-credentials-checklist) 6. [Implementation Order](#6-implementation-order) --- ## 1. Existing Infrastructure ### 1.1 IntegrationProvider Model **Location:** `backend/igny8_core/modules/system/models.py` The system already has a centralized model for all third-party credentials: ```python class IntegrationProvider(models.Model): provider_id = CharField(primary_key=True) # 'stripe', 'paypal', 'resend' display_name = CharField(max_length=100) provider_type = CharField(choices=['ai', 'payment', 'email', 'storage']) api_key = CharField(max_length=500) # Primary API key api_secret = CharField(max_length=500) # Secondary secret webhook_secret = CharField(max_length=500) # Webhook signing secret api_endpoint = URLField() # Custom endpoint (PayPal sandbox/live) config = JSONField() # Provider-specific config is_active = BooleanField() is_sandbox = BooleanField() ``` **Pre-seeded Providers (from migration 0016):** | Provider ID | Type | Purpose | Status | |------------|------|---------|--------| | `stripe` | payment | Card payments, subscriptions | Active (sandbox) | | `paypal` | payment | PayPal payments | Active (sandbox) | | `resend` | email | Transactional emails | Active | ### 1.2 Existing Billing Models **Location:** `backend/igny8_core/business/billing/models.py` | Model | Purpose | |-------|---------| | `Payment` | Payment records with Stripe/PayPal/Manual support | | `Invoice` | Invoice records with line items | | `CreditPackage` | One-time credit purchase packages | | `CreditTransaction` | Credit ledger | | `Subscription` (in auth) | Account subscription tracking | | `Plan` (in auth) | Plan definitions with Stripe IDs | ### 1.3 Existing Services | Service | Location | Status | |---------|----------|--------| | `PaymentService` | `business/billing/services/payment_service.py` | ✅ Complete | | `InvoiceService` | `business/billing/services/invoice_service.py` | ✅ Complete | | `StripeService` | `business/billing/services/stripe_service.py` | ✅ **NEW - Complete** | | `PayPalService` | `business/billing/services/paypal_service.py` | ✅ **NEW - Complete** | | `EmailService` | `business/billing/services/email_service.py` | ✅ **Updated - Resend Integration** | --- ## 2. Phase 3.2: Payment Integration ### 2.1 Stripe Integration #### 2.1.1 Required Credentials from Stripe Dashboard **From [dashboard.stripe.com](https://dashboard.stripe.com):** | Credential | Where to Find | Store In | |------------|---------------|----------| | **Publishable Key** | Developers → API Keys → Publishable key | `IntegrationProvider.api_key` | | **Secret Key** | Developers → API Keys → Secret key | `IntegrationProvider.api_secret` | | **Webhook Signing Secret** | Developers → Webhooks → Endpoint → Signing secret | `IntegrationProvider.webhook_secret` | **Webhook Endpoint to Configure:** ``` Production: https://api.igny8.com/api/v1/billing/webhooks/stripe/ Sandbox: https://api-staging.igny8.com/api/v1/billing/webhooks/stripe/ ``` **Events to Subscribe:** - `checkout.session.completed` - New subscription - `invoice.paid` - Recurring payment success - `invoice.payment_failed` - Payment failure - `customer.subscription.updated` - Plan changes - `customer.subscription.deleted` - Cancellation #### 2.1.2 IntegrationProvider Configuration ```json { "provider_id": "stripe", "display_name": "Stripe", "provider_type": "payment", "api_key": "pk_live_xxx...", // Publishable key "api_secret": "sk_live_xxx...", // Secret key "webhook_secret": "whsec_xxx...", // Webhook secret "config": { "currency": "usd", "payment_methods": ["card"], "billing_portal_enabled": true }, "is_active": true, "is_sandbox": false // Set to true for test mode } ``` #### 2.1.3 Backend Implementation **New Files to Create:** ``` backend/igny8_core/business/billing/ ├── services/ │ └── stripe_service.py # Stripe API wrapper ├── views/ │ └── stripe_views.py # Checkout, portal, webhooks └── tasks/ └── stripe_sync.py # Sync jobs (optional) ``` **stripe_service.py:** ```python """ Stripe Service - Wrapper for Stripe API operations """ import stripe from django.conf import settings from igny8_core.modules.system.models import IntegrationProvider class StripeService: """Service for Stripe payment operations""" def __init__(self): provider = IntegrationProvider.get_provider('stripe') if not provider: raise ValueError("Stripe provider not configured") self.is_sandbox = provider.is_sandbox stripe.api_key = provider.api_secret self.publishable_key = provider.api_key self.webhook_secret = provider.webhook_secret self.config = provider.config or {} def create_checkout_session(self, account, plan, success_url, cancel_url): """ Create Stripe Checkout session for new subscription """ session = stripe.checkout.Session.create( customer_email=account.billing_email or account.owner.email, payment_method_types=['card'], mode='subscription', line_items=[{ 'price': plan.stripe_price_id, 'quantity': 1, }], success_url=success_url, cancel_url=cancel_url, metadata={ 'account_id': str(account.id), 'plan_id': str(plan.id), }, subscription_data={ 'metadata': { 'account_id': str(account.id), } } ) return session def create_credit_checkout_session(self, account, credit_package, success_url, cancel_url): """ Create Stripe Checkout for one-time credit purchase """ session = stripe.checkout.Session.create( customer_email=account.billing_email or account.owner.email, payment_method_types=['card'], mode='payment', line_items=[{ 'price_data': { 'currency': self.config.get('currency', 'usd'), 'product_data': { 'name': credit_package.name, 'description': f'{credit_package.credits} credits', }, 'unit_amount': int(credit_package.price * 100), # Cents }, 'quantity': 1, }], success_url=success_url, cancel_url=cancel_url, metadata={ 'account_id': str(account.id), 'credit_package_id': str(credit_package.id), 'credit_amount': credit_package.credits, } ) return session def create_billing_portal_session(self, account, return_url): """ Create Stripe Billing Portal session for subscription management """ # Get or create Stripe customer customer_id = self._get_or_create_customer(account) session = stripe.billing_portal.Session.create( customer=customer_id, return_url=return_url, ) return session def construct_webhook_event(self, payload, sig_header): """ Verify and construct webhook event """ return stripe.Webhook.construct_event( payload, sig_header, self.webhook_secret ) def _get_or_create_customer(self, account): """Get existing Stripe customer or create new one""" if account.stripe_customer_id: return account.stripe_customer_id customer = stripe.Customer.create( email=account.billing_email or account.owner.email, name=account.name, metadata={'account_id': str(account.id)}, ) account.stripe_customer_id = customer.id account.save(update_fields=['stripe_customer_id']) return customer.id ``` **stripe_views.py:** ```python """ Stripe Views - Checkout, Portal, and Webhook endpoints """ from rest_framework.views import APIView from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response from rest_framework import status from django.views.decorators.csrf import csrf_exempt import json import logging from .stripe_service import StripeService from ..services.payment_service import PaymentService from igny8_core.auth.models import Plan, Account logger = logging.getLogger(__name__) class StripeCheckoutView(APIView): """Create Stripe Checkout session""" permission_classes = [IsAuthenticated] def post(self, request): account = request.user.account plan_id = request.data.get('plan_id') try: plan = Plan.objects.get(id=plan_id, is_active=True) except Plan.DoesNotExist: return Response({'error': 'Plan not found'}, status=404) if not plan.stripe_price_id: return Response({'error': 'Plan not configured for Stripe'}, status=400) service = StripeService() frontend_url = settings.FRONTEND_URL session = service.create_checkout_session( account=account, plan=plan, success_url=f'{frontend_url}/account/plans?success=true', cancel_url=f'{frontend_url}/account/plans?canceled=true', ) return Response({ 'checkout_url': session.url, 'session_id': session.id, }) class StripeCreditCheckoutView(APIView): """Create Stripe Checkout for credit package""" permission_classes = [IsAuthenticated] def post(self, request): from ..models import CreditPackage account = request.user.account package_id = request.data.get('package_id') try: package = CreditPackage.objects.get(id=package_id, is_active=True) except CreditPackage.DoesNotExist: return Response({'error': 'Credit package not found'}, status=404) service = StripeService() frontend_url = settings.FRONTEND_URL session = service.create_credit_checkout_session( account=account, credit_package=package, success_url=f'{frontend_url}/account/usage?purchase=success', cancel_url=f'{frontend_url}/account/usage?purchase=canceled', ) return Response({ 'checkout_url': session.url, 'session_id': session.id, }) class StripeBillingPortalView(APIView): """Create Stripe Billing Portal session""" permission_classes = [IsAuthenticated] def post(self, request): account = request.user.account service = StripeService() frontend_url = settings.FRONTEND_URL session = service.create_billing_portal_session( account=account, return_url=f'{frontend_url}/account/plans', ) return Response({ 'portal_url': session.url, }) @csrf_exempt @api_view(['POST']) @permission_classes([AllowAny]) def stripe_webhook(request): """ Handle Stripe webhook events Events handled: - checkout.session.completed: New subscription or credit purchase - invoice.paid: Recurring payment success - invoice.payment_failed: Payment failure - customer.subscription.updated: Plan changes - customer.subscription.deleted: Cancellation """ payload = request.body sig_header = request.META.get('HTTP_STRIPE_SIGNATURE') try: service = StripeService() event = service.construct_webhook_event(payload, sig_header) except Exception as e: logger.error(f'Stripe webhook error: {str(e)}') return Response({'error': str(e)}, status=400) event_type = event['type'] data = event['data']['object'] logger.info(f'Stripe webhook received: {event_type}') if event_type == 'checkout.session.completed': _handle_checkout_completed(data) elif event_type == 'invoice.paid': _handle_invoice_paid(data) elif event_type == 'invoice.payment_failed': _handle_payment_failed(data) elif event_type == 'customer.subscription.updated': _handle_subscription_updated(data) elif event_type == 'customer.subscription.deleted': _handle_subscription_deleted(data) return Response({'status': 'success'}) def _handle_checkout_completed(session): """Handle successful checkout""" metadata = session.get('metadata', {}) account_id = metadata.get('account_id') if not account_id: logger.error('No account_id in checkout session metadata') return try: account = Account.objects.get(id=account_id) except Account.DoesNotExist: logger.error(f'Account {account_id} not found') return if session.get('mode') == 'subscription': # Handle new subscription subscription_id = session.get('subscription') _activate_subscription(account, subscription_id, metadata) elif session.get('mode') == 'payment': # Handle credit purchase credit_package_id = metadata.get('credit_package_id') credit_amount = metadata.get('credit_amount') _add_purchased_credits(account, credit_package_id, credit_amount) def _activate_subscription(account, stripe_subscription_id, metadata): """Activate subscription after successful payment""" import stripe from django.utils import timezone from igny8_core.auth.models import Subscription, Plan # Get subscription details from Stripe stripe_sub = stripe.Subscription.retrieve(stripe_subscription_id) plan_id = metadata.get('plan_id') plan = Plan.objects.get(id=plan_id) # Update or create subscription subscription, created = Subscription.objects.update_or_create( account=account, defaults={ 'plan': plan, 'stripe_subscription_id': stripe_subscription_id, 'status': 'active', 'current_period_start': timezone.datetime.fromtimestamp( stripe_sub.current_period_start, tz=timezone.utc ), 'current_period_end': timezone.datetime.fromtimestamp( stripe_sub.current_period_end, tz=timezone.utc ), } ) # Add initial credits from ..services.credit_service import CreditService CreditService.add_credits( account=account, amount=plan.included_credits, transaction_type='subscription', description=f'Subscription: {plan.name}' ) logger.info(f'Subscription activated for account {account.id}') def _add_purchased_credits(account, credit_package_id, credit_amount): """Add purchased credits to account""" from ..services.credit_service import CreditService credit_amount = int(credit_amount) CreditService.add_credits( account=account, amount=credit_amount, transaction_type='purchase', description=f'Credit package purchase ({credit_amount} credits)' ) logger.info(f'Added {credit_amount} credits to account {account.id}') def _handle_invoice_paid(invoice): """Handle successful recurring payment""" subscription_id = invoice.get('subscription') if not subscription_id: return from igny8_core.auth.models import Subscription try: subscription = Subscription.objects.get(stripe_subscription_id=subscription_id) except Subscription.DoesNotExist: return # Add monthly credits from ..services.credit_service import CreditService CreditService.add_credits( account=subscription.account, amount=subscription.plan.included_credits, transaction_type='subscription', description=f'Monthly renewal: {subscription.plan.name}' ) def _handle_payment_failed(invoice): """Handle failed payment""" subscription_id = invoice.get('subscription') if not subscription_id: return from igny8_core.auth.models import Subscription try: subscription = Subscription.objects.get(stripe_subscription_id=subscription_id) subscription.status = 'past_due' subscription.save() except Subscription.DoesNotExist: pass # TODO: Send notification email def _handle_subscription_updated(subscription_data): """Handle subscription update (plan change)""" subscription_id = subscription_data.get('id') from igny8_core.auth.models import Subscription try: subscription = Subscription.objects.get(stripe_subscription_id=subscription_id) # Update status status_map = { 'active': 'active', 'past_due': 'past_due', 'canceled': 'canceled', 'trialing': 'trialing', } stripe_status = subscription_data.get('status') subscription.status = status_map.get(stripe_status, 'active') subscription.save() except Subscription.DoesNotExist: pass def _handle_subscription_deleted(subscription_data): """Handle subscription cancellation""" subscription_id = subscription_data.get('id') from igny8_core.auth.models import Subscription try: subscription = Subscription.objects.get(stripe_subscription_id=subscription_id) subscription.status = 'canceled' subscription.save() except Subscription.DoesNotExist: pass ``` #### 2.1.4 Frontend Implementation **New API calls in `frontend/src/services/billing.api.ts`:** ```typescript export const createStripeCheckout = async (planId: string): Promise<{ checkout_url: string }> => { const response = await api.post('/billing/stripe/checkout/', { plan_id: planId }); return response.data; }; export const createStripeCreditCheckout = async (packageId: string): Promise<{ checkout_url: string }> => { const response = await api.post('/billing/stripe/credit-checkout/', { package_id: packageId }); return response.data; }; export const openStripeBillingPortal = async (): Promise<{ portal_url: string }> => { const response = await api.post('/billing/stripe/billing-portal/'); return response.data; }; ``` **Button handlers:** ```typescript // Subscribe to plan const handleSubscribe = async (planId: string) => { const { checkout_url } = await createStripeCheckout(planId); window.location.href = checkout_url; }; // Buy credits const handleBuyCredits = async (packageId: string) => { const { checkout_url } = await createStripeCreditCheckout(packageId); window.location.href = checkout_url; }; // Manage subscription const handleManageSubscription = async () => { const { portal_url } = await openStripeBillingPortal(); window.location.href = portal_url; }; ``` --- ### 2.2 PayPal Integration #### 2.2.1 Required Credentials from PayPal Developer Dashboard **From [developer.paypal.com](https://developer.paypal.com):** | Credential | Where to Find | Store In | |------------|---------------|----------| | **Client ID** | My Apps & Credentials → App → Client ID | `IntegrationProvider.api_key` | | **Client Secret** | My Apps & Credentials → App → Secret | `IntegrationProvider.api_secret` | | **Webhook ID** | My Apps & Credentials → Webhooks → Webhook ID | `config.webhook_id` | **Endpoints:** - Sandbox: `https://api-m.sandbox.paypal.com` - Production: `https://api-m.paypal.com` **Webhook Events to Subscribe:** - `CHECKOUT.ORDER.APPROVED` - `PAYMENT.CAPTURE.COMPLETED` - `BILLING.SUBSCRIPTION.ACTIVATED` - `BILLING.SUBSCRIPTION.CANCELLED` - `PAYMENT.CAPTURE.DENIED` #### 2.2.2 IntegrationProvider Configuration ```json { "provider_id": "paypal", "display_name": "PayPal", "provider_type": "payment", "api_key": "AY...", // Client ID "api_secret": "EL...", // Client Secret "webhook_secret": "", // Not used for PayPal (uses webhook ID) "api_endpoint": "https://api-m.paypal.com", // or sandbox "config": { "currency": "USD", "webhook_id": "WH-xxx...", "return_url": "https://app.igny8.com/account/plans?paypal=success", "cancel_url": "https://app.igny8.com/account/plans?paypal=cancel" }, "is_active": true, "is_sandbox": false } ``` #### 2.2.3 Backend Implementation **paypal_service.py:** ```python """ PayPal Service - REST API v2 integration """ import requests import base64 from django.conf import settings from igny8_core.modules.system.models import IntegrationProvider class PayPalService: """Service for PayPal payment operations""" def __init__(self): provider = IntegrationProvider.get_provider('paypal') if not provider: raise ValueError("PayPal provider not configured") self.client_id = provider.api_key self.client_secret = provider.api_secret self.base_url = provider.api_endpoint or 'https://api-m.paypal.com' self.config = provider.config or {} self.is_sandbox = provider.is_sandbox self._access_token = None def _get_access_token(self): """Get OAuth access token""" if self._access_token: return self._access_token auth = base64.b64encode( f'{self.client_id}:{self.client_secret}'.encode() ).decode() response = requests.post( f'{self.base_url}/v1/oauth2/token', headers={ 'Authorization': f'Basic {auth}', 'Content-Type': 'application/x-www-form-urlencoded', }, data='grant_type=client_credentials' ) response.raise_for_status() self._access_token = response.json()['access_token'] return self._access_token def _make_request(self, method, endpoint, **kwargs): """Make authenticated API request""" token = self._get_access_token() headers = kwargs.pop('headers', {}) headers['Authorization'] = f'Bearer {token}' headers['Content-Type'] = 'application/json' url = f'{self.base_url}{endpoint}' response = requests.request(method, url, headers=headers, **kwargs) response.raise_for_status() return response.json() def create_order(self, account, amount, currency='USD', description=''): """ Create PayPal order for one-time payment """ order = self._make_request('POST', '/v2/checkout/orders', json={ 'intent': 'CAPTURE', 'purchase_units': [{ 'amount': { 'currency_code': currency, 'value': str(amount), }, 'description': description, 'custom_id': str(account.id), }], 'application_context': { 'return_url': self.config.get('return_url'), 'cancel_url': self.config.get('cancel_url'), 'brand_name': 'IGNY8', 'landing_page': 'BILLING', 'user_action': 'PAY_NOW', } }) return order def capture_order(self, order_id): """ Capture payment for approved order """ return self._make_request('POST', f'/v2/checkout/orders/{order_id}/capture') def get_order(self, order_id): """ Get order details """ return self._make_request('GET', f'/v2/checkout/orders/{order_id}') def create_subscription(self, account, plan_id): """ Create PayPal subscription Requires plan to be created in PayPal dashboard first """ subscription = self._make_request('POST', '/v1/billing/subscriptions', json={ 'plan_id': plan_id, # PayPal plan ID 'custom_id': str(account.id), 'application_context': { 'return_url': self.config.get('return_url'), 'cancel_url': self.config.get('cancel_url'), 'brand_name': 'IGNY8', 'user_action': 'SUBSCRIBE_NOW', } }) return subscription def verify_webhook_signature(self, headers, body): """ Verify webhook signature """ verification = self._make_request('POST', '/v1/notifications/verify-webhook-signature', json={ 'auth_algo': headers.get('PAYPAL-AUTH-ALGO'), 'cert_url': headers.get('PAYPAL-CERT-URL'), 'transmission_id': headers.get('PAYPAL-TRANSMISSION-ID'), 'transmission_sig': headers.get('PAYPAL-TRANSMISSION-SIG'), 'transmission_time': headers.get('PAYPAL-TRANSMISSION-TIME'), 'webhook_id': self.config.get('webhook_id'), 'webhook_event': body, }) return verification.get('verification_status') == 'SUCCESS' ``` --- ## 3. Phase 3.3: Plans & Packages ### 3.1 Plan Upgrade Flow **User Journey:** 1. User on Plans page sees current plan and upgrade options 2. Clicks "Upgrade" on desired plan 3. Redirected to Stripe Checkout (or PayPal) 4. Completes payment 5. Webhook triggers: - Updates Subscription to new plan - Prorates credits (optional) 6. User redirected back with success message **Stripe Proration:** Stripe handles proration automatically when upgrading. The `subscription_data` can include: ```python subscription_data={ 'proration_behavior': 'create_prorations', # or 'always_invoice' } ``` ### 3.2 Credit Package Purchase Flow **Current Packages (from CreditPackage model):** | Package | Credits | Price | Value | |---------|---------|-------|-------| | Starter | 500 | $9.99 | ~$0.02/credit | | Value | 2,000 | $29.99 | ~$0.015/credit | | Pro | 5,000 | $59.99 | ~$0.012/credit | | Enterprise | 15,000 | $149.99 | ~$0.01/credit | **Purchase Flow:** 1. User on Usage page clicks "Buy Credits" 2. Selects package 3. Redirected to Stripe/PayPal Checkout 4. Completes payment 5. Webhook adds credits to account 6. User redirected back to Usage page ### 3.3 Service Packages (Future) | Package | Type | Price | Includes | |---------|------|-------|----------| | Setup App | One-time | $299 | Initial config, 1hr onboarding | | Campaign Mgmt | Monthly | $199/mo | 10 keywords/mo, content review | **Implementation:** Create `ServicePackage` model similar to `CreditPackage`. --- ## 4. Phase 4: Email Services ### 4.1 Resend (Transactional) #### 4.1.1 Required Credentials **From [resend.com/api-keys](https://resend.com/api-keys):** | Credential | Where to Find | Store In | |------------|---------------|----------| | **API Key** | API Keys → Create API Key | `IntegrationProvider.api_key` | **Domain Verification:** 1. Add domain in Resend dashboard 2. Add DNS records (DKIM, SPF, DMARC) 3. Verify domain #### 4.1.2 IntegrationProvider Configuration ```json { "provider_id": "resend", "display_name": "Resend", "provider_type": "email", "api_key": "re_xxx...", "config": { "from_email": "noreply@igny8.com", "from_name": "IGNY8", "reply_to": "support@igny8.com" }, "is_active": true } ``` #### 4.1.3 Requirements.txt Addition ``` resend>=0.7.0 ``` #### 4.1.4 Email Service Implementation **email_service.py (Updated):** ```python """ Email Service - Multi-provider email sending Uses Resend for transactional, Brevo for marketing """ import resend import logging from typing import Optional, List, Dict from django.template.loader import render_to_string from igny8_core.modules.system.models import IntegrationProvider logger = logging.getLogger(__name__) class EmailService: """Unified email service supporting multiple providers""" def __init__(self): self._resend_configured = False self._brevo_configured = False self._setup_providers() def _setup_providers(self): """Initialize email providers""" # Setup Resend resend_provider = IntegrationProvider.get_provider('resend') if resend_provider and resend_provider.api_key: resend.api_key = resend_provider.api_key self._resend_config = resend_provider.config or {} self._resend_configured = True # Setup Brevo (future) brevo_provider = IntegrationProvider.get_provider('brevo') if brevo_provider and brevo_provider.api_key: self._brevo_config = brevo_provider.config or {} self._brevo_configured = True def send_transactional( self, to: str, subject: str, html: Optional[str] = None, text: Optional[str] = None, template: Optional[str] = None, context: Optional[Dict] = None, from_email: Optional[str] = None, reply_to: Optional[str] = None, attachments: Optional[List] = None, ): """ Send transactional email via Resend Args: to: Recipient email subject: Email subject html: HTML content (or use template) text: Plain text content template: Django template path (e.g., 'emails/welcome.html') context: Template context from_email: Override sender reply_to: Reply-to address attachments: List of attachments """ if not self._resend_configured: logger.error("Resend not configured - falling back to Django mail") return self._send_django_mail(to, subject, text or html) # Render template if provided if template: html = render_to_string(template, context or {}) try: params = { 'from': from_email or f"{self._resend_config.get('from_name', 'IGNY8')} <{self._resend_config.get('from_email', 'noreply@igny8.com')}>", 'to': [to] if isinstance(to, str) else to, 'subject': subject, } if html: params['html'] = html if text: params['text'] = text if reply_to: params['reply_to'] = reply_to response = resend.Emails.send(params) logger.info(f"Email sent via Resend: {subject} to {to}") return response except Exception as e: logger.error(f"Failed to send email via Resend: {str(e)}") raise def _send_django_mail(self, to, subject, message): """Fallback to Django's send_mail""" from django.core.mail import send_mail from django.conf import settings send_mail( subject=subject, message=message, from_email=settings.DEFAULT_FROM_EMAIL, recipient_list=[to], fail_silently=False, ) # Singleton instance _email_service = None def get_email_service(): global _email_service if _email_service is None: _email_service = EmailService() return _email_service # Convenience functions def send_welcome_email(user, account): """Send welcome email after signup""" service = get_email_service() service.send_transactional( to=user.email, subject='Welcome to IGNY8!', template='emails/welcome.html', context={ 'user': user, 'account': account, 'login_url': f'{settings.FRONTEND_URL}/login', } ) def send_password_reset_email(user, reset_token): """Send password reset email""" service = get_email_service() service.send_transactional( to=user.email, subject='Reset Your IGNY8 Password', template='emails/password_reset.html', context={ 'user': user, 'reset_url': f'{settings.FRONTEND_URL}/reset-password?token={reset_token}', } ) def send_payment_confirmation(account, payment): """Send payment confirmation email""" service = get_email_service() service.send_transactional( to=account.billing_email or account.owner.email, subject=f'Payment Confirmed - Invoice #{payment.invoice.invoice_number}', template='emails/payment_confirmed.html', context={ 'account': account, 'payment': payment, } ) def send_subscription_activated(account, subscription): """Send subscription activation email""" service = get_email_service() service.send_transactional( to=account.billing_email or account.owner.email, subject=f'Subscription Activated - {subscription.plan.name}', template='emails/subscription_activated.html', context={ 'account': account, 'subscription': subscription, 'plan': subscription.plan, } ) def send_low_credits_warning(account, current_credits, threshold): """Send low credits warning""" service = get_email_service() service.send_transactional( to=account.billing_email or account.owner.email, subject='Low Credits Warning - IGNY8', template='emails/low_credits.html', context={ 'account': account, 'current_credits': current_credits, 'threshold': threshold, 'topup_url': f'{settings.FRONTEND_URL}/account/usage', } ) ``` ### 4.2 Brevo (Marketing) - Future **Configuration (when needed):** ```json { "provider_id": "brevo", "display_name": "Brevo", "provider_type": "email", "api_key": "xkeysib-xxx...", "config": { "from_email": "hello@igny8.com", "from_name": "IGNY8", "list_id": 123 }, "is_active": false } ``` **Requirements:** `sib-api-v3-sdk>=7.6.0` --- ## 5. Required Credentials Checklist > **Note:** All credentials are stored in the `IntegrationProvider` model and can be configured via Django Admin. The implementation supports both sandbox (testing) and production environments. ### 5.1 Stripe Credentials | Item | Value | Status | |------|-------|--------| | Publishable Key (Live) | `pk_live_...` | ⬜ Add to IntegrationProvider | | Secret Key (Live) | `sk_live_...` | ⬜ Add to IntegrationProvider | | Webhook Signing Secret | `whsec_...` | ⬜ Add to IntegrationProvider | | Products/Prices Created | Plan IDs | ⬜ Create in Stripe Dashboard | | **Code Implementation** | — | ✅ **Complete** | **Stripe Products to Create:** 1. **Starter Plan** - $99/mo - `price_starter_monthly` 2. **Growth Plan** - $199/mo - `price_growth_monthly` 3. **Scale Plan** - $299/mo - `price_scale_monthly` ### 5.2 PayPal Credentials | Item | Value | Status | |------|-------|--------| | Client ID (Live) | `AY...` | ⬜ Add to IntegrationProvider | | Client Secret (Live) | `EL...` | ⬜ Add to IntegrationProvider | | Webhook ID | `WH-...` | ⬜ Add to IntegrationProvider config | | Subscription Plans Created | PayPal Plan IDs | ⬜ Create in PayPal Dashboard | | **Code Implementation** | — | ✅ **Complete** | ### 5.3 Resend Credentials | Item | Value | Status | |------|-------|--------| | API Key | `re_...` | ⬜ Add to IntegrationProvider | | Domain Verified | igny8.com | ⬜ Configure in Resend Dashboard | | DNS Records Added | DKIM, SPF | ⬜ Add to DNS provider | | **Code Implementation** | — | ✅ **Complete** | ### 5.4 Brevo Credentials (Future) | Item | Value | Status | |------|-------|--------| | API Key | `xkeysib-...` | ⬜ Future | | Contact List Created | List ID | ⬜ Future | --- ## 6. Implementation Order ### Phase 1: Backend Setup ✅ COMPLETE 1. **✅ Add dependencies:** ```bash pip install resend>=0.7.0 # PayPal uses requests (already installed) # Stripe already installed ``` 2. **✅ Create service files:** - ✅ `backend/igny8_core/business/billing/services/stripe_service.py` - ✅ `backend/igny8_core/business/billing/services/paypal_service.py` - ✅ Update `backend/igny8_core/business/billing/services/email_service.py` 3. **✅ Create view files:** - ✅ `backend/igny8_core/business/billing/views/stripe_views.py` - ✅ `backend/igny8_core/business/billing/views/paypal_views.py` 4. **✅ Add URL routes:** ```python # backend/igny8_core/business/billing/urls.py urlpatterns += [ path('stripe/checkout/', StripeCheckoutView.as_view()), path('stripe/credit-checkout/', StripeCreditCheckoutView.as_view()), path('stripe/billing-portal/', StripeBillingPortalView.as_view()), path('webhooks/stripe/', stripe_webhook), path('paypal/create-order/', PayPalCreateOrderView.as_view()), path('paypal/capture-order/', PayPalCaptureOrderView.as_view()), path('webhooks/paypal/', paypal_webhook), ] ``` 5. **✅ Add email templates:** ``` backend/igny8_core/templates/emails/ ├── base.html ├── welcome.html ├── password_reset.html ├── email_verification.html ├── payment_confirmation.html ├── payment_approved.html ├── payment_rejected.html ├── payment_failed.html ├── subscription_activated.html ├── subscription_renewal.html ├── low_credits.html └── refund_notification.html ``` ### Phase 2: Stripe Configuration ⏳ PENDING USER CREDENTIALS 1. ⏳ Create products and prices in Stripe Dashboard (User action needed) 2. ⏳ Configure webhook endpoint (User action needed) 3. ⏳ Get API keys and add to IntegrationProvider via admin (User action needed) 4. ⏳ Test in sandbox mode (After credentials added) ### Phase 3: Frontend Integration ✅ COMPLETE 1. ✅ Add billing API methods (stripe, paypal, purchaseCredits, subscribeToPlan) 2. ✅ Update PlansAndBillingPage with payment buttons 3. ✅ Add PaymentGatewaySelector component 4. ✅ Add success/cancel handling ### Phase 4: Email Configuration ⏳ PENDING USER CREDENTIALS 1. ⏳ Add Resend API key to IntegrationProvider (User action needed) 2. ⏳ Verify domain at resend.com (User action needed) 3. ⏳ Test all email triggers (After credentials added) ### Phase 5: Testing ⏳ PENDING USER CREDENTIALS 1. ⏳ Test Stripe checkout flow (sandbox) - After credentials added 2. ⏳ Test PayPal flow (sandbox) - After credentials added 3. ⏳ Test webhook handling - After webhooks configured 4. ⏳ Test email delivery - After Resend configured 5. ⏳ Switch to production credentials - After testing complete --- ## Summary **Implementation Status:** ✅ **CODE COMPLETE** - Ready for credentials and testing **Time Spent:** 2-3 days (Backend + Frontend implementation) **Pending Actions (User):** - ⏳ Stripe account: Create products/prices, configure webhooks, add API keys - ⏳ PayPal developer account: Create app, configure webhooks, add credentials - ⏳ Resend account: Verify domain, add API key **Files Created:** ✅ - ✅ `stripe_service.py` (500+ lines) - ✅ `stripe_views.py` (560+ lines) - ✅ `paypal_service.py` (500+ lines) - ✅ `paypal_views.py` (600+ lines) - ✅ Updated `email_service.py` (760+ lines) - ✅ 12 email templates - ✅ `PaymentGatewaySelector.tsx` (160+ lines) - ✅ Updated `PlansAndBillingPage.tsx` - ✅ Updated `billing.api.ts` (+285 lines) **Existing Infrastructure Used:** - ✅ `IntegrationProvider` model for all credentials - ✅ `Payment`, `Invoice`, `CreditPackage` models - ✅ `PaymentService` for payment processing - ✅ `CreditService` for credit management **Next Steps:** 1. Add Stripe credentials to Django Admin → Integration Providers 2. Add PayPal credentials to Django Admin → Integration Providers 3. Add Resend API key to Django Admin → Integration Providers 4. Test payment flows in sandbox mode 5. Switch to production credentials when ready