$data * @return array */ 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 $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 $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()]); } }