tomatophp/circle-invoices-module
Invoices module for TomatoPHP's Circle, adding invoice creation, line items, taxes, and status tracking to your Laravel app. Generate and manage billing records, attach them to Circle customers or subscriptions, and keep payment history organized in one place.
v1.0.0 is the first tagged release of tomatophp/circle-invoices-module, a module in the TomatoPHP Circle ecosystem. Treat it as the invoice domain layer (models, migrations, Filament resources, policies) rather than a full billing engine — tax calculation, payment capture, and PDF branding are yours to supply.
tomatophp/circle-core (and usually Filament) to already be installed and bootstrapped. Installing it into a plain Laravel app will leave you with orphaned resources and unresolved panel registration.composer require tomatophp/circle-invoices-module:1.0.0
Pin the exact version — there's no upgrade guide yet, and 1.0.x patches may still touch schema.php artisan vendor:publish --tag="circle-invoices-module-config"
php artisan vendor:publish --tag="circle-invoices-module-migrations"
php artisan migrate
src/Providers/*ServiceProvider.php (what it registers, what's bindable in the container), the database/migrations/ folder (the real contract — invoice header vs. line-item tables, money column types, status column), then the Filament resource under src/Filament/.php artisan tinker before touching the UI — it surfaces the model's guarded fields, casts, and required relations fastest.Write through a service, not the model. The module owns persistence; your app should own the workflow. Wrap invoice creation in an action class so numbering, totals, and status transitions happen in one transaction:
DB::transaction(function () use ($order) {
$invoice = app(CreateInvoice::class)->for($order->customer, $order->currency);
foreach ($order->lines as $line) {
$invoice->items()->create([...]);
}
$invoice->recalculateTotals();
});
Drive it from domain events. The cleanest day-to-day integration is a listener on your own OrderPlaced / SubscriptionRenewed event that creates a draft invoice. Keep the module's models out of your controllers entirely; that way a future major version changing the API only breaks one listener.
Status is a state machine, not a boolean. Map out draft → sent → paid → void (whatever the migration's enum/status column actually allows) up front and enforce it in one place — an observer or a dedicated InvoiceStatus enum with canTransitionTo(). Do not let Filament actions and your API both write status directly, or you'll end up with paid invoices that still have a sent_at of null.
Numbering. Invoice numbers must be gapless-looking and unique under concurrency. Put generation behind a single service or model observer, back it with a DB unique index, and catch the duplicate-key exception with a retry loop. Never generate from count() + 1.
Extend the Filament resource, don't edit it. Register your own resource that extends the module's and override table(), form(), and getNavigationGroup(). Vendor edits vanish on composer update.
Queue the expensive bits. PDF rendering and mail delivery belong in jobs dispatched from an observer on the "invoice issued" transition, so the HTTP request that creates an invoice stays fast and idempotent.
Multi-tenant scoping. If your Circle app uses teams/companies, add a global scope on the invoice model early — retrofitting tenancy after you have invoices in the wild is painful.
dev-main, diff your published migrations against the tagged ones before upgrading — published migrations are copied, so composer update will not bring schema changes to your app.bootstrap/providers.php, the Circle core provider must be registered before this module, otherwise panel/resource registration silently no-ops. Symptom: the module is installed, migrations ran, but no menu item appears.php artisan config:clear. A cached config during local development is the single most common "my setting is ignored" report.decimal vs. bigInteger minor units before writing a single line of code. If they're decimals, cast to a money value object immediately and never do arithmetic on the raw attribute; if integers, be explicit about the exponent in every formatter.total. Recompute from line items in a saving hook and overwrite whatever came in.number combined with soft-deleted invoices means a voided-then-deleted invoice permanently burns its number, or worse, blocks reissue. Either exclude deleted_at rows via a partial index or use a separate sequence table.php artisan about (providers loaded), php artisan route:list | grep -i invoice, php artisan event:list | grep -i invoice, php artisan migrate:status | grep invoice. Between those four you can localise almost any "it's installed but not working" issue.RefreshDatabase, the module's factories, and Carbon::setTestNow() when asserting due dates and overdue logic — otherwise your test suite will start failing on month boundaries.UPGRADE.md, pin to 1.0.* and read the diff on every bump. Budget a re-read of the migrations folder each time.How can I help you explore Laravel packages today?