Files
compras/tests/Feature/OperationalGeographyTest.php
nala 63d20b10fc
tests / ci (push) Canceled after 0s
commit de armazens
2026-11-04 09:40:26 +02:00

186 lines
14 KiB
PHP

<?php
use App\Enums\PurchasingRole;
use App\Http\Middleware\EnsureOrganizationAccess;
use App\Models\AuditLog;
use App\Models\CriticalityLevel;
use App\Models\Department;
use App\Models\Equipment;
use App\Models\Tenant;
use App\Models\User;
use App\Models\Warehouse;
use App\Services\AdministrativeGeography;
use App\Services\ConflictGuard;
use App\Services\OrganizationAccess;
use App\Services\ReceiptWorkflow;
use App\Services\RequisitionWorkflow;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Inertia\Testing\AssertableInertia as Assert;
require_once __DIR__.'/../Support/ReceiptFixture.php';
function operationalAssetData(Equipment $asset): array
{
return $asset->refresh()->only(['id', 'version', 'code', 'description', 'area', 'serial_number', 'status', 'department_id', 'stock_item_id', 'criticality_level_id']);
}
beforeEach(function () {
$this->withoutVite();
config(['geography.enabled' => true, 'geography.operational_enabled' => true]);
$this->f = receiptFixture();
$this->actingAs($this->f['receiver']);
$level = CriticalityLevel::query()->create(['tenant_id' => $this->f['tenant']->id, 'created_by' => $this->f['receiver']->id, 'code' => 'CRIT', 'name' => 'Categoria sintética', 'criteria' => 'Critério sintético de teste', 'is_critical' => false, 'created_at' => now()]);
$this->asset = Equipment::query()->create(['tenant_id' => $this->f['tenant']->id, 'created_by' => $this->f['receiver']->id, 'department_id' => $this->f['department']->id, 'criticality_level_id' => $level->id, 'code' => 'EQ-GEO', 'description' => 'Equipamento sintético', 'area' => 'Área textual original', 'status' => 'active']);
$this->reference = ['country' => 'MZ', 'catalogue' => config('geography.source_sha256'), 'record_id' => '2'];
$this->warehouseUrl = '/stock/armazens/'.$this->f['warehouse']->id.'/localizacao';
});
test('operational geography creates warehouse and equipment without movements or implicit locations', function () {
$this->post('/stock/armazens', ['code' => 'GEO', 'name' => 'Armazém geográfico', 'geography_reference' => $this->reference])->assertSessionHasNoErrors();
$warehouse = Warehouse::query()->where('code', 'GEO')->sole();
expect($warehouse->geography_reference)->toBe($this->reference);
$data = operationalAssetData($this->asset);
unset($data['id'], $data['version']);
$this->post('/equipamentos/activos', [...$data, 'code' => 'EQ-NOVO', 'geography_reference' => $this->reference])->assertSessionHasNoErrors();
expect(Equipment::query()->where('code', 'EQ-NOVO')->sole()->geography_reference)->toBe($this->reference);
$this->post('/stock/armazens', ['code' => 'SEM-GEO', 'name' => 'Sem classificação'])->assertSessionHasNoErrors();
expect(Warehouse::query()->where('code', 'SEM-GEO')->sole()->geography_reference)->toBeNull();
expect(DB::table('stock_movements')->count())->toBe(0)->and(DB::table('purchase_orders')->count())->toBe(1);
});
test('operational geography warehouse update is isolated audited and rejects stale overwrite', function () {
$before = $this->f['warehouse']->refresh()->only(['code', 'name', 'created_by']);
$payload = ['geography_reference' => $this->reference, 'expected_reference' => null, 'name' => 'Não aplicar', 'tenant_id' => 999];
$this->post($this->warehouseUrl, $payload)->assertSessionHasNoErrors();
expect($this->f['warehouse']->refresh()->only(array_keys($before)))->toBe($before)->and($this->f['warehouse']->geography_reference)->toBe($this->reference);
$this->post($this->warehouseUrl, ['geography_reference' => null, 'expected_reference' => null])->assertSessionHasErrors('geography_reference');
$this->post($this->warehouseUrl, ['geography_reference' => $this->reference, 'expected_reference' => $this->reference])->assertSessionHasNoErrors();
expect(AuditLog::query()->where('action', 'stock.warehouse_geography_updated')->count())->toBe(1);
$this->post($this->warehouseUrl, ['geography_reference' => null, 'expected_reference' => $this->reference])->assertSessionHasNoErrors();
expect($this->f['warehouse']->refresh()->geography_reference)->toBeNull();
});
test('operational geography warehouse changes require native role and company ownership', function () {
$this->actingAs($this->f['issuer'])->post($this->warehouseUrl, ['geography_reference' => $this->reference, 'expected_reference' => null])->assertForbidden();
$this->actingAs($this->f['receiver']);
$foreign = Tenant::query()->create(['name' => 'Outra empresa']);
$warehouse = Warehouse::query()->create(['tenant_id' => $foreign->id, 'created_by' => $this->f['receiver']->id, 'code' => 'OUTRO', 'name' => 'Não visível']);
$this->post('/stock/armazens/'.$warehouse->id.'/localizacao', ['geography_reference' => $this->reference, 'expected_reference' => null])->assertNotFound();
});
test('operational geography revalidates current warehouse authority after company locking', function () {
$actor = $this->f['receiver'];
$this->partialMock(ConflictGuard::class)->shouldReceive('lockCompany')->once()->andReturnUsing(function () use ($actor) {
DB::table('users')->where('id', $actor->id)->update(['purchasing_role' => PurchasingRole::Finance->value]);
});
$this->post($this->warehouseUrl, ['geography_reference' => $this->reference, 'expected_reference' => null])->assertForbidden();
expect($this->f['warehouse']->refresh()->geography_reference)->toBeNull();
});
test('operational geography warehouse audit failure rolls back the reference', function () {
$this->partialMock(RequisitionWorkflow::class)->shouldReceive('audit')->andThrow(new RuntimeException('Synthetic audit failure'));
$this->withoutExceptionHandling();
expect(fn () => $this->post($this->warehouseUrl, ['geography_reference' => $this->reference, 'expected_reference' => null]))->toThrow(RuntimeException::class, 'Synthetic audit failure');
expect($this->f['warehouse']->refresh()->geography_reference)->toBeNull();
});
test('operational geography invalid ambiguous or stale references never change the operational registries', function () {
$service = app(AdministrativeGeography::class);
$bad = collect($service->catalogue()['records'])->first(fn ($record) => in_array('duplicate_code_path', $record['issues'], true));
foreach ([['record_id' => '999999'], ['record_id' => $bad['id']], ['catalogue' => str_repeat('0', 64)]] as $change) {
$reference = [...$this->reference, ...$change];
$this->post($this->warehouseUrl, ['geography_reference' => $reference, 'expected_reference' => null])->assertSessionHasErrors('geography_reference');
$this->post('/equipamentos/activos', [...operationalAssetData($this->asset), 'geography_reference' => $reference])->assertSessionHasErrors('geography_reference');
}
expect($this->asset->refresh()->geography_reference)->toBeNull()->and($this->f['warehouse']->refresh()->geography_reference)->toBeNull();
});
test('operational geography equipment updates retain native version checks area and approved documents', function () {
$before = [];
foreach (['requisitions', 'requisition_items', 'approval_steps', 'purchase_orders', 'purchase_order_items'] as $table) {
$before[$table] = DB::table($table)->orderBy('id')->get()->toArray();
}
$payload = operationalAssetData($this->asset);
$this->post('/equipamentos/activos', [...$payload, 'geography_reference' => $this->reference])->assertSessionHasNoErrors();
expect($this->asset->refresh()->geography_reference)->toBe($this->reference)->and($this->asset->area)->toBe('Área textual original')->and($this->asset->version)->toBe(2);
$this->post('/equipamentos/activos', [...$payload, 'geography_reference' => null])->assertSessionHasErrors('version');
$this->post('/equipamentos/activos', operationalAssetData($this->asset))->assertSessionHasNoErrors();
expect($this->asset->refresh()->geography_reference)->toBe($this->reference);
foreach ($before as $table => $rows) {
expect(DB::table($table)->orderBy('id')->get()->toArray())->toEqual($rows);
}
$this->get('/equipamentos/'.$this->asset->id)->assertInertia(fn (Assert $page) => $page->where('geography.path.0.name', 'Niassa'));
$this->post('/equipamentos/activos', [...operationalAssetData($this->asset), 'geography_reference' => null])->assertSessionHasNoErrors();
expect($this->asset->refresh()->geography_reference)->toBeNull();
});
test('operational geography warehouse filter includes descendants and does not rewrite receipt snapshots', function () {
app(ReceiptWorkflow::class)->confirm($this->f['receiver'], $this->f['order'], receiptPayload($this->f));
$before = DB::table('stock_movements')->orderBy('id')->get()->toArray();
$child = collect(app(AdministrativeGeography::class)->catalogue()['records'])->first(fn ($record) => $record['selectable'] && $record['level'] > 1 && $record['codes'][0] === '01');
$this->post($this->warehouseUrl, ['geography_reference' => [...$this->reference, 'record_id' => $child['id']], 'expected_reference' => null])->assertSessionHasNoErrors();
$this->get('/stock?location=2')->assertInertia(fn (Assert $page) => $page->has('warehouses', 1)->has('movements.data', 1)->has('balances', 1));
$this->get('/stock?location=999999')->assertSessionHasErrors('geography_reference');
expect(DB::table('stock_movements')->orderBy('id')->get()->toArray())->toEqual($before);
});
test('operational geography equipment filter retains tenant and department scope', function () {
$this->asset->update(['geography_reference' => $this->reference]);
$otherDepartment = Department::query()->create(['tenant_id' => $this->f['tenant']->id, 'name' => 'Outra área']);
$data = $this->asset->only(['tenant_id', 'created_by', 'description', 'area', 'status', 'criticality_level_id']);
Equipment::query()->create([...$data, 'department_id' => $otherDepartment->id, 'code' => 'FORA-AREA', 'geography_reference' => $this->reference]);
$otherTenant = Tenant::query()->create(['name' => 'Outra empresa']);
Equipment::query()->create([...$data, 'tenant_id' => $otherTenant->id, 'department_id' => $otherDepartment->id, 'code' => 'FORA-EMPRESA', 'geography_reference' => $this->reference]);
$head = User::factory()->create(['tenant_id' => $this->f['tenant']->id, 'department_id' => $this->f['department']->id, 'purchasing_role' => PurchasingRole::DepartmentHead]);
$this->actingAs($head)->get('/equipamentos?location=2')->assertInertia(fn (Assert $page) => $page->has('equipment', 1)->where('equipment.0.id', $this->asset->id));
});
test('operational geography missing columns hide only the new controls and preserve legacy creation', function () {
foreach (['warehouses', 'equipment'] as $table) {
Schema::table($table, fn ($schema) => $schema->dropColumn('geography_reference'));
}
$this->get('/stock')->assertInertia(fn (Assert $page) => $page->where('geographyEnabled', false));
$this->get('/equipamentos')->assertInertia(fn (Assert $page) => $page->where('geographyEnabled', false));
$this->actingAs($this->f['issuer'])->get('/fornecedores')->assertInertia(fn (Assert $page) => $page->where('geographyEnabled', true));
$this->actingAs($this->f['receiver']);
$this->post('/stock/armazens', ['code' => 'LEGACY', 'name' => 'Cadastro sem nova coluna'])->assertSessionHasNoErrors();
$this->post($this->warehouseUrl, ['geography_reference' => $this->reference, 'expected_reference' => null])->assertServiceUnavailable();
$this->post('/equipamentos/activos', [...operationalAssetData($this->asset), 'geography_reference' => $this->reference])->assertSessionHasErrors('geography_reference');
});
test('operational geography maintenance access cannot mutate either registry even without route middleware', function () {
$this->withoutMiddleware(EnsureOrganizationAccess::class);
$this->partialMock(OrganizationAccess::class)->shouldReceive('maintenanceOnly')->andReturn(true);
$this->post($this->warehouseUrl, ['geography_reference' => $this->reference, 'expected_reference' => null])->assertForbidden();
$this->post('/equipamentos/activos', [...operationalAssetData($this->asset), 'geography_reference' => $this->reference])->assertForbidden();
expect($this->f['warehouse']->refresh()->geography_reference)->toBeNull()->and($this->asset->refresh()->geography_reference)->toBeNull();
});
test('operational geography equipment audit failure rolls back version and location together', function () {
$before = operationalAssetData($this->asset);
$this->partialMock(RequisitionWorkflow::class)->shouldReceive('audit')->andThrow(new RuntimeException('Synthetic equipment audit failure'));
$this->withoutExceptionHandling();
expect(fn () => $this->post('/equipamentos/activos', [...$before, 'geography_reference' => $this->reference]))->toThrow(RuntimeException::class, 'Synthetic equipment audit failure');
expect($this->asset->refresh()->geography_reference)->toBeNull()->and($this->asset->version)->toBe($before['version']);
});
test('operational geography revalidates current equipment authority before saving', function () {
$actor = $this->f['receiver'];
$this->partialMock(ConflictGuard::class)->shouldReceive('lockCompany')->once()->andReturnUsing(function () use ($actor) {
DB::table('users')->where('id', $actor->id)->update(['purchasing_role' => PurchasingRole::Finance->value]);
});
$this->post('/equipamentos/activos', [...operationalAssetData($this->asset), 'geography_reference' => $this->reference])->assertForbidden();
expect($this->asset->refresh()->geography_reference)->toBeNull();
});
test('operational geography can be suspended without disabling company supplier or catalogue selection', function () {
config(['geography.operational_enabled' => false]);
expect(app(AdministrativeGeography::class)->enabled())->toBeTrue();
$this->getJson('/api/divisao-administrativa')->assertOk()->assertJsonCount(11, 'data');
$this->get('/stock')->assertInertia(fn (Assert $page) => $page->where('geographyEnabled', false));
$this->get('/equipamentos')->assertInertia(fn (Assert $page) => $page->where('geographyEnabled', false));
$this->post($this->warehouseUrl, ['geography_reference' => $this->reference, 'expected_reference' => null])->assertServiceUnavailable();
$this->post('/equipamentos/activos', [...operationalAssetData($this->asset), 'geography_reference' => $this->reference])->assertSessionHasErrors('geography_reference');
});