30 lines
11 KiB
PHP
30 lines
11 KiB
PHP
<?php
|
|
if(!($s['receiptDocuments']??false))throw new LogicException('New receipt document fixture required');
|
|
$receiptUrl='/api/purchasing-receipts/orders/'.$order['id'];$r=$call('validator',$receiptUrl);$p=$call('validator',$receiptUrl.'/preview',['revision'=>$r['data']['revision'],'idempotency_key'=>(string)Str::uuid(),'delivery_note'=>'QA-DOCUMENT-ORIGIN','received_on'=>date('Y-m-d'),'warehouse_id'=>$n['warehouse'],'items'=>[['purchase_order_item_id'=>$order['items'][0]['id'],'received_quantity'=>'1.000','accepted_quantity'=>'1.000','quality_status'=>'compliant','stock_item_id'=>$n['stock'],'notes'=>null]]]);$r=$call('validator',$receiptUrl.'/confirm',['preview'=>$p['data']['preview']]);if($r['status']!==201)throw new RuntimeException('Receipt origin failed');$id=$r['data']['receipt']['id'];$url='/api/purchasing-receipts/history/'.$id;$docs=$url.'/documents';
|
|
$bytes=base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Wl6pAoAAAAASUVORK5CYII=').str_repeat("\0",2048);file_put_contents($s['root'].'/qa-document.png',$bytes);$enter('validator',1);
|
|
$send=function($path,$fields=[],$content=null,$filename='qa-document.png',$i=0)use($clients,$options,$bytes){$parts=[];foreach($fields as$name=>$value)$parts[]=['name'=>$name,'contents'=>(string)$value];$parts[]=['name'=>'file','contents'=>$content??$bytes,'filename'=>$filename];$r=$clients[$i]->post($path,$options('validator',$i)+['multipart'=>$parts]);$text=(string)$r->getBody();return ['status'=>$r->getStatusCode(),'data'=>json_decode($text,true),'body'=>$text];};
|
|
$preview=function($key=null,$content=null,$filename='qa-document.png',$i=0,$type='delivery_note')use($send,$call,$url,$docs){$r=$call('validator',$url,null,$i);return $send($docs.'/preview',['operation_key'=>$key??(string)Str::uuid(),'revision'=>$r['data']['document_revision'],'document_type'=>$type,'reason'=>'Evidencia sintetica individual da entrega recebida.'],$content,$filename,$i);};$confirm=fn($cut,$content=null,$filename='qa-document.png',$i=0)=>$send($docs.'/confirm',['preview'=>$cut],$content,$filename,$i);
|
|
$countFiles=function($dir){if(!is_dir($dir))return 0;$count=0;foreach(new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir,FilesystemIterator::SKIP_DOTS))as$file)if($file->isFile())$count++;return $count;};$root=$s['root'].'/receipt-documents';$stage=$s['root'].'/receipt-document-previews';
|
|
$physical=fn()=>hash('sha256',json_encode(array_map(fn($t)=>$db->table($t)->orderBy('id')->get(),['goods_receipts','goods_receipt_items','stock_movements','purchase_orders','purchase_order_items'])));$before=$physical();$audits=$db->table('audit_logs')->count();
|
|
$ok($db->table('receipt_documents')->count()===0,'fresh native receipt document book begins empty');$r=$call('validator',$url);$ok($r['status']===200&&$r['data']['documents_enabled']===true&&$r['data']['can_attach']===true,'receipt detail exposes attachment revision and existing warehouse capability');$r=$call('validator',$docs);$ok($r['status']===200&&$r['data']['data']===[]&&$r['data']['per_page']===15,'private metadata has server pagination15');
|
|
$ok($call('requester',$docs)['status']===403&&$call('approver',$docs)['status']===403,'request-only roles cannot inspect receipt attachment metadata');
|
|
$p=$preview();$ok($p['status']===200&&$p['data']['impact']['document']['sha256']===hash('sha256',$bytes)&&$p['data']['impact']['receipt_unchanged']&&!$p['data']['impact']['reused']&&!isset($p['data']['impact']['document']['path']),'preview uses original service and explicit safe metadata and impact');$ok($db->table('receipt_documents')->count()===0&&$db->table('audit_logs')->count()===$audits&&$countFiles($root)===0&&$countFiles($stage)===0&&$physical()===$before,'preview rolls back document and audit and removes only staged file');
|
|
$ok($preview(null,str_repeat('not an image',200))['status']===422&&$preview(null,$bytes,'qa.php')['status']===422&&$preview(null,null,'qa-document.png',0,'invalid')['status']===422,'native content extension and type validation preserved');
|
|
$key=(string)Str::uuid();$p0=$preview($key);$p1=$preview($key,null,'qa-document.png',1);$ok($p0['status']===200&&$p1['status']===200,'independent sessions prepare same native operation key');$ok($confirm($p0['data']['preview'],$bytes.'substituted')['status']===409,'confirmation rejects substituted bytes');
|
|
$fScope=$f['scope'];$grant=DB::table('permission_grants')->where($fScope)->where('user_id',$f['validator'])->where('permission','purchasing_receipts.confirm')->first();if(!$grant)throw new RuntimeException('Validator grant fixture not found');DB::table('permission_grants')->where('id',$grant->id)->delete();try{$ok($confirm($p0['data']['preview'])['status']===403,'confirmation rechecks existing host permission');}finally{DB::table('permission_grants')->insert((array)$grant);}
|
|
$db->unprepared("CREATE FUNCTION public.receipt_document_fail() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF NEW.action='sgf.receipt.document' THEN RAISE EXCEPTION 'QA_DOCUMENT_FAILURE'; END IF; RETURN NEW; END $$; CREATE TRIGGER receipt_document_fail BEFORE INSERT ON audit_logs FOR EACH ROW EXECUTE FUNCTION receipt_document_fail()");try{$r=$confirm($p0['data']['preview']);$ok($r['status']===500&&$db->table('receipt_documents')->count()===0&&$countFiles($root)===0,'outer provenance failure rolls back native document and compensates proven uncommitted file');}finally{$db->unprepared('DROP TRIGGER receipt_document_fail ON audit_logs; DROP FUNCTION public.receipt_document_fail()');}
|
|
$parts=fn($cut)=>[['name'=>'preview','contents'=>$cut],['name'=>'file','contents'=>$bytes,'filename'=>'qa-document.png']];$responses=GuzzleHttp\Promise\Utils::unwrap([$clients[0]->postAsync($docs.'/confirm',$options('validator')+['multipart'=>$parts($p0['data']['preview'])]),$clients[1]->postAsync($docs.'/confirm',$options('validator',1)+['multipart'=>$parts($p1['data']['preview'])])]);$statuses=array_map(fn($r)=>$r->getStatusCode(),$responses);sort($statuses);$ok($statuses===[200,201]&&$db->table('receipt_documents')->count()===1&&$countFiles($root)===1,'concurrent confirmations finish once with201 and replay200 after lock release');$document=$db->table('receipt_documents')->first();$ok($confirm($p0['data']['preview'])['status']===200&&$db->table('audit_logs')->where('action','sgf.receipt.document')->count()===1,'lost-response replay creates no additional root decision');
|
|
$reuse=$preview(null,null,'another-name.png');$ok($reuse['status']===200&&$reuse['data']['impact']['reused']&&$countFiles($root)===1&&$countFiles($stage)===0,'preview of same type and hash reuses original without deleting permanent file');$r=$confirm($reuse['data']['preview'],null,'another-name.png');$ok($r['status']===201&&$r['data']['reused']&&$r['data']['document_id']===$document->id&&$db->table('receipt_documents')->count()===1&&$countFiles($root)===1,'new explicit intent reuses original native document and filename without another file');
|
|
$r=$preview($key,$bytes.'different');$ok($r['status']===422,'native key rejects another content before confirmation');$r=$call('validator',$docs);$ok(!isset($r['data']['data'][0]['path'],$r['data']['data'][0]['tenant_id']),'metadata never exposes private path or native tenant');
|
|
$response=$clients[0]->get($docs.'/'.$document->id.'/download',$options('validator'));$ok($response->getStatusCode()===200&&(string)$response->getBody()===$bytes&&str_contains($response->getHeaderLine('Content-Disposition'),'attachment')&&$response->getHeaderLine('X-Content-Type-Options')==='nosniff'&&str_contains($response->getHeaderLine('Content-Security-Policy'),'sandbox'),'native download checks original hash and streams private sandboxed bytes');$ok($db->table('audit_logs')->where('action','sgf.receipt.document_downloaded')->count()===1,'authorised download trace linked to original requisition');
|
|
$path=$root.'/'.$document->path;file_put_contents($path,'QA changed bytes');try{$ok($call('validator',$docs.'/'.$document->id.'/download')['status']===409&&$confirm($p0['data']['preview'])['status']===409,'tampered stored evidence blocks both download and replay');}finally{file_put_contents($path,$bytes);}
|
|
$ok($call('validator','/api/purchasing-receipts/history/999999/documents/'.$document->id.'/download')['status']===404,'document cannot cross receipt parent');
|
|
// Bounded volume fixture through the same native service, not repeated HTTP workflow tests.
|
|
$user=App\Models\User::findOrFail($f['validator']);auth()->setUser($user);request()->setUserResolver(fn()=>$user);app(App\Shared\Context\CompanyContext::class)->selectFor($user->id,$f['tenant'],$f['company'],$f['unit']);
|
|
(new App\Modules\Purchasing\ReceiptRuntime)->run('purchasing_receipts.confirm',function($a)use($id,$s,$bytes){$receipt=Tempo\Purchasing\Embedded\Models\GoodsReceipt::findOrFail($id);$files=app(App\Modules\Purchasing\SupplierDocumentFiles::class);$files->using($files->root(false,'receipt'),function()use($a,$receipt,$s,$bytes){for($i=1;$i<=15;$i++){file_put_contents($s['root'].'/qa-document.png',$bytes.$i);$file=new Illuminate\Http\UploadedFile($s['root'].'/qa-document.png','QA-volume-'.$i.'.png','image/png',UPLOAD_ERR_OK,true);app(Tempo\Purchasing\Embedded\Services\ReceiptDocuments::class)->add($a,$receipt,['operation_key'=>(string)Str::uuid(),'document_type'=>'quality'],$file);}},'receipt_documents');});
|
|
$r=$call('validator',$docs.'?page=1');$r2=$call('validator',$docs.'?page=2');$ok($r['status']===200&&count($r['data']['data'])===15&&count($r2['data']['data'])===1&&array_intersect(array_column($r['data']['data'],'id'),array_column($r2['data']['data'],'id'))===[],'16 documents span distinct15 and1 pages');$ok($countFiles($root)===16&&$countFiles($stage)===0,'permanent files match native documents and preview files are absent');
|
|
$r=$call('actor','/api/purchasing-dossiers/'.$request);if($r['status']===200)$r=$call('actor','/api/purchasing-dossiers/'.$request.'?events_page='.$r['data']['events']['pagination']['last_page']);$ok($r['status']===200&&array_filter($r['data']['events']['data']??[],fn($v)=>str_starts_with($v['action'],'receipt.document')||$v['action']==='sgf.receipt.document_downloaded'),'receipt evidence appears in authorised requisition dossier timeline');
|
|
$enter('validator',1,['module'=>'commercial']);$ok($call('validator',$docs,null,1)['status']===403,'other module cannot reuse document capability');$enter('validator',1,['foreign'=>1]);$ok($call('validator',$docs,null,1)['status']===403,'unmapped unit cannot read private attachments');
|
|
$ok($physical()===$before,'attachments preserve receipt quantities quality acceptance orders and physical stock movements');$ok($db->table('invoices')->count()===0&&$db->table('pa_obligations')->count()===0&&$db->table('external_payments')->count()===0,'attachment decisions introduce no invoice debt payment or accounting effect');
|
|
file_put_contents($out,json_encode(['passed'=>true,'checks'=>$checks,'transport_documents'=>1,'native_volume_documents'=>15,'documents'=>16,'permanent_files'=>16,'staged_files'=>0,'concurrency_statuses'=>$statuses,'new_right_names'=>[],'visual_review'=>false,'deployment_capacity_validated'=>false],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo count($checks)." receipt document checks passed.\n";
|