Files
compras/app/Services/PeopleRegistry.php
nala c6373365bf
tests / ci (push) Canceled after 0s
commit9
2026-10-30 20:02:30 +02:00

160 lines
9.1 KiB
PHP

<?php
namespace App\Services;
use App\Models\PeopleProcess;
use App\Models\User;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use stdClass;
class PeopleRegistry
{
public function canRead(User $actor): bool
{
return $actor->tenant_id !== null && ($actor->hasFunctions(['people_registry_read', 'people_registry_manage']) || $this->preparer($actor));
}
public function canCreate(User $actor): bool
{
return ! app(OrganizationAccess::class)->maintenanceOnly($actor) && ($actor->hasFunctions(['people_registry_manage']) || $this->preparer($actor));
}
private function preparer(User $actor): bool
{
return app(PeopleHiring::class)->allowed($actor, 'service', 'prepare') || app(PeopleHiring::class)->allowed($actor, 'staff', 'prepare');
}
public function canEdit(User $actor, stdClass $person): bool
{
return $person->tenant_id === $actor->tenant_id && $this->canCreate($actor) && ($actor->hasFunctions(['people_registry_manage']) || $person->created_by === $actor->id);
}
public function visible(User $actor): Builder
{
$query = DB::table('people_registry')->where('tenant_id', $actor->tenant_id);
if (! $actor->hasFunctions(['people_registry_read', 'people_registry_manage'])) {
$query->where('created_by', $actor->id);
}
return $query;
}
/** @return array<string,mixed> */
public function item(User $actor, stdClass $person): array
{
return ['id' => $person->id, 'name' => $person->name, 'reference' => $person->reference, 'profile_summary' => $person->profile_summary,
'active' => (bool) $person->active, 'version' => $person->version, 'canEdit' => $this->canEdit($actor, $person)];
}
/** @return array<string,mixed> */
public function lookup(User $actor, PeopleProcess $process, string $search): array
{
$ready = Schema::hasTable('people_registry');
$allowed = $process->status === 'open' && $this->canRead($actor) && app(PeopleHiring::class)->allowed($actor, $process->kind, 'prepare')
&& app(PeopleAccess::class)->can($actor, $process, 'prepare');
$items = $ready && $allowed && mb_strlen(trim($search)) >= 2 ? $this->visible($actor)->where('active', true)
->where(fn ($q) => $q->whereLike('name', '%'.$search.'%')->orWhereLike('reference', '%'.$search.'%'))->orderBy('name')->orderBy('id')->limit(20)->get()
->map(fn ($person) => $this->item($actor, $person))->all() : [];
return ['ready' => $ready && $allowed, 'canCreate' => $allowed && $this->canCreate($actor), 'search' => $search, 'items' => $items];
}
/** @param array<string,mixed> $input */
public function execute(User $actor, ?int $id, array $input): stdClass
{
return DB::transaction(function () use ($actor, $id, $input) {
app(ConflictGuard::class)->lockCompany($actor);
$actor = app(CurrentActor::class)->lock($actor);
abort_unless($this->canCreate($actor), 403);
$data = Validator::make($input, ['name' => 'required|string|min:3|max:200', 'reference' => 'nullable|string|max:120',
'profile_summary' => 'required|string|min:10|max:5000', 'reason' => 'required|string|min:10|max:4000',
'operation_key' => $id === null ? 'required|uuid' : 'nullable|uuid', 'version' => $id === null ? 'nullable|integer' : 'required|integer|min:1',
'active' => $id === null ? 'nullable|boolean' : 'required|boolean'])->validate();
$reference = trim($data['reference'] ?? '');
if ($reference === '') {
if ($id !== null) {
throw ValidationException::withMessages(['reference' => 'Conserve ou indique uma referência do cadastro.']);
}
$reference = 'PESS-'.strtoupper($data['operation_key']);
}
$key = mb_strtoupper(Str::squish($reference));
$values = ['name' => trim($data['name']), 'reference' => $reference, 'reference_key' => $key, 'profile_summary' => trim($data['profile_summary'])];
$hash = hash('sha256', json_encode($values, JSON_THROW_ON_ERROR));
if ($id === null) {
$existing = DB::table('people_registry')->where('tenant_id', $actor->tenant_id)->where('operation_key', $data['operation_key'])->first();
if ($existing) {
abort_unless($existing->created_by === $actor->id && $existing->creation_hash === $hash, 409);
return $existing;
}
$before = null;
} else {
$before = $this->visible($actor)->where('id', $id)->lockForUpdate()->first();
abort_unless($before && $this->canEdit($actor, $before), 404);
if ($before->version !== (int) $data['version']) {
throw ValidationException::withMessages(['version' => 'O cadastro mudou. Actualize antes de guardar.']);
}
}
if (DB::table('people_registry')->where('tenant_id', $actor->tenant_id)->where('reference_key', $key)->when($id !== null, fn ($q) => $q->where('id', '!=', $id))->exists()) {
throw ValidationException::withMessages(['reference' => 'Já existe esta referência na empresa. Reutilize o cadastro autorizado ou peça a sua conferência.']);
}
if ($id === null) {
$id = DB::table('people_registry')->insertGetId([...$values, 'tenant_id' => $actor->tenant_id, 'created_by' => $actor->id,
'operation_key' => $data['operation_key'], 'creation_hash' => $hash, 'active' => true, 'version' => 1, 'created_at' => now(), 'updated_at' => now()]);
} else {
DB::table('people_registry')->where('id', $id)->update([...$values, 'active' => $data['active'], 'version' => $before->version + 1, 'updated_at' => now()]);
}
$after = DB::table('people_registry')->where('tenant_id', $actor->tenant_id)->where('id', $id)->first();
abort_unless($after !== null, 500);
app(RequisitionWorkflow::class)->audit($actor, $before === null ? 'people_registry.created' : 'people_registry.updated', null,
$before === null ? null : (array) $before, [...(array) $after, 'reason' => $data['reason']]);
return $after;
});
}
/** @param array<string,mixed> $input
* @return array<string,mixed>
*/
public function application(User $actor, array $input): array
{
$base = Validator::make($input, ['operation_key' => 'required|uuid', 'reference' => 'required|string|max:200',
'received_on' => 'required|date_format:Y-m-d|before_or_equal:today', 'person_id' => 'nullable|integer|min:1', 'save_person' => 'sometimes|boolean'])->validate();
$person = null;
if (! empty($base['person_id']) || ($base['save_person'] ?? false)) {
abort_unless(Schema::hasTable('people_registry') && Schema::hasColumn('people_applications', 'person_id'), 503, 'Instalação do cadastro de pessoas pendente.');
}
if (! empty($base['person_id'])) {
abort_unless($this->canRead($actor), 403);
$data = Validator::make($input, ['person_version' => 'required|integer|min:1', 'summary' => 'nullable|string|min:10|max:5000'])->validate();
$person = $this->visible($actor)->where('id', $base['person_id'])->lockForUpdate()->first();
abort_unless($person !== null, 404);
if (! $person->active || $person->version !== (int) $data['person_version']) {
throw ValidationException::withMessages(['person_id' => 'O cadastro foi alterado ou arquivado. Pesquise e seleccione a versão actual.']);
}
$name = $person->name;
$summary = $data['summary'] ?? $person->profile_summary;
} else {
$data = Validator::make($input, ['name' => 'required|string|min:3|max:200', 'summary' => 'required|string|min:10|max:5000', 'person_reference' => 'nullable|string|max:120'])->validate();
$name = $data['name'];
$summary = $data['summary'];
if ($base['save_person'] ?? false) {
$person = $this->execute($actor, null, ['name' => $name, 'reference' => $data['person_reference'] ?? null, 'profile_summary' => $summary,
'operation_key' => $base['operation_key'], 'reason' => $input['reason']]);
}
}
$application = ['operation_key' => $base['operation_key'], 'reference' => $base['reference'], 'received_on' => $base['received_on'], 'name' => $name, 'summary' => $summary];
if ($person) {
$application += ['person_id' => $person->id, 'person_snapshot' => json_encode(['id' => $person->id, 'version' => $person->version,
'reference' => $person->reference, 'name' => $person->name, 'profile_summary' => $person->profile_summary], JSON_THROW_ON_ERROR)];
}
return $application;
}
}