Preserva B1 aceite de confirmacao interna sem validade fiscal
This commit is contained in:
@@ -13,6 +13,7 @@ try {
|
||||
\Illuminate\Support\Facades\DB::table('companies')->where('id', $input['company'])->lockForUpdate()->firstOrFail();
|
||||
echo "LOCK_ACQUIRED\n"; flush(); usleep(2000000);
|
||||
}
|
||||
if (($input['operation'] ?? '') === 'b1.confirm') { return app(\App\Modules\Services\InternalConfirmation::class)->confirm($input['id'], $input['actor'], $input['version'], $input['key']); }
|
||||
return app(\App\Modules\Services\ServiceWorkflow::class)->save('clients', $input['id'] ?? null, $input['actor'], $input['data'], $input['key']);
|
||||
});
|
||||
echo json_encode(['status' => 200, 'result' => $result], JSON_THROW_ON_ERROR).PHP_EOL;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
// Local operator procedure: preview before applying; obtain specific user confirmation.
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
$app = require __DIR__.'/../bootstrap/app.php';
|
||||
$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
if (! \App\Modules\Identity\Services\LocalDatabaseGuard::allows()) { throw new RuntimeException('Own local development database required.'); }
|
||||
$args = array_values(array_filter(array_slice($argv, 1), fn ($arg) => $arg !== '--apply'));
|
||||
if (count($args) !== 3 || array_filter($args, fn ($id) => ! ctype_digit($id))) { throw new RuntimeException('Usage: b1-access.php user_id company_id unit_id [--apply]'); }
|
||||
[$userId, $companyId, $unitId] = array_map('intval', $args);
|
||||
$user = \Illuminate\Support\Facades\DB::table('users')->where('id', $userId)->first(['id','name','email']);
|
||||
$unit = \Illuminate\Support\Facades\DB::table('units as n')->join('companies as c', 'c.id', '=', 'n.company_id')->where('n.id', $unitId)->where('c.id', $companyId)->first(['n.tenant_id','n.company_id','n.id as unit_id','n.name as unit_name','c.name as company_name']);
|
||||
if (! $user || ! $unit) { throw new RuntimeException('Existing account or scope not found.'); }
|
||||
$permission = \App\Modules\Services\InternalConfirmation::PERMISSION;
|
||||
$plan = ['user' => $user, 'scope' => $unit, 'permission' => $permission];
|
||||
if (! in_array('--apply', $argv, true)) { echo json_encode($plan, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE).PHP_EOL; exit(0); }
|
||||
\Illuminate\Support\Facades\DB::transaction(function () use ($unit, $userId, $permission): void {
|
||||
\Illuminate\Support\Facades\DB::table('companies')->where('id', $unit->company_id)->lockForUpdate()->firstOrFail();
|
||||
foreach (['company.view','unit.view','billing_drafts.view'] as $required) { app(\App\Modules\Identity\Services\Permissions::class)->require($userId, $unit->tenant_id, $unit->company_id, $required, $required === 'company.view' ? null : $unit->unit_id); }
|
||||
$row = ['user_id' => $userId, 'tenant_id' => $unit->tenant_id, 'company_id' => $unit->company_id, 'unit_id' => $unit->unit_id, 'permission' => $permission];
|
||||
if (\Illuminate\Support\Facades\DB::table('permission_grants')->insertOrIgnore($row + ['created_at' => now(), 'updated_at' => now()])) {
|
||||
\App\Modules\Identity\Services\Audit::record('b1.access.initial_grant', null, $unit->tenant_id, $unit->company_id, $unit->unit_id, $userId, ['permission' => $permission, 'operator' => 'Codex por confirmação específica do utilizador', 'reason' => 'Confirmação interna B1, sem poderes fiscais ou contabilísticos.']);
|
||||
}
|
||||
});
|
||||
echo "One specifically confirmed internal-confirmation grant applied; no other rights changed.\n";
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
$app = require __DIR__.'/../bootstrap/app.php';
|
||||
$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
\App\Shared\Testing\TestingDatabaseGuard::assertSafe();
|
||||
$fixture = json_decode(file_get_contents('C:/laragon/data/sgf-tempo/secrets/browser-fixture.json'), true, 32, JSON_THROW_ON_ERROR);
|
||||
$base = ['operation' => 'b1.confirm', 'actor' => $fixture['manager_id'], 'tenant' => $fixture['tenant'], 'company' => $fixture['company'], 'unit' => $fixture['unit'], 'version' => 1];
|
||||
app(\App\Shared\Context\CompanyContext::class)->selectFor($base['actor'], $base['tenant'], $base['company'], $base['unit']);
|
||||
$workflow = app(\App\Modules\Services\ServiceWorkflow::class);
|
||||
$save = fn ($area, $data) => $workflow->save($area, null, $base['actor'], $data, (string) Str::uuid())['id'];
|
||||
$client = $save('clients', ['name' => 'Cliente concorrência B1 sintético']);
|
||||
$service = $save('services', ['name' => 'Serviço concorrência B1 sintético']);
|
||||
$source = $save('service_drafts', ['client_id' => $client, 'lines' => [['service_id' => $service, 'quantity' => '1', 'unit_price' => '12.50']]]);
|
||||
$reports = [];
|
||||
foreach (['same_key', 'different_keys'] as $mode) {
|
||||
$draft = $save('billing_drafts', ['source_id' => $source]);
|
||||
$key = (string) Str::uuid();
|
||||
$input = $base + ['id' => $draft, 'key' => $key];
|
||||
$first = new Process([PHP_BINARY, 'scripts/a-concurrency-worker.php', '--env=testing'], base_path(), null, json_encode($input + ['hold' => true]));
|
||||
$secondInput = $input; if ($mode === 'different_keys') { $secondInput['key'] = (string) Str::uuid(); }
|
||||
$second = new Process([PHP_BINARY, 'scripts/a-concurrency-worker.php', '--env=testing'], base_path(), null, json_encode($secondInput));
|
||||
$before = DB::table('a_events')->where('type', 'b1.billing_draft.confirmed_internal')->count();
|
||||
$first->start();
|
||||
if (! $first->waitUntil(fn ($type, $buffer) => str_contains($buffer, 'LOCK_ACQUIRED'))) { throw new RuntimeException('Concurrent lock not established.'); }
|
||||
$second->start(); $first->wait(); $second->wait(); $results = [];
|
||||
foreach ([$first, $second] as $process) {
|
||||
if (! $process->isSuccessful()) { throw new RuntimeException('Synthetic worker failed.'); }
|
||||
$lines = array_values(array_filter(explode("\n", trim($process->getOutput())), fn ($line) => str_starts_with($line, '{')));
|
||||
$results[] = json_decode(end($lines), true, 32, JSON_THROW_ON_ERROR);
|
||||
}
|
||||
if ($results[0]['status'] !== 200 || $results[1]['status'] !== 200 || $results[0]['result']['id'] !== $results[1]['result']['id'] || ! $results[1]['result']['replayed'] || DB::table('a_events')->where('type', 'b1.billing_draft.confirmed_internal')->count() !== $before + 1 || DB::table('b_internal_confirmations')->where('billing_draft_id', $draft)->count() !== 1) { throw new RuntimeException('Confirmation deduplication failed.'); }
|
||||
$reports[$mode] = ['parallel_processes' => true, 'company_lock_overlap' => true, 'same_confirmation' => true, 'new_confirmations' => 1, 'new_events' => 1, 'second_replayed' => true];
|
||||
}
|
||||
file_put_contents(base_path('docs/proofs/b1-concurrency.json'), json_encode(['database' => 'sgf_tempo_test', 'results' => $reports], JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
|
||||
echo "B1 concurrent confirmations passed with identical and different keys: one confirmation/event per draft.\n";
|
||||
@@ -0,0 +1,47 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import assert from 'node:assert/strict';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
const fixture = JSON.parse(await fs.readFile('C:/laragon/data/sgf-tempo/secrets/browser-fixture.json', 'utf8'));
|
||||
const base = 'http://127.0.0.1:8001';
|
||||
function client() {
|
||||
const cookies = new Map(); let csrf = ''; let context = '';
|
||||
return {
|
||||
async request(path, method = 'GET', body, key = randomUUID(), token = context) {
|
||||
const headers = { Accept: 'application/json', 'Content-Type': 'application/json', Cookie: [...cookies].map(([k,v]) => `${k}=${v}`).join('; '), 'X-CSRF-TOKEN': csrf, 'X-SGF-Context': token, 'Idempotency-Key': key };
|
||||
const response = await fetch(base + path, { method, headers, ...(body === undefined ? {} : { body: JSON.stringify(body) }) });
|
||||
for (const cookie of response.headers.getSetCookie()) { const pair = cookie.split(';', 1)[0], split = pair.indexOf('='); cookies.set(pair.slice(0, split), pair.slice(split + 1)); }
|
||||
const data = await response.json().catch(() => ({}));
|
||||
if (data.csrf) csrf = data.csrf; if (data.context?.token) context = data.context.token;
|
||||
return { status: response.status, data };
|
||||
},
|
||||
async login(email) { await this.request('/api/session'); assert.equal((await this.request('/api/login', 'POST', { email, password: fixture.password })).status, 200); await this.select(); },
|
||||
async select(unit = fixture.unit) { const result = await this.request('/api/context', 'POST', { tenant_id: fixture.tenant, company_id: fixture.company, unit_id: unit }); assert.equal(result.status, 200); return result.data.context.token; }
|
||||
};
|
||||
}
|
||||
const manager = client(), reader = client(); await manager.login('gestor@example.test'); await reader.login('consulta@example.test');
|
||||
async function create(area, data) { const result = await manager.request('/api/a/' + area, 'POST', data); assert.equal(result.status, 201); return result.data.id; }
|
||||
const customer = await create('clients', { name: 'Cliente fotografia B1 HTTP' });
|
||||
const service = await create('services', { name: 'Serviço fotografia B1 HTTP' });
|
||||
const source = await create('service_drafts', { client_id: customer, lines: [{ service_id: service, quantity: '1.25', unit_price: '0.00000001' }] });
|
||||
const draft = await create('billing_drafts', { source_id: source });
|
||||
const endpoint = `/api/b1/billing-drafts/${draft}/confirm-internal`, record = `/api/a/billing_drafts/${draft}`;
|
||||
assert.equal((await reader.request(endpoint, 'POST', { version: 1 })).status, 403);
|
||||
assert.equal((await manager.request(endpoint, 'POST', { version: 2 })).status, 409);
|
||||
const oldToken = await manager.select(); await manager.select(null);
|
||||
assert.equal((await manager.request(endpoint, 'POST', { version: 1 }, randomUUID(), oldToken)).status, 409); await manager.select();
|
||||
const key = randomUUID(), first = await manager.request(endpoint, 'POST', { version: 1 }, key); assert.equal(first.status, 200);
|
||||
const replay = await manager.request(endpoint, 'POST', { version: 1 }, key); assert.equal(replay.status, 200); assert.equal(replay.data.id, first.data.id); assert.equal(replay.data.replayed, true);
|
||||
assert.equal((await manager.request(endpoint, 'POST', { version: 2 }, key)).status, 409);
|
||||
const before = (await manager.request(record)).data.confirmation;
|
||||
assert.equal(before.snapshot.data.currency, null); assert.equal(before.snapshot.base_amount, '0.0000000125');
|
||||
for (const pending of ['currency_missing', 'tax_rule_pending', 'accounting_rule_pending', 'accounts_pending', 'recognition_pending']) assert.ok(before.snapshot.pending[pending]);
|
||||
assert.equal((await manager.request(`/api/a/clients/${customer}`, 'PATCH', { name: 'Cliente alterado após confirmação HTTP', version: 1 })).status, 200);
|
||||
assert.equal((await manager.request(`/api/a/services/${service}`, 'PATCH', { name: 'Serviço alterado após confirmação HTTP', version: 1 })).status, 200);
|
||||
assert.deepEqual((await manager.request(record)).data.confirmation, before);
|
||||
assert.equal((await manager.request(record, 'PATCH', { version: 1, reload_source: true })).status, 409);
|
||||
assert.equal((await reader.request(record)).status, 200);
|
||||
assert.equal(before.event.type, 'b1.billing_draft.confirmed_internal'); assert.equal(before.event.state, 'captured'); assert.equal(before.snapshot.fiscal_status, 'not_issued'); assert.equal(before.snapshot.accounting_status, 'blocked_pending');
|
||||
await manager.request('/api/logout', 'POST'); await reader.request('/api/logout', 'POST');
|
||||
await fs.writeFile('docs/proofs/b1-http-results.json', JSON.stringify({ database: 'sgf_tempo_test', real_synthetic_login: true, results: ['Confirmation authorized; reader denied 403.', 'Old context and version denied 409.', 'Identical command recovers confirmation; different content conflicts 409.', 'Snapshot unchanged after client/service edits.', 'Confirmed draft edit denied 409; reader consultation 200.', 'Currency remains unknown, tax/accounts/recognition pending; captured, not issued or accounted.'] }, null, 2));
|
||||
console.log('B1 isolated HTTP proof passed; no credentials displayed.');
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
$app = require __DIR__.'/../bootstrap/app.php';
|
||||
$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
\App\Shared\Testing\TestingDatabaseGuard::assertSafe();
|
||||
$db = \Illuminate\Support\Facades\DB::class;
|
||||
$count = $db::table('b_internal_confirmations')->count();
|
||||
$histories = $db::table('a_history')->where('area', 'internal_confirmations')->count();
|
||||
$events = $db::table('a_events')->where('type', 'b1.billing_draft.confirmed_internal')->count();
|
||||
$captured = $db::table('a_events as e')->join('a_outbox as o', 'e.id', '=', 'o.event_id')->where('e.type', 'b1.billing_draft.confirmed_internal')->where('o.state', 'captured')->count();
|
||||
$missingHistory = $db::table('b_internal_confirmations as b')->whereNotExists(fn ($q) => $q->selectRaw('1')->from('a_history as h')->where('h.area', 'internal_confirmations')->whereColumn('h.record_id', 'b.id'))->count();
|
||||
if ($count < 1 || $count !== $histories || $count !== $events || $count !== $captured || $missingHistory !== 0) { throw new RuntimeException('Incomplete B1 capture.'); }
|
||||
$report = ['database' => 'sgf_tempo_test', 'confirmations' => $count, 'histories' => $histories, 'confirmation_events' => $events, 'captured_outbox' => $captured, 'missing_history' => $missingHistory,
|
||||
'fiscal_states' => $db::table('b_internal_confirmations')->selectRaw("DISTINCT snapshot->>'fiscal_status' AS state")->pluck('state')->all(),
|
||||
'accounting_states' => $db::table('b_internal_confirmations')->selectRaw("DISTINCT snapshot->>'accounting_status' AS state")->pluck('state')->all()];
|
||||
file_put_contents(base_path('docs/proofs/b1-capture.json'), json_encode($report, JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
|
||||
echo "B1 confirmation/history/event/outbox counts match; no missing historical capture.\n";
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
$app = require __DIR__.'/../bootstrap/app.php';
|
||||
$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
if (! \App\Modules\Identity\Services\LocalDatabaseGuard::allows()) { throw new RuntimeException('Own local development database required.'); }
|
||||
$counts = [];
|
||||
foreach (['users','tenants','companies','units','permission_grants','a_clients','a_services','a_service_drafts','a_billing_drafts','b_internal_confirmations'] as $table) { $counts[$table] = \Illuminate\Support\Facades\DB::table($table)->count(); }
|
||||
$grants = \Illuminate\Support\Facades\DB::table('permission_grants')->where('permission', \App\Modules\Services\InternalConfirmation::PERMISSION)->get(['user_id','tenant_id','company_id','unit_id','permission']);
|
||||
echo json_encode(['database' => 'sgf_tempo_dev', 'role' => 'sgf_dev_app', 'counts' => $counts, 'b1_grants' => $grants], JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE).PHP_EOL;
|
||||
@@ -9,7 +9,7 @@ foreach (['users', 'tenants', 'companies', 'units'] as $table) {
|
||||
fwrite(STDERR, "Test database is not empty. No data was deleted. Stop other test workflows and prepare the test database explicitly.\n"); exit(1);
|
||||
}
|
||||
}
|
||||
$fixture = \Illuminate\Support\Facades\DB::transaction(fn () => \Tests\Support\ServicesFixture::create());
|
||||
$fixture = \Illuminate\Support\Facades\DB::transaction(fn () => \Tests\Support\InternalConfirmationFixture::create());
|
||||
$safe = array_intersect_key($fixture, array_flip(['tenant', 'otherTenant', 'company', 'sibling', 'foreign', 'unit', 'hidden', 'siblingUnit', 'password']));
|
||||
$safe['manager_id'] = $fixture['manager']->id;
|
||||
$safe['reader_id'] = $fixture['reader']->id;
|
||||
|
||||
Reference in New Issue
Block a user