How Product Messaging is built - observers, the send queue, matching, and schema
Module OCMLabs_ProductMessaging, namespace OCMLabs\ProductMessaging, Composer package ocmlabs/module-product-messaging, plus the ocmlabs/module-product-messaging-commerce Adobe Commerce licensing metapackage. Declarative schema only.
Design rule: observers never send
Observers never render templates, send mail, or resolve CMS content. They match SKUs against cached rules and insert queue rows, which are cheap database writes. A cron job does the sending. Any exception in the matching path is logged to var/log/ocmlabs_productmessaging.log (a dedicated Monolog handler) and swallowed, so a misconfigured rule cannot break checkout, invoicing, or shipping, and one unmatchable product or bad rule does not stop the other rules on the same order from queueing.
Order lifecycle observers
| Event | Trigger |
|---|---|
checkout_ | Order Placed (single and multi-address checkout) |
sales_ | Invoice Created |
sales_ | Shipment Created, matched against the shipment’s items only |
Each observer loads active rules for its trigger, applies store view and customer group scope, matches candidate SKUs, and inserts a row with scheduled_at set to now plus the rule’s delay. A duplicate insert for the same rule and order is a no-op.
Matching engine
SKU matching is a framework-independent class: patterns are split on newlines and commas, trimmed, and compared case-insensitively, as exact strings or with * as the only wildcard. A separate extractor resolves candidate SKUs for an order or shipment, including child and parent SKUs when the rule’s match-children flag is on, and skips fully cancelled or refunded lines.
The same matcher and extractor serve both the email observers and the success-page resolver, so there is one matching implementation. Active rules are cached, indexed by trigger event and by success-block participation, and the cache is invalidated on rule save and delete. The matcher sits behind an interface so a condition-tree matcher can be added later without touching the rest of the module.
Send cron
ocmlabs_productmessaging_send (every minute, default group) loads pending rows with scheduled_at <= now, up to the batch size, and processes each in its own try/catch. For each row it:
- Emulates the order’s frontend store and area, so locale, currency, and theme-scoped template resolution match the order.
- Reloads the order and builds the template variables from it plus the row’s matched-item snapshot.
- Attaches the rule’s PDFs and sends through an extended
TransportBuilderthat adds attachment support on top of core mail transport. - On failure, increments attempts and reschedules with
retry_delay * 2^(attempts-1)(capped at 24 hours) untilmax_attempts, then marks the row failed. A missing template fails immediately.
Rows for orders cancelled or refunded while waiting are not sent. ocmlabs_productmessaging_prune (daily, 2:45 AM) deletes old sent, failed, and cancelled rows.
Success-page resolver
On checkout_onepage_success and the multishipping success page, a block resolver loads the session’s last real order (or orders, for multishipping), runs the same matcher against rules with the success-block action on, and renders each matched CMS block through the CMS template filter, in rule ID order. Nothing is queued or stored, and any failure renders nothing.
Database
| Table | Purpose |
|---|---|
ocmlabs_ | Rule definitions: name, status, SKU patterns, match-children flag, the send_ and show_ toggles and their fields (trigger, template ID, delay, copy-to, copy method, success block), store and customer group scope, timestamps. Indexed on status and trigger_. |
ocmlabs_ | PDF attachments per rule: path relative to pub/, original file name, MIME type, size. ON DELETE CASCADE from the rule. |
ocmlabs_ | Queue and log in one table: rule ID and name, order ID and increment ID, store ID, recipient, matched-items JSON snapshot, status, scheduled and sent timestamps, attempts, error. Unique key on (rule_. |
Configuration access
All settings are read through Model\Config, a typed reader over ScopeConfigInterface at store scope that clamps numeric values, so values written with config:set or directly to core_config_data cannot disable retries, remove the batch limit, or wipe the log.
Admin and ACL
Menu entries sit under Sales > Product Messaging. ACL resources are OCMLabs_ProductMessaging::manage (with child ::manage_delete), ::log, and ::config, all under Magento_Sales::sales. The template quick-create additionally requires Magento’s email-template ACL. All admin and storefront strings are in i18n/en_US.csv.