charlielangridge / lunar-xero
Xero integration for Lunar with Filament admin tooling, OAuth connection management, and queued customer, invoice, and payment sync.
Requires
- php: ^8.4
- filament/filament: ^4.0
- guzzlehttp/guzzle: ^7.9
- illuminate/database: ^11.0||^12.0||^13.0
- illuminate/http: ^11.0||^12.0||^13.0
- illuminate/routing: ^11.0||^12.0||^13.0
- illuminate/support: ^11.0||^12.0||^13.0
- lunarphp/lunar: ^1.0
- spatie/laravel-package-tools: ^1.16
- xeroapi/xero-php-oauth2: ^7.4
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.8
- orchestra/testbench: ^9.0||^10.0||^11.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-arch: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- spatie/laravel-ray: ^1.35
README
lunar-xero connects a Lunar store to Xero. It adds a Xero settings page to the Lunar admin, stores the OAuth connection locally, and syncs contacts, invoices, captured payments, and refunds through queued jobs.
The package is built around Lunar's existing models and events, so it fits into a normal Lunar install instead of replacing the order flow.
What it does
- Connects to Xero with OAuth 2.0 and PKCE
- Stores and manages the active Xero tenant
- Adds a Xero settings page in the Lunar admin
- Adds Xero account code and item code fields to products and variants
- Lets admins link or unlink customers to existing Xero contacts
- Lets admins opt individual customers into including Lunar order line notes on Xero invoice lines
- Syncs orders to Xero invoices
- Can email authorised Xero invoices to customers while avoiding duplicate sends
- Syncs captured payments to Xero payments
- Syncs refunds to Xero credit notes and payout allocations
- Logs each sync attempt so failures and skips are inspectable
- Falls back to billing or shipping address data when it needs to create a contact for a guest order
Requirements
- PHP 8.4+
- Laravel 11, 12 or 13
- Filament 4
- Lunar 1.x
- A running queue worker
- A Xero app with OAuth enabled
Installation
Install the package:
composer require charlielangridge/lunar-xero
Publish the config file if you want to override the defaults:
php artisan vendor:publish --tag=lunarpanel-xero-config
Publish the views only if you need to customise them:
php artisan vendor:publish --tag=lunarpanel-xero-views
Run your migrations:
php artisan migrate
The service provider is auto-discovered. In a standard Lunar panel setup the plugin page is registered automatically as well.
Configuration
The package config lives at config/lunarpanel-xero.php.
The main settings are:
defaults.invoice_status: default invoice status sent to Xero, eitherDRAFTorAUTHORISEDdefaults.sync_queue: queue name used for contact, invoice, and payment jobsoauth.read_only: blocks write calls to Xero when enabledevents.order_created: event class used to dispatch invoice syncsevents.payment_completed: event class used to dispatch payment syncsmodels.*: model classes the package should usetables.*: table names for the package tables and patched Lunar tablesroutes.prefixandroutes.middleware: override the OAuth route group when neededcharity.*: configures where charity VAT relief metadata is read from on the order for Xero invoice traceability lines
Environment
Add these values to .env:
XERO_CLIENT_ID= XERO_CLIENT_SECRET= XERO_REDIRECT_URI= XERO_READ_ONLY=false LUNARPANEL_XERO_ROUTE_PREFIX=
If XERO_REDIRECT_URI is left empty, the package falls back to the generated lunarpanel-xero.callback route.
Routes
The package registers these routes:
GET /connectGET /callbackPOST /disconnectPOST /tenants/refreshPOST /tenants/select
By default they sit under the Lunar panel path with /xero appended. If the panel path cannot be resolved, the fallback prefix is lunar/xero.
Admin areas
The Xero settings page lets you:
- start or disconnect the OAuth connection
- refresh tenants and choose the active tenant
- choose whether invoices are created as draft or authorised
- set a default revenue account code
- map Lunar payment types to Xero account codes
The package also extends existing Lunar admin screens:
- products get a dedicated Xero page
- variants get a dedicated Xero page
- customers can be linked to or unlinked from a Xero contact, and can opt into Xero invoice line notes
- orders show the current invoice sync state, Xero invoice ID, last error, and a manual sync action
Sync behaviour
Contacts
Customer sync is observer-driven.
- New customers queue a contact sync job
- Unlinked customers queue another sync when they are updated
- Existing
xero_contact_idvalues are respected - Matching Xero contacts can be found by email before a new contact is created
- Guest orders can create a contact from billing details, with shipping used as a fallback
Invoices
Invoice sync runs from the configured order-created event and can also be queued manually from the order page.
During invoice sync the package:
- creates a sync log entry
- resolves or creates a Xero contact
- builds invoice lines from the order
- resolves account codes from variant, product, then package default settings
- resolves or creates Xero item codes where possible
- creates or updates the Xero invoice
- stores the returned Xero invoice ID, invoice number, invoice status, and customer online invoice URL on the order
- backfills payments and refunds where appropriate
For account or pay-later order flows, the package can also sync and email an invoice in one queued operation. That flow always pushes the latest Lunar order details to Xero first, creates or updates the invoice as AUTHORISED, fetches the invoice from Xero, and only calls Xero's email endpoint when sent_to_contact is still false. If the invoice has already been sent from Xero or by a previous package run, the email step is skipped and logged as successful.
Synced orders can contain these Xero invoice fields:
xero_invoice_id: the Xero invoice UUIDxero_invoice_number: the human-readable Xero invoice number, such asINV-1234xero_invoice_status: the latest status returned by Xero, such asDRAFT,AUTHORISED, orPAIDxero_online_invoice_url: Xero's customer-safe online invoice URL, fetched only for customer-visible statuses
Invoice references are composed from the order reference, purchase order reference, and customer reference, joined with -. Blank values and duplicate values are skipped, so an order with reference of GM-W-ABCDE and customer_reference of PO-1001 becomes GM-W-ABCDE - PO-1001. If the order stores a purchase order in meta.purchase_order, that value is used between the order reference and customer reference, such as GM-W-ABCDE - PO-1001 - Customer Ref.
The zero-value purchase-order line uses meta.purchase_order when present, falling back to customer_reference for stores that only use Lunar's built-in customer reference field.
Customers can be opted into order line notes from the Lunar customer edit page with the Include order line notes on Xero invoices toggle. The underlying xero_include_order_line_notes customer field defaults to false. When it is enabled and an order line has non-blank notes, the package appends the notes underneath the existing Xero invoice line description:
Existing description
Notes: Order line notes
If the order contains charity VAT relief metadata, the package also appends zero-value traceability lines for the charity name, charity number, declaration name, and declared-at timestamp. By default these values are read from order.meta.charity_vat_relief.*, matching ganda-webstore, and the taxable product lines still keep their VAT mapping from Lunar tax data.
You can override those defaults in config/lunarpanel-xero.php:
'charity' => [ 'enabled' => true, 'name_path' => 'meta.charity_vat_relief.charity_name', 'number_path' => 'meta.charity_vat_relief.charity_number', 'declaration_name_path' => 'meta.charity_vat_relief.declaration_name', 'declared_at_path' => 'meta.charity_vat_relief.declared_at', ],
Customer invoice links
For storefront customer order pages, use the stored xero_online_invoice_url after your app has already checked that the signed-in customer owns the order. Do not expose the internal go.xero.com invoice URL used by the Lunar admin panel; that link is intended for staff with Xero access.
Draft invoices are not customer-visible by default. The helper only returns a URL for AUTHORISED and PAID invoices:
use CharlieLangridge\LunarXero\Support\XeroUrlFactory; $invoiceUrl = XeroUrlFactory::customerInvoiceUrl( $order->xero_online_invoice_url, $order->xero_invoice_status, );
A ganda-webstore style order show view can then render the link conditionally:
@php $xeroInvoiceUrl = \CharlieLangridge\LunarXero\Support\XeroUrlFactory::customerInvoiceUrl( $order->xero_online_invoice_url, $order->xero_invoice_status, ); @endphp @if ($xeroInvoiceUrl) <a href="{{ $xeroInvoiceUrl }}" target="_blank" rel="noopener"> View Xero invoice </a> @endif
For an Inertia and Vue storefront, resolve the customer-safe URL in your Laravel controller or page response and pass only that final value to the component:
use CharlieLangridge\LunarXero\Support\XeroUrlFactory; use Inertia\Inertia; return Inertia::render('Account/Orders/Show', [ 'order' => [ 'id' => $order->id, 'reference' => $order->reference, 'xeroInvoiceUrl' => XeroUrlFactory::customerInvoiceUrl( $order->xero_online_invoice_url, $order->xero_invoice_status, ), ], ]);
Then render it in the Vue page only when the prop is present:
<script setup> defineProps({ order: { type: Object, required: true, }, }) </script> <template> <a v-if="order.xeroInvoiceUrl" :href="order.xeroInvoiceUrl" target="_blank" rel="noopener" > View Xero invoice </a> </template>
Payments and refunds
Payment sync runs from the configured payment event and from the transaction observer.
- Captured payments are posted against the matching Xero invoice
- Refund transactions are turned into Xero credit notes
- Refund credit notes are allocated back to the invoice and can be paid out through the mapped account
- Duplicate payments and refunds are checked before new records are created
Queueing
All sync jobs implement ShouldQueue and default to the xero queue unless you override defaults.sync_queue.
Example worker command:
php artisan queue:work --queue=xero,default
Logging
Each sync attempt writes to xero_sync_logs, including:
- operation type, such as
invoice,invoice_email,payment, orcredit_note - resource type and ID
- payload snapshot
- external reference
- status
- response payload
- error message and exception class when something fails
That table is the first place to look when a sync has been skipped or has failed.
Development
Run the test suite:
composer test
Run static analysis:
composer analyse
Run formatting:
composer format
License
MIT. See LICENSE.md.