f = receiptFixture(); $this->author = User::factory()->create(['tenant_id' => $this->f['tenant']->id, 'purchasing_role' => PurchasingRole::Finance]); $this->reviewer = User::factory()->create(['tenant_id' => $this->f['tenant']->id, 'purchasing_role' => PurchasingRole::Admin]); $this->actingAs($this->author); $this->budget = Budget::query()->create(['tenant_id' => $this->author->tenant_id, 'cost_center_id' => $this->f['r']->cost_center_id, 'year' => now()->year, 'amount_minor' => 5000, 'includes_tax' => false, 'created_by' => $this->author->id, 'approved_by' => $this->reviewer->id, 'reason' => 'Dotação inicial de teste']); $this->data = ['reference' => 'REF TEST 01', 'amount' => '70.25', 'reason' => 'Reforço fictício definido pela empresa de teste']; }); function proposeTestReinforcement(object $test): BudgetReinforcement { $test->actingAs($test->author); return app(BudgetReinforcementWorkflow::class)->propose($test->author, $test->budget, $test->data); } function approveTestReinforcement(object $test, BudgetReinforcement $reinforcement, string $decision = 'approved'): void { $test->actingAs($test->reviewer); app(BudgetReinforcementWorkflow::class)->review($test->reviewer, $reinforcement, $decision, 'Decisão independente de teste'); } test('only approved reinforcement increases available budget without rewriting the original', function () { $reinforcement = proposeTestReinforcement($this); expect($this->budget->approvedTotalMinor())->toBe(5000); approveTestReinforcement($this, $reinforcement); expect($this->budget->refresh()->amount_minor)->toBe(5000) ->and($this->budget->includes_tax)->toBeFalse() ->and($this->budget->approvedTotalMinor())->toBe(12025); $this->get('/orcamentos')->assertOk()->assertInertia(fn ($page) => $page->where('budgets.0.available_minor', 12025)->where('budgets.0.reinforced_minor', 7025)); }); test('approved reinforcement enables a previously unaffordable procurement reservation', function () { expect(fn () => DB::transaction(fn () => app(BudgetWorkflow::class)->record($this->author, $this->f['r'], 'reserve', 10000, 10000)))->toThrow(ValidationException::class); approveTestReinforcement($this, proposeTestReinforcement($this)); $this->actingAs($this->author); DB::transaction(fn () => app(BudgetWorkflow::class)->record($this->author, $this->f['r'], 'reserve', 10000, 10000)); expect((int) BudgetEntry::query()->sum('reserved_minor'))->toBe(10000); }); test('reinforcement proposals and approvals are idempotent', function () { $first = proposeTestReinforcement($this); $this->data['reference'] = ' ref test 01 '; expect(proposeTestReinforcement($this)->id)->toBe($first->id); approveTestReinforcement($this, $first); approveTestReinforcement($this, $first); expect(BudgetReinforcement::query()->count())->toBe(1)->and($this->budget->approvedTotalMinor())->toBe(12025); }); test('duplicate reference with changed values cannot create a second reinforcement', function () { proposeTestReinforcement($this); $this->data['amount'] = '71.00'; expect(fn () => proposeTestReinforcement($this))->toThrow(ValidationException::class); }); test('proposer cannot approve own reinforcement', function () { $reinforcement = proposeTestReinforcement($this); $this->post('/reforcos/'.$reinforcement->id.'/decidir', ['decision' => 'approved', 'reason' => 'Teste'])->assertForbidden(); }); test('rejection leaves balance unchanged and is final', function () { $reinforcement = proposeTestReinforcement($this); approveTestReinforcement($this, $reinforcement, 'rejected'); expect($this->budget->approvedTotalMinor())->toBe(5000); expect(fn () => approveTestReinforcement($this, $reinforcement))->toThrow(ValidationException::class); }); test('reinforcement amount must be positive and technically representable', function (string $amount) { $this->data['amount'] = $amount; expect(fn () => proposeTestReinforcement($this))->toThrow(ValidationException::class); expect(BudgetReinforcement::query()->count())->toBe(0); })->with(['0.00', '-1.00', '1000000000000.00']); test('foreign finance cannot inspect or approve another company reinforcement', function () { $reinforcement = proposeTestReinforcement($this); $other = receiptFixture(); $user = User::factory()->create(['tenant_id' => $other['tenant']->id, 'purchasing_role' => PurchasingRole::Finance]); $this->actingAs($user)->post('/reforcos/'.$reinforcement->id.'/decidir', ['decision' => 'approved', 'reason' => 'Teste'])->assertNotFound(); $this->get('/orcamentos')->assertOk()->assertInertia(fn ($page) => $page->has('reinforcements.data', 0)); }); test('warehouse procurement and requesters cannot propose reinforcements', function (PurchasingRole $role) { $user = User::factory()->create(['tenant_id' => $this->author->tenant_id, 'purchasing_role' => $role]); $this->actingAs($user)->post('/orcamentos/'.$this->budget->id.'/reforcos', $this->data)->assertForbidden(); })->with([PurchasingRole::Warehouse, PurchasingRole::Procurement, PurchasingRole::Requester, PurchasingRole::DepartmentHead]); test('reinforcement proposal and decision roll back on audit failure', function () { $reinforcement = proposeTestReinforcement($this); $this->mock(RequisitionWorkflow::class)->shouldReceive('audit')->andThrow(new RuntimeException('audit failed')); expect(fn () => approveTestReinforcement($this, $reinforcement))->toThrow(RuntimeException::class); expect($reinforcement->refresh()->status)->toBe('pending')->and($this->budget->approvedTotalMinor())->toBe(5000); $this->data['reference'] = 'REF TEST 02'; expect(fn () => proposeTestReinforcement($this))->toThrow(RuntimeException::class); expect(BudgetReinforcement::query()->count())->toBe(1); }); test('reinforcement history cannot be rewritten or deleted', function () { $reinforcement = proposeTestReinforcement($this); expect(fn () => $reinforcement->update(['amount_minor' => 999]))->toThrow(LogicException::class); expect(fn () => $reinforcement->refresh()->delete())->toThrow(LogicException::class); }); test('review rechecks current author eligibility', function () { $reinforcement = proposeTestReinforcement($this); $this->author->forceFill(['purchasing_role' => PurchasingRole::Warehouse])->save(); expect(fn () => approveTestReinforcement($this, $reinforcement))->toThrow(ValidationException::class); }); test('closed exercise prevents new reinforcement approval', function () { $reinforcement = proposeTestReinforcement($this); $this->travel(1)->years(); expect(fn () => approveTestReinforcement($this, $reinforcement))->toThrow(ValidationException::class); $this->travelBack(); }); test('unapproved initial allocation cannot receive reinforcement', function () { DB::table('budgets')->where('id', $this->budget->id)->update(['approved_by' => null]); $this->budget->refresh(); expect(fn () => proposeTestReinforcement($this))->toThrow(ValidationException::class); }); test('current operator role is rechecked instead of trusting a stale session profile', function () { DB::table('users')->where('id', $this->author->id)->update(['purchasing_role' => 'warehouse']); $this->post('/orcamentos/'.$this->budget->id.'/reforcos', $this->data)->assertForbidden(); }); test('browser cannot approve reinforcement or alter the parent allocation base', function () { $this->post('/orcamentos/'.$this->budget->id.'/reforcos', [...$this->data, 'status' => 'approved', 'includes_tax' => true, 'tenant_id' => PHP_INT_MAX, 'budget_id' => PHP_INT_MAX])->assertRedirect(); expect(BudgetReinforcement::query()->firstOrFail()->status)->toBe('pending') ->and($this->budget->refresh()->includes_tax)->toBeFalse() ->and($this->budget->approvedTotalMinor())->toBe(5000); }); test('different companies may use their own identical reinforcement references and different amounts', function () { proposeTestReinforcement($this); $other = receiptFixture(); $author = User::factory()->create(['tenant_id' => $other['tenant']->id, 'purchasing_role' => PurchasingRole::Finance]); $reviewer = User::factory()->create(['tenant_id' => $other['tenant']->id, 'purchasing_role' => PurchasingRole::Admin]); $this->actingAs($author); $budget = Budget::query()->create(['tenant_id' => $author->tenant_id, 'cost_center_id' => $other['r']->cost_center_id, 'year' => now()->year, 'amount_minor' => 2000, 'includes_tax' => true, 'created_by' => $author->id, 'approved_by' => $reviewer->id, 'reason' => 'Outra decisão fictícia']); $reinforcement = app(BudgetReinforcementWorkflow::class)->propose($author, $budget, [...$this->data, 'amount' => '10.01']); $this->actingAs($reviewer); app(BudgetReinforcementWorkflow::class)->review($reviewer, $reinforcement, 'approved', 'Outra decisão independente'); expect($budget->approvedTotalMinor())->toBe(3001); $this->actingAs($this->author); expect($this->budget->approvedTotalMinor())->toBe(5000); }); test('public reinforcement example contains no company allocations', function () { auth()->forgetGuards(); $this->get('/consulta/reforcos')->assertOk()->assertInertia(fn ($page) => $page->where('initialScreen', 'reinforcements')->where('auth.user', null)->missing('budgets')->missing('reinforcements')); });