commit melhorias sgf
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Push-Location -LiteralPath 'C:\laragon\www\sgf-tempo'
|
||||
try {
|
||||
& 'C:\laragon\tools\sgf-tempo\node-v24.22.1-win-arm64\node.exe' 'scripts/launch-sgf.mjs' 'start'
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Não foi possível abrir o SGF existente. Os dados e a cache foram preservados.' }
|
||||
Write-Output 'Abrir o SGF: http://127.0.0.1:8085/sgf'
|
||||
} finally { Pop-Location }
|
||||
@@ -0,0 +1,56 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import crypto from 'node:crypto';
|
||||
import assert from 'node:assert/strict';
|
||||
import {spawnSync} from 'node:child_process';
|
||||
|
||||
// Internal release gate, not a product menu. Read-only metadata; never bootstrap/migrate an application.
|
||||
const contractFile='docs/integration/sgf-solution-contract.json',contract=JSON.parse(fs.readFileSync(contractFile));
|
||||
assert.equal(contract.sgc_required,false);assert.equal(contract.new_demonstrations,false);
|
||||
assert.deepEqual(contract.modules.map(x=>x.id),['commercial','purchasing','stock','workshop','finance','accounting','human_resources']);
|
||||
const run=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json'));
|
||||
assert.equal(run.id,'2576427d1939');assert.equal(run.port,55439);
|
||||
const root=path.join(run.root,'private/receipt-composition-cc50a276c10c'),state=JSON.parse(fs.readFileSync(path.join(root,'state.json'))),server=JSON.parse(fs.readFileSync(path.join(root,'demo-server.json')));
|
||||
assert.equal(state.tag,'cc50a276c10c');assert.equal(server.port,8085);
|
||||
const hash=file=>crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
||||
// Keep the footprint tied to real declarations, rather than guessed prefixes.
|
||||
const declarations=['database/migrations','database/sql','packages/payables/src','packages/general-ledger/src'].flatMap(dir=>fs.readdirSync(dir).filter(f=>/\.(php|sql)$/.test(f)).map(f=>fs.readFileSync(path.join(dir,f),'utf8'))).join('\n')+'\n'+fs.readFileSync('scripts/sgf-document-cancellation-schema.php','utf8')+'\n'+fs.readFileSync('app/Shared/Installation/HumanResourcesProfiles.php','utf8');
|
||||
const known=new Set(['users','companies','units','permission_grants','a_clients','a_services','a_service_drafts','a_billing_drafts',
|
||||
...[...declarations.matchAll(/(?:CREATE TABLE\s+|Schema::create\(['"])([a-z0-9_]+)/gi)].map(x=>x[1])]);
|
||||
for(const m of contract.modules){for(const table of m.required_host_tables)assert(known.has(table),'Undeclared table '+table);for(const file of [...m.required_source_files,...m.proofs])assert(fs.existsSync(file),'Missing recorded source/evidence '+file);}
|
||||
const sql=`BEGIN READ ONLY;
|
||||
SELECT json_build_object('db',current_database(),'role',current_user,'cluster',current_setting('cluster_name'),'elevated',rolsuper OR rolcreatedb OR rolcreaterole OR rolreplication OR rolbypassrls) FROM pg_roles WHERE rolname=current_user;
|
||||
SELECT json_build_object('tables',json_agg(tablename ORDER BY tablename)) FROM pg_tables WHERE schemaname='public';
|
||||
COMMIT;`;
|
||||
const result=spawnSync('C:/Program Files/PostgreSQL/17/bin/psql.exe',['-X','-h','127.0.0.1','-p','55439','-U',state.env.DB_USERNAME,'-d',state.env.DB_DATABASE,'-Atq','-v','ON_ERROR_STOP=1'],{input:sql,encoding:'utf8',windowsHide:true,env:{...process.env,PGPASSWORD:state.env.DB_PASSWORD}});
|
||||
if(result.status!==0)throw Error('Selected presentation metadata unavailable; no ready result may be inferred');
|
||||
const [identity,metadata]=result.stdout.trim().split(/\r?\n/).map(JSON.parse);
|
||||
assert.equal(identity.db,'budget_core_sgf_cc50a276c10c');assert.equal(identity.role,'ir_composition_cc50a276c10c');assert.equal(identity.cluster,'sgf_modular_regression_'+run.id);assert.equal(identity.elevated,false);
|
||||
const tables=new Set(metadata.tables);
|
||||
const native=state.nativeConnection;
|
||||
assert.equal(native.database,'compras_receipts_test_cc50a276c10c');assert.equal(native.username,'payable_history_native_cc50a276c10c');
|
||||
const nr=spawnSync('C:/Program Files/PostgreSQL/17/bin/psql.exe',['-X','-h','127.0.0.1','-p','55439','-U',native.username,'-d',native.database,'-Atq','-v','ON_ERROR_STOP=1'],{input:sql,encoding:'utf8',windowsHide:true,env:{...process.env,PGPASSWORD:native.password}});
|
||||
assert.equal(nr.status,0,'Canonical native book metadata unavailable');const [ni,nm]=nr.stdout.trim().split(/\r?\n/).map(JSON.parse);assert.equal(ni.db,native.database);assert.equal(ni.role,native.username);assert.equal(ni.cluster,identity.cluster);assert.equal(ni.elevated,false);const nativeTables=new Set(nm.tables);
|
||||
const modules=contract.modules.map(module=>({id:module.id,label:module.label,features:module.features,
|
||||
sources:module.required_source_files.map(file=>({file,present:fs.existsSync(file),sha256:fs.existsSync(file)?hash(file):null})),
|
||||
existing_evidence:module.proofs.map(file=>({file,present:fs.existsSync(file),sha256:fs.existsSync(file)?hash(file):null,not_reexecuted:true})),
|
||||
missing_host_tables:module.required_host_tables.filter(x=>!tables.has(x)),
|
||||
missing_native_tables:(module.required_native_tables??[]).filter(x=>!nativeTables.has(x)),
|
||||
integrated_release_verified:false,client_presentable:false,remaining:module.work_remaining}));
|
||||
const purchasing=JSON.parse(fs.readFileSync('docs/proofs/purchasing-completion-final-20261104-r2.json'));
|
||||
modules.find(m=>m.id==='purchasing').accepted_component_journey=purchasing.passed===true&&purchasing.installation.id===state.tag
|
||||
&&purchasing.sources.every(source=>fs.existsSync(path.join(state.app,source.file))&&hash(path.join(state.app,source.file))===source.sha256);
|
||||
const acceptance=fs.existsSync(contract.acceptance_proof)?JSON.parse(fs.readFileSync(contract.acceptance_proof)):null;
|
||||
// No file-presence/schema/table-count shortcut can declare the six modules accepted.
|
||||
const ready=acceptance?.passed===true&&acceptance.contract_sha256===hash(contractFile)&&acceptance.installation==='cc50a276c10c'
|
||||
&&contract.release_gates.every(g=>acceptance.gates?.[g.id]===true)
|
||||
&&modules.every(m=>m.sources.every(s=>s.present)&&m.missing_host_tables.length===0&&m.missing_native_tables.length===0)
|
||||
&&contract.modules.every(m=>acceptance.modules?.[m.id]?.same_installation===true&&m.features.every(f=>acceptance.modules[m.id].features?.[f]===true));
|
||||
const output={date:'2026-11-11',contract_sha256:hash(contractFile),installation:'cc50a276c10c',port:8085,sgc_dependency:false,
|
||||
full_product_complete:(contract.product_backlog?.length>0&&contract.product_backlog.every(x=>x.implemented===true))??false,product_backlog:contract.product_backlog??[],
|
||||
modules,release_gates:contract.release_gates.map(g=>({...g,passed:ready?true:false})),client_presentable:ready,
|
||||
limits:['Host-table footprint is a minimum projection, not a full migration/dependency validation.','Native purchasing/stock tables live in their existing separate native book; this scan does not call that missing.','Source/evidence presence does not mean a feature is integrated, visually accepted or deployment-ready.'],
|
||||
database_writes:0,application_bootstraps:0,cache_cleared:false,new_demo:false,new_rights:false};
|
||||
const out=process.argv[2];if(out){assert(/^docs\/proofs\/[a-z0-9-]+\.json$/.test(out)&&!fs.existsSync(out),'Fresh proof required');fs.writeFileSync(out,JSON.stringify(output,null,2)+'\n');}
|
||||
console.log(JSON.stringify({modules:modules.length,client_presentable:ready,modules_missing_host_structures:modules.filter(m=>m.missing_host_tables.length).map(m=>m.id),database_writes:0,new_demo:false,output:out??null}));
|
||||
if(!out&&!ready)process.exitCode=2;
|
||||
@@ -0,0 +1,14 @@
|
||||
import fs from 'node:fs';import path from 'node:path';import assert from 'node:assert/strict';import {spawnSync} from 'node:child_process';
|
||||
const action=process.argv[2]??'start';assert(['start','status'].includes(action),'Only start/status; no reset, migration or provisioning');
|
||||
const run=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json'));assert.equal(run.id,'2576427d1939');assert.equal(run.port,55439);const expected='C:/laragon/data/sgf-tempo/run/modular-regression-'+run.id;assert.equal(path.resolve(run.root),path.resolve(expected));assert.equal(path.resolve(run.cluster),path.resolve(expected+'/postgres'));assert.equal(path.resolve(fs.realpathSync(run.cluster)),path.resolve(run.cluster));
|
||||
const s=JSON.parse(fs.readFileSync(expected+'/private/receipt-composition-cc50a276c10c/state.json'));assert.equal(s.tag,'cc50a276c10c');assert.equal(s.singleInstallation,true);const pg='C:/Program Files/PostgreSQL/17/bin/';
|
||||
const call=(exe,args,options={})=>spawnSync(exe,args,{encoding:'utf8',windowsHide:true,...options});let status=call(pg+'pg_ctl.exe',['-D',run.cluster,'status']);assert([0,3].includes(status.status),'Owned database status unavailable; preserve its files');
|
||||
if(status.status===3&&action==='start'){
|
||||
const ready=call(pg+'pg_isready.exe',['-h','127.0.0.1','-p','55439']);assert.notEqual(ready.status,0,'Unexpected service on owned cluster port; do not replace it');
|
||||
const started=call(pg+'pg_ctl.exe',['-D',run.cluster,'-l',expected+'/postgres.log','-w','-t','20','start']);assert.equal(started.status,0,'Existing database did not start; no recovery files were deleted');status=call(pg+'pg_ctl.exe',['-D',run.cluster,'status']);
|
||||
}
|
||||
if(status.status!==0){console.log(JSON.stringify({running:false,entry:'http://127.0.0.1:8085/sgf',new_database:false}));process.exit(2);}
|
||||
const check=call(pg+'psql.exe',['-X','-h','127.0.0.1','-p','55439','-U',s.env.DB_USERNAME,'-d',s.env.DB_DATABASE,'-Atq','-v','ON_ERROR_STOP=1','-c',"BEGIN READ ONLY; SELECT current_database()||'|'||current_user||'|'||current_setting('cluster_name'); COMMIT;"],{env:{...process.env,PGPASSWORD:s.env.DB_PASSWORD}});assert.equal(check.status,0,'Selected application database unavailable');assert.equal(check.stdout.trim(),'budget_core_sgf_cc50a276c10c|ir_composition_cc50a276c10c|sgf_modular_regression_'+run.id,'Wrong database identity; no fallback');
|
||||
if(action==='start'){const started=call('C:/laragon/tools/sgf-tempo/node-v24.22.1-win-arm64/node.exe',[path.resolve('scripts/purchasing-single-installation.mjs'),'start'],{cwd:path.resolve('.')});assert.equal(started.status,0,'Existing8085 application did not start');}
|
||||
let response;for(let attempt=0;attempt<12;attempt++){try{response=await fetch('http://127.0.0.1:8085/sgf',{signal:AbortSignal.timeout(3000)});if(response.status===200)break;await response.text();}catch{}await new Promise(resolve=>setTimeout(resolve,250));}
|
||||
assert(response&&response.status===200,'SGF entry did not become available');assert.equal(response.headers.get('x-sgf-installation'),s.tag);await response.text();console.log(JSON.stringify({running:true,entry:'http://127.0.0.1:8085/sgf',same_installation:true,sgc_required:false,new_database:false,new_demo:false,cache_cleared:false}));
|
||||
@@ -0,0 +1,65 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import crypto from 'node:crypto';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
// Prepare/check only. Deliberately has no install, activate, migrate, restart or grant action.
|
||||
const command=process.argv[2]??'check';assert(['check','prepare'].includes(command));
|
||||
const repo=process.cwd(),run=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json'));
|
||||
assert.equal(run.id,'2576427d1939');assert.equal(run.port,55439);
|
||||
const root=path.join(run.root,'private/receipt-composition-cc50a276c10c'),state=JSON.parse(fs.readFileSync(path.join(root,'state.json'))),server=JSON.parse(fs.readFileSync(path.join(root,'demo-server.json')));
|
||||
assert.equal(state.tag,'cc50a276c10c');assert.equal(server.port,8085);assert.equal(state.singleInstallation,true);
|
||||
const hash=p=>crypto.createHash('sha256').update(fs.readFileSync(p)).digest('hex'),digest=v=>crypto.createHash('sha256').update(JSON.stringify(v)).digest('hex'),normalise=p=>fs.readFileSync(p,'utf8').replaceAll('\r\n','\n');
|
||||
const prior=JSON.parse(fs.readFileSync('docs/proofs/purchasing-completion-final-20261104-r2.json'));
|
||||
for(const source of prior.sources)assert.equal(hash(path.join(state.app,source.file)),source.sha256,'Protected8085 '+source.file);
|
||||
const endpoint=JSON.parse(fs.readFileSync('docs/proofs/sgc-invoice-endpoint-20261110.json'));
|
||||
assert(endpoint.passed&&endpoint.checks===78);for(const source of endpoint.sources)assert.equal(hash(source.file),source.sha256,'Accepted endpoint '+source.file);
|
||||
const files=['app/Http/Middleware/AuthenticateSgcInvoice.php','app/Http/Requests/Sgc/StoreInvoiceRequest.php','app/Http/Controllers/Api/V1/SgcInvoiceIngressController.php','app/Modules/Purchasing/SgcInvoiceIngress.php','config/sgc_invoice.php','routes/sgc.php','bootstrap/app.php','app/Shared/Context/ModuleNavigation.php'];
|
||||
const additions={
|
||||
'bootstrap/app.php':" api: __DIR__.'/../routes/sgc.php',\n",
|
||||
'app/Shared/Context/ModuleNavigation.php':" if (preg_match('~^api/v1/integration/sgc/invoices(?:/orders/[1-9][0-9]*)?$~D', $path)) return 'purchasing';\n"
|
||||
};
|
||||
const entries=files.map(file=>{const target=path.join(state.app,file),exists=fs.existsSync(target);if(additions[file])assert.equal(normalise(file).replace(additions[file],''),normalise(target),'Only additive routing patch allowed');else assert(!exists,'Unexpected endpoint installation: '+file);return {file,before_sha256:exists?hash(target):null,after_sha256:hash(file)};});
|
||||
assert(/'enabled'\s*=>\s*false/.test(normalise('config/sgc_invoice.php'))&&/'principals'\s*=>\s*\[\]/.test(normalise('config/sgc_invoice.php')));
|
||||
const sgcRoot='C:/laragon/www/compras',sgcFiles=['app/Services/SgfInvoiceGateway.php','config/sgf_invoice.php','app/Services/SgfInvoiceDispatches.php','app/Http/Controllers/SgfInvoiceDispatchController.php',
|
||||
'database/migrations/2026_11_10_000001_create_sgf_invoice_dispatches.php',...['layout','document-form','index','evidence','show'].map(x=>'resources/views/sgf-invoices/'+x+'.blade.php'),
|
||||
'app/Services/InvoiceWorkflow.php','app/Http/Controllers/InvoiceController.php','routes/web.php','resources/js/pages/invoices/index.tsx'];
|
||||
const clientFiles=[{source:'packages/sgc-invoice-client/src/InvoiceClient.php',target:'client/InvoiceClient.php'},...sgcFiles.map(file=>({source:path.join(sgcRoot,file),target:'sgc/'+file}))].map(x=>({...x,sha256:hash(x.source)}));
|
||||
assert(/'enabled'\s*=>\s*false/.test(normalise(path.join(sgcRoot,'config/sgf_invoice.php'))));
|
||||
assert(/'workspace_enabled'\s*=>\s*false/.test(normalise(path.join(sgcRoot,'config/sgf_invoice.php'))));
|
||||
const plan={schema:1,installation:'cc50a276c10c',port:8085,endpoint_files:entries,client_files:clientFiles.map(({target,sha256})=>({target,sha256})),
|
||||
source_endpoint_proof_sha256:hash('docs/proofs/sgc-invoice-endpoint-20261110.json'),
|
||||
modes:{prepared_only:true,install:false,activate:false,publish:false,new_database:false,new_demo:false},
|
||||
endpoint_configuration:{enabled:false,principals:[]},sgc_configuration:{enabled:false,workspace_enabled:false,connections:[],client_package:null,client_package_sha256:null},
|
||||
source_schema:{migration:'2026_11_10_000001_create_sgf_invoice_dispatches',pilot_applied:false,source_regression_book_only:'b4093c6f6707'},
|
||||
sql_privileges_to_review:{approved:false,role:null,database:null,dispatches:['SELECT','INSERT','UPDATE'],events:['SELECT','INSERT'],sequences:['USAGE','SELECT'],public_grants:false},
|
||||
source_existing_files_require_manual_merge:['app/Services/InvoiceWorkflow.php','app/Http/Controllers/InvoiceController.php','routes/web.php','resources/js/pages/invoices/index.tsx'],
|
||||
review_required:['Approve installation of the implemented SGC draft workspace/schema and disabled code; no blanket migration or existing-invoice forwarding',
|
||||
'Explicit source tenant and SGF tenant/company/unit mapping; reviewed order and line IDs',
|
||||
'Named QA principal with existing company.view/unit.view/purchasing_invoices.view/prepare rights, auth/context versions',
|
||||
'Private credential provisioning, separately approved disabled-code installation and subsequent activation decision',
|
||||
'End-to-end SGC transport to this same8085 installation after approval; no deployment claim from kernel/fake tests']};
|
||||
const id=digest(plan).slice(0,20),stage=path.join(root,'prepared-sgc-invoice',id),protectedFiles=['state.json','binding.json','demo-server.json'];
|
||||
const before=protectedFiles.map(file=>({file,sha256:hash(path.join(root,file))}));
|
||||
if(command==='prepare'){
|
||||
if(fs.existsSync(stage)){assert(fs.existsSync(path.join(stage,'manifest.json')),'Incomplete preparation preserved; inspect manually');assert.deepEqual(JSON.parse(fs.readFileSync(path.join(stage,'manifest.json'))),plan);}
|
||||
else{
|
||||
fs.mkdirSync(stage,{recursive:true});for(const entry of entries){const out=path.join(stage,'8085',entry.file);fs.mkdirSync(path.dirname(out),{recursive:true});fs.copyFileSync(entry.file,out);if(entry.before_sha256){const backup=path.join(stage,'before',entry.file);fs.mkdirSync(path.dirname(backup),{recursive:true});fs.copyFileSync(path.join(state.app,entry.file),backup);}}
|
||||
for(const entry of clientFiles){const out=path.join(stage,entry.target);fs.mkdirSync(path.dirname(out),{recursive:true});fs.copyFileSync(entry.source,out);}
|
||||
fs.writeFileSync(path.join(stage,'mapping.template.json'),JSON.stringify({selected:false,id:null,version:null,authority:'sgf_only',source_scope:{tenant_id:null},target_scope:{tenant_id:null,company_id:null,unit_id:null},order_id:null,lines:{}},null,2)+'\n');
|
||||
fs.writeFileSync(path.join(stage,'principal.template.json'),JSON.stringify({selected:false,enabled:false,client_id:null,token_sha256:null,user_id:null,auth_version:null,tenant_id:null,company_id:null,unit_id:null,context_version:null},null,2)+'\n');
|
||||
fs.writeFileSync(path.join(stage,'source-mapping.template.json'),JSON.stringify({approved:false,label:null,approved_by:null,approved_at:null,source_supplier_id:null,mapping:{id:null,version:null,authority:'sgf_only',source_scope:{tenant_id:null},target_scope:{tenant_id:null,company_id:null,unit_id:null},order_id:null,lines:{}}},null,2)+'\n');
|
||||
fs.writeFileSync(path.join(stage,'manifest.json'),JSON.stringify(plan,null,2)+'\n');
|
||||
}
|
||||
}
|
||||
assert(fs.existsSync(path.join(stage,'manifest.json')),'Preparation must exist before check');
|
||||
assert.deepEqual(JSON.parse(fs.readFileSync(path.join(stage,'manifest.json'))),plan);
|
||||
for(const entry of entries)assert.equal(hash(path.join(stage,'8085',entry.file)),entry.after_sha256);
|
||||
for(const entry of clientFiles)assert.equal(hash(path.join(stage,entry.target)),entry.sha256);
|
||||
assert.deepEqual(JSON.parse(fs.readFileSync(path.join(stage,'mapping.template.json'))),{selected:false,id:null,version:null,authority:'sgf_only',source_scope:{tenant_id:null},target_scope:{tenant_id:null,company_id:null,unit_id:null},order_id:null,lines:{}});
|
||||
assert.deepEqual(JSON.parse(fs.readFileSync(path.join(stage,'principal.template.json'))),{selected:false,enabled:false,client_id:null,token_sha256:null,user_id:null,auth_version:null,tenant_id:null,company_id:null,unit_id:null,context_version:null});
|
||||
assert.deepEqual(JSON.parse(fs.readFileSync(path.join(stage,'source-mapping.template.json'))),{approved:false,label:null,approved_by:null,approved_at:null,source_supplier_id:null,mapping:{id:null,version:null,authority:'sgf_only',source_scope:{tenant_id:null},target_scope:{tenant_id:null,company_id:null,unit_id:null},order_id:null,lines:{}}});
|
||||
for(const source of prior.sources)assert.equal(hash(path.join(state.app,source.file)),source.sha256);
|
||||
for(const source of before)assert.equal(hash(path.join(root,source.file)),source.sha256);
|
||||
for(const entry of entries)assert.equal(fs.existsSync(path.join(state.app,entry.file))?hash(path.join(state.app,entry.file)):null,entry.before_sha256);
|
||||
console.log(JSON.stringify({passed:true,command,preparation_id:id,endpoint_files:entries.length,client_files:clientFiles.length,target_code_changed:false,target_state_changed:false,credentials_created:false,mappings_assigned:false,installed:false,activated:false,new_demo:false}));
|
||||
@@ -1,7 +1,11 @@
|
||||
<?php
|
||||
$s=json_decode(file_get_contents(getenv('RECEIPT_COMPOSITION_STATE')),true,64,JSON_THROW_ON_ERROR);
|
||||
if(is_file($s['app'].'/public'.parse_url($_SERVER['REQUEST_URI'],PHP_URL_PATH))&&preg_match('~^/(purchasing-exit\.mjs|purchasing-board\.mjs|purchasing-presenter\.mjs|stock-receipt-focus\.mjs|obligation-evidence\.mjs|invoice-matching\.mjs|request-history\.mjs|purchasing-focus\.mjs|receipt-workspace\.mjs|order-workspace\.mjs|sourcing-workspace\.mjs|supplier-workspace\.mjs|supplier-documents\.mjs|supplier-picker-model\.mjs|purchasing-dossier\.mjs|request-workspace\.mjs|geography-workspace\.mjs|supplier-obligation-workspace\.mjs|supplier-invoice-workspace\.mjs|sgf-module-context\.js|build/assets/[A-Za-z0-9_.-]+)$~',parse_url($_SERVER['REQUEST_URI'],PHP_URL_PATH)))return false;
|
||||
require $s['app'].'/vendor/autoload.php';$app=require $s['app'].'/bootstrap/app.php';$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();$f=json_decode(file_get_contents($s['root'].'/fixture.json'),true,64,JSON_THROW_ON_ERROR);
|
||||
require_once __DIR__.'/sgf-selected-assets.php';
|
||||
$assetPath=parse_url($_SERVER['REQUEST_URI'],PHP_URL_PATH);
|
||||
if(is_string($assetPath)&&sgfSelectedAsset($s,$assetPath)!==null)return false;
|
||||
if(is_file($s['app'].'/public'.parse_url($_SERVER['REQUEST_URI'],PHP_URL_PATH))&&preg_match('~^/(purchasing-exit\.mjs|stock-operations-workspace\.mjs|sgf-finance-payables\.mjs|sgf-composed-ledger\.mjs|purchasing-board\.mjs|purchasing-presenter\.mjs|stock-receipt-focus\.mjs|obligation-evidence\.mjs|invoice-matching\.mjs|request-history\.mjs|purchasing-focus\.mjs|receipt-workspace\.mjs|order-workspace\.mjs|sourcing-workspace\.mjs|supplier-workspace\.mjs|supplier-documents\.mjs|supplier-picker-model\.mjs|purchasing-dossier\.mjs|request-workspace\.mjs|geography-workspace\.mjs|supplier-obligation-workspace\.mjs|supplier-invoice-workspace\.mjs|sgf-module-context\.js|build/assets/[A-Za-z0-9_.-]+)$~',parse_url($_SERVER['REQUEST_URI'],PHP_URL_PATH)))return false;
|
||||
if(($s['singleInstallation']??false)===true&&($s['tag']??null)==='cc50a276c10c'&&is_string($assetPath)&&str_ends_with($assetPath,'.mjs')){http_response_code(404);header('Content-Type: text/plain; charset=UTF-8');echo 'Recurso de cliente indisponível.';return;}
|
||||
$loader=require $s['app'].'/vendor/autoload.php';if(($s['singleInstallation']??false)===true){$loader->setPsr4('App\\',$s['app'].'/app');$map=[];foreach($loader->getClassMap() as $class=>$file)if(str_starts_with($class,'App\\')){$local=$s['app'].'/app/'.str_replace('\\','/',substr($class,4)).'.php';if(is_file($local))$map[$class]=$local;}$loader->addClassMap($map);}$app=require $s['app'].'/bootstrap/app.php';$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();$f=json_decode(file_get_contents($s['root'].'/fixture.json'),true,64,JSON_THROW_ON_ERROR);
|
||||
Illuminate\Support\Facades\Route::middleware('web')->get('/qa/enter/{role}',function(Illuminate\Http\Request $r,string $role)use($f,$s){abort_unless(in_array($role,['actor','validator','issuer',...(($s['invoices']??false)?['reviewer','auditor']:[]),...(($s['requests']??false)?['requester','approver']:[])],true),404);$user=App\Models\User::findOrFail($f[$role]);auth()->login($user);$r->session()->put('auth_version',$user->auth_version);$token=bin2hex(random_bytes(20));$scope=$f['scope'];if($r->query('foreign'))$scope['unit_id']=$f['foreign_unit'];$r->session()->put('company_context',$scope+['token'=>$token,'module_id'=>$r->query('module','purchasing')]);if($r->boolean('open')&&($s['stockComposition']??false)&&$r->query('module')==='stock')return redirect('/');if($r->boolean('open')&&($s['integrated']??false)&&!$r->has('geography')&&!$r->has('obligations'))return redirect('/purchasing/workspace');if($r->boolean('open')&&($s['geography']??false)&&in_array($r->query('geography'),['company','unit','client'],true))return redirect('/geography/'.$r->query('geography').'/'.($r->query('geography')==='company'?$f['company']:($r->query('geography')==='unit'?$f['unit']:$f['client'])));if($r->boolean('open')&&($s['obligations']??false)&&$r->boolean('obligations'))return redirect('/purchasing/obligations');if($r->boolean('open')&&($s['invoices']??false)&&in_array($role,['issuer','reviewer','auditor'],true))return redirect('/purchasing/invoices');if($r->boolean('open'))return redirect(($s['requests']??false)&&in_array($role,['requester','approver'],true)?'/purchasing/requests':(($s['suppliers']??false)&&$role!=='validator'?'/purchasing/suppliers':(($s['sourcing']??false)&&$role!=='validator'?'/purchasing/sourcing':(($s['orders']??false)&&$role!=='validator'?'/purchasing/orders':'/purchasing/receipts'))));return response()->json(['csrf'=>csrf_token(),'context'=>$token]);});
|
||||
if(($s['singleInstallation']??false)===true){
|
||||
App\Modules\Purchasing\BudgetAdapter::guard();
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
$repo=dirname(__DIR__);$sgc='C:/laragon/www/compras';
|
||||
$run=json_decode(file_get_contents('C:/laragon/data/sgf-tempo/run/modular-regression-current.json'),true);
|
||||
if($run['id']!=='2576427d1939'||$run['port']!==55439)throw new LogicException('Existing regression cluster only');
|
||||
$root=getenv('SGC_DISPATCH_PG_ROOT');if(!$root||dirname(str_replace('\\','/',$root))!==str_replace('\\','/',$run['root'].'/private')||!preg_match('/^sgc-dispatch-pg-[a-f0-9]{12}$/D',basename($root)))throw new LogicException('Private proof root');
|
||||
$source=json_decode(file_get_contents($run['root'].'/private/receipt-composition-b4093c6f6707/state.json'),true);
|
||||
$n=$source['nativeConnection'];if($n['database']!=='compras_receipts_test_b4093c6f6707'||$n['username']!=='payable_history_native_b4093c6f6707')throw new LogicException('Source regression book only');
|
||||
$private=json_decode(file_get_contents($root.'/settings.json'),true);
|
||||
foreach(['APP_BASE_PATH'=>$sgc,'APP_ENV'=>'testing','APP_KEY'=>$private['app_key'],'APP_DEBUG'=>'false','DB_CONNECTION'=>'pgsql','DB_HOST'=>'127.0.0.1','DB_PORT'=>'55439','DB_DATABASE'=>$n['database'],'DB_USERNAME'=>$n['username'],'DB_PASSWORD'=>$n['password'],'DB_URL'=>'',
|
||||
'SESSION_DRIVER'=>'array','CACHE_STORE'=>'array','QUEUE_CONNECTION'=>'sync','COMMON_PAYABLES_ENABLED'=>'false','COMMON_PAYABLES_HISTORY_ENABLED'=>'false','COMMON_PAYABLES_HISTORY_DECISIONS_ENABLED'=>'false',
|
||||
'APP_CONFIG_CACHE'=>$root.'/config.php','APP_SERVICES_CACHE'=>$root.'/services.php','APP_PACKAGES_CACHE'=>$root.'/packages.php','APP_ROUTES_CACHE'=>$root.'/routes.php']as$k=>$v){putenv($k.'='.$v);$_ENV[$k]=$v;$_SERVER[$k]=$v;}
|
||||
foreach(['logs','framework/views','framework/sessions','framework/cache/data']as$d)if(!is_dir($root.'/storage/'.$d))mkdir($root.'/storage/'.$d,0700,true);
|
||||
require $sgc.'/vendor/autoload.php';$app=require $sgc.'/bootstrap/app.php';$app->addAbsoluteCachePathPrefix('C:');$app->useStoragePath($root.'/storage');$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
$db=Illuminate\Support\Facades\DB::connection();$identity=$db->selectOne("SELECT current_database() db,current_user role,current_setting('cluster_name') cluster,rolsuper OR rolcreatedb OR rolcreaterole OR rolreplication OR rolbypassrls elevated FROM pg_roles WHERE rolname=current_user");
|
||||
if($identity->db!==$n['database']||$identity->role!==$n['username']||$identity->cluster!=='sgf_modular_regression_'.$run['id']||$identity->elevated)throw new LogicException('Physical source identity refused');
|
||||
$actor=App\Models\User::where('tenant_id',1)->where('purchasing_role','finance')->findOrFail(4);auth()->guard('web')->setUser($actor);
|
||||
$receiverRoot=json_decode(file_get_contents($run['root'].'/private/sgc-invoice-current.json'),true)['root'];
|
||||
$receiverSecret=json_decode(file_get_contents($receiverRoot.'/client.json'),true);$fixture=json_decode(file_get_contents($receiverRoot.'/source-fixture.json'),true);
|
||||
$localOrder=$db->table('purchase_orders')->where('tenant_id',1)->first();
|
||||
$mapping=['id'=>'QA-INTERAPP','version'=>1,'source_scope'=>['tenant_id'=>1],'target_scope'=>['tenant_id'=>1,'company_id'=>2,'unit_id'=>2],'authority'=>'sgf_only','order_id'=>$fixture['order'],'lines'=>['QA-SOURCE-LINE'=>$fixture['line']]];
|
||||
config(['sgf_invoice.workspace_enabled'=>true,'sgf_invoice.enabled'=>true,'sgf_invoice.client_package'=>$repo.'/packages/sgc-invoice-client/src/InvoiceClient.php','sgf_invoice.client_package_sha256'=>hash_file('sha256',$repo.'/packages/sgc-invoice-client/src/InvoiceClient.php'),
|
||||
'sgf_invoice.connections'=>['qa'=>['enabled'=>true,'client_id'=>'synthetic','token'=>$receiverSecret['token'],'context_version'=>$receiverSecret['context'],'origin'=>'http://127.0.0.1:8085','source_scope'=>['tenant_id'=>1],
|
||||
'target_scope'=>['tenant_id'=>1,'company_id'=>2,'unit_id'=>2],'mappings'=>['map'=>['approved'=>true,'label'=>'QA bridge only','source_supplier_id'=>(int)$localOrder->supplier_id,'mapping'=>$mapping]]]]]);
|
||||
Illuminate\Support\Facades\Http::preventStrayRequests();$bridgeCalls=0;
|
||||
Illuminate\Support\Facades\Http::fake(function(Illuminate\Http\Client\Request $r)use($receiverRoot,$receiverSecret,$repo,$root,&$bridgeCalls){
|
||||
if(!str_starts_with($r->url(),'http://127.0.0.1:8085/api/v1/integration/sgc/invoices'))throw new LogicException('Unexpected bridge destination');
|
||||
$headers=[];foreach(['Authorization','X-SGC-Client','X-SGC-Context','X-Idempotency-Key']as$h)$headers[$h]=$r->header($h)[0]??null;
|
||||
if($headers['Authorization']!=='Bearer '.$receiverSecret['token']||$headers['X-SGC-Context']!==$receiverSecret['context']||$headers['X-SGC-Client']!=='synthetic')return Illuminate\Support\Facades\Http::response([],401);
|
||||
if(($r['phase']??null)==='confirm'&&getenv('SGC_BRIDGE_DELAY')==='true'){file_put_contents($root.'/sending-ready','ready');usleep(1800000);}
|
||||
$name='source-'.substr(basename($root),-12).'-'.getmypid().'-'.(++$bridgeCalls);$job=$name.'.json';$out=$name.'-result.json';
|
||||
file_put_contents($receiverRoot.'/'.$job,json_encode(['method'=>$r->method(),'path'=>parse_url($r->url(),PHP_URL_PATH),'body'=>$r->data(),'headers'=>$headers],JSON_THROW_ON_ERROR));
|
||||
$p=new Symfony\Component\Process\Process(['C:/laragon/tools/sgf-tempo/php-8.4.27/php.exe',$repo.'/scripts/sgc-dispatch-receiver.php',$job,$out],$repo,['SGC_INVOICE_PROOF_ROOT'=>$receiverRoot],null,30);$p->run();
|
||||
if(!$p->isSuccessful()||!is_file($receiverRoot.'/'.$out)){file_put_contents($root.'/receiver-error.log',$p->getOutput().$p->getErrorOutput());throw new RuntimeException('Receiver kernel incomplete');}
|
||||
$response=json_decode(file_get_contents($receiverRoot.'/'.$out),true);
|
||||
if(($r['phase']??null)==='confirm'&&getenv('SGC_BRIDGE_LOSE')==='true'){putenv('SGC_BRIDGE_LOSE=false');throw new Illuminate\Http\Client\ConnectionException('Synthetic source response lost after receiver commit');}
|
||||
return Illuminate\Support\Facades\Http::response($response['data'],$response['status']);
|
||||
});
|
||||
$service=app(App\Services\SgfInvoiceDispatches::class);
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
require __DIR__.'/sgc-dispatch-pg-bootstrap.php';
|
||||
use Illuminate\Support\Facades\{DB,Schema};
|
||||
if(($argv[1]??'')==='--worker'){
|
||||
$job=json_decode(file_get_contents($root.'/'.basename($argv[2])),true);
|
||||
try{$row=$service->dispatch($actor,$job['id'],$job['version'],'confirm','Confirmação individual sintética de concorrência.',$job['approval']);$result=['status'=>200,'phase'=>$row->phase,'receipt'=>json_decode($row->receipt,true)];}
|
||||
catch(Symfony\Component\HttpKernel\Exception\HttpException $e){$result=['status'=>$e->getStatusCode()];}
|
||||
file_put_contents($root.'/'.basename($argv[3]),json_encode($result,JSON_THROW_ON_ERROR));exit;
|
||||
}
|
||||
if(is_file($root.'/result.json'))throw new LogicException('Preserve completed source proof');
|
||||
$checks=[];$ok=function($v,$name)use(&$checks,$root){if(!$v)throw new RuntimeException($name);$checks[]=$name;file_put_contents($root.'/progress.json',json_encode($checks));};
|
||||
$snapshot=function($c,?array $tables=null,?array $sequences=null){$tables??=array_column($c->select("SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename"),'tablename');$out=[];foreach($tables as$t){if(!preg_match('/^[a-z_][a-z0-9_]*$/D',$t))throw new LogicException('Table');$out[$t]=(array)$c->selectOne('SELECT count(*) rows,md5(coalesce(string_agg(row_to_json(x)::text,chr(10) ORDER BY row_to_json(x)::text),\'\')) fingerprint FROM "'.$t.'" x');}$q=$c->table('pg_sequences')->where('schemaname','public');if($sequences!==null)$q->whereIn('sequencename',$sequences);$out['sequences']=$q->orderBy('sequencename')->get(['sequencename','last_value']);return hash('sha256',json_encode($out,JSON_THROW_ON_ERROR));};
|
||||
$selected=json_decode(file_get_contents($run['root'].'/private/receipt-composition-cc50a276c10c/state.json'),true);$receiver=json_decode(file_get_contents($run['root'].'/private/receipt-composition-da77bb730751/state.json'),true);
|
||||
config(['database.connections.protected_host'=>array_replace(config('database.connections.pgsql'),['host'=>'127.0.0.1','port'=>55439,'database'=>$selected['env']['DB_DATABASE'],'username'=>$selected['env']['DB_USERNAME'],'password'=>$selected['env']['DB_PASSWORD'],'url'=>null]),
|
||||
'database.connections.protected_native'=>$selected['nativeConnection'],'database.connections.receiver_verify'=>$receiver['nativeConnection']]);
|
||||
$protectedBefore=[$snapshot(DB::connection('protected_host')),$snapshot(DB::connection('protected_native'))];
|
||||
$tables=array_column($db->select("SELECT tablename FROM pg_tables WHERE schemaname='public' AND tablename NOT IN ('sgf_invoice_dispatches','sgf_invoice_dispatch_events') ORDER BY tablename"),'tablename');
|
||||
$seq=array_column($db->select("SELECT sequencename FROM pg_sequences WHERE schemaname='public' AND sequencename NOT LIKE 'sgf_invoice_dispatch%' ORDER BY sequencename"),'sequencename');
|
||||
if(Schema::hasTable('sgf_invoice_dispatches')&&!is_file($root.'/progress.json'))throw new LogicException('Only this interrupted proof may resume; never reset previous fixtures');
|
||||
$sourceBefore=$snapshot($db,$tables,$seq);if(!Schema::hasTable('sgf_invoice_dispatches')){$m=require $sgc.'/database/migrations/2026_11_10_000001_create_sgf_invoice_dispatches.php';$m->up();}
|
||||
$ok(Schema::hasTable('sgf_invoice_dispatches')&&Schema::hasTable('sgf_invoice_dispatch_events'),'two source persistence tables installed only in existing regression book');
|
||||
$receiverDb=DB::connection('receiver_verify');$receivedBefore=$receiverDb->table('invoices')->count();$budgetBefore=$receiverDb->table('budget_consumptions')->sum('amount_minor');$movesBefore=$receiverDb->table('stock_movements')->count();$paymentsBefore=$receiverDb->table('external_payments')->count();
|
||||
$data=function($suffix)use($root){$ref='QA-SOURCE-'.substr(basename($root),-12).'-'.$suffix;return ['reference'=>$ref,'connection_id'=>'qa','mapping_id'=>'map','reason'=>'Recepção documental sintética, autoridade única SGF.','document'=>[
|
||||
'number'=>$ref,'issued_on'=>'2026-11-10','subtotal'=>'100.00','tax'=>'0.00','total'=>'100.00','items'=>[['source_line_ref'=>'QA-SOURCE-LINE','quantity'=>'1.000','unit'=>'UN','unit_price'=>'100.00','tax_rate'=>'0.00']]]];};
|
||||
$existing=$db->table('sgf_invoice_dispatches')->where('tenant_id',1)->where('reference',$data('lost')['reference'])->first();
|
||||
$id=$existing?->id??$service->create($actor,$data('lost'));$version=$existing?->version??1;$row=$service->dispatch($actor,$id,$version,'preview','Conferir impacto real no livro receptor QA.',null);
|
||||
$ok($row->phase==='previewed','actual SGC service persists real SGF kernel preview');$key=$row->operation_key;
|
||||
putenv('SGC_BRIDGE_LOSE=true');$row=$service->dispatch($actor,$id,$row->version,'confirm','Aprovar individualmente o impacto recebido.',$service->presentation($actor,$row)['approval_hash']);
|
||||
$ok($row->phase==='uncertain'&&(bool)$row->ever_sending,'remote commit with lost response is durably uncertain at source');
|
||||
$row=$service->dispatch($actor,$id,$row->version,'recover','Recuperar a mesma intenção e o recibo original.',null);
|
||||
$ok($row->phase==='received_pending'&&$row->operation_key===$key&&json_decode($row->receipt,true)['replayed'],'source recovers one actual pending invoice from receiver');
|
||||
$ok($receiverDb->table('invoices')->count()===$receivedBefore+1,'first end-to-end intent creates exactly one receiver invoice');
|
||||
$local=App\Models\PurchaseOrder::where('tenant_id',1)->first();
|
||||
try{app(App\Services\InvoiceWorkflow::class)->register($actor,$local,['number'=>$data('lost')['document']['number']]);throw new RuntimeException('Unexpected local invoice');}
|
||||
catch(Illuminate\Validation\ValidationException $e){$ok(str_contains($e->getMessage(),'Documento reservado'),'native SGC writer refuses claimed invoice before any local duplicate');}
|
||||
$id2=$service->create($actor,$data('race'));$row2=$service->dispatch($actor,$id2,1,'preview','Conferir segundo impacto para concorrência.',null);
|
||||
file_put_contents($root.'/race.json',json_encode(['id'=>$id2,'version'=>$row2->version,'approval'=>$service->presentation($actor,$row2)['approval_hash']],JSON_THROW_ON_ERROR));
|
||||
$a=new Symfony\Component\Process\Process([PHP_BINARY,__FILE__,'--worker','race.json','race-a.json'],$repo,['SGC_DISPATCH_PG_ROOT'=>$root,'SGC_BRIDGE_DELAY'=>'true'],null,40);$a->start();
|
||||
for($i=0;$i<160&&!is_file($root.'/sending-ready');$i++)usleep(50000);if(!is_file($root.'/sending-ready'))throw new RuntimeException('First sender did not reach network');
|
||||
$b=new Symfony\Component\Process\Process([PHP_BINARY,__FILE__,'--worker','race.json','race-b.json'],$repo,['SGC_DISPATCH_PG_ROOT'=>$root],null,40);$b->start();
|
||||
foreach([$a,$b]as$p){$p->wait();if(!$p->isSuccessful()){file_put_contents($root.'/worker-error.log',$p->getOutput().$p->getErrorOutput());throw new RuntimeException('Sender worker incomplete');}}
|
||||
$ra=json_decode(file_get_contents($root.'/race-a.json'),true);$rb=json_decode(file_get_contents($root.'/race-b.json'),true);
|
||||
$ok($ra['status']===200&&$ra['phase']==='received_pending'&&$rb['status']===409,'concurrent source decisions yield one send and stale/in-flight refusal');
|
||||
$ok($receiverDb->table('invoices')->count()===$receivedBefore+2,'second end-to-end intent creates only one receiver invoice');
|
||||
$row2=$service->row($actor,$id2);$ok($row2->phase==='received_pending'&&$db->table('sgf_invoice_dispatch_events')->where('dispatch_id',$id2)->where('action','decision.confirmed')->count()===1,'source has one durable decision and receipt for concurrent intent');
|
||||
$db->beginTransaction();try{$db->table('sgf_invoice_dispatch_events')->where('dispatch_id',$id2)->update(['action'=>'tampered']);throw new RuntimeException('Audit mutable');}catch(Illuminate\Database\QueryException){$db->rollBack();$ok(true,'PostgreSQL refuses rewriting source event history');}
|
||||
$ok($snapshot($db,$tables,$seq)===$sourceBefore,'all original source business rows and sequences preserved');
|
||||
$ok($receiverDb->table('budget_consumptions')->sum('amount_minor')===$budgetBefore&&$receiverDb->table('stock_movements')->count()===$movesBefore&&$receiverDb->table('external_payments')->count()===$paymentsBefore,'receiver budget stock and payments unchanged by pending-document intake');
|
||||
$ok([$snapshot(DB::connection('protected_host')),$snapshot(DB::connection('protected_native'))]===$protectedBefore,'8085 all host/native rows and sequences unchanged');
|
||||
$ok($db->table('sgf_invoice_dispatch_events')->where('action','decision.confirmed')->count()===2,'exactly two individually audited decisions for two sources');
|
||||
file_put_contents($root.'/result.json',json_encode(['passed'=>true,'checks'=>$checks,'source_book'=>'b4093c6f6707','receiver_book'=>'da77bb730751','new_receiver_pending_invoices'=>2,
|
||||
'source_original_fingerprint'=>$sourceBefore,'protected_8085_fingerprints'=>$protectedBefore,'source_statuses'=>[200,409],'new_database'=>false,'new_role'=>false,'new_server'=>false,'tcp_8085'=>false,
|
||||
'two_independent_application_kernels'=>true,'pilot_migration'=>false,'activation'=>false,'publication'=>false],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo json_encode(['passed'=>true,'checks'=>count($checks)]).PHP_EOL;
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
// Parent is the SGC application. Do not let its server-adapter cache/DB variables
|
||||
// shadow the receiver harness's explicit isolated environment.
|
||||
foreach(array_keys($_SERVER)as$key)if(preg_match('/^(APP_|DB_|SGF_|SESSION_|CACHE_|QUEUE_|BCRYPT_)/',$key))unset($_SERVER[$key]);
|
||||
require __DIR__.'/sgc-invoice-proof-bootstrap.php';
|
||||
$input=json_decode(file_get_contents($proofRoot.'/'.basename($argv[1])),true,128,JSON_THROW_ON_ERROR);
|
||||
$headers=[];foreach(['Authorization'=>'HTTP_AUTHORIZATION','X-SGC-Client'=>'HTTP_X_SGC_CLIENT','X-SGC-Context'=>'HTTP_X_SGC_CONTEXT']as$from=>$to)$headers[$to]=$input['headers'][$from]??null;
|
||||
$r=$send($input['body']??[],$input['headers']['X-Idempotency-Key']??null,$headers,$input['path'],null,$input['method']);
|
||||
file_put_contents($proofRoot.'/'.basename($argv[2]),json_encode($r,JSON_THROW_ON_ERROR));
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
require __DIR__.'/sgc-invoice-proof-bootstrap.php';
|
||||
use Illuminate\Support\Facades\{Crypt,DB};
|
||||
if(!is_file($proofRoot.'/context-result.json')||is_file($proofRoot.'/authority-result.json'))throw new LogicException('Fresh authority continuation required');
|
||||
$p=json_decode(file_get_contents($proofRoot.'/initial-preview.json'),true);$cut=json_decode(Crypt::decryptString($p['data']['preview']),true);
|
||||
$body=['phase'=>'confirm','order_id'=>$cut['order_id'],'preview'=>$p['data']['preview']];$key=$cut['operation_key'];
|
||||
$checks=[];$ok=function($v,$name)use(&$checks){if(!$v)throw new RuntimeException($name);$checks[]=$name;};
|
||||
$invoices=$native->table('invoices')->orderBy('id')->get()->toJson();$events=$native->table('audit_logs')->count();
|
||||
$ok($send($body,$key)['status']===200,'current native operator lock permits authorised recovery');
|
||||
$nativeRuntime=DB::connection('receipt_native');$role=$nativeRuntime->table('users')->where('id',$cut['actor'])->value('purchasing_role');
|
||||
$nativeRuntime->beginTransaction();try{$nativeRuntime->table('users')->where('id',$cut['actor'])->update(['purchasing_role'=>'warehouse']);$ok($send($body,$key)['status']===403,'revoked native finance function prevents receipt recovery');}finally{$nativeRuntime->rollBack();}
|
||||
$ok($nativeRuntime->table('users')->where('id',$cut['actor'])->value('purchasing_role')===$role,'native finance fixture restored by transaction rollback');
|
||||
$app->instance(Tempo\Purchasing\Embedded\Services\OrganizationAccess::class,new class extends Tempo\Purchasing\Embedded\Services\OrganizationAccess{
|
||||
public function maintenanceOnly(Tempo\Purchasing\Embedded\Models\User $user):bool{return true;}
|
||||
});
|
||||
try{$ok($send($body,$key)['status']===403,'maintenance-only native operator cannot recover invoice receipt');}
|
||||
finally{$app->forgetInstance(Tempo\Purchasing\Embedded\Services\OrganizationAccess::class);}
|
||||
$ok($send($body,$key)['status']===200,'normal recovery resumes without another business command');
|
||||
$ok($invoices===$native->table('invoices')->orderBy('id')->get()->toJson()&&$events===$native->table('audit_logs')->count(),'operator guard checks add no invoice or audit commands');
|
||||
file_put_contents($proofRoot.'/authority-result.json',json_encode(['passed'=>true,'checks'=>$checks,'new_business_rows'=>0,'previous_context_sha256'=>hash_file('sha256',$proofRoot.'/context-result.json'),
|
||||
'sources'=>[['file'=>'app/Modules/Purchasing/SgcInvoiceIngress.php','sha256'=>hash_file('sha256',$repo.'/app/Modules/Purchasing/SgcInvoiceIngress.php')]]],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
|
||||
echo json_encode(['passed'=>true,'checks'=>count($checks)]).PHP_EOL;
|
||||
@@ -0,0 +1,23 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import crypto from 'node:crypto';
|
||||
import {spawnSync} from 'node:child_process';
|
||||
|
||||
// No provisioning, server launch, database migration, real credentials or grants.
|
||||
const repo=process.cwd(),run=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json'));
|
||||
if(run.id!=='2576427d1939'||run.port!==55439||path.resolve(run.root)!==path.resolve('C:/laragon/data/sgf-tempo/run/modular-regression-'+run.id))throw Error('Known existing QA cluster only');
|
||||
const pointer=path.join(run.root,'private/sgc-invoice-current.json');
|
||||
if(process.argv.includes('--new')){
|
||||
const root=path.join(run.root,'private','sgc-invoice-proof-'+crypto.randomBytes(6).toString('hex'));
|
||||
fs.mkdirSync(root);fs.writeFileSync(path.join(root,'client.json'),JSON.stringify({token:crypto.randomBytes(32).toString('hex'),context:crypto.randomBytes(32).toString('hex')}));
|
||||
fs.writeFileSync(pointer,JSON.stringify({root}));
|
||||
for(const [script,result]of [['sgc-invoice-proof.php','result.json'],['sgc-invoice-security-proof.php','security-result.json'],['sgc-invoice-context-proof.php','context-result.json'],['sgc-invoice-authority-proof.php','authority-result.json']]){
|
||||
const r=spawnSync('C:/laragon/tools/sgf-tempo/php-8.4.27/php.exe',['scripts/'+script],{cwd:repo,env:{...process.env,SGC_INVOICE_PROOF_ROOT:root},windowsHide:true,encoding:'utf8',timeout:180000});
|
||||
fs.writeFileSync(path.join(root,script+'.log'),(r.stdout??'')+(r.stderr??''));
|
||||
if(r.status!==0||!fs.existsSync(path.join(root,result))||JSON.parse(fs.readFileSync(path.join(root,result))).passed!==true)throw Error('Proof incomplete; inspect private log, preserve accepted steps before retrying');
|
||||
}
|
||||
}
|
||||
const {root}=JSON.parse(fs.readFileSync(pointer));if(path.dirname(path.resolve(root))!==path.resolve(run.root,'private')||!/^sgc-invoice-proof-[a-f0-9]{12}$/.test(path.basename(root)))throw Error('Private proof root invalid');
|
||||
const results=['result.json','security-result.json','context-result.json','authority-result.json'].map(file=>JSON.parse(fs.readFileSync(path.join(root,file))));
|
||||
if(results.some(x=>x.passed!==true))throw Error('Incomplete proof');
|
||||
console.log(JSON.stringify({passed:true,checks:results.reduce((n,x)=>n+x.checks.length,0),existing_regression_book:'da77bb730751',new_demo:false,new_server:false,endpoint_enabled_in_source:false,credentials_printed:false}));
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
require __DIR__.'/sgc-invoice-proof-bootstrap.php';
|
||||
require_once $repo.'/packages/sgc-invoice-client/src/InvoiceClient.php';
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Tempo\SgcInvoiceClient\InvoiceClient;
|
||||
if(!is_file($proofRoot.'/authority-result.json')||is_file($proofRoot.'/client-kernel-result.json'))throw new LogicException('Fresh client contract proof required');
|
||||
$old=json_decode(file_get_contents($proofRoot.'/initial-preview.json'),true);$cut=json_decode(Crypt::decryptString($old['data']['preview']),true);
|
||||
$checks=[];$ok=function($v,$name)use(&$checks){if(!$v)throw new RuntimeException($name);$checks[]=$name;};
|
||||
$invoiceBefore=$native->table('invoices')->orderBy('id')->get()->toJson();$auditBefore=$native->table('audit_logs')->count();
|
||||
$calls=[];
|
||||
$client=new InvoiceClient('synthetic',$secret['token'],$secret['context'],['tenant_id'=>91],$f['scope'],
|
||||
function($method,$url,$headers,$body)use($send,&$calls){$calls[]=['method'=>$method,'path'=>parse_url($url,PHP_URL_PATH),'key'=>$headers['X-Idempotency-Key']];
|
||||
$response=$send($body??[],$headers['X-Idempotency-Key'],[],parse_url($url,PHP_URL_PATH),null,$method);
|
||||
return ['status'=>$response['status'],'json'=>$response['data']];});
|
||||
$mapping=['id'=>'QA-PROTOCOL-MAPPING','version'=>1,'source_scope'=>['tenant_id'=>91],'target_scope'=>$f['scope'],
|
||||
'order_id'=>$cut['order_id'],'lines'=>[],'authority'=>'sgf_only'];
|
||||
$document=$cut['data'];foreach($document['items']as$i=>&$line){$ref='QA-SOURCE-LINE-'.$i;$mapping['lines'][$ref]=$line['purchase_order_item_id'];unset($line['purchase_order_item_id']);$line=['source_line_ref'=>$ref]+$line;}unset($line);
|
||||
$draft=['reference'=>'QA-PROTOCOL-DRAFT','version'=>'QA-v1','state'=>'unregistered_draft','document'=>$document];
|
||||
$intent=$client->intent($mapping,$draft,$cut['operation_key'],$cut['reason']);
|
||||
$received=$client->preview($intent,'QA-v1',InvoiceClient::mappingHash($mapping));
|
||||
$ok($received['phase']==='received_pending'&&$received['receipt']['replayed'],'prepared client recovers real existing native receipt through kernel');
|
||||
$ok(count($calls)===2&&$calls[0]['method']==='GET'&&$calls[1]['method']==='POST','client performs exact context read and explicit preview/recovery only');
|
||||
$ok($calls[0]['key']===$cut['operation_key']&&$calls[1]['key']===$cut['operation_key'],'native protocol retains one original UUID');
|
||||
$draft['document']['number']='QA-SGC-CLIENT-PREVIEW-'.substr(basename($proofRoot),-12);
|
||||
$fresh=$client->intent($mapping,$draft,(string)Illuminate\Support\Str::uuid(),$cut['reason']);
|
||||
$preview=$client->preview($fresh,'QA-v1',InvoiceClient::mappingHash($mapping));
|
||||
$ok($preview['phase']==='previewed'&&isset($preview['approval_hash'])&&$preview['impact']['budget_consumption_minor']===0,'prepared client consumes real native impact without confirming');
|
||||
$ok($invoiceBefore===$native->table('invoices')->orderBy('id')->get()->toJson()&&$auditBefore===$native->table('audit_logs')->count(),'client contract proof adds no invoice or audit rows');
|
||||
$ok(!str_contains(json_encode([$received,$preview]),$secret['token']),'serialised transport states exclude bearer credential');
|
||||
file_put_contents($proofRoot.'/client-kernel-result.json',json_encode(['passed'=>true,'checks'=>$checks,'new_business_rows'=>0,'kernel_http'=>true,'tcp_8085'=>false,
|
||||
'new_demo'=>false,'new_database'=>false,'sender_class_sha256'=>hash_file('sha256',$repo.'/packages/sgc-invoice-client/src/InvoiceClient.php'),
|
||||
'reused_endpoint_proof_sha256'=>hash_file('sha256',$repo.'/docs/proofs/sgc-invoice-endpoint-20261110.json')],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
|
||||
echo json_encode(['passed'=>true,'checks'=>count($checks)]).PHP_EOL;
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
require __DIR__.'/sgc-invoice-proof-bootstrap.php';
|
||||
if(!is_file($proofRoot.'/security-result.json')||is_file($proofRoot.'/context-result.json'))throw new LogicException('Fresh context proof only');
|
||||
$fixture=json_decode(file_get_contents($proofRoot.'/source-fixture.json'),true);$url='/api/v1/integration/sgc/invoices/orders/'.$fixture['order'];
|
||||
$before=$native->table('invoices')->count();$beforeAudit=$native->table('audit_logs')->count();
|
||||
$checks=[];$ok=function($v,$name)use(&$checks){if(!$v)throw new RuntimeException($name);$checks[]=$name;};
|
||||
$get=fn($path,$headers=[],$raw=null)=>$send([],null,$headers,$path,$raw,'GET');
|
||||
$r=$get($url);$ok($r['status']===200&&$r['data']['order']['id']===$fixture['order']&&strlen($r['data']['revision'])===64,'authenticated exact order exposes current revision and lines');
|
||||
$ok($r['no_store']&&!isset($r['data']['invoices']),'context response is uncached and excludes unrelated invoice list');
|
||||
$ok($get($url,['HTTP_AUTHORIZATION'=>null])['status']===401,'context read requires bearer');
|
||||
$ok($get('/api/v1/integration/sgc/invoices/orders/'.$nativeFixture['foreign_order'])['status']===403,'context read refuses foreign tenant order');
|
||||
$ok($get($url.'?unit_id=1')['status']===422,'context read rejects query authority');
|
||||
$ok($get($url,[],'{}')['status']===422,'context read rejects a request body');
|
||||
$host->beginTransaction();try{$host->table('permission_grants')->where('user_id',$f['issuer'])->where('permission','purchasing_invoices.view')->delete();$ok($get($url)['status']===403,'context read revalidates view grant');}finally{$host->rollBack();}
|
||||
config(['sgc_invoice.enabled'=>false]);$ok($get($url)['status']===404,'context route also remains disabled');config(['sgc_invoice.enabled'=>true]);
|
||||
$cut=json_decode(Illuminate\Support\Facades\Crypt::decryptString(json_decode(file_get_contents($proofRoot.'/initial-preview.json'),true)['data']['preview']),true);
|
||||
$retry=['phase'=>'preview','order_id'=>$fixture['order'],'revision'=>$cut['revision'],'reason'=>$cut['reason'],'data'=>$cut['data']];
|
||||
$ok($send($retry,$cut['operation_key'])['status']===200,'POST durable replay unchanged after adding read-only context route');
|
||||
$ok($native->table('invoices')->count()===$before&&$native->table('audit_logs')->count()===$beforeAudit,'context checks add no business effects');
|
||||
file_put_contents($proofRoot.'/context-result.json',json_encode(['passed'=>true,'checks'=>$checks,'new_business_rows'=>0,'previous_security_sha256'=>hash_file('sha256',$proofRoot.'/security-result.json')],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
|
||||
echo json_encode(['passed'=>true,'checks'=>count($checks)]).PHP_EOL;
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
// Test harness only. Reuses an existing regression book; never8085 or a new demo.
|
||||
$repo = dirname(__DIR__);
|
||||
$run = json_decode(file_get_contents('C:/laragon/data/sgf-tempo/run/modular-regression-current.json'), true, 64, JSON_THROW_ON_ERROR);
|
||||
$stateFile = $run['root'].'/private/receipt-composition-da77bb730751/state.json';
|
||||
$s = json_decode(file_get_contents($stateFile), true, 64, JSON_THROW_ON_ERROR);
|
||||
if ($run['id'] !== '2576427d1939' || $s['tag'] !== 'da77bb730751' || !is_file($s['root'].'/invoice-result.json') || is_file($s['root'].'/demo-server.json')) throw new LogicException('Existing regression only');
|
||||
$proofRoot = getenv('SGC_INVOICE_PROOF_ROOT');
|
||||
if (!$proofRoot || dirname(str_replace('\\','/',$proofRoot)) !== str_replace('\\','/',$run['root'].'/private') || !preg_match('/^sgc-invoice-proof-[a-f0-9]{12}$/D', basename($proofRoot))) throw new LogicException('Private proof path required');
|
||||
foreach ($s['env'] as $key=>$value) if (preg_match('/^(APP_|DB_|SGF_|SESSION_|CACHE_|QUEUE_|BCRYPT_)/', $key)) { putenv($key.'='.$value); $_ENV[$key] = $value; }
|
||||
$isolated = ['APP_BASE_PATH'=>$repo, 'APP_CONFIG_CACHE'=>$proofRoot.'/config.php', 'APP_ROUTES_CACHE'=>$proofRoot.'/routes.php',
|
||||
'APP_SERVICES_CACHE'=>$proofRoot.'/services.php', 'APP_PACKAGES_CACHE'=>$proofRoot.'/packages.php', 'SESSION_DRIVER'=>'array','CACHE_STORE'=>'array'];
|
||||
foreach ($isolated as $key=>$value) { putenv($key.'='.$value); $_ENV[$key]=$value; }
|
||||
foreach (['storage/logs','storage/framework/views','storage/framework/sessions','storage/framework/cache/data'] as $dir) if (!is_dir($proofRoot.'/'.$dir)) mkdir($proofRoot.'/'.$dir, 0700, true);
|
||||
require $repo.'/vendor/autoload.php';
|
||||
$app = require $repo.'/bootstrap/app.php'; $app->addAbsoluteCachePathPrefix('C:'); $app->useStoragePath($proofRoot.'/storage');
|
||||
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
App\Modules\Purchasing\BudgetAdapter::guard();
|
||||
$f = json_decode(file_get_contents($s['root'].'/fixture.json'), true);
|
||||
$nativeFixture = json_decode(file_get_contents($s['root'].'/native-fixture.json'), true);
|
||||
$secret = json_decode(file_get_contents($proofRoot.'/client.json'), true);
|
||||
$principal = ['enabled'=>true,'token_sha256'=>hash('sha256',$secret['token']), 'user_id'=>$f['issuer'], 'auth_version'=>1,
|
||||
'tenant_id'=>$f['tenant'],'company_id'=>$f['company'],'unit_id'=>$f['unit'],'context_version'=>$secret['context']];
|
||||
config(['sgc_invoice.enabled'=>true, 'sgc_invoice.principals'=>['synthetic'=>$principal], 'database.connections.sgc_verify'=>$s['nativeConnection'],
|
||||
'receipt_qa.requests_enabled'=>true]); // Test-only audited-origin reader; no request command is dispatched.
|
||||
$native = Illuminate\Support\Facades\DB::connection('sgc_verify');
|
||||
$host = Illuminate\Support\Facades\DB::connection();
|
||||
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
|
||||
$send = function (array $body, ?string $key=null, array $headers=[], string $path='/api/v1/integration/sgc/invoices', ?string $raw=null, string $method='POST') use ($secret,$kernel,$proofRoot) {
|
||||
$server = ['REMOTE_ADDR'=>'127.0.0.1','HTTP_ACCEPT'=>'application/json','CONTENT_TYPE'=>'application/json',
|
||||
'HTTP_AUTHORIZATION'=>'Bearer '.$secret['token'],'HTTP_X_SGC_CLIENT'=>'synthetic','HTTP_X_SGC_CONTEXT'=>$secret['context'],
|
||||
'HTTP_X_IDEMPOTENCY_KEY'=>$key??(string)Illuminate\Support\Str::uuid()];
|
||||
foreach ($headers as $name=>$value) { if ($value === null) unset($server[$name]); else $server[$name]=$value; }
|
||||
$request = Illuminate\Http\Request::create($path, $method, [], [], [], $server, $method==='GET'?$raw:($raw??json_encode($body, JSON_THROW_ON_ERROR)));
|
||||
$response = $kernel->handle($request); $kernel->terminate($request,$response);
|
||||
if ($response->getStatusCode()>=500) file_put_contents($proofRoot.'/last-error.json',$response->getContent());
|
||||
return ['status'=>$response->getStatusCode(),'data'=>json_decode($response->getContent(),true),'no_store'=>str_contains((string)$response->headers->get('Cache-Control'),'no-store')];
|
||||
};
|
||||
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
require __DIR__.'/sgc-invoice-proof-bootstrap.php';
|
||||
use Illuminate\Support\Facades\{DB, Route};
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
if (($argv[1]??'') === '--worker') {
|
||||
$job = json_decode(file_get_contents($proofRoot.'/'.basename($argv[2])), true);
|
||||
$result = $send($job['body'],$job['key']);
|
||||
file_put_contents($proofRoot.'/'.basename($argv[3]),json_encode($result,JSON_THROW_ON_ERROR));
|
||||
exit;
|
||||
}
|
||||
if (file_exists($proofRoot.'/result.json')) throw new LogicException('Preserve completed proof');
|
||||
$checks=[];
|
||||
$ok=function($value,$name) use (&$checks,$proofRoot) { if (!$value) throw new RuntimeException($name); $checks[]=$name; file_put_contents($proofRoot.'/progress.json',json_encode($checks)); };
|
||||
$snapshot = function ($db, ?array $tables=null) {
|
||||
$tables ??= array_column($db->select("SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename"),'tablename');
|
||||
$out=[];
|
||||
foreach ($tables as $table) { if (!preg_match('/^[a-z_][a-z0-9_]*$/D',$table)) throw new LogicException('Table');
|
||||
$out[$table]=(array)$db->selectOne('SELECT count(*) rows, md5(coalesce(string_agg(row_to_json(t)::text, chr(10) ORDER BY row_to_json(t)::text),\'\')) fingerprint FROM "'.$table.'" t'); }
|
||||
$out['sequences']=$db->select("SELECT sequencename,last_value FROM pg_sequences WHERE schemaname='public' ORDER BY sequencename");
|
||||
return hash('sha256',json_encode($out,JSON_THROW_ON_ERROR));
|
||||
};
|
||||
$selected=json_decode(file_get_contents($run['root'].'/private/receipt-composition-cc50a276c10c/state.json'),true);
|
||||
config(['database.connections.sgc_protected_host'=>array_replace(config('database.connections.pgsql'),[
|
||||
'host'=>'127.0.0.1','port'=>55439,'database'=>$selected['env']['DB_DATABASE'],'username'=>$selected['env']['DB_USERNAME'],'password'=>$selected['env']['DB_PASSWORD'],'url'=>null]),
|
||||
'database.connections.sgc_protected_native'=>$selected['nativeConnection']]);
|
||||
$protectedBefore=[$snapshot(DB::connection('sgc_protected_host')),$snapshot(DB::connection('sgc_protected_native'))];
|
||||
$hostBefore=$snapshot($host);
|
||||
$financialTables=['budget_entries','budget_consumptions','stock_movements','pa_obligations'];
|
||||
$effectsBefore=[]; foreach ($financialTables as $table) if ($native->getSchemaBuilder()->hasTable($table)) $effectsBefore[$table]=$native->table($table)->count();
|
||||
$existingInvoices=$native->table('invoices')->orderBy('id')->get()->toJson();
|
||||
$priorIds=$native->table('invoices')->pluck('id')->all();
|
||||
// SQL fixtures only: a distinct order/accepted line to exercise invoice intake. These
|
||||
// are not claimed as newly executed request/award/receipt/physical-stock workflows.
|
||||
$fixtureFile=$proofRoot.'/source-fixture.json';
|
||||
if(!is_file($fixtureFile)){
|
||||
$sourceOrder=$native->table('purchase_orders')->where('tenant_id',$nativeFixture['tenant'])->first();
|
||||
$sourceLine=$native->table('purchase_order_items')->where('purchase_order_id',$sourceOrder->id)->first();
|
||||
$copy=function($table,$sourceId,$replace=[])use($native){$row=(array)$native->table($table)->where('id',$sourceId)->first();unset($row['id']);return $native->table($table)->insertGetId(array_replace($row,$replace));};
|
||||
$fixture=$native->transaction(function()use($native,$copy,$sourceOrder,$sourceLine,$proofRoot,$f){
|
||||
$ref='QA-SGC-'.substr(basename($proofRoot),-12);
|
||||
$request=$copy('requisitions',$sourceOrder->requisition_id,['number'=>$ref]);
|
||||
$requestLine=$copy('requisition_items',$sourceLine->requisition_item_id,['requisition_id'=>$request]);
|
||||
$oldAward=$native->table('purchase_awards')->where('id',$sourceOrder->purchase_award_id)->first();
|
||||
$quote=$copy('quotations',$oldAward->quotation_id,['requisition_id'=>$request]);
|
||||
$award=$copy('purchase_awards',$oldAward->id,['requisition_id'=>$request,'quotation_id'=>$quote]);
|
||||
$order=$copy('purchase_orders',$sourceOrder->id,['number'=>$ref,'requisition_id'=>$request,'purchase_award_id'=>$award]);
|
||||
$line=$copy('purchase_order_items',$sourceLine->id,['purchase_order_id'=>$order,'requisition_item_id'=>$requestLine]);
|
||||
$receiptSource=$native->table('goods_receipt_items')->where('purchase_order_item_id',$sourceLine->id)->first();
|
||||
$receipt=$copy('goods_receipts',$receiptSource->goods_receipt_id,['purchase_order_id'=>$order,'number'=>$ref,'idempotency_key'=>(string)Str::uuid(),'delivery_note'=>$ref]);
|
||||
$copy('goods_receipt_items',$receiptSource->id,['goods_receipt_id'=>$receipt,'purchase_order_item_id'=>$line]);
|
||||
$oldOrigin=$native->table('audit_logs')->where('requisition_id',$sourceOrder->requisition_id)->first();
|
||||
$copy('audit_logs',$oldOrigin->id,['requisition_id'=>$request,'action'=>'sgf.request.command','before'=>null,
|
||||
'after'=>json_encode(['fixture_only'=>true,'request_scope'=>hash('sha256',json_encode($f['scope'],JSON_THROW_ON_ERROR))])]);
|
||||
return ['request'=>$request,'order'=>$order,'line'=>$line,'receipt'=>$receipt,'sql_fixture_only'=>true];
|
||||
});file_put_contents($fixtureFile,json_encode($fixture,JSON_THROW_ON_ERROR));
|
||||
}
|
||||
$fixture=json_decode(file_get_contents($fixtureFile),true);
|
||||
$order=$native->table('purchase_orders')->where('id',$fixture['order'])->first();
|
||||
$line=$native->table('purchase_order_items')->where('id',$fixture['line'])->first();
|
||||
$orderId=(int)$order->id;
|
||||
Route::post('/api/_sgc_test_state', fn(Illuminate\Http\Request $r)=>app(App\Modules\Purchasing\InvoiceController::class)->show($orderId,app(App\Modules\Purchasing\ReceiptRuntime::class)))
|
||||
->middleware(App\Http\Middleware\AuthenticateSgcInvoice::class);
|
||||
$state=fn()=> $send([],null,[], '/api/_sgc_test_state');
|
||||
$document=function(string $suffix)use($line,$proofRoot) {
|
||||
return ['number'=>'QA-SGC-'.basename($proofRoot).'-'.$suffix, 'issued_on'=>'2026-11-10','subtotal'=>'100.00','tax'=>'0.00','total'=>'100.00',
|
||||
'items'=>[['purchase_order_item_id'=>(int)$line->id,'quantity'=>'1.000','unit'=>$line->unit,'unit_price'=>'100.00','tax_rate'=>'0.00']]];
|
||||
};
|
||||
$preview=function(string $suffix, ?string $key=null)use($state,$send,$orderId,$document){
|
||||
$view=$state(); if($view['status']!==200)throw new RuntimeException('Native read refused: '.$view['status']);
|
||||
$body=['phase'=>'preview','order_id'=>$orderId,'revision'=>$view['data']['revision'],'reason'=>'Conferência sintética SGC, sem aprovação financeira.', 'data'=>$document($suffix)];
|
||||
$key??=(string)Str::uuid();return [$send($body,$key),$key,$body];
|
||||
};
|
||||
$confirm=fn($p,$key)=>$send(['phase'=>'confirm','order_id'=>$orderId,'preview'=>$p],$key);
|
||||
$firstState=$state(); $ok($firstState['status']===200,'existing regression native order readable with mapped identity');
|
||||
[$p,$key,$body]=$preview('main');
|
||||
file_put_contents($proofRoot.'/initial-preview.json', json_encode($p,JSON_THROW_ON_ERROR));
|
||||
$ok($p['status']===200&&isset($p['data']['preview']),'actual invoice preview via API transport');
|
||||
$ok($p['data']['impact']['budget_consumption_minor']===0&&!$p['data']['impact']['payable_created'],'preview has no budget consumption or payable');
|
||||
$ok($native->table('invoices')->count()===count($priorIds),'preview rolls all business rows back');
|
||||
$ok($p['no_store'],'successful response is not cached');
|
||||
config(['sgc_invoice.enabled'=>false]);$ok($send($body,$key)['status']===404,'disabled endpoint refuses even valid credentials');config(['sgc_invoice.enabled'=>true]);
|
||||
foreach ([['HTTP_AUTHORIZATION'=>null],['HTTP_AUTHORIZATION'=>'Bearer '.str_repeat('x',64)],['HTTP_X_SGC_CLIENT'=>'unknown']] as $headers)
|
||||
$ok($send($body,$key,$headers)['status']===401,'missing or wrong principal/token refused');
|
||||
$ok($send($body,$key,['HTTP_X_SGC_CONTEXT'=>str_repeat('0',64)])['status']===409,'stale context version refused');
|
||||
$ok($send($body,$key,['REMOTE_ADDR'=>'192.0.2.1'])['status']===403,'non-TLS remote request refused');
|
||||
$ok($send($body,$key,['CONTENT_TYPE'=>'text/plain'])['status']===415,'non JSON refused');
|
||||
$ok($send($body,$key,[], '/api/v1/integration/sgc/invoices',str_repeat('x',131073))['status']===413,'oversized body refused');
|
||||
$ok($send($body,$key,[], '/api/v1/integration/sgc/invoices','{bad')['status']===422,'malformed JSON refused');
|
||||
$bad=$body; $bad['tenant_id']=1;$ok($send($bad,$key)['status']===422,'client cannot choose tenant');
|
||||
$bad=$body; $bad['data']['supplier_id']=1;$ok($send($bad,$key)['status']===422,'supplier cannot be silently associated');
|
||||
$bad=$body; $bad['data']['total']=100.0;$ok($send($bad,$key)['status']===422,'floating point amounts refused');
|
||||
$bad=$body; $bad['data']['total']='101.00';$ok($send($bad,$key)['status']===422,'inconsistent total refused by original writer');
|
||||
$bad=$body; $bad['data']['items'][0]['purchase_order_item_id']=$nativeFixture['foreign_order_line'];$ok($send($bad,$key)['status']===422,'foreign native line refused');
|
||||
$bad=$body; $bad['order_id']=$nativeFixture['foreign_order'];$ok($send($bad,$key)['status']===403,'foreign native order refused');
|
||||
$ok($send($body,'not-a-uuid')['status']===422,'malformed idempotency key refused');
|
||||
$changed=$principal; $changed['auth_version']=2;config(['sgc_invoice.principals.synthetic'=>$changed]);$ok($send($body,$key)['status']===401,'revoked authentication version refused');
|
||||
$changed=$principal;$changed['unit_id']=$f['foreign_unit'];config(['sgc_invoice.principals.synthetic'=>$changed]);$ok($send($body,$key)['status']===403,'unauthorised unit mapping refused');
|
||||
config(['sgc_invoice.principals.synthetic'=>$principal]);
|
||||
$host->beginTransaction();try{$host->table('permission_grants')->where('user_id',$f['issuer'])->where('permission','purchasing_invoices.prepare')->delete();$ok($send($body,$key)['status']===403,'revoked prepare right refused');}finally{$host->rollBack();}
|
||||
$result=$confirm($p['data']['preview'],$key);$ok($result['status']===201&&$result['data']['status']==='pending','confirmation creates pending invoice only');
|
||||
$invoiceId=$result['data']['invoice_id'];
|
||||
$replay=$confirm($p['data']['preview'],$key);$ok($replay['status']===200&&$replay['data']['invoice_id']===$invoiceId&&$replay['data']['replayed'],'lost response recovered by same confirmation');
|
||||
$ok($send($body,$key)['status']===200,'same request recovered through preview after commit');
|
||||
$bad=$body; $bad['reason']='Outro fundamento sintético, conteúdo diferente.';$ok($send($bad,$key)['status']===409,'same key different content conflicts');
|
||||
$ok($confirm($p['data']['preview'],(string)Str::uuid())['status']===409,'confirmation must use the preview key');
|
||||
$ok($confirm(substr($p['data']['preview'],0,-4).'xxxx',$key)['status']===409,'tampered preview refused');
|
||||
$host->beginTransaction();try{$host->table('permission_grants')->where('user_id',$f['issuer'])->where('permission','purchasing_invoices.prepare')->delete();$ok($confirm($p['data']['preview'],$key)['status']===403,'replay revalidates current authority');}finally{$host->rollBack();}
|
||||
[$stale,$staleKey]=$preview('stale');[$fresh,$freshKey]=$preview('fresh');
|
||||
$ok($confirm($fresh['data']['preview'],$freshKey)['status']===201,'second individually prepared document confirmed');
|
||||
$ok($confirm($stale['data']['preview'],$staleKey)['status']===409,'changed source invalidates earlier preview');
|
||||
[$failure,$failureKey]=$preview('failure');$beforeFailure=$native->table('invoices')->count();$inject=true;
|
||||
Tempo\Purchasing\Embedded\Models\AuditLog::creating(function($log)use(&$inject){if($inject&&$log->action==='sgf.invoice.command')throw new RuntimeException('Synthetic failure immediately before durable receipt');});
|
||||
$failed=$confirm($failure['data']['preview'],$failureKey);$inject=false;
|
||||
$ok($failed['status']===500&&$native->table('invoices')->count()===$beforeFailure,'failure between writer and receipt rolls invoice back');
|
||||
$ok($confirm($failure['data']['preview'],$failureKey)['status']===201,'same operation recovers after failure');
|
||||
[$parallel,$parallelKey]=$preview('parallel');
|
||||
file_put_contents($proofRoot.'/job.json',json_encode(['body'=>['phase'=>'confirm','order_id'=>$orderId,'preview'=>$parallel['data']['preview']],'key'=>$parallelKey],JSON_THROW_ON_ERROR));
|
||||
$workers=[];foreach([0,1]as$i){$process=new Symfony\Component\Process\Process([PHP_BINARY,__FILE__,'--worker','job.json','worker-'.$i.'.json'],$repo,['SGC_INVOICE_PROOF_ROOT'=>$proofRoot],null,45);$process->start();$workers[]=$process;}
|
||||
foreach($workers as$process){$process->wait();if(!$process->isSuccessful())throw new RuntimeException('Worker failed; inspect private proof output');}
|
||||
$parallelResults=[];foreach([0,1]as$i)$parallelResults[]=json_decode(file_get_contents($proofRoot.'/worker-'.$i.'.json'),true);
|
||||
$statuses=array_column($parallelResults,'status');sort($statuses);$ok($statuses===[200,201],'concurrent same-key confirmations return one create and one recovery');
|
||||
$ok($parallelResults[0]['data']['invoice_id']===$parallelResults[1]['data']['invoice_id'],'concurrent receipts identify one invoice');
|
||||
$afterEffects=[];foreach(array_keys($effectsBefore)as$table)$afterEffects[$table]=$native->table($table)->count();
|
||||
$ok($effectsBefore===$afterEffects,'no new consumption stock obligation payment or ledger effects');
|
||||
$ok($native->table('invoices')->whereIn('id',$priorIds)->orderBy('id')->get()->toJson()===$existingInvoices,'previous accepted invoice rows preserved');
|
||||
$ok($snapshot($host)===$hostBefore,'host rows permissions budget and sequences unchanged');
|
||||
$protectedAfter=[$snapshot(DB::connection('sgc_protected_host')),$snapshot(DB::connection('sgc_protected_native'))];
|
||||
$ok($protectedBefore===$protectedAfter,'8085 host/native all rows and sequences unchanged');
|
||||
$cleared=false;try{app(App\Shared\Context\CompanyContext::class)->identifiers();}catch(Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException){$cleared=true;}
|
||||
$ok($cleared,'context cleaned after request');
|
||||
file_put_contents($proofRoot.'/result.json',json_encode(['passed'=>true,'checks'=>$checks,'new_pending_invoices'=>$native->table('invoices')->count()-count($priorIds),
|
||||
'concurrent_statuses'=>$statuses,'protected_8085_fingerprints'=>$protectedAfter,'database_reused'=>'receipt-composition-da77bb730751','new_database'=>false,'new_role'=>false,'new_server'=>false,
|
||||
'kernel_http'=>true,'tcp_http'=>false,'sql_source_fixture'=>$fixture,'activation'=>false,'publication'=>false],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
|
||||
echo json_encode(['passed'=>true,'checks'=>count($checks)]).PHP_EOL;
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
require __DIR__.'/sgc-invoice-proof-bootstrap.php';
|
||||
use Illuminate\Support\Facades\{Crypt,DB};
|
||||
use Illuminate\Support\Str;
|
||||
if(!is_file($proofRoot.'/result.json')||is_file($proofRoot.'/security-result.json'))throw new LogicException('Completed base proof and fresh continuation required');
|
||||
$base=json_decode(file_get_contents($proofRoot.'/result.json'),true);
|
||||
$p=json_decode(file_get_contents($proofRoot.'/initial-preview.json'),true);
|
||||
$cut=json_decode(Crypt::decryptString($p['data']['preview']),true);
|
||||
$key=$cut['operation_key'];$orderId=$cut['order_id'];
|
||||
$body=['phase'=>'preview','order_id'=>$orderId,'revision'=>$cut['revision'],'reason'=>$cut['reason'],'data'=>$cut['data']];
|
||||
$confirmation=['phase'=>'confirm','order_id'=>$orderId,'preview'=>$p['data']['preview']];
|
||||
$checks=[];$ok=function($value,$name)use(&$checks,$proofRoot){if(!$value)throw new RuntimeException($name);$checks[]=$name;file_put_contents($proofRoot.'/security-progress.json',json_encode($checks));};
|
||||
$beforeInvoices=$native->table('invoices')->orderBy('id')->get()->toJson();$beforeAudit=$native->table('audit_logs')->count();
|
||||
$r=$send($confirmation,$key);$ok($r['status']===200,'existing confirmed invoice recovered without new business write');$invoice=$r['data']['invoice_id'];
|
||||
foreach(['tenant_id','company_id']as$field){$bad=$principal;$bad[$field]=999999;config(['sgc_invoice.principals.synthetic'=>$bad]);$ok($send($confirmation,$key)['status']===403,'foreign '.$field.' principal rejected');}
|
||||
$bad=$principal;$bad['enabled']=false;config(['sgc_invoice.principals.synthetic'=>$bad]);$ok($send($confirmation,$key)['status']===401,'disabled principal rejected even for replay');
|
||||
$bad=$principal;$bad['token_sha256']=hash('sha256',str_repeat('r',64));config(['sgc_invoice.principals.synthetic'=>$bad]);$ok($send($confirmation,$key)['status']===401,'rotated credential invalidates old bearer');
|
||||
$bad=$principal;$bad['context_version']=hash('sha256','new synthetic context');config(['sgc_invoice.principals.synthetic'=>$bad]);
|
||||
$ok($send($confirmation,$key)['status']===409,'old context header rejected after rotation');
|
||||
$ok($send($confirmation,$key,['HTTP_X_SGC_CONTEXT'=>$bad['context_version']])['status']===409,'old preview rejected with new context header');
|
||||
config(['sgc_invoice.principals.synthetic'=>$principal]);
|
||||
config(['modules.companies.'.$f['tenant'].':'.$f['company']=>['contracted'=>[],'enabled'=>[]]]);
|
||||
$ok($send($confirmation,$key)['status']===403,'disabled purchasing module refuses API replay');config(['modules.companies'=>[]]);
|
||||
$oldOrder=$native->table('purchase_orders')->where('tenant_id',$nativeFixture['tenant'])->where('id','<>',$orderId)->first();
|
||||
$bad=$body;$bad['order_id']=(int)$oldOrder->id;$ok($send($bad,$key)['status']===409,'same key cannot be moved to another authorised order');
|
||||
$ok($send($confirmation,strtoupper($key))['status']===200,'UUID case does not create a second operation');
|
||||
$ok($send($body,$key,[], '/api/v1/integration/sgc/invoices?company_id=1')['status']===422,'query cannot inject another context');
|
||||
$bad=$body;$bad['data']['items'][0]['tenant_id']=1;$ok($send($bad,$key)['status']===422,'nested unknown authority field rejected');
|
||||
$bad=$body;$bad['phase']='validate';$ok($send($bad,$key)['status']===422,'transport cannot approve or consume invoice');
|
||||
$ok($send($body,$key,[], '/api/v1/integration/sgc/payments')['status']===404,'legacy payment route remains unmounted');
|
||||
$expired=$cut;$expired['observed']=time()-601;$old=Crypt::encryptString(json_encode($expired));
|
||||
$ok($send(['phase'=>'confirm','order_id'=>$orderId,'preview'=>$old],$key)['status']===200,'durable receipt recovered after preview lifetime with current authority');
|
||||
$expired['operation_key']=(string)Str::uuid();$old=Crypt::encryptString(json_encode($expired));
|
||||
$ok($send(['phase'=>'confirm','order_id'=>$orderId,'preview'=>$old],$expired['operation_key'])['status']===409,'expired unapplied preview cannot write');
|
||||
config(['database.connections.sgc_blocker'=>config('database.connections.'.DB::getDefaultConnection())]);$blocker=DB::connection('sgc_blocker');
|
||||
file_put_contents($proofRoot.'/lock-job.json',json_encode(['body'=>$confirmation,'key'=>$key],JSON_THROW_ON_ERROR));
|
||||
$worker=function(string $output)use($proofRoot,$repo){$p=new Symfony\Component\Process\Process([PHP_BINARY,__DIR__.'/sgc-invoice-proof.php','--worker','lock-job.json',$output],$repo,['SGC_INVOICE_PROOF_ROOT'=>$proofRoot],null,20);$p->run();if(!$p->isSuccessful())throw new RuntimeException('Lock worker failed');return json_decode(file_get_contents($proofRoot.'/'.$output),true);};
|
||||
$blocker->beginTransaction();try{$blocker->select('SELECT pg_advisory_xact_lock(hashtextextended(?,0))',['sgc.invoice:'.$key]);$blocked=$worker('blocked.json');}finally{$blocker->rollBack();}
|
||||
$ok($blocked['status']===503&&($blocked['data']['error']??null)==='RETRY_SAME_KEY','lock timeout returns recoverable503 without duplicate');
|
||||
$recovered=$worker('unblocked.json');$ok($recovered['status']===200&&$recovered['data']['invoice_id']===$invoice,'after lock release same operation recovers correct invoice');
|
||||
$ok($beforeInvoices===$native->table('invoices')->orderBy('id')->get()->toJson()&&$beforeAudit===$native->table('audit_logs')->count(),'security and lock continuation added no invoices or audit commands');
|
||||
$defaults=require $repo.'/config/sgc_invoice.php';$ok($defaults['enabled']===false&&$defaults['principals']===[],'source configuration remains disabled with no principals');
|
||||
file_put_contents($proofRoot.'/security-result.json',json_encode(['passed'=>true,'checks'=>$checks,'new_business_rows'=>0,'lock_statuses'=>[$blocked['status'],$recovered['status']],
|
||||
'base_proof_preserved_sha256'=>hash_file('sha256',$proofRoot.'/result.json'),'no_real_activation'=>true],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
|
||||
echo json_encode(['passed'=>true,'checks'=>count($checks)]).PHP_EOL;
|
||||
@@ -0,0 +1,10 @@
|
||||
import fs from 'node:fs';import assert from 'node:assert/strict';import {session} from './sgf-single-http.mjs';
|
||||
const run=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')),root=run.root+'/private/receipt-composition-cc50a276c10c';const old=JSON.parse(fs.readFileSync(root+'/single-financial-http-progress.json')),checks=[];const s=await session('sgf:accounting_prepare','accounting');
|
||||
for(const path of ['/general-ledger','/general-ledger/workspace','/api/general-ledger/sales?invoice_id=1','/api/general-ledger/costs?order_id=1','/api/general-ledger/industrial?lot_id=1','/api/catalogue-accounting']){const r=await s.get(path);const body=await r.text();assert.equal(r.status,200,path+': '+body.slice(0,120));if(path.includes('/sales?'))assert.equal(JSON.parse(body).journals.length,4);if(path.includes('/costs?')||path.includes('/industrial?'))assert.equal(JSON.parse(body).journals.length,3);checks.push(path+' reads qualified shared book');}
|
||||
let r=await s.get('/api/general-ledger?account_q=QA-STOCK&accounts_per_page=100');assert.equal(r.status,200);let data=await r.json();assert.equal(data.chart_accounts.length,1);assert.equal(data.chart_accounts[0].code,'QA-STOCK');checks.push('native chart exposes exact stock account with bounded search');const id=data.chart_accounts[0].id;
|
||||
r=await s.get('/general-ledger/account-ledger?from_on=2026-01-01&to_on=2026-12-31&account_id='+id);assert.equal(r.status,200);const html=await r.text();assert(html.includes('QA-STOCK'));checks.push('chart account opens actual account ledger');
|
||||
r=await s.get('/api/general-ledger?account_q=%25');assert.equal(r.status,200);data=await r.json();assert.equal(data.chart_accounts.length,0);checks.push('chart wildcard is literal rather than an unrestricted search');
|
||||
for(const [url,expected]of [['/api/general-ledger/receipts/'+old.operations['sale1000-post'].key,old.operations['sale1000-post'].result.journal_id],['/api/general-ledger/financial-receipts/settlement/'+old.operations.payment20.key,old.operations.payment20.result.payment_id]]){r=await s.get(url);assert.equal(r.status,200);data=await r.json();assert(JSON.stringify(data).includes(String(expected)));checks.push('legacy receipt reads its existing native command without executing it');}
|
||||
r=await s.get('/general-ledger/flow?source=2');assert.equal(r.status,302);assert(r.headers.get('location').includes('obligation_id=2'));await r.text();checks.push('supplier source retains qualified obligation in common journal continuation');
|
||||
r=await s.get('/api/general-ledger/sales?invoice_id=999999');assert.equal(r.status,403);await r.text();checks.push('unknown sales source is refused without falling back to another invoice');
|
||||
const proof=root+'/accounting-continuity-http-20261110.json';fs.writeFileSync(proof,JSON.stringify({passed:true,checks,new_business_commands:0,browser_visual_review:false},null,2));console.log(JSON.stringify({passed:true,checks:checks.length,proof}));
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
use App\Shared\Context\CompanyContext;use App\Modules\Accounting\{ComposedLedger,ComposedLedgerController};use Illuminate\Support\Facades\Auth;
|
||||
require __DIR__.'/sgf-ledger-proof-bootstrap.php';$user=App\Models\User::findOrFail($profiles['read_only']['user_id']);Auth::guard('web')->setUser($user);$session=app('session')->driver();$session->start();$session->put('company_context',$f['scope']+['module_id'=>'accounting','token'=>'report-boundaries']);$r=Illuminate\Http\Request::create('/api/general-ledger/account-ledger','GET');$r->attributes->set('sgf_module','accounting');$r->setLaravelSession($session);$r->setUserResolver(fn()=>$user);$app->instance('request',$r);app(CompanyContext::class)->selectFor($user->id,1,2,2);$controller=app(ComposedLedgerController::class);$base=['from_on'=>'2026-11-10','to_on'=>'2026-11-10'];
|
||||
foreach([[$base+['account_id'=>2147483647],403,'foreign or missing account denied instead of500'],[$base,422,'explicit account required instead of500'],[$base+['account_id'=>0],422,'invalid identifier rejected'],[$base+['company_id'=>3],422,'scope override rejected']]as[$query,$expected,$name]){Auth::forgetGuards();Auth::shouldUse('web');Auth::guard('web')->setUser($user);$r->setUserResolver(fn()=>$user);$r->query->replace($query);$actual=null;try{$controller->report($r,'account-ledger',app(ComposedLedger::class));}catch(Symfony\Component\HttpKernel\Exception\HttpExceptionInterface$e){$actual=$e->getStatusCode();}catch(Illuminate\Validation\ValidationException){$actual=422;}$ok($actual===$expected,$name);}
|
||||
Auth::forgetGuards();Auth::shouldUse('web');Auth::guard('web')->setUser($user);$r->setUserResolver(fn()=>$user);$r->query->replace($base);$response=$controller->report($r,'trial-balance',app(ComposedLedger::class));$ok($response->getStatusCode()===200&&$response->getData(true)['kind']==='trial_balance','valid read unaffected');file_put_contents($work.'/accounting-report-boundaries.json',json_encode(['passed'=>true,'checks'=>$checks,'business_commands'=>0,'new_rights'=>0],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/accounting-report-boundaries.json']).PHP_EOL;
|
||||
@@ -0,0 +1,4 @@
|
||||
import fs from 'node:fs';import assert from 'node:assert/strict';import {session} from './sgf-single-http.mjs';import {ledgerReportQuery,checkedReport,reportBasis,reportPageCsv,minorAmount} from '../resources/js/ledger-report-view.js';
|
||||
const s=await session('sgf:read_only','accounting'),checks=[],ok=(v,n)=>{assert(v,n);checks.push(n);},form={kind:'trial-balance',from_on:'2026-11-10',to_on:'2026-11-10',per_page:'25',account_id:''};
|
||||
const get=async(q,basis=null)=>{const r=await s.get('/api/general-ledger/'+q.kind+'?'+new URLSearchParams(q.params));assert.equal(r.status,200);return checkedReport(await r.json(),q,basis);};const query=ledgerReportQuery(form),trial=await get(query);ok(trial.basis.journals===27,'existing27 journal basis preserved');ok(trial.rows.length>0&&trial.rows.length<=25,'bounded real account rows');ok(trial.totals.debit_minor===trial.totals.credit_minor,'actual filtered debit and credit balanced');const row=trial.rows.find(r=>r.code==='QA-CAIXA');assert(row);ok(minorAmount(row.closing_net_minor)==='950,00','cash actual ledger closing950');const ledgerQuery=ledgerReportQuery({...form,kind:'account-ledger',account_id:String(row.account_id)}),ledger=await get(ledgerQuery);ok(ledger.account.code==='QA-CAIXA'&&ledger.closing_net_minor===row.closing_net_minor,'drilldown explicit same account same closing');ok(ledger.rows.length>0&&ledger.rows.every(r=>r.review_id&&r.proposal_id&&r.reason),'original proposal review evidence present');ok(ledger.rows.at(-1).balance_minor===ledger.closing_net_minor,'running balance matches closing on complete known page');ok(reportBasis(ledger)===reportBasis(trial),'same native basis through same date');const larger=await get(ledgerReportQuery({...form,per_page:'100'}));ok(JSON.stringify(larger.rows)===JSON.stringify(trial.rows)&&JSON.stringify(larger.totals)===JSON.stringify(trial.totals),'page sizes25 and100 actual known accounts equivalent');const csv=reportPageCsv(trial);ok(csv.includes('Apenas a pagina consultada')&&csv.includes("'QA-CAIXA")&&csv.includes('950,00'),'CSV page only exact values and safe account text');const unsafe=structuredClone(ledger);unsafe.rows[0].reason=' =HYPERLINK("bad")';ok(reportPageCsv(unsafe).includes("' =HYPERLINK"),'CSV formula-like reason protected');assert.throws(()=>checkedReport({...trial,basis:{...trial.basis,last_journal_id:999999}},query,reportBasis(trial)));ok(true,'changed book marker demands new conference without DB mutation');assert.throws(()=>checkedReport(trial,{...query,params:{...query.params,to_on:'2026-11-09'}}));ok(true,'late response different dates refused');assert.throws(()=>ledgerReportQuery({...form,kind:'account-ledger'}));ok(true,'no account auto selection');assert.throws(()=>ledgerReportQuery({...form,from_on:''}));ok(true,'no dates assumed');assert.throws(()=>ledgerReportQuery({...form,account_id:'9007199254740993'}));ok(true,'unsafe identifier refused');assert.throws(()=>minorAmount(Number.MAX_SAFE_INTEGER+2));ok(true,'unsafe amount refused');if(!process.argv.includes('--before-install')){const missing=await s.get('/api/general-ledger/account-ledger?from_on=2026-11-10&to_on=2026-11-10&account_id=2147483647');ok(missing.status===403||missing.status===409,'foreign nonexistent account refused');const wrong=await session('sgf:read_only','finance'),denied=await wrong.get('/api/general-ledger/trial-balance?from_on=2026-11-10&to_on=2026-11-10');ok([403,409].includes(denied.status),'wrong module refused');}
|
||||
const run=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')),root=run.root+'/private/receipt-composition-cc50a276c10c',file=root+(process.argv.includes('--before-install')?'/accounting-reports-preinstall-20261110.json':'/accounting-reports-http-20261110.json');fs.writeFileSync(root+'/accounting-reports-samples-private.json',JSON.stringify({trial,ledger}));fs.writeFileSync(file,JSON.stringify({passed:true,checks,business_commands:0,new_grants:0,new_demo:false,server_read_engine_unchanged:true,http_error_mapping_corrected:!process.argv.includes('--before-install')},null,2));console.log(JSON.stringify({passed:true,checks:checks.length,proof:file}));
|
||||
@@ -0,0 +1,3 @@
|
||||
import fs from 'node:fs';import assert from 'node:assert/strict';import {createRequire} from 'node:module';import {pathToFileURL} from 'node:url';import {transformWithOxc} from 'vite';import React from 'react';import {renderToStaticMarkup} from 'react-dom/server';
|
||||
const require=createRequire(import.meta.url),root=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')).root+'/private/receipt-composition-cc50a276c10c',samples=JSON.parse(fs.readFileSync(root+'/accounting-reports-samples-private.json'));const load=async name=>{let source=fs.readFileSync('resources/js/'+name,'utf8').replace("from 'react'",'from '+JSON.stringify(pathToFileURL(require.resolve('react')).href));for(const file of ['ledger-report-view.js','accounting-menu.js'])source=source.replace("from './"+file+"'",'from '+JSON.stringify(pathToFileURL(process.cwd()+'/resources/js/'+file).href));const compiled=await transformWithOxc(source,name,{jsx:{runtime:'classic'}});return import('data:text/javascript;base64,'+Buffer.from(compiled.code).toString('base64'));};const {default:Reports,LedgerReportTable}=await load('LedgerReportsWorkspace.jsx'),{default:Workspace}=await load('ComposedAccountingWorkspace.jsx'),checks=[],ok=(v,n)=>{assert(v,n);checks.push(n);};for(const [kind,data]of Object.entries(samples)){const html=renderToStaticMarkup(React.createElement(LedgerReportTable,{data,onAccount(){}}));ok(data.rows.every(r=>html.includes(String(kind==='trial'?r.code:r.journal_id))),kind+' actual rows rendered');ok(!html.includes('NaN')&&!html.includes('undefined'),kind+' exact present values');ok(kind==='trial'?html.includes('Abrir razão desta conta'):html.includes('revisão #'),kind+' explicit source continuation');}const form=renderToStaticMarkup(React.createElement(Reports,{context:{token:'test'},api(){throw Error('No automatic query');}}));ok(form.includes('data-sgf-read-filters="true"'),'read filter not business editor');ok(!form.includes('2026-01-01')&&form.includes('type="date" required="" value=""'),'no fixed or implicit reporting dates');ok(form.includes('Todas as contas do balancete')&&!form.includes('type="number"'),'account selected from rows without ID typing');
|
||||
const context={token:'scope',company:{name:'Empresa QA'},unit:{id:2,name:'Unidade QA'},fiscal_qa_enabled:true,fiscal_rules_qa_enabled:true,bank_qa_enabled:true};const outputs={};for(const section of ['accounting_panel','accounting_daily','accounting_review','accounting_closing','accounting_configuration'])outputs[section]=renderToStaticMarkup(React.createElement(Workspace,{section,context,can:()=>true,navigate(){}}));ok(new Set(Object.values(outputs)).size===5,'five sections distinct instead of duplicated panel');ok(outputs.accounting_closing.includes('não fecha nem reabre'),'closing consultation no implicit command');ok(outputs.accounting_review.includes('conferir extracto')&&outputs.accounting_configuration.includes('Fontes e regras'),'authorized cross-module existing functions exposed');const restricted=renderToStaticMarkup(React.createElement(Workspace,{section:'accounting_review',context,can:()=>false,navigate(){}}));ok(!restricted.includes('conferir extracto')&&!restricted.includes('Livro e mapa'),'restricted fiscal and bank links hidden');const escaped=structuredClone(samples.trial);escaped.rows[0].label='<script>bad</script>';ok(!renderToStaticMarkup(React.createElement(LedgerReportTable,{data:escaped})).includes('<script>'),'account label escaped');const file=root+'/accounting-reports-ui-20261110.json';fs.writeFileSync(file,JSON.stringify({passed:true,checks,business_commands:0,browser_visual_review:false},null,2));console.log(JSON.stringify({passed:true,checks:checks.length,proof:file}));
|
||||
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
$book=$argv[1]??'regression';require __DIR__.'/sgf-single-bootstrap.php';$p=json_decode(file_get_contents($repo.'/docs/integration/sgf-financial-indicators-access-proposal.json'),true);if($p['approved']!==false||$p['applied']!==false||count($p['new_right_names'])!==3||count($p['grants_per_book'])!==7)throw new LogicException('Pending exact analysis proposal required');if($host->table('permission_grants')->whereIn('permission',$p['new_right_names'])->exists())throw new LogicException('Reconcile existing analysis rights');foreach([13,14,15,16,17]as$id)if(!$host->table('company_memberships')->where(['tenant_id'=>1,'company_id'=>2,'user_id'=>$id])->exists())throw new LogicException('Existing recipient missing');file_put_contents($work.'/analysis-access-readonly.json',json_encode(['passed'=>true,'new_right_names'=>3,'proposed_grants'=>7,'existing_new_right_grants'=>0,'grants_applied'=>0,'new_users'=>0],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'book'=>$book,'proof'=>$work.'/analysis-access-readonly.json']).PHP_EOL;
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
$book='8085';require __DIR__.'/sgf-single-bootstrap.php';if(is_file($s['root'].'/analysis-backup-pointer.json'))throw new LogicException('Preserve existing IVA baseline');if($native->table('gl_journals')->count()!==27)throw new LogicException('Expected27 accepted journals');
|
||||
foreach(['host'=>['database'=>$s['env']['DB_DATABASE'],'username'=>$s['env']['DB_USERNAME'],'password'=>$s['env']['DB_PASSWORD']],'native'=>$s['nativeConnection']]as$name=>$c){$dump=new Symfony\Component\Process\Process(['C:/Program Files/PostgreSQL/17/bin/pg_dump.exe','-h','127.0.0.1','-p','55439','-U',$c['username'],'-d',$c['database'],'--format=custom','--no-owner','--no-privileges','--file',$work.'/'.$name.'-before-analysis.dump'],$repo,['PGPASSWORD'=>$c['password']]);$dump->setTimeout(120);$dump->mustRun();}
|
||||
$before=[];foreach(['si_invoices','sd_outputs','sd_versions','sr_records','sr_effects','cash_events','sf_proposals','sf_decisions','sf_activation_events','service_calculation_results','a_clients','fx_versions','fx_commands','fr_events','fr_calculations','fr_commands','br_statements','br_events','br_commands','permission_grants']as$t)$before['host'][$t]=$host->table($t)->orderBy('id')->get()->toJson();foreach(['stock_movements','gl_journals','gl_periods']as$t)$before['native'][$t]=$native->table($t)->orderBy('id')->get()->toJson();$before['binding']=hash_file('sha256',$s['root'].'/binding.json');file_put_contents($work.'/preservation-private.json',json_encode($before));file_put_contents($s['root'].'/analysis-backup-pointer.json',json_encode(['root'=>$work]));echo json_encode(['passed'=>true,'private_backup'=>$work,'database_writes'=>0]).PHP_EOL;
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
use Illuminate\Support\Facades\{DB,Crypt};use App\Shared\Context\CompanyContext;
|
||||
require __DIR__.'/sgf-analysis-proof-bootstrap.php';$before=$host->table('an_criteria')->count();$metric='cash_expense_share';$host->beginTransaction();try{
|
||||
$d=$criterionData($metric);$d['evidence']='Literal%_! metadata reference for history pagination.';[$p]=$analysisCommand(13,$criterionInput('prepare',$metric,0,$d));$template=(array)$host->table('an_criteria')->find($p['id']);unset($template['id']);$previous=$p['id'];for($i=1;$i<=126;$i++){$template['previous_id']=$previous;$template['version']=$i+1;$previous=$host->table('an_criteria')->insertGetId($template);}$selectAnalysis(17);$a=$analysis->listing(17,['view'=>'history','metric'=>$metric,'per_page'=>25]);$all=[];for($page=1;$page<=$a['records']['last_page'];$page++)$all=array_merge($all,$analysis->listing(17,['view'=>'history','metric'=>$metric,'per_page'=>25,'page'=>$page])['records']['data']);$ok(count($all)===127&&count(array_unique(array_column($all,'id')))===127,'127 criterion metadata versions paginated25');$a=$analysis->listing(17,['view'=>'history','metric'=>$metric,'per_page'=>100]);$a2=$analysis->listing(17,['view'=>'history','metric'=>$metric,'per_page'=>100,'page'=>2]);$ok(count($a['records']['data'])===100&&count($a2['records']['data'])===27,'pages100/27 same history universe');$ok($analysis->listing(17,['search'=>'%_!'])['records']['total']===1,'literal search on latest criterion evidence');$reject(fn()=>$analysis->listing(17,['per_page'=>500]),422,'unbounded page refused');$reject(fn()=>$analysis->listing(17,['view'=>'history']),422,'history requires explicit metric');
|
||||
$cut=$analysisPreview(13,$criterionInput('prepare',$metric,127,$criterionData($metric)));$selectAnalysis(13,'changed-generation');$reject(fn()=>$analysis->confirm(13,$cut['preview']),409,'changed generation invalidates preview');$selectAnalysis(13);$c=json_decode(Crypt::decryptString($cut['preview']),true);$c['observed']=time()-601;$reject(fn()=>$analysis->confirm(13,Crypt::encryptString(json_encode($c))),409,'expired preview refused');request()->attributes->set('sgf_module','accounting');$reject(fn()=>$analysis->listing(13,[]),403,'wrong module refused');$selectAnalysis(13);$reject(function()use($analysis){app(CompanyContext::class)->selectFor(13,1,2,1);return $analysis->listing(13,[]);},403,'wrong unit refused');$selectAnalysis(13);
|
||||
$attempt=function($fn)use($host){$host->beginTransaction();try{$fn();return false;}catch(Illuminate\Database\QueryException){return true;}finally{$host->rollBack();}};$ok($attempt(fn()=>$host->table('an_criteria')->where('id',$previous)->update(['reason'=>'Attempted alteration of immutable QA criteria'])),'SQL criteria append-only');$bad=$template;$bad['previous_id']=$previous;$bad['version']=128;$bad['actor_id']=17;$bad['prepared_by']=17;$ok($attempt(fn()=>$host->table('an_criteria')->insert($bad)),'SQL unprivileged writer refused');
|
||||
}finally{$host->rollBack();}$ok($host->table('an_criteria')->count()===$before,'all metadata fixtures rolled back');file_put_contents($work.'/analysis-boundaries.json',json_encode(['passed'=>true,'checks'=>$checks,'metadata_fixtures'=>127,'origin_simulated_in_harness'=>true,'rolled_back'=>true],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/analysis-boundaries.json']).PHP_EOL;
|
||||
@@ -0,0 +1,4 @@
|
||||
import fs from 'node:fs';import {spawn,spawnSync} from 'node:child_process';import assert from 'node:assert/strict';
|
||||
const php='C:/laragon/tools/sgf-tempo/php-8.4.27/php.exe',script='scripts/sgf-analysis-concurrency.php',run=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')),dir=run.root+'/private/receipt-composition-b4093c6f6707/analysis-concurrency';
|
||||
const call=mode=>{const r=spawnSync(php,[script,mode],{encoding:'utf8',windowsHide:true});assert.equal(r.status,0,r.stdout+r.stderr);assert.match(r.stdout,/"passed":true/);return r.stdout;};if(fs.existsSync(dir+'/result.json')){assert(JSON.parse(fs.readFileSync(dir+'/result.json')).passed);console.log(JSON.stringify({passed:true,reused:true,proof:dir+'/result.json'}));process.exit();}
|
||||
call('prepare');const first=spawn(php,[script,'first'],{stdio:'pipe',windowsHide:true});let out='';first.stdout.on('data',d=>out+=d);first.stderr.on('data',d=>out+=d);const ended=new Promise(r=>first.once('exit',r));try{const deadline=Date.now()+10000;while(!fs.existsSync(dir+'/locked')&&Date.now()<deadline)await new Promise(r=>setTimeout(r,40));assert(fs.existsSync(dir+'/locked'));const second=spawn(php,[script,'second'],{stdio:'pipe',windowsHide:true});let output='';second.stdout.on('data',d=>output+=d);second.stderr.on('data',d=>output+=d);assert.equal(await new Promise(r=>second.once('exit',r)),0,output);assert.match(output,/"passed":true/);const r=JSON.parse(fs.readFileSync(dir+'/second.json'));assert.equal(r.status,503);assert.equal(r.retry_after,'5');assert(r.elapsed_seconds>=4.5&&r.elapsed_seconds<12);}finally{fs.writeFileSync(dir+'/release','Release existing regression lock only');assert.equal(await ended,0,out);}console.log(call('recover').trim());
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
require __DIR__.'/sgf-analysis-proof-bootstrap.php';$dir=$s['root'].'/analysis-concurrency';if(!is_dir($dir))mkdir($dir,0700,true);$file=$dir.'/progress.json';$mode=$argv[1]??'';$metric='cash_coverage_single_payable';
|
||||
if($mode==='prepare'){if(is_file($file))throw new LogicException('Reuse preparation');[$p]=$analysisCommand(13,$criterionInput('prepare',$metric,0,$criterionData($metric)));$a=$analysisPreview(14,$criterionInput('review',$metric,1,['decision'=>'reviewed']));$b=$analysisPreview(14,$criterionInput('review',$metric,1,['decision'=>'reviewed']));file_put_contents($file,json_encode(['cuts'=>[1=>$a['preview'],2=>$b['preview']],'before'=>$host->table('an_criteria')->count()]));}
|
||||
elseif($mode==='first'){$p=json_decode(file_get_contents($file),true);$selectAnalysis(14);$host->beginTransaction();try{$out=$analysis->confirm(14,$p['cuts'][1]);file_put_contents($dir.'/locked','QA company lock held');$end=microtime(true)+25;while(!is_file($dir.'/release')&µtime(true)<$end)usleep(50000);if(!is_file($dir.'/release'))throw new LogicException('Release missing');$host->commit();file_put_contents($dir.'/first.json',json_encode($out));}finally{while($host->transactionLevel()>0)$host->rollBack();}}
|
||||
elseif($mode==='second'){$p=json_decode(file_get_contents($file),true);$selectAnalysis(14);request()->request->replace(['preview'=>$p['cuts'][2]]);$start=microtime(true);$r=app(App\Modules\Finance\Http\FinancialIndicatorsController::class)->confirm(request(),$analysis);file_put_contents($dir.'/second.json',json_encode(['status'=>$r->getStatusCode(),'retry_after'=>$r->headers->get('Retry-After'),'elapsed_seconds'=>microtime(true)-$start]));}
|
||||
elseif($mode==='recover'){$p=json_decode(file_get_contents($file),true);$selectAnalysis(14);$reject(fn()=>$analysis->confirm(14,$p['cuts'][2]),409,'second stale criterion review after release');$a=$analysis->confirm(14,$p['cuts'][1]);$ok($a['replayed']&&$host->table('an_criteria')->count()===$p['before']+1,'one criterion review without duplication');file_put_contents($dir.'/result.json',json_encode(['passed'=>true,'during_lock'=>json_decode(file_get_contents($dir.'/second.json'),true),'after_release'=>409,'one_review'=>true,'origin_simulated_in_harness'=>true,'regression_only'=>true],JSON_PRETTY_PRINT));}
|
||||
else throw new LogicException('Unknown mode');echo json_encode(['passed'=>true,'phase'=>$mode,'proof'=>$dir.'/result.json']).PHP_EOL;
|
||||
@@ -0,0 +1,5 @@
|
||||
// Actual UI client over actual HTTP. Only previews (rolled back) are allowed to POST.
|
||||
import fs from 'node:fs';import assert from 'node:assert/strict';import {parsePage,installDom} from './purchasing-workspace-dom.mjs';import {session} from './sgf-single-http.mjs';
|
||||
const nativeFetch=globalThis.fetch,s=await session('sgf:finance_prepare','finance');let r=await s.get('/finance/indicators',{headers:{Accept:'text/html'}});assert.equal(r.status,200);const document=parsePage(await r.text());installDom(document);document.getElementById=id=>document.querySelector('#'+id);globalThis.window={addEventListener(){}};const $=id=>document.getElementById(id);let previews=0,confirms=0;const calls=[],checks=[];const ok=(v,n)=>{assert(v,n);checks.push(n);};globalThis.fetch=async(url,options={})=>{if(url.startsWith('http:'))return nativeFetch(url,options);calls.push(url);if(options.method==='POST'){if(url.endsWith('/confirm')){confirms++;throw Error('No indicator confirmations in UI proof');}assert(url.endsWith('/preview'));previews++;const a=await s.command(url,JSON.parse(options.body),options.headers['Idempotency-Key']);return{status:a.status,json:async()=>a.value};}const a=await s.get(url);return{status:a.status,json:()=>a.json()};};
|
||||
await import('../public/sgf-indicators.mjs?dom-http');const wait=async f=>{const end=Date.now()+15000;while(!f()&&Date.now()<end)await new Promise(r=>setTimeout(r,25));assert(f(),$('message').textContent);};await wait(()=>$('events').children.length===2);ok($('selection').textContent.includes('Nenhuma versão'),'no first criterion auto-selected');ok($('summary').textContent.includes('950.00')&&$('summary').textContent.includes('100.00')&&$('summary').textContent.includes('50.00')&&$('summary').textContent.includes('1000.00'),'actual source amounts visible');ok($('summary').textContent.includes('fontes efectivas conciliadas'),'actual source kind visible');await $('events').children[0].querySelector('button').onclick();ok($('selection').textContent.includes('Despesa/entradas'),'explicit expense criterion selected');$('action').value='calculate';$('action').onchange();$('asOf').value='2026-11-10';$('reason').value='Ensaio UI QA com critério escolhido e fonte real do exercício; não confirmar.';$('operation').oninput();await $('operation').onsubmit({preventDefault(){}});ok(previews===1&&confirms===0,'actual rollback preview no confirmation');ok($('impact').textContent.includes('5.00 %')&&$('impact').textContent.includes('1/20'),'exact fraction and percent shown before commit');ok($('impact').textContent.includes('Série anterior/tendência: Indeterminadas'),'no fake prior comparison');ok($('fields').disabled&&$('filters').disabled,'preview locks form and catalogue');$('cancel').onclick();ok(!$('fields').disabled&&$('asOf').value==='2026-11-10','cancel keeps unsubmitted date');$('size').value='100';$('apply').onclick();await wait(()=>$('filters').disabled===false&&calls.some(c=>c.includes('per_page=100')));ok($('asOf').value==='2026-11-10'&&$('reason').value.includes('não confirmar'),'pagination preserves unsubmitted inputs');
|
||||
$('view').value='results';$('apply').onclick();await wait(()=>$('events').children.length===2&&$('events').textContent.includes('Resultado'));await $('events').children[0].querySelector('button').onclick();ok($('detail').textContent.includes('source_snapshot')&&$('detail').textContent.includes('5.00'),'historical result retains source and value');ok($('inputs').textContent.includes('Sem critério escolhido'),'historical result does not adopt its criterion');ok($('download').querySelector('a').href.endsWith('/report'),'historical report offered');const run=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')),root=run.root+'/private/receipt-composition-cc50a276c10c',p=JSON.parse(fs.readFileSync(root+'/analysis-http-20261110.json'));$('referenceKey').value=p.operations['calculate-coverage'].key;await $('recovery').onsubmit({preventDefault(){}});ok($('receipt').textContent.includes('Nenhuma operação repetida'),'own historical receipt recovered by GET');r=await s.get('/api/financial-indicators?view=results');ok((await r.json()).records.total===2&&confirms===0,'two saved results unchanged');r=await s.get('/api/bank-reconciliation');const bank=await r.json();ok(bank.finalized&&bank.history.total===7,'bank conference7events remains finalized');const file=root+'/analysis-dom-http-20261110.json';fs.writeFileSync(file,JSON.stringify({passed:true,checks,preview_posts:previews,confirmation_posts:confirms,real_http_forwarded:true,browser_visual_review:false},null,2));console.log(JSON.stringify({passed:true,checks:checks.length,proof:file}));
|
||||
@@ -0,0 +1,9 @@
|
||||
import fs from 'node:fs';import assert from 'node:assert/strict';import crypto from 'node:crypto';import {session} from './sgf-single-http.mjs';
|
||||
const run=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')),root=run.root+'/private/receipt-composition-cc50a276c10c',file=root+'/analysis-http-20261110.json';let p=fs.existsSync(file)?JSON.parse(fs.readFileSync(file)):{operations:{},checks:[]};if(p.passed){console.log(JSON.stringify({passed:true,reused:true,proof:file}));process.exit();}const save=()=>fs.writeFileSync(file,JSON.stringify(p,null,2));const ok=(v,n)=>{assert(v,n);if(!p.checks.includes(n))p.checks.push(n);save();};const prep=await session('sgf:finance_prepare','finance'),review=await session('sgf:finance_review','finance');const get=async(s=prep,query='')=>{const r=await s.get('/api/financial-indicators'+query);assert.equal(r.status,200);return r.json();};const state=await get();ok(prep.identity.context.analysis_qa_enabled===true,'feature in authorized finance context');ok(state.source.available&&state.source.kind==='composed_qa_sources','actual composed cash/ledger source no harness');ok(state.source.cash_balance_minor==='95000'&&state.source.supplier_credit_minor==='10000'&&state.source.cash_expenses_minor==='5000'&&state.source.cash_receipts_minor==='100000','actual950/100 and50/1000 sources');
|
||||
const def=metric=>{const {label,...d}=state.templates[metric];return{...d,scope_description:'Apenas as fontes QA seleccionadas, sem rácio geral ou efeito financeiro.',evidence:'Fonte Caixa conciliada e conta credora QA explicitamente conferidas na instalação.'};};const input=(action,metric,version,data)=>({action,...(metric?{metric}:{}),version,data,reason:'Conferência individual QA do critério, fontes e retrato exacto, sem efeito financeiro.'});const calc=id=>input('calculate',null,0,{criterion_id:id,as_of:'2026-11-10'});
|
||||
const command=async(name,s,body)=>{if(p.operations[name]?.result)return p.operations[name].result;let op=p.operations[name];if(!op){op=p.operations[name]={key:crypto.randomUUID(),input:body};save();}if(!op.cut){const r=await s.command('/api/financial-indicators/preview',op.input,op.key);assert.equal(r.status,200,JSON.stringify(r.value));op.cut=r.value.preview;ok(r.value.impact.new_financial_effects===0&&!r.value.impact.real_company_criterion,'preview no real criterion or financial effect');save();}const r=await s.command('/api/financial-indicators/confirm',{preview:op.cut});assert.equal(r.status,200,JSON.stringify(r.value));op.result=r.value;save();const again=await s.command('/api/financial-indicators/confirm',{preview:op.cut});ok(again.status===200&&again.value.replayed&&again.value.id===r.value.id,'repeat '+name+' no duplication');return r.value;};
|
||||
const coverage='cash_coverage_single_payable',expense='cash_expense_share';const a=await command('prepare-coverage',prep,input('prepare',coverage,0,def(coverage)));let denied=await prep.command('/api/financial-indicators/preview',calc(a.id));ok(denied.status===200&&denied.value.impact.result.state==='pending','unreviewed criterion never calculated as determined');const ar=await command('review-coverage',review,input('review',coverage,1,{decision:'reviewed'}));const ac=await command('calculate-coverage',prep,calc(ar.id));let r=await prep.get('/api/financial-indicators/records/'+ac.id+'?type=result');const coverageResult=await r.json();ok(r.status===200&&coverageResult.result.value==='9.50'&&coverageResult.result.exact_fraction==='19/2'&&coverageResult.result.unit==='times','actual source ratio950/100=9.5');
|
||||
await command('prepare-expense',prep,input('prepare',expense,0,def(expense)));const er=await command('review-expense',review,input('review',expense,1,{decision:'reviewed'}));const ec=await command('calculate-expense',prep,calc(er.id));r=await prep.get('/api/financial-indicators/records/'+ec.id+'?type=result');const expenseResult=await r.json();ok(r.status===200&&expenseResult.result.value==='5.00'&&expenseResult.result.exact_fraction==='1/20'&&expenseResult.result.unit==='percent','actual expense share50/1000=5percent');ok(expenseResult.result.previous_period===null&&expenseResult.result.trend===null,'missing time series not zero trend');ok(expenseResult.source_snapshot.kind==='composed_qa_sources'&&!expenseResult.result.real_company_criterion,'actual source snapshot retained without normative claim');
|
||||
r=await prep.get('/api/financial-indicators/results/'+ec.id+'/report');ok(r.status===200&&r.headers.get('content-type').includes('text/csv'),'internal report available');const bytes=Buffer.from(await r.arrayBuffer());ok(crypto.createHash('sha256').update(bytes).digest('hex')===r.headers.get('x-content-sha256'),'report checksum exact');ok(bytes.toString().includes('50.00')&&bytes.toString().includes('1000.00')&&bytes.toString().includes('NAO E RACIO GERAL'),'report source amounts and scope plain');
|
||||
const read=await session('sgf:read_only','finance'),catalogue=await get(read,'?per_page=100&state=reviewed');ok(catalogue.records.total===2&&!catalogue.capabilities.prepare&&!catalogue.capabilities.review,'reviewed catalogue and read-only rights');const results=await get(read,'?view=results&per_page=100');ok(results.records.total===2,'only two saved results');const history=await get(read,'?view=history&metric='+coverage);ok(history.records.total===2&&history.metric_version===2,'criterion history version2');denied=await read.command('/api/financial-indicators/preview',input('prepare',coverage,2,def(coverage)));ok(denied.status===403,'read-only writer refused');const missing=await prep.command('/api/financial-indicators/preview',calc(null));ok(missing.status===200&&missing.value.impact.result.state==='pending','no criterion auto-selected');const foreign=await prep.command('/api/financial-indicators/preview',calc(2147483647));ok(foreign.status===403,'unscoped criterion refused');r=await prep.get('/api/financial-indicators?tenant_id=2');ok(r.status===422,'tenant override refused');const wrong=await session('sgf:finance_prepare','accounting');r=await wrong.get('/api/financial-indicators');ok([403,409].includes(r.status),'wrong module refused');
|
||||
const fresh=await session('sgf:finance_prepare','finance');r=await fresh.get('/api/financial-indicators/receipts/'+p.operations['calculate-coverage'].key);const recovered=await r.json();ok(r.status===200&&recovered.result.id===ac.id&&recovered.operation_executed===false,'own result receipt recovered in fresh session');r=await review.get('/api/financial-indicators/receipts/'+p.operations['calculate-coverage'].key);ok(r.status===404,'other actor receipt denied');r=await read.get('/finance/indicators',{headers:{Accept:'text/html'}});ok(r.status===200&&(await r.text()).includes('não são rácios gerais'),'actual screen with explicit QA limits');p.passed=true;p.business_confirmations=6;p.criteria_events_retained=4;p.results_retained=2;p.origin_simulated=false;p.new_financial_effects=0;p.browser_visual_review=false;save();console.log(JSON.stringify({passed:true,checks:p.checks.length,proof:file}));
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
$book='8085';require __DIR__.'/sgf-single-bootstrap.php';$p=json_decode(file_get_contents($s['root'].'/analysis-backup-pointer.json'),true);$before=json_decode(file_get_contents($p['root'].'/preservation-private.json'),true);$checks=[];foreach(['host'=>$host,'native'=>$native]as$name=>$db)foreach($before[$name]as$table=>$json){$rows=json_decode($json,true);$after=$db->table($table)->whereIn('id',array_column($rows,'id'))->orderBy('id')->get()->toJson();if($after!==$json)throw new LogicException('Original table changed '.$table);if($table!=='permission_grants'&&$db->table($table)->count()!==count($rows))throw new LogicException('Unexpected domain append '.$table);$checks[]=$name.'.'.$table;}
|
||||
$proposal=json_decode(file_get_contents($repo.'/docs/integration/sgf-financial-indicators-access-proposal.json'),true);$existingIds=array_column(json_decode($before['host']['permission_grants'],true),'id');$added=$host->table('permission_grants')->whereNotIn('id',$existingIds)->get();if(count($added)!==7)throw new LogicException('Exact7 approved grants only');foreach($added as$g)if(!in_array($g->permission,$proposal['new_right_names'],true))throw new LogicException('Unexpected grant');if(hash_file('sha256',$s['root'].'/binding.json')!==$before['binding'])throw new LogicException('Original binding changed');
|
||||
file_put_contents($work.'/analysis-preservation.json',json_encode(['passed'=>true,'identical_original_tables'=>$checks,'journals'=>27,'new_grants'=>7,'native_writes'=>0,'six_protected_records'=>'No connections or commands target protected databases/records','blocked_cache'=>'No clear/delete','new_demo'=>false],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'proof'=>$work.'/analysis-preservation.json']).PHP_EOL;
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
require __DIR__.'/sgf-ledger-proof-bootstrap.php';
|
||||
use App\Modules\Finance\{FinancialIndicators,FinancialIndicatorSource};use App\Shared\Context\CompanyContext;use Illuminate\Support\Facades\Auth;use Illuminate\Support\Str;
|
||||
config(['analysis_qa.enabled'=>true]);if($book!=='regression')throw new LogicException('Simulated source only in regression harness');
|
||||
$analysisSource=new class extends FinancialIndicatorSource{public array $values=['available'=>true,'kind'=>'analysis_harness_fixture','scope'=>['tenant_id'=>1,'company_id'=>2,'unit_id'=>2],'currency'=>'MZN','unit'=>'money','as_of'=>'2026-11-10','cash_balance_minor'=>'95000','cash_expenses_minor'=>'5000','cash_receipts_minor'=>'100000','supplier_credit_minor'=>'10000','cash_revision'=>9];public function snapshot(int $actor,array $scope):array{if($scope!==['tenant_id'=>1,'company_id'=>2,'unit_id'=>2])throw new LogicException('Fixture scope');$v=$this->values;$v['hash']=hash('sha256',Tempo\Finance\Payables\Payables::canonical($v));return $v;}};
|
||||
$app->instance(FinancialIndicatorSource::class,$analysisSource);$analysis=app(FinancialIndicators::class);
|
||||
$selectAnalysis=function($actor=13,$generation='analysis-proof')use($app){Auth::forgetGuards();Auth::shouldUse('web');$user=App\Models\User::findOrFail($actor);Auth::guard('web')->setUser($user);$session=app('session')->driver();$session->start();$session->put('company_context',['tenant_id'=>1,'company_id'=>2,'unit_id'=>2,'module_id'=>'finance','token'=>$generation.'-'.$actor]);$r=Illuminate\Http\Request::create('/api/financial-indicators','POST');$r->setUserResolver(fn()=>$user);$r->setLaravelSession($session);$r->attributes->set('sgf_module','finance');$app->instance('request',$r);app(CompanyContext::class)->selectFor($actor,1,2,2);return $actor;};
|
||||
$criterionData=function($metric){$d=FinancialIndicators::templates()[$metric];unset($d['label']);return $d+['scope_description'=>'Apenas as fontes QA expressamente delimitadas e data2026-11-10.','evidence'=>'Critério de ensaio confirmado explicitamente, sem recomendação financeira real.'];};
|
||||
$criterionInput=fn($action,$metric,$version,$data)=>['action'=>$action,'metric'=>$metric,'version'=>$version,'data'=>$data,'reason'=>'Conferência individual QA do critério, fontes, direcção e fotografia.'];
|
||||
$indicatorInput=fn($id,$date='2026-11-10')=>['action'=>'calculate','version'=>0,'data'=>['criterion_id'=>$id,'as_of'=>$date],'reason'=>'Cálculo QA por versão do critério seleccionada e fontes conciliadas.'];
|
||||
$analysisPreview=function($actor,$in,$key=null)use($analysis,$selectAnalysis){$selectAnalysis($actor);return $analysis->preview($actor,$in,$key??(string)Str::uuid());};
|
||||
$analysisCommand=function($actor,$in)use($analysis,$analysisPreview,$selectAnalysis){$p=$analysisPreview($actor,$in);$selectAnalysis($actor);return [$analysis->confirm($actor,$p['preview']),$p];};
|
||||
$reject=function($fn,$status,$name)use($ok){try{$fn();throw new RuntimeException('Expected refusal: '.$name);}catch(Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e){$ok($e->getStatusCode()===$status,$name);}catch(Illuminate\Validation\ValidationException){$ok($status===422,$name);}};
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
use Illuminate\Support\Facades\DB;
|
||||
require __DIR__.'/sgf-analysis-proof-bootstrap.php';$before=[];foreach(['an_criteria','an_results','an_commands','br_statements','br_events','fx_versions','fr_events','fr_calculations','cash_events','sr_effects']as$t)$before[$t]=$host->table($t)->orderBy('id')->get()->toJson();$nativeBefore=$native->table('gl_journals')->orderBy('id')->get()->toJson();$host->beginTransaction();try{
|
||||
$metric='cash_coverage_single_payable';$v=(int)$host->table('an_criteria')->where('metric',$metric)->max('version');$p=$analysisPreview(13,$criterionInput('prepare',$metric,$v,$criterionData($metric)));$ok($host->table('an_criteria')->orderBy('id')->get()->toJson()===$before['an_criteria'],'criterion preview rolled back');$reject(fn()=>$analysisPreview(17,$criterionInput('prepare',$metric,$v,$criterionData($metric))),403,'read-only cannot prepare');$reject(fn()=>$analysisPreview(13,$criterionInput('prepare',$metric,$v,array_replace($criterionData($metric),['denominator_direction'=>'absolute']))),422,'cannot silently use absolute liability');$reject(fn()=>$analysisPreview(13,$criterionInput('prepare',$metric,$v,array_replace($criterionData($metric),['numerator_amount'=>'950']))),422,'client cannot inject financial amounts');$selectAnalysis(13);$prep=$analysis->confirm(13,$p['preview']);$again=$analysis->confirm(13,$p['preview']);$ok($again['replayed']&&$again['id']===$prep['id'],'same command no duplicate criterion');$pending=$analysisPreview(13,$indicatorInput($prep['id']));$ok($pending['impact']['result']['state']==='pending','unreviewed criterion pending');$reject(fn()=>$analysisPreview(13,$criterionInput('review',$metric,$v+1,['decision'=>'reviewed'])),403,'distinct reviewer required');
|
||||
$cut1=$analysisPreview(14,$criterionInput('review',$metric,$v+1,['decision'=>'reviewed']));$cut2=$analysisPreview(14,$criterionInput('review',$metric,$v+1,['decision'=>'reviewed']));$selectAnalysis(14);$reviewed=$analysis->confirm(14,$cut1['preview']);$reject(fn()=>$analysis->confirm(14,$cut2['preview']),409,'stale second reviewer cut refused');[$result]=$analysisCommand(13,$indicatorInput($reviewed['id']));$selectAnalysis(17);$snapshot=$analysis->detail(17,$result['id'],'result');$r=$snapshot['result'];$ok($r['exact_fraction']==='19/2'&&$r['value']==='9.50'&&$r['unit']==='times','950/100 exact19/2 displayed9.50times');$ok($r['previous_period']===null&&$r['trend']===null,'absent comparison never zero trend');$ok(!$r['real_company_criterion']&&$r['representation_difference_fraction']==='0','hypothetical scope and representation difference explicit');
|
||||
$expense='cash_expense_share';[$ep]=$analysisCommand(13,$criterionInput('prepare',$expense,0,$criterionData($expense)));[$er]=$analysisCommand(14,$criterionInput('review',$expense,1,['decision'=>'reviewed']));[$ec]=$analysisCommand(13,$indicatorInput($er['id']));$selectAnalysis(17);$e=$analysis->detail(17,$ec['id'],'result')['result'];$ok($e['exact_fraction']==='1/20'&&$e['value']==='5.00'&&$e['unit']==='percent','50/1000 exact1/20 displayed5percent');$report=$analysis->report(17,$ec['id']);$ok(hash('sha256',$report['bytes'])===$report['sha256']&&str_contains($report['bytes'],'NAO E RACIO GERAL'),'internal historical report checksum and limit');
|
||||
$valid=$analysisSource->values;$analysisSource->values['supplier_credit_minor']='0';$zero=$analysisPreview(13,$indicatorInput($reviewed['id']));$ok($zero['impact']['result']['state']==='pending'&&$zero['impact']['result']['value']===null,'zero denominator pending no infinity');$analysisSource->values=$valid;$analysisSource->values['supplier_credit_minor']='-10000';$negative=$analysisPreview(13,$indicatorInput($reviewed['id']));$ok($negative['impact']['result']['state']==='pending','negative credit direction not coerced');$analysisSource->values=$valid;unset($analysisSource->values['cash_receipts_minor']);$missing=$analysisPreview(13,$indicatorInput($er['id']));$ok($missing['impact']['result']['state']==='pending','absent source not zero');$analysisSource->values=$valid;$analysisSource->values['currency']='USD';$currency=$analysisPreview(13,$indicatorInput($reviewed['id']));$ok($currency['impact']['result']['state']==='pending','currency mismatch pending');$analysisSource->values=$valid;$analysisSource->values['unit']='UN';$dimension=$analysisPreview(13,$indicatorInput($reviewed['id']));$ok($dimension['impact']['result']['state']==='pending','dimension mismatch pending');$analysisSource->values=$valid;$otherDate=$analysisPreview(13,$indicatorInput($reviewed['id'],'2026-11-09'));$ok($otherDate['impact']['result']['state']==='pending','unknown historic window pending');$none=$analysisPreview(13,$indicatorInput(null));$ok($none['impact']['result']['state']==='pending','no first criterion chosen automatically');
|
||||
$inflight=$analysisPreview(13,$indicatorInput($reviewed['id']));$analysisSource->values['cash_revision']=10;$selectAnalysis(13);$reject(fn()=>$analysis->confirm(13,$inflight['preview']),409,'source change invalidates confirmation');$selectAnalysis(17);$old=$analysis->detail(17,$result['id'],'result');$ok($old['result']===$snapshot['result']&&$old['current_evaluation']['state']==='pending','old result unchanged with current pending exposed');$analysisSource->values=$valid;
|
||||
[$successor]=$analysisCommand(13,$criterionInput('prepare',$metric,$v+2,$criterionData($metric)));$outdated=$analysisPreview(13,$indicatorInput($reviewed['id']));$ok($outdated['impact']['result']['state']==='pending','successor criterion invalidates previous version for new calculation');$review=$analysisPreview(14,$criterionInput('review',$metric,$successor['version'],['decision'=>'reviewed']));$host->beginTransaction();try{$host->table('permission_grants')->where(['user_id'=>13,'permission'=>'financial_analysis.prepare','unit_id'=>2])->delete();$selectAnalysis(14);$reject(fn()=>$analysis->confirm(14,$review['preview']),403,'preparer authority revalidated');}finally{$host->rollBack();}
|
||||
$host->beginTransaction();try{$host->statement("CREATE FUNCTION an_fail_test() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'Injected indicator failure'; END; $$");$host->statement('CREATE TRIGGER an_fail_test BEFORE INSERT ON an_commands FOR EACH ROW EXECUTE FUNCTION an_fail_test()');$n=$host->table('an_criteria')->count();$selectAnalysis(14);$failed=false;try{$analysis->confirm(14,$review['preview']);}catch(Illuminate\Database\QueryException){$failed=true;}$ok($failed&&$host->table('an_criteria')->count()===$n,'failed command receipt rolls back reviewed criterion');}finally{$host->rollBack();}$selectAnalysis(14);$recovered=$analysis->confirm(14,$review['preview']);$ok($recovered['state']==='reviewed','same review recovers after failure');
|
||||
$selectAnalysis(13);$key=$host->table('an_commands')->where('actor_id',13)->orderBy('id')->value('command_key');$ok($analysis->receipt(13,$key)['operation_executed']===false,'own historical receipt no execution');$selectAnalysis(14);$reject(fn()=>$analysis->receipt(14,$key),404,'other actor cannot recover key');$selectAnalysis(17);$listing=$analysis->listing(17,['view'=>'results','per_page'=>100]);$ok($listing['records']['total']===2&&!$listing['capabilities']['prepare']&&!$listing['capabilities']['review'],'read-only results catalogue and paged history');$reject(fn()=>$analysis->listing(17,['tenant_id'=>2]),422,'scope override refused');
|
||||
}finally{$host->rollBack();}foreach($before as$t=>$json)$ok($host->table($t)->orderBy('id')->get()->toJson()===$json,'unchanged '.$t);$ok($native->table('gl_journals')->orderBy('id')->get()->toJson()===$nativeBefore,'native ledger unchanged');file_put_contents($work.'/analysis-result.json',json_encode(['passed'=>true,'checks'=>$checks,'origin_simulated_in_harness'=>true,'rolled_back'=>true,'new_financial_effects'=>0],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/analysis-result.json']).PHP_EOL;
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
$book=$argv[1]??'regression';require __DIR__.'/sgf-single-bootstrap.php';$p=json_decode(file_get_contents($repo.'/docs/integration/sgf-financial-indicators-access-proposal.json'),true);if(($p['approved']??false)!==true||($p['authorization']['decision']??null)!=='Autorizar os indicadores QA e os direitos apresentados'||count($p['grants_per_book'])!==7)throw new LogicException('Specific approval required');$count=0;
|
||||
$host->transaction(function()use($host,$p,&$count){$old=$host->selectOne("SELECT pg_get_constraintdef(oid) d FROM pg_constraint WHERE conrelid='permission_grants'::regclass AND conname='permission_grants_scope_check'")->d;if(!str_contains($old,'financial_analysis.view')){if(!preg_match('/^CHECK \((.*)\)$/s',$old,$m))throw new LogicException('Unknown scope check');$host->statement('ALTER TABLE permission_grants DROP CONSTRAINT permission_grants_scope_check');$names=implode(',',array_map(fn($s)=>"'".$s."'",$p['new_right_names']));$host->statement('ALTER TABLE permission_grants ADD CONSTRAINT permission_grants_scope_check CHECK (('.$m[1].') OR (unit_id IS NOT NULL AND permission IN('.$names.')))');}foreach($p['grants_per_book']as$g){if(!in_array($g['user_id'],[13,14,15,16,17],true)||array_intersect_key($g,array_flip(['tenant_id','company_id','unit_id']))!==['tenant_id'=>1,'company_id'=>2,'unit_id'=>2])throw new LogicException('Exact approved recipients');if($host->table('permission_grants')->where($g)->exists())continue;$host->table('permission_grants')->insert($g);App\Modules\Identity\Services\Audit::record('access.granted_on_creation',null,1,2,2,$g['user_id'],['permission'=>$g['permission'],'reason'=>$p['authorization']]);$count++;}if(!$host->getSchemaBuilder()->hasTable('an_criteria'))$host->unprepared(file_get_contents(dirname(__DIR__).'/database/sql/financial_indicators.sql'));});
|
||||
file_put_contents($work.'/analysis-setup.json',json_encode(['passed'=>true,'book'=>$book,'new_grants'=>$count,'new_right_names'=>3,'new_users'=>0,'new_pg_roles'=>0,'new_demo'=>false],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'book'=>$book,'new_grants'=>$count,'proof'=>$work.'/analysis-setup.json']).PHP_EOL;
|
||||
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
$book=$argv[1]??'8085';require __DIR__.'/sgf-single-bootstrap.php';$rows=$native->table('gl_accounts as a')->leftJoin('gl_lines as l','l.account_id','=','a.id')->where(['a.tenant_id'=>1,'a.company_id'=>2,'a.unit_id'=>2])->whereIn('a.code',['QA-CAIXA','QA-OBRIGACAO','QA-FORNECEDOR','QA-OBRIGACOES','QA-PASSIVO'])->groupBy('a.id','a.code')->select(['a.id','a.code'])->selectRaw('coalesce(sum(l.debit_minor-l.credit_minor),0)::text net_debit_minor')->orderBy('a.id')->get();file_put_contents($work.'/analysis-sources-readonly.json',json_encode(['passed'=>true,'candidate_sources'=>$rows,'criteria_assigned'=>false,'new_rights'=>0,'business_commands'=>0],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'sources'=>$rows,'proof'=>$work.'/analysis-sources-readonly.json']).PHP_EOL;
|
||||
@@ -0,0 +1,2 @@
|
||||
import fs from 'node:fs';import assert from 'node:assert/strict';import crypto from 'node:crypto';
|
||||
const root=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')).root+'/private/receipt-composition-cc50a276c10c',s=JSON.parse(fs.readFileSync(root+'/state.json')),checks=[];const names=['sgf-entry-menu','sgf-stock-final','sgf-stock-completion','sgf-stock-catalogue','sgf-service-documents','sgf-cash','sgf-iva','sgf-fiscal-rules','sgf-bank','sgf-indicators'];for(const name of names){const path='/'+name+'.mjs',r=await fetch('http://127.0.0.1:8085'+path),bytes=Buffer.from(await r.arrayBuffer());assert.equal(r.status,200,path);assert(/javascript|ecmascript/.test(r.headers.get('content-type')),path+' JavaScript MIME');assert.equal(crypto.createHash('sha256').update(bytes).digest('hex'),crypto.createHash('sha256').update(fs.readFileSync(s.app+'/public'+path)).digest('hex'),path+' published bytes');checks.push(path+' actual JavaScript MIME and exact bytes');}const queried=await fetch('http://127.0.0.1:8085/sgf-stock-final.mjs?check=1');assert(/javascript/.test(queried.headers.get('content-type')));checks.push('query string preserves module delivery');const missing=await fetch('http://127.0.0.1:8085/unknown-sgf-module.mjs');assert.equal(missing.status,404);assert(!/html/.test(missing.headers.get('content-type')));checks.push('missing module404 not application HTML');const old=await fetch('http://127.0.0.1:8085/stock-operations-workspace.mjs');assert.equal(old.status,200);assert(/javascript/.test(old.headers.get('content-type')));checks.push('existing physical operations script still served');const file=root+'/assets-http-20261110.json';fs.writeFileSync(file,JSON.stringify({passed:true,checks,business_commands:0,database_writes:0,browser_visual_review:false},null,2));console.log(JSON.stringify({passed:true,checks:checks.length,proof:file}));
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
require __DIR__.'/sgf-selected-assets.php';$repo=dirname(__DIR__);$run=json_decode(file_get_contents('C:/laragon/data/sgf-tempo/run/modular-regression-current.json'),true);$root=$run['root'].'/private/receipt-composition-cc50a276c10c';$s=json_decode(file_get_contents($root.'/state.json'),true);$checks=[];$ok=function($v,$name)use(&$checks){if(!$v)throw new LogicException($name);$checks[]=$name;};foreach(['sgf-entry-menu','sgf-stock-final','sgf-stock-completion','sgf-stock-catalogue','sgf-service-documents','sgf-cash','sgf-iva','sgf-fiscal-rules','sgf-bank','sgf-indicators']as$name)$ok(sgfSelectedAsset($s,'/'.$name.'.mjs')===realpath($s['app'].'/public/'.$name.'.mjs'),'selected exact public module '.$name);
|
||||
$other=$s;$other['tag']='other-composition';$ok(sgfSelectedAsset($other,'/sgf-stock-final.mjs')===null,'other installation unchanged');$off=$s;$off['singleInstallation']=false;$ok(sgfSelectedAsset($off,'/sgf-stock-final.mjs')===null,'non-selected composition unchanged');foreach(['/../binding.json','/config/stock_catalogue.php','/sgf-stock-final.mjs/../../binding.json','/unknown.mjs']as$path)$ok(sgfSelectedAsset($s,$path)===null,'no general filesystem access '.$path);
|
||||
$source=str_replace("\r\n","\n",file_get_contents(__DIR__.'/receipt-composition-http.php'));$before=str_replace("\r\n","\n",file_get_contents($root.'/single-integration/20261110-214607-6709696b/router-before-asset-fix.php'));$source=str_replace("require_once __DIR__.'/sgf-selected-assets.php';\n\$assetPath=parse_url(\$_SERVER['REQUEST_URI'],PHP_URL_PATH);\nif(is_string(\$assetPath)&&sgfSelectedAsset(\$s,\$assetPath)!==null)return false;\n",'',$source);$source=str_replace("if((\$s['singleInstallation']??false)===true&&(\$s['tag']??null)==='cc50a276c10c'&&is_string(\$assetPath)&&str_ends_with(\$assetPath,'.mjs')){http_response_code(404);header('Content-Type: text/plain; charset=UTF-8');echo 'Recurso de cliente indisponível.';return;}\n",'',$source);$ok($source===$before,'original router body and existing whitelist preserved');$file=$root.'/asset-routing-boundaries-20261110.json';file_put_contents($file,json_encode(['passed'=>true,'checks'=>$checks,'database_writes'=>0,'other_installations_unchanged'=>true,'browser_visual_review'=>false],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$file]).PHP_EOL;
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
$book=$argv[1]??'regression';require __DIR__.'/sgf-single-bootstrap.php';
|
||||
$p=App\Shared\Installation\SyntheticProfiles::policy();
|
||||
$beforeUsers=$host->table('users')->orderBy('id')->get()->toJson();$beforeClients=$host->table('a_clients')->orderBy('id')->get()->toJson();
|
||||
file_put_contents($work.'/before-users-private.json',$beforeUsers);
|
||||
$result=(new App\Shared\Installation\SyntheticProfiles)->install();
|
||||
if($beforeClients!==$host->table('a_clients')->orderBy('id')->get()->toJson())throw new LogicException('Client preservation failed');
|
||||
file_put_contents($s['root'].'/single-profiles.json',json_encode($result,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
|
||||
file_put_contents($work.'/profiles-result.json',json_encode(['passed'=>true,'profiles'=>$result['profiles'],'replayed'=>$result['replayed'],'source_hash'=>$result['source_hash'],'old_clients_preserved'=>true,'real_access'=>false,'new_database_roles'=>0],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
|
||||
echo json_encode(['passed'=>true,'book'=>$book,'profiles'=>count($result['profiles']),'replayed'=>$result['replayed'],'proof'=>$work.'/profiles-result.json']).PHP_EOL;
|
||||
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
$book=$argv[1]??'regression';require __DIR__.'/sgf-single-bootstrap.php';$p=json_decode(file_get_contents($repo.'/docs/integration/sgf-bank-reconciliation-access-proposal.json'),true);if($p['approved']!==false||$p['applied']!==false||count($p['new_right_names'])!==4||count($p['grants_per_book'])!==8)throw new LogicException('Pending exact proposal required');$present=$host->table('permission_grants')->whereIn('permission',$p['new_right_names'])->count();if($present!==0)throw new LogicException('Reconcile existing rights before asking');foreach([13,14,15,16,17]as$id)if(!$host->table('company_memberships')->where(['tenant_id'=>1,'company_id'=>2,'user_id'=>$id])->exists())throw new LogicException('Existing recipient missing');file_put_contents($work.'/bank-access-readonly.json',json_encode(['passed'=>true,'new_right_names'=>4,'proposed_grants'=>8,'existing_new_right_grants'=>0,'grants_applied'=>0,'new_users'=>0],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'book'=>$book,'proof'=>$work.'/bank-access-readonly.json']).PHP_EOL;
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
$book='8085';require __DIR__.'/sgf-single-bootstrap.php';if(is_file($s['root'].'/bank-backup-pointer.json'))throw new LogicException('Preserve existing IVA baseline');if($native->table('gl_journals')->count()!==27)throw new LogicException('Expected27 accepted journals');
|
||||
foreach(['host'=>['database'=>$s['env']['DB_DATABASE'],'username'=>$s['env']['DB_USERNAME'],'password'=>$s['env']['DB_PASSWORD']],'native'=>$s['nativeConnection']]as$name=>$c){$dump=new Symfony\Component\Process\Process(['C:/Program Files/PostgreSQL/17/bin/pg_dump.exe','-h','127.0.0.1','-p','55439','-U',$c['username'],'-d',$c['database'],'--format=custom','--no-owner','--no-privileges','--file',$work.'/'.$name.'-before-bank.dump'],$repo,['PGPASSWORD'=>$c['password']]);$dump->setTimeout(120);$dump->mustRun();}
|
||||
$before=[];foreach(['si_invoices','sd_outputs','sd_versions','sr_records','sr_effects','cash_events','sf_proposals','sf_decisions','sf_activation_events','service_calculation_results','a_clients','fx_versions','fx_commands','fr_events','fr_calculations','fr_commands','permission_grants']as$t)$before['host'][$t]=$host->table($t)->orderBy('id')->get()->toJson();foreach(['stock_movements','gl_journals','gl_periods']as$t)$before['native'][$t]=$native->table($t)->orderBy('id')->get()->toJson();$before['binding']=hash_file('sha256',$s['root'].'/binding.json');file_put_contents($work.'/preservation-private.json',json_encode($before));file_put_contents($s['root'].'/bank-backup-pointer.json',json_encode(['root'=>$work]));echo json_encode(['passed'=>true,'private_backup'=>$work,'database_writes'=>0]).PHP_EOL;
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
use Illuminate\Support\Facades\DB;use App\Shared\Context\CompanyContext;
|
||||
require __DIR__.'/sgf-bank-proof-bootstrap.php';$before=$host->table('br_events')->count();$host->beginTransaction();try{
|
||||
$selectBank(13);$state=$bank->listing(13,[]);$id=$state['statement']['id'];$last=(array)$host->table('br_events')->where('statement_id',$id)->orderByDesc('version')->first();$version=$last['version'];$previous=$last['id'];for($i=0;$i<63;$i++){$prep=['tenant_id'=>1,'company_id'=>2,'unit_id'=>2,'statement_id'=>$id,'version'=>++$version,'previous_id'=>$previous,'proposal_id'=>null,'action'=>'prepare_match','state'=>'match_prepared','prepared_by'=>13,'actor_id'=>13,'data'=>json_encode(['row_reference'=>'QA-OUT-20','ledger_line_id'=>4,'journal_id'=>2,'debit_minor'=>2000,'credit_minor'=>0,'source_hash'=>'fixture']),'reason'=>'Metadados de histórico QA para paginação, não uma correspondência confirmada.','created_at'=>now()];$proposal=$host->table('br_events')->insertGetId($prep);$prep['version']=++$version;$prep['previous_id']=$proposal;$prep['proposal_id']=$proposal;$prep['action']='review_match';$prep['state']='match_refused';$prep['actor_id']=14;$previous=$host->table('br_events')->insertGetId($prep);}
|
||||
$selectBank(17);$a=$bank->listing(17,['view'=>'history','per_page'=>25]);$all=[];for($page=1;$page<=$a['history']['last_page'];$page++)$all=array_merge($all,$bank->listing(17,['view'=>'history','per_page'=>25,'page'=>$page])['history']['data']);$ok(count($all)===$before+126&&count(array_unique(array_column($all,'id')))===count($all),'128 history metadata events paginated25 no duplication');$a=$bank->listing(17,['view'=>'history','per_page'=>100]);$a2=$bank->listing(17,['view'=>'history','per_page'=>100,'page'=>2]);$ok(count($a['history']['data'])===100&&count($a2['history']['data'])===$before+26,'pages100 and28 same history universe');$ok(count($bank->listing(17,['status'=>'unmatched'])['rows'])===2&&count($bank->listing(17,['status'=>'matched'])['rows'])===0,'row status filters do not treat refused proposals as matches');$reject(fn()=>$bank->listing(17,['tenant_id'=>2]),422,'no scope override');$reject(fn()=>$bank->listing(17,['per_page'=>500]),422,'no excessive page');
|
||||
$selectBank(13);request()->attributes->set('sgf_module','accounting');$reject(fn()=>$bank->listing(13,[]),403,'wrong module rejected');$selectBank(13);$reject(function()use($bank){app(CompanyContext::class)->selectFor(13,1,2,1);return $bank->listing(13,[]);},403,'other unit rejected');$selectBank(13);$p=$bankPreview(13,$bankInput('prepare_match',$version,['row_reference'=>'QA-IN-20','ledger_line_id'=>5]));$selectBank(13,'different-generation');$reject(fn()=>$bank->confirm(13,$p['preview']),409,'generation invalidates match preview');
|
||||
$attempt=function($fn)use($host){$host->beginTransaction();try{$fn();return false;}catch(Illuminate\Database\QueryException){return true;}finally{$host->rollBack();}};$ok($attempt(fn()=>$host->table('br_events')->where('id',$previous)->update(['reason'=>'Attempted history alteration'])),'SQL append-only history');$bad=$prep;$bad['version']=++$version;$bad['previous_id']=$previous;$bad['proposal_id']=null;$bad['action']='prepare_match';$bad['state']='match_prepared';$bad['actor_id']=17;$bad['prepared_by']=17;$ok($attempt(fn()=>$host->table('br_events')->insert($bad)),'SQL unprivileged writer refused');
|
||||
}finally{$host->rollBack();}$ok($host->table('br_events')->count()===$before,'metadata history fixtures rolled back');file_put_contents($work.'/bank-boundaries.json',json_encode(['passed'=>true,'checks'=>$checks,'metadata_history_fixtures'=>126,'origin_simulated_in_harness'=>true,'rolled_back'=>true],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/bank-boundaries.json']).PHP_EOL;
|
||||
@@ -0,0 +1,4 @@
|
||||
import fs from 'node:fs';import {spawn,spawnSync} from 'node:child_process';import assert from 'node:assert/strict';
|
||||
const php='C:/laragon/tools/sgf-tempo/php-8.4.27/php.exe',script='scripts/sgf-bank-concurrency.php',run=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')),dir=run.root+'/private/receipt-composition-b4093c6f6707/bank-concurrency';
|
||||
const call=mode=>{const r=spawnSync(php,[script,mode],{encoding:'utf8',windowsHide:true});assert.equal(r.status,0,r.stdout+r.stderr);assert.match(r.stdout,/"passed":true/);return r.stdout;};if(fs.existsSync(dir+'/result.json')){assert(JSON.parse(fs.readFileSync(dir+'/result.json')).passed);console.log(JSON.stringify({passed:true,reused:true,proof:dir+'/result.json'}));process.exit();}
|
||||
call('prepare');const first=spawn(php,[script,'first'],{stdio:'pipe',windowsHide:true});let out='';first.stdout.on('data',d=>out+=d);first.stderr.on('data',d=>out+=d);const ended=new Promise(r=>first.once('exit',r));try{const deadline=Date.now()+10000;while(!fs.existsSync(dir+'/locked')&&Date.now()<deadline)await new Promise(r=>setTimeout(r,40));assert(fs.existsSync(dir+'/locked'));const second=spawn(php,[script,'second'],{stdio:'pipe',windowsHide:true});let output='';second.stdout.on('data',d=>output+=d);second.stderr.on('data',d=>output+=d);assert.equal(await new Promise(r=>second.once('exit',r)),0,output);assert.match(output,/"passed":true/);const r=JSON.parse(fs.readFileSync(dir+'/second.json'));assert.equal(r.status,503);assert.equal(r.retry_after,'5');assert(r.elapsed_seconds>=4.5&&r.elapsed_seconds<12);}finally{fs.writeFileSync(dir+'/release','Release existing regression lock only');assert.equal(await ended,0,out);}console.log(call('recover').trim());
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
require __DIR__.'/sgf-bank-proof-bootstrap.php';$dir=$s['root'].'/bank-concurrency';if(!is_dir($dir))mkdir($dir,0700,true);$file=$dir.'/progress.json';$mode=$argv[1]??'';
|
||||
if($mode==='prepare'){if(is_file($file))throw new LogicException('Reuse preparation');[$p]=$bankCommand(13,$bankInput('import',0,$importData));$a=$bankPreview(14,$bankInput('review_statement',1,['decision'=>'reviewed']));$b2=$bankPreview(14,$bankInput('review_statement',1,['decision'=>'reviewed']));file_put_contents($file,json_encode(['cuts'=>[1=>$a['preview'],2=>$b2['preview']],'before'=>$host->table('br_events')->count()]));}
|
||||
elseif($mode==='first'){$p=json_decode(file_get_contents($file),true);$selectBank(14);$host->beginTransaction();try{$out=$bank->confirm(14,$p['cuts'][1]);file_put_contents($dir.'/locked','Host company lock held');$end=microtime(true)+25;while(!is_file($dir.'/release')&µtime(true)<$end)usleep(50000);if(!is_file($dir.'/release'))throw new LogicException('Release missing');$host->commit();file_put_contents($dir.'/first.json',json_encode($out));}finally{while($host->transactionLevel()>0)$host->rollBack();}}
|
||||
elseif($mode==='second'){$p=json_decode(file_get_contents($file),true);$selectBank(14);request()->request->replace(['preview'=>$p['cuts'][2]]);$start=microtime(true);$r=app(App\Modules\Finance\Http\BankReconciliationController::class)->confirm(request(),$bank);file_put_contents($dir.'/second.json',json_encode(['status'=>$r->getStatusCode(),'retry_after'=>$r->headers->get('Retry-After'),'elapsed_seconds'=>microtime(true)-$start]));}
|
||||
elseif($mode==='recover'){$p=json_decode(file_get_contents($file),true);$selectBank(14);$reject(fn()=>$bank->confirm(14,$p['cuts'][2]),409,'second review stale after release');$a=$bank->confirm(14,$p['cuts'][1]);$ok($a['replayed']&&$host->table('br_events')->count()===$p['before']+1,'one reviewed header without duplicate');file_put_contents($dir.'/result.json',json_encode(['passed'=>true,'during_lock'=>json_decode(file_get_contents($dir.'/second.json'),true),'after_release'=>409,'one_review'=>true,'origin_simulated_in_harness'=>true,'regression_only'=>true],JSON_PRETTY_PRINT));}
|
||||
else throw new LogicException('Unknown mode');echo json_encode(['passed'=>true,'phase'=>$mode,'proof'=>$dir.'/result.json']).PHP_EOL;
|
||||
@@ -0,0 +1,5 @@
|
||||
// Actual client over actual HTTP in its retained finalized state; zero POSTs.
|
||||
import fs from 'node:fs';import assert from 'node:assert/strict';import {parsePage,installDom} from './purchasing-workspace-dom.mjs';import {session} from './sgf-single-http.mjs';
|
||||
const nativeFetch=globalThis.fetch,s=await session('sgf:finance_prepare','finance');let r=await s.get('/finance/bank',{headers:{Accept:'text/html'}});assert.equal(r.status,200);const document=parsePage(await r.text());installDom(document);document.getElementById=id=>document.querySelector('#'+id);globalThis.window={addEventListener(){}};const $=id=>document.getElementById(id);let posts=0;const calls=[],checks=[];const ok=(v,n)=>{assert(v,n);checks.push(n);};
|
||||
globalThis.fetch=async(url,options={})=>{if(url.startsWith('http:'))return nativeFetch(url,options);calls.push(url);if(options.method==='POST'){posts++;throw Error('No bank POST allowed in retained final state proof');}const a=await s.get(url);return{status:a.status,json:()=>a.json()};};await import('../public/sgf-bank.mjs?dom-http');const wait=async f=>{const end=Date.now()+15000;while(!f()&&Date.now()<end)await new Promise(r=>setTimeout(r,25));assert(f(),$('message').textContent);};await wait(()=>$('events').children.length===7);ok($('summary').textContent.includes('Finalizada')&&$('summary').textContent.includes('versão 7'),'finalized conference visible');ok($('summary').textContent.includes('linhas efectivas do diário QA'),'actual native source identified');ok($('summary').textContent.includes('abertura 0.00')&&$('summary').textContent.includes('fecho 0.00'),'explicit statement opening/closing');ok($('fields').disabled,'finalized workflow has no writer controls');ok(!$('filters').disabled,'historical read controls remain available');ok($('rows').children.length===2&&$('rows').textContent.includes('diário 2')&&$('rows').textContent.includes('diário 3'),'two explicit historical correspondences visible');ok($('download').querySelector('a').href.endsWith('/report'),'reviewed report link available');$('status').value='unmatched';$('apply').onclick();await wait(()=>$('rows').children.length===0&&!$('filters').disabled);ok($('rows').children.length===0,'unmatched status filter applied');$('status').value='matched';$('size').value='100';$('apply').onclick();await wait(()=>$('rows').children.length===2&&!$('filters').disabled&&calls.some(c=>c.includes('per_page=100')));ok($('events').children.length===7,'history100 preserves7 existing events');$('events').children[0].querySelector('button').onclick();ok($('detail').textContent.includes('fundamento')&&$('detail').textContent.includes('difference_minor'),'selected historical version details');
|
||||
const run=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')),root=run.root+'/private/receipt-composition-cc50a276c10c',p=JSON.parse(fs.readFileSync(root+'/bank-http-20261110.json'));$('referenceKey').value=p.operations.import.key;await $('recovery').onsubmit({preventDefault(){}});ok($('receipt').textContent.includes('Nenhuma operação repetida'),'historical receipt recovered without execution');ok(posts===0,'zero business POSTs');r=await s.get('/api/cash');const cash=await r.json();ok(r.status===200&&cash.basis.closed&&cash.basis.balance_minor===95000,'cash remains closed950');const acct=await session('sgf:finance_prepare','accounting');r=await acct.get('/api/fiscal-assessments');const iva=await r.json();ok(r.status===200&&iva.form_current_review&&iva.events.total===4,'IVA book/map remains reviewed with4versions');r=await acct.get('/api/fiscal-rules?view=calculations');ok(r.status===200&&(await r.json()).records.total===2,'two fiscal calculations retained');const file=root+'/bank-dom-http-20261110.json';fs.writeFileSync(file,JSON.stringify({passed:true,checks,business_posts:posts,real_http_forwarded:true,browser_visual_review:false},null,2));console.log(JSON.stringify({passed:true,checks:checks.length,proof:file}));
|
||||
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
require __DIR__.'/sgf-single-bootstrap.php';if($tag!=='b4093c6f6707')throw new LogicException('Regression function correction only');$sql=file_get_contents($repo.'/database/sql/bank_reconciliation.sql');if(!preg_match('/CREATE FUNCTION sgf_br_guard\(\).*?END; \$\$;/s',$sql,$m))throw new LogicException('Guard declaration missing');$host->unprepared(str_replace('CREATE FUNCTION','CREATE OR REPLACE FUNCTION',$m[0]));echo json_encode(['passed'=>true,'regression_only'=>true,'data_changes'=>0]).PHP_EOL;
|
||||
@@ -0,0 +1,7 @@
|
||||
import fs from 'node:fs';import assert from 'node:assert/strict';import crypto from 'node:crypto';import {session} from './sgf-single-http.mjs';
|
||||
const run=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')),root=run.root+'/private/receipt-composition-cc50a276c10c',file=root+'/bank-http-20261110.json';let p=fs.existsSync(file)?JSON.parse(fs.readFileSync(file)):{operations:{},checks:[]};if(p.passed){console.log(JSON.stringify({passed:true,reused:true,proof:file}));process.exit();}const save=()=>fs.writeFileSync(file,JSON.stringify(p,null,2));const ok=(v,n)=>{assert(v,n);if(!p.checks.includes(n))p.checks.push(n);save();};const prep=await session('sgf:finance_prepare','finance'),review=await session('sgf:finance_review','finance');const get=async(s=prep,query='')=>{const r=await s.get('/api/bank-reconciliation'+query);assert.equal(r.status,200);return r.json();};let state=await get();ok(prep.identity.context.bank_qa_enabled===true,'feature available in authorized finance context');ok(state.source_kind==='native_qa_ledger'&&state.source_lines.length===2,'actual native reader no harness fallback');ok(state.source_lines[0].id===4&&state.source_lines[0].credit_minor===2000&&state.source_lines[1].id===5&&state.source_lines[1].debit_minor===2000,'two existing native20 lines exact');
|
||||
const importData={account_ref:'QA-BANCO-LOCAL',currency:'MZN',period:'2026-11',opening:'0.00',closing:'0.00',csv:state.template_csv};const input=(action,version,data={},proposal)=>({action,version,data,...(proposal?{proposal_id:proposal}:{}),reason:'Conferência QA individual do extracto e da correspondência exacta, sem efeito financeiro.'});
|
||||
const command=async(name,s,body)=>{if(p.operations[name]?.result)return p.operations[name].result;let op=p.operations[name];if(!op){op=p.operations[name]={key:crypto.randomUUID(),input:body};save();}if(!op.cut){const r=await s.command('/api/bank-reconciliation/preview',op.input,op.key);assert.equal(r.status,200,JSON.stringify(r.value));op.cut=r.value.preview;ok(r.value.impact.new_journals===0&&r.value.impact.bank_operations===false,'preview no financial or real bank effect');save();}const r=await s.command('/api/bank-reconciliation/confirm',{preview:op.cut});assert.equal(r.status,200,JSON.stringify(r.value));op.result=r.value;save();const repeat=await s.command('/api/bank-reconciliation/confirm',{preview:op.cut});ok(repeat.status===200&&repeat.value.replayed&&repeat.value.id===r.value.id,'repeat '+name+' no duplication');return r.value;};
|
||||
const imported=await command('import',prep,input('import',0,importData));state=await get();ok(state.statement.entries.length===2&&state.rows.every(r=>r.status==='unmatched'),'import does not automatically match either row');let r=await prep.command('/api/bank-reconciliation/preview',input('prepare_match',1,{row_reference:'QA-OUT-20',ledger_line_id:4}));ok(r.status===409,'unreviewed extract blocks matching');await command('review-extract',review,input('review_statement',1,{decision:'reviewed'}));r=await prep.command('/api/bank-reconciliation/preview',input('prepare_match',2,{row_reference:'QA-OUT-20',ledger_line_id:5}));ok(r.status===422,'wrong debit/credit direction refused');r=await prep.command('/api/bank-reconciliation/preview',input('prepare_match',2,{row_reference:'QA-OUT-20',ledger_line_id:999999}));ok(r.status===403,'unbound source line refused');const a=await command('prepare-out',prep,input('prepare_match',2,{row_reference:'QA-OUT-20',ledger_line_id:4}));await command('review-out',review,input('review_match',3,{decision:'reviewed'},a.id));r=await prep.command('/api/bank-reconciliation/preview',input('prepare_match',4,{row_reference:'QA-OUT-20',ledger_line_id:4}));ok(r.status===409,'already matched line cannot be reused');const b=await command('prepare-in',prep,input('prepare_match',4,{row_reference:'QA-IN-20',ledger_line_id:5}));await command('review-in',review,input('review_match',5,{decision:'reviewed'},b.id));r=await prep.get('/api/bank-reconciliation/report');ok(r.status===409,'report unavailable until final review');await command('finalize',review,input('finalize',6));state=await get();ok(state.finalized&&state.version===7&&state.history.total===7&&state.rows.every(r=>r.status==='matched'),'exact7 events and2 individually matched rows final');
|
||||
r=await prep.get('/api/bank-reconciliation/report');ok(r.status===200&&r.headers.get('content-type').includes('text/csv'),'internal report downloadable');const bytes=Buffer.from(await r.arrayBuffer());ok(crypto.createHash('sha256').update(bytes).digest('hex')===r.headers.get('x-content-sha256'),'report checksum verified');ok(bytes.toString().includes('QA-OUT-20;4;2;20.00;0.00')&&bytes.toString().includes('QA-IN-20;5;3;0.00;20.00')&&bytes.toString().includes('NAO E EXTRACTO BANCARIO REAL'),'report exact actual associations and clear artificial mark');
|
||||
const read=await session('sgf:read_only','finance'),readonly=await get(read,'?status=matched&per_page=100');ok(readonly.rows.length===2&&!readonly.capabilities.import&&!readonly.capabilities.prepare&&!readonly.capabilities.review,'matched filter and read-only capabilities');r=await read.command('/api/bank-reconciliation/preview',input('import',7,importData));ok(r.status===403,'read-only import refused');r=await prep.get('/api/bank-reconciliation?tenant_id=2');ok(r.status===422,'tenant override refused');const wrong=await session('sgf:finance_prepare','accounting');r=await wrong.get('/api/bank-reconciliation');ok([403,409].includes(r.status),'wrong module refused');const fresh=await session('sgf:finance_prepare','finance');r=await fresh.get('/api/bank-reconciliation/receipts/'+p.operations.import.key);const recovered=await r.json();ok(r.status===200&&recovered.result.id===imported.id&&recovered.operation_executed===false,'own import receipt recovered in fresh session');r=await review.get('/api/bank-reconciliation/receipts/'+p.operations.import.key);ok(r.status===404,'other actor receipt denied');r=await read.get('/finance/bank',{headers:{Accept:'text/html'}});ok(r.status===200&&(await r.text()).includes('sem banco ou dinheiro real'),'screen explicit synthetic scope');p.passed=true;p.business_confirmations=7;p.retained_statement_headers=1;p.retained_statement_rows=2;p.retained_events=7;p.origin_simulated=false;p.native_reader_proved=true;p.new_journals=0;p.browser_visual_review=false;save();console.log(JSON.stringify({passed:true,checks:p.checks.length,proof:file}));
|
||||
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
$book=$argv[1]??'8085';require __DIR__.'/sgf-single-bootstrap.php';$r=$native->table('gl_lines as l')->join('gl_accounts as a','a.id','=','l.account_id')->join('gl_journals as j','j.id','=','l.journal_id')->where('l.debit_minor','<=',2000)->where('l.credit_minor','<=',2000)->where('a.code','like','%LIQUID%')->select(['l.id','l.journal_id','a.code','l.debit_minor','l.credit_minor','j.journal_on'])->orderBy('l.id')->get();file_put_contents($work.'/bank-origin-readonly.json',json_encode(['passed'=>true,'candidate_lines'=>$r,'linked_as_bank'=>false,'new_rights'=>0,'business_commands'=>0],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'candidate_lines'=>$r,'proof'=>$work.'/bank-origin-readonly.json']).PHP_EOL;
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
$book='8085';require __DIR__.'/sgf-single-bootstrap.php';$p=json_decode(file_get_contents($s['root'].'/bank-backup-pointer.json'),true);$before=json_decode(file_get_contents($p['root'].'/preservation-private.json'),true);$checks=[];foreach(['host'=>$host,'native'=>$native]as$name=>$db)foreach($before[$name]as$table=>$json){$rows=json_decode($json,true);$after=$db->table($table)->whereIn('id',array_column($rows,'id'))->orderBy('id')->get()->toJson();if($after!==$json)throw new LogicException('Original table changed '.$table);if($table!=='permission_grants'&&$db->table($table)->count()!==count($rows))throw new LogicException('Unexpected domain append '.$table);$checks[]=$name.'.'.$table;}
|
||||
$proposal=json_decode(file_get_contents($repo.'/docs/integration/sgf-bank-reconciliation-access-proposal.json'),true);$existingIds=array_column(json_decode($before['host']['permission_grants'],true),'id');$added=$host->table('permission_grants')->whereNotIn('id',$existingIds)->get();if(count($added)!==8)throw new LogicException('Exact8 approved grants only');foreach($added as$g)if(!in_array($g->permission,$proposal['new_right_names'],true))throw new LogicException('Unexpected grant');if(hash_file('sha256',$s['root'].'/binding.json')!==$before['binding'])throw new LogicException('Original binding changed');
|
||||
file_put_contents($work.'/bank-preservation.json',json_encode(['passed'=>true,'identical_original_tables'=>$checks,'journals'=>27,'new_grants'=>8,'native_writes'=>0,'six_protected_records'=>'No connections or commands target protected databases/records','blocked_cache'=>'No clear/delete','new_demo'=>false],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'proof'=>$work.'/bank-preservation.json']).PHP_EOL;
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
require __DIR__.'/sgf-ledger-proof-bootstrap.php';
|
||||
use App\Modules\Finance\{BankReconciliation,BankLedgerSource};use App\Shared\Context\CompanyContext;use Illuminate\Support\Facades\Auth;use Illuminate\Support\Str;
|
||||
config(['bank_qa.enabled'=>true,'bank_qa.binding_sha256'=>hash_file('sha256',$s['root'].'/bank-binding.json')]);
|
||||
if($book!=='regression')throw new LogicException('Injected source only in regression harness');
|
||||
$fakeSource=new class extends BankLedgerSource{public bool $changed=false;public function snapshot(array $scope,array $binding):array{if($scope!==['tenant_id'=>1,'company_id'=>2,'unit_id'=>2]||$binding['composition']!=='b4093c6f6707')throw new LogicException('Regression fixture scope');$rows=[['id'=>4,'journal_id'=>2,'account_code'=>'QA-LIQUIDACAO','date'=>'2026-11-10','currency'=>'MZN','debit_minor'=>0,'credit_minor'=>2000,'journal_sha256'=>str_repeat($this->changed?'c':'a',64)],['id'=>5,'journal_id'=>3,'account_code'=>'QA-LIQUIDACAO','date'=>'2026-11-10','currency'=>'MZN','debit_minor'=>2000,'credit_minor'=>0,'journal_sha256'=>str_repeat('b',64)]];return ['kind'=>'regression_harness_fixture','lines'=>$rows,'hash'=>hash('sha256',Tempo\Finance\Payables\Payables::canonical($rows))];}};
|
||||
$app->instance(BankLedgerSource::class,$fakeSource);$bank=app(BankReconciliation::class);
|
||||
$selectBank=function($actor=13,$generation='bank-proof')use($app){Auth::forgetGuards();Auth::shouldUse('web');$user=App\Models\User::findOrFail($actor);Auth::guard('web')->setUser($user);$session=app('session')->driver();$session->start();$session->put('company_context',['tenant_id'=>1,'company_id'=>2,'unit_id'=>2,'module_id'=>'finance','token'=>$generation.'-'.$actor]);$r=Illuminate\Http\Request::create('/api/bank-reconciliation','POST');$r->setUserResolver(fn()=>$user);$r->setLaravelSession($session);$r->attributes->set('sgf_module','finance');$app->instance('request',$r);app(CompanyContext::class)->selectFor($actor,1,2,2);return $actor;};
|
||||
$importData=['account_ref'=>'QA-BANCO-LOCAL','currency'=>'MZN','period'=>'2026-11','opening'=>'0.00','closing'=>'0.00','csv'=>file_get_contents($repo.'/resources/data/bank-statement-qa.csv')];
|
||||
$bankInput=fn($action,$version,$data=[],$proposal=null)=>['action'=>$action,'version'=>$version,...($proposal?['proposal_id'=>$proposal]:[]),'data'=>$data,'reason'=>'Conferência individual QA de extracto, origem, sinais e versão, sem efeito financeiro.'];
|
||||
$bankPreview=function($actor,$in,$key=null)use($bank,$selectBank){$selectBank($actor);return $bank->preview($actor,$in,$key??(string)Str::uuid());};
|
||||
$bankCommand=function($actor,$in)use($bank,$bankPreview,$selectBank){$p=$bankPreview($actor,$in);$selectBank($actor);return [$bank->confirm($actor,$p['preview']),$p];};
|
||||
$reject=function($fn,$status,$name)use($ok){try{$fn();throw new RuntimeException('Expected refusal: '.$name);}catch(Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e){$ok($e->getStatusCode()===$status,$name);}catch(Illuminate\Validation\ValidationException){$ok($status===422,$name);}};
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
use Illuminate\Support\Facades\{DB,Crypt};
|
||||
require __DIR__.'/sgf-bank-proof-bootstrap.php';$before=[];foreach(['br_statements','br_events','br_commands','fx_versions','fr_events','fr_calculations','si_invoices','sr_effects','cash_events']as$t)$before[$t]=$host->table($t)->orderBy('id')->get()->toJson();$nativeBefore=$native->table('gl_journals')->orderBy('id')->get()->toJson();$host->beginTransaction();try{
|
||||
$selectBank(13);$state=$bank->listing(13,[]);$v=$state['version'];$ok($state['source_kind']==='regression_harness_fixture','fixture source explicitly identified not actual ledger');$p=$bankPreview(13,$bankInput('import',$v,$importData));$ok($host->table('br_statements')->orderBy('id')->get()->toJson()===$before['br_statements'],'import preview leaves no header');$reject(fn()=>$bankPreview(17,$bankInput('import',$v,$importData)),403,'read-only import refused');$reject(fn()=>$bankPreview(13,$bankInput('import',$v,array_replace($importData,['currency'=>'USD']))),422,'currency mismatch refused');$reject(fn()=>$bankPreview(13,$bankInput('import',$v,array_replace($importData,['csv'=>str_replace('20.00','21.00',$importData['csv'])]))),422,'amount/balance mismatch refused');
|
||||
$selectBank(13);$imported=$bank->confirm(13,$p['preview']);$again=$bank->confirm(13,$p['preview']);$ok($again['replayed']&&$again['id']===$imported['id'],'same import key no duplicate header or rows');$reject(fn()=>$bankPreview(13,$bankInput('prepare_match',$v+1,['row_reference'=>'QA-OUT-20','ledger_line_id'=>4])),409,'unreviewed statement blocks matching');$reject(fn()=>$bankPreview(13,$bankInput('review_statement',$v+1,['decision'=>'reviewed'])),403,'importer cannot review');
|
||||
[$review]=$bankCommand(14,$bankInput('review_statement',$v+1,['decision'=>'reviewed']));$ok($review['state']==='statement_reviewed','different person reviews exact statement');$reject(fn()=>$bankPreview(14,$bankInput('finalize',$v+2,[])),409,'incomplete statement not finalized');$reject(fn()=>$bankPreview(13,$bankInput('prepare_match',$v+2,['row_reference'=>'QA-OUT-20','ledger_line_id'=>5])),422,'same amount wrong direction refused');$reject(fn()=>$bankPreview(13,$bankInput('prepare_match',$v+2,['row_reference'=>'QA-OUT-20','ledger_line_id'=>999])),403,'unbound ledger line refused');
|
||||
[$match]=$bankCommand(13,$bankInput('prepare_match',$v+2,['row_reference'=>'QA-OUT-20','ledger_line_id'=>4]));$reject(fn()=>$bankPreview(13,$bankInput('prepare_match',$v+3,['row_reference'=>'QA-OUT-20','ledger_line_id'=>4])),409,'pending row cannot receive another proposal');$a=$bankPreview(14,$bankInput('review_match',$v+3,['decision'=>'reviewed'],$match['id']));$b2=$bankPreview(14,$bankInput('review_match',$v+3,['decision'=>'reviewed'],$match['id']));$selectBank(14);$out=$bank->confirm(14,$a['preview']);$reject(fn()=>$bank->confirm(14,$b2['preview']),409,'second stale review refused');$reject(fn()=>$bankPreview(13,$bankInput('prepare_match',$v+4,['row_reference'=>'QA-OUT-20','ledger_line_id'=>4])),409,'confirmed ledger/statement row cannot be reused');
|
||||
[$match2]=$bankCommand(13,$bankInput('prepare_match',$v+4,['row_reference'=>'QA-IN-20','ledger_line_id'=>5]));[$review2]=$bankCommand(14,$bankInput('review_match',$v+5,['decision'=>'reviewed'],$match2['id']));$selectBank(17);$state=$bank->listing(17,['status'=>'matched']);$ok(count($state['rows'])===2&&$state['statement']['closing_minor']===0&&!$state['report_available'],'both rows individually reviewed, report still not final');$reject(fn()=>$bank->report(17),409,'pre-final report not available');[$final]=$bankCommand(14,$bankInput('finalize',$v+6,[]));$ok($final['state']==='finalized'&&$final['impact']['data']['difference_minor']===0,'final conference zero difference');$selectBank(17);$report=$bank->report(17);$ok(hash('sha256',$report['bytes'])===$report['sha256']&&str_contains($report['bytes'],'NAO E EXTRACTO BANCARIO REAL')&&str_contains($report['bytes'],'QA-OUT-20;4;2'),'report recovers exact explicit correspondences');$reject(fn()=>$bankPreview(13,$bankInput('prepare_match',$v+7,['row_reference'=>'QA-IN-20','ledger_line_id'=>5])),409,'finalized book cannot be reopened');
|
||||
[$same]=$bankCommand(13,$bankInput('import',$v+7,$importData));$ok($same['already_imported']&&$host->table('br_events')->count()===7,'same CSV under new key returns same finalized statement without events');$selectBank(13);$key=$host->table('br_commands')->where('actor_id',13)->orderBy('id')->value('command_key');$ok($bank->receipt(13,$key)['operation_executed']===false,'own receipt read does not execute');$selectBank(14);$reject(fn()=>$bank->receipt(14,$key),404,'other actor cannot recover key');
|
||||
$fakeSource->changed=true;$selectBank(17);$blocked=$bank->listing(17,[]);$ok($blocked['blocked']!==null&&!$blocked['report_available']&&$blocked['source_lines']===[],'changed source blocks report without creating zero balance');$reject(fn()=>$bank->report(17),409,'changed source requires new conference');$fakeSource->changed=false;
|
||||
}finally{$host->rollBack();}
|
||||
// New import, inject receipt failure and recover. No accepted statement is edited.
|
||||
$host->beginTransaction();try{$p=$bankPreview(13,$bankInput('import',0,$importData));$host->beginTransaction();try{$host->statement("CREATE FUNCTION br_fail_test() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'Injected bank failure'; END; $$");$host->statement('CREATE TRIGGER br_fail_test BEFORE INSERT ON br_commands FOR EACH ROW EXECUTE FUNCTION br_fail_test()');$selectBank(13);$failed=false;try{$bank->confirm(13,$p['preview']);}catch(Illuminate\Database\QueryException){$failed=true;}$ok($failed&&$host->table('br_statements')->count()===0&&$host->table('br_events')->count()===0,'failed receipt insertion rolls back import/header/event');}finally{$host->rollBack();}$selectBank(13);$success=$bank->confirm(13,$p['preview']);$ok($success['state']==='imported','same import command recovers after failure');}finally{$host->rollBack();}
|
||||
foreach($before as$t=>$json)$ok($host->table($t)->orderBy('id')->get()->toJson()===$json,'unchanged '.$t);$ok($native->table('gl_journals')->orderBy('id')->get()->toJson()===$nativeBefore,'native ledger untouched');file_put_contents($work.'/bank-result.json',json_encode(['passed'=>true,'checks'=>$checks,'origin_simulated_in_harness'=>true,'rolled_back'=>true,'new_financial_effects'=>0,'new_demo'=>false],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/bank-result.json']).PHP_EOL;
|
||||
@@ -0,0 +1,2 @@
|
||||
import fs from 'node:fs';import assert from 'node:assert/strict';import crypto from 'node:crypto';import {session} from './sgf-single-http.mjs';
|
||||
const s=await session('sgf:read_only','finance'),r=await s.get('/api/bank-reconciliation/report');assert.equal(r.status,200);assert(r.headers.get('content-type').includes('text/csv'));const bytes=Buffer.from(await r.arrayBuffer());assert.equal(crypto.createHash('sha256').update(bytes).digest('hex'),r.headers.get('x-content-sha256'));const run=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')),file=run.root+'/private/receipt-composition-cc50a276c10c/bank-report-http-20261110.json';fs.writeFileSync(file,JSON.stringify({passed:true,checks:3,checksum:r.headers.get('x-content-sha256'),shared_http_busy_handler:true,business_commands:0},null,2));console.log(JSON.stringify({passed:true,checks:3,proof:file}));
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
$book=$argv[1]??'regression';require __DIR__.'/sgf-single-bootstrap.php';$p=json_decode(file_get_contents($repo.'/docs/integration/sgf-bank-reconciliation-access-proposal.json'),true);if(($p['approved']??false)!==true||($p['authorization']['decision']??null)!=='Autorizar a conferência bancária QA e os direitos apresentados'||count($p['grants_per_book'])!==8)throw new LogicException('Specific approval required');$count=0;
|
||||
$host->transaction(function()use($host,$p,&$count){$old=$host->selectOne("SELECT pg_get_constraintdef(oid) d FROM pg_constraint WHERE conrelid='permission_grants'::regclass AND conname='permission_grants_scope_check'")->d;if(!str_contains($old,'bank_reconciliation.view')){if(!preg_match('/^CHECK \((.*)\)$/s',$old,$m))throw new LogicException('Unknown scope check');$host->statement('ALTER TABLE permission_grants DROP CONSTRAINT permission_grants_scope_check');$names=implode(',',array_map(fn($s)=>"'".$s."'",$p['new_right_names']));$host->statement('ALTER TABLE permission_grants ADD CONSTRAINT permission_grants_scope_check CHECK (('.$m[1].') OR (unit_id IS NOT NULL AND permission IN('.$names.')))');}foreach($p['grants_per_book']as$g){if(!in_array($g['user_id'],[13,14,15,16,17],true)||array_intersect_key($g,array_flip(['tenant_id','company_id','unit_id']))!==['tenant_id'=>1,'company_id'=>2,'unit_id'=>2])throw new LogicException('Exact approved recipients');if($host->table('permission_grants')->where($g)->exists())continue;$host->table('permission_grants')->insert($g);App\Modules\Identity\Services\Audit::record('access.granted_on_creation',null,1,2,2,$g['user_id'],['permission'=>$g['permission'],'reason'=>$p['authorization']]);$count++;}if(!$host->getSchemaBuilder()->hasTable('br_statements'))$host->unprepared(file_get_contents(dirname(__DIR__).'/database/sql/bank_reconciliation.sql'));});
|
||||
$binding=['composition'=>$tag,'scope'=>$f['scope'],'statement_key'=>'e232a83b-14d2-43e2-aa74-225df3858b45','ledger_line_ids'=>[4,5],'account_ref'=>'QA-BANCO-LOCAL','period'=>'2026-11','synthetic_only'=>true];$file=$s['root'].'/bank-binding.json';if(is_file($file)&&json_decode(file_get_contents($file),true)!==$binding)throw new LogicException('Preserve different bank binding');if(!is_file($file))file_put_contents($file,json_encode($binding,JSON_PRETTY_PRINT));file_put_contents($work.'/bank-setup.json',json_encode(['passed'=>true,'book'=>$book,'new_grants'=>$count,'new_right_names'=>4,'new_users'=>0,'new_pg_roles'=>0,'new_demo'=>false],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'book'=>$book,'new_grants'=>$count,'proof'=>$work.'/bank-setup.json']).PHP_EOL;
|
||||
@@ -0,0 +1,7 @@
|
||||
import fs from'node:fs';import assert from'node:assert/strict';import{session}from'./sgf-single-http.mjs';
|
||||
const finance=await session('sgf:finance_review','commercial'),read=await session('sgf:read_only','commercial'),checks=[],ok=(v,t)=>{assert(v,t);checks.push(t);};
|
||||
let r=await finance.get('/api/service-documents/register?per_page=100');ok(r.status===200,'authenticated unified register200');const all=await r.json(),rows=all.documents.data;ok(all.documents.per_page===100&&rows.length<=100,'server100-row page');ok(['invoice','invoice_receipt','receipt','credit_note','quote','draft'].every(kind=>rows.some(r=>r.kind===kind)),'all six available document families visible');const fr=rows.find(r=>r.kind==='invoice_receipt');ok(rows.filter(r=>r.invoice_id===fr.invoice_id&&r.kind==='invoice').length===0,'technical invoice not duplicated beside invoice-receipt');ok(fr.balance_available&&Number(fr.origin_invoice_balance)===0,'invoice-receipt origin balance zero');const credit=rows.find(r=>r.kind==='credit_note');ok(credit.balance_available&&Number(credit.origin_invoice_balance)===500&&Number(credit.amount)===100,'credit100 retains distinct source invoice balance500');ok(rows.filter(r=>r.kind==='quote').every(r=>r.amount===null&&r.base!==null),'quotes retain base without fiscal total');ok(rows.filter(r=>r.kind==='draft').every(r=>r.amount===null&&r.document_on===null),'drafts not presented as issued');
|
||||
for(const kind of ['receipt','credit_note','invoice_receipt']){r=await read.get('/api/service-documents/register?kind='+kind);const q=await r.json();ok(r.status===200&&q.documents.total===1&&q.documents.data[0].kind===kind,kind+' own filter and count');}
|
||||
r=await finance.get('/api/service-documents/register?kind=quote&from=2026-01-01');ok(r.status===200&&(await r.json()).documents.total===0,'issue-date filter excludes unissued quotes');r=await finance.get('/api/service-documents/register?search='+encodeURIComponent('%_!'));ok(r.status===200&&(await r.json()).documents.total===0,'literal wildcard characters do not expand search');r=await finance.get('/api/service-documents/register?company_id=1');ok(r.status===422,'foreign context override rejected');r=await finance.get('/api/service-documents/register?per_page=1000');ok(r.status===422,'unbounded page refused');
|
||||
const account=await session('sgf:accounting_prepare','accounting');r=await account.get('/api/general-ledger');const ledger=await r.json();ok(r.status===200&&ledger.journals.length===25&&ledger.totals.debit_minor===ledger.totals.credit_minor,'read integration preserves balanced25-journal book');
|
||||
const run=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')),file=run.root+'/private/receipt-composition-cc50a276c10c/billing-register-http-20261110.json';fs.writeFileSync(file,JSON.stringify({passed:true,checks,business_commands:0,new_rights:0,browser_visual_review:false},null,2));console.log(JSON.stringify({passed:true,checks:checks.length,proof:file}));
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
use App\Modules\Services\BillingRegister;use Illuminate\Support\Str;
|
||||
require __DIR__.'/sgf-service-documents-proof-bootstrap.php';$actor=$select('finance_review');$service=app(BillingRegister::class);$before=[];foreach(['si_invoices','sd_outputs','cq_versions','sr_records','sr_effects','a_billing_drafts'] as $t)$before[$t]=$host->table($t)->orderBy('id')->get()->toJson();$host->beginTransaction();
|
||||
try{
|
||||
$state=$service->listing($actor,['per_page'=>100]);$ok($state['documents']->perPage()===100,'server register accepts100 rows');$fr=$host->table('sd_outputs')->where('kind','invoice_receipt')->firstOrFail();$rows=collect($state['documents']->items());$ok($rows->where('kind','invoice_receipt')->where('invoice_id',$fr->invoice_id)->count()===1&&$rows->where('kind','invoice')->where('invoice_id',$fr->invoice_id)->count()===0,'invoice-receipt appears once without its technical invoice duplicated');
|
||||
$row=$rows->first(fn($r)=>$r->kind==='invoice_receipt');$ok($row->balance_available&&bccomp($row->origin_invoice_balance,'0.00',2)===0,'bounded projection reconciles settled invoice-receipt balance');
|
||||
$series=explode('/',$row->reference,2)[0];$r=$service->listing($actor,['series'=>$series,'kind'=>'invoice_receipt']);$ok($r['documents']->total()===1,'exact series filter retained on server');$r=$service->listing($actor,['kind'=>'invoice_receipt','author'=>$row->actor_id]);$ok($r['documents']->total()===1,'recorded author filter retained on server');$r=$service->listing($actor,['currency'=>'USD']);$ok($r['documents']->total()===0,'currency filter does not invent conversions');
|
||||
foreach($rows->where('kind','draft') as $draft){$data=json_decode($host->table('a_billing_drafts')->where('id',$draft->source_id)->value('data'),true);$ok($draft->base===App\Modules\Services\DraftValues::base($data),'page draft'.$draft->source_id.' preserves canonical base without fiscal total');}
|
||||
$foreign=(array)$fr;unset($foreign['id']);$foreign['tenant_id']=1;$foreign['company_id']=1;$foreign['unit_id']=1;$foreign['document_key']=(string)Str::uuid();$foreign['version_id']=$host->table('sd_versions')->where(['document_key'=>$fr->document_key,'version'=>1])->value('id');$foreign['invoice_id']=$rows->first(fn($r)=>$r->kind==='invoice')->invoice_id;$foreign['number']=999999;$host->table('sd_outputs')->insert($foreign);
|
||||
$afterForeign=$service->listing($actor,['per_page'=>100]);$ok($afterForeign['documents']->total()===$state['documents']->total()&&collect($afterForeign['documents']->items())->where('kind','invoice')->where('invoice_id',$foreign['invoice_id'])->count()===1,'foreign output fixture cannot appear or suppress an authorised invoice');
|
||||
$base=(array)$host->table('cq_versions')->where('action','prepare')->firstOrFail();unset($base['id']);$deps=json_decode($base['dependencies'],true);
|
||||
for($i=0;$i<127;$i++){$data=$base;$data['quote_key']=(string)Str::uuid();$data['previous_id']=null;$data['version']=1;$data['state']='draft';$data['action']='prepare';$data['prepared_by']=$data['actor_id']=$profiles['commercial_prepare']['user_id'];$data['reason']='Fixture de consulta apenas; não decisão comercial.';$deps['client']['name']='QA-REGISTO-%_!-'.$i;$data['dependencies']=json_encode($deps);$host->table('cq_versions')->insert($data);}
|
||||
$keys=[];$last=null;for($page=1;$page<=6;$page++){$host->flushQueryLog();$host->enableQueryLog();$result=$service->listing($actor,['kind'=>'quote','search'=>'QA-REGISTO-%_!-','per_page'=>25,'page'=>$page]);$sql=array_filter($host->getQueryLog(),fn($q)=>str_contains($q['query'],'document_register')||str_contains($q['query'],'register_ob'));$host->disableQueryLog();$ok(count($sql)===2,'quote page'.$page.' uses two bounded register queries');foreach($result['documents']->items() as $r)$keys[]=$r->source_id;$last=$result;}
|
||||
$ok(count($keys)===127&&count(array_unique($keys))===127&&$last['documents']->total()===127,'six pages cover127 literal-search fixtures without loss or duplication');$r=$service->listing($actor,['kind'=>'quote','search'=>'QA-REGISTO-%_!-','per_page'=>100,'page'=>1]);$ok(count($r['documents']->items())===100,'first100-row page contains exact100 fixtures');$r=$service->listing($actor,['kind'=>'quote','search'=>'QA-REGISTO-%_!-','per_page'=>100,'page'=>2]);$ok(count($r['documents']->items())===27,'second100-row page contains exact27 remainder');
|
||||
$r=$service->listing($actor,['kind'=>'quote','search'=>'QA-REGISTO-%_!-','from'=>'2026-01-01']);$ok($r['documents']->total()===0,'emission filter excludes unissued quotes');$service->listing($actor,['to'=>'2026-12-31']);$ok(true,'standalone upper date filter accepted');$reject(fn()=>$service->listing($actor,['from'=>'2026-12-31','to'=>'2026-01-01']),422,'reversed emission range refused');$reject(fn()=>$service->listing($actor,['company_id'=>1]),422,'caller cannot override company context');
|
||||
$host->beginTransaction();try{$host->table('permission_grants')->where(['user_id'=>$actor,'permission'=>'service_receivables.view'])->delete();$r=$service->listing($actor,['per_page'=>100]);$ok(!$r['financial_access']&&collect($r['documents']->items())->every(fn($r)=>$r->origin_invoice_balance===null&&!$r->balance_available),'missing financial right hides all balances rather than exposing zero');$host->table('permission_grants')->where(['user_id'=>$actor,'permission'=>'service_documents.view'])->delete();$r=$service->listing($actor,['per_page'=>100]);$ok(collect($r['documents']->items())->every(fn($r)=>!in_array($r->kind,['receipt','credit_note','invoice_receipt'],true)),'missing additional-document right hides those families and counts');}finally{$host->rollBack();}
|
||||
request()->attributes->set('sgf_module','finance');$reject(fn()=>$service->listing($actor,[]),403,'other module cannot invoke Commercial register');request()->attributes->set('sgf_module','commercial');
|
||||
}finally{while($host->transactionLevel()>0)$host->rollBack();}
|
||||
foreach($before as $t=>$rows)$ok($rows===$host->table($t)->orderBy('id')->get()->toJson(),'read proof preserves original '.$t);
|
||||
file_put_contents($work.'/billing-register-result.json',json_encode(['passed'=>true,'checks'=>$checks,'fixtures_rolled_back'=>127,'new_rights'=>0,'browser_visual_review'=>false],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/billing-register-result.json']).PHP_EOL;
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
$book=$argv[1]??'regression';require __DIR__.'/sgf-single-bootstrap.php';$p=json_decode(file_get_contents($repo.'/docs/integration/sgf-cash-access-proposal.json'),true);if(($p['approved']??false)!==true||$p['scope']!==$f['scope'])throw new LogicException('Explicit cash approval required');$profiles=json_decode(file_get_contents($s['root'].'/single-profiles.json'),true)['profiles'];$count=0;
|
||||
$host->transaction(function()use($host,$p,$profiles,&$count){$old=$host->selectOne("SELECT pg_get_constraintdef(oid) d FROM pg_constraint WHERE conrelid='permission_grants'::regclass AND conname='permission_grants_scope_check'")->d;if(!str_contains($old,'cash.view')){if(!preg_match('/^CHECK \((.*)\)$/s',$old,$m))throw new LogicException('Unknown scope check');$host->statement('ALTER TABLE permission_grants DROP CONSTRAINT permission_grants_scope_check');$host->statement("ALTER TABLE permission_grants ADD CONSTRAINT permission_grants_scope_check CHECK ((".$m[1].") OR (unit_id IS NOT NULL AND permission IN('cash.view','cash.prepare','cash.review','cash.close')))");}foreach($p['new_host_rights'] as $g)foreach($g['profiles'] as $profile){$id=$profiles[$profile]['user_id'];if($id!==$p['profiles'][$profile]||!$host->table('company_memberships')->where(['user_id'=>$id,'tenant_id'=>1,'company_id'=>2])->exists())throw new LogicException('Existing approved QA identity only');$v=$p['scope']+['user_id'=>$id,'permission'=>$g['permission']];if($host->table('permission_grants')->where($v)->exists())continue;$host->table('permission_grants')->insert($v);App\Modules\Identity\Services\Audit::record('access.granted_on_creation',null,1,2,2,$id,['permission'=>$g['permission'],'reason'=>$p['authorization']]);$count++;}});file_put_contents($work.'/cash-access.json',json_encode(['passed'=>true,'book'=>$book,'new_grants'=>$count,'new_profiles'=>0,'public_grants'=>0],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'book'=>$book,'new_grants'=>$count,'proof'=>$work.'/cash-access.json']).PHP_EOL;
|
||||
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
$book='8085';require __DIR__.'/sgf-single-bootstrap.php';foreach(['host'=>['database'=>$s['env']['DB_DATABASE'],'username'=>$s['env']['DB_USERNAME'],'password'=>$s['env']['DB_PASSWORD']],'native'=>$s['nativeConnection']] as $name=>$c){$dump=new Symfony\Component\Process\Process(['C:/Program Files/PostgreSQL/17/bin/pg_dump.exe','-h','127.0.0.1','-p','55439','-U',$c['username'],'-d',$c['database'],'--format=custom','--no-owner','--no-privileges','--file',$work.'/'.$name.'-before-cash.dump'],$repo,['PGPASSWORD'=>$c['password']]);$dump->setTimeout(120);$dump->mustRun();}$before=['receivable_records'=>$host->table('sr_records')->orderBy('id')->get()->toJson(),'receivable_effects'=>$host->table('sr_effects')->orderBy('id')->get()->toJson(),'journals'=>$native->table('gl_journals')->orderBy('id')->get()->toJson(),'clients'=>$host->table('a_clients')->orderBy('id')->get()->toJson(),'invoices'=>$host->table('si_invoices')->orderBy('id')->get()->toJson(),'native_stock'=>$native->table('stock_movements')->orderBy('id')->get()->toJson(),'original_binding_sha256'=>hash_file('sha256',$s['root'].'/binding.json')];file_put_contents($work.'/preservation-private.json',json_encode($before));file_put_contents($s['root'].'/cash-backup-pointer.json',json_encode(['root'=>$work]));echo json_encode(['passed'=>true,'private_backup'=>$work,'database_writes'=>0]).PHP_EOL;
|
||||
@@ -0,0 +1,2 @@
|
||||
import fs from'node:fs';import{spawn,spawnSync}from'node:child_process';import assert from'node:assert/strict';const php='C:/laragon/tools/sgf-tempo/php-8.4.27/php.exe',script='scripts/sgf-cash-concurrency.php',run=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')),dir=run.root+'/private/receipt-composition-b4093c6f6707/cash-concurrency';
|
||||
const call=phase=>{const r=spawnSync(php,[script,phase],{encoding:'utf8',windowsHide:true});assert.equal(r.status,0,r.stdout+r.stderr);assert.match(r.stdout,/"passed":true/);return r.stdout;};if(fs.existsSync(dir+'/result.json')){assert(JSON.parse(fs.readFileSync(dir+'/result.json')).passed);console.log(JSON.stringify({passed:true,reused:true,proof:dir+'/result.json'}));process.exit();}call('prepare');const first=spawn(php,[script,'first'],{stdio:'pipe',windowsHide:true});let out='';first.stdout.on('data',d=>out+=d);first.stderr.on('data',d=>out+=d);const ended=new Promise(r=>first.once('exit',r));try{const end=Date.now()+10000;while(!fs.existsSync(dir+'/locked')&&Date.now()<end)await new Promise(r=>setTimeout(r,50));assert(fs.existsSync(dir+'/locked'));const second=spawn(php,[script,'second'],{stdio:'pipe',windowsHide:true});let text='';second.stdout.on('data',d=>text+=d);second.stderr.on('data',d=>text+=d);assert.equal(await new Promise(r=>second.once('exit',r)),0,text);const blocked=JSON.parse(fs.readFileSync(dir+'/second.json'));assert.equal(blocked.status,503);assert(blocked.elapsed_seconds>=4.5&&blocked.elapsed_seconds<12);}finally{fs.writeFileSync(dir+'/release','Release only the QA transaction');assert.equal(await ended,0,out);}console.log(call('recover').trim());
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
use App\Modules\Finance\CashWorkspace;
|
||||
require __DIR__.'/sgf-cash-proof-bootstrap.php';$dir=$s['root'].'/cash-concurrency';if(!is_dir($dir))mkdir($dir,0700,true);$file=$dir.'/progress.json';$mode=$argv[1]??'';
|
||||
if($mode==='prepare'){
|
||||
if(is_file($file))throw new LogicException('Reuse existing preparation');[$open]=$cashCommand('finance_prepare','prepare','open',['date'=>'2026-11-10','label'=>'Caixa QA concorrência','opening'=>'0.00','evidence'=>'Abertura zero para prova concorrente de Caixa.']);$cashCommand('finance_review','review','open',['decision'=>'approved'],$open['id']);[$receipt]=$cashCommand('finance_prepare','prepare','receipt',['date'=>'2026-11-10','receipt_id'=>$cashBinding['receipt_id'],'medium'=>'cash','evidence'=>'Classificação QA de numerário para prova concorrente.']);$cuts=[];foreach([1,2] as $i)$cuts[$i]=$cashPreview('finance_review','review','receipt',['decision'=>'approved'],$receipt['id'])['preview'];file_put_contents($file,json_encode(['cuts'=>$cuts,'proposal_id'=>$receipt['id'],'events_before'=>$host->table('cash_events')->count()]));
|
||||
}elseif($mode==='first'){
|
||||
$p=json_decode(file_get_contents($file),true);$actor=$selectCash('finance_review');$host->beginTransaction();try{$result=app(CashWorkspace::class)->confirm($actor,$p['cuts'][1]);file_put_contents($dir.'/locked','host company and current cash authority locked');$end=microtime(true)+20;while(!is_file($dir.'/release')&µtime(true)<$end)usleep(50000);if(!is_file($dir.'/release'))throw new LogicException('Release missing');$host->commit();file_put_contents($dir.'/first.json',json_encode($result));}finally{while($host->transactionLevel()>0)$host->rollBack();}
|
||||
}elseif($mode==='second'){
|
||||
$p=json_decode(file_get_contents($file),true);$selectCash('finance_review');request()->request->replace(['preview'=>$p['cuts'][2]]);$start=microtime(true);$r=app(App\Modules\Finance\Http\CashController::class)->confirm(request(),app(CashWorkspace::class));file_put_contents($dir.'/second.json',json_encode(['status'=>$r->getStatusCode(),'elapsed_seconds'=>microtime(true)-$start]));
|
||||
}elseif($mode==='recover'){
|
||||
$p=json_decode(file_get_contents($file),true);$actor=$selectCash('finance_review');$w=app(CashWorkspace::class);$reject(fn()=>$w->confirm($actor,$p['cuts'][2]),409,'second cash confirmation refuses stale version after release');$first=$w->confirm($actor,$p['cuts'][1]);$ok($first['replayed']&&$host->table('cash_events')->count()===$p['events_before']+1,'one cash movement and original result recovered');file_put_contents($dir.'/result.json',json_encode(['passed'=>true,'second_during_lock'=>json_decode(file_get_contents($dir.'/second.json'),true),'after_release_status'=>409,'one_cash_movement'=>true,'first_key_replayed'=>true,'regression_only'=>true],JSON_PRETTY_PRINT));
|
||||
}else throw new LogicException('Unknown phase');echo json_encode(['passed'=>true,'phase'=>$mode,'proof'=>$dir.'/result.json']).PHP_EOL;
|
||||
@@ -0,0 +1,6 @@
|
||||
import fs from'node:fs';import assert from'node:assert/strict';import{parsePage,installDom}from'./purchasing-workspace-dom.mjs';import{session}from'./sgf-single-http.mjs';
|
||||
const nativeFetch=globalThis.fetch,s=await session('sgf:finance_review','finance');let r=await s.get('/finance/cash',{headers:{Accept:'text/html'}});assert.equal(r.status,200);const document=parsePage(await r.text());installDom(document);document.getElementById=id=>document.querySelector('#'+id);globalThis.window={addEventListener(){}};const $=id=>document.getElementById(id);let posts=0;const calls=[];
|
||||
globalThis.fetch=async(url,options={})=>{if(url.startsWith('http:'))return nativeFetch(url,options);calls.push(url);if(options.method==='POST'){posts++;throw Error('No Cash POST permitted in this read-only UI proof');}const q=await s.get(url);return{status:q.status,json:()=>q.json()};};
|
||||
await import('../public/sgf-cash.mjs?dom-http');const wait=async f=>{const end=Date.now()+12000;while(!f()&&Date.now()<end)await new Promise(r=>setTimeout(r,25));assert(f(),$('message').textContent);};await wait(()=>$('events').children.length===9);assert.match($('summary').textContent,/Fechada/);assert.match($('summary').textContent,/950.00 MZN/);assert($('fields').disabled);assert.equal($('action').children.length,1);assert.equal($('filters').disabled,false);assert.match($('period').textContent,/1000.00 MZN/);assert.match($('period').textContent,/50.00 MZN/);
|
||||
$('filterKind').value='expense';$('apply').onclick();await wait(()=>$('events').children.length===2);assert.match($('period').textContent,/fecho 950.00 MZN/);$('from').value='2026-11-11';$('apply').onclick();await wait(()=>$('events').children.length===0);assert.match($('period').textContent,/abertura 950.00 MZN/);
|
||||
const run=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')),root=run.root+'/private/receipt-composition-cc50a276c10c',progress=JSON.parse(fs.readFileSync(root+'/cash-http-20261110.json'));$('referenceKey').value=progress.operations['fechar-caixa'].key;await $('recovery').onsubmit({preventDefault(){}});assert.match($('receipt').textContent,/versão 9, Fechado/);assert.match($('receipt').textContent,/Nenhuma operação repetida/);assert(calls.some(url=>url.includes('/api/cash/receipts/')));assert.equal(posts,0);const file=root+'/cash-dom-http-20261110.json';fs.writeFileSync(file,JSON.stringify({passed:true,checks:14,real_confirmation_posts:0,transport_mocked:false,read_transport_forwarded:true,closed_cash_preserved:true,browser_visual_review:false},null,2));console.log(JSON.stringify({passed:true,checks:14,proof:file}));
|
||||
@@ -0,0 +1,12 @@
|
||||
import fs from'node:fs';import assert from'node:assert/strict';import{session}from'./sgf-single-http.mjs';
|
||||
const run=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')),root=run.root+'/private/receipt-composition-cc50a276c10c',file=root+'/cash-http-20261110.json',p=fs.existsSync(file)?JSON.parse(fs.readFileSync(file)):{operations:{},checks:[]},save=()=>fs.writeFileSync(file,JSON.stringify(p,null,2));const c={prepare:await session('sgf:finance_prepare','finance'),review:await session('sgf:finance_review','finance'),read:await session('sgf:read_only','finance'),ap:await session('sgf:accounting_prepare','accounting'),ar:await session('sgf:accounting_review','accounting')};
|
||||
const ok=(v,t)=>{assert(v,t);if(!p.checks.includes(t))p.checks.push(t);save();};const state=async(role='read',query='')=>{const r=await c[role].get('/api/cash'+query);assert.equal(r.status,200,await r.clone().text());return r.json();};
|
||||
async function step(name,role,action,kind,data={},proposal=null){let o=p.operations[name];if(!o){o=p.operations[name]={role,action,kind,data,proposal,key:crypto.randomUUID()};save();}assert.deepEqual({role,action,kind,data,proposal},{role:o.role,action:o.action,kind:o.kind,data:o.data,proposal:o.proposal});if(o.result)return o.result;let r=await c[role].get('/api/cash/receipts/'+o.key);if(r.status===200){o.result=(await r.json()).result;save();return o.result;}assert.equal(r.status,404,'historical command lookup');const s=await state(role);const cut=await c[role].command('/api/cash/preview',{action,kind,data,proposal_id:proposal,version:s.basis.revision,reason:'Conferência individual QA8085: '+name+'.'},o.key);assert.equal(cut.status,200,name+': '+JSON.stringify(cut.value));const done=await c[role].command('/api/cash/confirm',{preview:cut.value.preview});assert.equal(done.status,200,name+': '+JSON.stringify(done.value));o.result=done.value;save();const again=await c[role].command('/api/cash/confirm',{preview:cut.value.preview});ok(again.status===200&&again.value.replayed&&again.value.id===done.value.id,name+' one result on replay');return o.result;}
|
||||
async function journal(name,role,path,body){let o=p.operations[name];if(!o){o=p.operations[name]={role,path,body,key:crypto.randomUUID()};save();}assert.deepEqual({role,path,body},{role:o.role,path:o.path,body:o.body});if(o.result)return o.result;const r=await c[role].command(path,body,o.key);assert.equal(r.status,200,name+': '+JSON.stringify(r.value));o.result=r.value;save();return o.result;}
|
||||
let r=await c.prepare.get('/finance/cash',{headers:{Accept:'text/html'}});ok(r.status===200&&(await r.text()).includes('sgf-cash.mjs'),'authenticated cash screen available');const initial=await state();ok(initial.capabilities.prepare===false&&initial.capabilities.review===false,'read-only cash reader has no write powers');const open=await step('abrir-zero','prepare','prepare','open',{date:'2026-11-10',label:'Caixa QA integrada8085',opening:'0.00',evidence:'Saldo inicial explicitamente zero, sem dinheiro real.'});await step('conferir-abertura','review','review','open',{decision:'approved'},open.id);
|
||||
const receipt=await step('classificar-1000','prepare','prepare','receipt',{date:'2026-11-10',receipt_id:initial.binding.receipt_id,medium:'cash',evidence:'Classificação explícita de custódia fictícia1000, QA-CASH-20261110.'});const received=await step('conferir-1000','review','review','receipt',{decision:'approved'},receipt.id);ok(received.impact.after_minor===100000&&!received.impact.new_invoice_or_receipt,'cash custody1000 reuses receipt without another debt or invoice');
|
||||
const expense=await step('preparar-despesa50','prepare','prepare','expense',{date:'2026-11-10',amount:'50.00',beneficiary:'Beneficiário fictício QA',description:'Expediente sintético50MZN, sem compra ou pagamento real.',evidence:'QA-DESPESA-CAIXA-50-20261110, comprovativo de ensaio.'});const paid=await step('conferir-despesa50','review','review','expense',{decision:'approved'},expense.id);ok(paid.impact.after_minor===95000,'distinct expense review leaves950');
|
||||
const count=await step('contar950','prepare','prepare','count',{date:'2026-11-10',counted:'950.00',evidence:'Contagem fictícia950MZN explicitamente conferida.'});const counted=await step('conferir-contagem','review','review','count',{decision:'approved'},count.id);const s=await state('review');const notReady=await c.review.command('/api/cash/preview',{action:'close',kind:'count',proposal_id:counted.id,version:s.basis.revision,data:{},reason:'Não fechar enquanto faltam as duas valorizações de Caixa.'});if(!p.operations['fechar-caixa']?.result)ok(notReady.status===409,'unposted cash facts refuse premature closing');
|
||||
for(const [kind,id]of [['cash.receipt_classified',received.id],['cash.expense_confirmed',paid.id]]){const q=await journal(kind+'-preparar','ap','/api/general-ledger/effect-prepare',{effect_type:kind,source_id:id,journal_on:'2026-11-10',period_key:'QA-2026-INTEGRADO',rule_key:kind,reason:'Origem de Caixa QA individualmente seleccionada: '+kind+'.'});await journal(kind+'-rever','ar','/api/general-ledger/validate',{proposal_id:q.proposal_id,revision:q.revision,decision:'approved',reason:'Conferência independente da origem e tratamento de Caixa.'});await journal(kind+'-aplicar','ap','/api/general-ledger/post',{proposal_id:q.proposal_id,revision:q.revision,reason:'Aplicação única da reclassificação ou despesa de Caixa QA.'});}
|
||||
await step('fechar-caixa','review','close','count',{},counted.id);const final=await state();ok(final.basis.closed&&final.basis.balance_minor===95000&&final.events.total===9,'selected cash closes950 with nine audit events');ok(final.period.opening_minor===0&&final.period.receipts_minor===100000&&final.period.expenses_minor===5000&&final.period.closing_minor===95000,'cash statement reconciles0+1000-50=950');const later=await state('read','?from=2026-11-11&per_page=100');ok(later.events.total===0&&later.period.opening_minor===95000&&later.period.closing_minor===95000,'next interval carries950 without extra movements');const denied=await c.read.command('/api/cash/preview',{action:'prepare',kind:'expense',version:9,data:{},reason:'Consulta não concede competência para despesa.'});ok(denied.status===403,'read-only cash writer403');
|
||||
const recovered=await session('sgf:finance_review','finance');r=await recovered.get('/api/cash/receipts/'+p.operations['fechar-caixa'].key);const historical=await r.json();ok(r.status===200&&historical.historical_receipt_only&&!historical.operation_executed&&historical.result.state==='closed','new session recovers old closing receipt without executing it');r=await c.ap.get('/api/general-ledger');const gl=await r.json();ok(r.status===200&&gl.pagination.total===27&&gl.totals.debit_minor===gl.totals.credit_minor,'same ledger27 balanced journals');r=await c.ap.get('/api/general-ledger?page=2');const second=await r.json();ok(r.status===200&&gl.journals.length===25&&second.journals.length===2&&new Set([...gl.journals,...second.journals].map(x=>x.id)).size===27,'two journal pages expose27 unique records without treating page size as total');p.passed=true;p.browser_visual_review=false;p.money_real=false;save();console.log(JSON.stringify({passed:true,checks:p.checks.length,proof:file}));
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
use Illuminate\Support\Str;
|
||||
$book='8085';require __DIR__.'/sgf-ledger-proof-bootstrap.php';config(['sales_ledger.synthetic_enabled'=>true,'cost_ledger.synthetic_enabled'=>true,'industrial_ledger.synthetic_enabled'=>true,'cost_ledger.correction_enabled'=>true,'industrial_ledger.correction_enabled'=>true]);
|
||||
config(['cash_qa.enabled'=>true,'cash_qa.binding_sha256'=>hash_file('sha256',$s['root'].'/cash-binding.json')]);
|
||||
$scope=['tenant_id'=>1,'company_id'=>2,'unit_id'=>2];$journals=$native->table('gl_journals')->where($scope)->orderBy('id')->get()->toJson();$host->beginTransaction();$native->beginTransaction();
|
||||
try{
|
||||
$p=$call('accounting_prepare','period_prepare',fn($e)=>$e->command('period-prepare',['period_key'=>'QA-2026-INTEGRADO','reason'=>'Conferência integral das origens efectivas na mesma8085; ensaio revertido para manter o período de apresentação aberto.'],(string)Str::uuid()));
|
||||
$snapshot=json_decode($native->table('gl_period_checks')->where('id',$p['check_id'])->value('snapshot'),true);$ok($snapshot['pending']===0&&$snapshot['debit_minor']===$snapshot['credit_minor'],'all supplier, sales, costs, corrections and physical origins reconcile with zero pending facts');
|
||||
$review=['check_id'=>$p['check_id'],'revision'=>$p['revision'],'decision'=>'approved','reason'=>'Revisão independente do conjunto integral de origens e montantes QA.'];$call('accounting_review','period_validate',fn($e)=>$e->command('period-validate',$review,(string)Str::uuid()));
|
||||
$native->beginTransaction();try{$rule=(array)$native->table('gl_rules')->where($scope)->where('rule_key','sales.invoice')->first();unset($rule['id']);$rule['version']=2;$rule['reason']='Versão adicional apenas no ensaio de recusa de conferência antiga.';$native->table('gl_rules')->insert($rule);$bad=false;try{$call('accounting_prepare','period_close',fn($e)=>$e->command('period-close',['check_id'=>$p['check_id'],'revision'=>$p['revision'],'reason'=>'Não fechar com regras alteradas.'],(string)Str::uuid()));}catch(DomainException $e){$bad=str_contains($e->getMessage(),'mudaram');}$ok($bad,'rule version changed after review refuses closure and requires fresh conference');}finally{$native->rollBack();}
|
||||
$key=(string)Str::uuid();$body=['check_id'=>$p['check_id'],'revision'=>$p['revision'],'reason'=>'Fecho QA integral apenas dentro da transacção de prova, sem reabrir depois.'];$closed=$call('accounting_prepare','period_close',fn($e)=>$e->command('period-close',$body,$key));require __DIR__.'/sgf-document-closed-period-check.php';$again=$call('accounting_prepare','period_close',fn($e)=>$e->command('period-close',$body,$key));
|
||||
$ok($closed['state']==='closed'&&$closed['closure_id']===$again['closure_id'],'distinctly reviewed close and replay retain one closure');
|
||||
$ok($native->table('gl_periods')->where($scope)->where('period_key','QA-2026-INTEGRADO')->orderByDesc('version')->value('status')==='closed','closure appends actual closed period version');
|
||||
$physical=json_decode(file_get_contents($s['root'].'/single-stock-physical-input.json'),true);$receiptId=(int)$native->table('stock_movements')->where('stock_item_id',$physical['stock_item'])->where('movement_type','receipt_in')->value('id');$bad=false;
|
||||
try{$call('accounting_prepare','prepare',fn($e)=>$e->command('effect-prepare',['effect_type'=>'stock.receipt','source_id'=>$receiptId,'journal_on'=>'2026-11-10','period_key'=>'QA-2026-INTEGRADO','rule_key'=>'stock.receipt','reason'=>'Não criar captura num período fechado.'],(string)Str::uuid()));}catch(DomainException $e){$bad=str_contains($e->getMessage(),'fechado');}$ok($bad,'closed QA period refuses further capture');
|
||||
$ok($journals===$native->table('gl_journals')->where($scope)->orderBy('id')->get()->toJson(),'closure and refusals preserve every journal present before this proof');$native->statement('SET CONSTRAINTS ALL IMMEDIATE');
|
||||
}finally{while($native->transactionLevel()>0)$native->rollBack();while($host->transactionLevel()>0)$host->rollBack();}
|
||||
$ok($native->table('gl_periods')->where($scope)->where('period_key','QA-2026-INTEGRADO')->orderByDesc('version')->value('status')==='open','proof rolls back instead of reopening; actual8085 presentation period remains open');
|
||||
file_put_contents($work.'/period-cash-result.json',json_encode(['passed'=>true,'checks'=>$checks,'facts'=>$snapshot['facts'],'pending'=>0,'journals'=>count(json_decode($journals,true)),'close_executed_in_rolled_back_transaction'=>true,'period_reopened'=>false,'presentation_period_open'=>true,'browser_visual_review'=>false],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'facts'=>$snapshot['facts'],'proof'=>$work.'/period-cash-result.json']).PHP_EOL;
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
$book='8085';require __DIR__.'/sgf-single-bootstrap.php';$pointer=json_decode(file_get_contents($s['root'].'/cash-backup-pointer.json'),true);$before=json_decode(file_get_contents($pointer['root'].'/preservation-private.json'),true);$checks=[];
|
||||
foreach(['journals'=>[$native,'gl_journals'],'native_stock'=>[$native,'stock_movements'],'clients'=>[$host,'a_clients'],'invoices'=>[$host,'si_invoices'],'receivable_records'=>[$host,'sr_records'],'receivable_effects'=>[$host,'sr_effects']] as $key=>[$db,$table]){$rows=json_decode($before[$key],true);$ids=array_column($rows,'id');$same=$db->table($table)->whereIn('id',$ids)->orderBy('id')->get()->toJson()===$before[$key];if(!$same)throw new LogicException('Original rows changed: '.$key);$checks[$key]=['original_rows'=>count($rows),'identical'=>true];}
|
||||
if(hash_file('sha256',$s['root'].'/binding.json')!==$before['original_binding_sha256'])throw new LogicException('Original binding changed');
|
||||
$checks['original_binding_unchanged']=true;$checks['six_protected_records']='No connection or command targets their databases; not reread or reassigned.';$checks['blocked_cache']='No clear/delete commands; original cache paths not changed.';
|
||||
file_put_contents($work.'/cash-preservation.json',json_encode(['passed'=>true,'checks'=>$checks,'journals_now'=>$native->table('gl_journals')->count(),'public_grants'=>0,'new_demo'=>false],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'proof'=>$work.'/cash-preservation.json']).PHP_EOL;
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
use Illuminate\Support\Facades\Auth;use Illuminate\Support\Str;use App\Shared\Context\CompanyContext;use App\Modules\Finance\CashWorkspace;
|
||||
require __DIR__.'/sgf-ledger-proof-bootstrap.php';config(['cash_qa.enabled'=>true,'cash_qa.binding_sha256'=>hash_file('sha256',$s['root'].'/cash-binding.json'),'sales_ledger.synthetic_enabled'=>true]);$cashBinding=json_decode(file_get_contents($s['root'].'/cash-binding.json'),true);
|
||||
set_exception_handler(function($e){echo json_encode(['passed'=>false,'exception'=>get_class($e),'message'=>$e->getMessage(),'errors'=>$e instanceof Illuminate\Validation\ValidationException?$e->errors():null,'frames'=>array_map(fn($x)=>[basename($x['file']??''),$x['line']??0,$x['function']??''],array_slice($e->getTrace(),0,10))]).PHP_EOL;exit(1);});
|
||||
$selectCash=function($role)use($profiles,$app){Auth::forgetGuards();Auth::shouldUse('web');$u=App\Models\User::findOrFail($profiles[$role]['user_id']);Auth::guard('web')->setUser($u);$r=Illuminate\Http\Request::create('/api/cash','POST');$r->attributes->set('sgf_module','finance');$r->setUserResolver(fn()=>$u);$session=app('session')->driver();$session->start();$session->put('company_context',['tenant_id'=>1,'company_id'=>2,'unit_id'=>2,'module_id'=>'finance','token'=>'cash-'.$role]);$r->setLaravelSession($session);$app->instance('request',$r);app(CompanyContext::class)->selectFor($u->id,1,2,2);return $u->id;};
|
||||
$cashPreview=function($role,$action,$kind,$data=[],$proposal=null)use($selectCash){$actor=$selectCash($role);$w=app(CashWorkspace::class);$state=$w->listing($actor,[]);return $w->preview($actor,['action'=>$action,'kind'=>$kind,'data'=>$data,'proposal_id'=>$proposal,'version'=>$state['basis']['revision'],'reason'=>'Conferência QA individual de Caixa, origem e saldo.'],(string)Str::uuid());};
|
||||
$cashCommand=function($role,$action,$kind,$data=[],$proposal=null)use($cashPreview,$selectCash){$cut=$cashPreview($role,$action,$kind,$data,$proposal);return [app(CashWorkspace::class)->confirm($selectCash($role),$cut['preview']),$cut];};
|
||||
$reject=function(Closure $fn,int $status,string $label)use($ok){$bad=false;try{$fn();}catch(Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e){$bad=$e->getStatusCode()===$status;}catch(Illuminate\Validation\ValidationException){$bad=$status===422;}$ok($bad,$label);};
|
||||
$postCashOrigin=function($kind,$id)use($call){$p=$call('accounting_prepare','prepare',fn($e)=>$e->command('effect-prepare',['effect_type'=>$kind,'source_id'=>$id,'journal_on'=>'2026-11-10','period_key'=>'QA-2026-INTEGRADO','rule_key'=>$kind,'reason'=>'Origem QA explícita de Caixa ou liquidez original.'],(string)Str::uuid()));$call('accounting_review','validate',fn($e)=>$e->command('validate',['proposal_id'=>$p['proposal_id'],'revision'=>$p['revision'],'decision'=>'approved','reason'=>'Revisão contabilística distinta da origem de Caixa.'],(string)Str::uuid()));return $call('accounting_prepare','post',fn($e)=>$e->command('post',['proposal_id'=>$p['proposal_id'],'revision'=>$p['revision'],'reason'=>'Aplicação única da origem de Caixa conferida.'],(string)Str::uuid()));};
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
use App\Modules\Finance\CashWorkspace;use Illuminate\Support\Str;
|
||||
require __DIR__.'/sgf-cash-proof-bootstrap.php';$host->beginTransaction();$native->beginTransaction();$originalInvoices=$host->table('si_invoices')->orderBy('id')->get()->toJson();$originalFinancial=$host->table('sr_effects')->orderBy('id')->get()->toJson();
|
||||
try{
|
||||
$actor=$selectCash('read_only');$state=app(CashWorkspace::class)->listing($actor,[]);$ok($state['basis']['revision']===0&&$state['basis']['balance_minor']===0,'new bound QA cash book starts explicitly empty');
|
||||
request()->attributes->set('sgf_module','commercial');$reject(fn()=>app(CashWorkspace::class)->listing($actor,[]),403,'Commercial cannot acquire Finance cash context');request()->attributes->set('sgf_module','finance');$reject(fn()=>app(CashWorkspace::class)->listing($actor,['company_id'=>1]),422,'cash context cannot be overridden by a filter');
|
||||
$reject(fn()=>$cashPreview('read_only','prepare','open',['date'=>'2026-11-10','label'=>'Caixa QA','opening'=>'0.00','evidence'=>'Abertura sintética explícita, sem dinheiro real.']),403,'read-only cannot prepare cash');
|
||||
[$open]=$cashCommand('finance_prepare','prepare','open',['date'=>'2026-11-10','label'=>'Caixa QA MZN','opening'=>'0.00','evidence'=>'Saldo de abertura QA explicitamente zero.']);$reject(fn()=>$cashCommand('finance_prepare','review','open',['decision'=>'approved'],$open['id']),403,'cash opener cannot self-review');$cashCommand('finance_review','review','open',['decision'=>'approved'],$open['id']);
|
||||
$reject(fn()=>$cashPreview('finance_prepare','prepare','expense',['date'=>'2026-11-10','amount'=>'50.00','beneficiary'=>'Beneficiário QA','description'=>'Despesa fictícia sem saldo disponível.','evidence'=>'Comprovativo exclusivamente sintético.']),422,'expense requires available cash before approval');
|
||||
$reject(fn()=>$cashPreview('finance_prepare','prepare','receipt',['date'=>'2026-11-10','receipt_id'=>1,'medium'=>'cash','evidence'=>'Origem não autorizada para Caixa QA.']),403,'unselected receipt refused without assigning cash automatically');
|
||||
[$receipt]=$cashCommand('finance_prepare','prepare','receipt',['date'=>'2026-11-10','receipt_id'=>$cashBinding['receipt_id'],'medium'=>'cash','evidence'=>'Declaração explícita de custódia fictícia em numerário1000.']);$stale=$cashPreview('finance_review','review','receipt',['decision'=>'approved'],$receipt['id']);[$accepted]=$cashCommand('finance_review','review','receipt',['decision'=>'approved'],$receipt['id']);$actor=$selectCash('finance_review');$reject(fn()=>app(CashWorkspace::class)->confirm($actor,$stale['preview']),409,'competing receipt conference refuses old cash version');$ok($accepted['impact']['after_minor']===100000,'distinct cash classification confirms1000 without another receipt');
|
||||
$host->beginTransaction();try{$actor=$selectCash('finance_prepare');$reject(fn()=>app(CashWorkspace::class)->confirm($actor,$stale['preview']),403,'different actor lacks the review authority');$actor=$selectCash('finance_review');request()->session()->put('company_context.token','changed-generation');$reject(fn()=>app(CashWorkspace::class)->confirm($actor,$stale['preview']),409,'changed generation cannot reuse a conference');}finally{$host->rollBack();}
|
||||
$reject(fn()=>$cashPreview('finance_prepare','prepare','receipt',['date'=>'2026-11-10','receipt_id'=>$cashBinding['receipt_id'],'medium'=>'cash','evidence'=>'Não duplicar o mesmo recebimento QA.']),409,'same confirmed receipt cannot be classified twice');
|
||||
[$expense]=$cashCommand('finance_prepare','prepare','expense',['date'=>'2026-11-10','amount'=>'50.00','beneficiary'=>'Beneficiário sintético QA','description'=>'Despesa fictícia de expediente50MZN.','evidence'=>'QA-DESPESA-50, não é comprovativo fiscal real.']);$pending=$cashPreview('finance_review','review','expense',['decision'=>'approved'],$expense['id']);
|
||||
$host->unprepared("CREATE FUNCTION cash_test_fail() RETURNS trigger LANGUAGE plpgsql AS 'BEGIN RAISE EXCEPTION ''CASH_TEST_FAILURE''; END'");$host->unprepared('REVOKE ALL ON FUNCTION cash_test_fail() FROM PUBLIC');$host->unprepared('CREATE TRIGGER cash_test_fail BEFORE INSERT ON cash_commands FOR EACH ROW EXECUTE FUNCTION cash_test_fail()');$actor=$selectCash('finance_review');$n=$host->table('cash_events')->count();$failed=false;try{app(CashWorkspace::class)->confirm($actor,$pending['preview']);}catch(Illuminate\Database\QueryException){$failed=true;}$ok($failed&&$host->table('cash_events')->count()===$n,'failure after cash movement rolls back event and confirmation together');$host->unprepared('DROP TRIGGER cash_test_fail ON cash_commands');$host->unprepared('DROP FUNCTION cash_test_fail()');
|
||||
$approved=app(CashWorkspace::class)->confirm($actor,$pending['preview']);$again=app(CashWorkspace::class)->confirm($actor,$pending['preview']);$ok($again['replayed']&&$again['id']===$approved['id']&&$approved['impact']['after_minor']===95000,'expense recovery confirms exactly50 leaving950 once');
|
||||
[$badCount]=$cashCommand('finance_prepare','prepare','count',['date'=>'2026-11-10','counted'=>'940.00','evidence'=>'Contagem divergente QA, sem ajuste automático.']);$reject(fn()=>$cashCommand('finance_review','review','count',['decision'=>'approved'],$badCount['id']),409,'count discrepancy cannot be approved as reconciled');$cashCommand('finance_review','review','count',['decision'=>'refused'],$badCount['id']);
|
||||
[$count]=$cashCommand('finance_prepare','prepare','count',['date'=>'2026-11-10','counted'=>'950.00','evidence'=>'Contagem QA950 conferida, sem movimentação de dinheiro.']);[$countReview]=$cashCommand('finance_review','review','count',['decision'=>'approved'],$count['id']);$reject(fn()=>$cashPreview('finance_review','close','count',[],$countReview['id']),409,'unposted cash origins block closing');
|
||||
$original=$host->table('sd_outputs')->where('id',$cashBinding['source_output_id'])->first();$postCashOrigin('sales.invoice',$cashBinding['invoice_id']);$postCashOrigin('receivable.allocated',$original->effect_id);$cashJournal=$postCashOrigin('cash.receipt_classified',$accepted['id']);$expenseJournal=$postCashOrigin('cash.expense_confirmed',$approved['id']);$ok((int)$native->table('gl_journals')->where('id',$cashJournal['journal_id'])->value('debit_minor')===100000&&(int)$native->table('gl_journals')->where('id',$expenseJournal['journal_id'])->value('debit_minor')===5000,'same writer reclassifies1000 and recognises50 expense without duplicate income');
|
||||
$close=$cashPreview('finance_review','close','count',[],$countReview['id']);$actor=$selectCash('finance_review');$host->beginTransaction();try{$host->table('permission_grants')->where(['user_id'=>$actor,'permission'=>'cash.close'])->delete();$reject(fn()=>app(CashWorkspace::class)->confirm($actor,$close['preview']),403,'revoked close right refuses an existing conference');}finally{$host->rollBack();}
|
||||
$host->beginTransaction();try{$actor=$selectCash('finance_prepare');$origin=$host->table('sr_records')->where('id',$cashBinding['receipt_id'])->first();$revision=(int)$host->table('sr_effects')->where('obligation_id',$origin->obligation_id)->count();$sr=app(App\Modules\Services\ServiceReceivables::class);$request=$sr->propose($actor,['kind'=>'correction','invoice_id'=>$origin->invoice_id,'obligation_id'=>$origin->obligation_id,'currency'=>'MZN','source'=>'SIM-CASH-REVERSAL','external_id'=>'QA-REV-CASH-'.Str::uuid(),'revision'=>$revision,'action'=>'reverse_allocation','target_effect_id'=>$original->effect_id,'reason'=>'Reversão da origem apenas em prova de bloqueio da Caixa.'],(string)Str::uuid());$actor=$selectCash('finance_review');$sr->decide($request['id'],$actor,['version'=>1,'revision'=>$revision,'decision'=>'approved','reason'=>'Conferência distinta da reversão da origem da Caixa.'],(string)Str::uuid());$state=app(CashWorkspace::class)->listing($actor,[]);$ok($state['basis']===null&&$state['blocked']!==null,'reversed source is explicit blockage rather than zero or automatic cash refund');$reject(fn()=>app(CashWorkspace::class)->confirm($actor,$close['preview']),409,'source reversal refuses an already conferred cash closing');}finally{$host->rollBack();}
|
||||
$actor=$selectCash('finance_review');$closed=app(CashWorkspace::class)->confirm($actor,$close['preview']);$ok($closed['state']==='closed'&&$closed['impact']['after_minor']===95000,'distinct reviewed count closes950 after both journal entries');$reject(fn()=>$cashPreview('finance_prepare','prepare','open',['date'=>'2026-11-10','label'=>'Reabrir','opening'=>'0.00','evidence'=>'Reabertura não autorizada e recusada.']),409,'closed cash session cannot reopen');
|
||||
$native->statement('SET CONSTRAINTS ALL IMMEDIATE');$host->statement('SET CONSTRAINTS ALL IMMEDIATE');$ok($originalInvoices===$host->table('si_invoices')->orderBy('id')->get()->toJson()&&$originalFinancial===$host->table('sr_effects')->orderBy('id')->get()->toJson(),'cash does not change original invoices, receipts, allocations or debt');
|
||||
}finally{while($native->transactionLevel()>0)$native->rollBack();while($host->transactionLevel()>0)$host->rollBack();}
|
||||
file_put_contents($work.'/cash-result.json',json_encode(['passed'=>true,'checks'=>$checks,'rolled_back'=>true,'real_money'=>false,'browser_visual_review'=>false],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/cash-result.json']).PHP_EOL;
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
use App\Modules\Finance\CashWorkspace;
|
||||
require __DIR__.'/sgf-cash-proof-bootstrap.php';$host->beginTransaction();try{
|
||||
$actor=$selectCash('read_only');$r=app(CashWorkspace::class)->listing($actor,['per_page'=>25,'from'=>'2026-11-10','to'=>'2026-11-10']);$ok($r['period']===['opening_minor'=>0,'receipts_minor'=>100000,'expenses_minor'=>0,'closing_minor'=>100000],'period summary reflects confirmed receipt only after concurrency proof');$ok($r['events']->perPage()===25&&$r['events']->total()===4,'SQL page25 retains four audit events');$r=app(CashWorkspace::class)->listing($actor,['per_page'=>100,'kind'=>'receipt']);$ok($r['events']->total()===2,'kind filter preserves preparation and distinct confirmation');$r=app(CashWorkspace::class)->listing($actor,['from'=>'2026-11-11']);$ok($r['period']['opening_minor']===100000&&$r['period']['closing_minor']===100000&&$r['events']->total()===0,'later interval carries opening without inventing movements');
|
||||
$reject(fn()=>$cashPreview('finance_prepare','prepare','expense',['date'=>'2026-11-09','amount'=>'50.00','beneficiary'=>'Beneficiário QA','description'=>'Despesa não pode anteceder abertura.','evidence'=>'Comprovativo fictício com data incompatível.']),422,'backdated expense before opening refused');$reject(fn()=>$cashPreview('finance_review','close','count',[],1),409,'incomplete selected cash cycle cannot close before expense and count');
|
||||
$actor=$selectCash('read_only');$old=App\Shared\Context\CompanyContext::class;$reject(fn()=>app($old)->selectFor($actor,1,1,1),403,'foreign company selection refused by existing membership');
|
||||
}finally{while($host->transactionLevel()>0)$host->rollBack();}
|
||||
file_put_contents($work.'/cash-read-boundaries.json',json_encode(['passed'=>true,'checks'=>$checks,'proof_changes_rolled_back'=>true,'browser_visual_review'=>false],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/cash-read-boundaries.json']).PHP_EOL;
|
||||
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
$book=$argv[1]??'8085';require __DIR__.'/sgf-single-bootstrap.php';$names=['cash.view','cash.prepare','cash.review','cash.close'];$n=$host->table('permission_grants')->whereIn('permission',$names)->count();$out=['passed'=>true,'book'=>$book,'right_names'=>$names,'existing_grants'=>$n,'grants_applied'=>0,'business_commands'=>0,'new_demo'=>false];file_put_contents($work.'/cash-rights-readonly.json',json_encode($out,JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'book'=>$book,'existing_grants'=>$n,'proof'=>$work.'/cash-rights-readonly.json']).PHP_EOL;
|
||||
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
$book='regression';require __DIR__.'/sgf-single-bootstrap.php';$native->transaction(function()use($native,$f){foreach(['cash.receipt_classified','cash.expense_confirmed'] as $kind){$row=$native->table('gl_rules')->where($f['scope'])->where('rule_key',$kind)->orderByDesc('version')->firstOrFail();if($row->policy==='QA-CASH-JOURNAL-v1')continue;if($native->table('gl_journals')->where('snapshot->source->effect_type',$kind)->exists())throw new LogicException('Cannot change posted fixture rule');$v=(array)$row;unset($v['id']);$v['version']++;$v['policy']='QA-CASH-JOURNAL-v1';$v['reason']='Completar a regra QA antes da primeira prova, sem alterar versões antigas.';$native->table('gl_rules')->insert($v);}});echo json_encode(['passed'=>true,'regression_only'=>true,'journals_changed'=>0]).PHP_EOL;
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
use Illuminate\Support\Str;
|
||||
$book=$argv[1]??'regression';require __DIR__.'/sgf-single-bootstrap.php';if(!$host->getSchemaBuilder()->hasTable('cash_events'))$host->transaction(fn()=>$host->unprepared(file_get_contents($repo.'/database/sql/cash.sql')));
|
||||
$output=$host->table('sd_outputs')->where($f['scope'])->where('kind','invoice_receipt')->firstOrFail();$allocation=$host->table('sr_effects')->where('id',$output->effect_id)->where('kind','allocate')->firstOrFail();$receipt=$host->table('sr_records')->where($f['scope'])->where(['id'=>$allocation->receipt_id,'kind'=>'receipt','amount'=>'1000.00'])->firstOrFail();$path=$s['root'].'/cash-binding.json';
|
||||
if(!is_file($path))file_put_contents($path,json_encode(['composition'=>$tag,'scope'=>$f['scope'],'session_key'=>(string)Str::uuid(),'receipt_id'=>$receipt->id,'invoice_id'=>$receipt->invoice_id,'source_output_id'=>$output->id,'amount_minor'=>100000,'expense_minor'=>5000,'period_key'=>'QA-2026-INTEGRADO'],JSON_PRETTY_PRINT));
|
||||
$native->transaction(function()use($native,$f){$s=$f['scope'];$ids=[];foreach(['QA-CAIXA','QA-DESPESA-CAIXA','QA-LIQUIDACAO-CLIENTE'] as $code){$id=$native->table('gl_accounts')->where($s)->where('code',$code)->value('id');if(!$id&&$code==='QA-LIQUIDACAO-CLIENTE')throw new LogicException('Existing liquidity account required');if(!$id)$id=$native->table('gl_accounts')->insertGetId($s+['code'=>$code,'label'=>'Conta exclusivamente QA '.$code,'currency'=>'MZN','is_movement'=>true]);$ids[$code]=$id;}foreach(['cash.receipt_classified'=>['QA-CAIXA','QA-LIQUIDACAO-CLIENTE'],'cash.expense_confirmed'=>['QA-DESPESA-CAIXA','QA-CAIXA']] as $kind=>[$debit,$credit])if(!$native->table('gl_rules')->where($s)->where('rule_key',$kind)->exists())$native->table('gl_rules')->insert($s+['rule_key'=>$kind,'effect_type'=>$kind,'version'=>1,'policy'=>'QA-CASH-JOURNAL-v1','currency'=>'MZN','valid_from'=>'2026-01-01','valid_to'=>'2026-12-31','debit_account_id'=>$ids[$debit],'credit_account_id'=>$ids[$credit],'created_by'=>4,'reason'=>'Ciclo Caixa QA0+1000-50=950 expressamente autorizado.','created_at'=>now()]);});echo json_encode(['passed'=>true,'book'=>$book,'binding_sha256'=>hash_file('sha256',$path),'journals_unchanged'=>true]).PHP_EOL;
|
||||
@@ -0,0 +1,10 @@
|
||||
import fs from'node:fs';import assert from'node:assert/strict';import{session}from'./sgf-single-http.mjs';const run=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')),root=run.root+'/private/receipt-composition-cc50a276c10c',commercial=JSON.parse(fs.readFileSync(root+'/single-commercial.json')),workshop=JSON.parse(fs.readFileSync(root+'/single-workshop-stock-input.json')),file=root+'/commercial-quotes-http-20261110.json',progress=fs.existsSync(file)?JSON.parse(fs.readFileSync(file)):{operations:{},quotes:{},checks:[]},save=()=>fs.writeFileSync(file,JSON.stringify(progress,null,2));
|
||||
const clients={prepare:await session('sgf:commercial_prepare','commercial'),review:await session('sgf:commercial_review','commercial'),workshopPrepare:await session('sgf:finance_prepare','commercial'),workshopReview:await session('sgf:finance_review','commercial'),read:await session('sgf:read_only','commercial')};const ok=(v,label)=>{assert(v,label);if(!progress.checks.includes(label))progress.checks.push(label);save();};
|
||||
const step=async(name,role,body)=>{let op=progress.operations[name];if(!op){op=progress.operations[name]={role,body,key:crypto.randomUUID()};save();}assert.deepEqual(op.body,body);assert.equal(op.role,role);if(op.result)return op.result;const p=await clients[role].command('/api/commercial-quotes/preview',body,op.key);assert.equal(p.status,200,name+': '+JSON.stringify(p.value));const result=await clients[role].command('/api/commercial-quotes/confirm',{preview:p.value.preview});assert.equal(result.status,200,name+': '+JSON.stringify(result.value));op.result=result.value;save();const again=await clients[role].command('/api/commercial-quotes/confirm',{preview:p.value.preview});ok(again.status===200&&again.value.replayed&&again.value.id===result.value.id,name+' same confirmation replays one event');return result.value;};
|
||||
for(const[kind,amount,preparer,reviewer,order]of[['commercial','1000','prepare','review',null],['workshop','500','workshopPrepare','workshopReview',workshop.order]]){
|
||||
progress.quotes[kind]??=crypto.randomUUID();save();const quote=progress.quotes[kind];const prepared=await step(kind+'-prepare',preparer,{action:'prepare',quote_key:quote,version:0,reason:'Preparação individual do orçamento '+kind+' com origem e base explícitas.',data:{client_id:commercial.client,currency:'MZN',valid_until:'2026-12-31',source_order_id:order,description:'Proposta sintética '+kind+'; impostos por conferir separadamente.',lines:[{service_id:commercial.service,quantity:'1',unit_price:amount}]}});ok(prepared.impact.base===amount+'.00'&&!prepared.impact.invoice_issued,kind+' proposal preserves base and declares no invoice');
|
||||
await step(kind+'-review',reviewer,{action:'review',quote_key:quote,version:1,reason:'Conferência comercial distinta do preço, cliente, origem e validade.',data:{decision:'approved'}});await step(kind+'-customer',preparer,{action:'customer',quote_key:quote,version:2,reason:'Registo explícito da aceitação fictícia do cliente com prova identificada.',data:{decision:'accepted',representative:'Representante fictício QA',capacity:'Cliente QA autorizado',evidence:'QA-ACEITACAO-'+kind.toUpperCase()+'-20261110',decided_on:'2026-11-10'}});const converted=await step(kind+'-convert',preparer,{action:'convert',quote_key:quote,version:3,reason:'Converter a proposta aceite uma única vez nos rascunhos originais.',data:{}});ok(converted.state==='converted'&&Number.isInteger(converted.drafts.billing_draft_id),kind+' one explicit converted draft');
|
||||
let r=await clients[preparer].get('/api/a/billing_drafts/'+converted.drafts.billing_draft_id);ok(r.status===200,kind+' linked draft accessible in Commercial');const draft=await r.json();ok(draft.client_id===commercial.client&&draft.base_amount===amount,kind+' draft retains exact client and price');r=await clients[preparer].get('/commercial-quotes/'+quote+'/print?version=4',{headers:{Accept:'text/html'}});ok(r.status===200,kind+' authorised exact-version print view200');const html=await r.text();ok(html.includes('SEM VALIDADE FISCAL')&&html.includes('QA-ACEITACAO-')&&html.includes(amount+'.00'),kind+' print preserves acceptance and proposed amount without fiscal claim');
|
||||
}
|
||||
let r=await clients.read.get('/api/commercial-quotes?per_page=100');ok(r.status===200,'read-only bounded quote list100');const list=await r.json();ok(list.quotes.total===2,'same two completed quotes in selected installation');r=await clients.read.get('/api/commercial-quotes/'+progress.quotes.commercial+'/history');ok(r.status===200,'read-only quote history');const h=await r.json();ok(h.events.total===4,'all four decisions retained');const denied=await clients.read.command('/api/commercial-quotes/preview',{action:'prepare',quote_key:crypto.randomUUID(),version:0,reason:'Tentativa de preparação por perfil de consulta.',data:{}});ok(denied.status===403,'read-only quote writer403');
|
||||
r=await clients.prepare.get('/api/service-invoices');ok(r.status===200,'original issued invoice source remains readable');const invoices=await r.json();ok(invoices.invoices.length===1,'accepting/converting quotes emits zero additional invoices');const accounting=await session('sgf:accounting_prepare','accounting');r=await accounting.get('/api/general-ledger');const ledger=await r.json();ok(r.status===200&&ledger.journals.length===18,'quotes preserve existing18 journals');progress.passed=true;progress.browser_visual_review=false;save();console.log(JSON.stringify({passed:true,checks:progress.checks.length,proof:file}));
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
use Illuminate\Support\Facades\Auth;use Illuminate\Support\Str;use App\Shared\Context\CompanyContext;use App\Modules\Services\{CommercialQuotes,ServiceWorkflow};
|
||||
require __DIR__.'/sgf-ledger-proof-bootstrap.php';config(['commercial_quotes.enabled'=>true]);$commercial=json_decode(file_get_contents($s['root'].'/single-commercial.json'),true);$workshop=json_decode(file_get_contents($s['root'].'/single-workshop-stock-input.json'),true);
|
||||
$select=function($role)use($profiles,$f,$app){Auth::forgetGuards();Auth::shouldUse('web');$u=App\Models\User::findOrFail($profiles[$role]['user_id']);Auth::guard('web')->setUser($u);$r=Illuminate\Http\Request::create('/api/commercial-quotes','GET');$r->attributes->set('sgf_module','commercial');$r->setUserResolver(fn()=>$u);$session=app('session')->driver();$session->start();$session->put('company_context',$f['scope']+['module_id'=>'commercial','token'=>'quote-'.$role]);$r->setLaravelSession($session);$app->instance('request',$r);app(CompanyContext::class)->selectFor($u->id,1,2,2);return $u->id;};
|
||||
$reject=function(Closure $fn,int $status,string $label)use($ok){$bad=false;try{$fn();}catch(Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e){$bad=$e->getStatusCode()===$status;}catch(Illuminate\Validation\ValidationException){$bad=$status===422;}$ok($bad,$label);};$key=fn()=>(string)Str::uuid();$w=app(CommercialQuotes::class);set_exception_handler(function($e){echo json_encode(['passed'=>false,'type'=>get_class($e),'message'=>$e->getMessage(),'frames'=>array_map(fn($f)=>[basename($f['file']??''),$f['line']??0],array_slice($e->getTrace(),0,10))]).PHP_EOL;exit(1);});
|
||||
$host->beginTransaction();$beforeClients=$host->table('a_clients')->orderBy('id')->get()->toJson();$beforeInvoices=$host->table('si_invoices')->orderBy('id')->get()->toJson();
|
||||
try{
|
||||
$actor=$select('commercial_prepare');$data=['action'=>'prepare','quote_key'=>$key(),'version'=>0,'reason'=>'Proposta comercial sintética com preço explicitamente seleccionado.','data'=>['client_id'=>$commercial['client'],'currency'=>'MZN','valid_until'=>'2026-12-31','description'=>'Proposta QA de base1000, impostos conferidos separadamente.','lines'=>[['service_id'=>$commercial['service'],'quantity'=>'1','unit_price'=>'1000']]]];$command=$key();$p=$w->preview($actor,$data,$command);$ok($p['impact']['base']==='1000.00'&&!$p['impact']['invoice_issued'],'quote preview shows exact proposed base without pretending tax or invoice');$ok($host->table('cq_versions')->count()===0,'quote preview writes nothing persistently');$created=$w->confirm($actor,$p['preview']);$replay=$w->confirm($actor,$p['preview']);$ok($created['id']===$replay['id']&&$replay['replayed'],'prepare replay retains one quote version');
|
||||
$review=['action'=>'review','quote_key'=>$data['quote_key'],'version'=>1,'reason'=>'Conferência comercial independente das linhas e validade.','data'=>['decision'=>'approved']];$reject(fn()=>$w->preview($actor,$review,$key()),403,'preparer cannot use reviewer authority');$reviewer=$select('commercial_review');$p=$w->preview($reviewer,$review,$key());$approved=$w->confirm($reviewer,$p['preview']);$ok($approved['state']==='approved','distinct reviewer approves exact quote version');
|
||||
$actor=$select('commercial_prepare');$convert=['action'=>'convert','quote_key'=>$data['quote_key'],'version'=>2,'reason'=>'Não converter antes de decisão documentada do cliente.','data'=>[]];$reject(fn()=>$w->preview($actor,$convert,$key()),409,'review alone does not authorise conversion');
|
||||
$customer=['action'=>'customer','quote_key'=>$data['quote_key'],'version'=>2,'reason'=>'Decisão fictícia do cliente registada com prova identificada.','data'=>['decision'=>'accepted','representative'=>'Representante fictício QA','capacity'=>'Cliente de teste','evidence'=>'QA-ACEITACAO-ORCAMENTO-1000','decided_on'=>'2026-11-10']];$p=$w->preview($actor,$customer,$key());$accepted=$w->confirm($actor,$p['preview']);$ok($accepted['state']==='customer_accepted','client acceptance requires representative and evidence');
|
||||
$convert['version']=3;$priorNow=Carbon\Carbon::getTestNow();Carbon\Carbon::setTestNow('2027-01-01 10:00:00');try{$reject(fn()=>$w->preview($actor,$convert,$key()),409,'expired customer acceptance cannot be converted');}finally{Carbon\Carbon::setTestNow($priorNow);}$p=$w->preview($actor,$convert,$key());$n=$host->table('a_billing_drafts')->count();$host->beginTransaction();try{$client=$host->table('a_clients')->where('id',$commercial['client'])->first();app(ServiceWorkflow::class)->save('clients',$client->id,$actor,['version'=>$client->version,'name'=>'Cliente QA alterado apenas no ensaio de versão'],$key());$reject(fn()=>$w->confirm($actor,$p['preview']),409,'new client version invalidates old quote conversion conference');}finally{$host->rollBack();}$host->beginTransaction();try{$host->table('permission_grants')->where(['user_id'=>$actor,'permission'=>'commercial_quotes.convert'])->delete();$reject(fn()=>$w->confirm($actor,$p['preview']),403,'revoked conversion right refuses prepared decision');}finally{$host->rollBack();}
|
||||
$host->unprepared("CREATE FUNCTION cq_test_fail() RETURNS trigger LANGUAGE plpgsql AS 'BEGIN RAISE EXCEPTION ''CQ_TEST_FAILURE''; END'");$host->unprepared('REVOKE ALL ON FUNCTION cq_test_fail() FROM PUBLIC');$host->unprepared('CREATE TRIGGER cq_test_fail BEFORE INSERT ON cq_commands FOR EACH ROW EXECUTE FUNCTION cq_test_fail()');$failed=false;try{$w->confirm($actor,$p['preview']);}catch(Illuminate\Database\QueryException){$failed=true;}$ok($failed&&$host->table('a_billing_drafts')->count()===$n,'failure after draft writer rolls back drafts, conversion and quote version');$host->unprepared('DROP TRIGGER cq_test_fail ON cq_commands');$host->unprepared('DROP FUNCTION cq_test_fail()');
|
||||
$converted=$w->confirm($actor,$p['preview']);$again=$w->confirm($actor,$p['preview']);$ok($converted['state']==='converted'&&$converted['drafts']['billing_draft_id']===$again['drafts']['billing_draft_id']&&$converted['drafts']['service_draft_id']===$again['drafts']['service_draft_id']&&$again['replayed'],'one explicit conversion recovers same two original-writer drafts');$billing=$host->table('a_billing_drafts')->where('id',$converted['drafts']['billing_draft_id'])->first();$ok($billing->client_id===$commercial['client']&&App\Modules\Services\DraftValues::base(json_decode($billing->data,true))==='1000','conversion preserves client, quantity and proposed price');
|
||||
$reject(fn()=>app(ServiceWorkflow::class)->save('billing_drafts',$billing->id,$actor,['version'=>1,'description'=>'Não editar a proposta aceite por arrasto.'],$key()),409,'converted quote draft cannot bypass its accepted version through generic edit');$reject(fn()=>$w->preview($actor,array_replace($data,['version'=>4]),$key()),409,'converted quote cannot be silently revised or converted twice');
|
||||
$finance=$select('finance_prepare');$q=array_replace($data,['quote_key'=>$key(),'data'=>array_replace($data['data'],['source_order_id'=>$workshop['order'],'description'=>'Orçamento previsto da reparação, sem autorizar trabalho.','lines'=>[['service_id'=>$commercial['service'],'quantity'=>'1','unit_price'=>'500']]])]);$p=$w->preview($finance,$q,$key());$createdW=$w->confirm($finance,$p['preview']);$ok($p['impact']['base']==='500.00'&&$p['impact']['source_reference']!==null,'workshop quote preserves explicit work-order reference and base500');
|
||||
$read=$select('read_only');$list=$w->listing($read,['per_page'=>100]);$ok($list['quotes']->total()===2,'read-only quote listing is server paginated25/100');$reject(fn()=>$w->preview($read,$data,$key()),403,'quote reader cannot prepare');$history=$w->history($read,$data['quote_key']);$ok($history['events']->total()===4,'quote history preserves preparation, distinct review, client decision and conversion');
|
||||
$ok($beforeClients===$host->table('a_clients')->orderBy('id')->get()->toJson()&&$beforeInvoices===$host->table('si_invoices')->orderBy('id')->get()->toJson(),'quotes do not change clients, old associations or issued invoices');
|
||||
}finally{while($host->transactionLevel()>0)$host->rollBack();}
|
||||
file_put_contents($work.'/commercial-quotes-result.json',json_encode(['passed'=>true,'checks'=>$checks,'proof_changes_rolled_back'=>true,'invoices_issued'=>0,'browser_visual_review'=>false],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/commercial-quotes-result.json']).PHP_EOL;
|
||||
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
$book=$argv[1]??'regression';require __DIR__.'/sgf-single-bootstrap.php';if(!$host->getSchemaBuilder()->hasTable('cq_versions'))$host->transaction(function()use($host,$repo){$host->unprepared(file_get_contents($repo.'/database/sql/commercial_quotes.sql'));foreach(['cq_versions','cq_commands','cq_conversions'] as $table){$host->unprepared('CREATE TRIGGER cq_immutable BEFORE UPDATE OR DELETE ON '.$table.' FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit()');$host->unprepared('CREATE TRIGGER cq_testing BEFORE INSERT ON '.$table.' FOR EACH ROW EXECUTE FUNCTION sgf_wo_test_only()');$host->statement('REVOKE ALL ON TABLE '.$table.' FROM PUBLIC');$host->statement('REVOKE ALL ON SEQUENCE '.$table.'_id_seq FROM PUBLIC');}$host->statement('REVOKE ALL ON FUNCTION sgf_cq_guard() FROM PUBLIC');});echo json_encode(['passed'=>true,'book'=>$book,'business_commands'=>0,'new_grants'=>0]).PHP_EOL;
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
use Illuminate\Support\Facades\{DB,Auth};
|
||||
use Illuminate\Support\Str;
|
||||
use App\Shared\Context\CompanyContext;
|
||||
use App\Modules\Services\{ServiceWorkflow,ServiceInvoicing};
|
||||
$book=$argv[1]??'regression';require __DIR__.'/sgf-single-bootstrap.php';
|
||||
$profiles=json_decode(file_get_contents($s['root'].'/single-profiles.json'),true)['profiles'];
|
||||
config(['sgf_single_installation.enabled'=>true,'sgf_single_installation.schema_sha256'=>$host->table('sgf_composition_schema')->value('source_hash')]);
|
||||
$root=$s['root'].'/single-commercial.json';
|
||||
if(is_file($root)){echo json_encode(['passed'=>true,'book'=>$book,'replayed'=>true,'new_business_commands'=>0]).PHP_EOL;exit;}
|
||||
$actor=App\Models\User::findOrFail($profiles['commercial_prepare']['user_id']);$reviewer=App\Models\User::findOrFail($profiles['commercial_review']['user_id']);Auth::guard('web')->setUser($actor);
|
||||
$request=Illuminate\Http\Request::create('/api/a/clients','POST');$request->attributes->set('sgf_module','commercial');$request->setUserResolver(fn()=>$actor);$session=app('session')->driver();$session->start();$session->put('company_context',$f['scope']+['module_id'=>'commercial','token'=>'fixture-commercial']);$request->setLaravelSession($session);$app->instance('request',$request);
|
||||
$oldClientIds=$host->table('a_clients')->pluck('id')->all();$before=$host->table('a_clients')->whereIn('id',$oldClientIds)->orderBy('id')->get()->toJson();
|
||||
$result=$host->transaction(function()use($actor,$reviewer,$f,$host){
|
||||
require_once base_path('tests/Support/IdentityFixture.php');require_once base_path('tests/Support/InvoiceFixture.php');
|
||||
$base=['tenant'=>$f['tenant'],'company'=>$f['company'],'unit'=>$f['unit'],'manager'=>$actor,'reader'=>$reviewer];
|
||||
$fixture=Tests\Support\InvoiceFixture::create($base,'862.07');
|
||||
app(CompanyContext::class)->selectFor($actor->id,$f['tenant'],$f['company'],$f['unit']);
|
||||
$invoices=app(ServiceInvoicing::class);$review=$invoices->review($fixture['draft'],$fixture['series'],$fixture['rule'],$actor->id);
|
||||
if(!$review['ready'])throw new LogicException('Test document is not ready: '.implode(' ',$review['payload']['blocks']??[]));
|
||||
$key=(string)Str::uuid();$data=['series_id'=>$fixture['series'],'rule_id'=>$fixture['rule'],'review_hash'=>$review['review_hash']];
|
||||
$issued=$invoices->issue($fixture['draft'],$actor->id,$data,$key);$retry=$invoices->issue($fixture['draft'],$actor->id,$data,$key);
|
||||
if($issued['id']!==$retry['id'])throw new LogicException('Invoice duplicated');
|
||||
$row=$host->table('si_invoices')->where('id',$issued['id'])->first();$snapshot=json_decode($row->snapshot,true);
|
||||
if($snapshot['calculation']['total_document_proposed']!=='1000.00'||hash('sha256',base64_decode($row->pdf_base64,true))!==$row->pdf_sha256)throw new LogicException('Document arithmetic/PDF mismatch');
|
||||
return array_intersect_key($fixture,array_flip(['client','service','source','draft','profile','temporal','calculation','series','rule','today','start','end']))+['invoice'=>$issued['id'],'issue_key'=>$key,'preparer'=>$actor->id,'reviewer'=>$reviewer->id,'pdf_sha256'=>$row->pdf_sha256,'total'=>'1000.00','synthetic_only'=>true];
|
||||
});
|
||||
if($before!==$host->table('a_clients')->whereIn('id',$oldClientIds)->orderBy('id')->get()->toJson())throw new LogicException('Existing client changed');
|
||||
file_put_contents($root,json_encode($result,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));file_put_contents($work.'/commercial-result.json',json_encode(['passed'=>true,'invoice'=>$result['invoice'],'total'=>'1000.00','pdf_integrity'=>true,'replayed_without_duplicate'=>true,'old_clients_preserved'=>true,'synthetic_only'=>true],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'book'=>$book,'total'=>'1000.00','old_clients_preserved'=>true,'proof'=>$work.'/commercial-result.json']).PHP_EOL;
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
// Explicit, local composition of the two EXISTING books. No server/database/role creation.
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Shared\Installation\CompositionSchema;
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
$repo=dirname(__DIR__);$mode=$argv[1]??'--plan';$book=$argv[2]??'8085';
|
||||
if(!in_array($mode,['--plan','--apply','--verify'],true)||!in_array($book,['regression','8085'],true))throw new LogicException('Use --plan|--apply|--verify regression|8085');
|
||||
$tag=$book==='8085'?'cc50a276c10c':'b4093c6f6707';$run=json_decode(file_get_contents('C:/laragon/data/sgf-tempo/run/modular-regression-current.json'),true);
|
||||
if($run['id']!=='2576427d1939'||(int)$run['port']!==55439)throw new LogicException('Known cluster only');
|
||||
$s=json_decode(file_get_contents($run['root'].'/private/receipt-composition-'.$tag.'/state.json'),true);
|
||||
if($s['tag']!==$tag||($book==='8085'&&(!($s['singleInstallation']??false)||json_decode(file_get_contents($s['root'].'/demo-server.json'),true)['port']!==8085)))throw new LogicException('Existing installation identity changed');
|
||||
$private=$s['root'].'/composition-installs/'.date('Ymd-His').'-'.bin2hex(random_bytes(4));mkdir($private,0700,true);
|
||||
foreach($s['env'] as $key=>$value)if(preg_match('/^(APP_|DB_|SGF_|SESSION_|CACHE_|QUEUE_|BCRYPT_)/',$key)){putenv($key.'='.$value);$_ENV[$key]=$value;$_SERVER[$key]=$value;}
|
||||
foreach(['APP_BASE_PATH'=>$repo,'APP_CONFIG_CACHE'=>$private.'/config.php','APP_ROUTES_CACHE'=>$private.'/routes.php','APP_SERVICES_CACHE'=>$private.'/services.php','APP_PACKAGES_CACHE'=>$private.'/packages.php','SESSION_DRIVER'=>'array','CACHE_STORE'=>'array'] as $key=>$value){putenv($key.'='.$value);$_ENV[$key]=$value;$_SERVER[$key]=$value;}
|
||||
require $repo.'/vendor/autoload.php';$app=require $repo.'/bootstrap/app.php';$app->addAbsoluteCachePathPrefix('C:');$app->useStoragePath($private.'/storage');foreach(['logs','framework/views','framework/sessions','framework/cache/data'] as $dir)mkdir($private.'/storage/'.$dir,0700,true);
|
||||
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();$db=DB::connection();$identity=CompositionSchema::identity();
|
||||
$files=['app/Shared/Installation/CompositionSchema.php','app/Shared/Installation/CompositionSchemaConnection.php','app/Shared/Installation/CompositionSchemaBuilder.php','app/Shared/Installation/CompositionRuntime.php','app/Modules/Services/ServiceCalculation.php','app/Shared/Testing/TestingDatabaseGuard.php','app/Providers/AppServiceProvider.php','config/sgf_single_installation.php'];
|
||||
foreach(CompositionSchema::MIGRATIONS as $file)$files[]='database/migrations/'.$file;
|
||||
foreach(glob($repo.'/database/sql/*.sql') as $file)$files[]='database/sql/'.basename($file);
|
||||
$pins=[];foreach($files as $file)$pins[$file]=hash_file('sha256',$repo.'/'.$file);
|
||||
$schema=new CompositionSchema;$plan=['book'=>$book,'composition'=>$tag,'mode'=>$mode,'source_hash'=>$schema->fingerprint(),'files'=>$pins,'new_access'=>false,'new_business_operations'=>false,'connector_required'=>false,'runtime_enabled'=>false];
|
||||
file_put_contents($private.'/plan.json',json_encode($plan,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
|
||||
if($mode==='--plan'){echo json_encode(['plan'=>$private.'/plan.json','files'=>count($files),'database_writes'=>0]).PHP_EOL;exit;}
|
||||
if($mode==='--verify'){
|
||||
$stored=$db->getSchemaBuilder()->hasTable('sgf_composition_schema')?$db->table('sgf_composition_schema')->where('id',1)->value('source_hash'):null;
|
||||
$copy=true;foreach($pins as $file=>$pin)if(!is_file($s['app'].'/'.$file)||!hash_equals($pin,hash_file('sha256',$s['app'].'/'.$file)))$copy=false;
|
||||
$valid=is_string($stored)&&hash_equals($schema->fingerprint(),$stored)&&$copy;
|
||||
echo json_encode(['passed'=>$valid,'composition'=>$tag,'code_matches'=>$copy,'database_writes'=>0,'runtime_enabled'=>false]).PHP_EOL;exit($valid?0:2);
|
||||
}
|
||||
// Snapshot pre-existing records and sequences, including every permission and association.
|
||||
$tables=array_column($db->select("SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename"),'tablename');
|
||||
$functionsBefore=array_column($db->select("SELECT oid FROM pg_proc WHERE pronamespace='public'::regnamespace"),'oid');
|
||||
$sequencesBefore=array_column($db->select("SELECT sequencename FROM pg_sequences WHERE schemaname='public'"),'sequencename');
|
||||
$snapshot=function()use($db,$tables){$out=[];foreach($tables as $table){if(!preg_match('/^[a-z_][a-z0-9_]*$/D',$table))throw new LogicException('Table');$out[$table]=(array)$db->selectOne('SELECT count(*) rows,md5(coalesce(string_agg(row_to_json(t)::text,chr(10) ORDER BY row_to_json(t)::text),\'\')) hash FROM "'.$table.'" t');}$seq=$db->select("SELECT sequencename,last_value FROM pg_sequences WHERE schemaname='public' AND sequencename=ANY(?::text[]) ORDER BY sequencename",['{'.implode(',',array_column($db->select("SELECT sequencename FROM pg_sequences WHERE schemaname='public'"),'sequencename')).'}']);return ['tables'=>$out,'sequences'=>$seq];};
|
||||
$before=$snapshot();file_put_contents($private.'/before.json',json_encode($before,JSON_THROW_ON_ERROR));
|
||||
$process=new Process(['C:/Program Files/PostgreSQL/17/bin/pg_dump.exe','-h','127.0.0.1','-p','55439','-U',$s['env']['DB_USERNAME'],'-d',$s['env']['DB_DATABASE'],'--format=custom','--no-owner','--no-privileges','--file',$private.'/host-before.dump'],$repo,['PGPASSWORD'=>$s['env']['DB_PASSWORD']]);$process->setTimeout(120);$process->mustRun();
|
||||
$original=[];foreach($files as $file){$dest=$s['app'].'/'.$file;$original[$file]=is_file($dest)?hash_file('sha256',$dest):null;if(is_file($dest)){$backup=$private.'/code-before/'.$file;if(!is_dir(dirname($backup)))mkdir(dirname($backup),0700,true);copy($dest,$backup);}}
|
||||
$copied=[];
|
||||
try{
|
||||
$result=$schema->run(true,function($connection)use($files,$pins,$original,$repo,$s,&$copied,$functionsBefore,$tables,$sequencesBefore){
|
||||
// New PostgreSQL functions otherwise inherit implicit PUBLIC EXECUTE.
|
||||
// Remove only defaults on NEW objects; existing ACLs remain untouched.
|
||||
$restricted=0;
|
||||
foreach($connection->select("SELECT oid,oid::regprocedure::text signature FROM pg_proc WHERE pronamespace='public'::regnamespace") as $fn)if(!in_array($fn->oid,$functionsBefore,true)){$connection->statement('REVOKE ALL ON FUNCTION '.$fn->signature.' FROM PUBLIC');$restricted++;}
|
||||
foreach($connection->select("SELECT tablename FROM pg_tables WHERE schemaname='public'") as $row)if(!in_array($row->tablename,$tables,true))$connection->statement('REVOKE ALL ON TABLE public.'.'"'.str_replace('"','""',$row->tablename).'" FROM PUBLIC');
|
||||
foreach($connection->select("SELECT sequencename FROM pg_sequences WHERE schemaname='public'") as $row)if(!in_array($row->sequencename,$sequencesBefore,true))$connection->statement('REVOKE ALL ON SEQUENCE public.'.'"'.str_replace('"','""',$row->sequencename).'" FROM PUBLIC');
|
||||
foreach($files as $file){$dest=$s['app'].'/'.$file;$now=is_file($dest)?hash_file('sha256',$dest):null;if($now!==$original[$file])throw new LogicException('Concurrent code modification; preserve it');if(!is_dir(dirname($dest)))mkdir(dirname($dest),0700,true);if(!copy($repo.'/'.$file,$dest)||!hash_equals($pins[$file],hash_file('sha256',$dest)))throw new RuntimeException('Code copy incomplete');$copied[]=$file;}
|
||||
return ['copied_files'=>count($copied),'new_grants'=>0,'new_documents'=>0,'new_function_public_defaults_removed'=>$restricted,'runtime_enabled'=>false];
|
||||
});
|
||||
}catch(Throwable $e){
|
||||
foreach(array_reverse($copied) as $file){$dest=$s['app'].'/'.$file;if(!is_file($dest)||!hash_equals($pins[$file],hash_file('sha256',$dest)))continue;if($original[$file]===null)unlink($dest);else copy($private.'/code-before/'.$file,$dest);}
|
||||
file_put_contents($private.'/failure.json',json_encode(['failed'=>true,'type'=>$e::class,'database_rollback_requested'=>true,'code_recovery_attempted'=>true]));throw $e;
|
||||
}
|
||||
$after=$snapshot();$preserved=$before['tables']===$after['tables'];
|
||||
// Restrict sequence comparison to the original names; newly installed sequences are expected.
|
||||
$oldSeq=[];foreach($before['sequences'] as $row)$oldSeq[$row->sequencename]=$row->last_value;$newSeq=[];foreach($after['sequences'] as $row)if(array_key_exists($row->sequencename,$oldSeq))$newSeq[$row->sequencename]=$row->last_value;$preserved=$preserved&&$oldSeq===$newSeq;
|
||||
file_put_contents($private.'/after.json',json_encode($after,JSON_THROW_ON_ERROR));
|
||||
$result=['passed'=>$preserved,'composition'=>$tag,'schema'=>$result,'previous_rows_sequences_permissions_associations_preserved'=>$preserved,'code_files'=>count($files),'runtime_enabled'=>false,'business_operations'=>0,'cache_touched'=>false,'connector_required'=>false,'backup_sha256'=>hash_file('sha256',$private.'/host-before.dump')];
|
||||
file_put_contents($private.'/result.json',json_encode($result,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo json_encode(['passed'=>$preserved,'composition'=>$tag,'new_tables'=>count($result['schema']['new_tables']??[]),'new_grants'=>0,'runtime_enabled'=>false,'proof'=>$private.'/result.json']).PHP_EOL;exit($preserved?0:2);
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
use Illuminate\Support\Str;
|
||||
use App\Shared\Context\CompanyContext;
|
||||
use App\Modules\Workshop\DirectCosts;
|
||||
require __DIR__.'/sgf-ledger-proof-bootstrap.php';
|
||||
config(['sales_ledger.synthetic_enabled'=>true,'cost_ledger.synthetic_enabled'=>true,'industrial_ledger.synthetic_enabled'=>true,'cost_ledger.correction_enabled'=>true,'industrial_ledger.correction_enabled'=>true]);
|
||||
$fixture=json_decode(file_get_contents($s['root'].'/single-direct-cost.json'),true);$order=$fixture['order'];$old=$host->table('dc_calculations')->where('id',$fixture['calculation']['id'])->value('snapshot');
|
||||
$host->beginTransaction();$native->beginTransaction();
|
||||
try{
|
||||
$p=$call('accounting_prepare','prepare',fn($e)=>$e->command('effect-prepare',['effect_type'=>'workshop.direct_cost.validated','source_id'=>$order,'journal_on'=>'2026-11-10','period_key'=>'QA-2026-INTEGRADO','rule_key'=>'workshop.direct_cost.validated','reason'=>'Custo275 proveniente da oficina e validado na mesma instalação.'],(string)Str::uuid()));
|
||||
$call('accounting_review','validate',fn($e)=>$e->command('validate',['proposal_id'=>$p['proposal_id'],'revision'=>$p['revision'],'decision'=>'approved','reason'=>'Revisão contabilística distinta do custo275.'],(string)Str::uuid()));
|
||||
$j=$call('accounting_prepare','post',fn($e)=>$e->command('post',['proposal_id'=>$p['proposal_id'],'revision'=>$p['revision'],'reason'=>'Aplicação explícita do custo original275.'],(string)Str::uuid()));
|
||||
$ok((int)$native->table('gl_journals')->where('id',$j['journal_id'])->value('debit_minor')===27500,'original host workshop cost275 posts in the same native ledger');
|
||||
$set=function($role)use($profiles,$f){$id=$profiles[$role]['user_id'];Illuminate\Support\Facades\Auth::guard('web')->setUser(App\Models\User::findOrFail($id));app(CompanyContext::class)->selectFor($id,$f['tenant'],$f['company'],$f['unit']);return $id;};
|
||||
$dc=app(DirectCosts::class);$prepare=$set('finance_prepare');
|
||||
$rate=$dc->rate($order,$prepare,['entry_id'=>$fixture['material'],'entry_version'=>1,'identifier'=>'QA-INTEGRADO-material','version'=>2,'previous_id'=>$fixture['rates']['material'],'currency'=>'MZN','unit'=>'UN','amount'=>'60','evidence'=>'Taxa sucessora QA60 para duas unidades.'],(string)Str::uuid())['id'];
|
||||
$review=$set('finance_review');$dc->decide($order,$review,['kind'=>'rate','id'=>$rate,'version'=>2,'decision'=>'approved','reason'=>'Validação independente da taxa sucessora60.'],(string)Str::uuid());
|
||||
$set('finance_prepare');$c=$dc->calculate($order,$prepare,['currency'=>'MZN','version'=>2,'previous_id'=>$fixture['calculation']['id'],'lines'=>[['entry_id'=>$fixture['material'],'entry_version'=>1,'rate_id'=>$rate,'rate_version'=>2],['entry_id'=>$fixture['time'],'entry_version'=>1,'rate_id'=>$fixture['rates']['time'],'rate_version'=>1]]],(string)Str::uuid());
|
||||
$set('finance_review');$dc->decide($order,$review,['kind'=>'calculation','id'=>$c['id'],'version'=>2,'decision'=>'approved','reason'=>'Novo total295 validado, conservando o custo anterior.'],(string)Str::uuid());
|
||||
$ok($dc->conference($order,$review)['snapshot']['selected_direct_total_exact']==='295','successor validated calculation uses120 material plus175 labour');
|
||||
$ok($old===$host->table('dc_calculations')->where('id',$fixture['calculation']['id'])->value('snapshot'),'original275 calculation remains immutable');
|
||||
$correction=$call('accounting_prepare','correct_prepare',fn($e)=>$e->command('correct-prepare',['journal_id'=>$j['journal_id'],'journal_on'=>'2026-11-10','period_key'=>'QA-2026-INTEGRADO','reason'=>'Correcção explicitamente autorizada275 para295 por par indivisível.'],(string)Str::uuid()));
|
||||
$call('accounting_review','correct_validate',fn($e)=>$e->command('correct-validate',['correction_id'=>$correction['correction_id'],'revision'=>$correction['revision'],'decision'=>'approved','reason'=>'Revisão independente do estorno275 e substituição295.'],(string)Str::uuid()));
|
||||
$key=(string)Str::uuid();$input=['correction_id'=>$correction['correction_id'],'revision'=>$correction['revision'],'reason'=>'Aplicação atómica dos dois lançamentos revistos.'];
|
||||
$pair=$call('accounting_prepare','correct_post',fn($e)=>$e->command('correct-post',$input,$key));$retry=$call('accounting_prepare','correct_post',fn($e)=>$e->command('correct-post',$input,$key));
|
||||
$ok($pair['reverse_journal_id']===$retry['reverse_journal_id']&&$pair['replacement_journal_id']===$retry['replacement_journal_id'],'correction replay recovers the original indivisible pair');
|
||||
$ok((int)$native->table('gl_journals')->where('id',$pair['reverse_journal_id'])->value('debit_minor')===27500&&(int)$native->table('gl_journals')->where('id',$pair['replacement_journal_id'])->value('debit_minor')===29500,'same ledger retains reversal275 and replacement295');
|
||||
$native->statement('SET CONSTRAINTS ALL IMMEDIATE');$ok(true,'balanced journal constraints validate the entire correction pair');
|
||||
}finally{while($native->transactionLevel()>0)$native->rollBack();while($host->transactionLevel()>0)$host->rollBack();}
|
||||
$ok($host->table('dc_calculations')->where('order_id',$order)->count()===1,'regression rolls back the successor without changing accepted original');
|
||||
file_put_contents($work.'/direct-ledger-result.json',json_encode(['passed'=>true,'checks'=>$checks,'rolled_back'=>true,'same_native_book'=>true],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/direct-ledger-result.json']).PHP_EOL;
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
use Illuminate\Support\Str;
|
||||
use App\Shared\Context\CompanyContext;
|
||||
use App\Modules\Industry\IndustrialCosts;
|
||||
require __DIR__.'/sgf-ledger-proof-bootstrap.php';
|
||||
config(['sales_ledger.synthetic_enabled'=>true,'cost_ledger.synthetic_enabled'=>true,'industrial_ledger.synthetic_enabled'=>true,'cost_ledger.correction_enabled'=>true,'industrial_ledger.correction_enabled'=>true]);
|
||||
$i=json_decode(file_get_contents($s['root'].'/single-industrial.json'),true);$lot=$i['lot'];$original=$host->table('ic_results')->where('id',$i['calculation']['id'])->value('snapshot');$host->beginTransaction();$native->beginTransaction();
|
||||
try{
|
||||
$p=$call('accounting_prepare','prepare',fn($e)=>$e->command('effect-prepare',['effect_type'=>'industry.cost.validated','source_id'=>$lot,'journal_on'=>'2026-11-10','period_key'=>'QA-2026-INTEGRADO','rule_key'=>'industry.cost.validated','reason'=>'Resultado industrial1800 efectivamente validado na origem SGF.'],(string)Str::uuid()));
|
||||
$call('accounting_review','validate',fn($e)=>$e->command('validate',['proposal_id'=>$p['proposal_id'],'revision'=>$p['revision'],'decision'=>'approved','reason'=>'Revisão contabilística distinta do lote1800.'],(string)Str::uuid()));
|
||||
$j=$call('accounting_prepare','post',fn($e)=>$e->command('post',['proposal_id'=>$p['proposal_id'],'revision'=>$p['revision'],'reason'=>'Aplicação única do custo industrial seleccionado.'],(string)Str::uuid()));
|
||||
$snapshot=json_decode($native->table('gl_journals')->where('id',$j['journal_id'])->value('snapshot'),true);$ok($snapshot['source']['principal_minor']===180000&&$snapshot['source']['residual_exact']==='300','same native ledger recognises1800 while retaining separate residual300');
|
||||
$set=function($role)use($profiles,$f){$id=$profiles[$role]['user_id'];Illuminate\Support\Facades\Auth::guard('web')->setUser(App\Models\User::findOrFail($id));app(CompanyContext::class)->selectFor($id,$f['tenant'],$f['company'],$f['unit']);return $id;};
|
||||
$ic=app(IndustrialCosts::class);$old=$host->table('ic_inputs')->where('id',$i['refs']['material_rate']['id'])->first();$data=json_decode($old->data,true);$data['amount']='51';$prepare=$set('finance_prepare');
|
||||
$rate=$ic->input($lot,$prepare,['kind'=>'material_rate','source_ref'=>$old->source_ref,'version'=>2,'previous_id'=>$old->id,'data'=>$data,'evidence'=>'Taxa sucessora51 para20KG, mantendo os restantes pressupostos.'],(string)Str::uuid());
|
||||
$review=$set('finance_review');$ic->decide($lot,$review,['type'=>'input','id'=>$rate['id'],'version'=>2,'decision'=>'approved','reason'=>'Conferência distinta da taxa artificial51.'],(string)Str::uuid());
|
||||
$refs=$i['refs'];$refs['material_rate']=['id'=>$rate['id'],'version'=>2];$set('finance_prepare');$r=$ic->calculate($lot,$prepare,['currency'=>'MZN','version'=>2,'previous_id'=>$i['calculation']['id'],'refs'=>$refs],(string)Str::uuid());
|
||||
$set('finance_review');$ic->decide($lot,$review,['type'=>'result','id'=>$r['id'],'version'=>2,'decision'=>'approved','reason'=>'Novo resultado1820 conferido, residual300 permanece separado.'],(string)Str::uuid());
|
||||
$snap=json_decode($host->table('ic_results')->where('id',$r['id'])->value('snapshot'),true);$ok($snap['known_cost_exact']==='1820'&&$snap['residual_exact']==='300','validated successor changes material only,1800 to1820');
|
||||
$p=$call('accounting_prepare','correct_prepare',fn($e)=>$e->command('correct-prepare',['journal_id'=>$j['journal_id'],'journal_on'=>'2026-11-10','period_key'=>'QA-2026-INTEGRADO','reason'=>'Par indivisível de correcção industrial1800 para1820.'],(string)Str::uuid()));
|
||||
$call('accounting_review','correct_validate',fn($e)=>$e->command('correct-validate',['correction_id'=>$p['correction_id'],'revision'=>$p['revision'],'decision'=>'approved','reason'=>'Revisão independente do par industrial exacto.'],(string)Str::uuid()));
|
||||
$body=['correction_id'=>$p['correction_id'],'revision'=>$p['revision'],'reason'=>'Aplicação única e atómica do par industrial.'];$key=(string)Str::uuid();$pair=$call('accounting_prepare','correct_post',fn($e)=>$e->command('correct-post',$body,$key));$again=$call('accounting_prepare','correct_post',fn($e)=>$e->command('correct-post',$body,$key));
|
||||
$ok($pair['reverse_journal_id']===$again['reverse_journal_id']&&$pair['replacement_journal_id']===$again['replacement_journal_id'],'industrial pair replay has no duplicated journal');
|
||||
$ok((int)$native->table('gl_journals')->where('id',$pair['reverse_journal_id'])->value('debit_minor')===180000&&(int)$native->table('gl_journals')->where('id',$pair['replacement_journal_id'])->value('debit_minor')===182000,'indivisible pair retains1800 reversal and1820 replacement');
|
||||
$ok($original===$host->table('ic_results')->where('id',$i['calculation']['id'])->value('snapshot'),'original industrial result remains immutable');$native->statement('SET CONSTRAINTS ALL IMMEDIATE');
|
||||
}finally{while($native->transactionLevel()>0)$native->rollBack();while($host->transactionLevel()>0)$host->rollBack();}
|
||||
$ok($host->table('ic_results')->where('lot_id',$lot)->count()===1,'successor and journal proofs roll back without changing original regression result');
|
||||
file_put_contents($work.'/industrial-ledger-result.json',json_encode(['passed'=>true,'checks'=>$checks,'rolled_back'=>true,'same_native_book'=>true],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/industrial-ledger-result.json']).PHP_EOL;
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
use IlluminateSupportStr;
|
||||
require __DIR__.'/sgf-ledger-proof-bootstrap.php';
|
||||
$obligationsBefore=$native->table('pa_obligations')->orderBy('id')->get()->toJson();$oldMovements=$native->table('stock_movements')->orderBy('id')->get()->toJson();$journalBefore=$native->table('gl_journals')->count();
|
||||
$source=(int)$native->table('pa_obligations')->where('tenant_id',$nf['tenant'])->orderByDesc('id')->value('id');
|
||||
$native->beginTransaction();
|
||||
try{
|
||||
$key=(string)Str::uuid();$input=['obligation_id'=>$source,'journal_on'=>'2026-11-10','period_key'=>'QA-2026-INTEGRADO','rule_key'=>'supplier_payable.recognised','reason'=>'Conferência integrada QA da obrigação existente, sem copiar dívida.'];
|
||||
$proposal=$call('accounting_prepare','prepare',fn($e)=>$e->command('prepare',$input,$key));
|
||||
$ok($proposal['state']==='prepared','existing native obligation is captured for the explicit SGF ledger scope');
|
||||
$same=$call('accounting_prepare','prepare',fn($e)=>$e->command('prepare',$input,$key));$ok($same['proposal_id']===$proposal['proposal_id'],'same preparation key recovers the same proposal');
|
||||
$p=$native->table('gl_proposals')->find($proposal['proposal_id']);$ok((int)$p->company_id===2&&(int)$p->unit_id===2,'ledger carries SGF company2/unit2 while obligation remains in native financial scope0/0');
|
||||
$review=['proposal_id'=>$proposal['proposal_id'],'revision'=>$proposal['revision'],'decision'=>'approved','reason'=>'Revisão independente do impacto de100MZN e da origem existente.'];
|
||||
$forbidden=false;try{$call('accounting_prepare','validate',fn($e)=>$e->command('validate',$review,(string)Str::uuid()));}catch(Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e){$forbidden=$e->getStatusCode()===403;}
|
||||
$ok($forbidden,'preparation profile cannot acquire the review right through native identity mapping');
|
||||
$decision=$call('accounting_review','validate',fn($e)=>$e->command('validate',$review,(string)Str::uuid()));$ok($decision['state']==='approved','distinct host and native person reviews the exact version');
|
||||
$postKey=(string)Str::uuid();$post=['proposal_id'=>$proposal['proposal_id'],'revision'=>$proposal['revision'],'reason'=>'Aplicação única do impacto revisto; sem nova obrigação ou pagamento.'];
|
||||
$posted=$call('accounting_prepare','post',fn($e)=>$e->command('post',$post,$postKey));$replay=$call('accounting_prepare','post',fn($e)=>$e->command('post',$post,$postKey));
|
||||
$ok($posted['journal_id']===$replay['journal_id']&&$native->table('gl_journals')->count()===$journalBefore+1,'posting and response recovery create one journal only');
|
||||
$lines=$native->table('gl_lines')->where('journal_id',$posted['journal_id'])->get();$ok($lines->count()===2&&(int)$lines->sum('debit_minor')===10000&&(int)$lines->sum('credit_minor')===10000,'same common writer balances100MZN in two scoped accounts');
|
||||
$report=$call('read_only','view',fn($e)=>$e->trialBalance(['from_on'=>'2026-01-01','to_on'=>'2026-12-31','page'=>1,'per_page'=>25]));$ok(is_array($report),'read-only SGF profile can obtain the same ledger trial balance');
|
||||
$native->statement('SET CONSTRAINTS ALL IMMEDIATE');$ok(true,'deferred journal and review constraints validate the qualified financial scope');
|
||||
$ok($native->table('pa_obligations')->orderBy('id')->get()->toJson()===$obligationsBefore&&$native->table('stock_movements')->orderBy('id')->get()->toJson()===$oldMovements,'ledger operation neither copies debt nor changes physical stock');
|
||||
}finally{while($native->transactionLevel()>0)$native->rollBack();}
|
||||
$ok($native->table('gl_journals')->count()===$journalBefore,'isolated ledger proof rolls back all new journal effects');
|
||||
$result=['passed'=>true,'checks'=>$checks,'book'=>'regression','native_financial_scope'=>['tenant_id'=>$nf['tenant'],'company_id'=>0,'unit_id'=>0],'sgf_ledger_scope'=>$f['scope'],'new_database'=>false,'target8085_touched'=>false];file_put_contents($work.'/ledger-result.json',json_encode($result,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/ledger-result.json']).PHP_EOL;
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Shared\Context\CompanyContext;
|
||||
use App\Modules\Purchasing\ComposedFinance;
|
||||
require __DIR__.'/sgf-ledger-proof-bootstrap.php';
|
||||
config(['sgf_single_installation.finance_enabled'=>true,'sales_ledger.synthetic_enabled'=>true,'cost_ledger.synthetic_enabled'=>true,'industrial_ledger.synthetic_enabled'=>true]);
|
||||
$finance=function($role,$action,Closure $fn)use($app,$profiles,$f){Auth::forgetGuards();Auth::shouldUse('web');$u=App\Models\User::findOrFail($profiles[$role]['user_id']);Auth::guard('web')->setUser($u);$session=app('session')->driver();$session->start();$session->put('company_context',$f['scope']+['token'=>'finance-'.$role,'module_id'=>'finance']);$r=Illuminate\Http\Request::create('/api/finance/payables','POST');$r->setLaravelSession($session);$r->setUserResolver(fn()=>$u);$app->instance('request',$r);app(CompanyContext::class)->selectFor($u->id,$f['tenant'],$f['company'],$f['unit']);try{return app(ComposedFinance::class)->run($action,$fn);}finally{app(CompanyContext::class)->clear();}};
|
||||
$source=(int)$native->table('pa_sources')->where('tenant_id',$nf['tenant'])->orderByDesc('id')->value('local_source_id');$before=$native->table('pa_obligations')->orderBy('id')->get()->toJson();$physical=$native->table('stock_movements')->orderBy('id')->get()->toJson();$native->beginTransaction();
|
||||
try{
|
||||
$position=$finance('finance_prepare','view',fn($e)=>$e->positionForSource($source));$ob=$position['id'];$revision=$position['source_revision'];
|
||||
$payInput=['obligation_id'=>$ob,'source_revision'=>$revision,'amount'=>'20.00','currency'=>'MZN','paid_on'=>'2026-11-10','fact_reference'=>'SIM-INTEGRADO-'.strtoupper(bin2hex(random_bytes(4))),'evidence_reference'=>'SIM-EVIDENCIA-'.strtoupper(bin2hex(random_bytes(4))),'reason'=>'Facto sintético de20MZN, sem execução monetária.'];$key=(string)Str::uuid();
|
||||
$payment=$finance('finance_prepare','payment-prepare',fn($e,$p)=>$p->command('payment-prepare',$payInput,$key));$retry=$finance('finance_prepare','payment-prepare',fn($e,$p)=>$p->command('payment-prepare',$payInput,$key));$ok($payment['payment_id']===$retry['payment_id'],'financial preparation recovers the original20MZN fact without duplicating it');
|
||||
$denied=false;try{$finance('finance_prepare','payment-decide',fn()=>null);}catch(Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e){$denied=$e->getStatusCode()===403;}$ok($denied,'preparation profile cannot confirm its own financial fact');
|
||||
$finance('finance_review','payment-decide',fn($e,$p)=>$p->command('payment-decide',['payment_id'=>$payment['payment_id'],'decision'=>'confirmed','evidence_reference'=>'SIM-CONF-'.strtoupper(bin2hex(random_bytes(4))),'reason'=>'Revisão distinta do facto artificial.'],(string)Str::uuid()));
|
||||
$unchanged=$finance('finance_prepare','view',fn($e)=>$e->positionForSource($source));$ok($unchanged['balance_minor']===10000,'confirmed payment alone leaves the original100MZN balance unchanged');
|
||||
$allocation=$finance('finance_prepare','allocation-prepare',fn($e,$p)=>$p->command('allocation-prepare',['payment_id'=>$payment['payment_id'],'reference'=>'SIM-AFECTACAO-'.strtoupper(bin2hex(random_bytes(4))),'allocations'=>[['obligation_id'=>$ob,'source_revision'=>$revision,'amount'=>'20.00']],'reason'=>'Proposta individual sobre a obrigação original, sem outra dívida.'],(string)Str::uuid()));
|
||||
$decision=$finance('finance_review','allocation-decide',fn($e,$p)=>$p->command('allocation-decide',['proposal_id'=>$allocation['proposal_id'],'decision'=>'confirmed','reason'=>'Afectação de20MZN conferida por outra pessoa.'],(string)Str::uuid()));
|
||||
$position=$finance('finance_prepare','view',fn($e)=>$e->positionForSource($source));$ok($position['balance_minor']===8000,'one confirmed allocation reduces the same original balance to80MZN');
|
||||
$allocationId=(int)$native->table('ps_allocations')->where('decision_id',$decision['decision_id'])->value('id');
|
||||
$postEffect=function($kind,$id)use($call){$action=$kind==='supplier_payable.recognised'?'prepare':'effect-prepare';$d=$kind==='supplier_payable.recognised'?['obligation_id'=>$id]:['effect_type'=>$kind,'source_id'=>$id];$p=$call('accounting_prepare','prepare',fn($e)=>$e->command($action,$d+['journal_on'=>'2026-11-10','period_key'=>'QA-2026-INTEGRADO','rule_key'=>$kind,'reason'=>'Origem financeira revalidada no diário comum.'],(string)Str::uuid()));$call('accounting_review','validate',fn($e)=>$e->command('validate',['proposal_id'=>$p['proposal_id'],'revision'=>$p['revision'],'decision'=>'approved','reason'=>'Revisão contabilística distinta.'],(string)Str::uuid()));return $call('accounting_prepare','post',fn($e)=>$e->command('post',['proposal_id'=>$p['proposal_id'],'revision'=>$p['revision'],'reason'=>'Aplicação única da fotografia revista.'],(string)Str::uuid()));};
|
||||
$postEffect('supplier_payable.recognised',$ob);$postEffect('allocation.confirmed',$allocationId);$ok($native->table('gl_journals')->count()===2,'recognition and financial allocation share one ledger with qualified financial scope');
|
||||
$proposal=$finance('finance_prepare','reversal-prepare',fn($e,$p)=>$p->command('reversal-prepare',['allocation_id'=>$allocationId,'source_revision'=>$revision,'reason'=>'Reversão explícita da afectação artificial, preservando o original.'],(string)Str::uuid()));
|
||||
$reverse=$finance('finance_review','reversal-confirm',fn($e,$p)=>$p->command('reversal-confirm',['proposal_id'=>$proposal['proposal_id'],'reason'=>'Conferência independente da reversão integral.'],(string)Str::uuid()));$reversalId=(int)$native->table('ps_reversals')->where('proposal_id',$proposal['proposal_id'])->value('id');
|
||||
$postEffect('allocation.reversed',$reversalId);$position=$finance('finance_prepare','view',fn($e)=>$e->positionForSource($source));$ok($position['balance_minor']===10000,'reversal restores100MZN without deleting original allocation or journals');
|
||||
$native->statement('SET CONSTRAINTS ALL IMMEDIATE');$ok($native->table('pa_obligations')->orderBy('id')->get()->toJson()===$before&&$native->table('stock_movements')->orderBy('id')->get()->toJson()===$physical,'three balanced journals retain original debt records and physical stock');
|
||||
$denied=false;try{$finance('read_only','payment-decide',fn()=>null);}catch(Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e){$denied=$e->getStatusCode()===403;}$ok($denied,'read-only host profile cannot borrow native legacy review rights');
|
||||
}finally{while($native->transactionLevel()>0)$native->rollBack();}
|
||||
$ok($native->table('ps_payments')->count()===0&&$native->table('gl_journals')->count()===0,'regression rolls back all payment and journal effects');file_put_contents($work.'/payments-ledger-result.json',json_encode(['passed'=>true,'checks'=>$checks,'native_scope_preserved'=>true,'new_database'=>false,'target8085_financial_commands'=>0],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/payments-ledger-result.json']).PHP_EOL;
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
use Illuminate\Support\Str;
|
||||
$book='8085';require __DIR__.'/sgf-ledger-proof-bootstrap.php';config(['sales_ledger.synthetic_enabled'=>true,'cost_ledger.synthetic_enabled'=>true,'industrial_ledger.synthetic_enabled'=>true,'cost_ledger.correction_enabled'=>true,'industrial_ledger.correction_enabled'=>true]);
|
||||
$scope=['tenant_id'=>1,'company_id'=>2,'unit_id'=>2];$journals=$native->table('gl_journals')->where($scope)->orderBy('id')->get()->toJson();$host->beginTransaction();$native->beginTransaction();
|
||||
try{
|
||||
$p=$call('accounting_prepare','period_prepare',fn($e)=>$e->command('period-prepare',['period_key'=>'QA-2026-INTEGRADO','reason'=>'Conferência integral das origens efectivas na mesma8085; ensaio revertido para manter o período de apresentação aberto.'],(string)Str::uuid()));
|
||||
$snapshot=json_decode($native->table('gl_period_checks')->where('id',$p['check_id'])->value('snapshot'),true);$ok($snapshot['pending']===0&&$snapshot['debit_minor']===$snapshot['credit_minor'],'all supplier, sales, costs, corrections and physical origins reconcile with zero pending facts');
|
||||
$review=['check_id'=>$p['check_id'],'revision'=>$p['revision'],'decision'=>'approved','reason'=>'Revisão independente do conjunto integral de origens e montantes QA.'];$call('accounting_review','period_validate',fn($e)=>$e->command('period-validate',$review,(string)Str::uuid()));
|
||||
$native->beginTransaction();try{$rule=(array)$native->table('gl_rules')->where($scope)->where('rule_key','sales.invoice')->first();unset($rule['id']);$rule['version']=2;$rule['reason']='Versão adicional apenas no ensaio de recusa de conferência antiga.';$native->table('gl_rules')->insert($rule);$bad=false;try{$call('accounting_prepare','period_close',fn($e)=>$e->command('period-close',['check_id'=>$p['check_id'],'revision'=>$p['revision'],'reason'=>'Não fechar com regras alteradas.'],(string)Str::uuid()));}catch(DomainException $e){$bad=str_contains($e->getMessage(),'mudaram');}$ok($bad,'rule version changed after review refuses closure and requires fresh conference');}finally{$native->rollBack();}
|
||||
$key=(string)Str::uuid();$body=['check_id'=>$p['check_id'],'revision'=>$p['revision'],'reason'=>'Fecho QA integral apenas dentro da transacção de prova, sem reabrir depois.'];$closed=$call('accounting_prepare','period_close',fn($e)=>$e->command('period-close',$body,$key));$again=$call('accounting_prepare','period_close',fn($e)=>$e->command('period-close',$body,$key));
|
||||
$ok($closed['state']==='closed'&&$closed['closure_id']===$again['closure_id'],'distinctly reviewed close and replay retain one closure');
|
||||
$ok($native->table('gl_periods')->where($scope)->where('period_key','QA-2026-INTEGRADO')->orderByDesc('version')->value('status')==='closed','closure appends actual closed period version');
|
||||
$physical=json_decode(file_get_contents($s['root'].'/single-stock-physical-input.json'),true);$receiptId=(int)$native->table('stock_movements')->where('stock_item_id',$physical['stock_item'])->where('movement_type','receipt_in')->value('id');$bad=false;
|
||||
try{$call('accounting_prepare','prepare',fn($e)=>$e->command('effect-prepare',['effect_type'=>'stock.receipt','source_id'=>$receiptId,'journal_on'=>'2026-11-10','period_key'=>'QA-2026-INTEGRADO','rule_key'=>'stock.receipt','reason'=>'Não criar captura num período fechado.'],(string)Str::uuid()));}catch(DomainException $e){$bad=str_contains($e->getMessage(),'fechado');}$ok($bad,'closed QA period refuses further capture');
|
||||
$ok($journals===$native->table('gl_journals')->where($scope)->orderBy('id')->get()->toJson(),'closure and refusals preserve every journal present before this proof');$native->statement('SET CONSTRAINTS ALL IMMEDIATE');
|
||||
}finally{while($native->transactionLevel()>0)$native->rollBack();while($host->transactionLevel()>0)$host->rollBack();}
|
||||
$ok($native->table('gl_periods')->where($scope)->where('period_key','QA-2026-INTEGRADO')->orderByDesc('version')->value('status')==='open','proof rolls back instead of reopening; actual8085 presentation period remains open');
|
||||
file_put_contents($work.'/period-composition-result.json',json_encode(['passed'=>true,'checks'=>$checks,'facts'=>$snapshot['facts'],'pending'=>0,'journals'=>count(json_decode($journals,true)),'close_executed_in_rolled_back_transaction'=>true,'period_reopened'=>false,'presentation_period_open'=>true,'browser_visual_review'=>false],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'facts'=>$snapshot['facts'],'proof'=>$work.'/period-composition-result.json']).PHP_EOL;
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
use Illuminate\Support\Str;
|
||||
require __DIR__.'/sgf-ledger-proof-bootstrap.php';
|
||||
config(['sales_ledger.synthetic_enabled'=>true,'cost_ledger.synthetic_enabled'=>true,'industrial_ledger.synthetic_enabled'=>true,'cost_ledger.correction_enabled'=>true,'industrial_ledger.correction_enabled'=>true]);
|
||||
$commercial=json_decode(file_get_contents($s['root'].'/single-commercial.json'),true);$receivables=json_decode(file_get_contents($s['root'].'/single-receivables.json'),true);
|
||||
$before=$native->table('gl_journals')->count();$native->beginTransaction();
|
||||
try{
|
||||
$posted=[];
|
||||
foreach([['sales.invoice',$commercial['invoice'],100000],['receivable.allocated',$receivables['allocations'][0],40000],['receivable.allocated',$receivables['allocations'][1],60000],['receivable.reversed',$receivables['reversal_effect'],40000]] as [$kind,$id,$amount]){
|
||||
$proposal=$call('accounting_prepare','prepare',fn($e)=>$e->command('effect-prepare',['effect_type'=>$kind,'source_id'=>$id,'journal_on'=>'2026-11-10','period_key'=>'QA-2026-INTEGRADO','rule_key'=>$kind,'reason'=>'Origem comercial explícita e actual do mesmo SGF, sem copiar dívida.'],(string)Str::uuid()));
|
||||
$call('accounting_review','validate',fn($e)=>$e->command('validate',['proposal_id'=>$proposal['proposal_id'],'revision'=>$proposal['revision'],'decision'=>'approved','reason'=>'Conferência independente da fotografia entre os dois livros.'],(string)Str::uuid()));
|
||||
$journal=$call('accounting_prepare','post',fn($e)=>$e->command('post',['proposal_id'=>$proposal['proposal_id'],'revision'=>$proposal['revision'],'reason'=>'Lançamento único após revalidar a origem de Comercial/Financeiro.'],(string)Str::uuid()));
|
||||
$rows=$native->table('gl_lines')->where('journal_id',$journal['journal_id'])->get();$ok((int)$rows->sum('debit_minor')===$amount&&(int)$rows->sum('credit_minor')===$amount,'qualified cross-book '.$kind.' amount '.$amount.' balances in the same common journal');
|
||||
if($kind==='sales.invoice')$ok($rows->count()===3,'test invoice uses exact base862.07 plus documentary tax137.93 in three lines');
|
||||
$posted[]=$journal['journal_id'];
|
||||
}
|
||||
$native->statement('SET CONSTRAINTS ALL IMMEDIATE');$ok(true,'native SQL balance constraints validate all four host-origin effects');
|
||||
$receipt=$native->table('gl_journals')->where('id',$posted[0])->first();$snap=json_decode($receipt->snapshot,true);$ok($snap['source']['document_revision']===hash('sha256',$host->table('si_invoices')->where('id',$commercial['invoice'])->value('snapshot')),'native journal retains exact immutable SGF document revision');
|
||||
$bad=false;try{$call('accounting_prepare','prepare',fn($e)=>$e->command('effect-prepare',['effect_type'=>'sales.invoice','source_id'=>999999999,'journal_on'=>'2026-11-10','period_key'=>'QA-2026-INTEGRADO','rule_key'=>'sales.invoice','reason'=>'Origem inexistente não pode ser substituída por outro documento.'],(string)Str::uuid()));}catch(Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e){$bad=$e->getStatusCode()===403;}
|
||||
$ok($bad,'unqualified or missing host invoice is refused without choosing another source');
|
||||
}finally{while($native->transactionLevel()>0)$native->rollBack();}
|
||||
$ok($native->table('gl_journals')->count()===$before,'new commercial journal effects are rolled back after regression');
|
||||
file_put_contents($work.'/sales-ledger-result.json',json_encode(['passed'=>true,'checks'=>$checks,'same_native_book'=>true,'new_database'=>false,'new_server'=>false,'host_business_writes_from_ledger'=>0],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/sales-ledger-result.json']).PHP_EOL;
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Shared\Installation\CompositionSchema;
|
||||
|
||||
$repo=dirname(__DIR__);$run=json_decode(file_get_contents('C:/laragon/data/sgf-tempo/run/modular-regression-current.json'),true);
|
||||
$s=json_decode(file_get_contents($run['root'].'/private/receipt-composition-b4093c6f6707/state.json'),true);
|
||||
if($run['id']!=='2576427d1939'||$s['tag']!=='b4093c6f6707'||!is_file($s['root'].'/integrated-result.json')||is_file($s['root'].'/demo-server.json'))throw new LogicException('Existing regression only');
|
||||
$proof=$run['root'].'/private/sgf-composition-proof-'.bin2hex(random_bytes(6));mkdir($proof,0700,true);
|
||||
foreach($s['env'] as $key=>$value)if(preg_match('/^(APP_|DB_|SGF_|SESSION_|CACHE_|QUEUE_|BCRYPT_)/',$key)){putenv($key.'='.$value);$_ENV[$key]=$value;$_SERVER[$key]=$value;}
|
||||
foreach(['APP_BASE_PATH'=>$repo,'APP_CONFIG_CACHE'=>$proof.'/config.php','APP_ROUTES_CACHE'=>$proof.'/routes.php','APP_SERVICES_CACHE'=>$proof.'/services.php','APP_PACKAGES_CACHE'=>$proof.'/packages.php','SESSION_DRIVER'=>'array','CACHE_STORE'=>'array'] as $key=>$value){putenv($key.'='.$value);$_ENV[$key]=$value;$_SERVER[$key]=$value;}
|
||||
require $repo.'/vendor/autoload.php';$app=require $repo.'/bootstrap/app.php';$app->addAbsoluteCachePathPrefix('C:');$app->useStoragePath($proof.'/storage');foreach(['logs','framework/views','framework/sessions','framework/cache/data'] as $dir)mkdir($proof.'/storage/'.$dir,0700,true);
|
||||
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();$host=DB::connection();
|
||||
if(($argv[1]??null)==='--inspect'){echo json_encode(['tables'=>array_column($host->select("SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename"),'tablename'),'a_client_columns'=>$host->getSchemaBuilder()->getColumns('a_clients')],JSON_PRETTY_PRINT).PHP_EOL;exit;}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
use Illuminate\Support\Facades\{Auth,DB,Event};
|
||||
use Illuminate\Support\Str;
|
||||
use App\Shared\Context\{CompanyContext,ModuleRecords};
|
||||
use App\Shared\Installation\CompositionSchema;
|
||||
use App\Modules\Services\ServiceWorkflow;
|
||||
use App\Modules\Workshop\WorkshopWorkflow;
|
||||
|
||||
require __DIR__.'/sgf-composition-bootstrap.php';
|
||||
$f=json_decode(file_get_contents($s['root'].'/fixture.json'),true);
|
||||
$checks=[];$ok=function($v,$name)use(&$checks,$proof){if(!$v)throw new RuntimeException($name);$checks[]=$name;file_put_contents($proof.'/progress.json',json_encode($checks));};
|
||||
$snapshot=function($db){$tables=[];foreach($db->select("SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename") as $r){$table=$r->tablename;if(!preg_match('/^[a-z_][a-z0-9_]*$/D',$table))throw new LogicException('Table');$tables[$table]=(array)$db->selectOne('SELECT count(*) rows,md5(coalesce(string_agg(row_to_json(t)::text,chr(10) ORDER BY row_to_json(t)::text),\'\')) hash FROM "'.$table.'" t');}return hash('sha256',json_encode($tables));};
|
||||
$before=$snapshot($host);$installer=new CompositionSchema;
|
||||
$result=$installer->run(false,function($db)use($app,$installer,$f,$ok){
|
||||
config(['sgf_single_installation.enabled'=>true,'sgf_single_installation.schema_sha256'=>$installer->fingerprint()]);
|
||||
$id=(int)$db->table('permission_grants')->max('id')+1000;
|
||||
foreach(['clients.view','clients.create','services.view','services.create','service_drafts.view','service_drafts.create','billing_drafts.view','billing_drafts.create','workshop.view','workshop.open','workshop.assign','workshop.authorize','workshop.record','workshop.verify'] as $right){
|
||||
$scope=['tenant_id'=>$f['tenant'],'company_id'=>$f['company'],'unit_id'=>str_starts_with($right,'clients.')||str_starts_with($right,'services.')?null:$f['unit'],'user_id'=>$f['actor'],'permission'=>$right];
|
||||
if(!$db->table('permission_grants')->where($scope)->exists())$db->table('permission_grants')->insert(['id'=>$id++]+$scope);
|
||||
}
|
||||
$user=App\Models\User::findOrFail($f['actor']);Auth::guard('web')->setUser($user);$session=app('session')->driver();$session->start();$session->put('company_context',$f['scope']+['token'=>'single-composition-proof','module_id'=>'commercial']);
|
||||
$request=Illuminate\Http\Request::create('/api/a/clients','POST');$request->setLaravelSession($session);$request->setUserResolver(fn()=>$user);$request->attributes->set('sgf_module','commercial');$app->instance('request',$request);
|
||||
$ctx=app(CompanyContext::class);$ctx->selectFor($user->id,$f['tenant'],$f['company'],$f['unit']);
|
||||
$service=app(ServiceWorkflow::class);$client=$service->save('clients',null,$user->id,['name'=>'Cliente novo do percurso único QA'],(string)Str::uuid())['id'];
|
||||
$catalogue=$service->save('services',null,$user->id,['name'=>'Diagnóstico de ensaio'],(string)Str::uuid())['id'];
|
||||
$draft=$service->save('service_drafts',null,$user->id,['client_id'=>$client,'currency'=>'MZN','lines'=>[['service_id'=>$catalogue,'description'=>'Diagnóstico QA','quantity'=>'1','unit_price'=>'100']]],(string)Str::uuid());
|
||||
$billing=$service->save('billing_drafts',null,$user->id,['source_id'=>$draft['id']],(string)Str::uuid());
|
||||
$ok($db->table('a_billing_drafts')->where('id',$billing['id'])->value('source_id')===$draft['id'],'Comercial preserves service-to-billing origin in the composed host');
|
||||
$ok(ModuleRecords::query('a_clients')->where('id',$client)->exists(),'new client explicitly belongs to Comercial');
|
||||
$request->attributes->set('sgf_module','workshop');$session->put('company_context.module_id','workshop');
|
||||
$input=['reference'=>'OT-UNICA-'.Str::uuid(),'client_id'=>$client,'equipment_identifier'=>'EQ-'.Str::uuid(),'equipment_description'=>'Equipamento fictício','request'=>'Diagnóstico no mesmo SGF','location'=>'Unidade QA','evidence'=>'Pedido novo sintético, sem alterar pendentes'];
|
||||
$workshop=app(WorkshopWorkflow::class);$blocked=false;try{$workshop->open($user->id,$input,(string)Str::uuid());}catch(Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e){$blocked=$e->getStatusCode()===403;}
|
||||
$ok($blocked,'cross-module client is refused before an explicit individual association');
|
||||
ModuleRecords::associate('clients',$client,$user->id,['tenant_id'=>$f['tenant'],'company_id'=>$f['company'],'unit_id'=>null],'workshop','Decisão individual apenas sobre este cliente novo do ensaio.',false,1);
|
||||
$key=(string)Str::uuid();$order=$workshop->open($user->id,$input,$key);$retry=$workshop->open($user->id,$input,$key);
|
||||
$ok($order['id']===$retry['id']&&$retry['replayed'],'Oficinas opens once and recovers the same order without duplication');
|
||||
$ok($db->table('wo_orders')->where('id',$order['id'])->value('client_id')===$client,'Oficinas retains the exact explicitly associated Comercial client');
|
||||
foreach(['unit.view','workshop.view'] as $right){$grant=['tenant_id'=>$f['tenant'],'company_id'=>$f['company'],'unit_id'=>$f['foreign_unit'],'user_id'=>$user->id,'permission'=>$right];if(!$db->table('permission_grants')->where($grant)->exists())$db->table('permission_grants')->insert(['id'=>$id++]+$grant);}
|
||||
$ctx->selectFor($user->id,$f['tenant'],$f['company'],$f['foreign_unit']);$foreign=$workshop->listing($user->id);$ok(!array_filter($foreign['orders'],fn($o)=>(int)$o['id']===$order['id']),'another authorized unit cannot see the new order');$ctx->selectFor($user->id,$f['tenant'],$f['company'],$f['unit']);
|
||||
$destructiveBlocked=false;try{Event::dispatch(new Illuminate\Console\Events\CommandStarting('migrate:fresh',new Symfony\Component\Console\Input\ArrayInput([]),new Symfony\Component\Console\Output\NullOutput));}catch(RuntimeException $e){$destructiveBlocked=$e->getMessage()==='SGF_COMPOSITION_DESTRUCTIVE_COMMAND_REFUSED';}
|
||||
$ok($destructiveBlocked,'destructive command is refused by the command-start guard without executing it');
|
||||
$ok($db->table('si_invoices')->count()===0&&$db->table('sr_records')->count()===0,'new drafts and workshop order do not issue invoices or create financial effects automatically');
|
||||
return ['synthetic_client'=>$client,'service_draft'=>$draft['id'],'billing_draft'=>$billing['id'],'workshop_order'=>$order['id'],'rollback'=>true];
|
||||
});
|
||||
$ok($snapshot($host)===$before,'all original table rows unchanged after rolling back the new composed flow');
|
||||
$result=['passed'=>true,'checks'=>$checks,'schema'=>$result,'target8085_touched'=>false,'qa_sequences_may_advance'=>true,'previous_schema_proof_reused'=>'sgf-composition-proof-32c029d5b44c','new_database'=>false,'new_server'=>false];file_put_contents($proof.'/flow-result.json',json_encode($result,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$proof.'/flow-result.json']).PHP_EOL;
|
||||
@@ -0,0 +1,25 @@
|
||||
import fs from 'node:fs';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
// Directed regression of the shared runtime change. Only role/context selection
|
||||
// and reads; no business workflow is executed again.
|
||||
const cookies=new Map(),checks=[];
|
||||
async function send(path,options={}){
|
||||
const response=await fetch('http://127.0.0.1:8085'+path,{redirect:'manual',...options,headers:{Cookie:[...cookies].map(([k,v])=>k+'='+v).join('; '),Accept:'application/json',...options.headers}});
|
||||
for(const raw of response.headers.getSetCookie()){const [name,value]=raw.split(';')[0].split('=');cookies.set(name,value);}
|
||||
return response;
|
||||
}
|
||||
function ok(v,name){assert(v,name);checks.push(name);}
|
||||
async function enter(role,stage){const entry=await send('/compras',{headers:{Accept:'text/html'}}),html=await entry.text();const csrf=/name="_token" value="([^"]+)"/.exec(html)?.[1];assert(csrf);const response=await send('/compras/entrar',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:new URLSearchParams({_token:csrf,role,stage})});assert.equal(response.status,302);const session=await send('/api/session');return (await session.json());}
|
||||
let session=await enter('issuer','obligations');
|
||||
const obligation=await send('/api/purchasing-obligations/2',{headers:{'X-SGF-Context':session.context.token}});const book=await obligation.json();
|
||||
ok(obligation.status===200&&book.obligations[0].principal_minor===10000&&book.obligations[0].balance_minor===10000,'existing obligation remains 100MZN with no new settlement');
|
||||
session=await enter('validator','receipts');
|
||||
const receipt=await send('/api/purchasing-receipts/orders/1',{headers:{'X-SGF-Context':session.context.token}});
|
||||
ok(receipt.status===200,'accepted purchase receipt remains readable after shared runtime update');
|
||||
const context=await send('/api/context',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-TOKEN':session.csrf,'X-SGF-Context':session.context.token},body:JSON.stringify({tenant_id:1,company_id:2,unit_id:2,module_id:'stock'})});
|
||||
ok(context.status===200,'explicit transition to authorized Stock module is preserved');session=await context.json();
|
||||
ok(session.context.stock.operations===false,'new Stock operations remain disabled without granting new rights');
|
||||
const disabled=await send('/stock/operations');ok(disabled.status===404,'installed workspace is blocked until explicit activation');
|
||||
const asset=await send('/stock-operations-workspace.mjs');ok(asset.status===200&&(await asset.text()).includes('stock-workspace'),'same server delivers the installed workspace asset');
|
||||
const result={passed:true,checks,real_http:true,business_commands:0,existing_role_selection:true,visual_review:false};if(process.argv[2])fs.writeFileSync(process.argv[2],JSON.stringify(result,null,2));console.log(JSON.stringify({passed:true,checks:checks.length,business_commands:0}));
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
use IlluminateSupportFacadesDB;
|
||||
use AppSharedInstallationCompositionSchema;
|
||||
require __DIR__.'/sgf-composition-bootstrap.php';
|
||||
$checks=[];$ok=function($v,$name)use(&$checks,$proof){if(!$v)throw new RuntimeException($name);$checks[]=$name;file_put_contents($proof.'/progress.json',json_encode($checks));};
|
||||
$snapshot=function($db){$tables=[];foreach($db->select("SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename") as $r){$table=$r->tablename;if(!preg_match('/^[a-z_][a-z0-9_]*$/D',$table))throw new LogicException('Table');$tables[$table]=(array)$db->selectOne('SELECT count(*) rows,md5(coalesce(string_agg(row_to_json(t)::text,chr(10) ORDER BY row_to_json(t)::text),\'\')) hash FROM "'.$table.'" t');}
|
||||
return hash('sha256',json_encode([$tables,$db->select("SELECT sequencename,last_value FROM pg_sequences WHERE schemaname='public' ORDER BY sequencename"),$db->select("SELECT conname,pg_get_constraintdef(oid) def FROM pg_constraint WHERE connamespace='public'::regnamespace ORDER BY conrelid,conname"),$db->select("SELECT tgrelid,tgname,pg_get_triggerdef(oid) def FROM pg_trigger WHERE NOT tgisinternal AND tgrelid IN (SELECT oid FROM pg_class WHERE relnamespace='public'::regnamespace) ORDER BY tgrelid,tgname")],JSON_THROW_ON_ERROR));};
|
||||
$before=$snapshot($host);$grants=$host->table('permission_grants')->orderBy('id')->get()->toJson();$clients=$host->table('a_clients')->orderBy('id')->get()->toJson();
|
||||
$installer=new CompositionSchema;
|
||||
$result=$installer->run(false,function($db)use($ok,$grants,$clients,$installer){
|
||||
config(['sgf_single_installation.enabled'=>true,'sgf_single_installation.schema_sha256'=>$installer->fingerprint()]);
|
||||
App\Shared\Installation\CompositionRuntime::assert();$ok(true,'composed runtime accepts only the physically verified host and schema pin');
|
||||
App\Shared\Testing\TestingDatabaseGuard::assertSafe();$ok(true,'existing domain guard accepts the verified composition');
|
||||
App\Modules\Services\ServiceInvoicing::guard();$ok(true,'synthetic invoice guard operates on the composed host');
|
||||
App\Modules\Workshop\WorkshopWorkflow::guard();App\Modules\Workshop\DirectCosts::guard();$ok(true,'workshop and direct costs guards operate on the same host');
|
||||
foreach(['wo','sr','dc','ic'] as $prefix)$db->select('SELECT sgf_'.$prefix.'_environment()');$ok(true,'four SQL environment guards point to the exact composed QA identity');
|
||||
config(['sgf_single_installation.schema_sha256'=>str_repeat('0',64)]);$refused=false;try{App\Shared\Installation\CompositionRuntime::assert();}catch(Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e){$refused=$e->getStatusCode()===503;}
|
||||
$ok($refused,'a stale schema pin blocks the runtime');config(['sgf_single_installation.schema_sha256'=>$installer->fingerprint()]);
|
||||
foreach(['a_clients','a_services','a_service_drafts','a_billing_drafts','b_internal_confirmations','doc_records','document_preview_models','service_calculation_profiles','ca_proposals','sf_proposals','si_invoices','sr_records','wo_orders','dc_calculations','ic_results','ib_budgets','wb_proposals','wc_requests'] as $table)$ok($db->getSchemaBuilder()->hasTable($table),'same host contains '.$table);
|
||||
$ok($db->table('permission_grants')->orderBy('id')->get()->toJson()===$grants,'existing grants retained without assigning new access');
|
||||
$ok($db->table('a_clients')->orderBy('id')->get()->toJson()===$clients,'existing clients retain identifiers versions payloads and associations');
|
||||
$old=(array)$db->table('permission_grants')->first();$old['id']=(int)$db->table('permission_grants')->max('id')+1000;$old['permission']='workshop.view';$old['unit_id']=$db->table('units')->value('id');
|
||||
// The expanded CHECK accepts the existing right name, but this probe is rolled back.
|
||||
$level=$db->transactionLevel();$db->beginTransaction();try{$db->table('permission_grants')->insert($old);$ok(true,'new module whitelist accepts scoped workshop right');}finally{while($db->transactionLevel()>$level)$db->rollBack();}
|
||||
$ok($db->selectOne("SELECT count(*) count FROM pg_trigger WHERE NOT tgisinternal AND tgname IN ('a_clients_guard','a_history_scope','b_confirmation_immutable','si_invoices_test_only','wo_orders_immutable','wc_requests_immutable')")->count>=4,'module constraints and append-only guards installed');
|
||||
return ['business_commands'=>0,'new_associations'=>0,'new_grants'=>0];
|
||||
});
|
||||
$ok(count($result['reused_tables'])===4,'four existing compatible A tables reused without rebuilding');
|
||||
$ok($result['committed']===false,'rehearsal rolls back instead of installing');
|
||||
$ok($snapshot($host)===$before,'all previous rows sequences constraints and triggers unchanged after rehearsal');
|
||||
$app->instance('env','production');$refused=false;try{CompositionSchema::identity();}catch(Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e){$refused=$e->getStatusCode()===403;}finally{$app->instance('env','budget-testing');}
|
||||
$ok($refused,'installer rejects production environment');
|
||||
$result=['passed'=>true,'checks'=>$checks,'schema'=>$result,'target8085_touched'=>false,'new_database'=>false,'new_server'=>false];file_put_contents($proof.'/result.json',json_encode($result,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$proof.'/result.json']).PHP_EOL;
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
use Illuminate\Support\Str;use App\Modules\Services\{ServiceDocuments,ServiceInvoicing,ServiceReceivables};
|
||||
require __DIR__.'/sgf-service-documents-proof-bootstrap.php';$host->beginTransaction();$native->beginTransaction();config(['sales_ledger.synthetic_enabled'=>true]);$before=$host->table('sd_outputs')->orderBy('id')->get()->toJson();
|
||||
try{
|
||||
$actor=$select('commercial_prepare');$i=$inputs['credit_invoice'];$w=app(ServiceInvoicing::class);$r=$w->review($i['draft_id'],$i['series_id'],$i['rule_id'],$actor);$invoice=$w->issue($i['draft_id'],$actor,['series_id'=>$i['series_id'],'rule_id'=>$i['rule_id'],'review_hash'=>$r['review_hash']],(string)Str::uuid())['id'];$sr=app(ServiceReceivables::class);
|
||||
$actor=$select('finance_prepare');$ob=$sr->propose($actor,['kind'=>'obligation','invoice_id'=>$invoice,'currency'=>'MZN','source'=>'SIM-CANCEL','external_id'=>'OB-'.$invoice,'revision'=>0],(string)Str::uuid())['id'];$actor=$select('finance_review');$sr->decide($ob,$actor,['version'=>1,'revision'=>0,'decision'=>'approved','reason'=>'Preparação de origem para ensaio de anulação documental.'],(string)Str::uuid());
|
||||
$actor=$select('finance_prepare');$rc=$sr->propose($actor,['kind'=>'receipt','invoice_id'=>$invoice,'obligation_id'=>$ob,'currency'=>'MZN','source'=>'SIM-CANCEL','external_id'=>'RC-'.$invoice,'amount'=>'400.00','date'=>'2026-11-10','evidence'=>'Pagamento fictício para reversão documental em rollback.','revision'=>1],(string)Str::uuid())['id'];$actor=$select('finance_review');$sr->decide($rc,$actor,['version'=>1,'revision'=>1,'decision'=>'approved','reason'=>'Recebimento conferido distintamente para anulação.'],(string)Str::uuid());$actor=$select('finance_prepare');$alloc=$sr->allocate($rc,$actor,['amount'=>'400.00','version'=>1,'revision'=>2],(string)Str::uuid())['id'];
|
||||
$receiptKey=(string)Str::uuid();$command('commercial_prepare','receipt','prepare',$receiptKey,0,['invoice_id'=>$invoice,'allocation_id'=>$alloc]);$command('commercial_review','receipt','review',$receiptKey,1,['decision'=>'approved']);[$receipt]=$command('commercial_prepare','receipt','issue',$receiptKey,2);
|
||||
$creditKey=(string)Str::uuid();$command('finance_prepare','credit_note','prepare',$creditKey,0,['invoice_id'=>$invoice,'amount'=>'100.00','explanation'=>'Crédito QA100 para comprovar reversão ligada ao original.']);$command('finance_review','credit_note','review',$creditKey,1,['decision'=>'approved']);[$credit]=$command('finance_review','credit_note','issue',$creditKey,2);
|
||||
$frOutput=$host->table('sd_outputs')->where($f['scope'])->where('kind','invoice_receipt')->firstOrFail();
|
||||
$post=function($kind,$id)use($call,$native){$p=$call('accounting_prepare','prepare',fn($e)=>$e->command('effect-prepare',['effect_type'=>$kind,'source_id'=>$id,'journal_on'=>'2026-11-10','period_key'=>'QA-2026-INTEGRADO','rule_key'=>$kind,'reason'=>'Origem ou reversão documental individual de teste.'],(string)Str::uuid()));$call('accounting_review','validate',fn($e)=>$e->command('validate',['proposal_id'=>$p['proposal_id'],'revision'=>$p['revision'],'decision'=>'approved','reason'=>'Revisão distinta da origem documental e da reversão.'],(string)Str::uuid()));return $call('accounting_prepare','post',fn($e)=>$e->command('post',['proposal_id'=>$p['proposal_id'],'revision'=>$p['revision'],'reason'=>'Aplicação única da origem ou reversão documental QA.'],(string)Str::uuid()));};
|
||||
foreach([['sales.invoice',$invoice],['receivable.allocated',$alloc],['receivable.reduced',$credit['effect_id']],['sales.invoice',$frOutput->invoice_id],['receivable.allocated',$frOutput->effect_id]] as [$kind,$id])$post($kind,$id);
|
||||
$pdfs=$host->table('sd_outputs')->orderBy('id')->get()->toJson();
|
||||
foreach([['credit_note',$creditKey,'receivable.reduction_reversed'],['receipt',$receiptKey,'receivable.reversed'],['invoice_receipt',$frOutput->document_key,'receivable.reversed']] as [$kind,$key,$effectKind]){
|
||||
[$prepared]=$command('finance_prepare',$kind,'cancel_prepare',$key,3);$ok($prepared['state']==='prepared','cancellation preparation preserves issued '.$kind);
|
||||
$reject(fn()=>$command('finance_prepare',$kind,'cancel_review',$key,3),403,'same preparer cannot approve cancellation '.$kind);
|
||||
[$cancelled,$cut]=$command('finance_review',$kind,'cancel_review',$key,3);$actor=$select('finance_review');$again=app(ServiceDocuments::class)->confirm($actor,$cut['preview']);$ok($again['replayed']&&$again['effect_id']===$cancelled['effect_id'],'cancellation result replays without duplicate '.$kind);$post($effectKind,$cancelled['effect_id']);
|
||||
$reject(fn()=>$command('finance_prepare',$kind,'cancel_prepare',$key,3),409,'cancelled document cannot be cancelled twice '.$kind);
|
||||
}
|
||||
$actor=$select('finance_review');$position=$sr->position($ob,$actor);$ok($position['balance']==='1000.00'&&$position['reduced']==='0.00'&&$position['allocated']==='0.00'&&$position['available']==='400.00','reversals restore1000 debt and leave400 unallocated without refund');
|
||||
$frOb=$host->table('sr_records')->where(['invoice_id'=>$frOutput->invoice_id,'kind'=>'obligation'])->value('id');$position=$sr->position($frOb,$actor);$ok($position['balance']==='1000.00'&&$position['available']==='1000.00','invoice-receipt cancellation preserves invoice and unallocated receipt');
|
||||
$ok($pdfs===$host->table('sd_outputs')->orderBy('id')->get()->toJson(),'all original PDFs and snapshots remain byte-identical after cancellation');
|
||||
$native->statement('SET CONSTRAINTS ALL IMMEDIATE');$host->statement('SET CONSTRAINTS ALL IMMEDIATE');$ok(true,'financial and journal constraints validate all linked reversals');
|
||||
}finally{while($native->transactionLevel()>0)$native->rollBack();while($host->transactionLevel()>0)$host->rollBack();}
|
||||
$ok($before===$host->table('sd_outputs')->orderBy('id')->get()->toJson(),'proof rolls back all cancellation fixtures and effects');file_put_contents($work.'/document-cancellation-result.json',json_encode(['passed'=>true,'checks'=>$checks,'rolled_back'=>true,'money_refunded'=>false,'visual_review'=>false],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/document-cancellation-result.json']).PHP_EOL;
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
$book=$argv[1]??'regression';require __DIR__.'/sgf-single-bootstrap.php';$host->transaction(function()use($host){
|
||||
$record=$host->selectOne("SELECT pg_get_functiondef('sgf_sr_record_guard()'::regprocedure) d")->d;if(!str_contains($record,'reverse_reduction')){$new=str_replace("'reverse_receipt','reduction','due'","'reverse_receipt','reduction','reverse_reduction','due'",$record);if($new===$record)throw new LogicException('Unknown correction record guard');$host->unprepared($new);}
|
||||
$def=$host->selectOne("SELECT pg_get_constraintdef(oid) d FROM pg_constraint WHERE conrelid='sr_effects'::regclass AND conname='sr_effects_kind_check'")->d;
|
||||
if(!str_contains($def,'reverse_reduction')){$new=str_replace("'reduction'::text","'reduction'::text, 'reverse_reduction'::text",$def);if($new===$def)throw new LogicException('Unknown effect contract');$host->statement('ALTER TABLE sr_effects DROP CONSTRAINT sr_effects_kind_check');$host->statement('ALTER TABLE sr_effects ADD CONSTRAINT sr_effects_kind_check '.$new);}
|
||||
$body=$host->selectOne("SELECT pg_get_functiondef('sgf_sr_effect_guard()'::regprocedure) d")->d;
|
||||
if(!str_contains($body,'reverse_reduction')){$body=str_replace(["'reverse_allocation','reverse_reconcile'","WHEN kind='reverse_allocation' THEN -amount","THEN 'allocate' ELSE 'reconcile' END"],["'reverse_allocation','reverse_reconcile','reverse_reduction'","WHEN kind IN ('reverse_allocation','reverse_reduction') THEN -amount","THEN 'allocate' WHEN NEW.kind='reverse_reduction' THEN 'reduction' ELSE 'reconcile' END"],$body);$host->unprepared($body);}
|
||||
if(!$host->getSchemaBuilder()->hasTable('sd_cancellations')){
|
||||
$host->unprepared("CREATE TABLE sd_cancellations(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,cancellation_key uuid NOT NULL,version int NOT NULL CHECK(version>0),output_id bigint NOT NULL REFERENCES sd_outputs(id),state text NOT NULL CHECK(state IN('prepared','cancelled')),actor_id bigint NOT NULL REFERENCES users(id),correction_id bigint NOT NULL REFERENCES sr_records(id),effect_id bigint REFERENCES sr_effects(id),snapshot jsonb NOT NULL,reason text NOT NULL CHECK(length(reason)>=10),created_at timestamptz NOT NULL DEFAULT now(),UNIQUE(tenant_id,company_id,unit_id,cancellation_key,version))");
|
||||
$host->statement("CREATE UNIQUE INDEX sd_one_cancellation ON sd_cancellations(output_id) WHERE state='cancelled'");
|
||||
$host->statement('CREATE TRIGGER sd_cancellation_immutable BEFORE UPDATE OR DELETE ON sd_cancellations FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit()');$host->statement('CREATE TRIGGER sd_cancellation_test BEFORE INSERT ON sd_cancellations FOR EACH ROW EXECUTE FUNCTION sgf_wo_test_only()');$host->statement('REVOKE ALL ON sd_cancellations FROM PUBLIC');$host->statement('REVOKE ALL ON SEQUENCE sd_cancellations_id_seq FROM PUBLIC');
|
||||
}
|
||||
$def=$host->selectOne("SELECT pg_get_constraintdef(oid) d FROM pg_constraint WHERE conrelid='sd_cancellations'::regclass AND conname='sd_cancellations_version_check'")->d;if(!str_contains($def,'version > 0')){$host->statement('ALTER TABLE sd_cancellations DROP CONSTRAINT sd_cancellations_version_check');$host->statement('ALTER TABLE sd_cancellations ADD CONSTRAINT sd_cancellations_version_check CHECK(version>0)');}
|
||||
$index=$host->selectOne("SELECT pg_get_indexdef('sd_one_cancellation'::regclass) d")->d;if(!str_contains($index,'state')){$host->statement('DROP INDEX sd_one_cancellation');$host->statement("CREATE UNIQUE INDEX sd_one_cancellation ON sd_cancellations(output_id) WHERE state='cancelled'");}
|
||||
});
|
||||
$native->transaction(function()use($native,$f){$r=$native->table('gl_rules')->where($f['scope'])->where('rule_key','receivable.reduced')->firstOrFail();if(!$native->table('gl_rules')->where($f['scope'])->where('rule_key','receivable.reduction_reversed')->exists()){$v=(array)$r;unset($v['id']);$v['rule_key']=$v['effect_type']='receivable.reduction_reversed';$v['debit_account_id']=$r->credit_account_id;$v['credit_account_id']=$r->debit_account_id;$v['reason']='Reversão técnica QA ligada ao crédito original, sem reabertura.';$native->table('gl_rules')->insert($v);}});echo json_encode(['passed'=>true,'book'=>$book,'old_effects_not_modified'=>true]).PHP_EOL;
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
// Included only while the existing selected-book period proof holds its rollback-only closure.
|
||||
$profiles=json_decode(file_get_contents($s['root'].'/single-profiles.json'),true)['profiles'];$u=App\Models\User::findOrFail($profiles['finance_review']['user_id']);Illuminate\Support\Facades\Auth::forgetGuards();Illuminate\Support\Facades\Auth::shouldUse('web');Illuminate\Support\Facades\Auth::guard('web')->setUser($u);$r=Illuminate\Http\Request::create('/api/service-documents/preview','POST');$r->attributes->set('sgf_module','commercial');$r->setUserResolver(fn()=>$u);$session=app('session')->driver();$session->start();$session->put('company_context',$f['scope']+['module_id'=>'commercial','token'=>'closed-documents-proof']);$r->setLaravelSession($session);$app->instance('request',$r);app(App\Shared\Context\CompanyContext::class)->selectFor($u->id,1,2,2);
|
||||
config(['service_documents.enabled'=>true,'service_documents.invoice_receipt_enabled'=>true,'service_documents.inputs_sha256'=>hash_file('sha256',$s['root'].'/single-document-inputs.json')]);$output=$host->table('sd_outputs')->where('kind','credit_note')->firstOrFail();$bad=false;try{app(App\Modules\Services\ServiceDocuments::class)->preview($u->id,['kind'=>'credit_note','action'=>'cancel_review','document_key'=>$output->document_key,'version'=>3,'data'=>[],'reason'=>'Recusar anulação em período QA efectivamente fechado dentro da prova.'],(string)Illuminate\Support\Str::uuid());}catch(Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e){$bad=$e->getStatusCode()===409&&str_contains($e->getMessage(),'período QA aberto');}$ok($bad,'document cancellation refuses an actually closed QA period without reopening');
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
use Illuminate\Support\Facades\Auth;use Illuminate\Support\Str;use App\Shared\Context\{CompanyContext,ModuleRecords};
|
||||
$book=$argv[1]??'regression';require __DIR__.'/sgf-single-bootstrap.php';$file=$s['root'].'/single-document-inputs.json';if(is_file($file)){echo json_encode(['passed'=>true,'replayed'=>true,'commands'=>0]).PHP_EOL;exit;}
|
||||
$profiles=json_decode(file_get_contents($s['root'].'/single-profiles.json'),true)['profiles'];$old=json_decode(file_get_contents($s['root'].'/single-commercial.json'),true);$actor=App\Models\User::findOrFail($profiles['commercial_prepare']['user_id']);$reviewer=App\Models\User::findOrFail($profiles['commercial_review']['user_id']);
|
||||
config(['sgf_single_installation.enabled'=>true,'sgf_single_installation.schema_sha256'=>$host->table('sgf_composition_schema')->value('source_hash')]);Auth::guard('web')->setUser($actor);$r=Illuminate\Http\Request::create('/api/a/billing_drafts','POST');$r->attributes->set('sgf_module','commercial');$r->setUserResolver(fn()=>$actor);$session=app('session')->driver();$session->start();$session->put('company_context',$f['scope']+['module_id'=>'commercial','token'=>'document-inputs']);$r->setLaravelSession($session);$app->instance('request',$r);app(CompanyContext::class)->selectFor($actor->id,1,2,2);
|
||||
require_once base_path('tests/Support/IdentityFixture.php');require_once base_path('tests/Support/InvoiceFixture.php');
|
||||
$before=$host->table('si_invoices')->orderBy('id')->get()->toJson();$grants=$host->table('permission_grants')->orderBy('id')->get()->toJson();
|
||||
$result=$host->transaction(function()use($old,$actor,$reviewer,$f,$host){$base=$old+['tenant'=>$f['tenant'],'company'=>$f['company'],'unit'=>$f['unit'],'manager'=>$actor,'reader'=>$reviewer];$out=[];
|
||||
foreach(['credit_invoice','invoice_receipt'] as $name){$created=Tests\Support\InvoiceFixture::additionalDraft($base,'862.07');$s=app(App\Modules\Services\ServiceInvoicing::class);$series=$s->propose($actor->id,['kind'=>'series','identifier'=>'TEST-DOC-'.($name==='credit_invoice'?'NC':'FR').'-2026','data'=>['valid_from'=>$created['start'],'code'=>'TEST-DOC-'.($name==='credit_invoice'?'NC':'FR').'-2026','first_number'=>1,'state'=>'open','evidence'=>'Nova série exclusivamente QA para o bloco documental autorizado.']]);app(CompanyContext::class)->selectFor($reviewer->id,1,2,2);$s->decide($series['id'],$reviewer->id,['version'=>1,'decision'=>'approved','reason'=>'Revisão distinta da nova série QA, sem reiniciar numeração existente.']);app(CompanyContext::class)->selectFor($actor->id,1,2,2);
|
||||
foreach(['service_drafts'=>'source','billing_drafts'=>'draft'] as $area=>$key){$row=$host->table('a_'.$area)->where('id',$created[$key])->firstOrFail();foreach(['finance','accounting'] as $module)ModuleRecords::associate($area,$row->id,$actor->id,$f['scope'],$module,'Conferência individual desta nova origem QA para documentos e mesmo diário.',false,(int)$row->version);}
|
||||
$out[$name]=['draft_id'=>$created['draft'],'source_id'=>$created['source'],'series_id'=>$series['id'],'rule_id'=>$created['rule'],'client_id'=>$created['client'],'total'=>'1000.00','base'=>'862.07','document_tax'=>'137.93'];
|
||||
}return $out;
|
||||
});
|
||||
if($before!==$host->table('si_invoices')->orderBy('id')->get()->toJson()||$grants!==$host->table('permission_grants')->orderBy('id')->get()->toJson())throw new LogicException('Original invoice or rights changed');
|
||||
file_put_contents($file,json_encode($result,JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'book'=>$book,'new_prepared_sources'=>2,'invoices_issued'=>0,'rights_changed'=>false]).PHP_EOL;
|
||||
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
$book=$argv[1]??'regression';require __DIR__.'/sgf-single-bootstrap.php';$native->transaction(function()use($native,$f){$s=$f['scope'];$id=$native->table('gl_accounts')->where($s)->where('code','QA-REDUCAO-CLIENTE')->value('id');if(!$id)$id=$native->table('gl_accounts')->insertGetId($s+['code'=>'QA-REDUCAO-CLIENTE','label'=>'Redução nominal de cliente exclusivamente QA','currency'=>'MZN','is_movement'=>true]);if(!$native->table('gl_rules')->where($s)->where('rule_key','receivable.reduced')->exists())$native->table('gl_rules')->insert($s+['rule_key'=>'receivable.reduced','effect_type'=>'receivable.reduced','version'=>1,'policy'=>'QA-SALES-JOURNAL-v1','currency'=>'MZN','valid_from'=>'2026-01-01','valid_to'=>'2026-12-31','debit_account_id'=>$id,'credit_account_id'=>$native->table('gl_accounts')->where($s)->where('code','QA-CLIENTES')->value('id'),'created_by'=>4,'reason'=>'Crédito nominal100MZN de nova origem QA autorizado no bloco documental.','created_at'=>now()]);});echo json_encode(['passed'=>true,'book'=>$book,'journals_unchanged'=>true]).PHP_EOL;
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
$book='8085';require __DIR__.'/sgf-single-bootstrap.php';if(is_file($s['root'].'/document-math-backup-pointer.json'))throw new LogicException('Preserve existing IVA baseline');if($native->table('gl_journals')->count()!==27)throw new LogicException('Expected27 accepted journals');
|
||||
foreach(['host'=>['database'=>$s['env']['DB_DATABASE'],'username'=>$s['env']['DB_USERNAME'],'password'=>$s['env']['DB_PASSWORD']],'native'=>$s['nativeConnection']]as$name=>$c){$dump=new Symfony\Component\Process\Process(['C:/Program Files/PostgreSQL/17/bin/pg_dump.exe','-h','127.0.0.1','-p','55439','-U',$c['username'],'-d',$c['database'],'--format=custom','--no-owner','--no-privileges','--file',$work.'/'.$name.'-before-document-math.dump'],$repo,['PGPASSWORD'=>$c['password']]);$dump->setTimeout(120);$dump->mustRun();}
|
||||
$before=[];foreach(['si_invoices','sd_outputs','sd_versions','sr_records','sr_effects','cash_events','sf_proposals','sf_decisions','sf_activation_events','service_calculation_results','a_clients','fx_versions','fx_commands','fr_events','fr_calculations','fr_commands','br_statements','br_events','br_commands','an_criteria','an_results','an_commands','permission_grants']as$t)$before['host'][$t]=$host->table($t)->orderBy('id')->get()->toJson();foreach(['stock_movements','gl_journals','gl_periods']as$t)$before['native'][$t]=$native->table($t)->orderBy('id')->get()->toJson();$before['binding']=hash_file('sha256',$s['root'].'/binding.json');file_put_contents($work.'/preservation-private.json',json_encode($before));file_put_contents($s['root'].'/document-math-backup-pointer.json',json_encode(['root'=>$work]));echo json_encode(['passed'=>true,'private_backup'=>$work,'database_writes'=>0]).PHP_EOL;
|
||||
@@ -0,0 +1,4 @@
|
||||
import fs from 'node:fs';import {spawn,spawnSync} from 'node:child_process';import assert from 'node:assert/strict';
|
||||
const php='C:/laragon/tools/sgf-tempo/php-8.4.27/php.exe',script='scripts/sgf-document-math-concurrency.php',run=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')),dir=run.root+'/private/receipt-composition-b4093c6f6707/document-math-concurrency';
|
||||
const call=mode=>{const r=spawnSync(php,[script,mode],{encoding:'utf8',windowsHide:true});assert.equal(r.status,0,r.stdout+r.stderr);assert.match(r.stdout,/"passed":true/);return r.stdout;};if(fs.existsSync(dir+'/result.json')){assert(JSON.parse(fs.readFileSync(dir+'/result.json')).passed);console.log(JSON.stringify({passed:true,reused:true,proof:dir+'/result.json'}));process.exit();}
|
||||
call('prepare');const first=spawn(php,[script,'first'],{stdio:'pipe',windowsHide:true});let out='';first.stdout.on('data',d=>out+=d);first.stderr.on('data',d=>out+=d);const ended=new Promise(r=>first.once('exit',r));try{const deadline=Date.now()+10000;while(!fs.existsSync(dir+'/locked')&&Date.now()<deadline)await new Promise(r=>setTimeout(r,40));assert(fs.existsSync(dir+'/locked'));const second=spawn(php,[script,'second'],{stdio:'pipe',windowsHide:true});let output='';second.stdout.on('data',d=>output+=d);second.stderr.on('data',d=>output+=d);assert.equal(await new Promise(r=>second.once('exit',r)),0,output);assert.match(output,/"passed":true/);const r=JSON.parse(fs.readFileSync(dir+'/second.json'));assert.equal(r.status,503);assert.equal(r.retry_after,'5');assert(r.elapsed_seconds>=4.5&&r.elapsed_seconds<12);}finally{fs.writeFileSync(dir+'/release','Release existing regression lock only');assert.equal(await ended,0,out);}console.log(call('recover').trim());
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
require __DIR__.'/sgf-document-math-proof-bootstrap.php';$dir=$s['root'].'/document-math-concurrency';if(!is_dir($dir))mkdir($dir,0700,true);$file=$dir.'/progress.json';$mode=$argv[1]??'';
|
||||
if($mode==='prepare'){if(is_file($file))throw new LogicException('Reuse prior preparation');$p=$rulePreview(13,$documentInput('gross_included',[$line('CONCURRENT','100')]));file_put_contents($file,json_encode(['cut'=>$p['preview'],'before'=>$host->table('fr_calculations')->count()]));}
|
||||
elseif($mode==='first'){$p=json_decode(file_get_contents($file),true);$selectRule(13);$host->beginTransaction();try{$out=$rules->confirm(13,$p['cut']);file_put_contents($dir.'/locked','Company lock held');$end=microtime(true)+25;while(!is_file($dir.'/release')&µtime(true)<$end)usleep(50000);if(!is_file($dir.'/release'))throw new LogicException('Release missing');$host->commit();file_put_contents($dir.'/first.json',json_encode($out));}finally{while($host->transactionLevel()>0)$host->rollBack();}}
|
||||
elseif($mode==='second'){$p=json_decode(file_get_contents($file),true);$selectRule(13);request()->request->replace(['preview'=>$p['cut']]);$start=microtime(true);$r=app(App\Modules\Services\Http\FiscalRulesController::class)->confirm(request(),$rules);file_put_contents($dir.'/second.json',json_encode(['status'=>$r->getStatusCode(),'retry_after'=>$r->headers->get('Retry-After'),'elapsed_seconds'=>microtime(true)-$start]));}
|
||||
elseif($mode==='recover'){$p=json_decode(file_get_contents($file),true);$selectRule(13);$out=$rules->confirm(13,$p['cut']);$first=json_decode(file_get_contents($dir.'/first.json'),true);$ok($out['replayed']&&$out['id']===$first['id']&&$host->table('fr_calculations')->count()===$p['before']+1,'same key recovers one calculation after lock release');file_put_contents($dir.'/result.json',json_encode(['passed'=>true,'during_lock'=>json_decode(file_get_contents($dir.'/second.json'),true),'after_release'=>200,'replayed'=>true,'one_calculation'=>true,'regression_only'=>true],JSON_PRETTY_PRINT));}
|
||||
else throw new LogicException('Unknown mode');echo json_encode(['passed'=>true,'phase'=>$mode,'proof'=>$dir.'/result.json']).PHP_EOL;
|
||||
@@ -0,0 +1,3 @@
|
||||
// Actual client forwarded to actual8085 HTTP; confirmation prohibited.
|
||||
import fs from 'node:fs';import assert from 'node:assert/strict';import {parsePage,installDom} from './purchasing-workspace-dom.mjs';import {session} from './sgf-single-http.mjs';
|
||||
const nativeFetch=globalThis.fetch,s=await session('sgf:finance_prepare','accounting');const r=await s.get('/fiscal/rules',{headers:{Accept:'text/html'}});assert.equal(r.status,200);const document=parsePage(await r.text());installDom(document);document.getElementById=id=>document.querySelector('#'+id);globalThis.window={addEventListener(){}};const $=id=>document.getElementById(id);let previews=0,confirms=0;const checks=[],calls=[];const ok=(v,n)=>{assert(v,n);checks.push(n);};globalThis.fetch=async(url,options={})=>{if(url.startsWith('http:'))return nativeFetch(url,options);calls.push(url);if(options.method==='POST'){if(url.endsWith('/confirm')){confirms++;throw Error('No confirmations in UI proof');}assert(url.endsWith('/preview'));previews++;const a=await s.command(url,JSON.parse(options.body),options.headers['Idempotency-Key']);return{status:a.status,json:async()=>a.value};}const a=await s.get(url);return{status:a.status,json:()=>a.json()};};await import('../public/sgf-fiscal-rules.mjs?document-dom-http');const wait=async f=>{const end=Date.now()+15000;while(!f()&&Date.now()<end)await new Promise(r=>setTimeout(r,25));assert(f(),$('message').textContent);};await wait(()=>$('events').children.length===2);ok($('selection').textContent.includes('Nenhuma versão'),'no rule selected automatically');const row=[...$('events').children].find(row=>row.textContent.includes('2026-11-30'));assert(row);await row.querySelector('button').onclick();ok($('selection').textContent.includes('v1'),'explicit existing version1');$('action').value='calculate_document';$('action').onchange();ok($('documentLines').children.length===1&&$('documentMode').value==='','blank line and explicit mode');$('documentMode').value='gross_included';$('documentOn').value='2026-11-10';$('documentCurrency').value='MZN';$('doc-reference-1').value='G100';$('doc-quantity-1').value='1';$('doc-price-1').value='100';$('reason').value='Conferência QA de modalidade incluída e diferenças exactas, sem confirmar.';$('operation').oninput();await $('operation').onsubmit({preventDefault(){}});ok(previews===1&&confirms===0,'one real rollback preview zero confirmations');ok($('impact').textContent.includes('86.21')&&$('impact').textContent.includes('13.79')&&$('impact').textContent.includes('100.00'),'documentary amounts shown before confirmation');ok($('impact').textContent.includes('2500/29')&&$('impact').textContent.includes('400/29')&&$('impact').textContent.includes('9/2900'),'exact fractions and differences shown');ok($('fields').disabled&&$('filters').disabled,'preview locks form and catalogue');$('cancel').onclick();ok(!$('fields').disabled&&$('doc-price-1').value==='100','cancel preserves line');$('addDocumentLine').onclick();ok($('documentLines').children.length===2,'add second line');$('documentLines').children[1].querySelector('button').onclick();ok($('documentLines').children.length===1&&$('doc-price-1').value==='100','remove explicit line preserves other input');$('size').value='100';$('apply').onclick();await wait(()=>$('filters').disabled===false&&calls.some(c=>c.includes('per_page=100')));ok($('doc-price-1').value==='100'&&$('documentMode').value==='gross_included','pagination preserves unsubmitted lines and mode');$('view').value='calculations';$('apply').onclick();await wait(()=>$('events').children.length===5);await $('events').children[0].querySelector('button').onclick();ok($('detail').textContent.includes('9/2900')&&$('detail').textContent.includes('current_evaluation'),'frozen result and current evaluation readable');ok($('documentDownload').textContent.includes('Descarregar mapa interno'),'historical line report link');ok($('inputs').textContent.includes('Sem regra escolhida'),'historical record does not automatically choose its rule');const run=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')),root=run.root+'/private/receipt-composition-cc50a276c10c',p=JSON.parse(fs.readFileSync(root+'/document-math-http-20261110.json'));$('referenceKey').value=p.operations.included100.key;await $('recovery').onsubmit({preventDefault(){}});ok($('receipt').textContent.includes('Nenhuma operação repetida'),'own receipt recovered via GET');const after=await s.get('/api/fiscal-rules?view=calculations');ok((await after.json()).records.total===5&&confirms===0,'five saved calculations preserved');const file=root+'/document-math-dom-http-20261110.json';fs.writeFileSync(file,JSON.stringify({passed:true,checks,preview_posts:previews,confirmation_posts:confirms,real_http_forwarded:true,browser_visual_review:false},null,2));console.log(JSON.stringify({passed:true,checks:checks.length,proof:file}));
|
||||
@@ -0,0 +1,6 @@
|
||||
import fs from 'node:fs';import assert from 'node:assert/strict';import crypto from 'node:crypto';import {session} from './sgf-single-http.mjs';
|
||||
const run=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')),root=run.root+'/private/receipt-composition-cc50a276c10c',file=root+'/document-math-http-20261110.json';let p=fs.existsSync(file)?JSON.parse(fs.readFileSync(file)):{operations:{},checks:[]};if(p.passed){console.log(JSON.stringify({passed:true,reused:true,proof:file}));process.exit();}const save=()=>fs.writeFileSync(file,JSON.stringify(p,null,2));const ok=(v,n)=>{assert(v,n);if(!p.checks.includes(n))p.checks.push(n);save();};
|
||||
const s=await session('sgf:finance_prepare','accounting'),read=await session('sgf:read_only','accounting');let response=await s.get('/api/fiscal-rules?per_page=100');assert.equal(response.status,200);const catalogue=await response.json();ok(catalogue.document_enabled===true,'approved line feature on selected8085');const rule=catalogue.records.data.find(r=>r.rule_key==='QA-RATE-TRANSVERSAL'&&r.rule_version===1&&r.state==='reviewed');assert(rule,'explicit existing approved version1');const line=(reference,unit_price)=>({reference,unit_price,quantity:'1'});const input=(mode,lines)=>({action:'calculate_document',revision:0,reason:'Conferência individual QA por linhas e modalidade aprovada, sem documento ou efeito financeiro.',data:{rule_event_id:rule.id,date:'2026-11-10',currency:'MZN',price_mode:mode,lines}});
|
||||
const command=async(name,body)=>{if(p.operations[name]?.result)return p.operations[name].result;let op=p.operations[name];if(!op){op=p.operations[name]={key:crypto.randomUUID(),input:body};save();}if(!op.cut){const r=await s.command('/api/fiscal-rules/preview',op.input,op.key);assert.equal(r.status,200,JSON.stringify(r.value));op.cut=r.value.preview;op.impact=r.value.impact;save();}const r=await s.command('/api/fiscal-rules/confirm',{preview:op.cut});assert.equal(r.status,200,JSON.stringify(r.value));op.result=r.value;save();const again=await s.command('/api/fiscal-rules/confirm',{preview:op.cut});ok(again.status===200&&again.value.replayed&&again.value.id===r.value.id,'repeat '+name+' without duplicate');return r.value;};
|
||||
for(const [name,mode,lines,base,tax,total]of [['net','net_added',[line('N400','400'),line('N600','600')],'1000.00','160.00','1160.00'],['included','gross_included',[line('G464','464'),line('G696','696')],'1000.00','160.00','1160.00'],['included100','gross_included',[line('G100','100')],'86.21','13.79','100.00']]){const result=await command(name,input(mode,lines));response=await read.get('/api/fiscal-rules/records/'+result.id+'?type=calculation');assert.equal(response.status,200);const detail=await response.json(),r=detail.result;ok(r.base_document===base&&r.tax_document===tax&&r.total===total,name+' actual documented amounts');ok(detail.current_evaluation.state==='determined_qa'&&detail.current_evaluation.total===total,name+' current source revalidation');if(name==='included100'){ok(r.lines[0].base_exact_fraction==='2500/29'&&r.lines[0].tax_exact_fraction==='400/29','exact included fractions retained');ok(r.base_difference_fraction==='9/2900'&&r.tax_difference_fraction==='9/2900','both rational differences retained');}response=await read.get('/api/fiscal-rules/documents/'+result.id+'/report');ok(response.status===200&&response.headers.get('content-type').includes('text/csv'),name+' line report authorized');const bytes=Buffer.from(await response.arrayBuffer()),text=bytes.toString();ok(crypto.createHash('sha256').update(bytes).digest('hex')===response.headers.get('x-content-sha256'),name+' report exact checksum');ok(text.includes(base+';'+tax+';'+total)&&text.includes("'"+lines[0].reference)&&text.includes('2026-11-10'),name+' report amounts safe references and date');}
|
||||
response=await read.get('/api/fiscal-rules?view=calculations&per_page=100');ok((await response.json()).records.total===5,'only three new calculations plus two originals');response=await read.get('/api/fiscal-rules?view=history&rule_key=QA-RATE-TRANSVERSAL');ok((await response.json()).records.total===4,'four rule events unchanged');let denied=await read.command('/api/fiscal-rules/preview',input('gross_included',[line('X','100')]));ok(denied.status===403,'read-only calculation writer refused');const bad=input('gross_included',[line('X','100')]);bad.data.rate='0.99';denied=await s.command('/api/fiscal-rules/preview',bad);ok(denied.status===422,'client rate injection refused by actual HTTP');const wrong=await session('sgf:finance_prepare','finance');response=await wrong.get('/api/fiscal-rules');ok([403,409].includes(response.status),'other module refused');response=await read.get('/api/fiscal-rules?tenant_id=2');ok(response.status===422,'tenant override refused');const fresh=await session('sgf:finance_prepare','accounting');response=await fresh.get('/api/fiscal-rules/receipts/'+p.operations.included100.key);const receipt=await response.json();ok(response.status===200&&receipt.result.id===p.operations.included100.result.id&&receipt.operation_executed===false,'own receipt recovered in new session');response=await read.get('/api/fiscal-rules/receipts/'+p.operations.included100.key);ok(response.status===404,'other actor receipt refused');p.passed=true;p.business_confirmations=3;p.calculations_retained=5;p.new_grants=0;p.new_financial_effects=0;p.browser_visual_review=false;save();console.log(JSON.stringify({passed:true,checks:p.checks.length,proof:file}));
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
$book='8085';require __DIR__.'/sgf-single-bootstrap.php';$p=json_decode(file_get_contents($s['root'].'/document-math-backup-pointer.json'),true);$before=json_decode(file_get_contents($p['root'].'/preservation-private.json'),true);$checks=[];foreach(['host'=>$host,'native'=>$native]as$name=>$db)foreach($before[$name]as$table=>$json){$rows=json_decode($json,true);$after=$db->table($table)->whereIn('id',array_column($rows,'id'))->orderBy('id')->get()->toJson();if($after!==$json)throw new LogicException('Original table changed '.$table);if(!in_array($table,['fr_calculations','fr_commands'],true)&&$db->table($table)->count()!==count($rows))throw new LogicException('Unexpected domain append '.$table);$checks[]=$name.'.'.$table;}
|
||||
if(hash_file('sha256',$s['root'].'/binding.json')!==$before['binding'])throw new LogicException('Original binding changed');
|
||||
file_put_contents($work.'/document-math-preservation.json',json_encode(['passed'=>true,'identical_original_tables'=>$checks,'journals'=>27,'new_grants'=>0,'native_writes'=>0,'six_protected_records'=>'No connections or commands target protected databases/records','blocked_cache'=>'No clear/delete','new_demo'=>false],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'proof'=>$work.'/document-math-preservation.json']).PHP_EOL;
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
require __DIR__.'/sgf-fiscal-rules-proof-bootstrap.php';config(['fiscal_rules_qa.document_enabled'=>true]);$review=json_decode(file_get_contents($s['root'].'/fiscal-rules-concurrency/first.json'),true);$ruleId=$review['id'];
|
||||
$line=fn($ref,$price,$quantity='1')=>['reference'=>$ref,'quantity'=>$quantity,'unit_price'=>$price];
|
||||
$documentInput=fn($mode,$lines,$id=null,$date='2026-11-10')=>['action'=>'calculate_document','revision'=>0,'data'=>['rule_event_id'=>$id??$ruleId,'currency'=>'MZN','date'=>$date,'price_mode'=>$mode,'lines'=>$lines],'reason'=>'Conferência QA individual da modalidade, linhas, política e diferenças preservadas.'];
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
use App\Modules\Services\FiscalDocumentMath;use Illuminate\Support\Facades\DB;
|
||||
require __DIR__.'/sgf-document-math-proof-bootstrap.php';$before=[];foreach(['fr_calculations','fr_events','fr_commands','an_criteria','an_results','br_events','fx_versions','si_invoices','sr_effects','cash_events']as$t)$before[$t]=$host->table($t)->orderBy('id')->get()->toJson();$host->beginTransaction();try{
|
||||
$p=$rulePreview(13,$documentInput('net_added',[$line('N1','400'),$line('N2','600')]));$r=$p['impact']['result'];$ok($r['base_document']==='1000.00'&&$r['tax_document']==='160.00'&&$r['total']==='1160.00'&&count($r['lines'])===2,'net400+600 exact1000+160=1160');$ok($host->table('fr_calculations')->orderBy('id')->get()->toJson()===$before['fr_calculations'],'document preview fully reverted');$included=$rulePreview(13,$documentInput('gross_included',[$line('G1','464'),$line('G2','696')]));$r=$included['impact']['result'];$ok($r['base_document']==='1000.00'&&$r['tax_document']==='160.00'&&$r['total']==='1160.00','included464+696 decomposes1000+160');
|
||||
[$hundred,$cut]=$ruleCommand(13,$documentInput('gross_included',[$line('G100','100')]));$selectRule(17);$detail=$rules->detail(17,$hundred['id'],'calculation');$r=$detail['result'];$ok($r['base_document']==='86.21'&&$r['tax_document']==='13.79'&&$r['total']==='100.00','gross100 produces86.21+13.79');$ok($r['lines'][0]['base_exact_fraction']==='2500/29'&&$r['lines'][0]['tax_exact_fraction']==='400/29'&&$r['base_difference_fraction']==='9/2900'&&$r['tax_difference_fraction']==='9/2900','both exact fractions and differences9/2900 preserved');$report=$rules->documentReport(17,$hundred['id']);$ok(hash('sha256',$report['bytes'])===$report['sha256']&&str_contains($report['bytes'],'86.21;13.79;100.00')&&str_contains($report['bytes'],'SEM VALIDADE FISCAL'),'internal per-line map matches frozen result');$selectRule(13);$again=$rules->confirm(13,$cut['preview']);$ok($again['replayed']&&$again['id']===$hundred['id'],'same document calculation key no duplicate');
|
||||
$small=$rulePreview(13,$documentInput('net_added',[$line('A','0.03'),$line('B','0.03'),$line('C','0.03')]));$r=$small['impact']['result'];$ok($r['base_document']==='0.09'&&$r['tax_document']==='0.00'&&$r['tax_difference_fraction']==='9/625','per-line tax sum preserved instead of aggregate re-round');$noncent=$rulePreview(13,$documentInput('net_added',[$line('P','0.001')]));$ok($noncent['impact']['result']['state']==='pending_representation'&&$noncent['impact']['result']['base_document']===null,'non-cent net base remains pending');$grossNoncent=$rulePreview(13,$documentInput('gross_included',[$line('P','0.001')]));$ok($grossNoncent['impact']['result']['state']==='pending_representation'&&$grossNoncent['impact']['result']['total']===null,'non-cent included gross remains pending');
|
||||
$many=[];for($i=1;$i<=25;$i++)$many[]=$line('L'.$i,'1');$limit=$rulePreview(13,$documentInput('net_added',$many));$ok(count($limit['impact']['result']['lines'])===25&&$limit['impact']['result']['tax_document']==='4.00','25 line boundary exact');$many[]=$line('L26','1');$reject(fn()=>$rulePreview(13,$documentInput('net_added',$many)),422,'26 lines refused');$reject(fn()=>$rulePreview(13,$documentInput('net_added',[])),422,'empty lines refused');$reject(fn()=>$rulePreview(13,$documentInput('net_added',[$line('X','1','0')])),422,'zero quantity refused');$reject(fn()=>$rulePreview(13,$documentInput('net_added',[$line('X','-1')])),422,'negative price refused');$reject(fn()=>$rulePreview(13,$documentInput('net_added',[$line('X','1'),$line('X','2')])),422,'duplicate references refused');$bad=$documentInput('net_added',[$line('X','1')]);$bad['data']['rate']='0.99';$reject(fn()=>$rulePreview(13,$bad),422,'client rate injection refused');$reject(fn()=>$rulePreview(17,$documentInput('net_added',[$line('X','1')])),403,'read-only calculation writer refused');$wrong=$documentInput('net_added',[$line('X','1')]);$wrong['data']['currency']='USD';$out=$rulePreview(13,$wrong);$ok($out['impact']['result']['state']==='pending'&&$out['impact']['result']['tax_document']===null,'wrong currency does not become zero');
|
||||
$outOfDate=$rulePreview(13,$documentInput('net_added',[$line('X','1')],null,'2026-12-01'));$ok($outOfDate['impact']['result']['state']==='pending','rule expiry enforced in document consumer');
|
||||
$csv=$rulePreview(13,$documentInput('gross_included',[$line(' =DANGER','100')]));$selectRule(13);$csvRecord=$rules->confirm(13,$csv['preview']);$selectRule(17);$safe=$rules->documentReport(17,$csvRecord['id']);$ok(str_contains($safe['bytes'],"' =DANGER"),'CSV formula-like reference escaped');
|
||||
$inflight=$rulePreview(13,$documentInput('net_added',[$line('C','100')]));$source=$host->table('fr_events')->find($ruleId);[$withdraw]=$ruleCommand(14,$ruleInput('withdraw',$source->revision,[],$ruleId,$source->rule_key));$selectRule(13);$reject(fn()=>$rules->confirm(13,$inflight['preview']),409,'source withdrawal invalidates in-flight document calculation');$selectRule(17);$old=$rules->detail(17,$hundred['id'],'calculation');$ok($old['result']===$detail['result']&&$old['current_evaluation']['state']==='pending','old line result preserved after rule withdrawal');
|
||||
}finally{$host->rollBack();}
|
||||
// Pure arithmetic for the already-approved successor17%, no new rule or document.
|
||||
$future=FiscalDocumentMath::calculate([$line('F','1170')],'gross_included','0.17');$ok($future['base_document']==='1000.00'&&$future['tax_document']==='170.00'&&$future['total']==='1170.00','successor artificial17 included1170=1000+170');foreach($before as$t=>$json)$ok($host->table($t)->orderBy('id')->get()->toJson()===$json,'unchanged '.$t);file_put_contents($work.'/document-math-result.json',json_encode(['passed'=>true,'checks'=>$checks,'rolled_back'=>true,'new_rights'=>0,'new_financial_effects'=>0,'new_demo'=>false],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/document-math-result.json']).PHP_EOL;
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
use Illuminate\Support\Facades\DB;
|
||||
require __DIR__.'/sgf-document-math-proof-bootstrap.php';$before=$host->table('fr_calculations')->count();$p=$rulePreview(13,$documentInput('gross_included',[$line('RECOVERY','100')]));$host->beginTransaction();try{$host->beginTransaction();try{$host->statement("CREATE FUNCTION dm_fail_test() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'Injected document math failure'; END; $$");$host->statement('CREATE TRIGGER dm_fail_test BEFORE INSERT ON fr_commands FOR EACH ROW EXECUTE FUNCTION dm_fail_test()');$selectRule(13);$failed=false;try{$rules->confirm(13,$p['preview']);}catch(Illuminate\Database\QueryException){$failed=true;}$ok($failed&&$host->table('fr_calculations')->count()===$before,'failed command receipt leaves no line result');}finally{$host->rollBack();}$selectRule(13);$out=$rules->confirm(13,$p['preview']);$ok($out['state']==='determined_qa'&&$host->table('fr_calculations')->count()===$before+1,'same command recovers line calculation after failure');}finally{$host->rollBack();}$ok($host->table('fr_calculations')->count()===$before,'failure recovery test fully rolled back');file_put_contents($work.'/document-math-recovery.json',json_encode(['passed'=>true,'checks'=>$checks,'rolled_back'=>true,'new_financial_effects'=>0],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/document-math-recovery.json']).PHP_EOL;
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
use Illuminate\Support\Str;
|
||||
$book='8085';require __DIR__.'/sgf-ledger-proof-bootstrap.php';config(['sales_ledger.synthetic_enabled'=>true,'cost_ledger.synthetic_enabled'=>true,'industrial_ledger.synthetic_enabled'=>true,'cost_ledger.correction_enabled'=>true,'industrial_ledger.correction_enabled'=>true]);
|
||||
$scope=['tenant_id'=>1,'company_id'=>2,'unit_id'=>2];$journals=$native->table('gl_journals')->where($scope)->orderBy('id')->get()->toJson();$host->beginTransaction();$native->beginTransaction();
|
||||
try{
|
||||
$p=$call('accounting_prepare','period_prepare',fn($e)=>$e->command('period-prepare',['period_key'=>'QA-2026-INTEGRADO','reason'=>'Conferência integral das origens efectivas na mesma8085; ensaio revertido para manter o período de apresentação aberto.'],(string)Str::uuid()));
|
||||
$snapshot=json_decode($native->table('gl_period_checks')->where('id',$p['check_id'])->value('snapshot'),true);$ok($snapshot['pending']===0&&$snapshot['debit_minor']===$snapshot['credit_minor'],'all supplier, sales, costs, corrections and physical origins reconcile with zero pending facts');
|
||||
$review=['check_id'=>$p['check_id'],'revision'=>$p['revision'],'decision'=>'approved','reason'=>'Revisão independente do conjunto integral de origens e montantes QA.'];$call('accounting_review','period_validate',fn($e)=>$e->command('period-validate',$review,(string)Str::uuid()));
|
||||
$native->beginTransaction();try{$rule=(array)$native->table('gl_rules')->where($scope)->where('rule_key','sales.invoice')->first();unset($rule['id']);$rule['version']=2;$rule['reason']='Versão adicional apenas no ensaio de recusa de conferência antiga.';$native->table('gl_rules')->insert($rule);$bad=false;try{$call('accounting_prepare','period_close',fn($e)=>$e->command('period-close',['check_id'=>$p['check_id'],'revision'=>$p['revision'],'reason'=>'Não fechar com regras alteradas.'],(string)Str::uuid()));}catch(DomainException $e){$bad=str_contains($e->getMessage(),'mudaram');}$ok($bad,'rule version changed after review refuses closure and requires fresh conference');}finally{$native->rollBack();}
|
||||
$key=(string)Str::uuid();$body=['check_id'=>$p['check_id'],'revision'=>$p['revision'],'reason'=>'Fecho QA integral apenas dentro da transacção de prova, sem reabrir depois.'];$closed=$call('accounting_prepare','period_close',fn($e)=>$e->command('period-close',$body,$key));require __DIR__.'/sgf-document-closed-period-check.php';$again=$call('accounting_prepare','period_close',fn($e)=>$e->command('period-close',$body,$key));
|
||||
$ok($closed['state']==='closed'&&$closed['closure_id']===$again['closure_id'],'distinctly reviewed close and replay retain one closure');
|
||||
$ok($native->table('gl_periods')->where($scope)->where('period_key','QA-2026-INTEGRADO')->orderByDesc('version')->value('status')==='closed','closure appends actual closed period version');
|
||||
$physical=json_decode(file_get_contents($s['root'].'/single-stock-physical-input.json'),true);$receiptId=(int)$native->table('stock_movements')->where('stock_item_id',$physical['stock_item'])->where('movement_type','receipt_in')->value('id');$bad=false;
|
||||
try{$call('accounting_prepare','prepare',fn($e)=>$e->command('effect-prepare',['effect_type'=>'stock.receipt','source_id'=>$receiptId,'journal_on'=>'2026-11-10','period_key'=>'QA-2026-INTEGRADO','rule_key'=>'stock.receipt','reason'=>'Não criar captura num período fechado.'],(string)Str::uuid()));}catch(DomainException $e){$bad=str_contains($e->getMessage(),'fechado');}$ok($bad,'closed QA period refuses further capture');
|
||||
$ok($journals===$native->table('gl_journals')->where($scope)->orderBy('id')->get()->toJson(),'closure and refusals preserve every journal present before this proof');$native->statement('SET CONSTRAINTS ALL IMMEDIATE');
|
||||
}finally{while($native->transactionLevel()>0)$native->rollBack();while($host->transactionLevel()>0)$host->rollBack();}
|
||||
$ok($native->table('gl_periods')->where($scope)->where('period_key','QA-2026-INTEGRADO')->orderByDesc('version')->value('status')==='open','proof rolls back instead of reopening; actual8085 presentation period remains open');
|
||||
file_put_contents($work.'/period-documents-result.json',json_encode(['passed'=>true,'checks'=>$checks,'facts'=>$snapshot['facts'],'pending'=>0,'journals'=>count(json_decode($journals,true)),'close_executed_in_rolled_back_transaction'=>true,'period_reopened'=>false,'presentation_period_open'=>true,'browser_visual_review'=>false],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'facts'=>$snapshot['facts'],'proof'=>$work.'/period-documents-result.json']).PHP_EOL;
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
$book=$argv[1]??'regression';require __DIR__.'/sgf-single-bootstrap.php';$p=json_decode(file_get_contents($repo.'/docs/integration/sgf-document-read-access-proposal.json'),true);if(($p['approved']??false)!==true||$p['scope']!==$f['scope'])throw new LogicException('Exact approval required');$profiles=json_decode(file_get_contents($s['root'].'/single-profiles.json'),true)['profiles'];$count=0;
|
||||
$host->transaction(function()use($host,$p,$profiles,&$count){foreach($p['existing_host_rights'] as $grant)foreach($grant['profiles'] as $profile){$id=$profiles[$profile]['user_id'];if(!in_array($profile,['finance_prepare','finance_review'],true)||$id!==$p['profiles'][$profile])throw new LogicException('Only approved existing identities');if(!$host->table('company_memberships')->where(['tenant_id'=>1,'company_id'=>2,'user_id'=>$id])->exists())throw new LogicException('Membership changed');$v=['tenant_id'=>1,'company_id'=>2,'unit_id'=>$grant['scope']==='unit'?2:null,'user_id'=>$id,'permission'=>$grant['permission']];if($host->table('permission_grants')->where($v)->exists())continue;$host->table('permission_grants')->insert($v);App\Modules\Identity\Services\Audit::record('access.granted_on_creation',null,1,2,$v['unit_id'],$id,['permission'=>$grant['permission'],'reason'=>$p['authorization']]);$count++;}});
|
||||
file_put_contents($work.'/document-read-access.json',json_encode(['passed'=>true,'book'=>$book,'new_grants'=>$count,'approved_concessions'=>10,'new_permission_names'=>0,'new_profiles'=>0,'public_grants'=>0],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'book'=>$book,'new_grants'=>$count,'proof'=>$work.'/document-read-access.json']).PHP_EOL;
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
$book=$argv[1]??'8085';require __DIR__.'/sgf-single-bootstrap.php';$profiles=json_decode(file_get_contents($s['root'].'/single-profiles.json'),true)['profiles'];$missing=[];$checks=[];
|
||||
$rights=['issuer_documents.view'=>null,'client_documents.view'=>null,'draft_documents.view'=>2,'service_fiscal.view'=>null,'billing_drafts.view'=>2,'service_drafts.view'=>2,'clients.view'=>null,'services.view'=>null];
|
||||
foreach(['finance_prepare','finance_review'] as $profile)foreach($rights as $right=>$unit){$exists=$host->table('permission_grants')->where(['tenant_id'=>1,'company_id'=>2,'unit_id'=>$unit,'user_id'=>$profiles[$profile]['user_id'],'permission'=>$right])->exists();$row=['profile'=>$profile,'user_id'=>$profiles[$profile]['user_id'],'permission'=>$right,'unit_id'=>$unit,'present'=>$exists];$checks[]=$row;if(!$exists)$missing[]=$row;}
|
||||
$out=['passed'=>true,'book'=>$book,'read_only'=>true,'grants_applied'=>0,'missing'=>$missing,'checks'=>$checks];file_put_contents($work.'/document-read-rights.json',json_encode($out,JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'book'=>$book,'missing'=>$missing,'proof'=>$work.'/document-read-rights.json']).PHP_EOL;
|
||||
@@ -0,0 +1,2 @@
|
||||
import fs from'node:fs';import{spawn,spawnSync}from'node:child_process';import assert from'node:assert/strict';const php='C:/laragon/tools/sgf-tempo/php-8.4.27/php.exe',script='scripts/sgf-documents-concurrency.php',run=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')),dir=run.root+'/private/receipt-composition-b4093c6f6707/documents-concurrency';
|
||||
const call=phase=>{const r=spawnSync(php,[script,phase],{encoding:'utf8',windowsHide:true});assert.equal(r.status,0,r.stdout+r.stderr);assert.match(r.stdout,/"passed":true/);return r.stdout;};if(fs.existsSync(dir+'/result.json')){assert(JSON.parse(fs.readFileSync(dir+'/result.json')).passed);console.log(JSON.stringify({passed:true,reused:true,proof:dir+'/result.json'}));process.exit();}call('prepare');const first=spawn(php,[script,'first'],{stdio:'pipe',windowsHide:true});let out='';first.stdout.on('data',d=>out+=d);first.stderr.on('data',d=>out+=d);const ended=new Promise(r=>first.once('exit',r));try{const end=Date.now()+10000;while(!fs.existsSync(dir+'/locked')&&Date.now()<end)await new Promise(r=>setTimeout(r,50));assert(fs.existsSync(dir+'/locked'));const second=spawn(php,[script,'second'],{stdio:'pipe',windowsHide:true});let text='';second.stdout.on('data',d=>text+=d);second.stderr.on('data',d=>text+=d);assert.equal(await new Promise(r=>second.once('exit',r)),0,text);const blocked=JSON.parse(fs.readFileSync(dir+'/second.json'));assert.equal(blocked.status,503);assert(blocked.elapsed_seconds>=4.5&&blocked.elapsed_seconds<12);}finally{fs.writeFileSync(dir+'/release','Release only the QA transaction');assert.equal(await ended,0,out);}console.log(call('recover').trim());
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
require __DIR__.'/sgf-service-documents-proof-bootstrap.php';$dir=$s['root'].'/documents-concurrency';if(!is_dir($dir))mkdir($dir,0700,true);$file=$dir.'/progress.json';$mode=$argv[1]??'';
|
||||
if($mode==='prepare'){
|
||||
if(is_file($file))throw new LogicException('Reuse existing preparation');$key=(string)Illuminate\Support\Str::uuid();$data=array_intersect_key($inputs['invoice_receipt'],array_flip(['draft_id','series_id','rule_id']))+['payment_date'=>'2026-11-10','evidence'=>'Intenção QA1000 para duas emissões concorrentes.'];$command('finance_prepare','invoice_receipt','prepare',$key,0,$data);$command('finance_review','invoice_receipt','review',$key,1,['decision'=>'approved']);$actor=$select('finance_review');$cuts=[];foreach([1,2] as $i)$cuts[$i]=app(App\Modules\Services\ServiceDocuments::class)->preview($actor,['kind'=>'invoice_receipt','action'=>'issue','document_key'=>$key,'version'=>2,'data'=>[],'reason'=>'Conclusão QA concorrente da mesma intenção financeira.'],(string)Illuminate\Support\Str::uuid())['preview'];file_put_contents($file,json_encode(['cuts'=>$cuts,'document_key'=>$key,'invoices_before'=>$host->table('si_invoices')->count()]));
|
||||
}elseif($mode==='first'){
|
||||
$p=json_decode(file_get_contents($file),true);$actor=$select('finance_review');$host->beginTransaction();try{$result=app(App\Modules\Services\ServiceDocuments::class)->confirm($actor,$p['cuts'][1]);file_put_contents($dir.'/locked','host company, invoice counter and financial intent locked');$end=microtime(true)+20;while(!is_file($dir.'/release')&µtime(true)<$end)usleep(50000);if(!is_file($dir.'/release'))throw new LogicException('Release missing');$host->commit();file_put_contents($dir.'/first.json',json_encode($result));}finally{while($host->transactionLevel()>0)$host->rollBack();}
|
||||
}elseif($mode==='second'){
|
||||
$p=json_decode(file_get_contents($file),true);$select('finance_review');request()->request->replace(['preview'=>$p['cuts'][2]]);$start=microtime(true);$r=app(App\Modules\Services\Http\ServiceDocumentsController::class)->confirm(request(),app(App\Modules\Services\ServiceDocuments::class));file_put_contents($dir.'/second.json',json_encode(['status'=>$r->getStatusCode(),'elapsed_seconds'=>microtime(true)-$start]));
|
||||
}elseif($mode==='recover'){
|
||||
$p=json_decode(file_get_contents($file),true);$actor=$select('finance_review');$w=app(App\Modules\Services\ServiceDocuments::class);$reject(fn()=>$w->confirm($actor,$p['cuts'][2]),409,'second invoice-receipt refuses old version after first commit');$first=$w->confirm($actor,$p['cuts'][1]);$ok($first['replayed'],'first key recovers original invoice-receipt');$ok($host->table('si_invoices')->count()===$p['invoices_before']+1&&$host->table('sd_outputs')->where('document_key',$p['document_key'])->count()===1,'one invoice and one aggregate PDF');$out=$host->table('sd_outputs')->where('document_key',$p['document_key'])->first();$ok($host->table('sr_records')->where('invoice_id',$out->invoice_id)->count()===2,'one obligation and one receipt');file_put_contents($dir.'/result.json',json_encode(['passed'=>true,'second_during_lock'=>json_decode(file_get_contents($dir.'/second.json'),true),'after_release_status'=>409,'one_invoice'=>true,'one_obligation'=>true,'one_receipt'=>true,'one_allocation'=>true,'first_key_replayed'=>true,'regression_only'=>true],JSON_PRETTY_PRINT));
|
||||
}else throw new LogicException('Unknown phase');echo json_encode(['passed'=>true,'phase'=>$mode,'proof'=>$dir.'/result.json']).PHP_EOL;
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
$book='8085';require __DIR__.'/sgf-single-bootstrap.php';
|
||||
$baseline=$s['root'].'/single-integration/20261111-074653-1680702c';$before=json_decode(file_get_contents($baseline.'/preservation-private.json'),true);$installation=json_decode(file_get_contents($baseline.'/equipment-orders-installation.json'),true);$binding=hash_file('sha256',$s['root'].'/binding.json');$order=fn($table)=>in_array($table,['sr_outbox','sr_deliveries'],true)?'event_id':(in_array($table,['sc_catalogue_meta','pd_due_meta','fo_meta','fs_meta'],true)?'policy':(in_array($table,['fs_results','fs_result_lines'],true)?'snapshot_key':'id'));
|
||||
$checks=[];foreach($before as$name=>$tables){$db=$name==='host'?$host:$native;foreach($tables as$table=>$json){if($db->table($table)->orderBy($order($table))->when($table==='fs_result_lines',fn($q)=>$q->orderBy('account_id'))->get()->toJson()!==$json)throw new LogicException('Original facts changed '.$table);$checks[]=$name.'.'.$table;}}if($binding!==hash_file('sha256',$s['root'].'/binding.json'))throw new LogicException('Binding changed');foreach($installation['hashes']as$file=>$hash)if(hash_file('sha256',$s['app'].'/'.$file)!==$hash)throw new LogicException('Installed code changed');file_put_contents($work.'/equipment-orders-preservation.json',json_encode(['passed'=>true,'installation_proof'=>$baseline.'/equipment-orders-installation.json','identical_original_tables'=>$checks,'business_commands'=>0,'new_grants'=>0,'new_demo'=>false,'cache_cleared'=>false,'native_writes'=>0,'binding_unchanged'=>true],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'proof'=>$work.'/equipment-orders-preservation.json']).PHP_EOL;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import fs from 'node:fs';import assert from 'node:assert/strict';import {Element,walk,parsePage,installDom} from './purchasing-workspace-dom.mjs';
|
||||
Element.prototype.contains=function(el){return walk(this).includes(el);};Element.prototype.hasAttribute=function(name){return Object.hasOwn(this.attributes,name);};Object.defineProperty(Element.prototype,'tagName',{get(){return this.tag.toUpperCase();}});
|
||||
const checks=[];for(const kind of ['finance','ledger']){
|
||||
const finance=kind==='finance',attribute=finance?'data-action':'data-ledger-action';const inputs=finance?'<input name="payment_id" value="7"><input name="obligation_id" value="2"><input name="source_revision" value="revision"><input name="amount" value="20.00"><input name="reference" value="SIM-AFECTACAO-QA"><input name="reason" value="Razão sintética individual">':'<input name="source" value="sales.invoice|4"><input name="journal_on" value="2026-11-10"><input name="period_key" value="QA-2026-INTEGRADO"><input name="reason" value="Razão sintética individual">';
|
||||
const document=parsePage(`<main id="${finance?'finance-payables':'composed-ledger'}" data-context="context-qa" data-csrf="csrf-qa"><p id="${finance?'result':'ledger-result'}"></p><form ${attribute}="${finance?'allocation-prepare':'capture'}">${inputs}<button>Confirmar</button></form><form ${attribute}="other"><button>Outra decisão</button></form></main>`);installDom(document);const listeners={};globalThis.window={addEventListener:(key,fn)=>listeners[key]=fn};let reloads=0;globalThis.location={reload:()=>reloads++};
|
||||
const calls=[];let lose=true;globalThis.fetch=async(path,options)=>{calls.push({path,...options});if(lose){lose=false;throw Error('Response lost after commit');}return {ok:true,status:200,json:async()=>({replayed:true})};};
|
||||
await import('../public/sgf-'+(finance?'finance-payables':'composed-ledger')+'.mjs?client-proof');const [form,other]=document.querySelectorAll('form');await form.onsubmit({preventDefault(){}});assert.equal(calls.length,1);assert.equal(reloads,0);checks.push(kind+' uncertain response does not imply success');
|
||||
assert(document.querySelectorAll('input').every(e=>e.disabled));assert.equal(form.querySelector('button').disabled,false);checks.push(kind+' uncertain fields frozen and only recovery button enabled');
|
||||
const body=JSON.parse(calls[0].body);if(finance){assert.deepEqual(body.allocations,[{obligation_id:2,source_revision:'revision',amount:'20.00'}]);assert.equal(body.amount,undefined);}else{assert.equal(calls[0].path,'/api/general-ledger/effect-prepare');assert.equal(body.source_id,4);assert.equal(body.effect_type,'sales.invoice');}checks.push(kind+' visible selection maps to exact original command');
|
||||
await other.onsubmit({preventDefault(){}});assert.equal(calls.length,1);checks.push(kind+' unrelated decision blocked during recovery');let protectedExit=false;listeners.beforeunload({preventDefault(){protectedExit=true;}});assert(protectedExit);checks.push(kind+' uncertain operation protects navigation');
|
||||
await form.onsubmit({preventDefault(){}});assert.equal(calls.length,2);assert.equal(calls[1].body,calls[0].body);assert.equal(calls[1].headers['Idempotency-Key'],calls[0].headers['Idempotency-Key']);assert.equal(reloads,1);checks.push(kind+' recovery sends identical normalized body and key after disabled fields');
|
||||
}
|
||||
const run=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json'));const proof=run.root+'/private/receipt-composition-cc50a276c10c/single-finance-ledger-client-20261110.json';fs.writeFileSync(proof,JSON.stringify({passed:true,checks,dom_simulation:true,visual_review:false},null,2));console.log(JSON.stringify({passed:true,checks:checks.length,proof}));
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user