146 lines
10 KiB
PHP
146 lines
10 KiB
PHP
<?php
|
|
|
|
use App\Models\Requisition;
|
|
use App\Services\AutomaticPurchases;
|
|
use App\Services\ReplenishmentQueue;
|
|
use App\Services\SupplyPlanning;
|
|
use App\Services\WarehousePilotReadiness;
|
|
use Illuminate\Support\Facades\DB;
|
|
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']);
|
|
});
|
|
|
|
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('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);
|
|
}
|
|
});
|