Files
compras/app/Services/OrganizationConfiguration.php
T
nala 48347e2d3a
tests / ci (push) Canceled after 0s
commit5
2026-10-30 10:55:18 +02:00

143 lines
9.7 KiB
PHP

<?php
namespace App\Services;
use App\Enums\PurchasingRole;
use App\Models\Tenant;
use App\Models\User;
use Brick\Math\BigDecimal;
use Carbon\CarbonImmutable;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
class OrganizationConfiguration
{
/** @param array<string,mixed> $data
* @return array<string,int|bool>
*/
public static function rules(array $data): array
{
$first = BigDecimal::of($data['first_limit'])->multipliedBy(100)->toInt();
$second = BigDecimal::of($data['second_limit'])->multipliedBy(100)->toInt();
if ($second < $first) {
throw ValidationException::withMessages(['second_limit' => 'O segundo limite deve ser igual ou superior ao primeiro.']);
}
return ['first_limit_minor' => $first, 'second_limit_minor' => $second, 'approval_includes_tax' => (bool) $data['includes_tax'], 'rules_provisional' => false];
}
public function administrator(User $actor): User
{
abort_unless(auth()->id() === $actor->id && $actor->tenant_id !== null, 403);
$actor = User::query()->where('tenant_id', $actor->tenant_id)->findOrFail($actor->id);
abort_unless($actor->hasFunctions(['admin']), 403);
return $actor;
}
/** @param array<string,mixed> $data */
public function execute(User $actor, string $command, array $data): void
{
DB::transaction(function () use ($actor, $command, $data) {
app(ConflictGuard::class)->lockCompany($actor);
$actor = $this->administrator($actor);
DB::table('organization_access')->insertOrIgnore(['tenant_id' => $actor->tenant_id, 'active' => false, 'version' => 1, 'created_at' => now(), 'updated_at' => now()]);
$state = DB::table('organization_access')->where('tenant_id', $actor->tenant_id)->lockForUpdate()->first();
if ((int) $data['version'] !== (int) $state->version) {
throw ValidationException::withMessages(['version' => 'A parametrização foi alterada. Actualize e compare antes de guardar.']);
}
$before = null;
if ($command === 'profile') {
$profile = isset($data['id']) ? DB::table('organization_profiles')->where('tenant_id', $actor->tenant_id)->where('id', $data['id'])->first() : null;
abort_if(isset($data['id']) && ! $profile, 404);
if ($profile && in_array('admin', $data['permissions'], true) && DB::table('organization_memberships')->where('tenant_id', $actor->tenant_id)->where('profile_id', $profile->id)->where('maintenance', true)->exists()) {
throw ValidationException::withMessages(['permissions' => 'Retire as atribuições de manutenção antes de dar administração a este perfil.']);
}
$before = $profile ? (array) $profile : null;
$values = ['name' => $data['name'], 'permissions' => json_encode(array_values(array_unique($data['permissions'])), JSON_THROW_ON_ERROR), 'updated_at' => now()];
if ($profile) {
DB::table('organization_profiles')->where('id', $profile->id)->update([...$values, 'version' => $profile->version + 1]);
$id = $profile->id;
} else {
$id = DB::table('organization_profiles')->insertGetId([...$values, 'tenant_id' => $actor->tenant_id, 'created_at' => now()]);
}
$after = ['profile_id' => $id, 'name' => $data['name'], 'permissions' => $data['permissions']];
} elseif ($command === 'user') {
$department = DB::table('departments')->where('tenant_id', $actor->tenant_id)->where('id', $data['department_id'])->exists();
abort_unless($department, 422, 'Departamento não autorizado.');
$user = new User;
$user->forceFill(['tenant_id' => $actor->tenant_id, 'department_id' => $data['department_id'], 'name' => $data['name'], 'email' => $data['email'], 'password' => $data['password'], 'purchasing_role' => PurchasingRole::Configured])->save();
$this->assign($actor, $user->id, $data['profile_ids'], false, null, $data['reason']);
$after = [...$user->only(['id', 'name', 'email', 'department_id']), 'profile_ids' => $data['profile_ids'], 'reason' => $data['reason']];
} elseif ($command === 'assign') {
$target = User::query()->where('tenant_id', $actor->tenant_id)->whereKey((int) $data['user_id'])->firstOrFail();
$before = ['user_id' => $target->id, 'memberships' => DB::table('organization_memberships')->where('tenant_id', $actor->tenant_id)->where('user_id', $target->id)->get(['profile_id', 'maintenance', 'expires_at', 'reason'])->map(fn ($m) => (array) $m)->all()];
if (($data['maintenance'] ?? false) && ! $state->active) {
throw ValidationException::withMessages(['maintenance' => 'Active a parametrização antes de atribuir manutenção temporária.']);
}
$this->assign($actor, $target->id, $data['profile_ids'], $data['maintenance'] ?? false, $data['expires_at'] ?? null, $data['reason']);
$after = ['user_id' => $target->id, 'profile_ids' => $data['profile_ids'], 'maintenance' => $data['maintenance'] ?? false, 'expires_at' => $data['expires_at'] ?? null, 'reason' => $data['reason']];
} elseif ($command === 'rules') {
$company = Tenant::query()->findOrFail($actor->tenant_id);
$before = $company->only(['first_limit_minor', 'second_limit_minor', 'approval_includes_tax', 'rules_provisional']);
$after = [...self::rules($data), 'reason' => $data['reason']];
$company->fill(self::rules($data))->save();
} else {
abort_unless($command === 'activate', 404);
if (Tenant::query()->findOrFail($actor->tenant_id)->rules_provisional) {
throw ValidationException::withMessages(['rules' => 'Defina e guarde as regras de aprovação da empresa antes da activação.']);
}
DB::table('organization_access')->where('id', $state->id)->update(['active' => true]);
$after = ['active' => true, 'reason' => $data['reason']];
}
if ($state->active || $command === 'activate') {
$this->twoAdministrators((int) $actor->tenant_id);
}
DB::table('organization_access')->where('id', $state->id)->update(['version' => $state->version + 1, 'updated_at' => now()]);
app(RequisitionWorkflow::class)->audit($actor, 'organization.'.$command, null, $before, $after);
});
}
/** @param list<int> $profiles */
private function assign(User $actor, int $user, array $profiles, bool $maintenance, ?string $expires, string $reason): void
{
$ids = DB::table('organization_profiles')->where('tenant_id', $actor->tenant_id)->whereIn('id', $profiles)->pluck('id')->all();
if (count($ids) !== count($profiles)) {
throw ValidationException::withMessages(['profile_ids' => 'Escolha apenas perfis desta empresa.']);
}
if ($maintenance && ($expires === null || now()->parse($expires)->isPast())) {
throw ValidationException::withMessages(['expires_at' => 'O acesso de manutenção exige uma data futura de expiração.']);
}
if ($maintenance && DB::table('organization_profiles')->where('tenant_id', $actor->tenant_id)->whereIn('id', $ids)->pluck('permissions')->contains(fn ($json) => in_array('admin', json_decode($json, true, 32, JSON_THROW_ON_ERROR), true))) {
throw ValidationException::withMessages(['profile_ids' => 'A manutenção temporária não pode administrar os acessos da empresa.']);
}
DB::table('organization_memberships')->where('tenant_id', $actor->tenant_id)->where('user_id', $user)->delete();
foreach ($ids as $id) {
DB::table('organization_memberships')->insert(['tenant_id' => $actor->tenant_id, 'user_id' => $user, 'profile_id' => $id, 'maintenance' => $maintenance, 'expires_at' => $expires === null ? null : CarbonImmutable::parse($expires)->utc()->format('Y-m-d H:i:s'), 'reason' => $reason, 'created_at' => now(), 'updated_at' => now()]);
}
}
private function twoAdministrators(int $tenant): void
{
$count = 0;
foreach (User::query()->where('tenant_id', $tenant)->get() as $user) {
$profiles = DB::table('organization_memberships as m')->join('organization_profiles as p', 'p.id', '=', 'm.profile_id')->where('m.tenant_id', $tenant)->where('p.tenant_id', $tenant)->where('m.user_id', $user->id)->where('m.maintenance', false)->whereNull('m.expires_at')->pluck('p.permissions');
if ($profiles->contains(fn ($json) => in_array('admin', json_decode($json, true, 32, JSON_THROW_ON_ERROR), true))) {
$count++;
}
}
if ($count < 2) {
throw ValidationException::withMessages(['administrators' => 'Mantenha pelo menos dois administradores permanentes da empresa; manutenção não conta para esse mínimo.']);
}
}
public function initialise(User $first, User $second): void
{
$profile = DB::table('organization_profiles')->insertGetId(['tenant_id' => $first->tenant_id, 'name' => 'Administração da empresa', 'permissions' => json_encode(['admin'], JSON_THROW_ON_ERROR), 'version' => 1, 'created_at' => now(), 'updated_at' => now()]);
foreach ([$first, $second] as $user) {
$this->assign($first, $user->id, [$profile], false, null, 'Configuração inicial e entrega do produto.');
}
DB::table('organization_access')->insert(['tenant_id' => $first->tenant_id, 'active' => true, 'version' => 1, 'created_at' => now(), 'updated_at' => now()]);
}
}