Merlin reads supplier invoices and posts them to your ledger — automatically. It uses an LLM pipeline to extract line items from PDFs, DOCX, XLSX, and CSV files, suggest GL account codes, and apply confidence-based auto-posting rules so that routine invoices never need human review.
| Layer | Package |
|---|---|
| Framework | Laravel 13, PHP 8.4 |
| Frontend | Livewire 4 (single-file classes, no Volt), Flux UI, Alpine.js, Tailwind CSS 3 |
| Auth | Laravel Breeze (Livewire stack) |
| Roles & permissions | spatie/laravel-permission |
| Audit log | spatie/laravel-activitylog |
| File storage | spatie/laravel-medialibrary |
| Settings | spatie/laravel-settings |
| nettsite/nettmail-laravel (transactional + campaigns) | |
| Testing | Pest + PHPUnit 12 |
git clone https://github.com/nettsite/merlin.git
cd merlin
composer run setup # install, .env, key:generate, migrate, npm install + buildCopy .env and fill in:
DB_CONNECTION=mysql # MariaDB preferred
DB_DATABASE=merlin
DB_USERNAME=...
DB_PASSWORD=...
ANTHROPIC_API_KEY=... # Claude API key for invoice extraction
ANTHROPIC_MODEL=claude-sonnet-4-6
ANTHROPIC_MODEL_FAST=claude-haiku-4-5-20251001
ANTHROPIC_MODEL_BACKUP=claude-opus-4-8
ANTHROPIC_ALERT_RECIPIENTS=... # email(s) alerted when a model is retired/unreachable
ANTHROPIC_MODEL_DOWN_TTL=3600 # seconds to circuit-break a down model
EXCHANGERATE_API_KEY=... # exchangerate-api.com — for foreign currency invoicesSeed reference data after migrating:
php artisan db:seedcomposer run dev # starts server + queue + pail + vite concurrently
composer run test # clears config cache, then runs full test suite
vendor/bin/pint --dirty # format changed PHP filesRun a single test:
php artisan test --compact --filter=TestNameBusiness logic lives under app/Modules/, grouped by domain:
app/Modules/
├── Core/ User, Party, Person, Business, Address, ContactAssignment, Document, DocumentLine, LlmLog
├── Accounting/ Account, AccountGroup, AccountType, FinancialYearService
├── Purchasing/ PostingRule + invoice/payment-notification pipeline services
└── Billing/ PaymentTerm, RecurringInvoice + BillingService, RecurringInvoiceService, DueDateCalculator, WorkingDayCalculator
All models use UUID primary keys. Polymorphic relationships are registered in AppServiceProvider via Relation::enforceMorphMap() — always add new morph-mapped models there before writing data.
- A PDF, DOCX, XLSX, or CSV is dropped into the watched folder or uploaded manually
- Magika detects the actual file type; unsupported formats are rejected
- Claude extracts supplier, dates, line items, and amounts — all amounts are stored ex-VAT. Extraction tries
ANTHROPIC_MODEL_FAST(Haiku) first; falls back toANTHROPIC_MODEL(Sonnet) on bad output, thenANTHROPIC_MODEL_BACKUP(Opus) if a model returns a 404 (retired) - Each line gets a suggested GL account code with a confidence score drawn from posting history and the current chart of accounts
- Posting rules evaluate the document; invoices above the confidence threshold are auto-posted
- Every extraction is logged — tokens used, confidence score, warnings, supplier match method
| Score | Treatment |
|---|---|
| ≥ 85 % | Auto-posted |
| 60 – 84 % | Queued for review |
| < 60 % | Queued, flagged as low confidence |
Thresholds are configurable per business via PurchasingSettings.
| Group | Pages |
|---|---|
| Expenses | Suppliers, Purchase Invoices, Payment Notifications, Posting Rules |
| Billing | Clients, Sales Invoices, Quotes, Credit Notes, Recurring Invoices, Payment Terms |
| Accounting | Bank Statements, Bank Templates, Accounts, Account Groups |
| Reports | Income Statement, Trial Balance, Balance Sheet, Income by Client/Account, Expenses by Account, Expenses by Supplier, LLM Performance |
| Settings | General, Purchasing, Billing, Roles, Users, LLM Logs |
| Emails | NettMail templates, contacts, lists, segments, campaigns |
Visual decisions are documented in DESIGN.md. The short version: warm amber accent (#C8772E), Inter font, flat surfaces, no gradients, confidence pills on every AI suggestion, tabular figures everywhere money appears.
- No
$user->hasRole()anywhere — always$user->can('permission-name') QUEUE_CONNECTION=syncin local.env— no worker needed in dev- Migrations have no
down()methods - Auth routes are owned by Breeze — do not register conflicting routes
PurchaseInvoiceUI is always fully custom Livewire — never routed through shared CRUD helpers- Party model uses class table inheritance —
Partyis the parent;PersonandBusinessshare its primary key