← Code-Übersicht

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) ?? [];
    }
}