f = receiptFixture(); $this->actingAs($this->f['receiver']); $this->receipt = app(ReceiptWorkflow::class)->confirm($this->f['receiver'], $this->f['order'], receiptPayload($this->f)); $this->directory = storage_path('framework/testing/receipt-block-'.Str::uuid()); Storage::set('receipt_documents', Storage::build(['driver' => 'local', 'root' => $this->directory, 'throw' => true])); $this->image = UploadedFile::fake()->createWithContent('guia.png', base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Wl6E4sAAAAASUVORK5CYII=')); $this->key = (string) Str::uuid(); $this->url = '/recepcoes/'.$this->receipt->id; }); afterEach(function () { File::deleteDirectory($this->directory); }); function receiptDocumentInput(object $test, ?string $key = null, ?UploadedFile $file = null): array { return ['operation_key' => $key ?? $test->key, 'document_type' => 'delivery_note', 'file' => $file ?? $test->image]; } test('receipt document block uploads reuses and downloads evidence without repeating receipt stock or approvals', function () { $receiptBefore = $this->receipt->getAttributes(); $before = []; foreach (['goods_receipts', 'goods_receipt_items', 'stock_movements', 'approval_steps', 'budget_entries', 'purchase_orders'] as $table) { $before[$table] = DB::table($table)->orderBy('id')->get()->toArray(); } $this->post($this->url.'/documentos', receiptDocumentInput($this))->assertRedirect($this->url.'#receipt-documents'); $document = ReceiptDocument::query()->sole(); $this->post($this->url.'/documentos', receiptDocumentInput($this))->assertRedirect(); $this->post($this->url.'/documentos', receiptDocumentInput($this, (string) Str::uuid()))->assertRedirect(); expect(ReceiptDocument::query()->count())->toBe(1)->and(Storage::disk('receipt_documents')->allFiles())->toHaveCount(1); expect(DB::table('audit_logs')->where('action', 'receipt.document_added')->count())->toBe(1); expect(DB::table('audit_logs')->where('action', 'receipt.document_reused')->count())->toBe(1); $this->get($this->url)->assertInertia(fn (Assert $p) => $p->where('documentsReady', true)->where('canAddDocuments', true) ->has('documents', 1)->missing('documents.0.path')->where('documents.0.id', $document->id)); $response = $this->get($this->url.'/documentos/'.$document->id)->assertOk()->assertHeader('Content-Type', 'application/octet-stream') ->assertHeader('X-Content-Type-Options', 'nosniff')->assertHeader('Cache-Control', 'no-store, private'); expect(hash('sha256', $response->streamedContent()))->toBe($document->sha256); foreach ($before as $table => $rows) { expect(DB::table($table)->orderBy('id')->get()->toArray())->toEqual($rows); } expect($this->receipt->fresh()->getAttributes())->toBe($receiptBefore); }); test('receipt document block preserves role independence tenant and receipt scopes', function () { $this->post($this->url.'/documentos', receiptDocumentInput($this))->assertRedirect(); $document = ReceiptDocument::query()->sole(); $this->actingAs($this->f['issuer'])->post($this->url.'/documentos', receiptDocumentInput($this, (string) Str::uuid()))->assertForbidden(); $this->get($this->url.'/documentos/'.$document->id)->assertOk(); supplyGrant($this->f['receiver'], ['warehouse']); supplyGrant($this->f['issuer'], ['warehouse']); $this->post($this->url.'/documentos', receiptDocumentInput($this, (string) Str::uuid()))->assertForbidden(); $foreign = receiptFixture(); $this->actingAs($foreign['receiver'])->get($this->url.'/documentos/'.$document->id)->assertNotFound(); $this->post($this->url.'/documentos', receiptDocumentInput($this))->assertNotFound(); $this->actingAs($this->f['receiver']); $another = app(ReceiptWorkflow::class)->confirm($this->f['receiver'], $this->f['order'], receiptPayload($this->f, '0.100')); $this->get('/recepcoes/'.$another->id.'/documentos/'.$document->id)->assertNotFound(); supplyGrant($this->f['receiver'], ['warehouse']); DB::table('organization_memberships')->where('user_id', $this->f['receiver']->id)->update(['maintenance' => true]); $this->post($this->url.'/documentos', receiptDocumentInput($this, (string) Str::uuid()))->assertForbidden(); $this->get($this->url.'/documentos/'.$document->id)->assertForbidden(); }); test('receipt document block rejects changed replay and invalid files and detects tampering', function () { $this->post($this->url.'/documentos', receiptDocumentInput($this))->assertRedirect(); $document = ReceiptDocument::query()->sole(); $this->withHeader('Accept', 'application/json')->post($this->url.'/documentos', receiptDocumentInput($this, null, UploadedFile::fake()->createWithContent('guia.png', file_get_contents($this->image->getPathname()).'changed')))->assertUnprocessable(); $this->post($this->url.'/documentos', receiptDocumentInput($this, (string) Str::uuid(), UploadedFile::fake()->create('bad.html', 1, 'text/html')))->assertUnprocessable(); $this->post($this->url.'/documentos', receiptDocumentInput($this, (string) Str::uuid(), UploadedFile::fake()->create('large.pdf', 2049, 'application/pdf')))->assertUnprocessable(); expect(ReceiptDocument::query()->count())->toBe(1)->and(Storage::disk('receipt_documents')->allFiles())->toHaveCount(1); foreach (['update', 'delete'] as $operation) { try { DB::transaction(function () use ($document, $operation) { $query = DB::table('receipt_documents')->where('id', $document->id); $operation === 'update' ? $query->update(['original_name' => 'changed.png']) : $query->delete(); }); test()->fail('Document metadata must remain append-only'); } catch (QueryException) { expect($document->fresh()->original_name)->toBe('guia.png'); } } Storage::disk('receipt_documents')->put($document->path, 'TAMPERED'); $this->get($this->url.'/documentos/'.$document->id)->assertStatus(409); Storage::disk('receipt_documents')->delete($document->path); $this->get($this->url.'/documentos/'.$document->id)->assertNotFound(); }); test('receipt document block rolls back metadata and private file if audit fails then permits safe retry', function () { $this->partialMock(RequisitionWorkflow::class)->shouldReceive('audit')->once()->andThrow(new RuntimeException('Injected document audit failure')); try { app(ReceiptDocuments::class)->add($this->f['receiver'], $this->receipt, receiptDocumentInput($this), $this->image); test()->fail('Expected rollback'); } catch (RuntimeException $error) { expect($error->getMessage())->toBe('Injected document audit failure'); } expect(ReceiptDocument::query()->count())->toBe(0)->and(Storage::disk('receipt_documents')->allFiles())->toBe([]); app()->forgetInstance(RequisitionWorkflow::class); $document = app(ReceiptDocuments::class)->add($this->f['receiver'], $this->receipt, receiptDocumentInput($this), $this->image); expect($document->id)->toBeGreaterThan(0)->and(ReceiptDocument::query()->count())->toBe(1); }); test('receipt document block leaves the established receipt page usable before installation', function () { Schema::drop('receipt_documents'); $this->get($this->url)->assertOk()->assertInertia(fn (Assert $p) => $p->where('documentsReady', false)->where('canAddDocuments', false)->has('documents', 0)); $this->post($this->url.'/documentos', receiptDocumentInput($this))->assertStatus(503); expect(GoodsReceipt::query()->whereKey($this->receipt->id)->exists())->toBeTrue(); });