116 lines
9.0 KiB
PHP
116 lines
9.0 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\CostCenter;
|
|
use App\Models\StockItem;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use App\Models\Warehouse;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
|
|
/** A diagnostic, never an activation, grant, business command or production certificate. */
|
|
class WarehousePilotReadiness
|
|
{
|
|
/** @return array<string,mixed> */
|
|
public function inspect(User $actor): array
|
|
{
|
|
abort_unless(auth()->id() === $actor->id && $actor->hasFunctions(['admin']), 403);
|
|
$users = User::query()->where('tenant_id', $actor->tenant_id)->orderBy('name')->get(['id', 'tenant_id', 'name', 'department_id', 'purchasing_role']);
|
|
$centers = CostCenter::query()->get(['id', 'department_id']);
|
|
$accessReady = Schema::hasTable('organization_memberships') && Schema::hasTable('organization_profiles');
|
|
$groups = ['prepare' => [], 'review' => [], 'deliver' => [], 'purchase_requester' => [], 'supply_prepare' => [], 'supply_review' => []];
|
|
$permanentAdmins = 0;
|
|
foreach ($users as $user) {
|
|
$grants = $accessReady ? DB::table('organization_memberships as m')->join('organization_profiles as p', 'p.id', '=', 'm.profile_id')->where('m.tenant_id', $actor->tenant_id)->where('p.tenant_id', $actor->tenant_id)->where('m.user_id', $user->id)->where('m.maintenance', false)->whereNull('m.expires_at')->pluck('p.permissions') : collect();
|
|
if ($grants->contains(fn ($json) => in_array('admin', json_decode($json, true, 32, JSON_THROW_ON_ERROR), true))) {
|
|
$permanentAdmins++;
|
|
}
|
|
if (app(OrganizationAccess::class)->maintenanceOnly($user)) {
|
|
continue;
|
|
}
|
|
$context = app(WorkContext::class)->enforced($user) ? 'procurement.internal' : null;
|
|
if ($context !== null && ! in_array($context, app(WorkContext::class)->available($user), true)) {
|
|
continue;
|
|
}
|
|
$permissions = app(OrganizationAccess::class)->permissions($user, $context);
|
|
$person = $user->only(['id', 'name', 'department_id']) + ['has_cost_center' => $user->department_id !== null && $centers->contains('department_id', $user->department_id)];
|
|
foreach (['prepare' => 'warehouse_request_prepare', 'review' => 'warehouse_request_review', 'purchase_requester' => 'requester', 'supply_prepare' => 'supply_prepare', 'supply_review' => 'supply_review'] as $key => $permission) {
|
|
if (in_array($permission, $permissions, true)) {
|
|
$groups[$key][] = $person;
|
|
}
|
|
}
|
|
if (in_array('warehouse_request_deliver', $permissions, true) && in_array('warehouse', $permissions, true)) {
|
|
$groups['deliver'][] = $person;
|
|
}
|
|
}
|
|
$independent = false;
|
|
$deliveryIds = array_fill_keys(array_column($groups['deliver'], 'id'), true);
|
|
foreach ($groups['prepare'] as $requester) {
|
|
foreach ($groups['review'] as $reviewer) {
|
|
if ($requester['id'] === $reviewer['id'] || $requester['department_id'] === null || $requester['department_id'] !== $reviewer['department_id'] || ! $requester['has_cost_center']) {
|
|
continue;
|
|
}
|
|
if (count($deliveryIds) > (isset($deliveryIds[$requester['id']]) ? 1 : 0) + (isset($deliveryIds[$reviewer['id']]) ? 1 : 0)) {
|
|
$independent = true;
|
|
break 2;
|
|
}
|
|
}
|
|
}
|
|
$tables = [];
|
|
foreach (['warehouse_requests', 'warehouse_request_events', 'warehouse_reservations', 'supply_records', 'replenishment_work', 'requisition_catalogue'] as $table) {
|
|
$tables[$table] = Schema::hasTable($table);
|
|
}
|
|
$warehouses = Warehouse::query()->count();
|
|
$items = StockItem::query()->get(['id', 'code', 'name', 'unit'])->keyBy('id');
|
|
$published = 0;
|
|
$stale = 0;
|
|
if ($tables['requisition_catalogue']) {
|
|
foreach (DB::table('requisition_catalogue')->where('tenant_id', $actor->tenant_id)->where('published', true)->get(['stock_item_id', 'source_snapshot']) as $entry) {
|
|
$item = $items->get($entry->stock_item_id);
|
|
if ($item !== null && app(RequisitionCatalogue::class)->source($item) === json_decode($entry->source_snapshot, true, 32, JSON_THROW_ON_ERROR)) {
|
|
$published++;
|
|
} else {
|
|
$stale++;
|
|
}
|
|
}
|
|
}
|
|
$planning = app(SupplyPlanning::class);
|
|
$policies = $tables['supply_records'] ? $planning->latest('policy')->where('state', 'approved')->where('data->purpose', 'internal')->count() : 0;
|
|
$validRules = 0;
|
|
$invalidRules = 0;
|
|
if (! in_array(false, $tables, true)) {
|
|
foreach ($planning->latest('auto_rule')->where('state', 'approved')->cursor() as $rule) {
|
|
$status = app(AutomaticPurchases::class)->inspectRule($actor, $rule);
|
|
if ($status['valid']) {
|
|
$validRules++;
|
|
} else {
|
|
$invalidRules++;
|
|
}
|
|
}
|
|
}
|
|
$runtime = app(ReplenishmentQueue::class)->status($actor);
|
|
$pending = (int) ($runtime['work']->pending ?? 0);
|
|
$failed = (int) ($runtime['work']->failed ?? 0);
|
|
$recoveryClear = $runtime['recoveryReady'] && $pending === 0 && $failed === 0;
|
|
$active = app(OrganizationAccess::class)->active($actor->tenant_id);
|
|
$company = Tenant::query()->findOrFail($actor->tenant_id);
|
|
$internalConfigured = $company->work_context_policy !== null && in_array('procurement.internal', $company->work_context_policy['enabled'] ?? [], true) && in_array('procurement.internal', config('work_context.entitlements.'.$actor->tenant_id, []), true);
|
|
$checks = [
|
|
['key' => 'installation', 'label' => 'Instalação dos percursos', 'ok' => ! in_array(false, $tables, true), 'detail' => in_array(false, $tables, true) ? 'Existem componentes por instalar.' : 'Requisições, reservas, catálogo e recuperação durável instalados.', 'path' => null],
|
|
['key' => 'participants', 'label' => 'Tarefas e independência no armazém', 'ok' => $independent, 'detail' => $independent ? 'Há combinação elegível de requisitante, autorizador e entregador distintos, com centro de custo do departamento.' : 'Faltam tarefas, centro de custo ou uma combinação de três intervenientes independentes. Não são atribuídas competências nesta consulta.', 'path' => '/administracao/parametrizacao'],
|
|
['key' => 'catalogue', 'label' => 'Artigos e armazéns', 'ok' => $warehouses > 0 && $published > 0, 'detail' => $warehouses.' armazém(ns), '.$published.' artigo(s) publicado(s) com origem actual e '.$stale.' publicação(ões) a rever.', 'path' => '/stock/catalogo'],
|
|
['key' => 'replenishment', 'label' => 'Reposição parametrizada e válida', 'ok' => $validRules > 0, 'detail' => $policies.' política(s) interna(s) aprovada(s), '.$validRules.' regra(s) activa(s) válida(s) e '.$invalidRules.' a rever. Falta de stock não é, por si, erro de configuração.', 'path' => '/stock/reposicao'],
|
|
['key' => 'scheduler', 'label' => 'Verificação periódica observada', 'ok' => $runtime['recoveryReady'] && $runtime['schedulerRecent'] && ($runtime['schedulerStatus']['result'] ?? null) === 'passed', 'detail' => $runtime['schedulerRecent'] ? 'Há uma passagem recente conhecida do serviço.' : 'Sem passagem recente confirmada nesta interface; instalação não prova execução.', 'path' => '/stock/reposicao'],
|
|
['key' => 'administration', 'label' => 'Configuração própria da empresa', 'ok' => $active && $permanentAdmins >= 2 && $internalConfigured, 'detail' => $active ? 'Perfis próprios activos; '.$permanentAdmins.' administrador(es) permanente(s). Conferir também o contexto interno contratado/activado.' : 'Acesso ainda em compatibilidade com funções de desenvolvimento. Isto não é configuração de cliente concluída.', 'path' => '/administracao/parametrizacao'],
|
|
['key' => 'recovery', 'label' => 'Trabalhos técnicos de reposição', 'ok' => $recoveryClear, 'detail' => $pending.' sinal(is) por processar e '.$failed.' trabalho(s) com recuperação pendente. Necessidades funcionais sem configuração válida continuam visíveis na reposição.', 'path' => '/stock/reposicao'],
|
|
];
|
|
|
|
return ['checked_at' => now()->toIso8601String(), 'checks' => $checks, 'participants' => $groups, 'registered' => $users->map(fn ($u) => $u->only(['id', 'name', 'department_id']))->all(), 'tables' => $tables, 'runtime' => $runtime,
|
|
'warehouse_scenario_ready' => ! in_array(false, $tables, true) && $independent && $warehouses > 0 && $published > 0,
|
|
'replenishment_scenario_ready' => ! in_array(false, $tables, true) && $recoveryClear && $validRules > 0 && $runtime['schedulerRecent'] && ($runtime['schedulerStatus']['result'] ?? null) === 'passed',
|
|
'client_configuration_complete' => $active && $permanentAdmins >= 2 && $internalConfigured];
|
|
}
|
|
}
|