diff --git a/AGENTS.md b/AGENTS.md index 5f152ab..d987d6a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,7 @@ # SGF Tempo — instruções locais +- Autorização específica posterior de 28/10/2026 executada: apenas `120000` aplicada em `sgf_tempo_dev`, por caminho exclusivo com `sgf_dev_owner`, e seis concessões auditadas no tenant #1 / Tempo Lda #1 / SEGI #1: conta #1 `workshop_billing.view/prepare/generate`; conta #2 `workshop_billing.view/review` e `workshop.view`. Ver actualização de `docs/WORKSHOP_BILLING_DELIVERY.md`. Preservadas 74 tabelas anteriores e acessos; sete tabelas novas vazias. Aplicação mantém `sgf_dev_app`; conversão/oficinas/custos/recebimentos/emissão reais bloqueados e guardas SQL verificadas. `110000` e sete concessões do orçamento permanecem pendentes; não aplicar por arrasto. Consulta de oficina da conta #2 não concede apontamento/verificação/supervisão. Indicações anteriores de `120000`/seis concessões por aplicar são históricas; sem utilização real ou publicação. + - Aceitação específica de 28/10/2026: utilizador aceitou `WORKSHOP-BILLING-DRAFT-SIM-v1` com limitação de capturas e autorizou checkpoint exclusivamente local, sem credenciais/dados privados. Ver `docs/WORKSHOP_BILLING_ACCEPTANCE.md`. Não transformar provas pendentes em realizadas; Edge não recuperado e capturas não recebidas permanecem limites. Substitui apenas a pendência de aceitação/encerramento deste ensaio. `120000`, seis concessões e utilização real continuam por autorizar; `110000` e sete concessões também pendentes. Sem aplicação, activação operacional ou publicação. - Autorização técnica de 28/10/2026: implementar `WORKSHOP-BILLING-DRAFT-SIM-v1` conforme `docs/WORKSHOP_BILLING_DRAFT_SCOPE.md`, exclusivamente sintético em `sgf_tempo_test`. Ver `docs/WORKSHOP_BILLING_DELIVERY.md`. Reutilizar origens verificadas por ID/versão, sem consumo duplicado, preços comerciais sintéticos 200 MZN/H e 75 MZN/UN (350+150=500 sem imposto), revisão comercial distinta. Não derivar preço do custo interno ou atribuir verificação técnica à revisora. Sem edição directa de rascunhos gerados ou confirmação B1 automática. `120000` e seis concessões reais só propostas, não aplicar sem autorização específica; `110000` e sete concessões continuam pendentes. Preservar `04870f0`, `090b7da` e alterações posteriores; sem utilização real, stock, emissão, lançamentos, publicação ou aceitação presumida. diff --git a/README.md b/README.md index 83898aa..0c32350 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ O primeiro núcleo de cálculo de serviço e a política operacional de truncage O menu **Classificação contabilística**, disponível após actualizar a sessão com os acessos especificamente concedidos, permite preparar e submeter versões PGC-NIRF, validar/recusar sem editar e consultar classificação por data. Categoria e excepções são empresariais; operações e respectivos históricos continuam limitados às unidades autorizadas. As tabelas reais começam vazias, sem contas de movimento ou regras aprovadas por omissão. Consultar [utilização, provas e limites da classificação](docs/CATALOGUE_ACCOUNTING_DELIVERY.md). ## Ensaio isolado de custo directo -Ligação sintética oficina → prestação/rascunho rastreável: [âmbito implementado, provas e limites](docs/WORKSHOP_BILLING_DELIVERY.md). Migração `120000` e seis concessões apenas propostas; nenhuma conversão real activada. +Ligação sintética oficina → prestação/rascunho rastreável: [âmbito implementado, provas e limites](docs/WORKSHOP_BILLING_DELIVERY.md). Migração `120000` e seis concessões instaladas tecnicamente por autorização específica de 28/10/2026; conversão real continua bloqueada. `110000` e os direitos do orçamento permanecem separados e pendentes. Página inicial e menus agrupados: [alterações, verificações e limites visuais](docs/HOME_NAVIGATION_DELIVERY.md). Os atalhos dependem dos acessos existentes; não apresentam contagens de pendências ou indicadores financeiros simulados. diff --git a/docs/WORKSHOP_BILLING_DELIVERY.md b/docs/WORKSHOP_BILLING_DELIVERY.md index 7124ec4..483fd82 100644 --- a/docs/WORKSHOP_BILLING_DELIVERY.md +++ b/docs/WORKSHOP_BILLING_DELIVERY.md @@ -1,5 +1,18 @@ # Oficina → rascunho — entrega técnica em verificação +## Aplicação técnica especificamente autorizada — 28/10/2026 + +Após apresentação da migração, destino e seis concessões exactas, o utilizador confirmou «autorizo». Executado `scripts/sgf.ps1 php scripts/workshop-billing-dev-install.php --apply-confirmed-120000-and-six-grants`. Apenas `2026_10_28_120000_create_workshop_billing_drafts.php` aplicada em `sgf_tempo_dev`/55432 por ligação temporária `sgf_dev_owner`, sem superutilizador, criação de bases/papéis ou bypass RLS. A aplicação mantém `sgf_dev_app`. Migração e concessões verificadas na mesma transacção de instalação; ligação de proprietário removida da configuração em memória no fim. + +- 74 tabelas preexistentes conferidas por contagens/impressões digitais, sem exportar conteúdos; dados, acessos e migrações anteriores preservados. Apenas uma entrada no histórico de migrações, seis concessões e seis entradas de auditoria acrescentadas. +- Tenant #1 / Tempo Lda #1 / SEGI #1: conta #1 `workshop_billing.view`, `workshop_billing.prepare`, `workshop_billing.generate`; conta #2 `workshop_billing.view`, `workshop_billing.review`, `workshop.view`. Contas/pertenças e pré-requisitos existentes conferidos, sem nomes/emails ou credenciais no instalador. Nenhum direito de verificação técnica ou supervisão atribuído. +- Sete tabelas `wb_*` novas vazias; nenhum cliente, ordem, origem, preço, proposta, decisão comercial, prestação ou rascunho criado em desenvolvimento. +- Listagens das duas contas permanecem bloqueadas, sem ordens de conversão. Guarda, preparação, revisão e geração recusam com 403; também recusam as guardas de oficinas, custo directo, custo industrial, recebimentos e emissão. Guardas SQL de ambiente industrial/oficina/custo directo/recebimentos recusam desenvolvimento, incluindo a guarda usada pelas sete tabelas novas. +- `110000` permanece não aplicada e fora do caminho executado. As sete concessões do orçamento continuam separadas, sem autorização de aplicação. +- Fontes funcionais/guardas preservadas. Sintaxe do instalador aprovada; verificações de preservação e bloqueios executadas antes do commit transaccional e repetidas com o papel da aplicação após a instalação. Sem repetir testes que recriam a fixture manual aceite. + +Prova local em `storage/app/private/workshop-billing-dev-install-proof.json`, sem conteúdos de registos ou credenciais; não incluída no repositório. Aceitação `3932b94` e respectivas limitações de capturas permanecem preservadas. As referências abaixo a `120000`/seis concessões não aplicadas são históricas. Esta instalação não habilita utilização real, confirmação, emissão, stock, recebimentos, lançamentos ou publicação. Nenhum novo checkpoint é declarado por esta actualização. + **Aceitação posterior de 28/10/2026:** utilizador aceitou a entrega com a limitação de capturas e autorizou checkpoint exclusivamente local. Ver [WORKSHOP_BILLING_ACCEPTANCE.md](WORKSHOP_BILLING_ACCEPTANCE.md). As referências abaixo a aceitação/checkpoint pendentes são históricas; nenhuma prova pendente passa a realizada. `120000`, seis concessões e uso real continuam por autorizar. ## Percurso manual de geração relatado e reconciliado diff --git a/scripts/workshop-billing-dev-install.php b/scripts/workshop-billing-dev-install.php new file mode 100644 index 0000000..0c3b88b --- /dev/null +++ b/scripts/workshop-billing-dev-install.php @@ -0,0 +1,83 @@ +make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap(); +use Illuminate\Support\Facades\{DB,Artisan}; +use App\Modules\Identity\Services\{LocalDatabaseGuard,Permissions,Audit}; +use App\Modules\Workshop\{WorkshopBilling,WorkshopWorkflow,DirectCosts}; +use App\Modules\Industry\IndustrialCosts; +use App\Modules\Services\{ServiceReceivables,ServiceInvoicing}; +use App\Shared\Context\CompanyContext; +function ensureWbInstall(bool $ok,string $why): void {if(!$ok)throw new RuntimeException($why);} +function wbFingerprint(string $table,?int $max=null): array { + ensureWbInstall((bool)preg_match('/^[a-z_]+$/D',$table),'Invalid table identifier.'); + $where=$max===null?'':' WHERE id <= '.$max; + $row=DB::selectOne('SELECT count(*) AS n, md5(COALESCE(string_agg(row_to_json(t)::text,E\'\\n\' ORDER BY row_to_json(t)::text),\'\')) AS h FROM (SELECT * FROM "'.$table.'"'.$where.') t'); + return [(int)$row->n,$row->h]; // Record contents and fingerprints are not exported. +} +ensureWbInstall(LocalDatabaseGuard::allows(),'Own SGF application connection required.'); +ensureWbInstall($argv===[$argv[0],'--apply-confirmed-120000-and-six-grants'],'Exact authorised operation required.'); +$migration='2026_10_28_120000_create_workshop_billing_drafts'; +$excluded='2026_10_28_110000_create_industrial_budgets'; +ensureWbInstall(!DB::table('migrations')->where('migration',$migration)->exists(),'120000 already installed; review instead of replaying.'); +ensureWbInstall(!DB::table('migrations')->where('migration',$excluded)->exists(),'110000 state differs from reviewed proposal.'); +foreach(['2026_10_27_100000_create_industrial_cost_simulation','2026_10_27_080000_create_workshop_resource_records'] as $prerequisite)ensureWbInstall(DB::table('migrations')->where('migration',$prerequisite)->exists(),'Migration prerequisite missing.'); +$newTables=['wb_proposals','wb_decisions','wb_generations','wb_claims','wb_commands','wb_events','wb_outbox']; +$tables=array_map(fn($r)=>$r->tablename,DB::select("SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename")); +ensureWbInstall(!array_intersect($tables,$newTables),'Unexpected existing conversion tables.'); +$rights=[1=>['workshop_billing.view','workshop_billing.prepare','workshop_billing.generate'],2=>['workshop_billing.view','workshop_billing.review','workshop.view']]; +$checkScope=function()use($rights): void { + ensureWbInstall(DB::table('companies')->where(['id'=>1,'tenant_id'=>1,'name'=>'Tempo Lda'])->exists(),'Company differs.'); + ensureWbInstall(DB::table('units')->where(['id'=>1,'tenant_id'=>1,'company_id'=>1,'name'=>'SEGI'])->exists(),'Unit differs.'); + foreach($rights as $user=>$permissions){ + ensureWbInstall(DB::table('users')->where('id',$user)->exists()&&DB::table('company_memberships')->where(['user_id'=>$user,'tenant_id'=>1,'company_id'=>1])->exists(),'Existing account/member required.'); + foreach(['company.view','clients.view','services.view'] as $permission)app(Permissions::class)->require($user,1,1,$permission); + app(Permissions::class)->require($user,1,1,'unit.view',1); + foreach($permissions as $permission)ensureWbInstall(!DB::table('permission_grants')->where(['user_id'=>$user,'tenant_id'=>1,'company_id'=>1,'unit_id'=>1,'permission'=>$permission])->exists(),'Proposed grant already present; review required.'); + } + foreach(['workshop.view','service_drafts.view','service_drafts.create','billing_drafts.view','billing_drafts.create'] as $permission)app(Permissions::class)->require(1,1,1,$permission,1); + ensureWbInstall(!DB::table('permission_grants')->where('permission','like','workshop_billing.%')->exists(),'Unexpected conversion rights.'); +}; +$checkScope();$baseline=[];foreach($tables as $t)$baseline[$t]=wbFingerprint($t); +$max=[];foreach(['permission_grants','identity_audit'] as $t)$max[$t]=(int)DB::table($t)->max('id'); +$ledger=DB::table('migrations')->orderBy('id')->get()->map(fn($r)=>(array)$r)->all(); +$sources=[];foreach(['app/Modules/Workshop/WorkshopBilling.php','app/Modules/Workshop/WorkshopWorkflow.php','app/Modules/Workshop/DirectCosts.php','app/Modules/Industry/IndustrialCosts.php','app/Modules/Services/ServiceInvoicing.php','app/Modules/Services/ServiceReceivables.php','app/Modules/Services/ServiceWorkflow.php'] as $path)$sources[$path]=hash_file('sha256',base_path($path)); +$secrets=json_decode(file_get_contents('C:/laragon/data/sgf-tempo/secrets/database.json'),true,32,JSON_THROW_ON_ERROR); +$owner=config('database.connections.'.config('database.default'));$owner['username']='sgf_dev_owner';$owner['password']=$secrets['dev_owner'];$owner['url']=null; +config(['database.connections.sgf_wb_install_owner'=>$owner]);unset($secrets,$owner); +$application=DB::getDefaultConnection();$grants=[];$denials=[]; +$checkBlocks=function()use(&$denials): void { + foreach([1,2] as $actor){app(CompanyContext::class)->selectFor($actor,1,1,1);$list=app(WorkshopBilling::class)->listing($actor);ensureWbInstall($list['blocked']&&$list['orders']===[],'Conversion listing must remain blocked.');} + foreach(['conversion','prepare','review','generate','workshop','direct_cost','industrial_cost','receivables','issuance'] as $entry){ + try{match($entry){'conversion'=>WorkshopBilling::guard(),'prepare'=>app(WorkshopBilling::class)->prepare(0,1,[],'blocked-probe'),'review'=>app(WorkshopBilling::class)->review(0,2,[],'blocked-probe'),'generate'=>app(WorkshopBilling::class)->generate(0,1,[],'blocked-probe'),'workshop'=>WorkshopWorkflow::guard(),'direct_cost'=>DirectCosts::guard(),'industrial_cost'=>IndustrialCosts::guard(),'receivables'=>ServiceReceivables::guard(),'issuance'=>ServiceInvoicing::guard()};throw new RuntimeException('Operational guard did not refuse.');} + catch(\Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e){ensureWbInstall($e->getStatusCode()===403,'Expected 403.');$denials[$entry]=403;} + } + foreach(['ic','wo','dc','sr'] as $prefix){ + try{DB::transaction(fn()=>DB::select('SELECT sgf_'.$prefix.'_environment()'));throw new RuntimeException('SQL environment guard allowed development.');} + catch(\Illuminate\Database\QueryException $e){ensureWbInstall(str_contains($e->getMessage(),'SGF_'.strtoupper($prefix).'_TEST_DATABASE_ONLY'),'Unexpected SQL guard refusal.');} + } +}; +try{ + $connection=DB::connection('sgf_wb_install_owner'); + $id=$connection->selectOne('SELECT current_database() AS db,current_user AS role,inet_server_port() AS port,rolsuper,rolcreatedb,rolcreaterole,rolbypassrls FROM pg_roles WHERE rolname=current_user'); + ensureWbInstall($id->db==='sgf_tempo_dev'&&$id->role==='sgf_dev_owner'&&(int)$id->port===55432&&!$id->rolsuper&&!$id->rolcreatedb&&!$id->rolcreaterole&&!$id->rolbypassrls,'Unexpected owner/destination.'); + DB::setDefaultConnection('sgf_wb_install_owner'); + $connection->transaction(function()use($migration,$excluded,$checkScope,$rights,$baseline,$max,$ledger,$newTables,$checkBlocks,&$grants){ + DB::table('companies')->where('id',1)->lockForUpdate()->firstOrFail();$checkScope(); + $code=Artisan::call('migrate',['--database'=>'sgf_wb_install_owner','--path'=>'database/migrations/'.$migration.'.php','--force'=>true,'--no-interaction'=>true]);ensureWbInstall($code===0,'Targeted migration failed.'); + foreach($rights as $user=>$permissions)foreach($permissions as $permission){$g=['user_id'=>$user,'tenant_id'=>1,'company_id'=>1,'unit_id'=>1,'permission'=>$permission];DB::table('permission_grants')->insert($g+['created_at'=>now(),'updated_at'=>now()]);Audit::record('workshop_billing.initial_grant',null,1,1,1,subject:$user,details:['permission'=>$permission,'reason'=>'Six specifically authorised grants; operational use remains blocked.']);$grants[]=$g;} + foreach($baseline as $t=>$fp)if($t!=='migrations')ensureWbInstall(wbFingerprint($t,$max[$t]??null)===$fp,'Existing records changed: '.$t); + ensureWbInstall(DB::table('permission_grants')->where('id','>',$max['permission_grants'])->count()===6,'Exactly six new grants required.'); + ensureWbInstall(DB::table('identity_audit')->where('id','>',$max['identity_audit'])->count()===6&&DB::table('identity_audit')->where('id','>',$max['identity_audit'])->where('action','workshop_billing.initial_grant')->count()===6,'Exactly six audited grants required.'); + $actual=DB::table('permission_grants')->where('id','>',$max['permission_grants'])->get(['user_id','tenant_id','company_id','unit_id','permission'])->map(fn($r)=>(array)$r)->all();$sort=fn($a,$b)=>[$a['user_id'],$a['permission']]<=>[$b['user_id'],$b['permission']];usort($actual,$sort);$expected=$grants;usort($expected,$sort);ensureWbInstall($actual===$expected,'Grant scope differs.'); + $after=DB::table('migrations')->orderBy('id')->get()->map(fn($r)=>(array)$r)->all();ensureWbInstall(count($after)===count($ledger)+1&&array_slice($after,0,count($ledger))===$ledger&&end($after)['migration']===$migration,'Unexpected migration delta.'); + ensureWbInstall(!DB::table('migrations')->where('migration',$excluded)->exists(),'110000 must stay pending.'); + foreach($newTables as $t)ensureWbInstall(DB::table($t)->count()===0,'New table not empty.'); + $checkBlocks(); + }); +}finally{DB::setDefaultConnection($application);DB::purge('sgf_wb_install_owner');config(['database.connections.sgf_wb_install_owner'=>null]);} +ensureWbInstall(LocalDatabaseGuard::allows(),'Application must remain sgf_dev_app.');$checkBlocks(); +foreach($newTables as $t)ensureWbInstall(DB::table($t)->count()===0,'New table not empty after installation.'); +foreach($sources as $path=>$hash)ensureWbInstall(hash_file('sha256',base_path($path))===$hash,'Functional source changed.'); +$proof=['date'=>'2026-10-28','database'=>'sgf_tempo_dev','migration_applied'=>$migration,'only_one_migration_added'=>true,'migration_110000_still_pending'=>true,'preexisting_tables_checked'=>count($baseline),'prior_records_and_accesses_preserved'=>true,'new_empty_tables'=>$newTables,'new_grants'=>$grants,'audited_new_grants'=>6,'application_role'=>'sgf_dev_app','conversion_account_listings_blocked'=>true,'entry_denials'=>$denials,'sql_environment_guards_refused'=>true,'functional_sources_unchanged'=>true,'real_operations_created'=>false]; +file_put_contents(storage_path('app/private/workshop-billing-dev-install-proof.json'),json_encode($proof,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR)); +echo 'Applied only 120000 and six audited grants; '.count($baseline)." previous tables preserved, seven new tables empty, 110000 pending, operational guards verified.\n";