Certified/app/Services/Moneybird/MoneybirdClient.php
2026-08-09 16:33:53 +02:00

165 lines
5.1 KiB
PHP

<?php
namespace App\Services\Moneybird;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
/**
* Thin wrapper around the Moneybird REST API (https://developer.moneybird.com)
* exposing only the calls subscription billing needs. Every method returns
* plain decoded-JSON arrays rather than a bespoke SDK's value objects, and
* throws MoneybirdException on unexpected failures.
*/
class MoneybirdClient
{
private function http(): PendingRequest
{
$administrationId = config('services.moneybird.administration_id');
if (blank(config('services.moneybird.api_token')) || blank($administrationId)) {
throw new MoneybirdException('Moneybird is not configured — set MONEYBIRD_API_TOKEN and MONEYBIRD_ADMINISTRATION_ID (see `php artisan moneybird:setup`).');
}
return Http::withToken(config('services.moneybird.api_token'))
->baseUrl(rtrim(config('services.moneybird.base_url'), '/').'/'.$administrationId)
->acceptJson()
->timeout(15);
}
public function findContactByCustomerId(string $customerId): ?array
{
$response = $this->http()->get("contacts/customer_id/{$customerId}.json");
if ($response->status() === 404) {
return null;
}
return $this->decode($response, "GET contacts/customer_id/{$customerId}");
}
/**
* @param array<string, mixed> $attributes
*/
public function createContact(array $attributes): array
{
$response = $this->http()->post('contacts.json', ['contact' => $attributes]);
return $this->decode($response, 'POST contacts');
}
/**
* @param array<string, mixed> $attributes
*/
public function createSalesInvoice(array $attributes): array
{
$response = $this->http()->post('sales_invoices.json', ['sales_invoice' => $attributes]);
return $this->decode($response, 'POST sales_invoices');
}
public function findSalesInvoice(string $id): array
{
$response = $this->http()->get("sales_invoices/{$id}.json");
return $this->decode($response, "GET sales_invoices/{$id}");
}
/**
* @param array<string, mixed> $attributes
*/
public function createRecurringSalesInvoice(array $attributes): array
{
$response = $this->http()->post('recurring_sales_invoices.json', ['recurring_sales_invoice' => $attributes]);
return $this->decode($response, 'POST recurring_sales_invoices');
}
/**
* @param array<string, mixed> $attributes
*/
public function updateRecurringSalesInvoice(string $id, array $attributes): array
{
$response = $this->http()->patch("recurring_sales_invoices/{$id}.json", ['recurring_sales_invoice' => $attributes]);
return $this->decode($response, "PATCH recurring_sales_invoices/{$id}");
}
public function deleteRecurringSalesInvoice(string $id): void
{
$response = $this->http()->delete("recurring_sales_invoices/{$id}.json");
// Already gone is fine — deleting is meant to be idempotent here.
if ($response->status() === 404) {
return;
}
$this->decode($response, "DELETE recurring_sales_invoices/{$id}", allowEmpty: true);
}
/**
* @return array<int, array<string, mixed>>
*/
public function listAdministrations(): array
{
$response = Http::withToken(config('services.moneybird.api_token'))
->acceptJson()
->timeout(15)
->get(rtrim(config('services.moneybird.base_url'), '/').'/administrations.json');
return $this->decode($response, 'GET administrations');
}
/**
* @return array<int, array<string, mixed>>
*/
public function listTaxRates(): array
{
$response = $this->http()->get('tax_rates.json');
return $this->decode($response, 'GET tax_rates');
}
/**
* @return array<int, array<string, mixed>>
*/
public function listLedgerAccounts(): array
{
$response = $this->http()->get('ledger_accounts.json');
return $this->decode($response, 'GET ledger_accounts');
}
/**
* @param array<string, mixed> $attributes
*/
public function createWebhook(array $attributes): array
{
$response = $this->http()->post('webhooks.json', $attributes);
return $this->decode($response, 'POST webhooks');
}
/**
* @return array<mixed>
*/
private function decode(Response $response, string $context, bool $allowEmpty = false): array
{
if ($response->failed()) {
Log::error("Moneybird API request failed: {$context}", [
'status' => $response->status(),
'body' => $response->body(),
]);
throw new MoneybirdException("Moneybird API request failed ({$context}): HTTP {$response->status()}");
}
if ($allowEmpty && blank($response->body())) {
return [];
}
return $response->json() ?? [];
}
}