LexofficeApiAdapterTest.php
Pfad: tests/Rechnung/LexofficeApiAdapterTest.php
Ext: php
Größe: 4333 Bytes
Geändert: 2026-07-16 17:34:48+02
<?php
declare(strict_types=1);
namespace Demo\Tests\Rechnung;
use Demo\Domain\Rechnung\Amount;
use Demo\Domain\Rechnung\ExtractedInvoiceData;
use Demo\Infrastructure\Rechnung\LexofficeApiAdapter;
use Demo\Infrastructure\Rechnung\LexwareTransportPort;
use PHPUnit\Framework\TestCase;
use RuntimeException;
final class LexofficeApiAdapterTest extends TestCase
{
public function testCreatesStructuredVendorContact(): void
{
$transport = new RecordingLexwareTransport([['id' => 'contact-1']]);
$adapter = $this->adapter($transport);
self::assertSame('contact-1', $adapter->createContact('WEtell GmbH', 'Street 1|79106 Freiburg|DE'));
$payload = $transport->calls[0]['payload'];
self::assertEquals((object) [], $payload['roles']['vendor']);
self::assertSame('WEtell GmbH', $payload['company']['name']);
self::assertSame('79106', $payload['addresses']['billing'][0]['zip']);
}
public function testReusesMatchingVoucherAndOnlyUploadsFile(): void
{
$transport = new RecordingLexwareTransport([
['content' => [['id' => 'voucher-1', 'voucherNumber' => 'RF123', 'contactId' => 'contact-1', 'totalAmount' => 31.0]]],
['id' => 'file-1'],
]);
$result = $this->adapter($transport)->postInvoice($this->invoice(), 'contact-1', $this->pdf());
self::assertSame('voucher-1', $result->toArray()['voucherId']);
self::assertCount(2, $transport->calls);
self::assertSame('/v1/vouchers/voucher-1/files', $transport->calls[1]['path']);
}
public function testCreatesPurchaseInvoiceBeforeUpload(): void
{
$transport = new RecordingLexwareTransport([
['content' => []],
['id' => 'voucher-2'],
['fileId' => 'file-2'],
]);
$adapter = $this->adapter($transport, '11111111-1111-1111-1111-111111111111');
$result = $adapter->postInvoice($this->invoice(), 'contact-1', $this->pdf());
self::assertSame('OPEN', $result->toArray()['postStatus']);
self::assertSame('purchaseinvoice', $transport->calls[1]['payload']['type']);
self::assertSame('RF123', $transport->calls[1]['payload']['voucherNumber']);
self::assertSame('file-2', $result->toArray()['externalRef']);
}
public function testRejectsSameNumberWithDifferentAmount(): void
{
$transport = new RecordingLexwareTransport([['content' => [[
'id' => 'voucher-x', 'voucherNumber' => 'RF123', 'contactId' => 'contact-1', 'totalAmount' => 30.0,
]]]]);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Duplikatkonflikt');
$this->adapter($transport)->postInvoice($this->invoice(), 'contact-1', $this->pdf());
}
private function adapter(RecordingLexwareTransport $transport, ?string $category = null): LexofficeApiAdapter
{
return new LexofficeApiAdapter(
'https://api.lexoffice.io',
'test-key',
2,
$category === null ? [] : ['postingCategoryId' => $category],
$transport,
);
}
private function invoice(): ExtractedInvoiceData
{
return new ExtractedInvoiceData(
'WEtell GmbH',
'Street 1|79106 Freiburg|DE',
'RF123',
'2026-07-04',
new Amount('26.0504', '4.9496', '31.0000', 'EUR'),
0.99,
);
}
private function pdf(): string
{
$path = sys_get_temp_dir() . '/lexware-contract-' . getmypid() . '.pdf';
file_put_contents($path, "%PDF-1.4\ncontract fixture");
return $path;
}
}
final class RecordingLexwareTransport implements LexwareTransportPort
{
/** @var list<array{method:string,path:string,query:array<string,string|int|float|bool>,payload:?array<string,mixed>,filePath:?string}> */
public array $calls = [];
/** @param list<array<string,mixed>> $responses */
public function __construct(private array $responses)
{
}
public function request(string $method, string $path, array $query = [], ?array $payload = null, ?string $filePath = null): array
{
$this->calls[] = compact('method', 'path', 'query', 'payload', 'filePath');
return array_shift($this->responses) ?? [];
}
}