commit21
This commit is contained in:
@@ -1,17 +1,21 @@
|
||||
<?php
|
||||
|
||||
use App\Models\InventoryLotAllocation;
|
||||
use App\Models\StockItem;
|
||||
use App\Models\StockMovement;
|
||||
use App\Models\SupplyRecord;
|
||||
use App\Models\User;
|
||||
use App\Models\Warehouse;
|
||||
use App\Services\InventoryLots;
|
||||
use App\Services\ReceiptWorkflow;
|
||||
use App\Services\ReplenishmentProjection;
|
||||
use App\Services\StockWorkflow;
|
||||
use App\Services\WorkflowTaskInbox;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
|
||||
require_once __DIR__.'/../Support/ReceiptFixture.php';
|
||||
require_once __DIR__.'/../Support/SupplyFixture.php';
|
||||
@@ -41,6 +45,13 @@ function lotContext($test): array
|
||||
return app(InventoryLots::class)->context($test->f['stock']->id, $test->f['warehouse']->id);
|
||||
}
|
||||
|
||||
function lotQualityTasks(object $test, User $actor): array
|
||||
{
|
||||
$test->actingAs($actor);
|
||||
|
||||
return array_values(array_filter(app(WorkflowTaskInbox::class)->build($actor, 'procurement'), fn ($row) => str_starts_with($row['id'], 'lot-quality:')));
|
||||
}
|
||||
|
||||
function lotDecision($test, array $lot, string $action = 'release', $actor = null, ?int $warehouse = null): SupplyRecord
|
||||
{
|
||||
$actor ??= $test->f['receiver'];
|
||||
@@ -332,3 +343,270 @@ test('later identification corrections preserve the metadata actually used in th
|
||||
->and(lotContext($this)['usable_milli'])->toBe(0)
|
||||
->and(app(InventoryLots::class)->context($this->f['stock']->id, $destination->id)['usable_milli'])->toBe(0);
|
||||
});
|
||||
function taskLotDraft($test, array $extra = [], ?SupplyRecord $previous = null): SupplyRecord
|
||||
{
|
||||
$test->actingAs($test->f['issuer']);
|
||||
|
||||
return app(InventoryLots::class)->save($test->f['issuer'], supplyInput(['stock_item_id' => $test->f['stock']->id, 'warehouse_id' => $test->f['warehouse']->id,
|
||||
'requires_expiry' => true, 'minimum_remaining_days' => 0, 'lots' => [], ...$extra], 'prepare', $previous));
|
||||
}
|
||||
|
||||
test('lot setup task connects independent activation without changing physical stock or releasing quality', function () {
|
||||
$draft = taskLotDraft($this, ['lots' => [['code' => 'TASK-LOT', 'expires_on' => today()->addDays(20)->toDateString()]]]);
|
||||
supplyGrant($this->f['issuer'], ['inventory_lot_review']);
|
||||
$path = '/stock/lotes?record_id='.$draft->id;
|
||||
$this->get('/requisicoes/tarefas?module=warehouse')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0));
|
||||
$this->get($path)->assertInertia(fn (Assert $page) => $page->where('records.0.canActivate', false));
|
||||
$this->actingAs($this->f['receiver'])->get('/requisicoes/tarefas?module=warehouse')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 1)
|
||||
->where('tasks.data.0.task', 'lot_setup_activate')->where('tasks.data.0.href', $path.'#lot-setup-'.$draft->id)->missing('tasks.data.0.snapshot'));
|
||||
$before = [SupplyRecord::query()->count(), StockMovement::query()->count(), InventoryLotAllocation::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('records.0.canActivate', true));
|
||||
expect([SupplyRecord::query()->count(), StockMovement::query()->count(), InventoryLotAllocation::query()->count(), DB::table('audit_logs')->count()])->toBe($before);
|
||||
$command = supplyInput([], 'activate', $draft);
|
||||
$this->postJson('/stock/lotes', $command)->assertRedirect();
|
||||
$this->postJson('/stock/lotes', $command)->assertRedirect();
|
||||
$context = lotContext($this);
|
||||
expect($context['physical']['quantity'])->toBe(10000)->and($context['usable_milli'])->toBe(0)->and($context['lots'][0]['state'])->toBe('held')
|
||||
->and(StockMovement::query()->count())->toBe($before[1])->and(InventoryLotAllocation::query()->count())->toBe(1);
|
||||
$this->get('/requisicoes/tarefas?module=warehouse')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 1)->where('tasks.data.0.task', 'lot_release'));
|
||||
$this->get($path)->assertInertia(fn (Assert $page) => $page->where('focusChanged', true)->where('records.0.state', 'active')->where('records.0.canActivate', false));
|
||||
});
|
||||
|
||||
test('lot setup task rejects a stale balance and reuses the revised preparation at the old link', function () {
|
||||
$draft = taskLotDraft($this);
|
||||
lotIssue($this, '1.000');
|
||||
$this->get('/requisicoes/tarefas?module=warehouse')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 1));
|
||||
$this->postJson('/stock/lotes', supplyInput([], 'activate', $draft))->assertUnprocessable();
|
||||
expect(InventoryLotAllocation::query()->count())->toBe(0);
|
||||
$updated = taskLotDraft($this, [], $draft);
|
||||
$this->actingAs($this->f['receiver'])->get('/stock/lotes?record_id='.$draft->id)->assertInertia(fn (Assert $page) => $page->where('focusChanged', true)->where('records.0.id', $updated->id));
|
||||
$this->postJson('/stock/lotes', supplyInput([], 'activate', $updated))->assertRedirect();
|
||||
expect(lotContext($this)['physical']['quantity'])->toBe(9000);
|
||||
});
|
||||
|
||||
test('lot setup task preserves active configuration while an independently prepared revision awaits review', function () {
|
||||
$active = lotSetup($this, [['code' => 'BASE-TASK', 'expires_on' => today()->addDays(20)->toDateString()]]);
|
||||
$counts = [StockMovement::query()->count(), InventoryLotAllocation::query()->count()];
|
||||
$this->actingAs($this->f['otherReceiver']);
|
||||
$draft = app(InventoryLots::class)->save($this->f['otherReceiver'], supplyInput([...$active->data, 'minimum_remaining_days' => 5, 'lots' => []], 'prepare', $active));
|
||||
supplyGrant($this->f['issuer'], ['inventory_lot_review']);
|
||||
supplyGrant($this->f['otherReceiver'], ['inventory_lot_review']);
|
||||
expect(app(InventoryLots::class)->canActivate($this->f['issuer'], $draft))->toBeFalse()
|
||||
->and(app(InventoryLots::class)->canActivate($this->f['otherReceiver'], $draft))->toBeFalse();
|
||||
$this->actingAs($this->f['receiver'])->get('/stock/lotes?record_id='.$draft->id)->assertInertia(fn (Assert $page) => $page->has('records', 2)
|
||||
->where('records.0.id', $draft->id)->where('records.1.id', $active->id)->where('contexts.'.$active->record_key.'.setup_id', $active->id));
|
||||
$this->get('/requisicoes/tarefas?module=warehouse')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 2)
|
||||
->where('tasks.data', fn ($rows) => collect($rows)->pluck('task')->sort()->values()->all() === ['lot_release', 'lot_setup_activate']));
|
||||
$this->postJson('/stock/lotes', supplyInput([], 'activate', $draft))->assertRedirect();
|
||||
expect([StockMovement::query()->count(), InventoryLotAllocation::query()->count()])->toBe($counts)
|
||||
->and($active->fresh()->data['minimum_remaining_days'])->toBe(0)->and(app(InventoryLots::class)->setup($active->stock_item_id, $active->warehouse_id)->data['minimum_remaining_days'])->toBe(5);
|
||||
});
|
||||
|
||||
test('lot setup task retains older pending setups past the overview limit', function () {
|
||||
$first = taskLotDraft($this);
|
||||
for ($index = 1; $index < 105; $index++) {
|
||||
taskLotDraft($this);
|
||||
}
|
||||
$this->actingAs($this->f['receiver'])->get('/requisicoes/tarefas?module=warehouse')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 105)->has('tasks.data', 25)
|
||||
->where('tasks.data.0.id', 'lot-setup:'.$first->id.':activate'));
|
||||
$this->get('/requisicoes/tarefas?module=warehouse&page=5')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 105)->has('tasks.data', 5));
|
||||
$this->get('/stock/lotes')->assertInertia(fn (Assert $page) => $page->has('records', 25)->where('controlPagination.total', 105));
|
||||
$this->get('/stock/lotes?controls_page=5')->assertInertia(fn (Assert $page) => $page->has('records', 5)->where('records.4.id', $first->id));
|
||||
$this->get('/stock/lotes?record_id='.$first->id)->assertInertia(fn (Assert $page) => $page->has('records', 1)->where('records.0.id', $first->id));
|
||||
});
|
||||
|
||||
test('lot setup task preserves company maintenance and expiry boundaries', function () {
|
||||
$draft = taskLotDraft($this);
|
||||
$path = '/stock/lotes?record_id='.$draft->id;
|
||||
$foreign = receiptFixture();
|
||||
supplyGrant($foreign['receiver'], ['inventory_lot_review']);
|
||||
$this->actingAs($foreign['receiver'])->getJson($path)->assertUnprocessable();
|
||||
$this->get('/requisicoes/tarefas?module=warehouse')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0));
|
||||
DB::table('organization_memberships')->where('user_id', $this->f['receiver']->id)->update(['maintenance' => true, 'expires_at' => now()->addHour()]);
|
||||
$this->actingAs($this->f['receiver'])->get('/requisicoes/tarefas?module=warehouse')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0));
|
||||
$this->get($path)->assertForbidden();
|
||||
expect(app(InventoryLots::class)->canActivate($this->f['receiver'], $draft))->toBeFalse();
|
||||
DB::table('organization_memberships')->where('user_id', $this->f['receiver']->id)->update(['expires_at' => now()->subMinute()]);
|
||||
$this->get('/requisicoes/tarefas?module=warehouse')->assertForbidden();
|
||||
});
|
||||
test('lot setup task does not keep an obsolete competing preparation actionable after another control is activated', function () {
|
||||
$older = taskLotDraft($this);
|
||||
$winner = taskLotDraft($this);
|
||||
$this->actingAs($this->f['receiver'])->postJson('/stock/lotes', supplyInput([], 'activate', $winner))->assertRedirect();
|
||||
$this->postJson('/stock/lotes', supplyInput([], 'activate', $older))->assertUnprocessable();
|
||||
$this->get('/requisicoes/tarefas?module=warehouse')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0));
|
||||
$this->get('/stock/lotes?record_id='.$older->id)->assertInertia(fn (Assert $page) => $page->where('records.0.canActivate', false));
|
||||
expect($older->fresh()->state)->toBe('draft')->and(app(InventoryLots::class)->setup($older->stock_item_id, $older->warehouse_id)->record_key)->toBe($winner->record_key);
|
||||
});
|
||||
|
||||
test('lot quality task connects identification and independent release without changing physical stock', function () {
|
||||
lotSetup($this);
|
||||
$lot = lotContext($this)['lots'][0];
|
||||
$tasks = lotQualityTasks($this, $this->f['issuer']);
|
||||
expect($tasks)->toHaveCount(1)->and($tasks[0]['task'])->toBe('lot_identify')->and($tasks[0]['due_on'])->toBeNull();
|
||||
$this->get($tasks[0]['href'])->assertInertia(fn (Assert $page) => $page->where('focusLotId', $lot['id'])->where('contexts.'.app(InventoryLots::class)->setup($this->f['stock']->id, $this->f['warehouse']->id)->record_key.'.lots.0.canIdentify', true));
|
||||
$metadata = SupplyRecord::query()->findOrFail($lot['metadata_id']);
|
||||
$before = $metadata->getAttributes();
|
||||
app(InventoryLots::class)->save($this->f['issuer'], supplyInput(['code' => 'IDENTIFIED-QA', 'expires_on' => today()->addDays(10)->toDateString()], 'identify', $metadata));
|
||||
expect(lotQualityTasks($this, $this->f['issuer']))->toBe([]);
|
||||
$tasks = lotQualityTasks($this, $this->f['receiver']);
|
||||
expect($tasks)->toHaveCount(1)->and($tasks[0]['task'])->toBe('lot_release');
|
||||
lotDecision($this, lotContext($this)['lots'][0]);
|
||||
expect(lotQualityTasks($this, $this->f['receiver']))->toBe([])->and(lotContext($this)['usable_milli'])->toBe(10000)
|
||||
->and(lotContext($this)['physical']['quantity'])->toBe(10000)->and($metadata->fresh()->getAttributes())->toBe($before);
|
||||
});
|
||||
|
||||
test('lot quality task excludes receipt contributors even when release permission is accumulated', function () {
|
||||
lotSetup($this, [['code' => 'BASE', 'expires_on' => today()->addDays(10)->toDateString()]]);
|
||||
lotDecision($this, lotContext($this)['lots'][0]);
|
||||
$data = receiptPayload($this->f, '2.000');
|
||||
$data['items'][0]['lots'] = [['code' => 'RECEIVED-QA', 'expires_on' => today()->addDays(10)->toDateString()]];
|
||||
app(ReceiptWorkflow::class)->confirm($this->f['receiver'], $this->f['order'], $data);
|
||||
expect(lotQualityTasks($this, $this->f['receiver']))->toBe([]);
|
||||
$tasks = lotQualityTasks($this, $this->f['otherReceiver']);
|
||||
expect($tasks)->toHaveCount(1)->and($tasks[0]['task'])->toBe('lot_release');
|
||||
$lot = collect(lotContext($this)['lots'])->firstWhere('data.code', 'RECEIVED-QA');
|
||||
lotDecision($this, $lot, 'release', $this->f['otherReceiver']);
|
||||
expect(lotQualityTasks($this, $this->f['otherReceiver']))->toBe([]);
|
||||
});
|
||||
|
||||
test('lot quality task detects expiry without movement and requests explicit retention only for prior release', function () {
|
||||
lotSetup($this, [['code' => 'EXPIRY-QA', 'expires_on' => today()->addDay()->toDateString()]]);
|
||||
$release = lotDecision($this, lotContext($this)['lots'][0]);
|
||||
$before = SupplyRecord::query()->count();
|
||||
$this->travel(2)->days();
|
||||
$tasks = lotQualityTasks($this, $this->f['receiver']);
|
||||
expect($tasks)->toHaveCount(1)->and($tasks[0]['task'])->toBe('lot_hold')
|
||||
->and(SupplyRecord::query()->count())->toBe($before)->and(lotContext($this)['usable_milli'])->toBe(0)
|
||||
->and(lotContext($this)['physical']['quantity'])->toBe(10000)->and($release->fresh()->state)->toBe('released');
|
||||
$context = lotContext($this);
|
||||
$this->get($tasks[0]['href'])->assertInertia(fn (Assert $page) => $page->where('contexts.'.app(InventoryLots::class)->setup($this->f['stock']->id, $this->f['warehouse']->id)->record_key.'.lots.0.canHold', true));
|
||||
lotDecision($this, $context['lots'][0], 'hold');
|
||||
expect(lotQualityTasks($this, $this->f['receiver']))->toBe([])->and($release->fresh()->state)->toBe('released');
|
||||
});
|
||||
|
||||
test('lot quality task reappears after metadata correction and stale metadata cannot decide', function () {
|
||||
lotSetup($this, [['code' => 'BEFORE-QA', 'expires_on' => today()->addDays(10)->toDateString()]]);
|
||||
$entry = lotContext($this)['lots'][0];
|
||||
$release = lotDecision($this, $entry);
|
||||
$metadata = SupplyRecord::query()->findOrFail($entry['metadata_id']);
|
||||
$this->actingAs($this->f['issuer']);
|
||||
app(InventoryLots::class)->save($this->f['issuer'], supplyInput(['code' => 'AFTER-QA', 'expires_on' => today()->addDays(9)->toDateString()], 'identify', $metadata));
|
||||
$tasks = lotQualityTasks($this, $this->f['receiver']);
|
||||
expect($tasks)->toHaveCount(1)->and($tasks[0]['task'])->toBe('lot_release')
|
||||
->and(app(InventoryLots::class)->canIdentify($this->f['issuer'], $metadata))->toBeFalse()
|
||||
->and(app(InventoryLots::class)->canDecideLot($this->f['receiver'], $metadata, lotContext($this), $entry, 'release'))->toBeFalse()
|
||||
->and($release->fresh()->data['metadata_id'])->toBe($metadata->id);
|
||||
$this->get($tasks[0]['href'])->assertInertia(fn (Assert $page) => $page->where('contexts.'.app(InventoryLots::class)->setup($this->f['stock']->id, $this->f['warehouse']->id)->record_key.'.lots.0.data.code', 'AFTER-QA'));
|
||||
});
|
||||
|
||||
test('lot quality task enforces company warehouse maintenance and membership expiry', function () {
|
||||
$setup = lotSetup($this, [['code' => 'SCOPE-QA', 'expires_on' => today()->addDays(10)->toDateString()]]);
|
||||
$entry = lotContext($this)['lots'][0];
|
||||
$path = '/stock/lotes?lot_id='.$entry['id'].'&warehouse_id='.$this->f['warehouse']->id;
|
||||
$foreign = receiptFixture();
|
||||
supplyGrant($foreign['receiver'], ['inventory_lot_release']);
|
||||
expect(lotQualityTasks($this, $foreign['receiver']))->toBe([]);
|
||||
$this->getJson($path)->assertUnprocessable();
|
||||
$this->actingAs($this->f['receiver'])->getJson('/stock/lotes?lot_id='.$entry['id'])->assertUnprocessable();
|
||||
$this->getJson($path.'&record_id='.$setup->id)->assertUnprocessable();
|
||||
DB::table('organization_memberships')->where('user_id', $this->f['receiver']->id)->update(['maintenance' => true, 'expires_at' => now()->addHour()]);
|
||||
expect(lotQualityTasks($this, $this->f['receiver']))->toBe([]);
|
||||
$this->postJson('/stock/lotes', supplyInput(['lot_id' => $entry['id'], 'warehouse_id' => $this->f['warehouse']->id, 'state_version' => $entry['state_version']], 'release', SupplyRecord::query()->findOrFail($entry['metadata_id'])))->assertForbidden();
|
||||
DB::table('organization_memberships')->where('user_id', $this->f['receiver']->id)->update(['expires_at' => now()->subMinute()]);
|
||||
expect(lotQualityTasks($this, $this->f['receiver']))->toBe([]);
|
||||
expect(lotContext($this)['usable_milli'])->toBe(0);
|
||||
});
|
||||
|
||||
test('lot quality task does not offer release against an unreconciled partition', function () {
|
||||
$setup = lotSetup($this, [['code' => 'BALANCE-QA', 'expires_on' => today()->addDays(10)->toDateString()]]);
|
||||
$context = lotContext($this);
|
||||
$entry = $context['lots'][0];
|
||||
$context['reconciled'] = false;
|
||||
$this->partialMock(InventoryLots::class)->shouldReceive('context')->andReturn($context);
|
||||
expect(lotQualityTasks($this, $this->f['receiver']))->toBe([])->and(lotContext($this)['reconciled'])->toBeFalse();
|
||||
$this->get('/stock/lotes?lot_id='.$entry['id'].'&warehouse_id='.$this->f['warehouse']->id)->assertInertia(fn (Assert $page) => $page->where('contexts.'.$setup->record_key.'.lots.0.canRelease', false));
|
||||
});
|
||||
|
||||
test('lot quality task retains over one hundred lots with paginated tasks and exact links', function () {
|
||||
$entries = array_map(fn ($i) => ['code' => 'VOLUME-'.$i, 'quantity' => '0.100', 'expires_on' => today()->addDays(10)->toDateString()], range(1, 100));
|
||||
lotSetup($this, $entries);
|
||||
$data = receiptPayload($this->f, '5.000');
|
||||
$data['items'][0]['lots'] = array_map(fn ($i) => ['code' => 'NEW-'.$i, 'quantity' => '1.000', 'expires_on' => today()->addDays(10)->toDateString()], range(1, 5));
|
||||
app(ReceiptWorkflow::class)->confirm($this->f['receiver'], $this->f['order'], $data);
|
||||
$tasks = lotQualityTasks($this, $this->f['otherReceiver']);
|
||||
expect($tasks)->toHaveCount(105);
|
||||
$this->get('/requisicoes/tarefas?module=warehouse')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 105)->has('tasks.data', 25));
|
||||
$this->get('/requisicoes/tarefas?module=warehouse&page=5')->assertInertia(fn (Assert $page) => $page->has('tasks.data', 5));
|
||||
$before = SupplyRecord::query()->count();
|
||||
$this->get($tasks[104]['href'])->assertOk()->assertInertia(fn (Assert $page) => $page->where('focusLotId', lotContext($this)['lots'][104]['id']));
|
||||
expect(SupplyRecord::query()->count())->toBe($before)->and(lotContext($this)['physical']['quantity'])->toBe(15000);
|
||||
});
|
||||
|
||||
test('lot quality task preserves the approved receipt quarantine policy and omits empty lots', function () {
|
||||
lotSetup($this, [['code' => 'POLICY-BASE', 'expires_on' => today()->addDays(10)->toDateString()]], ['quarantine_on_receipt' => false]);
|
||||
lotDecision($this, lotContext($this)['lots'][0]);
|
||||
$data = receiptPayload($this->f, '2.000');
|
||||
$data['items'][0]['lots'] = [['code' => 'POLICY-NEW', 'expires_on' => today()->addDays(10)->toDateString()]];
|
||||
app(ReceiptWorkflow::class)->confirm($this->f['receiver'], $this->f['order'], $data);
|
||||
$entry = collect(lotContext($this)['lots'])->firstWhere('data.code', 'POLICY-NEW');
|
||||
expect($entry['quality_released'])->toBeTrue()->and($entry['release_mode'])->toBe('receipt_policy')
|
||||
->and(lotQualityTasks($this, $this->f['otherReceiver']))->toBe([]);
|
||||
lotIssue($this, '12.000');
|
||||
$this->actingAs($this->f['issuer']);
|
||||
app(InventoryLots::class)->save($this->f['issuer'], supplyInput(['code' => 'EMPTY-CORRECTED', 'expires_on' => today()->addDays(9)->toDateString()], 'identify', SupplyRecord::query()->findOrFail($entry['metadata_id'])));
|
||||
expect(lotQualityTasks($this, $this->f['receiver']))->toBe([])->and(lotContext($this)['physical']['quantity'])->toBe(0);
|
||||
});
|
||||
|
||||
test('lot pagination reaches old controls and history beyond the former fixed limits without writes', function () {
|
||||
$first = taskLotDraft($this);
|
||||
for ($i = 0; $i < 209; $i++) {
|
||||
$copy = $first->replicate();
|
||||
$copy->record_key = (string) Str::uuid();
|
||||
$copy->operation_key = (string) Str::uuid();
|
||||
$copy->created_at = $first->created_at;
|
||||
$copy->save();
|
||||
}
|
||||
$before = [SupplyRecord::query()->count(), DB::table('audit_logs')->count(), StockMovement::query()->count()];
|
||||
$this->actingAs($this->f['receiver'])->get('/stock/lotes')->assertInertia(fn (Assert $page) => $page->where('controlPagination.total', 210)
|
||||
->where('historyPagination.total', 210)->has('records', 25)->has('history', 50));
|
||||
$this->get('/stock/lotes?controls_page=9&history_page=5')->assertHeader('Cache-Control', 'max-age=0, no-store, private')->assertInertia(fn (Assert $page) => $page
|
||||
->where('controlPagination.current_page', 9)->where('historyPagination.current_page', 5)->has('records', 10)->has('history', 10)
|
||||
->where('records.9.id', $first->id)->where('history.9.id', $first->id)
|
||||
->where('controlPagination.prev_page_url', fn ($url) => str_contains($url, 'history_page=5'))
|
||||
->where('historyPagination.prev_page_url', fn ($url) => str_contains($url, 'controls_page=9')));
|
||||
$this->get('/stock/lotes?record_id='.$first->id.'&history_page=5')->assertInertia(fn (Assert $page) => $page->where('controlPagination', null)
|
||||
->has('records', 1)->where('records.0.id', $first->id)->where('historyPagination.total', 210));
|
||||
expect([SupplyRecord::query()->count(), DB::table('audit_logs')->count(), StockMovement::query()->count()])->toBe($before);
|
||||
});
|
||||
|
||||
test('lot pagination keeps active control and pending revision together at the page boundary and exact lot focus', function () {
|
||||
$active = lotSetup($this, [['code' => 'PAGE-BASE', 'expires_on' => today()->addDays(20)->toDateString()]]);
|
||||
$entry = lotContext($this)['lots'][0];
|
||||
$before = $active->fresh()->getAttributes();
|
||||
$this->actingAs($this->f['otherReceiver']);
|
||||
$draft = app(InventoryLots::class)->save($this->f['otherReceiver'], supplyInput([...$active->data, 'minimum_remaining_days' => 5, 'lots' => []], 'prepare', $active));
|
||||
$this->actingAs($this->f['issuer']);
|
||||
for ($i = 0; $i < 25; $i++) {
|
||||
$item = StockItem::query()->create(['tenant_id' => $this->f['tenant']->id, 'created_by' => $this->f['issuer']->id, 'code' => 'PAGE-'.$i, 'name' => 'Artigo sintético sem saldo '.$i, 'unit' => 'cx']);
|
||||
app(InventoryLots::class)->save($this->f['issuer'], supplyInput(['stock_item_id' => $item->id, 'warehouse_id' => $this->f['warehouse']->id,
|
||||
'requires_expiry' => true, 'minimum_remaining_days' => 0, 'lots' => []], 'prepare'));
|
||||
}
|
||||
$this->actingAs($this->f['receiver'])->get('/stock/lotes?controls_page=2')->assertInertia(fn (Assert $page) => $page->where('controlPagination.total', 26)
|
||||
->has('records', 2)->where('records.0.id', $draft->id)->where('records.1.id', $active->id)
|
||||
->where('contexts.'.$active->record_key.'.setup_id', $active->id));
|
||||
$this->get('/stock/lotes?lot_id='.$entry['id'].'&warehouse_id='.$this->f['warehouse']->id)->assertInertia(fn (Assert $page) => $page->where('controlPagination', null)
|
||||
->has('records', 2)->where('focusLotId', $entry['id'])->where('contexts.'.$active->record_key.'.setup_id', $active->id));
|
||||
expect($active->fresh()->getAttributes())->toBe($before);
|
||||
});
|
||||
|
||||
test('lot pagination rejects invalid pages and preserves tenant history boundaries', function () {
|
||||
taskLotDraft($this);
|
||||
$foreign = receiptFixture();
|
||||
supplyGrant($foreign['receiver'], ['inventory_lot_read']);
|
||||
$this->actingAs($foreign['receiver'])->get('/stock/lotes')->assertInertia(fn (Assert $page) => $page->where('controlPagination.total', 0)->where('historyPagination.total', 0));
|
||||
$this->actingAs($this->f['receiver']);
|
||||
foreach (['controls_page=0', 'history_page=-1', 'controls_page=abc', 'history_page=1000001'] as $query) {
|
||||
$this->getJson('/stock/lotes?'.$query)->assertUnprocessable();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user