Sager for Craft CMS

Configuration

How Sager stores its configuration

Sager keeps two kinds of configuration, in two different places. Most of what follows comes back to this.

WhatWhere it livesChanged on production?
Settings: credentials (as env var references), triggers, references, retries, retentionProject configNo. Change them on dev and deploy
Mapping: the Sage ledger accounts, tax rates, bank account and so onProject configNo. Change them on dev and deploy
Connection: OAuth tokens, the chosen Sage businessDatabase, encryptedYes, on every environment
Sage lists: the cached chart of accounts, tax rates, etc.Craft's cacheYes

So on production, where allowAdminChanges is off, admins can still connect to Sage, choose the business, reload the Sage lists and clear the log. The Settings and Mapping screens still appear, read-only, with a note saying so. The Sage connection pane on the settings screen keeps working.

Creating the Sage app

Sager connects through OAuth 2.0, using an app you register with Sage.

  1. Sign in to Sage's developer portal at developer.sage.com and create an app for Sage Business Cloud Accounting.
  2. In Craft, open Sager → Settings and copy the Redirect URI. It's Craft's action URL for sager/connect/callback, so it looks like https://example.com/actions/sager/connect/callback or https://example.com/index.php?p=actions/sager/connect/callback, depending on your omitScriptNameInUrls setting.
  3. Register that exact URL as the app's callback URL. Scheme, host, path and trailing characters all have to match. Add one callback URL per environment you'll connect from.
  4. Copy the app's client ID and client secret.

Credentials

Put both values in .env:

# .env
SAGE_CLIENT_ID="…"
SAGE_CLIENT_SECRET="…"

Then type $SAGE_CLIENT_ID and $SAGE_CLIENT_SECRET into the Client ID and Client secret fields. They autosuggest.

The client secret must be an environment variable reference. Settings are written to project config, and project config is committed. Sager refuses to save a literal secret, so it never lands in config/project/*.yaml or your git history. The client ID isn't a secret, and can be either a literal or an environment variable.

Connecting

  1. Save the credentials.
  2. Press Connect to Sage. You're sent to Sage to sign in and approve the app.
  3. Sage sends you back to the redirect URI, and Sager stores the tokens, encrypted with Craft's security key.
  4. If your Sage login reaches one business, Sager selects it. If it reaches more, choose one from Sage business and press Use this business.
  5. You land on Sager → Mapping.

The Country setting is optional. A two-letter code such as GB or US sends you straight to the right Sage region instead of Sage's region picker.

Connecting again, after a disconnect or an expiry, forgets the business that was chosen before. Pick it again afterwards.

Disconnecting

Disconnect throws away the stored tokens and the chosen business on this environment. It doesn't revoke the app in Sage. Orders stop being sent until you connect again. Orders completed while disconnected aren't queued, so send them afterwards with Queue everything not sent.

Choosing a different business

Changing the business clears the cached Sage lists, because ledger accounts and tax rates belong to a business. It doesn't clear the mapping. The GUIDs saved there belong to the old business, and Sage rejects them. Redo the mapping on your development site, against the new business, and deploy it.

Settings

Sager → Settings, or Settings → Plugins → Sager. Admins only. Sections are listed as they appear on the screen.

Sage connection

SettingConfig keyDefaultWhat it does
Client IDclientIdblankYour Sage app's client ID. A literal or an environment variable
Client secretclientSecretblankYour Sage app's client secret. Must be an environment variable reference, such as $SAGE_CLIENT_SECRET
Redirect URI——Read-only. The URL to register with your Sage app
Redirect URI overrideredirectUriOverrideblankOnly for when Craft can't work out its own public URL, such as behind some proxies. Accepts an environment variable
CountrycountryCodeblankOptional two-letter code for the Sage region, such as GB or US
Status——Connected, expired or not connected, with Connect, Reconnect or Disconnect
Sage business——Which business this store posts to. Stored in the database, not project config

When to sync

SettingConfig keyDefaultWhat it does
Sync automaticallyautoSynconTurn off to send nothing on its own. Orders, payments and refunds are then only sent from the CP or the console
TriggersyncTriggerWhen the order is completedcomplete, paid, status or manual. See Triggers
StatusessyncStatusHandlesnoneOrder status handles, used when the trigger is status
Send paymentssyncPaymentsonCaptured payments become Sage contact payments allocated against the invoice
Send refundssyncRefundsonRefunds become Sage sales credit notes
Release invoicesreleaseInvoicesoffAsks Sage to take each invoice out of draft as soon as it's created
Skip zero-total ordersskipZeroTotalOrdersonFree and fully comped orders aren't sent

Documents

SettingConfig keyDefaultWhat it does
Invoice referenceinvoiceReferenceFormat{reference}The Sage invoice reference. {number}, {shortNumber}, {reference} and {id} are replaced. Up to 60 characters. An order with no reference uses its short number
Contact reference prefixcontactReferencePrefixWEBStamped on contacts Sager creates, such as WEB-12, and used to find them again. Up to 20 characters
Copy the customer notecopyCustomerNoteonPuts the order's customer note in the invoice's notes
Rounding toleranceroundingToleranceMinor5In minor units: cents, pence. See Usage

Retries and logging

SettingConfig keyDefaultWhat it does
Maximum attemptsmaxAttempts5How many tries an order gets before it's marked failed, 1–50
First retry delayretryDelaySeconds60Seconds before the first retry. Doubles each attempt, up to an hour, unless Sage sends its own Retry-After
Log retentionlogRetentionDays30Days of connection log to keep. 0 keeps everything

Retention is enforced during Craft's garbage collection, so you don't need a cron job for it. php craft sager/log/prune runs the same pruning on demand.

Triggers

TriggerAn order is queued when
When the order is completedthe customer completes checkout
When the order is paidCommerce marks the order paid. A completed order that isn't paid yet is skipped
When the order reaches a statusthe order moves into one of the ticked Statuses
Never — manual onlynever. Send orders from the CP or the console

Payments and refunds follow on their own whatever the trigger, as long as Sync automatically is on. A payment captured before the invoice exists is sent with the invoice.

Mapping

Sager → Mapping. Admins only. Every field is a dropdown built from your own Sage business, so you never type a GUID. The screen needs a live connection to read those lists.

Sales

FieldConfig keyWhat it's for
Sales ledger accountsalesLedgerAccountIdWhere product revenue lands. Required. Nothing is sent without it
Default tax ratedefaultTaxRateIdThe tax rate for any line with nothing more specific. Every Sage line needs one
Zero-rated tax ratezeroTaxRateIdUsed for lines Commerce didn't tax, and for discount and rounding lines. Without it, those lines use the default rate, and Sage may add tax to a line that never had any

Shipping, discounts and rounding

FieldConfig keyWhat it's for
Shipping tax rateshippingTaxRateIdThe tax rate on the invoice's shipping. Falls back to the default rate
Discount ledger accountdiscountLedgerAccountIdWhere the order-level discount line posts. Falls back to the sales ledger account
Rounding ledger accountroundingLedgerAccountIdWhere a rounding line posts. Falls back to the sales ledger account

Payments

FieldConfig keyWhat it's for
Bank accountbankAccountIdWhere received money lands. Required when Send payments is on
Payment methodpaymentMethodIdOptional. The Sage payment method on each payment
Receipt transaction typereceiptTransactionTypeIdUsually Customer Receipt. Leave it unmapped and Sager picks the receipt type from your Sage business

Tax categories

One dropdown per Commerce tax category (config key taxCategoryMap, category ID → Sage tax rate ID). A line uses the rate mapped to its tax category. An unmapped category uses the zero-rated rate if Commerce charged no tax on the line, and the default rate otherwise.

Map these if you sell at more than one rate, for example standard-rated goods and zero-rated books.

Product types

Optional. One dropdown per Commerce product type (config key productTypeMap, product type ID → Sage ledger account ID). A line for a product of that type posts to that ledger account instead of the sales ledger account. Useful when goods and services are reported separately.

Sage lists

The dropdowns are cached for an hour. After changing your chart of accounts or tax rates in Sage, press Reload from Sage, or run php craft sager/connect/refresh. Both work on production.

The ledger account dropdowns show the accounts Sage makes available for sales.

Development and production

The mapping is project config, and its values are GUIDs from one specific Sage business. Your development site and your production site therefore have to connect to the same Sage business. If they're connected to different businesses, such as a Sage trial business on dev and the real one on production, every GUID in the deployed mapping is wrong on production, and Sage rejects every invoice.

Each environment makes its own connection. Tokens are database state and never deploy. A sensible setup:

  1. On dev, connect to the production Sage business and build the mapping. Reading the lists changes nothing in Sage.
  2. Keep dev from posting test orders into your real books: turn Sync automatically off for dev in config/sager.php (below), or press Disconnect once the mapping is saved.
  3. Deploy. On production, press Connect to Sage, approve, and choose the same business.
  4. Check Sager → Overview on production.

If you really do need a different business on another environment, override the mapping fields for that environment in config/sager.php with environment variables. Otherwise, redo the mapping.

Copying a database between environments

Sage refresh tokens are replaced every time they're used. If you copy the production database to dev and both environments then use the same stored connection, whichever refreshes second is refused, and has to be reconnected by hand. That can be production.

After pulling a production database down, press Disconnect on dev straight away, before its queue runs, then reconnect dev if you need it.

The config file

Any setting can be fixed in config/sager.php, using the config keys above. Values in the file override whatever is saved in project config. That's Craft's standard behaviour for plugin settings. A field set this way still shows on the settings screen, but saving the screen doesn't change it.

<?php

use craft\helpers\App;

return [
    'clientId' => '$SAGE_CLIENT_ID',
    'clientSecret' => '$SAGE_CLIENT_SECRET',

    // Off on dev, so test orders don't land in the real books.
    'autoSync' => App::env('CRAFT_ENVIRONMENT') === 'production',

    'invoiceReferenceFormat' => 'WEB-{reference}',
    'logRetentionDays' => 14,
];

Permissions

PermissionWhat it allows
View Sage sync statusSager → Overview, Sager → Orders, each order's Sager page, and the Sage panel on Commerce's order screen
↳ Send orders to SageSend, Re-check, Send to Sage now, Queue everything not sent and Preview payload
View the connection logSager → Log and its entries, read-only

Every Sager screen also requires Commerce's Manage orders permission, because the order screens, previews and log all show customer names, addresses and order totals.

Admin-only, whatever the permissions:

  • Sager → Settings and Sager → Mapping. Saving either also needs allowAdminChanges.
  • Connecting, disconnecting and choosing the business. These work wherever admin changes are off.
  • Clear the log.