Skip to content

sharpapi/laravel-invoice-manager

Repository files navigation

SharpAPI GitHub cover

Invoice Manager for Laravel: AI Invoice Parsing, Database and Nova Admin (powered by SharpAPI)

Turn any invoice (PDF, scan, or photo) into clean, queryable database rows and a ready-made admin UI, with one command.

Latest Version on Packagist Total Downloads

laravel-invoice-manager is the batteries-included layer on top of the SharpAPI Invoice Parser. The SharpAPI endpoint reads an invoice and returns 100+ structured fields. This package takes that result and does the part you would otherwise build by hand: a battle-tested database schema, Eloquent models, an idempotent importer, a queued job, an artisan command, and optional Laravel Nova resources.

You run one command. You get a fully populated invoices table with line items, tax details, seller and buyer parties, bank details, and the complete raw payload, all related and ready to query.

Architecture at a glance

flowchart TB
    FILE["Invoice file<br/>PDF / JPG / PNG / TIFF / DOC / DOCX"]
    CLI["Artisan command<br/>invoice-manager:import"]
    JOB["Queued job<br/>ParseAndStoreInvoiceJob"]
    PS["InvoiceParserService<br/>(sharpapi/laravel-invoice-parser)"]
    API[("SharpAPI<br/>Invoice Parser API")]
    IMP["InvoiceImporter<br/>map + persist<br/>(DB transaction, idempotent)"]
    DB[("Database<br/>invoice_* tables")]
    EVT(["InvoiceImported event"])
    NOVA["Laravel Nova admin<br/>(optional, auto-registered)"]
    APP["Your code:<br/>storeMany(result)"]

    FILE --> CLI
    FILE --> JOB
    CLI --> PS
    JOB --> PS
    PS -->|"parseInvoice()"| API
    API -->|"fetchResults(): result JSON"| PS
    CLI --> IMP
    JOB --> IMP
    APP --> IMP
    IMP --> DB
    IMP --> EVT
    DB --> NOVA

    classDef pkg fill:#eef6ff,stroke:#3b82f6,color:#0b3a82;
    classDef ext fill:#fff7ed,stroke:#fb923c,color:#7c2d12;
    class CLI,JOB,IMP,EVT,NOVA pkg;
    class PS,API ext;
Loading

This package owns the blue boxes: the import command and job, the InvoiceImporter, the InvoiceImported event, and the optional Nova admin. The parsing itself is delegated to sharpapi/laravel-invoice-parser, which calls the SharpAPI endpoint. See How an import flows for the runtime sequence and Data model for the full schema.


Why this exists (the value)

Manual invoice processing costs businesses an average of $15 to $40 per invoice once you add up labor, errors, and delays. It is the single biggest source of accounts payable mistakes. The SharpAPI Invoice Parser takes that from minutes per invoice to seconds and removes manual data entry entirely.

But the parser hands you JSON. To actually use it you still need a schema, models, a mapper that handles missing fields and empty strings, idempotency so retries do not duplicate rows, and an admin screen so your team can see what came in. That is days to weeks of work, and it is the same work for every project.

This package is that work, done once, the right way:

  • Dream outcome: every field from any invoice, in your own database, queryable with Eloquent, visible in a Nova admin, feeding your ERP or reconciliation pipeline.
  • Proven and reliable: a normalized schema, an importer wrapped in a transaction, idempotent imports keyed on the SharpAPI job id, and a Pest test suite that runs with zero network calls.
  • Near-zero time delay: composer require, php artisan migrate, then php artisan invoice-manager:import invoice.pdf --sync. Rows appear.
  • Near-zero effort: no OCR to train, no parsing rules to maintain, no schema to design, no edge cases to chase. The AI does extraction, this package does persistence.

If you process invoices in Laravel, this is the shortest path from a file on disk to structured, managed data.


Key Features

  • One-command import: invoice-manager:import {path} parses with SharpAPI and stores everything.
  • Complete schema: invoices, parties (seller and buyer), bank details, line items, and tax details, with frequently-queried values as real columns and rarely-queried groups as JSON.
  • Idempotent imports: re-running the same SharpAPI job never creates duplicate rows (configurable: update, skip, or always new).
  • Queued processing: dispatch ParseAndStoreInvoiceJob to handle invoices in the background and at scale.
  • Optional Laravel Nova admin: ready-made resources that activate automatically when Nova is installed, and stay out of the way when it is not.
  • Clean data guarantees: empty strings become null, dates become real date columns, money is decimal(15,2).
  • Fully configurable and publishable: table prefix, model classes, queue, and Nova behaviour are all config-driven.

How it relates to laravel-invoice-parser

This package builds on top of sharpapi/laravel-invoice-parser, the thin SDK that calls the SharpAPI endpoint. It does not reimplement the API call; it depends on the parser and reuses its InvoiceParserService.

Package Responsibility
sharpapi/laravel-invoice-parser Calls the SharpAPI endpoint, returns the raw structured JSON.
sharpapi/laravel-invoice-manager Stores that JSON in a managed database and gives you models, jobs, a command, and a Nova admin.

Install the manager and the parser comes with it.


Use Cases

  • Accounts payable automation: vendor invoices land as structured rows, ready for approval and posting.
  • ERP and accounting sync: feed parsed invoices into SAP, Oracle, NetSuite, QuickBooks, or Xero.
  • Spend analytics and vendor management: query line items, totals, and tax across every supplier.
  • Audit and compliance: keep the full raw payload alongside the normalized data for traceability.
  • Document digitization: convert scanned and photographed invoices into clean database records.

Requirements

  • PHP >= 8.1
  • Laravel >= 10.48.29
  • A SharpAPI API key (free to start at SharpAPI.com)
  • Laravel Nova (optional, only if you want the admin UI)

Installation

  1. Install the package via composer:
composer require sharpapi/laravel-invoice-manager
  1. Register at SharpAPI.com to obtain your API key.

  2. Set the API key in your .env file (used by the underlying parser):

SHARP_API_KEY=your_api_key_here
  1. Run the migrations (tables are created with the invoice_ prefix by default):
php artisan migrate
  1. [OPTIONAL] Publish the configuration file:
php artisan vendor:publish --tag=sharpapi-invoice-manager-config
  1. [OPTIONAL] Publish the migrations to customize them before running:
php artisan vendor:publish --tag=sharpapi-invoice-manager-migrations

Quickstart

1. Artisan command

Parse and store an invoice inline (hits SharpAPI with your key):

php artisan invoice-manager:import /path/to/invoice.pdf --sync

On success it prints a table of the stored invoices:

+----+-----------+----------+--------+
| ID | Invoice # | Currency | Total  |
+----+-----------+----------+--------+
| 1  | INV-1688  | MYR      | 373.00 |
+----+-----------+----------+--------+

Drop --sync to dispatch the work to a queue instead:

php artisan invoice-manager:import /path/to/invoice.pdf

2. Queued job

use SharpAPI\InvoiceManager\Jobs\ParseAndStoreInvoiceJob;

ParseAndStoreInvoiceJob::dispatch('/path/to/invoice.pdf');

The job parses the file with SharpAPI, then persists every invoice in the result. Configure the queue connection and name in config/sharpapi-invoice-manager.php.

3. Service (full control)

If you already have a parse result (for example from a webhook), store it directly:

use SharpAPI\InvoiceManager\Services\InvoiceImporter;
use SharpAPI\InvoiceParser\InvoiceParserService;

$parser = app(InvoiceParserService::class);
$importer = app(InvoiceImporter::class);

$statusUrl = $parser->parseInvoice('/path/to/invoice.pdf');
$job = $parser->fetchResults($statusUrl);

$invoices = $importer->storeMany($job->getResultArray(), [
    'sharp_api_job_id' => $job->id,
    'source_file_path' => '/path/to/invoice.pdf',
    'source_file_name' => 'invoice.pdf',
]);

$invoice = $invoices->first();
$invoice->invoice_number;            // "INV-1688"
$invoice->total;                     // total_payable, falling back to total_incl_tax
$invoice->lineItems;                 // collection of line items
$invoice->seller->name;              // "L Dairy Foods Sdn Bhd"
$invoice->buyer->billing_city;       // "Kuala Lumpur"
$invoice->taxDetails;                // collection of tax breakdowns

Listen for imports

Every successful import fires an event:

use SharpAPI\InvoiceManager\Events\InvoiceImported;

// In a listener
public function handle(InvoiceImported $event): void
{
    $invoice = $event->invoice;
    // notify, post to ERP, reconcile, etc.
}

How an import flows

sequenceDiagram
    autonumber
    participant App as Your app (command or job)
    participant Parser as InvoiceParserService
    participant API as SharpAPI
    participant Importer as InvoiceImporter
    participant DB as Database
    participant Event as InvoiceImported

    App->>Parser: parseInvoice(filePath)
    Parser->>API: POST /finance/parse_invoice
    API-->>Parser: status_url
    App->>Parser: fetchResults(status_url)
    Parser->>API: poll until job done
    API-->>Parser: result JSON (array of invoices)
    App->>Importer: storeMany(result, meta)
    loop each invoice element
        Importer->>DB: begin transaction
        Importer->>DB: upsert invoice (idempotent on sharp_api_job_id)
        Importer->>DB: insert parties, bank details, line items, tax details
        Importer->>DB: commit
        Importer-)Event: dispatch(InvoiceImported)
    end
    Importer-->>App: Collection of Invoice models
Loading

On import the mapper coerces empty strings to null, numeric strings to numbers, and date strings to Carbon dates. The whole element is also stored verbatim in raw_payload for lossless re-processing.


Data model (all tables and fields)

All tables use the configurable invoice_ prefix (shown without the prefix below). Money columns are decimal(15,2), dates are real date columns, and empty strings from the API are coerced to null.

Table Holds
invoices One row per detected invoice: document, invoice, and financial scalars as real columns; references, e-invoice, sales info, payment, logistics, and source pages as JSON; plus the full raw_payload.
invoice_parties One seller and one buyer per invoice, with billing and delivery addresses flattened to columns.
invoice_bank_details Bank accounts belonging to a party.
invoice_line_items Every line item, with money and quantities as decimals and date fields as real dates.
invoice_tax_details Tax breakdown rows (type, rate, taxable amount, tax amount).
erDiagram
    invoices ||--o{ invoice_parties : "parties (seller, buyer)"
    invoices ||--o{ invoice_line_items : lineItems
    invoices ||--o{ invoice_tax_details : taxDetails
    invoice_parties ||--o{ invoice_bank_details : bankDetails

    invoices {
        bigint id PK
        uuid uuid "indexed"
        string sharp_api_job_id UK "nullable; idempotency key"
        string source_file_path "nullable"
        string source_file_name "nullable"
        string document_type
        string original_type_label
        boolean is_invoice
        boolean is_copy
        string copy_type
        string invoice_number "indexed"
        date issue_date "indexed"
        date due_date
        date document_date
        date order_date
        date delivery_date
        date shipping_date
        date pricing_date
        string currency "3 chars"
        decimal exchange_rate "15,6"
        string page_info
        text amount_in_words
        text notes
        text remarks
        text delivery_instructions
        decimal late_payment_interest_rate "8,4"
        json terms_and_conditions
        decimal subtotal "15,2"
        decimal gross_amount "15,2"
        decimal total_discount_amount "15,2"
        decimal shipping_charge "15,2"
        decimal delivery_fee "15,2"
        decimal total_excl_tax "15,2"
        decimal total_tax_amount "15,2"
        decimal service_tax_amount "15,2"
        decimal total_incl_tax "15,2"
        decimal rounding_adjustment "15,2"
        decimal total_payable "15,2"
        decimal amount_paid "15,2"
        decimal amount_due "15,2"
        json references
        json e_invoice
        json sales_info
        json payment
        json logistics
        json source_pages
        json raw_payload
        timestamp created_at
        timestamp updated_at
    }

    invoice_parties {
        bigint id PK
        bigint invoice_id FK
        string role "seller or buyer"
        string name
        string trade_name
        string registration_number
        string tin
        string brn
        string sst_id
        string gst_id
        string vat_id
        string msic_code
        string business_activity
        string customer_account_number
        string billing_location_name
        string billing_recipient_name
        string billing_street_line_1
        string billing_street_line_2
        string billing_city
        string billing_state
        string billing_postcode
        string billing_country
        string delivery_recipient_name
        string delivery_location_name
        string delivery_street_line_1
        string delivery_street_line_2
        string delivery_city
        string delivery_state
        string delivery_postcode
        string delivery_country
        boolean delivery_address_same_as_billing
        string phone
        string fax
        string email
        string website
        json contact_person
        json attention_to
        timestamp created_at
        timestamp updated_at
    }

    invoice_bank_details {
        bigint id PK
        bigint invoice_party_id FK
        string bank_name
        string account_name
        string account_number
        string sort_code
        string swift_code
        string iban
        timestamp created_at
        timestamp updated_at
    }

    invoice_line_items {
        bigint id PK
        bigint invoice_id FK
        integer line_number "indexed"
        string item_code
        string stock_code
        string barcode
        text description
        string classification_code
        string country_of_origin
        decimal quantity "15,4"
        decimal free_quantity "15,4"
        string unit_of_measure
        string unit_of_measure_raw
        string pack_size
        decimal total_units "15,4"
        decimal weight "15,4"
        string weight_uom
        decimal unit_price "15,4"
        decimal discount_percent "8,4"
        decimal discount_amount "15,2"
        decimal subtotal "15,2"
        decimal tax_rate "8,4"
        string tax_type
        decimal tax_amount "15,2"
        decimal total_excl_tax "15,2"
        decimal total_incl_tax "15,2"
        date expiry_date
        string batch_lot_number
        date service_start_date
        date service_end_date
        timestamp created_at
        timestamp updated_at
    }

    invoice_tax_details {
        bigint id PK
        bigint invoice_id FK
        string tax_type
        decimal tax_rate "8,4"
        decimal taxable_amount "15,2"
        decimal tax_amount "15,2"
        timestamp created_at
        timestamp updated_at
    }
Loading

Eloquent models

The package ships five Eloquent models (namespace SharpAPI\InvoiceManager\Models). Each can be swapped for your own subclass via the models.* config keys.

classDiagram
    class Invoice {
        +lineItems() HasMany
        +taxDetails() HasMany
        +parties() HasMany
        +seller() HasOne
        +buyer() HasOne
        +getTotalAttribute() string
    }
    class InvoiceParty {
        +invoice() BelongsTo
        +bankDetails() HasMany
        +scopeSellers() Builder
        +scopeBuyers() Builder
    }
    class InvoiceBankDetail {
        +party() BelongsTo
    }
    class InvoiceLineItem {
        +invoice() BelongsTo
    }
    class InvoiceTaxDetail {
        +invoice() BelongsTo
    }
    Invoice "1" --> "*" InvoiceLineItem : lineItems
    Invoice "1" --> "*" InvoiceTaxDetail : taxDetails
    Invoice "1" --> "*" InvoiceParty : parties
    Invoice "1" --> "1" InvoiceParty : seller / buyer
    InvoiceParty "1" --> "*" InvoiceBankDetail : bankDetails
Loading
Model Backing table Config key Relationships and helpers
Invoice invoices models.invoice lineItems, taxDetails, parties, seller, buyer, total accessor
InvoiceParty invoice_parties models.party invoice, bankDetails, sellers() / buyers() scopes
InvoiceBankDetail invoice_bank_details models.bank_detail party
InvoiceLineItem invoice_line_items models.line_item invoice
InvoiceTaxDetail invoice_tax_details models.tax_detail invoice

Configuration

config/sharpapi-invoice-manager.php:

return [
    'table_prefix' => env('INVOICE_MANAGER_TABLE_PREFIX', 'invoice_'),

    'import' => [
        // update | skip | new
        'on_duplicate' => env('INVOICE_MANAGER_ON_DUPLICATE', 'update'),
    ],

    'queue' => [
        'connection' => env('INVOICE_MANAGER_QUEUE_CONNECTION'),
        'name' => env('INVOICE_MANAGER_QUEUE'),
    ],

    'nova' => [
        'enabled' => env('INVOICE_MANAGER_NOVA', true),
        'resource' => \SharpAPI\InvoiceManager\Nova\Invoice::class,
    ],

    'models' => [
        'invoice' => \SharpAPI\InvoiceManager\Models\Invoice::class,
        'party' => \SharpAPI\InvoiceManager\Models\InvoiceParty::class,
        'line_item' => \SharpAPI\InvoiceManager\Models\InvoiceLineItem::class,
        'tax_detail' => \SharpAPI\InvoiceManager\Models\InvoiceTaxDetail::class,
        'bank_detail' => \SharpAPI\InvoiceManager\Models\InvoiceBankDetail::class,
    ],
];

Point any models.* entry at your own subclass to add behaviour without forking the package.


Laravel Nova admin

If Laravel Nova is installed and nova.enabled is true, the package registers an Invoice resource automatically, with the line items, tax details, and parties exposed as related resources, and the raw JSON groups shown as read-only code fields. No manual registration required.

If Nova is not installed, nothing happens and the rest of the package works exactly the same.

The package ships five Nova resources (namespace SharpAPI\InvoiceManager\Nova), listed separately from the Eloquent models above. Only the top-level Invoice resource appears in the navigation; the rest are exposed as related (HasMany) sub-resources from the invoice detail view.

flowchart TB
    INV["Invoice resource<br/>navigation group: Invoices<br/>index: invoice #, seller, buyer, currency, total, issue date<br/>panels: Document, Invoice, Financials, Raw Groups"]
    PARTY["InvoiceParty resource<br/>hidden from navigation"]
    LI["InvoiceLineItem resource<br/>hidden from navigation"]
    TAX["InvoiceTaxDetail resource<br/>hidden from navigation"]
    BANK["InvoiceBankDetail resource<br/>hidden from navigation"]

    INV -->|"HasMany lineItems"| LI
    INV -->|"HasMany taxDetails"| TAX
    INV -->|"HasMany parties"| PARTY
    PARTY -->|"HasMany bankDetails"| BANK
Loading
Nova resource Backing model In navigation Key fields and panels
Nova\Invoice Invoice Yes (group "Invoices") Index: invoice number, seller, buyer, currency, total, issue date. Detail panels: Document, Invoice, Financials, Raw Groups (references, e-invoice, payment, logistics, sales info, raw payload as read-only code fields). HasMany: line items, tax details, parties.
Nova\InvoiceParty InvoiceParty No (sub-resource) Role, name, VAT id, registration number, addresses, contact JSON. HasMany bank details.
Nova\InvoiceLineItem InvoiceLineItem No (sub-resource) Line number, description, quantity, unit price, subtotal, tax, totals, codes, expiry date.
Nova\InvoiceTaxDetail InvoiceTaxDetail No (sub-resource) Tax type, rate, taxable amount, tax amount.
Nova\InvoiceBankDetail InvoiceBankDetail No (sub-resource) Bank name, account name, account number, SWIFT, IBAN.

To point the admin at your own customized resource, set nova.resource in the config to your subclass. The sub-resources are registered automatically alongside it.


Testing

composer test

The suite runs fully offline against a committed fixture using Orchestra Testbench and in-memory SQLite. No API key and no network access are required.


Suggested GitHub topics

laravel-invoice-manager, invoice-management, laravel-invoice, ai-invoice-parser, accounts-payable, laravel-nova, nova-resource, invoice-database, invoice-ocr, sharpapi


Support & Feedback

For issues or suggestions, please:


Changelog

Please see CHANGELOG for a detailed list of changes.


Credits


License

The MIT License (MIT). Please see License File for more information.


Follow Us

Stay updated with news, tutorials, and case studies:

About

AI-powered Invoice Manager for Laravel. Parse invoices with SharpAPI, store every extracted field in your database, and manage them with ready-made Laravel Nova resources.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages