Files
SGF/app/Modules/Purchasing/SupplierHistoryReceipts.php
2026-11-04 09:37:20 +02:00

79 lines
3.7 KiB
PHP

<?php
namespace App\Modules\Purchasing;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Tempo\Finance\Payables\HistoricalPayments;
use Tempo\Finance\Payables\HistoricalCapture;
use Tempo\Finance\Payables\Payables;
/** Reads explicitly reconciled immutable receipts; never imports or decides payments. */
final class SupplierHistoryReceipts
{
public const VERSION = 'SGF-PAYABLE-HISTORY-RECEIPT-SIM-v1';
public function available(): bool
{
SupplierPayables::guard();
return config('supplier_payable_history.synthetic_enabled') === true
&& DB::getSchemaBuilder()->hasTable('pa_source_histories')
&& (new HistoricalPayments(DB::connection(), []))->available();
}
public function payments(User $user, int $localId): array
{
abort_unless($this->available(), 503, 'Recibos históricos não ligados neste ensaio.');
$adapter = app(SupplierPayables::class);
$scope = $adapter->scope($user);
abort_unless($adapter->can($user, 'view'), 403);
$source = $adapter->document($user, $localId);
$receipt = DB::table('pa_source_histories')->where($scope)->where('local_source_id', $localId)->orderByDesc('version')->first();
return $this->decode($source, $receipt);
}
/** Local read snapshots only; no command obtains its source through this batch. */
public function page(User $user, array $documents, bool $validatedCapture = false): array
{
abort_unless($this->available(), 503);
$adapter = app(SupplierPayables::class); $scope = $adapter->scope($user);
abort_unless($adapter->can($user, 'view'), 403);
$receipts = DB::table('pa_source_histories')->where($scope)->whereIn('local_source_id', array_keys($documents))
->distinct('local_source_id')->orderBy('local_source_id')->orderByDesc('version')->get()->keyBy('local_source_id');
$result = [];
foreach ($documents as $id => $source) {
try { $result[$id] = $this->decode($source, $receipts->get($id), $validatedCapture ? $scope : null); }
catch (\DomainException $e) { $result[$id] = $e; }
}
return $result;
}
private function decode(array $source, ?object $receipt, ?array $captureScope = null): array|HistoricalCapture
{
if (! $receipt) {
throw new \DomainException('Histórico não reconciliado para esta origem e contexto.');
}
$payload = json_decode($receipt->payload, true, 64, JSON_THROW_ON_ERROR);
if (! is_array($payload) || ! hash_equals($receipt->receipt_hash, hash('sha256', Payables::canonical($payload)))) {
throw new \DomainException('Recibo histórico divergente.');
}
if (!(($payload['version'] ?? null) === self::VERSION
&& ($payload['source_key'] ?? null) === Payables::identity($source)
&& ($payload['source_revision'] ?? null) === Payables::revision($source)
&& ($payload['receipt_version'] ?? null) === (int) $receipt->version
&& is_array($payload['payments'] ?? null) && array_is_list($payload['payments']))) {
throw new \DomainException('Recibo histórico antigo ou incompatível; exige nova conferência da origem.');
}
$value = $captureScope === null ? null : HistoricalCapture::fromPayments($captureScope, $source, $payload['payments']);
$capture = $value ? $value->forSource($captureScope, $source) : HistoricalPayments::capture($source, $payload['payments']);
if (($payload['history_hash'] ?? null) !== $capture['history_hash']) {
throw new \DomainException('Fotografia histórica incompatível com os pagamentos originais.');
}
return $value ?? $capture['payments'];
}
}