Templating
craft.sager tells a template whether an order has reached your books, and under what invoice
number. It's handy on a customer's order page, in a Commerce email, or in your own CP templates.
Methods
| Method | Returns |
|---|---|
craft.sager.isConnected() | true while Sager has a live connection to Sage |
craft.sager.businessName() | The name of the connected Sage business, or null |
craft.sager.status(order) | pending, synced, failed or skipped, or null if Sager has never considered the order |
craft.sager.isSynced(order) | true once the order has a Sage invoice |
craft.sager.invoiceId(order) | The Sage invoice's ID (a GUID), or null |
craft.sager.invoiceNumber(order) | The Sage invoice number, such as SI-1042, or null |
craft.sager.documents(order) | Every Sage document Sager created for the order, oldest first. An empty array if there are none |
Every method that takes order accepts an order or an order ID.
isSynced() and status() can disagree for a moment. isSynced() is about whether an invoice
exists. status() is about the order's last sync, so an order with an invoice can briefly be
pending again while a later payment is queued.
The document object
documents() returns plain arrays, one per document:
| Key | What it is |
|---|---|
documentType | invoice, payment or credit_note |
sageId | The document's ID in Sage |
sageNumber | Sage's number for it, such as the invoice or credit note number. Payments have none |
amount | The amount, as a decimal string |
currency | The currency code |
transactionId | The Commerce transaction behind a payment or credit note, or null for an invoice |
dateCreated | When Sager created it, as a UTC date string from the database |
The payload Sager sent isn't included. It holds the customer's addresses, and a front-end template never needs it. The full request is in Sager → Log for anyone allowed to see it.
Load the order safely
None of these methods checks who's asking. Pass an order your template has already loaded and
verified, never an ID taken straight from the query string. With
craft.sager.invoiceNumber(craft.app.request.getParam('id')), anyone could read invoice numbers by
counting upwards.
Look the order up by its number, the long unguessable hash Commerce puts in order links, and
check it belongs to the logged-in customer:
{% set number = craft.app.request.getQueryParam('number') %}
{% set order = number ? craft.orders()
.number(number)
.isCompleted(true)
.one() : null %}
{% if not order or (currentUser and order.customerId != currentUser.id) %}
{% exit 404 %}
{% endif %}
An invoice line on the order page
{% if craft.sager.isSynced(order) %}
{% set invoiceNumber = craft.sager.invoiceNumber(order) %}
<p>
Invoiced{% if invoiceNumber %} as <strong>{{ invoiceNumber }}</strong>{% endif %}.
</p>
{% endif %}
Credit notes for refunds
{% set credits = craft.sager.documents(order)|filter(d => d.documentType == 'credit_note') %}
{% if credits is not empty %}
<h3>Credit notes</h3>
<ul>
{% for credit in credits %}
<li>
{{ credit.sageNumber ?: 'Credit note' }}:
{{ credit.amount|commerceCurrency(credit.currency ?: order.currency) }}
</li>
{% endfor %}
</ul>
{% endif %}
In an email
Commerce emails are rendered with the order, so the same calls work in an email template. Bear in mind that the order-completed email is sent in the same request that completes the order, while Sager sends the invoice from the queue afterwards. The invoice number won't exist yet in that email. It will in a later status email, such as shipped.
{% set invoiceNumber = craft.sager.invoiceNumber(order) %}
{% if invoiceNumber %}
Your invoice number is {{ invoiceNumber }}.
{% endif %}
A warning in your own CP template
{% if not craft.sager.isConnected() %}
<p class="error">Sager isn't connected to Sage. Orders aren't reaching the books.</p>
{% elseif craft.sager.status(order) == 'failed' %}
<p class="error">
This order didn't reach {{ craft.sager.businessName() ?? 'Sage' }}.
<a href="{{ cpUrl('sager/orders/' ~ order.id) }}">See why</a>.
</p>
{% endif %}