Files
compras/tests/Feature/SupplyHistoryTest.php
2026-11-03 11:51:44 +02:00

324 lines
27 KiB
PHP

<?php
use App\Models\StockMovement;
use App\Models\SupplyRecord;
use App\Models\Tenant;
use App\Models\User;
use App\Models\Warehouse;
use App\Services\ReceiptWorkflow;
use App\Services\ReplenishmentProjection;
use App\Services\RequisitionCatalogue;
use App\Services\StockWorkflow;
use App\Services\SupplyHistory;
use App\Services\SupplyPlanning;
use Carbon\CarbonImmutable;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Inertia\Testing\AssertableInertia as Assert;
require_once __DIR__.'/../Support/ReceiptFixture.php';
require_once __DIR__.'/../Support/SupplyFixture.php';
beforeEach(function () {
$this->travelTo(CarbonImmutable::parse('2026-09-30 12:00:00'));
$this->f = receiptFixture('1500.000');
supplyGrant($this->f['issuer'], ['procurement', 'supply_prepare', 'supply_history_prepare']);
supplyGrant($this->f['receiver'], ['warehouse', 'supply_review', 'supply_history_review', 'supply_reserve']);
$this->actingAs($this->f['receiver']);
app(ReceiptWorkflow::class)->confirm($this->f['receiver'], $this->f['order'], receiptPayload($this->f, '1200.000'));
historyIssue($this, '2026-10-10', '100.000');
historyIssue($this, '2026-10-20', '200.000');
$this->travelTo(CarbonImmutable::parse('2026-11-01 12:00:00'));
$this->actingAs($this->f['issuer']);
});
function historyIssue($test, string $date, string $quantity): object
{
$test->travelTo(CarbonImmutable::parse($date.' 12:00:00'));
$test->actingAs($test->f['receiver']);
return app(StockWorkflow::class)->record($test->f['receiver'], ['kind' => 'issue', 'reference' => 'HIST-'.Str::random(10), 'idempotency_key' => (string) Str::uuid(),
'responsible_id' => $test->f['receiver']->id, 'warehouse_id' => $test->f['warehouse']->id, 'stock_item_id' => $test->f['stock']->id, 'quantity' => $quantity, 'reason' => 'Consumo fictício para testar o período histórico.']);
}
function historyData(array $f): array
{
return ['stock_item_id' => $f['stock']->id, 'warehouse_id' => $f['warehouse']->id, 'purpose' => 'internal', 'period_from' => '2026-10-01', 'period_until' => '2026-10-30', 'quality' => 'complete', 'pattern' => 'regular'];
}
function approvedHistory($test, array $extra = []): SupplyRecord
{
$test->actingAs($test->f['issuer'])->postJson('/compras/fornecimento/history', supplyInput([...historyData($test->f), ...$extra]))->assertRedirect();
$draft = app(SupplyPlanning::class)->latest('history')->sole();
$test->actingAs($test->f['receiver'])->postJson('/compras/fornecimento/history', supplyInput(['confirm_scope' => true], 'approve', $draft))->assertRedirect();
return app(SupplyPlanning::class)->latest('history')->sole();
}
function historyPolicy($test, SupplyRecord $history, array $extra = []): SupplyRecord
{
$data = [...supplyPolicy($test->f), 'purpose' => 'internal', 'safety_milli' => 600000, 'minimum_stock_milli' => 600000, 'target_milli' => 900000, 'maximum_milli' => 1200000,
'forecast_method' => 'historical', 'history_key' => $history->record_key, 'history_min_days' => 30, 'history_max_age_days' => 90, 'daily_milli' => 999999, ...$extra];
$test->actingAs($test->f['issuer'])->postJson('/compras/fornecimento/policy', supplyInput($data))->assertRedirect();
$draft = app(SupplyPlanning::class)->latest('policy')->sole();
$test->actingAs($test->f['receiver'])->postJson('/compras/fornecimento/policy', supplyInput([], 'approve', $draft))->assertRedirect();
return app(SupplyPlanning::class)->latest('policy')->sole();
}
test('history source search finds an older approved period before the recent choice limit', function () {
$old = approvedHistory($this);
$this->actingAs($this->f['issuer']);
$draft = app(SupplyHistory::class)->save($this->f['issuer'], supplyInput([...historyData($this->f), 'period_from' => '2026-09-01', 'period_until' => '2026-09-30']));
$this->actingAs($this->f['receiver']);
$recent = app(SupplyHistory::class)->save($this->f['receiver'], supplyInput(['confirm_scope' => true], 'approve', $draft));
for ($i = 0; $i < 99; $i++) {
$copy = $recent->replicate();
$copy->record_key = (string) Str::uuid();
$copy->operation_key = (string) Str::uuid();
$copy->created_at = $recent->created_at;
$copy->save();
}
$query = '/compras/fornecimento?view=calendar&history_stock_item_id='.$this->f['stock']->id.'&history_warehouse_id='.$this->f['warehouse']->id.'&history_purpose=internal';
$before = [$old->fresh()->getAttributes(), DB::table('audit_logs')->count(), DB::table('stock_movements')->count()];
$this->actingAs($this->f['issuer'])->get('/compras/fornecimento?view=calendar')->assertInertia(fn (Assert $p) => $p->has('histories', 100)->where('histories', fn ($rows) => collect($rows)->where('key', $old->record_key)->isEmpty()));
$this->get($query.'&history_period_from=2026-10-01&history_period_until=2026-10-30')->assertInertia(fn (Assert $p) => $p->has('histories', 1)->where('histories.0.key', $old->record_key));
$this->get($query.'&history_period_until=2026-09-30&selected_history_key='.$old->record_key)->assertInertia(fn (Assert $p) => $p->has('histories', 101));
$this->get(str_replace('history_purpose=internal', 'history_purpose=resale', $query).'&selected_history_key='.$old->record_key)->assertInertia(fn (Assert $p) => $p->has('histories', 0));
expect([$old->fresh()->getAttributes(), DB::table('audit_logs')->count(), DB::table('stock_movements')->count()])->toBe($before);
$this->actingAs($this->f['receiver']);
app(SupplyHistory::class)->save($this->f['receiver'], supplyInput([], 'suspend', $old));
$this->actingAs($this->f['issuer'])->get($query.'&history_period_from=2026-10-01&selected_history_key='.$old->record_key)->assertInertia(fn (Assert $p) => $p->has('histories', 0));
});
test('history source search validates scope dates and preparation rights', function () {
$old = approvedHistory($this);
$query = '/compras/fornecimento?view=calendar&history_stock_item_id='.$this->f['stock']->id.'&history_warehouse_id='.$this->f['warehouse']->id.'&history_purpose=internal';
$this->actingAs($this->f['issuer'])->getJson($query.'&history_period_from=2026-10-30&history_period_until=2026-10-01')->assertUnprocessable();
$this->getJson('/compras/fornecimento?view=calendar&history_period_from=2026-10-01')->assertUnprocessable();
$tenant = Tenant::query()->create(['name' => 'Histórico isolado']);
$foreign = User::factory()->create(['tenant_id' => $tenant->id]);
supplyGrant($foreign, ['supply_prepare']);
$this->actingAs($foreign)->getJson($query.'&selected_history_key='.$old->record_key)->assertUnprocessable();
$this->actingAs($this->f['receiver'])->get($query)->assertInertia(fn (Assert $p) => $p->where('canPrepare', false)->has('histories', 0));
});
test('supply pagination preserves an approved historical source already selected outside the recent choices', function () {
$history = approvedHistory($this);
$policy = historyPolicy($this, $history);
for ($i = 0; $i < 100; $i++) {
$copy = $history->replicate();
$copy->record_key = (string) Str::uuid();
$copy->operation_key = (string) Str::uuid();
$copy->created_at = $history->created_at;
$copy->save();
}
$before = [$policy->fresh()->getAttributes(), $history->fresh()->getAttributes(), DB::table('audit_logs')->count()];
$this->actingAs($this->f['issuer'])->get('/compras/fornecimento?view=calendar&record_id='.$policy->id)->assertInertia(fn (Assert $page) => $page
->has('histories', 101)->where('histories', fn ($rows) => collect($rows)->where('key', $history->record_key)->count() === 1)
->where('records.0.data.history_key', $history->record_key));
expect([$policy->fresh()->getAttributes(), $history->fresh()->getAttributes(), DB::table('audit_logs')->count()])->toBe($before);
$this->actingAs($this->f['receiver']);
app(SupplyHistory::class)->save($this->f['receiver'], supplyInput([], 'suspend', $history));
$this->actingAs($this->f['issuer'])->get('/compras/fornecimento?view=calendar&record_id='.$policy->id)->assertInertia(fn (Assert $page) => $page
->has('histories', 100)->where('histories', fn ($rows) => collect($rows)->where('key', $history->record_key)->isEmpty())
->where('records.0.data.history_key', $history->record_key));
expect($policy->fresh()->getAttributes())->toBe($before[0])->and($history->fresh()->getAttributes())->toBe($before[1]);
});
test('historical mean includes all calendar days and policy reuses the approved period without accepting forged totals', function () {
$history = approvedHistory($this, ['total_milli' => 999999, 'daily_mean_milli' => '999']);
expect($history->snapshot['analysis']['total_milli'])->toBe(300000)->and($history->snapshot['analysis']['days'])->toBe(30)
->and($history->snapshot['analysis']['daily_mean_milli'])->toBe('10000.000000')->and($history->snapshot['analysis']['active_days'])->toBe(2);
$policy = historyPolicy($this, $history);
expect($policy->data['daily_milli'])->toBeNull()->and($policy->data['history_version'])->toBe($history->version);
$effective = app(SupplyHistory::class)->resolve($policy);
$projection = app(ReplenishmentProjection::class)->calculate($effective['data'], 900000, today()->toDateString());
expect($projection['method'])->toBe('historical_mean_calendar_days')->and($projection['daily_milli'])->toBe('10000.000000')->and($projection['series'][0]['demand_milli'])->toBe(10000);
$this->actingAs($this->f['issuer'])->get('/compras/fornecimento?view=calendar')->assertInertia(fn (Assert $page) => $page->where('projections.'.$policy->record_key.'.forecast_history.available', true));
});
test('transfers adjustments counts and receptions affect stock but never inflate consumption', function () {
$this->travelTo(CarbonImmutable::parse('2026-10-22 12:00:00'));
$this->actingAs($this->f['receiver']);
$other = Warehouse::query()->create(['tenant_id' => $this->f['tenant']->id, 'created_by' => $this->f['receiver']->id, 'code' => 'HIST-2', 'name' => 'Outro local do ensaio']);
$base = ['responsible_id' => $this->f['receiver']->id, 'warehouse_id' => $this->f['warehouse']->id, 'stock_item_id' => $this->f['stock']->id, 'reason' => 'Operação não representa consumo na análise.'];
app(StockWorkflow::class)->record($this->f['receiver'], [...$base, 'kind' => 'transfer', 'reference' => 'HIST-TRANS', 'idempotency_key' => (string) Str::uuid(), 'destination_warehouse_id' => $other->id, 'quantity' => '10.000']);
$version = app(StockWorkflow::class)->balance($this->f['stock']->id, $this->f['warehouse']->id)['version'];
$count = app(StockWorkflow::class)->record($this->f['receiver'], [...$base, 'kind' => 'count', 'reference' => 'HIST-COUNT', 'idempotency_key' => (string) Str::uuid(), 'quantity' => '905.000', 'ledger_version' => $version]);
app(StockWorkflow::class)->record($this->f['receiver'], [...$base, 'kind' => 'adjustment', 'reference' => 'HIST-ADJUST', 'idempotency_key' => (string) Str::uuid(), 'quantity' => '905.000', 'count_id' => $count->id]);
app(ReceiptWorkflow::class)->confirm($this->f['receiver'], $this->f['order'], receiptPayload($this->f, '50.000'));
$this->travelTo(CarbonImmutable::parse('2026-11-01 12:00:00'));
$analysis = app(SupplyHistory::class)->analyse($this->f['stock']->id, $this->f['warehouse']->id, '2026-10-01', '2026-10-30');
expect($analysis['total_milli'])->toBe(300000)->and($analysis['movement_types']['issue'])->toBe(2)->and($analysis['movement_types']['transfer_out'])->toBe(1)
->and($analysis['movement_types']['adjustment'])->toBe(1)->and($analysis['movement_types']['receipt_in'])->toBe(1);
});
test('unknown history remains unavailable whereas reviewed zero consumption is a genuine zero and known needs remain', function () {
$history = approvedHistory($this, ['period_from' => '2026-10-21']);
$policy = historyPolicy($this, $history, ['history_min_days' => 10]);
$effective = app(SupplyHistory::class)->resolve($policy);
$zero = app(ReplenishmentProjection::class)->calculate($effective['data'], 900000, today()->toDateString());
$unknown = app(ReplenishmentProjection::class)->calculate([...supplyPolicy($this->f), 'daily_milli' => null], 900000, today()->toDateString());
expect($zero['available'])->toBeTrue()->and($zero['daily_milli'])->toBe('0.000000')->and($zero['quantity_milli'])->toBe(0)->and($unknown['available'])->toBeFalse();
$known = app(ReplenishmentProjection::class)->calculate($effective['data'], 900000, today()->toDateString(), [['date' => today()->addDay()->toDateString(), 'quantity' => 350000, 'additional' => true]]);
expect($known['risk_date'])->toBe(today()->addDay()->toDateString())->and($known['quantity_milli'])->toBeGreaterThan(0);
});
test('fractional milliunit average is projected cumulatively instead of becoming false zero', function () {
historyIssue($this, '2026-10-28', '0.001');
$this->travelTo(CarbonImmutable::parse('2026-11-01 12:00:00'));
$history = approvedHistory($this, ['period_from' => '2026-10-28']);
$policy = historyPolicy($this, $history, ['history_min_days' => 3]);
$data = app(SupplyHistory::class)->resolve($policy)['data'];
$projection = app(ReplenishmentProjection::class)->calculate([...$data, 'horizon_days' => 3], 900000, today()->toDateString());
expect($projection['daily_milli'])->toBe('0.333333')->and(array_sum(array_column($projection['series'], 'demand_milli')))->toBe(1);
});
test('independent confirmation is mandatory and late historical entries invalidate rather than overwrite an approved analysis', function () {
$input = supplyInput(historyData($this->f));
$this->postJson('/compras/fornecimento/history', $input)->assertRedirect();
$this->postJson('/compras/fornecimento/history', $input)->assertRedirect();
$draft = app(SupplyPlanning::class)->latest('history')->sole();
supplyGrant($this->f['issuer'], ['supply_history_review']);
$this->postJson('/compras/fornecimento/history', supplyInput(['confirm_scope' => true], 'approve', $draft))->assertUnprocessable();
$this->actingAs($this->f['receiver'])->postJson('/compras/fornecimento/history', supplyInput([], 'approve', $draft))->assertUnprocessable();
$this->postJson('/compras/fornecimento/history', supplyInput(['confirm_scope' => true], 'approve', $draft))->assertRedirect();
$approved = app(SupplyPlanning::class)->latest('history')->sole();
$policy = historyPolicy($this, $approved);
historyIssue($this, '2026-10-25', '1.000');
$this->travelTo(CarbonImmutable::parse('2026-11-01 12:00:00'));
$result = app(SupplyHistory::class)->resolve($policy);
expect($result['history']['available'])->toBeFalse()->and($result['data']['daily_milli'])->toBeNull()->and($approved->fresh()->snapshot['analysis']['total_milli'])->toBe(300000);
});
test('insufficient coverage and nonregular patterns cannot silently feed a simple average', function (string $quality, string $pattern) {
$history = approvedHistory($this, ['quality' => $quality, 'pattern' => $pattern]);
$data = [...supplyPolicy($this->f), 'purpose' => 'internal', 'forecast_method' => 'historical', 'history_key' => $history->record_key, 'history_min_days' => 30, 'history_max_age_days' => 90];
$this->actingAs($this->f['issuer'])->postJson('/compras/fornecimento/policy', supplyInput($data))->assertUnprocessable();
})->with([['unknown', 'regular'], ['incomplete', 'regular'], ['stockout', 'regular'], ['complete', 'seasonal'], ['complete', 'intermittent']]);
test('minimum coverage purpose age and suspension remain explicit policy conditions', function () {
$history = approvedHistory($this);
$data = [...supplyPolicy($this->f), 'forecast_method' => 'historical', 'history_key' => $history->record_key, 'history_min_days' => 31, 'history_max_age_days' => 90];
$this->actingAs($this->f['issuer'])->postJson('/compras/fornecimento/policy', supplyInput([...$data, 'purpose' => 'internal']))->assertUnprocessable();
$this->postJson('/compras/fornecimento/policy', supplyInput([...$data, 'history_min_days' => 30]))->assertUnprocessable();
$policy = historyPolicy($this, $history);
$this->travelTo(CarbonImmutable::parse('2027-02-01 12:00:00'));
expect(app(SupplyHistory::class)->resolve($policy)['history']['available'])->toBeFalse();
$this->travelTo(CarbonImmutable::parse('2026-11-01 12:00:00'));
$this->actingAs($this->f['receiver'])->postJson('/compras/fornecimento/history', supplyInput([], 'suspend', $history))->assertRedirect();
expect(app(SupplyHistory::class)->resolve($policy)['history']['available'])->toBeFalse();
});
test('historical dates must be closed and the source ledger never crosses companies or changes through analysis', function () {
$before = StockMovement::query()->count();
$this->postJson('/compras/fornecimento/history', supplyInput([...historyData($this->f), 'period_until' => today()->toDateString()]))->assertUnprocessable();
$other = receiptFixture();
$this->actingAs($this->f['issuer'])->postJson('/compras/fornecimento/history', supplyInput([...historyData($this->f), 'stock_item_id' => $other['stock']->id]))->assertNotFound();
$this->get('/compras/fornecimento?view=history')->assertInertia(fn (Assert $page) => $page->component('supply/history')->where('canPrepare', true));
expect(StockMovement::query()->count())->toBe($before);
});
test('suggestions freeze the historical period and a suspended source cannot be used to convert an older approval', function () {
$history = approvedHistory($this);
$policy = historyPolicy($this, $history);
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' => 'Reutilização do catálogo no percurso histórico.']);
supplyGrant($this->f['issuer'], ['supply_suggest_prepare', 'supply_suggest_convert', 'requester']);
supplyGrant($this->f['receiver'], ['supply_suggest_review']);
$this->actingAs($this->f['issuer'])->postJson('/compras/fornecimento/suggestion', supplyInput(['policy_key' => $policy->record_key, 'unit_price' => '10.00']))->assertRedirect();
$suggestion = app(SupplyPlanning::class)->latest('suggestion')->sole();
expect($suggestion->snapshot['forecast_history']['version'])->toBe($history->version)->and($suggestion->snapshot['forecast_history']['analysis']['total_milli'])->toBe(300000);
$this->actingAs($this->f['receiver'])->postJson('/compras/fornecimento/suggestion', supplyInput([], 'approve', $suggestion))->assertRedirect();
$approved = app(SupplyPlanning::class)->latest('suggestion')->sole();
$this->postJson('/compras/fornecimento/history', supplyInput([], 'suspend', $history))->assertRedirect();
$this->actingAs($this->f['issuer'])->postJson('/compras/fornecimento/suggestion', supplyInput(['cost_center_id' => $this->f['r']->cost_center_id], 'convert', $approved))->assertUnprocessable();
expect($approved->fresh()->snapshot['forecast_history']['version'])->toBe($history->version)->and($approved->fresh()->state)->toBe('approved');
});
test('replacement policy can reuse all approved parameters by reference while activation remains independent and unique', function () {
$history = approvedHistory($this);
$source = historyPolicy($this, $history);
$this->actingAs($this->f['issuer'])->postJson('/compras/fornecimento/policy', supplyInput(['predecessor_key' => $source->record_key, 'predecessor_version' => $source->version]))->assertRedirect();
$copy = app(SupplyPlanning::class)->latest('policy')->where('record_key', '!=', $source->record_key)->sole();
expect($copy->data['target_milli'])->toBe($source->data['target_milli'])->and($copy->data['history_key'])->toBe($history->record_key)->and($copy->data['predecessor_version'])->toBe($source->version);
$this->actingAs($this->f['receiver'])->postJson('/compras/fornecimento/policy', supplyInput([], 'approve', $copy))->assertUnprocessable();
$this->postJson('/compras/fornecimento/policy', supplyInput([], 'suspend', $source))->assertRedirect();
$this->postJson('/compras/fornecimento/policy', supplyInput([], 'approve', $copy))->assertRedirect();
expect(app(SupplyPlanning::class)->latest('policy')->where('state', 'approved')->count())->toBe(1)->and($source->fresh()->data['target_milli'])->toBe(900000);
});
test('history task routes the prepared analysis to an independent reviewer without changing scope or ledger', function () {
$draft = app(SupplyHistory::class)->save($this->f['issuer'], supplyInput(historyData($this->f)));
supplyGrant($this->f['issuer'], ['supply_history_review']);
$path = '/compras/fornecimento?view=history&record_id='.$draft->id;
$this->get('/requisicoes/tarefas?module=supply')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0));
$this->get($path)->assertInertia(fn (Assert $page) => $page->where('records.0.canApprove', false));
$this->actingAs($this->f['receiver'])->get('/requisicoes/tarefas?module=supply')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 1)
->where('tasks.data.0.href', $path.'#supply-record-'.$draft->id)->where('tasks.data.0.due_on', null)->missing('tasks.data.0.snapshot'));
$before = [SupplyRecord::query()->count(), StockMovement::query()->count(), DB::table('audit_logs')->count()];
$this->get($path)->assertHeader('Cache-Control', 'max-age=0, no-store, private')->assertInertia(fn (Assert $page) => $page->has('records', 1)
->where('focusRecordId', $draft->id)->where('focusChanged', false)->where('records.0.canApprove', true)->where('records.0.canReturn', true));
expect([SupplyRecord::query()->count(), StockMovement::query()->count(), DB::table('audit_logs')->count()])->toBe($before);
$this->postJson('/compras/fornecimento/history', supplyInput(['confirm_scope' => false], 'approve', $draft))->assertUnprocessable();
$approved = app(SupplyHistory::class)->save($this->f['receiver'], supplyInput(['confirm_scope' => true], 'approve', $draft));
$this->get('/requisicoes/tarefas?module=supply')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0));
$this->get($path)->assertInertia(fn (Assert $page) => $page->where('focusChanged', true)->where('records.0.id', $approved->id)
->where('records.0.canApprove', false)->where('records.0.canSuspend', true));
});
test('history task excludes every previous preparer and uses the specific historical competence', function () {
$draft = app(SupplyHistory::class)->save($this->f['issuer'], supplyInput(historyData($this->f)));
supplyGrant($this->f['receiver'], ['supply_history_prepare']);
$this->actingAs($this->f['receiver']);
$updated = app(SupplyHistory::class)->save($this->f['receiver'], supplyInput(historyData($this->f), 'save', $draft));
supplyGrant($this->f['issuer'], ['supply_history_review']);
foreach ([$this->f['issuer'], $this->f['receiver']] as $user) {
$this->actingAs($user)->get('/requisicoes/tarefas?module=supply')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0));
expect(app(SupplyHistory::class)->canReview($user, $updated))->toBeFalse();
}
supplyGrant($this->f['otherReceiver'], ['supply_review']);
$this->actingAs($this->f['otherReceiver'])->get('/requisicoes/tarefas?module=supply')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0));
$this->get('/compras/fornecimento?view=history')->assertForbidden();
supplyGrant($this->f['otherReceiver'], ['supply_history_review']);
$this->get('/requisicoes/tarefas?module=supply')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 1)->where('tasks.data.0.task', 'history_review'));
expect(app(SupplyHistory::class)->canReview($this->f['otherReceiver'], $draft))->toBeFalse()
->and(app(SupplyHistory::class)->canReview($this->f['otherReceiver'], $updated))->toBeTrue();
});
test('history task keeps changed sources available for return and never approves stale analysis silently', function () {
$draft = app(SupplyHistory::class)->save($this->f['issuer'], supplyInput(historyData($this->f)));
$this->f['stock']->update(['name' => 'Nome alterado depois da análise']);
$this->actingAs($this->f['receiver'])->get('/requisicoes/tarefas?module=supply')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 1));
$this->postJson('/compras/fornecimento/history', supplyInput(['confirm_scope' => true], 'approve', $draft))->assertUnprocessable();
$returned = app(SupplyHistory::class)->save($this->f['receiver'], supplyInput([], 'return', $draft));
expect($returned->state)->toBe('returned')->and($returned->snapshot['item'])->toBe($draft->snapshot['item']);
});
test('history task preserves old pending analyses beyond the display cap and all task pages', function () {
$first = app(SupplyHistory::class)->save($this->f['issuer'], supplyInput(historyData($this->f)));
for ($index = 1; $index < 105; $index++) {
app(SupplyHistory::class)->save($this->f['issuer'], supplyInput(historyData($this->f)));
}
$this->actingAs($this->f['receiver'])->get('/requisicoes/tarefas?module=supply')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 105)->has('tasks.data', 25)->where('tasks.data.0.id', 'supply:'.$first->id.':review'));
$this->get('/requisicoes/tarefas?module=supply&page=5')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 105)->has('tasks.data', 5));
$this->get('/compras/fornecimento?view=history')->assertInertia(fn (Assert $page) => $page->has('records', 25)->where('recordPagination.total', 105));
$this->get('/compras/fornecimento?view=history&record_id='.$first->id)->assertInertia(fn (Assert $page) => $page->has('records', 1)->where('records.0.id', $first->id));
});
test('history task preserves company isolation document type maintenance and grant expiry', function () {
$draft = app(SupplyHistory::class)->save($this->f['issuer'], supplyInput(historyData($this->f)));
$path = '/compras/fornecimento?view=history&record_id='.$draft->id;
$tenant = Tenant::query()->create(['name' => 'Outra empresa de ensaio']);
$foreign = User::factory()->create(['tenant_id' => $tenant->id]);
supplyGrant($foreign, ['supply_history_review']);
$this->actingAs($foreign)->get('/requisicoes/tarefas?module=supply')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0));
$this->getJson($path)->assertUnprocessable();
$this->actingAs($this->f['receiver'])->getJson('/compras/fornecimento?view=calendar&record_id='.$draft->id)->assertUnprocessable();
DB::table('organization_memberships')->where('user_id', $this->f['receiver']->id)->update(['maintenance' => true, 'expires_at' => now()->addHour()]);
$this->get('/requisicoes/tarefas?module=supply')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0));
$this->get($path)->assertForbidden();
DB::table('organization_memberships')->where('user_id', $this->f['receiver']->id)->update(['expires_at' => now()->subMinute()]);
$this->get('/requisicoes/tarefas?module=supply')->assertForbidden();
});