Hi,
To answer your direct question first: for a status change, the customer email is sent when three things are true, not just "notified":
1. the history "notified" flag is set (which you confirmed),
2. no plugin clears the send flag during the onAfterOrderUpdate event,
3. the order has a customer email address to send to.
HikaSerial hooks onAfterOrderUpdate too, but it only assigns the serials there; it does not clear the send flag. At send time it adds the serials into the status email rather than blocking it. So a serial-ticket order has no extra email condition compared to a normal order, it should send like any other.
Since "notified" is confirmed and HikaSerial is not blocking it, the cleanest next step is to log the email decision so we see exactly where it is lost. In administrator/components/com_hikashop/classes/order.php, find the order update branch (search for "history_notified" near "onAfterOrderUpdate") and add the four hikashop_writeToLog lines marked NEW:
$send_email = @$order->history->history_notified;
hikashop_writeToLog('EMAILDEBUG order '.@$order->order_id.': update branch reached, notified='.var_export($send_email,true).', status='.@$order->order_status); // NEW
$app->triggerEvent('onAfterOrderUpdate', array( &$order, &$send_email) );
hikashop_writeToLog('EMAILDEBUG order '.@$order->order_id.': after onAfterOrderUpdate, send_email='.var_export($send_email,true)); // NEW
$historyClass->addRecord($order);
if(!$send_email) {
hikashop_writeToLog('EMAILDEBUG order '.@$order->order_id.': SUPPRESSED, send_email empty after the plugins'); // NEW
return $order->order_id;
}
a bit further down, where the mail is sent:
if(!empty($order->mail)) {
$mailClass = hikashop_get('class.mail');
hikashop_writeToLog('EMAILDEBUG order '.@$order->order_id.': dst_email='.var_export(@$order->mail->dst_email,true).', mail_name='.@$order->mail->mail_name); // NEW
if(!empty($order->mail->dst_email)) {
$mailClass->sendMail($order->mail);
hikashop_writeToLog('EMAILDEBUG order '.@$order->order_id.': sendMail done, success='.var_export(@$mailClass->mail_success,true)); // NEW
}
Then place one order paid through RO Payments (the failing case) and change one order's status by hand in the backend (the working case with the user notification activated), and send me the EMAILDEBUG lines from the HikaShop log for both. They will tell us exactly where it diverges:
- if the "update branch reached" line is missing for the RO Payments order, then the order modification did not save through this path at all (for example the order object, not its id, was passed), and the issue is on the call side, not the email,
- if "send_email" becomes empty after onAfterOrderUpdate, a plugin is clearing it,
- if send_email stays set but dst_email is empty, the order has no recipient in that context,
- if sendMail runs with success=false, it is a mailer problem.
If that second line shows send_email becoming empty after onAfterOrderUpdate (the most likely case), a plugin is clearing it. To find exactly which one, replace that single dispatch line:
$app->triggerEvent('onAfterOrderUpdate', array( &$order, &$send_email) );
with this block, which runs the same plugins one by one (each exactly once, so the order is still processed normally) and logs the culprit:
$diagDispatcher = new \Joomla\Event\Dispatcher();
foreach(JPluginHelper::getPlugin('hikashop') as $diagP) {
$diagClass = 'plgHikashop'.ucfirst($diagP->name);
if(!class_exists($diagClass))
continue;
try {
$diagInst = new $diagClass($diagDispatcher, (array)$diagP);
if(!method_exists($diagInst, 'onAfterOrderUpdate'))
continue;
$diagBefore = $send_email;
$diagInst->onAfterOrderUpdate($order, $send_email);
hikashop_writeToLog('EMAILDEBUG order '.@$order->order_id.': plugin "'.$diagP->name.'" -> send_email='.var_export($send_email,true).($send_email != $diagBefore ? ' <<< CLEARED BY THIS PLUGIN' : ''));
} catch(\Throwable $diagE) {
hikashop_writeToLog('EMAILDEBUG order '.@$order->order_id.': plugin "'.$diagP->name.'" could not be run on its own: '.$diagE->getMessage());
}
}
The line marked "<<< CLEARED BY THIS PLUGIN" names the culprit. A throwaway dispatcher is used so the plugins are not re-registered on the live one, and each plugin still runs exactly once, so the order processes as usual. Put the original triggerEvent line back once you have the name.
Remove the NEW lines once we have the answer.