Files
hub/docs/sendportal.md
kelly ce5c670bf2 feat: Add AI Copilot module with multi-provider support + brand-scoped campaigns + comprehensive docs
## AI Copilot Module System
- Add copilot_enabled flag to businesses table
- Add Copilot Module toggle in Filament admin (Premium Features)
- Gate all Copilot buttons with copilot_enabled check
- Enable by default for Cannabrands only

## AI Settings Multi-Provider Support
- Support 5 AI providers: Anthropic, OpenAI, Perplexity, Canva, Jasper
- Separate encrypted API keys per provider
- Model dropdowns populated from config/ai.php
- Test Connection feature with real API validation
- Existing Connections summary shows status for all providers
- Provider-specific settings only shown when provider selected

## Brand-Scoped Campaign System
- Add brand_id to broadcasts table
- Brand selector component (reusable across forms)
- Campaign create/edit requires brand selection
- All campaigns now scoped to specific brand

## Documentation (9 New Files)
- docs/modules.md - Module system architecture
- docs/messaging.md - Messaging and conversation system
- docs/copilot.md - AI Copilot features and configuration
- docs/segmentation.md - Buyer segmentation system
- docs/sendportal.md - SendPortal multi-brand integration
- docs/campaigns.md - Campaign creation and delivery flow
- docs/conversations.md - Conversation lifecycle and threading
- docs/brand-settings.md - Brand configuration and voice system
- docs/architecture.md - High-level system overview

## Bug Fixes
- Remove duplicate FailedJobResource (Horizon already provides this at /horizon/failed)
- Fix missing Action import in Filament resources
- Update brand policy for proper access control

## Database Migrations
- 2025_11_23_175211_add_copilot_enabled_to_businesses_table.php
- 2025_11_23_180000_add_brand_id_to_broadcasts_table.php
- 2025_11_23_180326_update_ai_settings_for_multiple_providers.php
- 2025_11_23_184331_add_new_ai_providers_to_ai_settings.php

## Notes
- Pint:  Passed (all code style checks)
- Tests: Failing due to pre-existing database schema dump conflicts (not related to these changes)
- Schema issue needs separate fix: pgsql-schema.sql contains tables that migrations also create
2025-11-23 12:31:19 -07:00

6.2 KiB

SendPortal Integration

Overview

SendPortal is integrated as the primary email delivery provider for marketing campaigns. It provides multi-brand inbox support, campaign management, and delivery tracking.

Multi-Brand Usage

Architecture

Each brand has its own SendPortal workspace:

  • Workspace URL: https://{brand-slug}.sendportal.io
  • Inbox: Brand-specific email address (e.g., support@cannabrands.sendportal.io)
  • API Key: Stored in brands table or marketing_channels table

Brand-Level Configuration

Location: /s/{business}/brands/{brand}/edit → Email Settings

Fields:

  • SendPortal API Key
  • SendPortal Workspace URL
  • From Email (default sender)
  • From Name (default sender name)
  • Reply-To Email

Database Storage

Option 1: brands table

brands:
  - sendportal_api_key (encrypted)
  - sendportal_workspace_url
  - sendportal_from_email
  - sendportal_from_name

Option 2: marketing_channels table (preferred for multi-provider support)

marketing_channels:
  - brand_id
  - type ('email' or 'sms')
  - provider ('sendportal', 'smtp', 'twilio', etc.)
  - config (JSON with API keys, URLs, etc.)
  - from_email
  - from_name
  - is_default (one default per brand)

Campaign Sending via SendPortal

Mapping Campaigns to Brand Identity

When a campaign is sent:

  1. Brand Resolution: Campaign belongs to a brand
  2. Sender Identity: Use brand's SendPortal configuration:
    • From Email: support@cannabrands.sendportal.io
    • From Name: Cannabrands
    • Reply-To: Brand's configured reply-to
  3. API Call: Send via brand's SendPortal workspace
  4. Message-ID: SendPortal returns message ID for tracking

API Integration

// Example SendPortal API call
$client = new SendPortalClient($brand->sendportal_api_key, $brand->sendportal_workspace_url);

$response = $client->sendEmail([
    'from_email' => $brand->sendportal_from_email,
    'from_name' => $brand->sendportal_from_name,
    'to' => $recipient->email,
    'subject' => $campaign->subject,
    'body' => $campaign->content,
    'reply_to' => $brand->reply_to_email
]);

$messageId = $response['id'];

Campaign → Message Linkage

After sending:

  1. Create Message record with:
    • conversation_id (if reply) or create new conversation
    • brand_id = campaign's brand
    • direction = 'outbound'
    • channel = 'email'
    • message_id = SendPortal message ID
    • delivery_status = 'sent'
  2. Store campaign-message relationship for reporting

Logging to Conversations

Automatic Conversation Creation

When campaign email is sent:

  1. Check Existing: Does conversation exist for this brand + buyer?
  2. Create if New: Create conversation with subject = campaign name
  3. Append Message: Add message to conversation timeline

Benefits

  • Unified View: All brand ↔ buyer communications in one place
  • Context: See campaign messages alongside manual replies
  • Threading: Buyer replies to campaign email auto-thread to conversation

Implementation

// After sending campaign message
$conversation = Conversation::firstOrCreate([
    'brand_id' => $brand->id,
    'buyer_business_id' => $buyer->id,
    'subject' => $campaign->name
]);

$conversation->messages()->create([
    'brand_id' => $brand->id,
    'direction' => 'outbound',
    'channel' => 'email',
    'from' => $brand->sendportal_from_email,
    'to' => $buyer->email,
    'subject' => $campaign->subject,
    'body' => $campaign->content,
    'message_id' => $messageId,
    'delivery_status' => 'sent'
]);

Webhooks & Callbacks

SendPortal Webhook Events

SendPortal can send webhooks for:

  • sent - Message handed to provider
  • delivered - Message delivered to recipient
  • opened - Email opened (tracking pixel)
  • clicked - Link clicked
  • bounced - Email bounced
  • complained - Spam complaint
  • unsubscribed - Recipient unsubscribed

Webhook Endpoint

Route: POST /webhooks/sendportal

Handler: SendPortalWebhookController@handle

Processing:

  1. Verify webhook signature (security)
  2. Parse event type and message ID
  3. Find Message record by message_id
  4. Update delivery_status and delivery_status_detail
  5. Update delivery_attempted_at, delivery_detail_message
public function handle(Request $request)
{
    $event = $request->input('event'); // 'delivered', 'bounced', etc.
    $messageId = $request->input('message_id');

    $message = Message::where('message_id', $messageId)->first();

    if ($message) {
        $message->update([
            'delivery_status' => $this->mapEventToStatus($event),
            'delivery_status_detail' => $event,
            'delivery_detail_message' => $request->input('description'),
            'delivery_attempted_at' => now()
        ]);
    }
}

Status Mapping

SendPortal Event delivery_status UI Display
sent sent Blue "Sent" badge
delivered delivered Green "Delivered" badge
opened opened Green "Opened" badge
clicked clicked Green "Clicked" badge
bounced bounced Red "Bounced" badge
complained failed Red "Spam Complaint"

Multi-Provider Strategy

While SendPortal is primary, the system supports multiple email providers:

marketing_channels Table

Stores provider configurations per brand:

marketing_channels:
  id: 1
  brand_id: 5
  type: 'email'
  provider: 'sendportal'
  config: {
    "api_key": "encrypted-key",
    "workspace_url": "https://cannabrands.sendportal.io"
  }
  from_email: "support@cannabrands.sendportal.io"
  from_name: "Cannabrands"
  is_default: true

Provider Selection

When sending campaign:

  1. Get brand's default email channel
  2. Use channel's provider (SendPortal, SMTP, Mailgun, etc.)
  3. Send using provider's API/driver
  4. Log to conversations

Future Enhancements

  • Webhook Queue: Queue webhook processing for reliability
  • Retry Logic: Retry failed sends via SendPortal
  • Rate Limiting: Respect SendPortal rate limits
  • Template Sync: Sync email templates to SendPortal
  • List Management: Sync segments to SendPortal lists
  • Unsubscribe Handling: Auto-update contact preferences from SendPortal webhooks