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

202 lines
14 KiB
PHP

<?php
use App\Models\Requisition;
use App\Services\AutomaticPurchases;
use App\Services\ReplenishmentHeartbeat;
use App\Services\ReplenishmentQueue;
use App\Services\SupplyPlanning;
use App\Services\WarehousePilotReadiness;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use Inertia\Testing\AssertableInertia as Assert;
require_once __DIR__.'/../Support/SuggestionFixture.php';
beforeEach(function () {
$this->f = suggestionFixture();
app(SupplyPlanning::class)->save($this->f['receiver'], 'policy', supplyInput([], 'suspend', $this->f['policy']));
$this->actingAs($this->f['issuer']);
$policy = app(SupplyPlanning::class)->save($this->f['issuer'], 'policy', supplyInput([...supplyPolicy($this->f), 'purpose' => 'internal', 'agreement_key' => $this->f['agreement']->record_key]));
$this->actingAs($this->f['receiver']);
$this->f['policy'] = app(SupplyPlanning::class)->save($this->f['receiver'], 'policy', supplyInput([], 'approve', $policy));
supplyGrant($this->f['otherReceiver'], ['admin']);
$this->readinessOriginalStorage = app()->storagePath();
$this->readinessStorage = $this->readinessOriginalStorage.'/framework/testing/readiness-'.Str::uuid();
File::ensureDirectoryExists($this->readinessStorage.'/framework');
app()->useStoragePath($this->readinessStorage);
});
afterEach(function () {
app()->useStoragePath($this->readinessOriginalStorage);
File::deleteDirectory($this->readinessStorage);
});
function pilotReadinessRule($test): void
{
$test->actingAs($test->f['issuer']);
$rule = app(AutomaticPurchases::class)->save($test->f['issuer'], ['operation_key' => (string) Str::uuid(), 'version' => 0, 'action' => 'save', 'reason' => 'Parametrização sintética do diagnóstico de piloto.', 'data' => ['policy_key' => $test->f['policy']->record_key, 'requester_id' => $test->f['issuer']->id, 'cost_center_id' => $test->f['r']->cost_center_id, 'unit_price' => null, 'tax_rate' => null, 'needed_days' => 5, 'valid_until' => today()->addMonth()->toDateString()]]);
$test->actingAs($test->f['receiver']);
app(AutomaticPurchases::class)->save($test->f['receiver'], ['operation_key' => (string) Str::uuid(), 'record_key' => $rule->record_key, 'version' => 1, 'action' => 'approve', 'reason' => 'Activação independente da regra sintética conferida.', 'data' => []]);
}
test('pilot configuration export is private read only and does not include the account register', function () {
$counts = [];
foreach (['users', 'organization_memberships', 'organization_profiles', 'audit_logs', 'stock_movements', 'supply_records', 'requisitions'] as $table) {
$counts[$table] = DB::table($table)->count();
}
$this->actingAs($this->f['receiver'])->get('/stock/piloto.csv')->assertForbidden();
$response = $this->actingAs($this->f['otherReceiver'])->get('/stock/piloto.csv')->assertOk()->assertHeader('Content-Type', 'text/csv; charset=UTF-8');
$csv = $response->streamedContent();
expect($csv)->toContain('Conferido em')->toContain('A preparar')->toContain('SGC autónomo')->not->toContain($this->f['issuer']->name);
foreach ($counts as $table => $count) {
expect(DB::table($table)->count())->toBe($count);
}
});
test('pilot export renders formula-like text as text in spreadsheet readers', function () {
$this->mock(WarehousePilotReadiness::class)->shouldReceive('inspect')->once()->andReturn(['checked_at' => now()->toIso8601String(), 'checks' => [['label' => '=1+1', 'ok' => false, 'detail' => "\t@SUM(1)", 'path' => null]]]);
$response = $this->actingAs($this->f['otherReceiver'])->get('/stock/piloto.csv')->assertOk();
$csv = $response->streamedContent();
expect($csv)->toContain("'=1+1")->toContain("'\t@SUM(1)");
});
test('pilot diagnostics are read only and distinguish a viable synthetic warehouse scenario from client delivery and an unobserved scheduler', function () {
supplyGrant($this->f['issuer'], ['warehouse_request_prepare']);
supplyGrant($this->f['receiver'], ['warehouse_request_review']);
supplyGrant($this->f['otherReceiver'], ['warehouse', 'warehouse_request_deliver']);
pilotReadinessRule($this);
$counts = [];
foreach (['requisitions', 'warehouse_requests', 'warehouse_request_events', 'warehouse_reservations', 'stock_movements', 'audit_logs', 'supply_records', 'replenishment_work', 'organization_memberships', 'organization_profiles'] as $table) {
$counts[$table] = DB::table($table)->count();
}
$this->actingAs($this->f['otherReceiver'])->get('/stock/piloto')->assertOk()->assertHeader('Cache-Control', 'max-age=0, no-store, private')->assertInertia(fn (Assert $p) => $p->component('stock/pilot')->where('syntheticEvaluation', true)->where('report.warehouse_scenario_ready', true)->where('report.replenishment_scenario_ready', false)->where('report.client_configuration_complete', false)->where('report.checks.3.ok', true)->where('report.checks.4.ok', false)->has('report.participants.prepare', 1)->has('report.participants.deliver', 1));
foreach ($counts as $table => $count) {
expect(DB::table($table)->count())->toBe($count);
}
});
test('having all task labels among two people does not satisfy three-way independence', function () {
supplyGrant($this->f['issuer'], ['warehouse_request_prepare', 'warehouse_request_review', 'warehouse_request_deliver', 'warehouse']);
supplyGrant($this->f['receiver'], ['warehouse_request_review']);
$this->actingAs($this->f['otherReceiver'])->get('/stock/piloto')->assertOk()->assertInertia(fn (Assert $p) => $p->where('report.warehouse_scenario_ready', false)->where('report.checks.1.ok', false)->has('report.participants.prepare', 1)->has('report.participants.review', 2)->has('report.participants.deliver', 1));
});
test('rule inspection reuses generation authority and source validity without creating drafts', function () {
pilotReadinessRule($this);
$before = Requisition::query()->count();
DB::table('organization_memberships')->where('user_id', $this->f['issuer']->id)->update(['expires_at' => now()->subMinute()]);
$this->actingAs($this->f['otherReceiver'])->get('/stock/piloto')->assertOk()->assertInertia(fn (Assert $p) => $p->where('report.checks.3.ok', false));
expect(Requisition::query()->count())->toBe($before);
});
test('pilot diagnostics do not admit operational readers or expose accounts from another company', function () {
$this->actingAs($this->f['receiver'])->get('/stock/piloto')->assertForbidden();
$foreign = receiptFixture();
supplyGrant($foreign['issuer'], ['admin']);
$this->actingAs($foreign['issuer'])->get('/stock/piloto')->assertOk()->assertInertia(fn (Assert $p) => $p->has('report.registered', 3)->where('report.warehouse_scenario_ready', false)->where('report.checks.2.ok', false)->where('report.registered', fn ($people) => collect($people)->pluck('id')->sort()->values()->all() === collect([$foreign['issuer']->id, $foreign['receiver']->id, $foreign['otherReceiver']->id])->sort()->values()->all()));
});
test('stale catalogue and expired automatic configuration are reported instead of being treated as ready', function () {
pilotReadinessRule($this);
$this->f['stock']->update(['name' => 'Cadastro actualizado depois da publicação']);
$this->travel(32)->days();
$this->actingAs($this->f['otherReceiver'])->get('/stock/piloto')->assertOk()->assertInertia(fn (Assert $p) => $p->where('report.checks.2.ok', false)->where('report.checks.3.ok', false)->where('report.replenishment_scenario_ready', false));
});
test('optional scheduler telemetry is isolated and a stale or damaged file does not imply a running service', function () {
$originalStorage = $this->app->storagePath();
$originalEnv = $this->app->environment();
$temporary = $originalStorage.'/framework/testing/pilot-status-'.Str::uuid();
mkdir($temporary.'/framework', 0755, true);
$this->app->useStoragePath($temporary);
$this->app->instance('env', 'payable-testing');
$path = $temporary.'/framework/replenishment-status.json';
$this->actingAs($this->f['otherReceiver']);
try {
file_put_contents($path, '{bad-json');
expect(app(ReplenishmentQueue::class)->status($this->f['otherReceiver'])['schedulerStatus'])->toBeNull();
file_put_contents($path, json_encode(['result' => 'passed', 'finished_at' => now()->subHour()->toIso8601String()], JSON_THROW_ON_ERROR));
expect(app(ReplenishmentQueue::class)->status($this->f['otherReceiver'])['schedulerRecent'])->toBeFalse();
file_put_contents($path, json_encode(['result' => 'passed', 'finished_at' => now()->toIso8601String()], JSON_THROW_ON_ERROR));
expect(app(ReplenishmentQueue::class)->status($this->f['otherReceiver'])['schedulerRecent'])->toBeTrue();
} finally {
$this->app->useStoragePath($originalStorage);
$this->app->instance('env', $originalEnv);
unlink($path);
rmdir($temporary.'/framework');
rmdir($temporary);
}
});
test('native scheduler observation is available outside evaluation and takes precedence over helper telemetry', function () {
$originalStorage = app()->storagePath();
$originalEnv = app()->environment();
$temporary = $originalStorage.'/framework/testing/native-status-'.Str::uuid();
mkdir($temporary.'/framework', 0755, true);
app()->useStoragePath($temporary);
app()->instance('env', 'production');
$this->actingAs($this->f['otherReceiver']);
try {
app(ReplenishmentHeartbeat::class)->record(now()->toIso8601String(), 0);
$status = app(ReplenishmentQueue::class)->status($this->f['otherReceiver']);
expect($status['schedulerRecent'])->toBeTrue()->and($status['schedulerStatus']['result'])->toBe('passed');
app(ReplenishmentHeartbeat::class)->record(now()->toIso8601String(), 1);
file_put_contents($temporary.'/framework/replenishment-status.json', json_encode(['result' => 'passed', 'finished_at' => now()->toIso8601String()], JSON_THROW_ON_ERROR));
app()->instance('env', 'payable-testing');
expect(app(ReplenishmentQueue::class)->status($this->f['otherReceiver'])['schedulerStatus']['result'])->toBe('retry_pending');
file_put_contents($temporary.'/framework/replenishment-command-status.json', '{broken');
expect(app(ReplenishmentQueue::class)->status($this->f['otherReceiver'])['schedulerStatus'])->toBeNull();
} finally {
app()->useStoragePath($originalStorage);
app()->instance('env', $originalEnv);
File::deleteDirectory($temporary);
}
});
test('a recent successful scheduler does not hide durable work still awaiting recovery', function () {
pilotReadinessRule($this);
$this->actingAs($this->f['otherReceiver']);
DB::table('replenishment_work')->where('tenant_id', $this->f['tenant']->id)->update(['processed_generation' => DB::raw('generation'), 'failures' => 0]);
$originalStorage = $this->app->storagePath();
$originalEnv = $this->app->environment();
$temporary = $originalStorage.'/framework/testing/pilot-recovery-'.Str::uuid();
mkdir($temporary.'/framework', 0755, true);
$this->app->useStoragePath($temporary);
$this->app->instance('env', 'payable-testing');
$path = $temporary.'/framework/replenishment-status.json';
try {
file_put_contents($path, json_encode(['result' => 'passed', 'finished_at' => now()->toIso8601String()], JSON_THROW_ON_ERROR));
expect(app(WarehousePilotReadiness::class)->inspect($this->f['otherReceiver'])['replenishment_scenario_ready'])->toBeTrue();
DB::table('replenishment_work')->where('tenant_id', $this->f['tenant']->id)->update(['failures' => 1, 'due_at' => now()->addHour()]);
$report = app(WarehousePilotReadiness::class)->inspect($this->f['otherReceiver']);
expect($report['runtime']['schedulerRecent'])->toBeTrue()->and($report['replenishment_scenario_ready'])->toBeFalse()->and($report['checks'][6]['ok'])->toBeFalse();
} finally {
$this->app->useStoragePath($originalStorage);
$this->app->instance('env', $originalEnv);
unlink($path);
rmdir($temporary.'/framework');
rmdir($temporary);
}
});
test('command readiness does not certify a pilot missing receipt storage or the requisition version', function (string $missing) {
supplyGrant($this->f['issuer'], ['warehouse_request_prepare']);
supplyGrant($this->f['receiver'], ['warehouse_request_review']);
supplyGrant($this->f['otherReceiver'], ['warehouse', 'warehouse_request_deliver']);
pilotReadinessRule($this);
$this->actingAs($this->f['otherReceiver']);
expect(app(WarehousePilotReadiness::class)->inspect($this->f['otherReceiver'])['warehouse_scenario_ready'])->toBeTrue();
$counts = [DB::table('requisitions')->count(), DB::table('stock_movements')->count(), DB::table('audit_logs')->count()];
if ($missing === 'receipts') {
Schema::drop('requisition_commands');
} else {
Schema::table('requisitions', fn (Blueprint $table) => $table->dropColumn('process_version'));
}
$report = app(WarehousePilotReadiness::class)->inspect($this->f['otherReceiver']);
expect($report['checks'][0]['ok'])->toBeFalse()->and($report['warehouse_scenario_ready'])->toBeFalse()->and($report['replenishment_scenario_ready'])->toBeFalse()
->and($report['tables'][$missing === 'receipts' ? 'requisition_commands' : 'requisition_process_version'])->toBeFalse()
->and([DB::table('requisitions')->count(), DB::table('stock_movements')->count(), DB::table('audit_logs')->count()])->toBe($counts);
})->with(['receipts', 'version']);