diff --git a/docs/plans/THIRD-PARTY-INTEGRATIONS-PLAN.md b/docs/plans/THIRD-PARTY-INTEGRATIONS-PLAN.md new file mode 100644 index 00000000..1e437ae3 --- /dev/null +++ b/docs/plans/THIRD-PARTY-INTEGRATIONS-PLAN.md @@ -0,0 +1,1248 @@ +# 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