Lexies for Craft CMS

Usage

What happens when an order completes

  1. Commerce completes the order and fires its event. Lexies pushes a queue job and returns — checkout never waits for Lexware.
  2. The job checks the trigger rules and whether this order already has an invoice.
  3. If contacts are on (Pro), the customer is found or created in Lexware.
  4. The tax treatment is resolved, with its reasoning.
  5. The payload is built and proved against the order total.
  6. It is sent — as a draft, or finalised if you asked for that.
  7. Lexies reads the voucher back for its number, status and PDF.

Every step is on the document row afterwards, under Lexies → Documents.

The reconciliation guard

Before anything is sent, Lexies builds the payload and then computes it the way Lexware will — leading unit price times quantity rounded to the cent per line, tax recomputed per rate from those line totals — and compares the result to what Commerce charged, in integer cents.

A cent or two of rounding is absorbed into the largest line's unit price, where Lexware's four decimals have room for it, and the whole thing is recomputed to confirm. Anything larger is blocked: the document is not sent, and it carries both figures so you can see the gap.

Things that block on purpose:

  • an order in a currency other than EUR
  • a line item carrying two different tax rates — Lexware allows one per line
  • an intra-community or third-country invoice with no Lexware contact to reference
  • totals that will not add up

A blocked document is a finished job, not a failed one. It sits in the Documents screen with its reason until you fix the cause and press Send again.

Statuses

StatusMeans
PendingBuilt, not yet sent
Draft in LexwareSent, awaiting finalisation
OpenFinalised, unpaid
Paid / Paid offSettled in Lexware
BlockedRefused before sending, because sending it would have been wrong
Needs checkingSent, and we do not know whether it landed
FailedLexware rejected it. Safe to fix and retry

"Needs checking" and why nothing is retried

Lexware has no idempotency key, and their own documentation says of a gateway timeout: "It is possible that your request has been processed successfully." A duplicate invoice in German bookkeeping is not an inconvenience — it is a correction filing.

So when a create times out or fails with a 5xx, Lexies stops. It does not try again. The document is parked as Needs checking, and either you press Check with Lexware or the console does the same thing:

php craft lexies/sync/reconcile

That searches Lexware's voucher list for a matching voucher created in the window — right type, right amount, right contact, not already claimed by another order — and either adopts it or confirms nothing was created, at which point the document is safe to send again.

Finalising

A draft can be finalised from the document screen. That allocates the consecutive voucher number and is the point of no return: from then on the invoice can only be voided, never edited or deleted. Lexies will not finalise a draft whose original payload it no longer holds — do that one in Lexware.

Refunds and credit notes (Pro)

With Credit note on refund on, capturing a refund in Commerce raises a Lexware credit note.

  • A full refund mirrors the invoice line for line, which is what a merchant expects to see on the printed Gutschrift.
  • A partial refund cannot be attributed to particular products without inventing facts, so it goes out as one line per tax rate, split in the same proportion the order's own tax sits in. That is the only split that keeps the VAT return correct.

Each credit note is keyed on the Commerce refund transaction, so two refunds are two credit notes and the same refund replayed is one. Where the invoice is already finalised, the credit note is raised against it, and Lexware settles the invoice off automatically.

Templates

{% set invoiceUrl = craft.lexies.invoiceUrl(order) %}
{% if invoiceUrl %}
    <a href="{{ invoiceUrl }}">Rechnung herunterladen</a>
{% endif %}

invoiceUrl() returns null unless customer downloads are on and there is a finalised invoice with a PDF, so {% if %} is enough — it never hands you a link that 404s.

{{ craft.lexies.isInvoiced(order) }}      {# bool #}
{{ craft.lexies.invoiceFor(order) }}      {# the Document, or null #}
{{ craft.lexies.documentsFor(order) }}    {# invoice plus any credit notes #}

A Document exposes voucherNumber, voucherStatus, totalNet, totalTax, totalGross, openAmount, isPaid(), isDraft(), canDownload() and getDeeplink().

Console

php craft lexies/sync/order 1234            # invoice one order
php craft lexies/sync/order 1234 --dry-run  # build it and print the JSON, send nothing
php craft lexies/sync/backfill --limit=200  # invoice completed orders that have none
php craft lexies/sync/reconcile             # resolve documents whose fate is unknown
php craft lexies/sync/refresh --limit=100   # re-read sent documents from Lexware

php craft lexies/payments/poll              # check open invoices for payment

php craft lexies/webhooks/list              # what Lexware currently sends us
php craft lexies/webhooks/sync              # subscribe, and drop anything unused
php craft lexies/webhooks/remove            # unsubscribe this site entirely
php craft lexies/webhooks/replay            # handle callbacks that were never processed

php craft lexies/log/prune --days=30        # housekeeping, for cron

--dry-run is worth using on a real order before you trust the mapping. It prints the tax reasoning line by line, both totals, and the payload:

Order 1042
  · The store charges tax-inclusive prices, so unit prices are sent gross.
  · AT is in the EU and the customer supplied VAT ID ATU12345678, so the supply is
    an intra-community one.
  tax type: intraCommunitySupply
  order total: 238   invoice total: 238

Backfilling an existing shop

lexies/sync/backfill walks completed orders that have no document yet, oldest first. Run it with --dry-run first, and in batches — Lexware's two-requests-a-second limit means a few thousand orders is a background job, not a coffee break.

Orders that block are reported and skipped, so a handful of odd historical orders will not stop the run.