139 lines
11 KiB
PHP
139 lines
11 KiB
PHP
<?php
|
|
|
|
use App\Models\Requisition;
|
|
use App\Models\SupplyRecord;
|
|
use App\Services\RequisitionWorkflow;
|
|
use App\Services\SupplyPlanning;
|
|
use App\Services\SupplySuggestions;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Inertia\Testing\AssertableInertia as Assert;
|
|
|
|
require_once __DIR__.'/../Support/SuggestionFixture.php';
|
|
|
|
beforeEach(function () {
|
|
$this->f = suggestionFixture();
|
|
$this->actingAs($this->f['issuer']);
|
|
});
|
|
|
|
function saveSuggestion($test, array $extra = []): SupplyRecord
|
|
{
|
|
$test->actingAs($test->f['issuer'])->postJson('/compras/fornecimento/suggestion', supplyInput(['policy_key' => $test->f['policy']->record_key, ...$extra]))->assertRedirect();
|
|
|
|
return app(SupplyPlanning::class)->latest('suggestion')->sole();
|
|
}
|
|
|
|
function approveSuggestion($test, SupplyRecord $draft, array $extra = []): SupplyRecord
|
|
{
|
|
$test->actingAs($test->f['receiver'])->postJson('/compras/fornecimento/suggestion', supplyInput($extra, 'approve', $draft))->assertRedirect();
|
|
|
|
return app(SupplyPlanning::class)->latest('suggestion')->sole();
|
|
}
|
|
|
|
test('suggestion freezes the forecast catalogue agreement and company defaults without repeating descriptors', function () {
|
|
$input = supplyInput(['policy_key' => $this->f['policy']->record_key, 'quantity_milli' => null, 'name' => 'FORGED', 'snapshot' => ['balance' => 999]]);
|
|
$this->postJson('/compras/fornecimento/suggestion', $input)->assertRedirect();
|
|
$this->postJson('/compras/fornecimento/suggestion', $input)->assertRedirect();
|
|
$saved = app(SupplyPlanning::class)->latest('suggestion')->sole();
|
|
expect($saved->data['quantity_milli'])->toBe(84000)->and($saved->data['unit_price'])->toBe('10.00')->and($saved->data['price_source'])->toBe('agreement')
|
|
->and($saved->snapshot['item']['name'])->toBe($this->f['stock']->name)->and($saved->snapshot['balance']['quantity'])->toBe(120000)
|
|
->and($saved->snapshot['projection']['risk_date'])->toBe(today()->addDays(9)->toDateString());
|
|
$this->postJson('/compras/fornecimento/suggestion', supplyInput(['policy_key' => $this->f['policy']->record_key]))->assertUnprocessable();
|
|
$this->get('/compras/fornecimento?view=suggestions')->assertInertia(fn (Assert $page) => $page->component('supply/suggestions')->has('records', 1)->where('canPrepare', true)->where('canConvert', true));
|
|
});
|
|
|
|
test('independent review and a single conversion create an ordinary draft with server owned catalogue facts', function () {
|
|
$draft = saveSuggestion($this);
|
|
supplyGrant($this->f['issuer'], ['supply_suggest_review']);
|
|
$this->postJson('/compras/fornecimento/suggestion', supplyInput([], 'approve', $draft))->assertUnprocessable();
|
|
$approved = approveSuggestion($this, $draft);
|
|
supplyGrant($this->f['receiver'], ['supply_suggest_convert', 'requester']);
|
|
$this->postJson('/compras/fornecimento/suggestion', supplyInput(['cost_center_id' => $this->f['r']->cost_center_id], 'convert', $approved))->assertUnprocessable();
|
|
$input = supplyInput(['cost_center_id' => $this->f['r']->cost_center_id, 'quantity_milli' => 999, 'unit_price' => '0.01', 'description' => 'FORGED'], 'convert', $approved);
|
|
$before = Requisition::query()->count();
|
|
$this->actingAs($this->f['issuer'])->postJson('/compras/fornecimento/suggestion', $input)->assertRedirect();
|
|
$this->postJson('/compras/fornecimento/suggestion', $input)->assertRedirect();
|
|
$converted = app(SupplyPlanning::class)->latest('suggestion')->sole();
|
|
$request = Requisition::query()->whereKey($converted->data['requisition_id'])->firstOrFail();
|
|
$line = $request->items()->sole();
|
|
expect(Requisition::query()->count())->toBe($before + 1)->and($request->status)->toBe('draft')->and($request->requester_id)->toBe($this->f['issuer']->id)
|
|
->and($line->quantity)->toBe('84.000')->and($line->unit_price_minor)->toBe(1000)->and($line->description)->toBe($this->f['stock']->name)
|
|
->and($line->catalogue_snapshot['stock_item_id'])->toBe($this->f['stock']->id)->and($converted->snapshot['conversion']['requisition_created']['number'])->toBe($request->number);
|
|
$this->postJson('/compras/fornecimento/suggestion', supplyInput($input['data'], 'convert', $converted))->assertUnprocessable();
|
|
$this->postJson('/compras/fornecimento/suggestion', supplyInput(['policy_key' => $this->f['policy']->record_key]))->assertUnprocessable();
|
|
$this->get('/compras/fornecimento?view=suggestions')->assertInertia(fn (Assert $page) => $page->where('requestLinks.'.$request->id.'.id', $request->id));
|
|
$this->get('/requisicoes/'.$request->id)->assertInertia(fn (Assert $page) => $page->where('replenishmentSource.approval_version', $approved->version)->where('replenishmentSource.quantity_milli', 84000)->missing('replenishmentSource.supplier'));
|
|
});
|
|
|
|
test('changed source data blocks approval and conversion and refresh preserves the previous version', function () {
|
|
$draft = saveSuggestion($this);
|
|
$oldHash = $draft->snapshot['hash'];
|
|
$this->actingAs($this->f['receiver'])->postJson('/compras/fornecimento/reservation', supplyInput(['stock_item_id' => $this->f['stock']->id, 'warehouse_id' => $this->f['warehouse']->id,
|
|
'quantity_milli' => 1000, 'needed_on' => today()->addDay()->toDateString(), 'additional' => true, 'reference' => 'Nova necessidade conhecida']))->assertRedirect();
|
|
$this->postJson('/compras/fornecimento/suggestion', supplyInput([], 'approve', $draft))->assertUnprocessable();
|
|
$this->actingAs($this->f['issuer'])->postJson('/compras/fornecimento/suggestion', supplyInput(['policy_key' => $this->f['policy']->record_key], 'save', $draft))->assertRedirect();
|
|
$new = app(SupplyPlanning::class)->latest('suggestion')->sole();
|
|
expect($new->snapshot['hash'])->not->toBe($oldHash)->and($draft->fresh()->snapshot['hash'])->toBe($oldHash);
|
|
$approved = approveSuggestion($this, $new);
|
|
$this->f['stock']->update(['name' => 'Cadastro alterado depois da aprovação']);
|
|
$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()->state)->toBe('approved');
|
|
});
|
|
|
|
test('manual plans require acknowledgement and packaging and maximum constraints are enforced', function () {
|
|
$this->postJson('/compras/fornecimento/suggestion', supplyInput(['policy_key' => $this->f['policy']->record_key, 'quantity_milli' => 85000]))->assertUnprocessable();
|
|
$draft = saveSuggestion($this, ['quantity_milli' => 96000]);
|
|
$this->actingAs($this->f['receiver'])->postJson('/compras/fornecimento/suggestion', supplyInput([], 'approve', $draft))->assertUnprocessable();
|
|
$approved = approveSuggestion($this, $draft, ['confirm_exception' => true]);
|
|
expect($approved->data['exception_confirmed'])->toBeTrue();
|
|
$this->postJson('/compras/fornecimento/suggestion', supplyInput([], 'return', $approved))->assertRedirect();
|
|
$returned = app(SupplyPlanning::class)->latest('suggestion')->sole();
|
|
$this->actingAs($this->f['issuer'])->postJson('/compras/fornecimento/suggestion', supplyInput(['policy_key' => $this->f['policy']->record_key, 'quantity_milli' => 120000], 'save', $returned))->assertRedirect();
|
|
$excess = app(SupplyPlanning::class)->latest('suggestion')->sole();
|
|
expect(app(SupplySuggestions::class)->hasExcess($excess))->toBeTrue();
|
|
$this->actingAs($this->f['receiver'])->postJson('/compras/fornecimento/suggestion', supplyInput(['confirm_exception' => true], 'approve', $excess))->assertUnprocessable();
|
|
});
|
|
|
|
test('foreign agreement currency is never silently converted into a local requisition estimate', function () {
|
|
$f = suggestionFixture('USD');
|
|
$this->actingAs($f['issuer'])->postJson('/compras/fornecimento/suggestion', supplyInput(['policy_key' => $f['policy']->record_key]))->assertUnprocessable();
|
|
$this->postJson('/compras/fornecimento/suggestion', supplyInput(['policy_key' => $f['policy']->record_key, 'unit_price' => '650.00']))->assertRedirect();
|
|
$record = app(SupplyPlanning::class)->latest('suggestion')->sole();
|
|
expect($record->data['price_source'])->toBe('manual_estimate')->and($record->data['unit_price'])->toBe('650.00')->and($record->snapshot['agreement']['data']['currency'])->toBe('USD');
|
|
});
|
|
|
|
test('conversion is atomic with its journal even when final audit fails', function () {
|
|
$approved = approveSuggestion($this, saveSuggestion($this));
|
|
$this->actingAs($this->f['issuer']);
|
|
$before = Requisition::query()->count();
|
|
$this->partialMock(RequisitionWorkflow::class, function ($mock) {
|
|
$mock->shouldReceive('audit')->andReturnUsing(function ($actor, $action) {
|
|
if ($action === 'supply.suggestion.convert') {
|
|
throw new RuntimeException('Injected final audit failure.');
|
|
}
|
|
});
|
|
});
|
|
$this->withoutExceptionHandling();
|
|
expect(fn () => $this->postJson('/compras/fornecimento/suggestion', supplyInput(['cost_center_id' => $this->f['r']->cost_center_id], 'convert', $approved)))->toThrow(RuntimeException::class);
|
|
expect(Requisition::query()->count())->toBe($before)->and(app(SupplyPlanning::class)->latest('suggestion')->sole()->state)->toBe('approved');
|
|
});
|
|
|
|
test('tracking cannot be closed while the created request remains open and role revocation is effective', function () {
|
|
$approved = approveSuggestion($this, saveSuggestion($this));
|
|
$this->actingAs($this->f['issuer'])->postJson('/compras/fornecimento/suggestion', supplyInput(['cost_center_id' => $this->f['r']->cost_center_id], 'convert', $approved))->assertRedirect();
|
|
$converted = app(SupplyPlanning::class)->latest('suggestion')->sole();
|
|
$this->actingAs($this->f['receiver'])->postJson('/compras/fornecimento/suggestion', supplyInput([], 'close', $converted))->assertUnprocessable();
|
|
Requisition::query()->whereKey($converted->data['requisition_id'])->firstOrFail()->update(['status' => 'rejected']);
|
|
$this->postJson('/compras/fornecimento/suggestion', supplyInput([], 'close', $converted))->assertRedirect();
|
|
expect(app(SupplyPlanning::class)->latest('suggestion')->sole()->state)->toBe('closed');
|
|
DB::table('organization_memberships')->where('user_id', $this->f['issuer']->id)->update(['expires_at' => now()->subMinute()]);
|
|
$this->actingAs($this->f['issuer'])->postJson('/compras/fornecimento/suggestion', supplyInput(['policy_key' => $this->f['policy']->record_key]))->assertForbidden();
|
|
});
|
|
|
|
test('another company cannot resolve a suggestion or convert with another department cost center', function () {
|
|
$draft = saveSuggestion($this);
|
|
$other = suggestionFixture();
|
|
$this->actingAs($other['receiver'])->postJson('/compras/fornecimento/suggestion', supplyInput([], 'approve', $draft))->assertNotFound();
|
|
$approved = approveSuggestion($this, $draft);
|
|
$this->actingAs($this->f['issuer'])->postJson('/compras/fornecimento/suggestion', supplyInput(['cost_center_id' => $other['r']->cost_center_id], 'convert', $approved))->assertUnprocessable();
|
|
});
|