This commit is contained in:
2026-11-03 11:51:44 +02:00
parent badaada043
commit e753f7a0a4
309 changed files with 14537 additions and 529 deletions
+136
View File
@@ -2,6 +2,8 @@
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;
@@ -10,6 +12,7 @@ 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;
@@ -63,6 +66,66 @@ function historyPolicy($test, SupplyRecord $history, array $extra = []): SupplyR
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)
@@ -185,3 +248,76 @@ test('replacement policy can reuse all approved parameters by reference while ac
$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();
});