A Core dna store is a set of records and an API that moves a purchase from cart to order: products described in every language and priced in every currency the store sells in, a cart, a checkout that becomes an order, invoices, payments, tax and shipping. The site builds the storefront on top: the cart and checkout pages, their scripts, and the customer emails.
L2–L3
Unit 05 · From payment to fulfilment
Customer emails
From payment to fulfilmentPage 22 of 31Updated
Which job sends each commerce email (the receipt, the shipping notice and the refund notice), the record and event that start it, what its template reads, and the currency and language each email is written in.
Who this is for. Developers setting up a store's customer emails. After this page you know which record and event each email hangs on, who it goes to and what its template reads. How a job sends email, writing its template, attachments, deliverability and safe testing are Guide 9's subject: How a job's email is sent.
Every commerce email is a job
A commerce email is a job (Orchestration, formerly Hooks) with a Send Email action, started by an entity event on the record that a payment or a shipment changes: the order, the refund or the fulfilment. The action has the recipient, copies, the sender, a reply-to address, the subject and a template for the body.
A payment shows up in those records. A capture marks the invoice paid, and the order's financial status becomes paid; a refund that goes through has its status become success; a fulfilment's status follows the parcel from pending to delivered. Each email listens for one of those changes.
What starts each email
Email
Entity event on
Methods
Pre-Event property
Sends when
Order receipt
Orders (the order)
Created and Updated
financialStatus
it became paid
Shipping notice
Fulfillment
Updated
status
it became in_transit
Delivery notice
Fulfillment
Updated
status
it became delivered
Refund notice
Refund, from the order (Orders refund) or from an invoice (Invoices refund)
Created and Updated
status
it became success
Each condition compares the property before and after the change, with the guard from Guarding against recursion, so a later save of the same record does not send the email a second time. What an update can see of the earlier values is on Entity events. The order receipt is worked through in full, job and template, on Three worked emails; the notices below follow it with another record and condition.
The recipient
Address each email to the order's person by id, as the worked receipt does, so the mailer's placeholders such as %firstName% are filled from that person:
order receipt: {$event->getRecord()->getPerson()->getId()};
shipping and delivery notices, and a refund from the order: {$event->getRecord()->getOrder()->getPerson()->getId()}.
The shipping notice
In a job's template the record is $event->getRecord() (event.getRecord() in Twig), here the fulfilment. It lists the lines leaving in this fulfilment and a tracking link for each shipment that has one:
{$fulfillment = $event->getRecord()}
{$order = $fulfillment->getOrder()}
<p>Hi %firstName%,</p>
<p>Your order {$order->getNumber()|escape} is on its way.</p>
<table>
{foreach $fulfillment->getFulfillmentItems() as $fulfillmentItem}
<tr><td>{$fulfillmentItem->getItem()->getName()|escape} × {$fulfillmentItem->getQuantity()}</td></tr>
{/foreach}
</table>
{foreach $fulfillment->getShipments() as $shipment}
{if $shipment->getTrackingUrl()}
<p><a href="{$shipment->getTrackingUrl()|escape}">Track parcel {$shipment->getTrackingCode()|escape}</a></p>
{/if}
{/foreach}
{% set fulfillment = event.getRecord() %}
{% set order = fulfillment.getOrder() %}
<p>Hi %firstName%,</p>
<p>Your order {{ order.getNumber()|e }} is on its way.</p>
<table>
{% for fulfillmentItem in fulfillment.getFulfillmentItems() %}
<tr><td>{{ fulfillmentItem.getItem().getName()|e }} × {{ fulfillmentItem.getQuantity() }}</td></tr>
{% endfor %}
</table>
{% for shipment in fulfillment.getShipments() %}
{% if shipment.getTrackingUrl() %}
<p><a href="{{ shipment.getTrackingUrl()|e }}">Track parcel {{ shipment.getTrackingCode()|e }}</a></p>
{% endif %}
{% endfor %}
The delivery notice is the same job with the condition "became delivered" and its own wording. The statuses, and what the order's summary status is at each stage, are on Stock, fulfilment and the member's order screens.
The refund notice
For a refund made from the order, the record is the refund; its order gives the number and the total refunded so far, which matters when an order is refunded in more than one go:
{$refund = $event->getRecord()}
{$order = $refund->getOrder()}
<p>Hi %firstName%,</p>
<p>We have refunded {$refund->getTotalAsMoney()->format()} for order {$order->getNumber()|escape}.</p>
<p>Refunded on this order so far: {$order->getRefundedTotalAsMoney()->format()}</p>
{% set refund = event.getRecord() %}
{% set order = refund.getOrder() %}
<p>Hi %firstName%,</p>
<p>We have refunded {{ refund.getTotalAsMoney().format() }} for order {{ order.getNumber()|e }}.</p>
<p>Refunded on this order so far: {{ order.getRefundedTotalAsMoney().format() }}</p>
The order, its invoices, its refunds and its lines are all in the currency the purchase was made in, and the money getters' format() writes each amount in that currency. Print every amount that way and never put a currency symbol in front by hand: on a multi-currency store the same template sends receipts in several currencies.
The language of an email
An email is in the language its template and subject are written in. The job runs on the record after the purchase, so the language is the template's choice:
A single-language store writes its templates in that language.
A multilingual store records the shopper's language on the checkout, in a metadata field the checkout page fills in (Metadata fields); the value moves to the order with the other metadata at the conversion. The template then writes its wording in that language.
The lines' stored names are in the client's default language (The order's currency and the language of its lines). For product names in the shopper's language, read each line's product description in that language, with the query helper, which a job's template receives as query, and keep the stored name as the fallback, as the member's order screen does.