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]); } 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'); });