133 lines
6.4 KiB
PHP
133 lines
6.4 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Enums\PurchasingRole;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Tempo\Finance\GeneralLedger\LedgerSchema;
|
|
use Tempo\Finance\GeneralLedger\PayableJournal;
|
|
use Tempo\Finance\GeneralLedger\ReportsWorkspace;
|
|
use Tempo\Finance\Payables\Payables;
|
|
|
|
/** Standalone adapter; same local package, native finance authority, no SGF remote service. */
|
|
final class GeneralLedger
|
|
{
|
|
/** @return array{actor:User,scope:array<string,int>,person:string} */
|
|
private function context(User $user): array
|
|
{
|
|
abort_unless(app()->environment('payable-testing') && config('general-ledger.synthetic_enabled') === true, 403);
|
|
$package = config('general-ledger.path');
|
|
abort_unless(is_string($package) && is_file($package.'/bootstrap.php'), 503, 'Pacote contabilístico não composto.');
|
|
require_once $package.'/bootstrap.php';
|
|
LedgerSchema::assertSynthetic(DB::connection());
|
|
$context = app(SupplierPayables::class)->readContext($user);
|
|
abort_unless(DB::table('pa_access')->where($context['scope'])->where(['user_id' => $user->id, 'action' => 'general_ledger.view', 'person' => $context['person']])->exists(), 403);
|
|
|
|
return $context;
|
|
}
|
|
|
|
public function can(User $user, string $action): bool
|
|
{
|
|
$c = $this->context($user);
|
|
|
|
return DB::table('pa_access')->where($c['scope'])->where(['user_id' => $user->id, 'action' => 'general_ledger.'.$action, 'person' => $c['person']])->exists();
|
|
}
|
|
|
|
public function engine(User $user, ?\Closure $fault = null): PayableJournal
|
|
{
|
|
$c = $this->context($user);
|
|
$scope = $c['scope'];
|
|
$person = $c['person'];
|
|
$authorize = function (string $action, bool $lock = true) use ($user, $scope, $person) {
|
|
if ($lock) {
|
|
app(ConflictGuard::class)->lockCompany($user);
|
|
app(CurrentActor::class)->lock($user, [PurchasingRole::Finance, PurchasingRole::Admin]);
|
|
}
|
|
$fresh = $this->context($user);
|
|
abort_unless($fresh['scope'] === $scope && $fresh['person'] === $person, 409, 'Pessoa ou contexto alterados.');
|
|
abort_unless($this->can($user, $action), 403);
|
|
};
|
|
|
|
return new PayableJournal(DB::connection(), $scope, $user->id, $person, fn ($a) => $authorize($a), fn () => $authorize('view', false), fn ($id) => $this->source($user, $scope, $id), $fault);
|
|
}
|
|
|
|
/** @param array<string,int> $scope
|
|
* @return array<string,mixed>
|
|
*/
|
|
private function source(User $user, array $scope, int $id): array
|
|
{
|
|
$o = DB::table('pa_obligations')->where($scope)->where('id', $id)->lockForUpdate()->first() ?? throw new \DomainException('Obrigação indisponível nesta organização.');
|
|
$origin = DB::table('pa_sources')->where($scope)->where('id', $o->source_id)->lockForUpdate()->first() ?? throw new \DomainException('Origem ausente.');
|
|
$finance = app(SupplierPayables::class);
|
|
$doc = $finance->document($user, (int) $origin->local_source_id);
|
|
$position = $finance->engine($user)->positionForSource((int) $origin->local_source_id);
|
|
if (! $position || (int) $position['id'] !== $id || $position['source_conflict']) {
|
|
throw new \DomainException('Origem financeira mudou; nova conferência necessária.');
|
|
}
|
|
$p = DB::table('pa_proposals')->where($scope)->where('id', $o->proposal_id)->first() ?? throw new \DomainException('Conferência original ausente.');
|
|
|
|
return ['effect_type' => 'supplier_payable.recognised', 'origin_key' => $origin->source_key, 'obligation_id' => $id, 'local_source_id' => (int) $origin->local_source_id, 'document_revision' => $o->source_revision, 'recognition_revision' => hash('sha256', Payables::canonical(['obligation' => (array) $o, 'recognition_snapshot' => $p->snapshot])), 'principal_minor' => (int) $o->principal_minor, 'currency' => $o->currency, 'due_on' => $o->due_on, 'tax_minor' => $doc['tax_minor'], 'compatible' => true, 'history_imported' => $position['history_imported']];
|
|
}
|
|
|
|
/** @param array<string,mixed> $options
|
|
* @return array<string,mixed>
|
|
*/
|
|
public function report(User $user, string $kind, array $options): array
|
|
{
|
|
if (! in_array($kind, ['trial-balance', 'account-ledger'], true)) {
|
|
throw new \InvalidArgumentException('Relatório desconhecido.');
|
|
}
|
|
$own = DB::transactionLevel() === 0;
|
|
|
|
return DB::transaction(function () use ($user, $kind, $options, $own) {
|
|
if ($own) {
|
|
DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');
|
|
}
|
|
$engine = $this->engine($user);
|
|
abort_unless(class_exists(ReportsWorkspace::class), 503, 'Relatórios não incluídos no pacote composto.');
|
|
foreach (LedgerSchema::TABLES as $table) {
|
|
abort_unless(DB::getSchemaBuilder()->hasTable($table), 503, 'Fonte contabilística indisponível neste ensaio.');
|
|
}
|
|
|
|
return $kind === 'trial-balance' ? $engine->trialBalance($options) : $engine->accountLedger($options);
|
|
});
|
|
}
|
|
|
|
/** @return array<string,mixed> */
|
|
public function workspace(User $user, int $page = 1): array
|
|
{
|
|
abort_unless(config('general-ledger.cycle_enabled') === true, 403);
|
|
$own = DB::transactionLevel() === 0;
|
|
|
|
return DB::transaction(function () use ($user, $page, $own) {
|
|
if ($own) {
|
|
DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');
|
|
}
|
|
$engine = $this->engine($user);
|
|
$data = $engine->cycleListing($page);
|
|
$data['book'] = $engine->listing(['page' => $page]);
|
|
$actions = ['prepare', 'validate', 'post', 'correct_prepare', 'correct_validate', 'correct_post', 'period_prepare', 'period_validate', 'period_close'];
|
|
$data['permissions'] = array_combine($actions, array_map(fn ($a) => $this->can($user, $a), $actions));
|
|
|
|
return $data;
|
|
});
|
|
}
|
|
|
|
/** @param array<string,mixed> $options
|
|
* @return array<string,mixed>
|
|
*/
|
|
public function listing(User $user, array $options = []): array
|
|
{
|
|
$own = DB::transactionLevel() === 0;
|
|
|
|
return DB::transaction(function () use ($user, $options, $own) {
|
|
if ($own) {
|
|
DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');
|
|
}
|
|
|
|
return $this->engine($user)->listing($options);
|
|
});
|
|
}
|
|
}
|