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