Short answer. Build a Laravel invoicing app in three passes. Model the data first (customers, invoices, line items, taxes, payments) with money stored as integer minor units, then generate the CRUD, PDF, and payment layer from one detailed prompt, then harden the parts an AI will not guess for you, which are gapless invoice numbers, tax rounding, immutability after issue, and authorization. Done that way, the build takes days instead of weeks.

Invoicing looks like CRUD until the first rounding complaint lands in your inbox.

You have built forms that save rows. Invoicing is different because the rows are money, and money has rules. A total that disagrees with the sum of its lines is a support ticket, an awkward call with an accountant, and occasionally a legal problem. That is exactly where AI earns its place. It writes the scaffolding fast so your attention goes to the arithmetic, the numbering, and the audit trail.

This guide covers the whole build. The schema that prevents the classic bugs, the prompts that produce code worth keeping, gapless numbering under concurrency, tax rounding per line, PDF delivery, Stripe payments, multi-tenancy, and the Pest tests that keep the numbers honest.

Key Takeaways

What a Laravel invoicing app must get right

An invoicing app is a ledger with a user interface. The moment an invoice leaves your system it becomes a record other people rely on, so the constraints are tighter than a normal resource. Five rules cover most of the risk.

Rule

The shortcut that breaks

What to build instead

Money is never a float

Float math on decimal columns

Integer minor units plus a currency code per invoice

Invoice numbers are gapless

count() + 1 at save time

A locked counter row inside a transaction

Issued invoices are immutable

Editing the row after sending it

A status enum plus credit notes for corrections

Tax rounds per line

Rounding once on the grand total

Round each line, then sum the lines

Every record is tenant scoped

Filtering inside the controller

A global scope plus a Policy on every action

Miss any of these and the bug shows up late, usually in front of a paying customer. Encode them in the schema early and everything after that is ordinary Laravel work, which is the part AI does well.

Design the invoice data model before you prompt anything

An AI generates whatever schema your prompt implies. That makes the data model the highest leverage thing you write yourself. Four tables carry most invoicing systems, which are customers, invoices, invoice_items, and payments. Add invoice_counters for numbering, and credit_notes once corrections become real.

Schema::create('invoices', function (Blueprint $table) {
    $table->id();
    $table->foreignId('team_id')->constrained();
    $table->foreignId('customer_id')->constrained();
    $table->string('number')->nullable();
    $table->string('status')->default('draft');
    $table->char('currency', 3);
    $table->unsignedBigInteger('subtotal_minor');
    $table->unsignedBigInteger('tax_minor');
    $table->unsignedBigInteger('total_minor');
    $table->date('issued_on')->nullable();
    $table->date('due_on')->nullable();
    $table->timestamps();

    $table->unique(['team_id', 'number']);
});

That unique index is not decoration. It is the last line of defence when two requests race to issue an invoice at the same moment.

Store money as integers, always

A price of 19.99 EUR is 1999. Multiply, add, and tax in whole minor units, and format for display only at the edge of the system. Binary floating point cannot represent 0.1 exactly, so float arithmetic drifts, and the drift is invisible until an invoice with 40 lines is off by a cent.

Cast the status column to a backed PHP enum with draft, issued, paid, and void cases, cast the dates, and put the relations on the model. Scaffold the whole set in one command, since every flag saves a file you would otherwise write by hand and the options are listed in the php artisan make:model cheatsheet.

php artisan make:model Invoice -mfc --policy --requests

Build the Laravel invoicing app with AI in five prompts

Generic assistants produce generic PHP. A Laravel-native tool starts from the conventions you already use, so the same request returns very different output depending on what you ask it with. StackMivo generates production-ready Laravel apps from a description, including Eloquent models and relations, migrations, controllers, FormRequests, Policies, Filament resources, and Pest tests, as standard code you keep and can edit.

Split the build into five prompts rather than one giant request. Smaller prompts are easier to review, and review is where the quality actually comes from.

  1. Schema and models. Tables, relations, casts, enums, and the tenant scope.

  2. Domain actions. IssueInvoice, RecalculateTotals, MarkAsPaid, and VoidInvoice as single purpose classes.

  3. Delivery. The Blade invoice template, PDF rendering, and a queued Mailable.

  4. Payments. Checkout links, the webhook controller, and idempotent payment recording.

  5. Tests. Pest coverage for totals, numbering, immutability, and policy denials.

Build your Laravel app with AI

Skip the boilerplate. StackMivo turns a plain-English prompt into production-ready Laravel apps, with models, migrations, controllers, CRUD, auth, and tests.

Get started free

The prompt that produces usable code

Vague prompts return vague apps. State the rules you already decided on, in the vocabulary of the framework, and the output arrives close to shippable.

Build an invoicing module for an existing Laravel 12 app.
Tenancy: every table has team_id with a TeamScope global scope.
Money: integers in minor units, currency per invoice, never floats.
Status: draft, issued, paid, void as a backed enum cast.
Numbering: gapless per team per year, assigned inside a locked
transaction when the invoice is issued.
Lines: quantity, unit_price_minor, tax_basis_points, with net and
tax computed per line and rounded half up.
Rule: an issued invoice is immutable, corrections create a credit note.
Deliver migrations, models with relations, FormRequests, Policies,
an IssueInvoice action, a Filament resource, and Pest tests
for the totals and the numbering.

A freelance Laravel developer rebuilding billing for a design studio ran the first version of that prompt without the money line. The generated app used decimal columns and PHP float math, which passed every manual test on two line items and then produced a 3 cent gap on a 60 line retainer invoice. One sentence about minor units, regenerated in the same session, removed the entire class of bug. The lesson holds for any assistant. Constraints in, correctness out.

Latest StackMivo Review

Review the output before you keep it

Read generated code the way you read a pull request from a competent stranger. Check that relations have foreign key constraints, that FormRequests validate currency and quantity, that Policies are registered and actually called, and that no action mutates an invoice once its status has left draft. When the shape is right, you can generate the rest of the module from a prompt and own every file it writes.

Invoice numbering, totals, and tax without rounding bugs

These two pieces are where invoicing apps quietly fail, and where an AI needs explicit instruction because there is no single universal convention to infer.

An agency team shipped an internal billing tool where the next invoice number came from a count of existing rows plus one. It worked for a year. Then a batch job issued 30 invoices in parallel from a queue, two workers read the same count in the same millisecond, and the tool wrote two invoices with the identical number. Their accountant found it before they did. A locked counter row and a unique index would have made the collision impossible.

public function next(int $teamId, int $year): string
{
    return DB::transaction(function () use ($teamId, $year) {
        $counter = InvoiceCounter::query()
            ->where('team_id', $teamId)
            ->where('year', $year)
            ->lockForUpdate()
            ->first();

        $counter ??= InvoiceCounter::create([
            'team_id' => $teamId,
            'year' => $year,
            'last_number' => 0,
        ]);

        $counter->increment('last_number');

        return sprintf('INV-%d-%05d', $year, $counter->last_number);
    });
}

The pessimistic locking documentation explains what lockForUpdate does at the database level. Hold the lock for as little work as possible, and never send an email or call an API inside that transaction.

Totals follow the same discipline. Express tax rates in basis points so the rate itself is an integer, compute each line in minor units, round half up per line, then sum. A Laravel invoice generator that renders a beautiful PDF from wrong numbers is worse than no tool at all.

final class LineTotal
{
    public static function for(int $quantity, int $unitPriceMinor, int $taxBasisPoints): array
    {
        $net = $quantity * $unitPriceMinor;
        $tax = intdiv($net * $taxBasisPoints + 5000, 10000);

        return ['net_minor' => $net, 'tax_minor' => $tax];
    }
}

Rounding order is a real decision, not a detail. Per line and per tax rate are both defensible depending on the jurisdiction you invoice in, so confirm the rule that applies to your customers, then lock your choice into a test so nobody changes it by accident later.

PDF invoices, email delivery, and Stripe payments

Rendering is the easy half. Point a Blade template at the invoice, render it with laravel-dompdf for straightforward layouts, or a headless Chrome renderer when the design needs modern CSS. Then store the generated file.

Archiving matters more than developers expect. If you re-render an old invoice from current data, a changed logo, a corrected address, or a renamed product silently rewrites history. Save the PDF to storage on issue and serve that exact file forever.

Delivery is a queued Mailable with the archived PDF attached, plus a scheduled command that dispatches reminder jobs for anything overdue. Both are small classes, both are ideal AI-generated boilerplate, and both belong on a queue so a slow mail server never blocks a request.

For payment, Laravel Cashier handles subscription billing, while one off invoices usually map to a payment intent or a hosted payment link. Whatever you choose, the webhook is the part to get right. Verify the signature, then record the provider event id in a table with a unique index before you touch the invoice, so a replayed event cannot mark the same invoice paid twice. Stripe documents the delivery guarantees in its webhooks guide, and the same idempotency pattern applies to every provider. Teams building checkout flows alongside billing usually reuse the same event handling for their e-commerce features.

Authorization, multi-tenancy, and tests for the money paths

Invoices leak across tenants when scoping lives in a controller. Put a global scope on the model so every query is filtered by default, add a Policy for view, update, issue, and void, and gate the destructive actions behind the Policy rather than a role check scattered through Blade.

A Filament resource gives your finance team a working admin panel without a second frontend build, which is often the difference between shipping in a week and shipping in a month. This is also where a Laravel invoicing app benefits most from Laravel-native intelligence, because the generated Policies, FormRequests, and resource classes follow framework conventions instead of approximating them.

Then test the money. Four tests catch almost everything that matters.

it('rounds tax per line and never drifts from the invoice total', function () {
    $invoice = Invoice::factory()
        ->hasItems(3, [
            'quantity' => 1,
            'unit_price_minor' => 333,
            'tax_basis_points' => 2000,
        ])
        ->create();

    app(RecalculateTotals::class)->handle($invoice);

    expect($invoice->fresh()->tax_minor)->toBe(201)
        ->and($invoice->fresh()->total_minor)->toBe(1200);
});

Add a test that issuing twice never produces a duplicate number, one that updating an issued invoice throws, and one that a user from another team receives a 403. Ask your assistant to write them alongside the feature, because tests generated from the same specification tend to encode the same rules you asked for.

Ship your Laravel invoicing app

The build is short once the decisions are made. Integers for money, a locked counter for numbering, tax rounded per line, immutability after issue, archived PDFs, idempotent webhooks, tenant scoping through a global scope and a Policy, and Pest tests over every one of those rules. AI writes almost all of that code. Your job is to hand it the constraints and read the diff.

Start with the schema, generate the module in the five passes above, and run the money tests before a single real invoice goes out. When it is green, push it live with one-click deployment so migrations run and queues restart without a manual checklist. The same approach carries over to the next internal system you build, whether that is a CRM in Laravel or the billing side of a SaaS product. You can get started free and have the first version of the module in front of you today.

Build your Laravel app with AI

Skip the boilerplate. StackMivo turns a plain-English prompt into production-ready Laravel apps, with models, migrations, controllers, CRUD, auth, and tests.

Get started free