f = receiptFixture('100.000'); $this->head = User::factory()->create(['tenant_id' => $this->f['tenant']->id, 'department_id' => $this->f['department']->id]); supplyGrant($this->f['issuer'], ['requester', 'warehouse_request_prepare']); supplyGrant($this->f['receiver'], ['warehouse', 'warehouse_request_deliver']); supplyGrant($this->head, ['department_head']); supplyGrant($this->head, ['warehouse_request_review']); supplyGrant($this->f['otherReceiver'], ['admin']); $this->actingAs($this->f['receiver']); app(ReceiptWorkflow::class)->confirm($this->f['receiver'], $this->f['order'], receiptPayload($this->f, '10.000')); app(RequisitionCatalogue::class)->save($this->f['receiver'], $this->f['stock']->id, ['version' => 0, 'stock_basis' => app(RequisitionCatalogue::class)->basis($this->f['stock']), 'category' => 'Material', 'purchase_type' => 'consumable', 'published' => true, 'reason' => 'Publicação sintética para reutilizar o cadastro nas tarefas.']); }); function taskInboxRequest($test, string $quantity = '4.000'): void { $test->actingAs($test->f['issuer']); $test->r = app(WarehouseRequests::class)->draft($test->f['issuer'], ['operation_key' => (string) Str::uuid(), 'warehouse_id' => $test->f['warehouse']->id, 'cost_center_id' => $test->f['r']->cost_center_id, 'priority' => 'normal', 'destination' => 'Oficina sintética', 'reason' => 'Material para manutenção no ensaio de tarefas.', 'lines' => [['stock_item_id' => $test->f['stock']->id, 'quantity' => $quantity, 'use' => 'internal']]]); taskInboxCommand($test, $test->f['issuer'], 'submit', ['partial_allowed' => true]); } test('task deadline filtering uses the whole authorised projection before pagination and preserves module search scope', function () { $this->travelTo(now()->startOfDay()->addHours(23)->addMinutes(59)); $rows = []; for ($i = 0; $i < 28; $i++) { $rows[] = ['id' => 'deadline:'.$i, 'module' => 'supply', 'reference' => 'PRAZO-'.$i, 'label' => 'Acompanhar origem existente', 'blocked' => false, 'created_at' => now()->subMonth()->toIso8601String(), 'due_on' => today()->subDay()->toDateString(), 'href' => '/compras/fornecimento?view=reservations']; } foreach (['today' => today()->toDateString(), 'upcoming' => today()->addDay()->toDateString(), 'undated' => null] as $status => $date) { $rows[] = [...$rows[0], 'id' => 'deadline:'.$status, 'reference' => 'PRAZO-'.$status, 'due_on' => $date]; } $rows[] = [...$rows[0], 'id' => 'other', 'module' => 'warehouse', 'reference' => 'OUTRO']; $this->mock(WorkflowTaskInbox::class)->shouldReceive('build')->andReturn($rows); $this->get('/requisicoes/tarefas?module=supply&due=overdue')->assertInertia(fn (Assert $p) => $p ->where('tasks.total', 28)->has('tasks.data', 25)->where('tasks.data.0.due_status', 'overdue') ->where('deadlineCounts.overdue', 28)->where('deadlineCounts.today', 1)->where('deadlineCounts.upcoming', 1)->where('deadlineCounts.undated', 1) ->where('counts.warehouse', 1)->where('asOf', today()->toDateString())->where('filters.due', 'overdue') ->where('tasks.next_page_url', fn ($url) => str_contains($url, 'due=overdue') && str_contains($url, 'module=supply'))); $this->get('/requisicoes/tarefas?module=supply&due=overdue&page=2')->assertInertia(fn (Assert $p) => $p->has('tasks.data', 3)->where('tasks.data.0.id', 'deadline:25')); foreach (['today', 'upcoming', 'undated'] as $status) { $this->get('/requisicoes/tarefas?module=supply&due='.$status)->assertInertia(fn (Assert $p) => $p->where('tasks.total', 1)->where('tasks.data.0.due_status', $status)); } $this->get('/requisicoes/tarefas?module=supply&search=PRAZO-today&due=overdue')->assertInertia(fn (Assert $p) => $p ->where('tasks.total', 0)->where('deadlineCounts.today', 1)->where('deadlineCounts.overdue', 0)); $this->getJson('/requisicoes/tarefas?due=invalid')->assertUnprocessable(); }); function taskInboxCommand($test, User $actor, string $action, array $extra = []) { $test->actingAs($actor); return app(WarehouseFulfilment::class)->command($actor, $test->r->id, ['operation_key' => (string) Str::uuid(), 'version' => app(WarehouseFulfilment::class)->progress($test->r)['version'], 'action' => $action, 'reason' => 'Intervenção efectiva no ensaio das tarefas.', ...$extra]); } function taskInboxPurchase($test) { $test->actingAs($test->f['issuer']); $entry = DB::table('requisition_catalogue')->where('stock_item_id', $test->f['stock']->id)->sole(); return app(RequisitionWorkflow::class)->save($test->f['issuer'], ['operation_key' => (string) Str::uuid(), 'cost_center_id' => $test->f['r']->cost_center_id, 'priority' => 'normal', 'needed_by' => today()->addWeek()->toDateString(), 'justification' => 'Compra sintética para validar o encaminhamento de tarefas.', 'items' => [['catalogue_entry_id' => $entry->id, 'catalogue_version' => $entry->version, 'quantity' => '2.000', 'unit_price' => '10.00', 'tax_rate' => '0']]]); } test('task inbox connects purchasing draft and real human approval without granting administrative execution', function () { $purchase = taskInboxPurchase($this); $this->get('/requisicoes/tarefas')->assertOk()->assertInertia(fn (Assert $page) => $page->where('tasks.total', 1)->where('tasks.data.0.task', 'prepare')->where('tasks.data.0.href', '/requisicoes/'.$purchase->id)); app(RequisitionWorkflow::class)->submit($this->f['issuer'], $purchase); $this->get('/requisicoes/tarefas')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0)); $this->actingAs($this->head)->get('/requisicoes/tarefas')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 1)->where('tasks.data.0.task', 'approve')); $this->actingAs($this->f['otherReceiver'])->get('/requisicoes/tarefas')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0)); $this->actingAs($this->head); app(RequisitionWorkflow::class)->decide($this->head, $purchase->fresh(), 'approved', 'Conferência humana no ensaio sintético.'); $this->get('/requisicoes/tarefas')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0)); }); test('warehouse review delivery and individual receipt tasks follow the existing commands including cancelled balances', function () { taskInboxRequest($this); $this->actingAs($this->head)->get('/requisicoes/tarefas')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 1)->where('tasks.data.0.task', 'review')); taskInboxCommand($this, $this->head, 'authorize', ['partial_allowed' => true]); $this->get('/requisicoes/tarefas')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0)); $this->actingAs($this->f['receiver'])->get('/requisicoes/tarefas')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 1)->where('tasks.data.0.task', 'delivery')->where('tasks.data.0.blocked', false)); $first = taskInboxCommand($this, $this->f['receiver'], 'deliver', ['lines' => [['index' => 0, 'quantity' => '1.000']]]); $second = taskInboxCommand($this, $this->f['receiver'], 'deliver', ['lines' => [['index' => 0, 'quantity' => '1.000']]]); taskInboxCommand($this, $this->head, 'cancel'); $this->actingAs($this->f['issuer'])->get('/requisicoes/tarefas?module=warehouse')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 2) ->where('tasks.data.0.href', '/stock/requisicoes/'.$this->r->id.'#warehouse-command-receive-'.$first->id) ->where('tasks.data.1.href', '/stock/requisicoes/'.$this->r->id.'#warehouse-command-receive-'.$second->id)); taskInboxCommand($this, $this->f['issuer'], 'receive', ['delivery_event_id' => $first->id]); $this->get('/requisicoes/tarefas?module=warehouse')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 1)); taskInboxCommand($this, $this->f['issuer'], 'receive', ['delivery_event_id' => $second->id]); $this->get('/requisicoes/tarefas?module=warehouse')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0)); }); test('reservation deficits and blocked delivery remain visible to the appropriate people', function () { taskInboxRequest($this, '25.000'); taskInboxCommand($this, $this->head, 'authorize', ['partial_allowed' => true]); $this->get('/requisicoes/tarefas')->assertInertia(fn (Assert $page) => $page->where('tasks.data.0.task', 'reserve')); $this->f['stock']->update(['name' => 'Cadastro alterado depois da autorização']); $this->actingAs($this->f['receiver'])->get('/requisicoes/tarefas')->assertInertia(fn (Assert $page) => $page->where('tasks.data.0.blocked', true)->where('tasks.data.0.href', '/stock/requisicoes/'.$this->r->id)); $this->actingAs($this->f['issuer'])->get('/requisicoes/tarefas?module=warehouse')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0)); }); test('task filtering pagination and oldest records use the whole authorised scope', function () { $first = null; foreach (range(1, 30) as $i) { $purchase = taskInboxPurchase($this); $first ??= $purchase; } taskInboxRequest($this); $this->actingAs($this->f['issuer'])->get('/requisicoes/tarefas')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 30)->has('tasks.data', 25)->where('tasks.data.0.reference', $first->number)); $this->get('/requisicoes/tarefas?page=2')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 30)->has('tasks.data', 5)); $this->get('/requisicoes/tarefas?search='.urlencode($first->number))->assertInertia(fn (Assert $page) => $page->where('tasks.total', 1)->where('counts.purchases', 30)); $this->actingAs($this->head)->get('/requisicoes/tarefas?module=warehouse')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 1)->where('counts.purchases', 0)->where('counts.warehouse', 1)); }); test('consulting tasks does not record decisions read receipts reservations or stock changes', function () { taskInboxRequest($this); $this->actingAs($this->head); $tables = ['warehouse_request_events', 'warehouse_reservations', 'stock_movements', 'requisitions', 'approval_steps', 'audit_logs', 'supply_records']; $before = []; foreach ($tables as $table) { $before[$table] = DB::table($table)->get()->toArray(); } $this->get('/requisicoes/tarefas')->assertOk()->assertHeader('Cache-Control', 'max-age=0, no-store, private'); foreach ($tables as $table) { expect(DB::table($table)->get()->toArray())->toEqual($before[$table]); } $profile = DB::table('organization_profiles')->whereJsonContains('permissions', 'warehouse_request_review')->value('id'); DB::table('organization_memberships')->where('user_id', $this->head->id)->where('profile_id', $profile)->update(['expires_at' => now()->subMinute()]); $this->get('/requisicoes/tarefas')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0)); }); test('people tasks reuse the assigned team and stay separate from purchasing and warehouse', function () { $owner = $this->f['otherReceiver']; $reviewer = User::factory()->create(['tenant_id' => $owner->tenant_id, 'department_id' => $owner->department_id]); supplyGrant($reviewer, ['people_service_review']); $this->actingAs($owner); $process = app(PeopleHiring::class)->execute($owner, null, 'create', ['operation_key' => (string) Str::uuid(), 'kind' => 'service', 'title' => 'Contratação sintética do ensaio', 'purpose' => 'Necessidade documentada para testar a equipa responsável.', 'closes_on' => today()->addWeek()->toDateString(), 'minimum_reviews' => 1, 'criteria' => [['label' => 'Competências', 'weight' => 100]]]); app(PeopleAccess::class)->insert($process, $reviewer->id, ['view', 'review'], 'Equipa atribuída no ensaio descartável.'); $this->actingAs($reviewer)->get('/pessoas/tarefas')->assertInertia(fn (Assert $page) => $page->where('area', 'people')->where('tasks.total', 1)->where('tasks.data.0.module', 'people')->where('tasks.data.0.task', 'open')); $this->getJson('/pessoas/tarefas?module=warehouse')->assertUnprocessable(); $this->actingAs($owner)->get('/pessoas/tarefas')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0)); $this->actingAs($reviewer); app(PeopleHiring::class)->execute($reviewer, $process->id, 'open', ['version' => $process->version, 'no_conflict' => true, 'reason' => 'Conferência independente da abertura no ensaio.']); $this->actingAs($owner)->get('/pessoas/tarefas')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 1)->where('tasks.data.0.task', 'apply')); DB::table('people_process_access')->where('people_process_id', $process->id)->where('user_id', $owner->id)->update(['expires_at' => now()->subMinute()]); $this->get('/pessoas/tarefas')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0)); }); test('tasks do not cross company or the currently selected work context', function () { taskInboxRequest($this); $foreign = receiptFixture(); supplyGrant($foreign['issuer'], ['admin']); $this->actingAs($foreign['issuer'])->get('/requisicoes/tarefas')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0)); $actor = $this->f['otherReceiver']; $this->f['tenant']->update(['work_context_policy' => ['revision' => 1, 'enabled' => ['people.internal', 'procurement.internal']]]); config(['work_context.entitlements.'.$actor->tenant_id => ['people.internal', 'procurement.internal']]); DB::table('organization_profiles')->where('tenant_id', $actor->tenant_id)->update(['work_contexts' => json_encode(['people.internal', 'procurement.internal'])]); $this->actingAs($actor)->post('/contexto', ['area' => 'people', 'option' => 'people.internal'])->assertRedirect(); $this->get('/requisicoes/tarefas')->assertForbidden(); $this->get('/pessoas/tarefas')->assertOk(); $this->post('/contexto', ['area' => 'procurement', 'option' => 'procurement.internal'])->assertRedirect(); $this->get('/pessoas/tarefas')->assertForbidden(); }); test('temporary maintenance does not receive operational decisions even with the corresponding profile', function () { $purchase = taskInboxPurchase($this); app(RequisitionWorkflow::class)->submit($this->f['issuer'], $purchase); DB::table('organization_memberships')->where('user_id', $this->head->id)->update(['maintenance' => true]); $this->actingAs($this->head)->get('/requisicoes/tarefas')->assertOk()->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0)); expect($purchase->fresh()->status)->toBe('pending'); });