Search by

basicxii / finance-laravel-client

modat

Laravel client and verified queued webhooks for the BasicXII Finance API.

Package info

github.com/BasicXii/finance-laravel-client

pkg:composer/basicxii/finance-laravel-client

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-09-21 13:27 UTC

This package is not auto-updated.

Last update: 2026-09-21 20:36:39 UTC


README

basicxii/finance-laravel-client provides a Laravel HTTP client and a signed, durable webhook inbox for BasicXII Finance. PHP 8.3+, Laravel 12 or 13. No application-specific order models or customer data are included.

Install

Until this package is registered on Packagist, add its public Git repository:

composer config repositories.basicxii-finance vcs https://github.com/BasicXii/finance-laravel-client
composer require basicxii/finance-laravel-client:^0.1
php artisan vendor:publish --tag=basicxii-finance-config
php artisan migrate

The service provider, facade, webhook route and inbox migration are discovered automatically. Migrations run with your application's normal migrate command.

Configuration

Copy the variables in .env.example into your application's private .env:

BASICXII_FINANCE_URL=https://finance.basicxii.com/api/v1
BASICXII_FINANCE_TOKEN=
BASICXII_FINANCE_TIMEOUT=15
BASICXII_FINANCE_CONNECT_TIMEOUT=5
BASICXII_FINANCE_WEBHOOK_ENABLED=false
BASICXII_FINANCE_WEBHOOK_PATH=api/webhooks/basicxii-finance
BASICXII_FINANCE_WEBHOOK_SECRET=
BASICXII_FINANCE_QUEUE_CONNECTION=database
BASICXII_FINANCE_QUEUE=finance

Use your company API token with only the required scopes. A typical order integration needs contacts.read, contacts.write, invoices.read, invoices.write, payments.read and payments.write. Keep tokens and webhook secrets out of frontend bundles and version control. Configuration supports config:cache; rebuild your configuration and route caches after changing it.

Client

Inject BasicXii\Finance\FinanceClient or use BasicXii\Finance\Facades\Finance:

use BasicXii\Finance\FinanceClient;

$finance = app(FinanceClient::class);
$referenceData = $finance->referenceData()->json('data');
$customers = $finance->contacts(['code' => 'STORE-CUSTOMER-123'])->json('data');

$invoice = $finance->createInvoice([
    'contact_id' => $customerId,
    'invoice_date' => '2026-09-21',
    'reference' => 'ORDER-123',
    'currency_id' => $currencyId,
    'shipping_amount' => '3.000',
    'lines' => [
        ['description' => 'Carpet', 'quantity' => '2', 'unit_price' => '15.000'],
    ],
], 'store-order-123-create-invoice');

$approval = $finance->approveInvoice($invoice->json('data.id'), 'store-order-123-approve');
if ($approval->status() === 202) {
    // Wait for Finance approval before allocating a receipt.
}

Methods return Laravel's HTTP Response, preserving HTTP status, headers, pagination and the API's data envelope. Supply amounts and quantities as decimal strings; floats are rejected. IDs must be non-empty alphanumeric identifiers (hyphens and underscores allowed). The configured API URL must use HTTPS; redirects are never followed with your credentials.

Resource Methods
Reference data referenceData()
Customers/suppliers contacts($filters), contact($id), createContact($data, $key), updateContact($id, $data)
Products products($filters), product($id), createProduct($data, $key), updateProduct($id, $data)
Invoices invoices($filters), invoice($id), createInvoice($data, $key), updateInvoice($id, $data), deleteInvoice($id), approveInvoice($id, $key), voidInvoice($id, $key, $reason)
Customer receipts receipts($filters), createReceipt($data, $key)
Credit notes createCreditNote($data, $key), postCreditNote($id, $key)
Supplier bills/payments bills($filters), createBill($data, $key), approveBill($id, $key), createPayment($data, $key)
Inventory stock($filters)

List endpoints accept ['page' => 2]. Inspect meta.last_page when paging. The package never automatically exhausts pages or invents unsupported filters.

Customer payments use receipts, not supplier payment vouchers:

$receipt = $finance->createReceipt([
    'contact_id' => $customerId,
    'date' => '2026-09-21',
    'payment_method' => 'bank_transfer',
    'deposit_account_id' => $bankAccountId,
    'amount' => '20.000',
    'reference' => 'store-payment-456',
    'allocations' => [['invoice_id' => $invoiceId, 'amount' => '20.000']],
], 'store-payment-456-receipt');

Use referenceData() or php artisan finance:reference-data to select currency, deposit account, tax and warehouse IDs for the token's company. Draft invoices can be replaced with updateInvoice() (complete payload required) or deleted. Approved invoices with no payments can be voided. Paid invoices require a separately reviewed credit note. Fulfilment statuses such as “out for delivery” are not accounting statuses; implement that mapping in the consuming app.

Reliable outgoing delivery

All POST methods require a persisted key per operation. Reuse both the exact payload and key on retry. Finance retains keys for 24 hours; the same key with another payload is rejected. Store remote IDs and reconcile uncertain operations before retrying beyond the retention window. Never generate a fresh key for every retry.

The client makes one HTTP attempt. Queue your writes, persist an outbox in the same transaction as the local order/payment, and store the successful remote IDs. This keeps external failures out of checkout and lets applications choose their own transaction and recovery strategy. Serialize work per order and re-read state to prevent concurrent writes or stale events. Laravel Http::fake() and Http::preventStrayRequests() work normally.

Failures throw BasicXii\Finance\Exceptions\FinanceApiException. retryable() identifies 409, 429 and server failures; retryAfter() reads Retry-After; errors() exposes field validation errors; response is available for deliberate inspection. Exception messages omit response bodies. Connection failures use Laravel's ConnectionException. Back off on retryable failures, respect Retry-After, and stop for credentials, permissions or validation errors. The API allows 120 requests/minute per token; coordinate rate limiting across all consumers. Do not log response bodies, request bodies or credentials indiscriminately.

Webhooks

  1. Set a dedicated BASICXII_FINANCE_WEBHOOK_SECRET and enable BASICXII_FINANCE_WEBHOOK_ENABLED.
  2. Configure a durable asynchronous queue connection (database or Redis) and a shared cache supporting locks. Do not use sync, null, deferred or background for production webhook handling.
  3. Run php artisan queue:work --queue=finance --timeout=60. Set the connection's retry_after greater than 60 seconds (the Laravel default 90 works).
  4. Register your public HTTPS endpoint /api/webhooks/basicxii-finance in Finance → Settings → Integrations → Webhooks with the same secret.
  5. Register a synchronous listener for FinanceWebhookReceived. The package already processes events in a queued job.
use BasicXii\Finance\Events\FinanceWebhookReceived;

class UpdateOrderFromFinance
{
    public function handle(FinanceWebhookReceived $event): void
    {
        $eventId = $event->payload['id'];
        $eventType = $event->payload['type'];
        $data = $event->payload['data'];

        // Find your order by data.reference and verify its linked invoice ID.
        // Re-fetch current invoice state: deliveries can arrive out of order.
        // Apply local changes without re-exporting the same payment.
    }
}

Laravel discovers listeners in app/Listeners; alternatively register using Event::listen. The package verifies HMAC-SHA256 over timestamp.rawBody, rejects timestamps at least five minutes away from the server clock in either direction, validates event headers, and stores one encrypted inbox row per event ID. Repeated successful events are not processed again; reusing an event ID with different bytes returns 409. Valid intake returns 202 after durable storage and queue dispatch. The route is outside the web/session middleware group and authenticates using the signature rather than CSRF cookies. It has a 1 MiB request limit and a request throttle. Unsafe synchronous or non-durable queue drivers are rejected with 503.

Listener work and the inbox completion marker share a database transaction on the default connection. Keep local writes on that connection. A listener failure rolls back local writes and leaves the event retryable. External side effects and separately queued listeners still require their own idempotency; exactly-once remote execution cannot be guaranteed.

basicxii_finance_webhooks records event IDs, type, encrypted payload, processed time and terminal failure time. Use php artisan finance:webhooks:retry --id=123 to retry one delivery, or omit --id for all unprocessed deliveries. This also recovers a queue-dispatch interruption after inbox storage. Monitor failed jobs and inbox backlog; retain the application's encryption key and configure an appropriate data retention policy. Do not prune event IDs earlier than your replay protection requirements.

Documented events: invoice.created, invoice.updated, invoice.deleted, invoice.posted, invoice.payment_status_changed, invoice.paid, invoice.voided, credit_note.posted, payment.received, customer.created, quotation.accepted. Unknown signed event types are also delivered so the client remains forward-compatible. Select only events your application needs, and explicitly ignore unrelated events in the listener.

The package stores events but does not create store orders, mark deliveries fulfilled, issue refunds, or choose deposit accounts. Those business decisions belong in your application.

Development

composer install
composer test
composer format

Tests use Orchestra Testbench, Pest and an isolated SQLite database; enable pdo_sqlite. They fake all HTTP requests and do not need a Finance token. CI tests Laravel 12 / PHP 8.3 and Laravel 13 / PHP 8.4.

API contract: OpenAPI JSON. License: MIT.