77 lines
3.4 KiB
PHP
77 lines
3.4 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\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): 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)); }
|
|
catch (\DomainException $e) { $result[$id] = $e; }
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
private function decode(array $source, ?object $receipt): array
|
|
{
|
|
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.');
|
|
}
|
|
$capture = 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 $capture['payments'];
|
|
}
|
|
}
|