## 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.5 KiB
6.5 KiB
Segmentation System
Overview
The segmentation system allows brands to create dynamic audience segments based on buyer behavior, demographics, and purchase history. Segments are brand-scoped and automatically refresh to reflect current data.
Database Schema
segments Table
id- Primary keybrand_id- Owner brand (scoped to single brand)name- Segment name (e.g., "High-Value Buyers", "Inactive 90 Days")description- Purpose/criteria descriptionconditions- JSON array of conditions (see format below)buyer_count- Cached count (refreshed periodically)last_refreshed_at- Last time segment was calculatedis_active- Toggle for active/inactivecreated_at,updated_at
segment_buyers Pivot Table
segment_idbuyer_business_id- Business in the segment- Tracks which buyers currently match segment conditions
Condition Format
Conditions stored as JSON array in segments.conditions:
[
{
"field": "total_order_value",
"operator": "greater_than",
"value": 10000
},
{
"field": "last_order_date",
"operator": "within_days",
"value": 30
},
{
"field": "purchased_product_sku",
"operator": "equals",
"value": "TB-BM-AZ1G"
}
]
Available Fields
Buyer Fields
total_order_value- Lifetime order totaltotal_order_count- Number of orders placedaverage_order_value- Average order amountlast_order_date- Date of most recent orderfirst_order_date- Date of first orderbuyer_type- Business typebuyer_state- Geographic statebuyer_city- Geographic city
Product Purchase Fields
purchased_product_sku- Has purchased specific SKUpurchased_product_category- Has purchased from categorypurchased_brand- Has purchased from specific brandpurchase_frequency- Orders per month
Available Operators
equals- Exact matchnot_equals- Does not matchgreater_than- Numeric comparison (>)less_than- Numeric comparison (<)greater_than_or_equal- Numeric comparison (>=)less_than_or_equal- Numeric comparison (<=)contains- String contains substringwithin_days- Date within X daysolder_than_days- Date older than X days
Segment Refresh Process
Automatic Refresh
Segments refresh automatically via scheduled task:
Schedule: Hourly (or configurable)
Command: php artisan segments:refresh
Process:
- Loop through all active segments
- For each segment, query buyers matching conditions
- Update
segment_buyerspivot table - Update
buyer_countandlast_refreshed_at
Manual Refresh
Admin can trigger refresh:
- Via admin panel: "Refresh Segment" button
- Route:
POST /s/{business}/marketing/segments/{segment}/refresh
Refresh Logic
// Example refresh logic
$segment = Segment::find($id);
$query = Business::where('type', 'buyer');
foreach ($segment->conditions as $condition) {
switch ($condition['field']) {
case 'total_order_value':
$query->whereHas('orders', function($q) use ($condition) {
$q->havingRaw('SUM(total) ' . $condition['operator'] . ' ?', [$condition['value']]);
});
break;
case 'last_order_date':
if ($condition['operator'] === 'within_days') {
$query->whereHas('orders', function($q) use ($condition) {
$q->where('created_at', '>=', now()->subDays($condition['value']));
});
}
break;
case 'purchased_product_sku':
$query->whereHas('orders.items.product', function($q) use ($condition) {
$q->where('sku', $condition['operator'], $condition['value']);
});
break;
}
}
$buyers = $query->pluck('id');
$segment->buyers()->sync($buyers);
$segment->update([
'buyer_count' => $buyers->count(),
'last_refreshed_at' => now()
]);
Brand Scoping
Critical: Segments are ALWAYS scoped to a single brand.
Why Brand-Scoped?
- Privacy: Brand A shouldn't see Brand B's buyers
- Relevance: Segments based on purchases from specific brand
- Campaign Targeting: Campaigns send from a brand to their buyers
Implementation
// Controller
$segments = Segment::where('brand_id', $brand->id)->get();
// Creating segment
$segment = $brand->segments()->create([
'name' => $request->name,
'conditions' => $request->conditions
]);
Usage in Campaigns
Segments are used to target campaigns:
- Campaign Creation: Select segments to target
- Recipient Resolution: When campaign sends:
- Resolve segment → get buyer list
- For each buyer, send message
- Dynamic: Segment refreshes before send for latest data
Example:
$campaign->segments()->attach([1, 2, 3]); // Attach multiple segments
$buyers = $campaign->segments->flatMap->buyers->unique('id');
// Send campaign to each buyer
Segment Builder UI
Creating Segments (/s/{business}/marketing/segments/create)
Form Fields:
- Name (required)
- Description
- Conditions builder:
- Add condition button
- Field dropdown
- Operator dropdown (dynamic based on field)
- Value input (text/number/date based on field)
- Remove condition button
Condition Builder
<div x-data="{ conditions: [] }">
<template x-for="(condition, index) in conditions">
<div class="flex gap-2">
<select x-model="condition.field">
<option value="total_order_value">Total Order Value</option>
<option value="last_order_date">Last Order Date</option>
{{-- etc --}}
</select>
<select x-model="condition.operator">
{{-- Dynamic based on field type --}}
</select>
<input type="text" x-model="condition.value">
<button @click="conditions.splice(index, 1)">Remove</button>
</div>
</template>
<button @click="conditions.push({field: '', operator: '', value: ''})">
Add Condition
</button>
</div>
Performance Considerations
- Caching: Store
buyer_countto avoid expensive queries on list view - Indexing: Index on
brand_id,is_active,last_refreshed_at - Queue: Run refresh in background job for large segments
- Throttling: Limit refresh frequency (max once per hour per segment)
Future Enhancements
- Condition groups (AND/OR logic)
- Nested conditions
- Predictive segments (ML-based)
- Segment comparison (overlap analysis)
- Export segment members to CSV