commit de compras
This commit is contained in:
@@ -13,7 +13,9 @@ final class SupplierPayables
|
||||
public static function guard(): void
|
||||
{
|
||||
$c = DB::connection();
|
||||
abort_unless(app()->environment('payable-testing'), 403, 'Obrigações apenas no ensaio isolado.');
|
||||
$composedQa=app()->environment(['testing','browser-testing'])&&(config('sales_ledger.synthetic_enabled')===true||config('cost_ledger.synthetic_enabled')===true);
|
||||
abort_unless(app()->environment('payable-testing')||$composedQa, 403, 'Obrigações apenas no ensaio isolado.');
|
||||
if($composedQa)\App\Shared\Testing\TestingDatabaseGuard::assertSafe();
|
||||
if ($c->getDriverName() !== 'pgsql' || ! preg_match('/^payable_sgf_[a-f0-9]{12}$/D', $c->getDatabaseName()) || $c->getConfig('host') !== '127.0.0.1' || $c->selectOne('select current_database() as d')->d !== $c->getDatabaseName()) {
|
||||
throw new \LogicException('PAYABLE_QA_DATABASE_REFUSED');
|
||||
}
|
||||
@@ -81,7 +83,32 @@ final class SupplierPayables
|
||||
};
|
||||
|
||||
return new Payables(DB::connection(), $u->id, $person, $scope, fn (string $a) => $authorize($a), $source,
|
||||
app(SupplierHistoryReceipts::class)->available() ? $history : null, fn () => $authorize('view', false));
|
||||
app(SupplierHistoryReceipts::class)->available() ? $history : null, fn () => $authorize('view', false), fn () => $authorize('payments_view', false));
|
||||
}
|
||||
|
||||
/** One predicate for list windows and complete position reports. */
|
||||
private function sourceQuery(array $scope, array $options): array
|
||||
{
|
||||
$q = trim((string) ($options['q'] ?? '')); $recognition = $options['recognition'] ?? 'all';
|
||||
abort_unless(mb_strlen($q) <= 100 && in_array($recognition, ['all', 'recognised', 'unrecognised'], true), 422);
|
||||
$query = DB::table('pa_imported_sources AS r')->where($scope);
|
||||
$sourceId = array_key_exists('source', $options) ? filter_var($options['source'], FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]) : null;
|
||||
abort_if($sourceId === false || (isset($options['source']) && is_bool($options['source'])), 422, 'Origem inválida.');
|
||||
if ($sourceId !== null) {
|
||||
abort_unless((clone $query)->where('r.id', $sourceId)->exists(), 403, 'Origem não mapeada neste contexto.');
|
||||
$query->where('r.id', $sourceId);
|
||||
}
|
||||
if ($q !== '') {
|
||||
$needle = '%'.str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $q).'%';
|
||||
$query->where(fn ($w) => $w->whereRaw("payload->>'number' ILIKE ?", [$needle])->orWhereRaw("payload->>'supplier_name' ILIKE ?", [$needle]));
|
||||
}
|
||||
if ($recognition !== 'all') {
|
||||
$method = $recognition === 'recognised' ? 'whereExists' : 'whereNotExists';
|
||||
$query->$method(fn ($w) => $w->selectRaw('1')->from('pa_sources AS s')->join('pa_obligations AS o', 'o.source_id', '=', 's.id')
|
||||
->whereColumn('s.local_source_id', 'r.id')->whereColumn('s.tenant_id', 'r.tenant_id')->whereColumn('s.company_id', 'r.company_id')->whereColumn('s.unit_id', 'r.unit_id')
|
||||
->whereColumn('o.tenant_id', 'r.tenant_id')->whereColumn('o.company_id', 'r.company_id')->whereColumn('o.unit_id', 'r.unit_id'));
|
||||
}
|
||||
return [$query, $sourceId];
|
||||
}
|
||||
|
||||
public function listing(User $u, array $options = []): array
|
||||
@@ -93,17 +120,7 @@ final class SupplierPayables
|
||||
$page = (int) ($options['page'] ?? 1); $perPage = (int) ($options['per_page'] ?? 25);
|
||||
$q = trim((string) ($options['q'] ?? '')); $recognition = $options['recognition'] ?? 'all';
|
||||
abort_unless($page >= 1 && $page <= 100000 && in_array($perPage, [25, 100], true) && mb_strlen($q) <= 100 && in_array($recognition, ['all', 'recognised', 'unrecognised'], true), 422);
|
||||
$query = DB::table('pa_imported_sources AS r')->where($scope);
|
||||
if ($q !== '') {
|
||||
$needle = '%'.str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $q).'%';
|
||||
$query->where(fn ($w) => $w->whereRaw("payload->>'number' ILIKE ?", [$needle])->orWhereRaw("payload->>'supplier_name' ILIKE ?", [$needle]));
|
||||
}
|
||||
if ($recognition !== 'all') {
|
||||
$method = $recognition === 'recognised' ? 'whereExists' : 'whereNotExists';
|
||||
$query->$method(fn ($w) => $w->selectRaw('1')->from('pa_sources AS s')->join('pa_obligations AS o', 'o.source_id', '=', 's.id')
|
||||
->whereColumn('s.local_source_id', 'r.id')->whereColumn('s.tenant_id', 'r.tenant_id')->whereColumn('s.company_id', 'r.company_id')->whereColumn('s.unit_id', 'r.unit_id')
|
||||
->whereColumn('o.tenant_id', 'r.tenant_id')->whereColumn('o.company_id', 'r.company_id')->whereColumn('o.unit_id', 'r.unit_id'));
|
||||
}
|
||||
[$query, $sourceId] = $this->sourceQuery($scope, $options);
|
||||
$pager = $query->orderByDesc('r.id')->paginate($perPage, ['r.*'], 'page', $page); $documents = [];
|
||||
foreach ($pager->items() as $row) { $documents[(int) $row->id] = $this->receiptDocument($row); }
|
||||
$provider = app(SupplierHistoryReceipts::class);
|
||||
@@ -111,6 +128,7 @@ final class SupplierPayables
|
||||
$d = $this->buildEngine($u, $documents, $histories)->listingPage(array_keys($documents), (int) ($options['proposal_page'] ?? 1), (int) ($options['event_page'] ?? 1), $perPage);
|
||||
$d['pagination']['documents'] = ['page' => $pager->currentPage(), 'last_page' => $pager->lastPage(), 'total' => $pager->total(), 'per_page' => $pager->perPage()];
|
||||
$d['listing_query'] = ['page' => $page, 'per_page' => $perPage, 'q' => $q, 'recognition' => $recognition, 'proposal_page' => (int) ($options['proposal_page'] ?? 1), 'event_page' => (int) ($options['event_page'] ?? 1)];
|
||||
if ($sourceId !== null) { $d['listing_query']['source'] = $sourceId; }
|
||||
$d['permissions'] = ['prepare' => $this->can($u, 'prepare'), 'validate' => $this->can($u, 'validate')];
|
||||
$d['history_import_available'] = app(SupplierHistoryReceipts::class)->available();
|
||||
$d['history_decisions_available'] = false;
|
||||
@@ -122,4 +140,79 @@ final class SupplierPayables
|
||||
return $d;
|
||||
});
|
||||
}
|
||||
|
||||
/** Current recognised positions, aggregated through the existing engine in bounded windows. */
|
||||
public function report(User $u, array $criteria = []): array
|
||||
{
|
||||
abort_if(array_diff(array_keys($criteria), ['q', 'source']) !== [], 422);
|
||||
abort_if(isset($criteria['q']) && !is_string($criteria['q']), 422);
|
||||
if (array_key_exists('source', $criteria)) {
|
||||
$id = filter_var($criteria['source'], FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
|
||||
abort_if($id === false || is_bool($criteria['source']), 422);
|
||||
$criteria['source'] = $id;
|
||||
}
|
||||
$own = DB::transactionLevel() === 0;
|
||||
return DB::transaction(function () use ($u, $criteria, $own): array {
|
||||
if ($own) DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');
|
||||
else abort_unless(in_array(DB::selectOne('SHOW transaction_isolation')->transaction_isolation, ['repeatable read', 'serializable'], true), 409, 'O relatório exige uma fotografia coerente própria.');
|
||||
$scope = $this->scope($u);
|
||||
abort_unless($this->can($u, 'view'), 403);
|
||||
$criteria['q'] = trim((string) ($criteria['q'] ?? ''));
|
||||
$hash = hash_init('sha256');
|
||||
hash_update($hash, Payables::canonical(['scope' => $scope, 'criteria' => $criteria, 'basis' => 'current_recognised_positions', 'window' => 100])."\n");
|
||||
$total = ['count' => 0, 'principal_minor' => 0, 'unknown_balance_count' => 0, 'unknown_due_count' => 0];
|
||||
$known = ['count' => 0, 'principal_minor' => 0, 'historical_confirmed_minor' => 0, 'net_allocated_minor' => 0, 'balance_minor' => 0];
|
||||
[$query] = $this->sourceQuery($scope, $criteria + ['recognition' => 'recognised']);
|
||||
$person = DB::table('pa_access')->where($scope)->where(['user_id' => $u->id, 'action' => 'view'])->value('person');
|
||||
abort_unless(is_string($person) && $person !== '', 403);
|
||||
$provider = app(SupplierHistoryReceipts::class); $historical = $provider->available();
|
||||
$read = function () use ($u, $scope, $person): void {
|
||||
$context = app(CompanyContext::class);
|
||||
abort_unless(auth()->id() === $u->id && $context->userId() === $u->id, 403);
|
||||
abort_unless(($context->identifiers() + ['unit_id' => $context->unitId()]) === $scope, 409);
|
||||
abort_unless($this->can($u, 'view'), 403);
|
||||
abort_unless(DB::table('pa_access')->where($scope)->where(['user_id' => $u->id, 'action' => 'view'])->value('person') === $person, 409);
|
||||
};
|
||||
$last = null; $windows = 0;
|
||||
do {
|
||||
$window = clone $query;
|
||||
if ($last !== null) $window->where('r.id', '<', $last);
|
||||
$rows = $window->orderByDesc('r.id')->limit(100)->get(['r.*']);
|
||||
if ($rows->isEmpty()) break;
|
||||
$documents = [];
|
||||
foreach ($rows as $row) $documents[(int) $row->id] = $this->receiptDocument($row);
|
||||
$histories = $historical ? $provider->page($u, $documents, true) : [];
|
||||
$engine = new Payables(DB::connection(), $u->id, $person, $scope,
|
||||
static fn () => throw new \LogicException('Relatórios nunca executam comandos.'),
|
||||
fn (int $id) => $documents[$id] ?? abort(403),
|
||||
$historical ? static function (int $id) use ($histories) {
|
||||
$result = $histories[$id] ?? new \DomainException('Histórico fora da janela.');
|
||||
if ($result instanceof \DomainException) throw $result;
|
||||
return $result;
|
||||
} : null, $read);
|
||||
$data = $engine->positionsPage(array_keys($documents));
|
||||
foreach ($data['documents'] as $doc) hash_update($hash, Payables::canonical(['local_source_id' => $doc['local_id'], 'current_revision' => $doc['revision']])."\n");
|
||||
foreach ($data['obligations'] as $o) {
|
||||
abort_unless($o['currency'] === 'MZN', 409, 'Moeda incompatível no relatório; nenhuma soma entre moedas.');
|
||||
$total['count']++; $total['principal_minor'] += $o['principal_minor'];
|
||||
if ($o['due_on'] === null) $total['unknown_due_count']++;
|
||||
if ($o['source_conflict'] || $o['balance_minor'] === null) $total['unknown_balance_count']++;
|
||||
else {
|
||||
$known['count']++; $known['principal_minor'] += $o['principal_minor'];
|
||||
$known['historical_confirmed_minor'] += $o['confirmed_payments_minor'];
|
||||
$known['net_allocated_minor'] += $o['allocated_payments_minor'];
|
||||
$known['balance_minor'] += $o['balance_minor'];
|
||||
}
|
||||
hash_update($hash, Payables::canonical($o)."\n");
|
||||
}
|
||||
$windows++; $last = (int) $rows->last()->id;
|
||||
} while ($rows->count() === 100);
|
||||
abort_unless($known['principal_minor'] - $known['historical_confirmed_minor'] - $known['net_allocated_minor'] === $known['balance_minor'], 409, 'Componentes financeiros não reconciliados.');
|
||||
$total['balance_minor'] = $total['unknown_balance_count'] === 0 ? $known['balance_minor'] : null;
|
||||
return ['version' => 'SGF-PAYABLE-POSITION-REPORT-SIM-v1', 'synthetic_only' => true,
|
||||
'basis' => 'current_recognised_positions', 'captured_at_utc' => DB::selectOne("SELECT transaction_timestamp() AT TIME ZONE 'UTC' AS captured")->captured,
|
||||
'scope' => $scope, 'criteria' => $criteria, 'currency' => 'MZN', 'totals' => $total, 'known_positions' => $known,
|
||||
'positions_sha256' => hash_final($hash), 'window_size' => 100, 'windows' => $windows];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user