Files
SGF/app/Modules/Purchasing/BudgetAdapter.php
T

83 lines
4.3 KiB
PHP

<?php
namespace App\Modules\Purchasing;
use App\Models\User;
use App\Modules\Identity\Services\Permissions;
use App\Shared\Context\CompanyContext;
use Illuminate\Support\Facades\DB;
use Tempo\Finance\Budget\BudgetContext;
use Tempo\Finance\Budget\BudgetControl;
/** SGF composition adapter; synthetic-only until schema/rights are explicitly deployed. */
final class BudgetAdapter
{
public static function guard(): void
{
abort_unless(app()->environment('budget-testing'), 403, 'Controlo orçamental comum disponível apenas no ensaio isolado.');
$c = DB::connection();
$name = $c->getDatabaseName();
if ($c->getDriverName() !== 'pgsql' || ! preg_match('/^budget_core_sgf_[a-f0-9]{12}$/D', $name)
|| (string) $c->getConfig('host') !== '127.0.0.1' || ! empty($c->getConfig('url'))
|| $c->selectOne('select current_database() as db')->db !== $name) {
throw new \LogicException('COMMON_BUDGET_QA_DATABASE_REFUSED');
}
}
public function for(User $user): BudgetControl
{
self::guard();
$selection = app(CompanyContext::class);
abort_unless(auth()->id() === $user->id && $selection->userId() === $user->id && $selection->unitId() !== null, 403);
$scope = $selection->identifiers() + ['unit_id' => $selection->unitId()];
$context = new BudgetContext($user->id, $scope, (int) now()->year, 'MZN');
return new BudgetControl(DB::connection(), $context, 'cb_',
function (string $action, BudgetContext $ctx): void {
self::guard();
$s = $ctx->scope;
DB::table('companies')->where('tenant_id', $s['tenant_id'])->where('id', $s['company_id'])->lockForUpdate()->firstOrFail();
DB::table('users')->where('id', $ctx->actorId)->lockForUpdate()->firstOrFail();
abort_unless(auth()->id() === $ctx->actorId, 403);
$current = app(CompanyContext::class);
abort_unless($current->identifiers() === array_intersect_key($s, array_flip(['tenant_id', 'company_id'])) && $current->unitId() === $s['unit_id'], 409, 'O contexto mudou.');
$current->selectFor($ctx->actorId, $s['tenant_id'], $s['company_id'], $s['unit_id']);
app(Permissions::class)->require($ctx->actorId, $s['tenant_id'], $s['company_id'], 'purchasing_budget.'.$action, $s['unit_id']);
},
function (string $action, array $data, ?int $origin, BudgetContext $ctx): void {
DB::table('cb_audit')->insert($ctx->scope + ['actor_id' => $ctx->actorId, 'action' => $action, 'origin_id' => $origin, 'payload' => json_encode($data, JSON_THROW_ON_ERROR), 'created_at' => now()]);
},
function (int $author, BudgetContext $ctx): bool {
$s = $ctx->scope;
DB::table('users')->where('id', $author)->lockForUpdate()->firstOrFail();
return DB::table('company_memberships')->where(['user_id' => $author, 'tenant_id' => $s['tenant_id'], 'company_id' => $s['company_id']])->exists()
&& app(Permissions::class)->has($author, $s['tenant_id'], $s['company_id'], 'purchasing_budget.reinforcement.propose', $s['unit_id']);
},
);
}
/** A mapped procurement reference, never an industrial lot or a department inferred as a unit. */
public function record(User $user, int $origin, string $kind, int $amount = 0, ?int $subtotal = null): ?array
{
self::guard();
return DB::transaction(function () use ($user, $origin, $kind, $amount, $subtotal) {
$engine = $this->for($user);
$source = DB::table('cb_origins')->where($engine->context->scope)->where('id', $origin)->first();
abort_unless($source, 403, 'Origem de compras não mapeada neste contexto.');
return $engine->record($origin, (int) $source->cost_center_id, $source->required_kind, $kind, $amount, $subtotal);
});
}
public function create(User $user, array $data): int
{
self::guard();
$engine = $this->for($user);
abort_unless(DB::table('cb_centres')->where($engine->context->scope)->where('id', $data['cost_center_id'])->exists(), 403, 'Centro não mapeado neste contexto.');
return $engine->create($data);
}
}