Customer Segment Email Automation - Developer Guide v25.11+
This guide covers the technical implementation details of Maho's Customer Segment Email Automation system, including database schema, architecture, code examples, and extension points.
Architecture Overview
The email automation system is built on three core subsystems:
┌─────────────────────┐
│ Customer Segments │ (Segment matching & membership)
└──────────┬──────────┘
│ triggers
↓
┌─────────────────────┐
│ Email Automation │ (Sequence scheduling & progression)
└──────────┬──────────┘
│ generates
↓
┌─────────────────────┐
│ Newsletter System │ (Email delivery via queue)
└─────────────────────┘
Key Components
| Component | Location | Purpose |
|---|---|---|
| Segment Model | Maho_CustomerSegmentation_Model_Segment | Manages segment logic and customer matching |
| Email Automation Observer | Maho_CustomerSegmentation_Model_Observer_EmailAutomation | Handles segment change events and email sending |
| Email Sequence Model | Maho_CustomerSegmentation_Model_EmailSequence | Represents individual sequence steps |
| Sequence Progress Model | Maho_CustomerSegmentation_Model_SequenceProgress | Tracks customer progress through sequences |
| Coupon Helper | Maho_CustomerSegmentation_Helper_Coupon | Generates and manages automation coupons |
| Cron Model | Maho_CustomerSegmentation_Model_Cron | Executes scheduled tasks |
Database Schema
customer_segment (Extended)
New field added for email automation:
ALTER TABLE customer_segment ADD COLUMN (
auto_email_active SMALLINT NOT NULL DEFAULT 0 -- Master on/off switch
);
Trigger Type Configuration
Trigger type ('enter' or 'exit') is configured per-sequence using the trigger_event field in the customer_segment_email_sequence table. This allows one segment to have both enter AND exit sequences.
customer_segment_email_sequence
Stores email sequence configuration:
CREATE TABLE customer_segment_email_sequence (
sequence_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
segment_id INT UNSIGNED NOT NULL,
trigger_event VARCHAR(10) NOT NULL DEFAULT 'enter', -- 'enter' or 'exit'
template_id INT UNSIGNED NOT NULL, -- Newsletter template
step_number INT UNSIGNED NOT NULL,
delay_minutes INT UNSIGNED NOT NULL DEFAULT 0,
is_active SMALLINT NOT NULL DEFAULT 1,
max_sends INT UNSIGNED NOT NULL DEFAULT 1,
generate_coupon SMALLINT NOT NULL DEFAULT 0,
coupon_sales_rule_id INT UNSIGNED NULL,
coupon_prefix VARCHAR(50) NULL,
coupon_expires_days INT UNSIGNED NOT NULL DEFAULT 30,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY unique_segment_trigger_step (segment_id, trigger_event, step_number),
FOREIGN KEY (segment_id) REFERENCES customer_segment(segment_id) ON DELETE CASCADE,
FOREIGN KEY (template_id) REFERENCES newsletter_template(template_id),
FOREIGN KEY (coupon_sales_rule_id) REFERENCES salesrule(rule_id) ON DELETE SET NULL
);
Key Field: trigger_event - Values: 'enter' or 'exit' - Determines when this sequence is triggered - Allows independent enter and exit sequences for the same segment - Step numbers are unique per (segment_id, trigger_event) combination
customer_segment_sequence_progress
Tracks individual customer progress:
CREATE TABLE customer_segment_sequence_progress (
progress_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
customer_id INT UNSIGNED NOT NULL,
segment_id INT UNSIGNED NOT NULL,
sequence_id INT UNSIGNED NOT NULL,
queue_id INT UNSIGNED NULL, -- Links to newsletter_queue
step_number INT UNSIGNED NOT NULL,
trigger_type VARCHAR(10) NOT NULL, -- 'enter' or 'exit'
scheduled_at TIMESTAMP NULL, -- When to send
sent_at TIMESTAMP NULL, -- When sent
status VARCHAR(20) NOT NULL DEFAULT 'scheduled', -- 'scheduled', 'sent', 'failed', 'skipped'
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_customer_segment (customer_id, segment_id),
INDEX idx_scheduled (scheduled_at, status),
FOREIGN KEY (customer_id) REFERENCES customer_entity(entity_id) ON DELETE CASCADE,
FOREIGN KEY (segment_id) REFERENCES customer_segment(segment_id) ON DELETE CASCADE,
FOREIGN KEY (sequence_id) REFERENCES customer_segment_email_sequence(sequence_id) ON DELETE CASCADE,
FOREIGN KEY (queue_id) REFERENCES newsletter_queue(queue_id) ON DELETE SET NULL
);
newsletter_queue (Extended)
New fields for automation tracking:
ALTER TABLE newsletter_queue ADD COLUMN (
automation_source VARCHAR(50) DEFAULT NULL, -- 'customer_segmentation'
automation_source_id INT UNSIGNED DEFAULT NULL -- segment_id
);
Event System
Events Dispatched
customer_segment_refresh_after
Fired after a segment refreshes its customer membership.
Mage::dispatchEvent('customer_segment_refresh_after', [
'segment' => $segment, // Maho_CustomerSegmentation_Model_Segment
'matched_customers' => $customerIds, // array of customer IDs
]);
Use case: Start email sequences when customers enter/exit segments
customer_segmentation_process_scheduled_emails
Fired by cron to process ready-to-send emails.
Use case: Trigger email queue processing
newsletter_subscriber_save_after
Standard Maho event, observed to stop sequences on unsubscribe.
Mage::dispatchEvent('newsletter_subscriber_save_after', [
'subscriber' => $subscriber, // Mage_Newsletter_Model_Subscriber
]);
Use case: Cleanup sequences when customer unsubscribes
customer_delete_after
Standard Maho event, observed to cleanup sequence data.
Mage::dispatchEvent('customer_delete_after', [
'customer' => $customer, // Mage_Customer_Model_Customer
]);
Use case: Remove orphaned sequence progress records
Observer Configuration
<events>
<customer_segment_refresh_after>
<observers>
<customersegmentation_email_automation>
<class>customersegmentation/observer_emailAutomation</class>
<method>onSegmentRefreshAfter</method>
</customersegmentation_email_automation>
</observers>
</customer_segment_refresh_after>
<newsletter_subscriber_save_after>
<observers>
<customersegmentation_subscriber_change>
<class>customersegmentation/observer_emailAutomation</class>
<method>onNewsletterSubscriberSaveAfter</method>
</customersegmentation_subscriber_change>
</observers>
</newsletter_subscriber_save_after>
</events>
Code Examples
Programmatically Start a Sequence
// Load segment
$segment = Mage::getModel('customersegmentation/segment')->load($segmentId);
// Start sequence for a customer
$customerId = 123;
$triggerType = Maho_CustomerSegmentation_Model_EmailSequence::TRIGGER_ENTER;
// or: Maho_CustomerSegmentation_Model_EmailSequence::TRIGGER_EXIT
$segment->startEmailSequence($customerId, $triggerType);
Check if Customer is in an Active Sequence
$segment = Mage::getModel('customersegmentation/segment')->load($segmentId);
$customerId = 123;
// Check specific trigger type
if ($segment->hasActiveSequence($customerId, 'enter')) {
// Customer has active enter sequence
}
// Check any trigger type
if ($segment->hasAnyActiveSequence($customerId)) {
// Customer has any active sequence
}
Get Sequence Progress for a Customer
$collection = Mage::getResourceModel('customersegmentation/sequenceProgress_collection')
->addCustomerFilter($customerId)
->addSegmentFilter($segmentId)
->addStatusFilter('scheduled'); // or 'sent', 'failed', 'skipped'
foreach ($collection as $progress) {
/** @var Maho_CustomerSegmentation_Model_SequenceProgress $progress */
echo "Step {$progress->getStepNumber()}: {$progress->getStatus()}\n";
echo "Scheduled: {$progress->getScheduledAt()}\n";
if ($progress->getSentAt()) {
echo "Sent: {$progress->getSentAt()}\n";
}
}
Manually Process Scheduled Emails
// This is what cron does
$observer = Mage::getModel('customersegmentation/observer_emailAutomation');
$observer->processScheduledEmails(new Varien_Event_Observer());
Generate a Coupon
$helper = Mage::helper('customersegmentation/coupon');
$couponCode = $helper->generateCustomerCoupon(
$customerId, // int
$salesRuleId, // int
'CART', // string prefix
30 // int days until expiration
);
if ($couponCode) {
echo "Generated: {$couponCode}\n";
}
Get Coupon Template Variables
$helper = Mage::helper('customersegmentation/coupon');
$rule = Mage::getModel('salesrule/rule')->load($ruleId);
$variables = $helper->getCouponTemplateVariables(
'CART123ABC', // coupon code
'2025-11-20', // expiration date (Y-m-d)
$rule // Mage_SalesRule_Model_Rule
);
print_r($variables);
/*
Array(
[coupon_code] => CART123ABC
[coupon_expires_date] => 2025-11-20
[coupon_expires_formatted] => Nov 20, 2025
[coupon_discount_amount] => 15
[coupon_discount_text] => 15% off
[coupon_description] => Cart abandonment discount
)
*/
Validate a Sales Rule for Coupon Generation
$helper = Mage::helper('customersegmentation/coupon');
$errors = $helper->validateSalesRule($ruleId);
if (!empty($errors)) {
foreach ($errors as $error) {
echo "Error: {$error}\n";
}
}
Create Email Sequence Programmatically
$sequence = Mage::getModel('customersegmentation/emailSequence');
$sequence->setSegmentId($segmentId)
->setTemplateId($templateId)
->setStepNumber(1)
->setDelayMinutes(60) // 1 hour delay
->setIsActive(1)
->setGenerateCoupon(1)
->setCouponSalesRuleId($ruleId)
->setCouponPrefix('CART')
->setCouponExpiresDays(7)
->save();
Get Automation Statistics
$segment = Mage::getModel('customersegmentation/segment')->load($segmentId);
$stats = $segment->getEmailAutomationStats();
print_r($stats);
/*
Array(
[total_sequences] => 150
[scheduled] => 45
[sent] => 100
[failed] => 5
[open_rate] => 0.35 // If tracking enabled
[click_rate] => 0.12
[conversion_rate] => 0.08
)
*/
Extending the System
Add Custom Template Variables
Hook into the email sending process to add custom variables:
class My_Module_Model_Observer
{
public function addCustomTemplateVariables(Varien_Event_Observer $observer)
{
// You would need to create this event in the core
// This is a conceptual example
$variables = $observer->getVariables();
$customer = $observer->getCustomer();
// Add custom data
$variables['customer_loyalty_points'] = $this->getLoyaltyPoints($customer);
$variables['customer_tier'] = $this->getCustomerTier($customer);
$variables['recommended_products'] = $this->getRecommendations($customer);
}
}
Create Custom Trigger Types
While the core supports 'enter' and 'exit', you could extend this:
// In your module's observer
public function onCustomEvent(Varien_Event_Observer $observer)
{
$customer = $observer->getCustomer();
$segment = $this->determineSegment($customer);
if ($segment && $segment->hasEmailAutomation()) {
// Start custom sequence
$segment->startEmailSequence(
$customer->getId(),
'custom_trigger' // Your custom trigger
);
}
}
Implement Sequence Callbacks
Add logic after specific sequence steps:
class My_Module_Model_Observer
{
public function afterSequenceEmailSent(Varien_Event_Observer $observer)
{
$progress = $observer->getProgress();
$customer = $observer->getCustomer();
// Custom logic based on step
if ($progress->getStepNumber() === 3) {
// Tag customer in CRM
$this->tagInCrm($customer, 'abandoned_cart_step_3');
}
}
}
Performance Optimization
Batching Email Processing
The system processes emails in batches:
// In Maho_CustomerSegmentation_Model_Resource_SequenceProgress
public function getReadyToSendSequences(int $limit = 100): array
{
// Limits query to prevent memory issues
// Default: 100 emails per cron run
}
To adjust batch size, override this method in your module.
Index Optimization
Critical indexes for performance:
-- Progress lookup
CREATE INDEX idx_customer_segment ON customer_segment_sequence_progress(customer_id, segment_id);
-- Ready-to-send query
CREATE INDEX idx_scheduled ON customer_segment_sequence_progress(scheduled_at, status);
-- Segment lookup
CREATE INDEX idx_segment_active ON customer_segment(is_active, auto_email_active);
Caching Strategies
// Cache segment email sequences
$cacheKey = "segment_sequences_{$segmentId}";
$sequences = Mage::app()->getCache()->load($cacheKey);
if (!$sequences) {
$sequences = $segment->getEmailSequences();
Mage::app()->getCache()->save(
serialize($sequences),
$cacheKey,
[Maho_CustomerSegmentation_Model_Segment::CACHE_TAG],
3600 // 1 hour
);
}
Testing
Unit Testing Email Sequences
class Maho_CustomerSegmentation_Test_Model_SegmentTest extends PHPUnit\Framework\TestCase
{
public function testStartEmailSequence()
{
$segment = Mage::getModel('customersegmentation/segment');
$segment->setAutoEmailActive(1)
->setAutoEmailTrigger('enter')
->setId(1);
$customerId = 999;
$segment->startEmailSequence($customerId, 'enter');
// Verify progress record created
$collection = Mage::getResourceModel('customersegmentation/sequenceProgress_collection')
->addCustomerFilter($customerId)
->addSegmentFilter(1);
$this->assertGreaterThan(0, $collection->getSize());
}
}
Integration Testing
public function testCartAbandonmentSequence()
{
// Create test customer
$customer = $this->createTestCustomer();
$this->subscribeToNewsletter($customer);
// Create abandoned cart
$quote = $this->createAbandonedCart($customer);
// Trigger segment refresh
$segment = Mage::getModel('customersegmentation/segment')->load(1);
$segment->refreshCustomers();
// Verify sequence started
$progress = Mage::getResourceModel('customersegmentation/sequenceProgress_collection')
->addCustomerFilter($customer->getId())
->addSegmentFilter(1)
->getFirstItem();
$this->assertEquals('scheduled', $progress->getStatus());
// Simulate cron run
$observer = Mage::getModel('customersegmentation/observer_emailAutomation');
$observer->processScheduledEmails(new Varien_Event_Observer());
// Verify email sent
$progress->load($progress->getId()); // Reload
$this->assertEquals('sent', $progress->getStatus());
$this->assertNotNull($progress->getQueueId());
}
Debugging
Enable Detailed Logging
// In app/etc/local.xml or use Mage::log directly
Mage::log(
'Detailed automation info: ' . print_r($data, true),
Mage::LOG_DEBUG,
'customer_segmentation.log'
);
Common Debug Queries
-- Find stuck sequences
SELECT * FROM customer_segment_sequence_progress
WHERE status = 'scheduled'
AND scheduled_at < DATE_SUB(NOW(), INTERVAL 1 HOUR);
-- Count sequences by status
SELECT status, COUNT(*) as count
FROM customer_segment_sequence_progress
GROUP BY status;
-- Find customers with multiple active sequences
SELECT customer_id, COUNT(*) as sequence_count
FROM customer_segment_sequence_progress
WHERE status = 'scheduled'
GROUP BY customer_id
HAVING sequence_count > 1;
-- Check coupon usage
SELECT
sc.code,
sc.times_used,
sc.usage_limit,
sc.expiration_date,
sr.name as rule_name
FROM salesrule_coupon sc
JOIN salesrule sr ON sc.rule_id = sr.rule_id
WHERE sc.type = 3 -- Auto-generated
ORDER BY sc.created_at DESC
LIMIT 100;
Debugging Checklist
When emails aren't sending:
// 1. Check segment is active
$segment = Mage::getModel('customersegmentation/segment')->load($segmentId);
echo "Active: " . $segment->getIsActive() . "\n";
echo "Auto Email Active: " . $segment->getAutoEmailActive() . "\n";
echo "Trigger: " . $segment->getAutoEmailTrigger() . "\n";
// 2. Check customer is in segment
$inSegment = Mage::getResourceModel('customersegmentation/segment')
->isCustomerInSegment($segmentId, $customerId, $websiteId);
echo "In segment: " . ($inSegment ? 'YES' : 'NO') . "\n";
// 3. Check newsletter subscription
$subscriber = Mage::getModel('newsletter/subscriber')
->loadByCustomerId($customerId);
echo "Subscribed: " . ($subscriber->getSubscriberStatus() == 1 ? 'YES' : 'NO') . "\n";
// 4. Check sequence configuration
$sequences = $segment->getEmailSequences();
echo "Sequences: " . $sequences->getSize() . "\n";
// 5. Check progress records
$progress = Mage::getResourceModel('customersegmentation/sequenceProgress_collection')
->addCustomerFilter($customerId)
->addSegmentFilter($segmentId);
foreach ($progress as $p) {
echo "Step {$p->getStepNumber()}: {$p->getStatus()}\n";
}
// 6. Check newsletter queue
if ($progress->getQueueId()) {
$queue = Mage::getModel('newsletter/queue')->load($progress->getQueueId());
echo "Queue status: " . $queue->getQueueStatus() . "\n";
}
Security Considerations
Newsletter Subscription Validation
Critical: Always verify newsletter subscription status:
protected function isCustomerSubscribed(int $customerId): bool
{
$collection = Mage::getResourceModel('newsletter/subscriber_collection');
$collection->addFieldToFilter('customer_id', $customerId);
$subscriber = $collection->getFirstItem();
return $subscriber->getId() &&
(int) $subscriber->getSubscriberStatus() === Mage_Newsletter_Model_Subscriber::STATUS_SUBSCRIBED;
}
Coupon Security
Unique coupons prevent sharing:
Rate Limiting
Prevent email spam:
// In your custom observer
protected function checkEmailRateLimit(int $customerId): bool
{
// Max 5 automation emails per day per customer
$cutoff = date('Y-m-d H:i:s', strtotime('-24 hours'));
$count = Mage::getResourceModel('customersegmentation/sequenceProgress_collection')
->addCustomerFilter($customerId)
->addFieldToFilter('sent_at', ['gteq' => $cutoff])
->getSize();
return $count < 5;
}
Migration & Upgrades
Adding New Fields
// In sql/maho_customersegmentation_setup/upgrade-X.X.X-Y.Y.Y.php
$installer = $this;
$installer->startSetup();
$installer->getConnection()->addColumn(
$installer->getTable('customer_segment_email_sequence'),
'new_field_name',
[
'type' => Maho\Db\Ddl\Table::TYPE_VARCHAR,
'length' => 255,
'nullable' => true,
'comment' => 'New Field Description',
]
);
$installer->endSetup();
Data Migration
// Migrate from custom implementation
$oldSequences = $connection->fetchAll("SELECT * FROM old_email_sequences");
foreach ($oldSequences as $old) {
$sequence = Mage::getModel('customersegmentation/emailSequence');
$sequence->setSegmentId($old['segment_id'])
->setTemplateId($old['template_id'])
->setStepNumber($old['step'])
->setDelayMinutes($old['delay_hours'] * 60)
->save();
}
Best Practices for Developers
1. Use Resource Models
Always use resource models for database operations:
// Good
$resource = Mage::getResourceSingleton('customersegmentation/sequenceProgress');
$readySequences = $resource->getReadyToSendSequences(100);
// Avoid
$connection = Mage::getSingleton('core/resource')->getConnection('core_read');
$result = $connection->fetchAll("SELECT * FROM customer_segment_sequence_progress...");
2. Leverage Collections
$collection = Mage::getResourceModel('customersegmentation/emailSequence_collection')
->addSegmentFilter($segmentId)
->addActiveFilter()
->addStepNumberOrder('ASC');
3. Handle Exceptions Gracefully
try {
$segment->startEmailSequence($customerId, $triggerType);
} catch (Exception $e) {
Mage::logException($e);
Mage::log(
"Failed to start sequence for customer {$customerId}: " . $e->getMessage(),
Mage::LOG_ERROR,
'customer_segmentation.log'
);
}
4. Use Type Hints
public function generateCustomerCoupon(
int $customerId,
int $ruleId,
string $prefix = 'AUTO',
int $expireDays = 30
): ?string {
// Implementation
}
5. Document Template Variables
When adding custom variables, document them:
/**
* Get template variables for email
*
* Available variables:
* - customer: Full customer object
* - customer_firstname: First name
* - customer_lastname: Last name
* - segment_name: Segment name
* - step_number: Current step
*
* @param Mage_Customer_Model_Customer $customer
* @param array $sequenceData
* @return array
*/
protected function getTemplateVariables($customer, $sequenceData): array
API Reference
Model Methods
Maho_CustomerSegmentation_Model_Segment
public function startEmailSequence(int $customerId, string $triggerType): void
public function hasActiveSequence(int $customerId, string $triggerType): bool
public function hasAnyActiveSequence(int $customerId): bool
public function getEmailSequences(): Maho_CustomerSegmentation_Model_Resource_EmailSequence_Collection
public function getEmailAutomationStats(): array
public function hasEmailAutomation(): bool
Maho_CustomerSegmentation_Model_SequenceProgress
public function markAsSent(?int $queueId = null): self
public function markAsFailed(): self
public function markAsSkipped(): self
Maho_CustomerSegmentation_Helper_Coupon
public function generateCustomerCoupon(int $customerId, int $ruleId, string $prefix, int $expireDays): ?string
public function getCouponTemplateVariables(string $couponCode, ?string $expirationDate, ?Mage_SalesRule_Model_Rule $rule): array
public function validateSalesRule(int $ruleId): array
public function getAvailableSalesRules(): array
public function getCouponStats(string $couponCode): array
public function cleanupExpiredCoupons(int $daysOld = 30): int
Resource Model Methods
Maho_CustomerSegmentation_Model_Resource_SequenceProgress
public function getActiveSequenceCustomers(int $segmentId, string $triggerType): array
public function stopSequencesForCustomers(int $segmentId, array $customerIds, string $triggerType): int
public function getReadyToSendSequences(int $limit = 100): array
public function hasActiveSequence(int $customerId, int $segmentId, string $triggerType): bool
public function hasAnyActiveSequence(int $customerId, int $segmentId): bool
public function createSequenceProgress(int $customerId, int $segmentId, array $sequenceData, string $triggerType): void
public function cleanupOldProgress(int $daysOld = 90): int
Configuration Reference
System Configuration Paths
// Enable/disable email automation globally
customer_segmentation/email_automation/enabled // 1 or 0
// Cleanup settings
customer_segmentation/email_automation/cleanup_days // Default: 90
customer_segmentation/email_automation/coupon_cleanup_days // Default: 30