Adding a New Accounting Provider
The integration layer is built around AccountingSyncInterface. Adding a new provider means wiring up a driver — no changes to the core sync flow.

1. Create the service class
Create app/Services/Accounting/{Provider}AccountingService.php implementing AccountingSyncInterface:


<?php

namespace App\Services\Accounting;

use App\Contracts\AccountingSyncInterface;
use App\DTOs\AccountingPayload;
use App\DTOs\AccountingSyncResult;

class SageAccountingService implements AccountingSyncInterface
{
    public function __construct(private SageApiClient $client) {}

    public function addEntry(AccountingPayload $payload): AccountingSyncResult
    {
        $response = $this->client->postJournalEntry([
            'reference'    => $payload->entryReference,
            'account'      => $payload->externalAccountId,
            'contra'       => $payload->contrepartieAccount,
            'site'         => $payload->externalSiteId,
            'amount'       => $payload->amount,
            'date'         => $payload->date,
            'description'  => $payload->description,
        ]);

        return new AccountingSyncResult(
            success:     true,
            externalId:  (string) $response['entry_id'],
            rawResponse: $response,
        );
    }

    public function reverseEntry(string $externalId): AccountingSyncResult
    {
        $response = $this->client->deleteJournalEntry($externalId);

        return new AccountingSyncResult(
            success:     true,
            externalId:  $externalId,
            rawResponse: $response,
        );
    }
}
The only contract you must satisfy: addEntry returns a AccountingSyncResult with a non-null externalId on success, and reverseEntry accepts the externalId that was stored from a prior addEntry.

2. Register the driver
In app/Providers/AppServiceProvider.php, add your driver inside the register() singleton:


$factory->register('sage', function ($config) {
    return new SageAccountingService(
        new SageApiClient($config->settings['api_key'], $config->settings['base_url'])
    );
});
The $config argument is the AccountingConfig model for that service provider. All provider-specific credentials live in $config->settings (a JSON column).

3. Create the config record
Insert a row into accounting_configs for the service provider:


INSERT INTO accounting_configs (service_provider_id, provider_type, settings)
VALUES (
    42,
    'sage',
    '{"api_key": "sk-...", "base_url": "https://api.sage.com/v1"}'
);
Or via the VilogiConfigController-equivalent — at minimum, add a similar controller/endpoint for your provider or handle it directly in Tinker during setup.

4. Seed contrepartie accounts
Each service type needs a counterpart account. Add rows to accounting_contreparties:


INSERT INTO accounting_contreparties
    (service_provider_id, service_type_id, provider_type, account)
VALUES
    (42, 1, 'sage', '60100000'),  -- Water
    (42, 2, 'sage', '60200000');  -- Electricity
service_type_id maps to your service types table. The account value is whatever the provider expects for the credit side of the double entry.

5. Set the feature flag

UPDATE service_providers
SET allow_vilogi_webhook = 1
WHERE id = 42;
The allow_vilogi_webhook column is still the gate. It will be renamed in a future migration.

6. Set accounting_external_id on sites
Each site needs the provider's site identifier stored in sites.accounting_external_id:


UPDATE sites SET accounting_external_id = 'SAGE-SITE-001' WHERE id = 7;
What happens at runtime
When a bill email is sent, MailingRepo checks isAccountingEnabled(). If the service provider has a config and the feature flag is on, it creates an AccountingJournalEntry (with provider_type = 'sage') and dispatches SyncToAccountingJob.

The job calls AccountingSyncFactory::make($config) which looks up the 'sage' driver you registered and returns your SageAccountingService. It then calls addEntry(AccountingPayload $payload) with all the data pre-filled — account code, site ID, contrepartie, amount, description, PDF as base64.

No changes needed to JournalEntryService, SyncToAccountingJob, MailingRepo, or any model.

Checklist
 App\Services\Accounting\{Provider}AccountingService implementing AccountingSyncInterface
 Driver registered in AppServiceProvider::register()
 Row in accounting_configs with correct provider_type and settings
 Rows in accounting_contreparties per service type
 allow_vilogi_webhook = 1 on the service provider
 accounting_external_id populated on each site