← Code-Übersicht

Amount.php

Pfad: src/Domain/Rechnung/Amount.php

Ext: php

Größe: 3758 Bytes

Geändert: 2026-07-16 16:57:10+02

<?php
declare(strict_types=1);

namespace Demo\Domain\Rechnung;

use InvalidArgumentException;

/**
 * Exakter Geldbetrag mit vier Dezimalstellen ohne binaere Float-Arithmetik.
 */
final class Amount
{
    public readonly string $netto;
    public readonly string $steuer;
    public readonly string $brutto;
    public readonly string $währung;

    /**
     * @param string|int|float $netto   Nettobetrag.
     * @param string|int|float $steuer  Steuerbetrag.
     * @param string|int|float $brutto  Bruttobetrag.
     * @param string           $währung ISO-Waehrung.
     */
    public function __construct(string|int|float $netto, string|int|float $steuer, string|int|float $brutto, string $währung)
    {
        $this->netto = self::decimal($netto);
        $this->steuer = self::decimal($steuer);
        $this->brutto = self::decimal($brutto);
        $this->währung = strtoupper(trim($währung));
    }

    /**
     * Normalisiert einen lokalen Dezimalwert auf vier Nachkommastellen.
     */
    public static function decimal(string|int|float $value): string
    {
        $raw = is_float($value) ? sprintf('%.4F', $value) : trim((string) $value);
        $raw = str_replace(["\u{00A0}", ' '], '', $raw);
        if (str_contains($raw, ',') && str_contains($raw, '.')) {
            $raw = strrpos($raw, ',') > strrpos($raw, '.')
                ? str_replace(',', '.', str_replace('.', '', $raw))
                : str_replace(',', '', $raw);
        } elseif (str_contains($raw, ',')) {
            $raw = str_replace(',', '.', $raw);
        }
        if (preg_match('/^-?\d+(?:\.\d{1,4})?$/', $raw) !== 1) {
            throw new InvalidArgumentException('Ungueltiger Dezimalbetrag');
        }
        [$whole, $fraction] = array_pad(explode('.', $raw, 2), 2, '');
        return $whole . '.' . str_pad($fraction, 4, '0');
    }

    /**
     * Prueft Netto plus Steuer gegen Brutto in Integer-Arithmetik.
     */
    public function isConsistent(float $toleranz): bool
    {
        $limit = (int) round($toleranz * 10_000);
        return abs(self::units($this->netto) + self::units($this->steuer) - self::units($this->brutto)) <= $limit;
    }

    /**
     * Prueft auf einen positiven Bruttobetrag und gueltige Waehrung.
     */
    public function isPositive(): bool
    {
        return self::units($this->brutto) > 0 && preg_match('/^[A-Z]{3}$/', $this->währung) === 1;
    }

    /**
     * Subtrahiert zwei Dezimalwerte verlustfrei.
     */
    public static function subtract(string|int|float $minuend, string|int|float $subtrahend): string
    {
        return self::fromUnits(self::units(self::decimal($minuend)) - self::units(self::decimal($subtrahend)));
    }

    /**
     * Serialisiert den exakten Betrag.
     *
     * @return array{netto:string,steuer:string,brutto:string,currency:string}
     */
    public function toArray(): array
    {
        return ['netto' => $this->netto, 'steuer' => $this->steuer, 'brutto' => $this->brutto, 'currency' => $this->währung];
    }

    /**
     * Wandelt vier Dezimalstellen verlustfrei in Integer-Einheiten.
     */
    private static function units(string $value): int
    {
        $negative = str_starts_with($value, '-');
        $unsigned = ltrim($value, '-');
        [$whole, $fraction] = explode('.', $unsigned, 2);
        $units = ((int) $whole * 10_000) + (int) $fraction;
        return $negative ? -$units : $units;
    }

    /**
     * Wandelt Integer-Einheiten in einen Dezimalstring.
     */
    private static function fromUnits(int $units): string
    {
        $sign = $units < 0 ? '-' : '';
        $absolute = abs($units);
        return $sign . intdiv($absolute, 10_000) . '.' . str_pad((string) ($absolute % 10_000), 4, '0', STR_PAD_LEFT);
    }
}

Frühere Versionen

Version Zeitpunkt Operation
17 2026-07-16 19:01:40.811561+02 UPDATE