Scheduling emails to be sent before an event takes place

I recently helped a client setting up tasks in Scheduler for products sold with SimpleCart. The goal was to send two email reminders when selling events, one 3 weeks before the scheduled date, and one 3 days before the event. As others may find themselves with similar needs, below you’ll find the resulting code.

Thanks to MediaKracht for the project and giving permission to share the result.

This code should be seen as inspiration; it will likely need tweaking for specific implementations.

The code requires SimpleCart, Scheduler, and email sending in MODX to be properly set up.

Setup:

  • Create a namespace via system > namespaces, the code below assumes “mynamespace”
  • Add a tv with the event date as a Date tv. (not date and time). Set a mynamespace.event_date_tv system setting with the name of your tv.
  • Add a setting mynamespace.event_reminder_parent with the ID of your events category
  • Create elements listed below.

Snippet: ReminderHook

The hook is added to the scFinishOrder snippet as a post hook, which runs when the payment was confirmed and schedules the tasks in SimpleCart.

In this example, 2 emails are scheduled (3 weeks, 3 days).

Emails are only scheduled when the products purchased are in a certain category.

<?php
/**
 * @var scHooks $hook
 * @var SimpleCart $sc
 */
$corePath = $modx->getOption('simplecart.core_path', null, $modx->getOption('core_path').'components/simplecart/') . 'model/simplecart/';
$sc = $modx->getService('simplecart','SimpleCart', $corePath, $scriptProperties);
if (!($sc instanceof SimpleCart)) {
    $modx->log(modX::LOG_LEVEL_ERROR, '[ReminderHook] Cannot load SimpleCart service');
    return false;
}

// Load the Scheduler service class
$path = $modx->getOption('scheduler.core_path', null, $modx->getOption('core_path') . 'components/scheduler/');
$scheduler = $modx->getService('scheduler', 'Scheduler', $path . 'model/scheduler/');
if (!($scheduler instanceof Scheduler)) {
    $modx->log(modX::LOG_LEVEL_ERROR, '[ReminderHook] Cannot load Scheduler service');
    return false;
}


// Get order from the the hook
$order =& $hook->getValue('order');
if (!($order instanceof simpleCartOrder)) { 
    $modx->log(modX::LOG_LEVEL_ERROR, '[ReminderHook] Order not available in hook');
    return false;
}
$products = $order->getMany('Product');
if (empty($products) || !is_array($products)) {
    $modx->log(modX::LOG_LEVEL_ERROR, '[ReminderHook] No products available for order');
    return false;
}


/**
 * Get the tasks to schedule
 */
$task = $scheduler->getTask('mynamespace', 'reminder');

/**
 * Configurations
 */
$targetParent = (int)$modx->getOption('mynamespace.event_reminder_parent', null, 61);
$dateTV = (string)$modx->getOption('mynamespace.event_date_tv', null, 'event_date');

foreach ($products as $product) {
    $resource = $product->getOne('Resource');
    if (!$resource) {
        continue;
    }
    
    $parent = (int)$resource->get('parent');
    if ($parent !== $targetParent) {
        continue;
    }
    
    $date = $resource->getTVValue($dateTV);
    $date = str_replace('/', '-', $date);
    $timestamp = strtotime($date . ' 08:00:00');
    
    $t3w = $timestamp - (21 * 24 * 60 * 60); // doesnt account for DST, but shouldn't be a problem for a simple reminder
    $t3d = $timestamp - (3 * 24 * 60 * 60);
    
    if ($task instanceof sTask) {
        if ($t3w > time()) {
            $task->schedule($t3w, [
                'templateSuffix' => '3Weeks',
                'order' => $order->get('id'),
                'ordernr' => $order->get('ordernr'),
                'product' => $product->get('id'),
                'resource' => $resource->get('id'), 
            ]);
        }
        
        if ($t3d > time()) {
            $task->schedule($t3d, [
                'templateSuffix' => '3Days',
                'order' => $order->get('id'),
                'ordernr' => $order->get('ordernr'),
                'product' => $product->get('id'),
                'resource' => $resource->get('id'), 
            ]);
        }
    }
    else {
        $modx->log(modX::LOG_LEVEL_ERROR, '[ReminderHook] Task mynamespace:reminder not found for scheduling for order ' . $order->get('ordernr') . ' / ' . $product->get('title'));
    }
}
return true;

Add the snippet to your scFinishOrder snippet call on the thank you page.

Replace mynamespace with the namespace you created.

Snippet: ReminderTask

This snippet is used as task in Scheduler. After creating the snippet, go to Extras > Scheduler and create a new task. Select your namespace mynamespace, give it the name reminder, select the type as snippet, and set the snippet to ReminderTask.

<?php
/**
 * @var SimpleCart $sc
 * @var sTask $task
 * @var sTaskRun $run
 */
$corePath = $modx->getOption('simplecart.core_path', null, $modx->getOption('core_path').'components/simplecart/') . 'model/simplecart/';
$sc = $modx->getService('simplecart','SimpleCart', $corePath, $scriptProperties);
if (!($sc instanceof SimpleCart)) {
    $modx->log(modX::LOG_LEVEL_ERROR, '[ReminderTask] Cannot load SimpleCart service');
    return false;
}

// Get order from the the hook
$orderId = (int)$modx->getOption('order', $scriptProperties, 0);
$order = $modx->getObject('simpleCartOrder', ['id' => $orderId]);
if (!($order instanceof simpleCartOrder)) { 
    $run->addError('order_not_found', ['order' => $orderId]);
    return 'Could not find order';
}

// Get product
$resourceId = $modx->getOption('resource', $scriptProperties, 0);
$resource = $modx->getObject('modResource', ['id' => $resourceId]);
if (!($resource instanceof modResource)) {
    $run->addError('resource_not_found', ['order' => $orderId, 'resource' => $resourceId]);
    return 'Could not find product resource';
}
// Store resource for snippets included in content
$modx->resource = $resource;

// Grab the order address record
$c = $modx->newQuery('simpleCartOrderAddress');
$c->where(array(
    'order_id' => $order->get('id'),
    'type' => 'order',
));
/** @var simpleCartOrderAddress $address */
$address = $modx->getObject('simpleCartOrderAddress', $c);
if (!($address instanceof simpleCartOrderAddress)) {
    $run->addError('order_address_not_found', ['order' => $orderId]);
    return 'OrderAddress could not be loaded';
}

$resourceArray = $resource->toArray();
foreach ($resource->getTemplateVars() as $tv) {
    $resourceArray[$tv->get('name')] = $tv->get('value');
}

$phs = [
    'order' => $order->toArray(),
    'address' => $address->toArray(),
    'resource' => $resourceArray,
];
$phs['_all'] = json_encode($phs, JSON_PRETTY_PRINT);

$customersEmail = $address->get('email');
$bcc = $modx->getOption('mynamespace.reminder_bcc', null, '');

$suffix = $modx->getOption('templateSuffix', $scriptProperties, '3Weeks');
$contentChunk = 'ReminderEmailContent' . $suffix;
$subjectChunk = 'ReminderEmailSubject' . $suffix;
$message = $modx->getChunk($contentChunk, $phs);
$subject = $modx->getChunk($subjectChunk, $phs);

$parser = $modx->getParser();

// Process all tags in the message
$parser->processElementTags('', $message, true, false, '[[', ']]', array(), 10);
$parser->processElementTags('', $message, true, true, '[[', ']]', array(), 10);
// Process all tags in the subject
$parser->processElementTags('', $subject, true, false, '[[', ']]', array(), 10);
$parser->processElementTags('', $subject, true, true, '[[', ']]', array(), 10);

$modx->getService('mail', 'mail.modPHPMailer');
$modx->mail->set(modMail::MAIL_BODY, $message);
$modx->mail->set(modMail::MAIL_FROM, $modx->getOption('emailsender'));
$modx->mail->set(modMail::MAIL_FROM_NAME, $modx->getOption('site_name'));
$modx->mail->set(modMail::MAIL_SUBJECT, $subject);
$modx->mail->address('to', $customersEmail);
if (!empty($bcc)) {
    $modx->mail->address('bcc', $bcc);
}
$modx->mail->address('reply-to', $modx->getOption('emailsender'));
$modx->mail->setHTML(true);
if (!$modx->mail->send()) {
    $msg = $modx->mail->mailer->ErrorInfo;
    $modx->mail->reset();
    $run->addError('error_sending_email', ['order' => $orderId, 'resource' => $resourceId, 'error' => $msg]);
    return 'An error occurred while trying to send the email: ' . $msg;
}
$modx->mail->reset();
return 'Email reminder sent.';

Chunks

The code assumes 4 chunks:

  • ReminderEmailContent3Weeks
  • ReminderEmailSubject3Weeks
  • ReminderEmailContent3Days
  • ReminderEmailSubject3Days

The names are self-explanatory.

To see all available placeholders, add the following in one of the content chunks:

<pre><code>[[+_all]]</code></pre>

For subjects you can use something like:

[[+address.fullname]], [[+resource.pagetitle]] is in 3 weeks!

Good luck!