Regista aceitacao funcional do primeiro incremento PGC-NIRF e preserva as dependencias documentais e tecnicas existentes. A validacao do calculo fiscal continua separada e pendente. Sem credenciais, bases operacionais ou publicacao.
91 lines
8.8 KiB
PHP
91 lines
8.8 KiB
PHP
<?php
|
|
|
|
namespace App\Modules\Services;
|
|
|
|
use App\Modules\Identity\Services\Audit;
|
|
use App\Modules\Identity\Services\Permissions;
|
|
use App\Shared\Context\CompanyContext;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
final class InternalConfirmation
|
|
{
|
|
public const PERMISSION = 'billing_drafts.confirm_internal';
|
|
public const LABEL = 'Confirmado internamente — sem validade fiscal';
|
|
|
|
public function __construct(private CompanyContext $context, private Permissions $permissions, private ServiceWorkflow $workflow) {}
|
|
|
|
public function show(int $draft, int $actor): ?array
|
|
{
|
|
$scope = $this->workflow->scope('billing_drafts', $actor, 'view');
|
|
$row = DB::table('b_internal_confirmations')->where('tenant_id', $scope['tenant_id'])->where('company_id', $scope['company_id'])->where('unit_id', $scope['unit_id'])->where('billing_draft_id', $draft)->first();
|
|
if (! $row) { return null; }
|
|
$history = DB::table('a_history')->where('area', 'internal_confirmations')->where('record_id', $row->id)->value('id');
|
|
$event = DB::table('a_events as e')->join('a_outbox as o', 'e.id', '=', 'o.event_id')->where('history_id', $history)->first(['e.id','e.type','o.state']);
|
|
$snapshot = json_decode($row->snapshot, true, 64, JSON_THROW_ON_ERROR);
|
|
if (isset($snapshot['document_preparation'])) { foreach (['issuer','client','billing_drafts'] as $kind) { app(DocumentPreparation::class)->scope($kind, $actor, 'view'); } }
|
|
return ['id' => $row->id, 'internal_reference' => 'INT-B1-'.$row->id, 'label' => self::LABEL, 'snapshot' => $snapshot, 'event' => $event];
|
|
}
|
|
|
|
public function confirm(int $draft, int $actor, int $version, string $key): array
|
|
{
|
|
$scope = $this->workflow->scope('billing_drafts', $actor, 'view');
|
|
$this->permissions->require($actor, $scope['tenant_id'], $scope['company_id'], self::PERMISSION, $scope['unit_id']);
|
|
abort_unless((bool) preg_match('/^[a-zA-Z0-9-]{8,100}$/D', $key), 422, 'É necessária uma chave de repetição válida.');
|
|
$hash = hash('sha256', json_encode(['b1.confirm_internal', $draft, $scope, $version], JSON_THROW_ON_ERROR));
|
|
try { return DB::transaction(function () use ($scope, $draft, $actor, $version, $key, $hash): array {
|
|
DB::table('companies')->where('id', $scope['company_id'])->where('tenant_id', $scope['tenant_id'])->lockForUpdate()->firstOrFail();
|
|
$this->context->selectFor($actor, $scope['tenant_id'], $scope['company_id'], $scope['unit_id']);
|
|
$this->workflow->scope('billing_drafts', $actor, 'view');
|
|
$this->permissions->require($actor, $scope['tenant_id'], $scope['company_id'], self::PERMISSION, $scope['unit_id']);
|
|
$old = DB::table('a_commands')->where('tenant_id', $scope['tenant_id'])->where('company_id', $scope['company_id'])->where('command_key', $key)->first();
|
|
if ($old) {
|
|
abort_unless((int) $old->actor_id === $actor && hash_equals($old->request_hash, $hash), 409, 'Chave repetida com conteúdo ou autor diferente.');
|
|
return json_decode($old->result, true, 32, JSON_THROW_ON_ERROR) + ['replayed' => true];
|
|
}
|
|
$billing = DB::table('a_billing_drafts')->where('id', $draft)->where('tenant_id', $scope['tenant_id'])->where('company_id', $scope['company_id'])->where('unit_id', $scope['unit_id'])->lockForUpdate()->first();
|
|
abort_unless($billing, 403, 'Rascunho fora do âmbito autorizado.');
|
|
abort_unless((int) $billing->version === $version, 409, 'A versão mudou; reabra o rascunho.');
|
|
$existing = DB::table('b_internal_confirmations')->where('billing_draft_id', $draft)->first();
|
|
if ($existing) {
|
|
$history = DB::table('a_history')->where('area', 'internal_confirmations')->where('record_id', $existing->id)->value('id');
|
|
$event = DB::table('a_events')->where('history_id', $history)->value('id');
|
|
$result = ['id' => $existing->id, 'billing_draft_id' => $draft, 'version' => $version, 'internal_reference' => 'INT-B1-'.$existing->id, 'event_id' => $event];
|
|
} else {
|
|
$data = json_decode($billing->data, true, 32, JSON_THROW_ON_ERROR);
|
|
$source = DB::table('a_service_drafts')->where('id', $billing->source_id)->where('tenant_id', $scope['tenant_id'])->where('company_id', $scope['company_id'])->where('unit_id', $scope['unit_id'])->first();
|
|
abort_unless($source && (int) $source->version === (int) $data['source_version'], 409, 'A prestação mudou; reveja e actualize explicitamente o rascunho antes de confirmar.');
|
|
$client = DB::table('a_clients')->where('id', $billing->client_id)->where('tenant_id', $scope['tenant_id'])->where('company_id', $scope['company_id'])->first();
|
|
abort_unless($client && trim($client->name) !== '' && trim($billing->name) !== '', 422, 'Complete o cliente e a designação do percurso.');
|
|
abort_unless((int) $source->client_id === (int) $billing->client_id, 422, 'O cliente diverge da prestação de origem.');
|
|
$data['lines'] = DraftValues::lines($data['lines'], $scope);
|
|
foreach ($data['lines'] as $line) { abort_if($line['quantity'] === null || $line['unit_price'] === null || trim($line['service_name']) === '', 422, 'Complete quantidade, preço e serviço de todas as linhas.'); }
|
|
$pending = DraftValues::pending($data) + ['accounts_pending' => 'Contas e classificação por validar; contabilização bloqueada.', 'recognition_pending' => 'Momento e regras de reconhecimento por validar; contabilização bloqueada.'];
|
|
$catalogue = [];
|
|
foreach (array_unique(array_column($data['lines'], 'service_id')) as $id) { $service = DB::table('a_services')->where('id', $id)->first(); $catalogue[] = $this->record($service); }
|
|
$data['client_name'] = $client->name;
|
|
$snapshot = ['label' => self::LABEL, 'scope' => $scope, 'billing_draft' => $this->record($billing), 'service_draft' => $this->record($source),
|
|
'client' => $this->record($client), 'services' => $catalogue, 'data' => $data, 'base_amount' => DraftValues::base($data),
|
|
'pending' => $pending, 'actor_id' => $actor, 'confirmed_at' => now()->toIso8601String(), 'accounting_status' => 'blocked_pending', 'fiscal_status' => 'not_issued'];
|
|
$preparation = app(DocumentPreparation::class)->snapshot($draft, $actor);
|
|
if ($preparation !== null) { $snapshot['schema_version'] = 2; $snapshot['document_preparation'] = $preparation; }
|
|
$id = DB::table('b_internal_confirmations')->insertGetId($scope + ['billing_draft_id' => $draft, 'billing_version' => $version, 'version' => 1, 'actor_id' => $actor, 'snapshot' => json_encode($snapshot, JSON_THROW_ON_ERROR), 'created_at' => now()]);
|
|
$history = DB::table('a_history')->insertGetId($scope + ['area' => 'internal_confirmations', 'record_id' => $id, 'version' => 1, 'actor_id' => $actor, 'snapshot' => json_encode($snapshot, JSON_THROW_ON_ERROR), 'created_at' => now()]);
|
|
foreach ($pending as $code => $message) { DB::table('a_pending')->insert(['history_id' => $history, 'code' => $code, 'message' => $message, 'created_at' => now()]); }
|
|
$event = DB::table('a_events')->insertGetId(['history_id' => $history, 'type' => 'b1.billing_draft.confirmed_internal', 'schema_version' => $preparation !== null ? 2 : 1, 'payload' => json_encode(['confirmation_id' => $id, 'snapshot' => $snapshot], JSON_THROW_ON_ERROR), 'created_at' => now()]);
|
|
DB::table('a_outbox')->insert(['event_id' => $event, 'state' => 'captured', 'created_at' => now()]);
|
|
Audit::record('b1.billing_draft.confirmed_internal', $actor, $scope['tenant_id'], $scope['company_id'], $scope['unit_id'], details: ['area' => 'internal_confirmations', 'record_id' => $id, 'version' => 1, 'event_id' => $event]);
|
|
$result = ['id' => $id, 'billing_draft_id' => $draft, 'version' => $version, 'internal_reference' => 'INT-B1-'.$id, 'event_id' => $event];
|
|
}
|
|
DB::table('a_commands')->insert(['tenant_id' => $scope['tenant_id'], 'company_id' => $scope['company_id'], 'actor_id' => $actor, 'command_key' => $key, 'request_hash' => $hash, 'result' => json_encode($result, JSON_THROW_ON_ERROR), 'created_at' => now()]);
|
|
return $result + ['replayed' => isset($existing) && $existing !== null];
|
|
}); } catch (\Illuminate\Database\QueryException) { throw new \Symfony\Component\HttpKernel\Exception\HttpException(500, 'Não foi possível capturar a confirmação. Nenhum dado bancário foi incluído no erro.'); }
|
|
}
|
|
|
|
private function record(object $record): array
|
|
{
|
|
$data = (array) $record;
|
|
if (isset($data['data'])) { $data['data'] = json_decode($data['data'], true, 32, JSON_THROW_ON_ERROR); }
|
|
return $data;
|
|
}
|
|
}
|