## 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
6.2 KiB
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
brandstable ormarketing_channelstable
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:
- Brand Resolution: Campaign belongs to a brand
- Sender Identity: Use brand's SendPortal configuration:
- From Email:
support@cannabrands.sendportal.io - From Name:
Cannabrands - Reply-To: Brand's configured reply-to
- From Email:
- API Call: Send via brand's SendPortal workspace
- 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:
- Create
Messagerecord with:conversation_id(if reply) or create new conversationbrand_id= campaign's branddirection= 'outbound'channel= 'email'message_id= SendPortal message IDdelivery_status= 'sent'
- Store campaign-message relationship for reporting
Logging to Conversations
Automatic Conversation Creation
When campaign email is sent:
- Check Existing: Does conversation exist for this brand + buyer?
- Create if New: Create conversation with subject = campaign name
- 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:
- Verify webhook signature (security)
- Parse event type and message ID
- Find
Messagerecord bymessage_id - Update
delivery_statusanddelivery_status_detail - 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:
- Get brand's default email channel
- Use channel's provider (SendPortal, SMTP, Mailgun, etc.)
- Send using provider's API/driver
- 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