171 lines
12 KiB
PHP
171 lines
12 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\Support\Facades\Schema;
|
|
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 === 'comparison') {
|
|
$company = Tenant::query()->findOrFail($actor->tenant_id);
|
|
$before = $company->only(['comparison_policy']);
|
|
$current = app(ComparisonPolicy::class)->company($company);
|
|
$policy = app(ComparisonPolicy::class)->checked([...$data, 'revision' => $current['revision'] + 1, 'configured' => true]);
|
|
$company->update(['comparison_policy' => $policy]);
|
|
$after = ['comparison_policy' => $policy, 'reason' => $data['reason'], 'scope' => 'new_requisitions_only'];
|
|
} elseif ($command === 'ai_plan') {
|
|
abort_unless(Schema::hasColumn('tenants', 'intelligence_policy'), 503, 'Instalação da configuração de inteligência pendente.');
|
|
$company = Tenant::query()->findOrFail($actor->tenant_id);
|
|
$before = $company->only(['intelligence_policy']);
|
|
$policy = app(IntelligencePolicy::class)->company($company);
|
|
$policy = [...$policy, 'revision' => $policy['revision'] + 1, 'configured' => true, 'ai_plan' => app(AiPreparationPlan::class)->checked($data)];
|
|
$company->update(['intelligence_policy' => $policy]);
|
|
$after = ['intelligence_policy' => $policy, 'reason' => $data['reason'], 'scope' => 'draft_preparations_only', 'external_processing_enabled' => false];
|
|
} elseif ($command === 'intelligence') {
|
|
abort_unless(Schema::hasColumn('tenants', 'intelligence_policy'), 503, 'Instalação da configuração de inteligência pendente.');
|
|
$company = Tenant::query()->findOrFail($actor->tenant_id);
|
|
$before = $company->only(['intelligence_policy']);
|
|
$policy = app(IntelligencePolicy::class)->next($company, $data);
|
|
$company->update(['intelligence_policy' => $policy]);
|
|
$after = ['intelligence_policy' => $policy, 'reason' => $data['reason'], 'scope' => 'future_consultations_only', 'external_processing_enabled' => false];
|
|
} elseif ($command === 'tax') {
|
|
$company = Tenant::query()->findOrFail($actor->tenant_id);
|
|
$before = $company->only(['default_tax_rate']);
|
|
$company->update(['default_tax_rate' => (string) BigDecimal::of($data['default_tax_rate'])->toScale(2)]);
|
|
$after = [...$company->only(['default_tax_rate']), 'reason' => $data['reason'], 'scope' => 'new_lines_only'];
|
|
} 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()]);
|
|
}
|
|
}
|