InvoiceRecord.php
Pfad: src/Domain/Rechnung/InvoiceRecord.php
Ext: php
Größe: 6299 Bytes
Geändert: 2026-07-16 16:57:10+02
<?php
declare(strict_types=1);
namespace Demo\Domain\Rechnung;
/**
* Unveraenderlicher, serialisierbarer Verarbeitungsstand einer Rechnung.
*/
final class InvoiceRecord
{
/** @param list<array<string,mixed>> $errors Fehlerhistorie. */
public function __construct(
public readonly string $quelleId,
public readonly string $rechnungsschluessel,
public readonly string $status,
public readonly ?string $originalPfad = null,
public readonly ?string $downloadAt = null,
public readonly ?string $payloadSha256 = null,
public readonly ?string $sourceUrl = null,
public readonly ?string $invoiceDate = null,
public readonly ?string $invoiceNumber = null,
public readonly ?string $vendorName = null,
public readonly ?string $vendorAddressSignature = null,
public readonly ?string $netto = null,
public readonly ?string $steuer = null,
public readonly ?string $brutto = null,
public readonly ?string $currency = null,
public readonly ?string $lexofficeContactId = null,
public readonly ?string $kontaktStatus = null,
public readonly ?string $lexofficeVoucherId = null,
public readonly ?string $buchungsStatus = null,
public readonly ?float $extractionConfidence = null,
public readonly int $retryAttempts = 0,
public readonly ?string $lastAttemptAt = null,
public readonly array $errors = []
) {
}
/** Erzeugt den initialen Datensatz. */
public static function fromSourceInvoice(SourceInvoice $source): self
{
return new self($source->sourceId, $source->rechnungsschluessel(), InvoiceStatus::DISCOVERED, sourceUrl: $source->downloadUrl, invoiceDate: $source->invoiceDateHint, invoiceNumber: $source->invoiceNumberHint, vendorName: $source->vendorName);
}
/** @param array<string,mixed> $data Persistierte Daten. */
public static function fromArray(array $data): self
{
$decimal = static fn (mixed $v): ?string => $v === null ? null : Amount::decimal(is_string($v) || is_int($v) || is_float($v) ? $v : '0');
return new self((string) ($data['quelleId'] ?? ''), (string) ($data['rechnungsschluessel'] ?? ''), (string) ($data['status'] ?? InvoiceStatus::NEW), self::nullable($data['originalPfad'] ?? null), self::nullable($data['downloadAt'] ?? null), self::nullable($data['payloadSha256'] ?? null), self::nullable($data['sourceUrl'] ?? null), self::nullable($data['invoiceDate'] ?? null), self::nullable($data['invoiceNumber'] ?? null), self::nullable($data['vendorName'] ?? null), self::nullable($data['vendorAddressSignature'] ?? null), $decimal($data['netto'] ?? null), $decimal($data['steuer'] ?? null), $decimal($data['brutto'] ?? null), self::nullable($data['currency'] ?? null), self::nullable($data['lexofficeContactId'] ?? null), self::nullable($data['kontaktStatus'] ?? null), self::nullable($data['lexofficeVoucherId'] ?? null), self::nullable($data['buchungsStatus'] ?? null), isset($data['extractionConfidence']) ? (float) $data['extractionConfidence'] : null, (int) ($data['retryAttempts'] ?? 0), self::nullable($data['lastAttemptAt'] ?? null), is_array($data['errors'] ?? null) ? array_values($data['errors']) : []);
}
/** Liefert eine Kopie mit neuem Status. */
public function withStatus(string $status): self { return $this->copy(['status' => $status]); }
/** Liefert eine Kopie mit Downloadnachweis. */
public function withDownload(string $targetPath, string $sha256, string $downloadAt): self { return $this->copy(['status' => InvoiceStatus::DOWNLOADED, 'originalPfad' => $targetPath, 'payloadSha256' => $sha256, 'downloadAt' => $downloadAt]); }
/** Liefert eine Kopie mit fachlich extrahierten Daten. */
public function withExtraction(ExtractedInvoiceData $e): self
{
$key = self::identity($e->vendorName, $e->invoiceNumber, $e->invoiceDate);
return $this->copy(['rechnungsschluessel' => $key, 'status' => InvoiceStatus::EXTRACTED, 'invoiceDate' => $e->invoiceDate, 'invoiceNumber' => $e->invoiceNumber, 'vendorName' => $e->vendorName, 'vendorAddressSignature' => $e->vendorAddressSignature, 'netto' => $e->amount->netto, 'steuer' => $e->amount->steuer, 'brutto' => $e->amount->brutto, 'currency' => $e->amount->währung, 'extractionConfidence' => $e->extractionConfidence]);
}
/** Liefert eine Kopie mit Kontakt. */
public function withContact(string $kontaktId, string $kontaktStatus): self { return $this->copy(['status' => InvoiceStatus::CONTACT_RESOLVED, 'lexofficeContactId' => $kontaktId, 'kontaktStatus' => $kontaktStatus]); }
/** Liefert eine Kopie mit Buchungsnachweis. */
public function withPosting(PostingResult $posting): self { return $this->copy(['status' => InvoiceStatus::POSTED, 'lexofficeVoucherId' => $posting->voucherId, 'buchungsStatus' => $posting->postStatus]); }
/** Liefert eine Kopie mit Fehlerhistorie. */
public function withError(string $status, string $code, string $step, string $message, string $zeit): self
{
$errors = $this->errors;
$errors[] = compact('code', 'step', 'message', 'zeit');
return $this->copy(['status' => $status, 'lastAttemptAt' => $zeit, 'errors' => $errors]);
}
/** Liefert eine Kopie mit Retryzaehler. */
public function withRetry(int $attempts, string $zeit): self { return $this->copy(['retryAttempts' => $attempts, 'lastAttemptAt' => $zeit]); }
/** @return array<string,mixed> Persistenzdarstellung. */
public function toArray(): array { return get_object_vars($this); }
/** @param array<string,mixed> $changes Aenderungen. */
private function copy(array $changes): self { return self::fromArray([...$this->toArray(), ...$changes]); }
/** Baut die kanonische Fachidentitaet. */
private static function identity(string $vendor, string $number, string $date): string
{
$name = preg_replace('/[^a-z0-9]+/', '', strtolower($vendor)) ?: 'unknown';
return $name . '-' . strtoupper(preg_replace('/\s+/', '', $number) ?? '') . '-' . $date;
}
/** Normalisiert nullable Persistenzwerte. */
private static function nullable(mixed $value): ?string { return $value === null ? null : (string) $value; }
}
Frühere Versionen
| Version | Zeitpunkt | Operation |
|---|---|---|
| 19 | 2026-07-16 19:01:40.812288+02 | UPDATE |